├── .husky └── pre-commit ├── .mocharc.json ├── .github ├── ISSUE_TEMPLATE │ ├── other-issue.md │ ├── feature-request.yml │ └── bug-report.yml ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── tests.yml ├── test ├── loader-bootstrap.mjs ├── fixture-projects │ ├── hardhat-project-extended │ │ ├── package.json │ │ ├── contracts │ │ │ ├── interfaces │ │ │ │ └── IMock.sol │ │ │ └── Mock.sol │ │ ├── hardhat.config.ts │ │ └── node_modules │ │ │ └── @openzeppelin │ │ │ └── contracts │ │ │ ├── package.json │ │ │ ├── utils │ │ │ └── Context.sol │ │ │ └── access │ │ │ └── Ownable.sol │ ├── hardhat-project-defined-config │ │ ├── package.json │ │ ├── hardhat.config.ts │ │ └── contracts │ │ │ └── Lock.sol │ └── hardhat-project-undefined-config │ │ ├── package.json │ │ ├── hardhat.config.ts │ │ └── contracts │ │ └── Lock.sol ├── unit.test.ts ├── helpers.ts ├── extractConfigValues.test.ts └── integration.test.ts ├── src ├── constants.ts ├── type-extensions.ts ├── types.ts ├── internal │ └── tasks │ │ └── gobind │ │ ├── index.ts │ │ └── task-action.ts ├── index.ts ├── hre-integration.ts └── config.ts ├── .editorconfig ├── .prettierrc.json ├── tsconfig.json ├── .gitignore ├── LICENSE ├── eslint.config.ts ├── package.json └── README.md /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint-fix && git add -u 2 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["test/fixture-projects/**/*"], 3 | "timeout": 10000 4 | } 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other issue 3 | about: Other kind of issue 4 | --- 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be requested for review when someone opens a pull request. 2 | * @Arvolear @Hrom131 3 | -------------------------------------------------------------------------------- /test/loader-bootstrap.mjs: -------------------------------------------------------------------------------- 1 | import { register } from "node:module"; 2 | import { pathToFileURL } from "node:url"; 3 | register("ts-node/esm", pathToFileURL("./")); -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const PLUGIN_ID = "hardhat-gobind"; 2 | export const TASK_GOBIND = "gobind"; 3 | export const GOBIND_NPM_PACKAGE = "@solarity/hardhat-gobind"; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_style = space 5 | insert_final_newline = true 6 | [*.{js,ts}] 7 | indent_size = 2 8 | max_line_length = 120 9 | [*.sol] 10 | indent_size = 4 11 | max_line_length = 99 -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-extended/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-project-extended", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "author": "Distributed Lab", 6 | "readme": "README.md", 7 | "type": "module" 8 | } 9 | -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-defined-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-project-defined-config", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "author": "Distributed Lab", 6 | "readme": "README.md", 7 | "type": "module" 8 | } 9 | -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-undefined-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-project-undefined-config", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "author": "Distributed Lab", 6 | "readme": "README.md", 7 | "type": "module" 8 | } 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] Since this PR suggests a **bug fix**, the tests have been added. 2 | - [ ] Since this PR introduces a **new feature**, the update has been discussed in an Issue or with the team. 3 | - [ ] This PR is just a **minor change**, like a typo fix. 4 | 5 | --- 6 | -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-extended/contracts/interfaces/IMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | interface IMock1 { 5 | function mockFun1(address) external; 6 | } 7 | 8 | interface IMock2 { 9 | function mockFun2(uint) external; 10 | } 11 | -------------------------------------------------------------------------------- /src/type-extensions.ts: -------------------------------------------------------------------------------- 1 | import "hardhat/types/config"; 2 | 3 | import { DlGoBindConfig, DlGoBindUserConfig } from "./types.js"; 4 | 5 | declare module "hardhat/types/config" { 6 | interface HardhatUserConfig { 7 | gobind?: DlGoBindUserConfig; 8 | } 9 | 10 | interface HardhatConfig { 11 | gobind: DlGoBindConfig; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-undefined-config/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import gobind from "../../../src/index.js"; 2 | 3 | import type { HardhatUserConfig } from "hardhat/config"; 4 | 5 | const config: HardhatUserConfig = { 6 | solidity: { 7 | version: "0.8.9", 8 | npmFilesToBuild: [], 9 | }, 10 | plugins: [gobind], 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface DlGoBindConfig { 2 | outdir: string; 3 | deployable: boolean; 4 | runOnCompile: boolean; 5 | abigenVersion: abigenVersionType; 6 | verbose: boolean; 7 | onlyFiles: string[]; 8 | skipFiles: string[]; 9 | abigenPath: string; 10 | } 11 | 12 | export type DlGoBindUserConfig = Partial; 13 | 14 | type abigenVersionType = "v1" | "v2"; 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a new feature 3 | labels: ['feature'] 4 | assignees: 5 | - KyrylR 6 | body: 7 | - type: textarea 8 | id: feature-description 9 | attributes: 10 | label: "Describe the feature" 11 | description: "A description of what you would like to see in the project" 12 | validations: 13 | required: true 14 | -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-extended/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import gobind from "../../../src/index.js"; 2 | 3 | import type { HardhatUserConfig } from "hardhat/config"; 4 | 5 | const config: HardhatUserConfig = { 6 | solidity: { 7 | version: "0.8.9", 8 | npmFilesToBuild: ["@openzeppelin/contracts/access/Ownable.sol", "@openzeppelin/contracts/utils/Context.sol"], 9 | }, 10 | plugins: [gobind], 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-solidity"], 3 | "overrides": [ 4 | { 5 | "files": "*.sol", 6 | "options": { 7 | "printWidth": 99, 8 | "tabWidth": 4, 9 | "useTabs": false, 10 | "singleQuote": false, 11 | "bracketSpacing": false 12 | } 13 | }, 14 | { 15 | "files": "[*.{js,ts}]", 16 | "options": { 17 | "printWidth": 120, 18 | "tabWidth": 2 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-defined-config/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import gobind from "../../../src/index.js"; 2 | 3 | import type { HardhatUserConfig } from "hardhat/config"; 4 | 5 | const config: HardhatUserConfig = { 6 | solidity: "0.8.9", 7 | plugins: [gobind], 8 | gobind: { 9 | outdir: "go", 10 | deployable: true, 11 | runOnCompile: true, 12 | abigenVersion: "v2", 13 | verbose: true, 14 | onlyFiles: ["./contracts", "local/MyContract.sol"], 15 | skipFiles: ["@openzeppelin", "./contracts/interfaces"], 16 | }, 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "isolatedModules": true, 9 | "noEmitOnError": true, 10 | "noImplicitOverride": true, 11 | "noUncheckedSideEffectImports": true, 12 | "skipDefaultLibCheck": true, 13 | "sourceMap": true, 14 | "composite": true, 15 | "incremental": true, 16 | "typeRoots": ["./node_modules/@types"] 17 | }, 18 | "exclude": ["./dist", "./node_modules", "./test"] 19 | } 20 | -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-extended/contracts/Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | import "./interfaces/IMock.sol"; 7 | 8 | contract Mock1 is Ownable, IMock1 { 9 | address public mockAddr; 10 | 11 | constructor() Ownable() { 12 | mockAddr = address(0); 13 | } 14 | 15 | function mockFun1(address mockAddr_) public onlyOwner { 16 | mockAddr = mockAddr_; 17 | } 18 | } 19 | 20 | contract Mock2 is IMock2 { 21 | uint public mockAm; 22 | 23 | constructor() { 24 | mockAm = 0; 25 | } 26 | 27 | function mockFun2(uint mockAm_) public { 28 | mockAm = mockAm_; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .DS_Store 4 | .idea 5 | 6 | # Hardhat files 7 | cache 8 | artifacts 9 | coverage.json 10 | coverage 11 | 12 | publish/* 13 | !publish/package.json 14 | 15 | # Compilation output 16 | /build-test/ 17 | /dist 18 | 19 | # Below is Github's node gitignore template, 20 | # ignoring the node_modules part, as it'd ignore every node_modules, and we have some for testing 21 | 22 | # Logs 23 | logs 24 | *.log 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | lerna-debug.log* 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | *.pid.lock 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-defined-config/contracts/Lock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | contract Lock { 5 | uint public unlockTime; 6 | address payable public owner; 7 | 8 | event Withdrawal(uint amount, uint when); 9 | 10 | constructor(uint _unlockTime) payable { 11 | require(block.timestamp < _unlockTime, "Unlock time should be in the future"); 12 | 13 | unlockTime = _unlockTime; 14 | owner = payable(msg.sender); 15 | } 16 | 17 | function withdraw() public { 18 | require(block.timestamp >= unlockTime, "You can't withdraw yet"); 19 | require(msg.sender == owner, "You aren't the owner"); 20 | 21 | emit Withdrawal(address(this).balance, block.timestamp); 22 | 23 | owner.transfer(address(this).balance); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-undefined-config/contracts/Lock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | contract Lock { 5 | uint public unlockTime; 6 | address payable public owner; 7 | 8 | event Withdrawal(uint amount, uint when); 9 | 10 | constructor(uint _unlockTime) payable { 11 | require(block.timestamp < _unlockTime, "Unlock time should be in the future"); 12 | 13 | unlockTime = _unlockTime; 14 | owner = payable(msg.sender); 15 | } 16 | 17 | function withdraw() public { 18 | require(block.timestamp >= unlockTime, "You can't withdraw yet"); 19 | require(msg.sender == owner, "You aren't the owner"); 20 | 21 | emit Withdrawal(address(this).balance, block.timestamp); 22 | 23 | owner.transfer(address(this).balance); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ['bug'] 4 | assignees: 5 | - KyrylR 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: Thanks for taking the time to fill out this bug report! 10 | - type: input 11 | id: version 12 | attributes: 13 | label: "Project version" 14 | placeholder: "1.2.3" 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: what-happened 19 | attributes: 20 | label: What happened? 21 | description: A brief description of what happened and what you expected to happen 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: reproduction-steps 26 | attributes: 27 | label: "Minimal reproduction steps" 28 | description: "The minimal steps needed to reproduce the bug" 29 | validations: 30 | required: true 31 | -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-extended/node_modules/@openzeppelin/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openzeppelin/contracts", 3 | "description": "Secure Smart Contract library for Solidity", 4 | "version": "4.8.0", 5 | "files": [ 6 | "**/*.sol", 7 | "/build/contracts/*.json", 8 | "!/mocks/**/*" 9 | ], 10 | "scripts": { 11 | "prepare": "bash ../scripts/prepare-contracts-package.sh", 12 | "prepare-docs": "cd ..; npm run prepare-docs" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/OpenZeppelin/openzeppelin-contracts.git" 17 | }, 18 | "keywords": [ 19 | "solidity", 20 | "ethereum", 21 | "smart", 22 | "contracts", 23 | "security", 24 | "zeppelin" 25 | ], 26 | "author": "OpenZeppelin Community ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/OpenZeppelin/openzeppelin-contracts/issues" 30 | }, 31 | "homepage": "https://openzeppelin.com/contracts/" 32 | } 33 | -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-extended/node_modules/@openzeppelin/contracts/utils/Context.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Provides information about the current execution context, including the 8 | * sender of the transaction and its data. While these are generally available 9 | * via msg.sender and msg.data, they should not be accessed in such a direct 10 | * manner, since when dealing with meta-transactions the account sending and 11 | * paying for execution may not be the actual sender (as far as an application 12 | * is concerned). 13 | * 14 | * This contract is only required for intermediate, library-like contracts. 15 | */ 16 | abstract contract Context { 17 | function _msgSender() internal view virtual returns (address) { 18 | return msg.sender; 19 | } 20 | 21 | function _msgData() internal view virtual returns (bytes calldata) { 22 | return msg.data; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | - dev 10 | 11 | jobs: 12 | test: 13 | name: 'Node.js v${{ matrix.node }}' 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node: 18 | - 22 19 | steps: 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: '${{ matrix.node }}' 23 | 24 | - uses: actions/checkout@v4 25 | 26 | - name: 'Cache node_modules' 27 | uses: actions/cache@v4 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-node-v${{ matrix.node }}-${{ hashFiles('**/package.json') }} 31 | restore-keys: | 32 | ${{ runner.os }}-node-v${{ matrix.node }}- 33 | 34 | - name: Install Dependencies 35 | run: npm install 36 | 37 | - name: Run eslint 38 | run: npm run eslint-check 39 | 40 | - name: Run All Node.js Tests 41 | run: npm run test 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Solarity 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/unit.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | 3 | import { containsPath } from "../src/hre-integration.js"; 4 | 5 | describe("containsPath", function () { 6 | it("_contains should correctly process paths", function () { 7 | const pathList = ["contracts/interfaces", "contracts/A.sol", "contracts/sub/B.sol", "contracts/sub/sub2/sub3"]; 8 | const testCases = [ 9 | { src: "contracts/A.sol", exp: true }, 10 | { src: "contracts/interfaces/I.sol", exp: true }, 11 | { src: "contracts/interfaces/mock/M.sol", exp: true }, 12 | { src: "contracts/sub/sub2/sub3/C.sol", exp: true }, 13 | { src: "contracts/sub/sub2/sub3/4/5/6/7/D.sol", exp: true }, 14 | 15 | { src: "A.sol", exp: false }, 16 | { src: "sub/B.sol", exp: false }, 17 | { src: "sub/sub2/sub3/A.sol", exp: false }, 18 | { src: "sub2/sub3/A.sol", exp: false }, 19 | { src: "contracts/B.sol", exp: false }, 20 | { src: "above/contracts/A.sol", exp: false }, 21 | { src: "above/contracts/interfaces/I.sol", exp: false }, 22 | ]; 23 | 24 | testCases.forEach((c) => assert.equal(containsPath(pathList, c.src), c.exp)); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/internal/tasks/gobind/index.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentType } from "hardhat/types/arguments"; 2 | import type { NewTaskDefinition } from "hardhat/types/tasks"; 3 | 4 | import { task } from "hardhat/config"; 5 | 6 | const gobindTask: NewTaskDefinition = task(["gobind"], "Generate Go bindings for compiled contracts") 7 | .addOption({ 8 | name: "outdir", 9 | type: ArgumentType.STRING_WITHOUT_DEFAULT, 10 | description: "Output directory for generated bindings (Go package name is derived from it as well)", 11 | defaultValue: undefined, 12 | }) 13 | .addFlag({ 14 | name: "deployable", 15 | description: "Generate bindings with the bytecode in order to deploy the contracts within Go", 16 | }) 17 | .addFlag({ 18 | name: "noCompile", 19 | description: "Do not compile smart contracts before the generation", 20 | }) 21 | .addFlag({ 22 | name: "v2", 23 | description: "Use abigen version 2", 24 | }) 25 | .addOption({ 26 | name: "abigenPath", 27 | type: ArgumentType.STRING_WITHOUT_DEFAULT, 28 | description: "Path to the abigen binary", 29 | defaultValue: undefined, 30 | }) 31 | .setAction(() => import("./task-action.js")) 32 | .build(); 33 | 34 | export default gobindTask; 35 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import { fileURLToPath } from "url"; 4 | 5 | import "../src/type-extensions.js"; 6 | 7 | import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; 8 | 9 | import { createHardhatRuntimeEnvironment } from "hardhat/hre"; 10 | 11 | declare module "mocha" { 12 | interface Context { 13 | env: HardhatRuntimeEnvironment; 14 | outdir: string; 15 | _cwd?: string; 16 | } 17 | } 18 | 19 | const __filename = fileURLToPath(import.meta.url); 20 | const __dirname = path.dirname(__filename); 21 | 22 | export function useEnvironment(fixtureProjectName: string, networkName = "hardhat") { 23 | beforeEach("Loading hardhat environment", async function () { 24 | this._cwd = process.cwd(); 25 | 26 | const projectPath = path.join(__dirname, "fixture-projects", fixtureProjectName); 27 | const configPath = path.join(__dirname, "fixture-projects", fixtureProjectName, "hardhat.config.ts"); 28 | 29 | process.chdir(projectPath); 30 | process.env.HARDHAT_NETWORK = networkName; 31 | 32 | this.env = await createHardhatRuntimeEnvironment( 33 | (await import(configPath)).default, 34 | { config: configPath }, 35 | projectPath, 36 | ); 37 | this.outdir = path.join(projectPath, this.env.config.gobind.outdir); 38 | }); 39 | 40 | afterEach("Resetting hardhat", async function () { 41 | await this.env.tasks.getTask("clean").run({}); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | 3 | import { defineConfig } from "eslint/config"; 4 | import globals from "globals"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | export default defineConfig( 8 | { 9 | ignores: [ 10 | "dist/**", 11 | "node_modules/**", 12 | "generated-types/**", 13 | "artifacts/**", 14 | "coverage/**", 15 | "src/abigen/**", 16 | "bin/**", 17 | ], 18 | }, 19 | js.configs.recommended, 20 | ...tseslint.configs.recommended, 21 | { 22 | files: ["**/*.{cjs}"], 23 | languageOptions: { 24 | globals: { 25 | ...globals.node, 26 | }, 27 | }, 28 | rules: { 29 | // These are CommonJS runtime files; allow require() usage 30 | "@typescript-eslint/no-require-imports": "off", 31 | }, 32 | }, 33 | { 34 | files: ["src/**/*.ts"], 35 | languageOptions: { 36 | parser: tseslint.parser, 37 | parserOptions: { 38 | project: "./tsconfig.json", 39 | tsconfigRootDir: process.cwd(), 40 | }, 41 | globals: { 42 | ...globals.node, 43 | }, 44 | }, 45 | plugins: { 46 | "@typescript-eslint": tseslint.plugin, 47 | }, 48 | rules: { 49 | "@typescript-eslint/ban-ts-comment": "off", 50 | "@typescript-eslint/no-explicit-any": "off", 51 | "@typescript-eslint/no-unused-expressions": "off", 52 | "@typescript-eslint/no-floating-promises": "error", 53 | "@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: { attributes: false } }], 54 | }, 55 | }, 56 | ); 57 | -------------------------------------------------------------------------------- /test/extractConfigValues.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | 3 | import { useEnvironment } from "./helpers.js"; 4 | 5 | describe("hardhat-gobind configuration extension", function () { 6 | useEnvironment("hardhat-project-defined-config", "hardhat"); 7 | 8 | it("the gobind field should be present", async function () { 9 | assert.isDefined(this.env.config.gobind); 10 | }); 11 | 12 | it("the gobind object should have values from hardhat.env.config.js", async function () { 13 | const { gobind } = this.env.config; 14 | 15 | assert.equal(gobind.outdir, "go"); 16 | assert.equal(gobind.deployable, true); 17 | assert.equal(gobind.runOnCompile, true); 18 | assert.equal(gobind.abigenVersion, "v2"); 19 | assert.equal(gobind.verbose, true); 20 | assert.deepEqual(gobind.onlyFiles, ["./contracts", "local/MyContract.sol"]); 21 | assert.deepEqual(gobind.skipFiles, ["@openzeppelin", "./contracts/interfaces"]); 22 | }); 23 | }); 24 | 25 | describe("hardhat-gobind configuration defaults in an empty project", function () { 26 | useEnvironment("hardhat-project-undefined-config", "hardhat"); 27 | 28 | it("the gobind field should be present", async function () { 29 | assert.isDefined(this.env.config.gobind); 30 | }); 31 | 32 | it("fields of the gobind object should be set to default", async function () { 33 | const { gobind } = this.env.config; 34 | 35 | assert.equal(gobind.outdir, "./generated-types/bindings"); 36 | assert.equal(gobind.deployable, false); 37 | assert.equal(gobind.runOnCompile, false); 38 | assert.equal(gobind.verbose, false); 39 | assert.equal(gobind.abigenVersion, "v1"); 40 | assert.deepEqual(gobind.onlyFiles, []); 41 | assert.deepEqual(gobind.skipFiles, []); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "./type-extensions.js"; 2 | 3 | import type { HardhatPlugin } from "hardhat/types/plugins"; 4 | import { overrideTask } from "hardhat/config"; 5 | import { HardhatPluginError } from "hardhat/plugins"; 6 | 7 | import { Generator } from "abigenjs/generator"; 8 | 9 | import gobindTask from "./internal/tasks/gobind/index.js"; 10 | 11 | import { GOBIND_NPM_PACKAGE, PLUGIN_ID } from "./constants.js"; 12 | 13 | export { getArtifacts } from "./hre-integration.js"; 14 | 15 | const hardhatPlugin: HardhatPlugin = { 16 | id: PLUGIN_ID, 17 | hookHandlers: { 18 | config: () => import("./config.js"), 19 | }, 20 | tasks: [ 21 | gobindTask, 22 | overrideTask("compile") 23 | .setAction(async () => ({ 24 | default: async (args, hre, runSuper) => { 25 | const result = await runSuper(args); 26 | 27 | if (hre.config.gobind.runOnCompile) { 28 | const abigenPath = hre.config.gobind.abigenPath || undefined; 29 | await hre.tasks.getTask("gobind").run({ noCompile: true, abigenPath }); 30 | } 31 | 32 | return result; 33 | }, 34 | })) 35 | .build(), 36 | overrideTask("clean") 37 | .setAction(async () => ({ 38 | default: async (args, hre, runSuper) => { 39 | if (!args.global) 40 | try { 41 | const abigenPath = hre.config.gobind.abigenPath || undefined; 42 | const abigenVersion = hre.config.gobind.abigenVersion || "v2"; 43 | const outDir = hre.config.gobind.outdir || "./generated-types/bindings"; 44 | 45 | await new Generator(outDir, abigenVersion, abigenPath).clean(); 46 | } catch (e: any) { 47 | throw new HardhatPluginError( 48 | PLUGIN_ID, 49 | `Failed to remove gobind artifacts at ${hre.config.gobind.outdir}`, 50 | e, 51 | ); 52 | } 53 | 54 | await runSuper(args); 55 | }, 56 | })) 57 | .build(), 58 | ], 59 | npmPackage: GOBIND_NPM_PACKAGE, 60 | } satisfies HardhatPlugin; 61 | 62 | export default hardhatPlugin; 63 | -------------------------------------------------------------------------------- /src/internal/tasks/gobind/task-action.ts: -------------------------------------------------------------------------------- 1 | import type { NewTaskActionFunction } from "hardhat/types/tasks"; 2 | 3 | import { HardhatPluginError } from "@nomicfoundation/hardhat-errors"; 4 | 5 | import { Generator } from "abigenjs/generator"; 6 | 7 | import { PLUGIN_ID } from "../../../constants.js"; 8 | 9 | import { getArtifacts, tryFindAbigenJS } from "../../../hre-integration.js"; 10 | 11 | export interface DlGoBindArgs { 12 | outdir?: string; 13 | deployable?: boolean; 14 | noCompile?: boolean; 15 | v2?: boolean; 16 | abigenPath?: string; 17 | } 18 | 19 | const gobindAction: NewTaskActionFunction = async ( 20 | { outdir, deployable, noCompile, v2, abigenPath }, 21 | hre, 22 | ) => { 23 | if (outdir !== undefined) { 24 | hre.config.gobind.outdir = outdir; 25 | } 26 | 27 | hre.config.gobind.deployable = deployable || hre.config.gobind.deployable; 28 | 29 | if (v2) { 30 | hre.config.gobind.abigenVersion = "v2"; 31 | } 32 | if (abigenPath !== undefined) { 33 | hre.config.gobind.abigenPath = abigenPath; 34 | } 35 | 36 | if (!noCompile) { 37 | await hre.tasks.getTask("compile").run({ 38 | quiet: true, 39 | defaultBuildProfile: "production", 40 | }); 41 | } 42 | 43 | try { 44 | const onlyFiles = hre.config.gobind.onlyFiles || []; 45 | const skipFiles = hre.config.gobind.skipFiles || []; 46 | 47 | const artifacts = await getArtifacts(hre, onlyFiles, skipFiles); 48 | 49 | const outDir = hre.config.gobind.outdir || "./generated-types/bindings"; 50 | 51 | const abigenVersion = hre.config.gobind.abigenVersion || "v2"; 52 | const effectiveAbigenPath = tryFindAbigenJS( 53 | abigenPath && abigenPath !== "" ? abigenPath : hre.config.gobind.abigenPath, 54 | ); 55 | 56 | const deployable = hre.config.gobind.deployable || false; 57 | const verbose = hre.config.gobind.verbose || false; 58 | 59 | await new Generator(outDir, abigenVersion, effectiveAbigenPath).generate(artifacts, deployable, verbose); 60 | 61 | console.log(`\nGenerated bindings for ${artifacts.length} contracts`); 62 | } catch (e: any) { 63 | throw new HardhatPluginError(PLUGIN_ID, `Failed to generate bindings: ${e.message}`, e); 64 | } 65 | }; 66 | 67 | export default gobindAction; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@solarity/hardhat-gobind", 3 | "version": "2.0.2", 4 | "description": "Generation of smart contract bindings for Golang", 5 | "main": "dist/src/index.js", 6 | "types": "dist/src/index.d.ts", 7 | "exports": { 8 | ".": "./dist/src/index.js" 9 | }, 10 | "type": "module", 11 | "files": [ 12 | "bin/", 13 | "dist/src/", 14 | "src/", 15 | "LICENSE", 16 | "README.md" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/dl-solarity/hardhat-gobind.git" 21 | }, 22 | "keywords": [ 23 | "ethereum", 24 | "solidity", 25 | "smart-contracts", 26 | "go-bindings", 27 | "hardhat", 28 | "hardhat-plugin", 29 | "distributedlab", 30 | "solarity" 31 | ], 32 | "author": "Distributed Lab", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/dl-solarity/hardhat-gobind/issues" 36 | }, 37 | "homepage": "https://github.com/dl-solarity/hardhat-gobind#readme", 38 | "scripts": { 39 | "prepare": "husky", 40 | "build": "tsc --build .", 41 | "test": "node --enable-source-maps --import ./test/loader-bootstrap.mjs --no-deprecation ./node_modules/mocha/bin/mocha.js --config .mocharc.json --recursive \"test/**/*.test.ts\" --timeout 100000 --exit", 42 | "lint-fix": "prettier --write \"./**/*.{ts,js,sol,cjs,cts}\"", 43 | "publish-to-npm": "npm run build && npm run lint-fix && npm publish ./ --access public", 44 | "eslint-check": "eslint ." 45 | }, 46 | "dependencies": { 47 | "@nomicfoundation/hardhat-utils": "3.0.0", 48 | "@nomicfoundation/hardhat-zod-utils": "3.0.0", 49 | "abigenjs": "0.2.0", 50 | "lodash": "4.17.21", 51 | "zod": "3.25.76" 52 | }, 53 | "peerDependencies": { 54 | "hardhat": "^3.0.0" 55 | }, 56 | "devDependencies": { 57 | "@tsconfig/node22": "^22.0.2", 58 | "@types/chai": "^4.3.20", 59 | "@types/mocha": "^10.0.10", 60 | "@typescript-eslint/eslint-plugin": "^8.44.1", 61 | "chai": "^4.5.0", 62 | "eslint": "^9.36.0", 63 | "eslint-plugin-promise": "^7.2.1", 64 | "hardhat": "^3.0.6", 65 | "husky": "^9.1.7", 66 | "jiti": "^2.6.0", 67 | "mocha": "^11.2.2", 68 | "prettier": "^3.5.3", 69 | "prettier-plugin-solidity": "^2.0.0", 70 | "ts-node": "^10.9.2", 71 | "typescript": "^5.8.3", 72 | "typescript-eslint": "^8.44.1" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/hre-integration.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | 3 | import { Artifact } from "abigenjs/generator"; 4 | 5 | import { HardhatRuntimeEnvironment } from "hardhat/types/hre"; 6 | 7 | import path from "path"; 8 | 9 | import { GOBIND_NPM_PACKAGE } from "./constants.js"; 10 | 11 | export async function getArtifacts( 12 | hre: HardhatRuntimeEnvironment, 13 | onlyFiles: string[], 14 | skipFiles: string[], 15 | ): Promise { 16 | const artifacts = hre.artifacts; 17 | const names = Array.from(await artifacts.getAllFullyQualifiedNames()); 18 | 19 | onlyFiles = onlyFiles.map((p) => toUnixPath(path.normalize(p))); 20 | skipFiles = skipFiles.map((p) => toUnixPath(path.normalize(p))); 21 | 22 | const namesWithSources = await Promise.all( 23 | names.map(async (n) => { 24 | const artifact = await artifacts.readArtifact(n); 25 | return { 26 | name: n, 27 | contractName: artifact.contractName, 28 | sourceName: artifact.sourceName, 29 | abi: artifact.abi, 30 | bytecode: artifact.bytecode, 31 | }; 32 | }), 33 | ); 34 | 35 | const filtered = namesWithSources.filter(({ sourceName }) => { 36 | return (onlyFiles.length === 0 || containsPath(onlyFiles, sourceName)) && !containsPath(skipFiles, sourceName); 37 | }); 38 | 39 | _verboseLog(hre, `${names.length} compiled contracts found, skipping ${names.length - filtered.length} of them\n`); 40 | 41 | return filtered; 42 | } 43 | 44 | export function tryFindAbigenJS(currentPath: string): string { 45 | if (existsSync(currentPath)) { 46 | return currentPath; 47 | } 48 | 49 | // Here we expect that the currentPath to the AbigenJS is as follows: 50 | // ./node_modules/abigenjs/bin/abigen.wasm 51 | // if we did not find the AbigenJS there, let's try to find it in the node_modules of the plugin 52 | const candidates = [ 53 | path.join("node_modules", GOBIND_NPM_PACKAGE, "node_modules", "abigenjs", "bin", "abigen.wasm"), 54 | path.join("node_modules", "abigenjs", "bin", "abigen.wasm"), 55 | ]; 56 | 57 | for (const candidate of candidates) { 58 | if (existsSync(candidate)) { 59 | return candidate; 60 | } 61 | } 62 | 63 | throw new Error("AbigenJS not found"); 64 | } 65 | 66 | function toUnixPath(userPath: string) { 67 | return userPath.split(path.sep).join(path.posix.sep); 68 | } 69 | 70 | function _verboseLog(hre: HardhatRuntimeEnvironment, msg: string) { 71 | if (hre && hre.config && hre.config.gobind && hre.config.gobind.verbose) { 72 | console.log(msg); 73 | } 74 | } 75 | 76 | export function containsPath(pathList: string[], source: string): boolean { 77 | const isSubPath = (parent: string, child: string) => { 78 | const parentTokens = parent.split(path.posix.sep).filter((i) => i.length); 79 | const childTokens = child.split(path.posix.sep).filter((i) => i.length); 80 | return parentTokens.every((t, i) => childTokens[i] === t); 81 | }; 82 | 83 | return pathList === undefined ? false : pathList.some((p) => isSubPath(p, source)); 84 | } 85 | -------------------------------------------------------------------------------- /test/fixture-projects/hardhat-project-extended/node_modules/@openzeppelin/contracts/access/Ownable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "../utils/Context.sol"; 7 | 8 | /** 9 | * @dev Contract module which provides a basic access control mechanism, where 10 | * there is an account (an owner) that can be granted exclusive access to 11 | * specific functions. 12 | * 13 | * By default, the owner account will be the one that deploys the contract. This 14 | * can later be changed with {transferOwnership}. 15 | * 16 | * This module is used through inheritance. It will make available the modifier 17 | * `onlyOwner`, which can be applied to your functions to restrict their use to 18 | * the owner. 19 | */ 20 | abstract contract Ownable is Context { 21 | address private _owner; 22 | 23 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 24 | 25 | /** 26 | * @dev Initializes the contract setting the deployer as the initial owner. 27 | */ 28 | constructor() { 29 | _transferOwnership(_msgSender()); 30 | } 31 | 32 | /** 33 | * @dev Throws if called by any account other than the owner. 34 | */ 35 | modifier onlyOwner() { 36 | _checkOwner(); 37 | _; 38 | } 39 | 40 | /** 41 | * @dev Returns the address of the current owner. 42 | */ 43 | function owner() public view virtual returns (address) { 44 | return _owner; 45 | } 46 | 47 | /** 48 | * @dev Throws if the sender is not the owner. 49 | */ 50 | function _checkOwner() internal view virtual { 51 | require(owner() == _msgSender(), "Ownable: caller is not the owner"); 52 | } 53 | 54 | /** 55 | * @dev Leaves the contract without owner. It will not be possible to call 56 | * `onlyOwner` functions anymore. Can only be called by the current owner. 57 | * 58 | * NOTE: Renouncing ownership will leave the contract without an owner, 59 | * thereby removing any functionality that is only available to the owner. 60 | */ 61 | function renounceOwnership() public virtual onlyOwner { 62 | _transferOwnership(address(0)); 63 | } 64 | 65 | /** 66 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 67 | * Can only be called by the current owner. 68 | */ 69 | function transferOwnership(address newOwner) public virtual onlyOwner { 70 | require(newOwner != address(0), "Ownable: new owner is the zero address"); 71 | _transferOwnership(newOwner); 72 | } 73 | 74 | /** 75 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 76 | * Internal function without access restriction. 77 | */ 78 | function _transferOwnership(address newOwner) internal virtual { 79 | address oldOwner = _owner; 80 | _owner = newOwner; 81 | emit OwnershipTransferred(oldOwner, newOwner); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute } from "path"; 2 | 3 | import type { ConfigurationVariableResolver, HardhatConfig, HardhatUserConfig } from "hardhat/types/config"; 4 | import type { ConfigHooks, HardhatUserConfigValidationError } from "hardhat/types/hooks"; 5 | 6 | import { validateUserConfigZodType } from "@nomicfoundation/hardhat-zod-utils"; 7 | 8 | import { z } from "zod"; 9 | 10 | import type { DlGoBindConfig, DlGoBindUserConfig } from "./types.js"; 11 | 12 | export default async (): Promise> => ({ 13 | validateUserConfig, 14 | resolveUserConfig, 15 | }); 16 | 17 | const userConfigType = z.object({ 18 | gobind: z 19 | .object({ 20 | outdir: z.string().optional(), 21 | deployable: z.boolean().optional(), 22 | runOnCompile: z.boolean().optional(), 23 | abigenVersion: z.enum(["v1", "v2"]).optional(), 24 | abigenPath: z.string().optional(), 25 | verbose: z.boolean().optional(), 26 | onlyFiles: z.array(z.string().refine((p) => !isAbsolute(p), "Expected a relative path")).optional(), 27 | skipFiles: z.array(z.string().refine((p) => !isAbsolute(p), "Expected a relative path")).optional(), 28 | }) 29 | .optional(), 30 | }); 31 | 32 | export async function validateUserConfig(userConfig: HardhatUserConfig): Promise { 33 | return validateUserConfigZodType(userConfig, userConfigType); 34 | } 35 | 36 | export async function resolveUserConfig( 37 | userConfig: HardhatUserConfig, 38 | resolveConfigurationVariable: ConfigurationVariableResolver, 39 | next: ( 40 | nextUserConfig: HardhatUserConfig, 41 | nextResolveConfigurationVariable: ConfigurationVariableResolver, 42 | ) => Promise, 43 | ): Promise { 44 | const resolvedConfig = await next(userConfig, resolveConfigurationVariable); 45 | 46 | const gobind = await resolveGobindConfig(userConfig.gobind, resolveConfigurationVariable); 47 | 48 | return { 49 | ...resolvedConfig, 50 | gobind, 51 | }; 52 | } 53 | 54 | async function resolveGobindConfig( 55 | gobindConfig: DlGoBindUserConfig | undefined, 56 | resolveConfigurationVariable: ConfigurationVariableResolver, 57 | ): Promise { 58 | const defaultConfig: DlGoBindConfig = { 59 | outdir: "./generated-types/bindings", 60 | deployable: false, 61 | runOnCompile: false, 62 | abigenVersion: "v1", 63 | verbose: false, 64 | onlyFiles: [], 65 | skipFiles: [], 66 | abigenPath: "./node_modules/abigenjs/bin/abigen.wasm", 67 | }; 68 | 69 | if (gobindConfig === undefined) { 70 | return defaultConfig; 71 | } 72 | 73 | const resolved: DlGoBindConfig = { ...defaultConfig }; 74 | 75 | if (typeof gobindConfig.outdir === "string") { 76 | resolved.outdir = await resolveConfigurationVariable(gobindConfig.outdir).get(); 77 | } 78 | 79 | if (typeof gobindConfig.deployable === "boolean") { 80 | resolved.deployable = gobindConfig.deployable; 81 | } 82 | 83 | if (typeof gobindConfig.runOnCompile === "boolean") { 84 | resolved.runOnCompile = gobindConfig.runOnCompile; 85 | } 86 | 87 | if (typeof gobindConfig.abigenVersion === "string") { 88 | resolved.abigenVersion = gobindConfig.abigenVersion as DlGoBindConfig["abigenVersion"]; 89 | } 90 | 91 | if (typeof gobindConfig.verbose === "boolean") { 92 | resolved.verbose = gobindConfig.verbose; 93 | } 94 | 95 | if (typeof gobindConfig.abigenPath === "string") { 96 | resolved.abigenPath = await resolveConfigurationVariable(gobindConfig.abigenPath).get(); 97 | } 98 | 99 | if (Array.isArray(gobindConfig.onlyFiles)) { 100 | resolved.onlyFiles = await Promise.all( 101 | gobindConfig.onlyFiles.map((p: string) => resolveConfigurationVariable(p).get()), 102 | ); 103 | } 104 | 105 | if (Array.isArray(gobindConfig.skipFiles)) { 106 | resolved.skipFiles = await Promise.all( 107 | gobindConfig.skipFiles.map((p: string) => resolveConfigurationVariable(p).get()), 108 | ); 109 | } 110 | 111 | return resolved; 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/@solarity/hardhat-gobind.svg)](https://www.npmjs.com/package/@solarity/hardhat-gobind) [![hardhat](https://hardhat.org/buidler-plugin-badge.svg?1)](https://hardhat.org) 2 | 3 | # Hardhat GoBind 4 | 5 | [Hardhat](https://hardhat.org) plugin to simplify generation of smart contract bindings for Golang. 6 | 7 | ## What 8 | 9 | This plugin helps you generate `.go` files with bindings to call smart contracts from Go code. To produce them, the plugin uses `abigenjs` -- a `wasm` binary that is built from [go-ethereum/cmd/abigen](https://github.com/ethereum/go-ethereum/tree/master/cmd/abigen) Go module. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install --save-dev @solarity/hardhat-gobind 15 | ``` 16 | 17 | Add the plugin to your `hardhat.config.ts`: 18 | 19 | ```ts 20 | import hardhatGobind from "@solarity/hardhat-gobind"; 21 | 22 | const config: HardhatUserConfig = { 23 | plugins: [hardhatGobind], 24 | // ... your config 25 | }; 26 | ``` 27 | 28 | ## Tasks 29 | 30 | The bindings generation can be run either with built-in `compile` or the provided `gobind` task. 31 | 32 | To view the available options, run these help commands: 33 | 34 | ```bash 35 | npx hardhat help compile 36 | npx hardhat help gobind 37 | ``` 38 | 39 | ## Environment extensions 40 | 41 | This plugin does not extend the environment. 42 | 43 | ## Usage 44 | 45 | The plugin works out of the box: `npx hardhat gobind` will compile and generate bindings for all the contracts used in the project into the default folder. 46 | 47 | To generate the most recent bindings, clean old artifacts with `npx hardhat clean` beforehand. 48 | 49 | ### Configuration 50 | 51 | The default configuration looks as follows. You may customize all fields in your *hardhat config* file. 52 | 53 | ```js 54 | module.exports = { 55 | gobind: { 56 | outdir: "./generated-types/bindings", 57 | deployable: false, 58 | runOnCompile: false, 59 | abigenVersion: "v1", 60 | verbose: false, 61 | onlyFiles: [], 62 | skipFiles: [], 63 | }, 64 | } 65 | ``` 66 | 67 | - `outdir` : The directory where the generated bindings will be placed 68 | - `deployable` : Generates the bindings with the bytecode (makes them deployable within Go) 69 | - `runOnCompile` : Whether to run bindings generation on compilation 70 | - `abigenVersion`: The version of abigen to use (v1 or v2) 71 | - `verbose`: Detailed logging on generation (e.g. count of included and skipped contracts, source paths, names) 72 | - `onlyFiles`: If specified, bindings will be generated **only for matching** sources, other will be ignored 73 | - `skipFiles`: Bindings will not be generated for **any matching** sources, also if those match `onlyFiles` 74 | 75 | Some of the parameters are only available in CLI and they override the ones defined in your *hardhat config* (e.g. `--deployable` will generate deploy method regardless of `config.gobind.deployable` value). Run `npx hardhat help gobind` to get available options. 76 | 77 | ### Including/excluding files 78 | 79 | - Path stands for relative path from project root to either `.sol` file or directory. 80 | - If path is a directory, all its files and sub-directories are considered matching. 81 | - If source is a node module, `node_modules` must not be present in the path. 82 | 83 | ## How it works 84 | 85 | The plugin runs `compile` task (if `--no-compile` is not given), gets the artifacts from *Hardhat Runtime Environment* (HRE), filters them according to `onlyFiles` and `skipFiles`, and performs the following actions: 86 | 87 | 1. Writes contract's ABI (and bytecode, if necessary) into a temporary file `ContractName.abi` (and `ContractName.bin` with bytecode). 88 | 2. Derives destination folder from the original file location: if the file is in `./contracts`, the folder will be `./your_outdir/contracts`. 89 | 3. Derives Go package name from the parent folder: for `./your_outdir/nested/My_Contracts` it will be `mycontracts`. 90 | 4. Depending on the presence of `--v2` flag, it calls `abigen` or `abigen v2` via WebAssembly: `abigen `(`--v2`)` --abi /path/to/file.abi --pkg packagename --type ContractName --out /path/to/your_project/your_outdir` (and `--bin /path/to/file.bin`, if necessary). 91 | 5. Removes temporary files. 92 | 93 | 94 | Bindings are generated for contracts, not files. Having 3 contracts in a single file, you get 3 `.go` files named after contracts. If you skip the file, all 3 contracts are ignored. 95 | 96 | Consider we have Hardhat project with the following structure (excluding some files for brevity): 97 | 98 | ``` 99 | . 100 | ├── contracts 101 | │ ├── Example.sol 102 | │ ├── Sample.sol 103 | │ └── interfaces 104 | │ ├── IExample.sol 105 | │ └── ISample.sol 106 | ├── hardhat.config.ts 107 | └── node_modules 108 | └── @openzeppelin 109 | └── contracts 110 | └── access 111 | ├── Ownable 112 | │ └── Ownable.sol 113 | └── Ownable2Step 114 | └── Ownable2Step.sol 115 | ``` 116 | 117 | `npx hardhat gobind` with the default configuration will create the following directory structure. Note there are no `node_modules` parent directory for `@openzeppelin` dependency. 118 | 119 | ``` 120 | generated-types 121 | └── bindings 122 | ├── @openzeppelin 123 | │ └── contracts 124 | │ └── access 125 | │ ├── ownable 126 | │ │ └── Ownable.go 127 | │ └── ownable2step 128 | │ └── Ownable2Step.go 129 | │ 130 | └── contracts 131 | ├── example 132 | │ └── Example.go 133 | ├── sample 134 | │ └── Sample.go 135 | └── interfaces 136 | ├── iexample 137 | │ └── IExample.go 138 | └── isample 139 | └── ISample.go 140 | ``` 141 | 142 | In most cases, you want bindings only for your `contracts/` directory, excluding `contracts/interfaces` and all the dependencies from `node_modules`. 143 | 144 | It is achieved by adding the following into your *hardhat config*: 145 | 146 | ```js 147 | onlyFiles: ["contracts"], 148 | skipFiles: ["contracts/interfaces", "@openzeppelin", "@solarity"], 149 | ``` 150 | 151 | ## Known limitations 152 | 153 | - `--verbose` is not available in CLI because of names clash with Hardhat. [Learn more](https://hardhat.org/hardhat-runner/docs/errors#HH202). 154 | - `node_modules` must not be present in the path. 155 | - All environment variables are omitted when the abigen.wasm is called 156 | -------------------------------------------------------------------------------- /test/integration.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import { resolve } from "path"; 3 | import { existsSync } from "fs"; 4 | 5 | import { useEnvironment } from "./helpers.js"; 6 | 7 | describe("GoBind x Hardhat integration", function () { 8 | let abigenPath: { v2?: boolean; abigenPath: string }; 9 | 10 | const assertExists = (path: string) => assert.isTrue(existsSync(path), `path ${path} should exist`); 11 | const assertNotExists = (path: string) => assert.isFalse(existsSync(path), `path ${path} should not exist`); 12 | const assertContractsGenerated = (outdir: string) => { 13 | assertExists(outdir); 14 | assertExists(`${outdir}/contracts`); 15 | assertExists(`${outdir}/contracts/lock/Lock.go`); 16 | }; 17 | 18 | const setupToTest = ["abigen version 1", "abigen version 2"]; 19 | 20 | setupToTest.forEach((setupName) => { 21 | describe(setupName, () => { 22 | beforeEach(async () => { 23 | if (setupName == "abigen version 1") { 24 | abigenPath = { abigenPath: "../../../node_modules/abigenjs/bin/abigen.wasm" }; 25 | } else { 26 | abigenPath = { v2: true, abigenPath: "../../../node_modules/abigenjs/bin/abigen.wasm" }; 27 | } 28 | }); 29 | 30 | describe("Main logic with default config", function () { 31 | useEnvironment("hardhat-project-undefined-config"); 32 | 33 | it("does not generate bindings with --no-compile and no artifacts", async function () { 34 | await this.env.tasks.getTask("gobind").run({ noCompile: true, ...abigenPath }); 35 | 36 | assertNotExists(this.outdir); 37 | }); 38 | 39 | it("compiles and generates bindings", async function () { 40 | assertNotExists(this.outdir); 41 | 42 | await this.env.tasks.getTask("gobind").run(abigenPath); 43 | 44 | assertContractsGenerated(this.outdir); 45 | }); 46 | 47 | it("cleans up generated bindings", async function () { 48 | this.env.config.gobind.runOnCompile = true; 49 | this.env.config.gobind.abigenPath = abigenPath.abigenPath; 50 | await this.env.tasks.getTask("compile").run({ quiet: true, defaultBuildProfile: "production" }); 51 | 52 | assertExists(this.outdir); 53 | 54 | await this.env.tasks.getTask("clean").run({}); 55 | 56 | assertNotExists(this.outdir); 57 | }); 58 | 59 | it("generates bindings on compilation when runOnCompile is enabled", async function () { 60 | assertNotExists(this.outdir); 61 | 62 | this.env.config.gobind.runOnCompile = true; 63 | this.env.config.gobind.abigenPath = abigenPath.abigenPath; 64 | await this.env.tasks.getTask("compile").run({ quiet: true, defaultBuildProfile: "production" }); 65 | 66 | assertContractsGenerated(this.outdir); 67 | }); 68 | }); 69 | 70 | describe("onlyFiles, skipFiles parameters", function () { 71 | useEnvironment("hardhat-project-extended"); 72 | 73 | beforeEach(async function () { 74 | assertNotExists(this.outdir); 75 | }); 76 | 77 | const contractPaths = ["mock1/Mock1.go", "mock2/Mock2.go"].map((p) => "contracts/" + p); 78 | const interfacePaths = ["imock1/IMock1.go", "imock2/IMock2.go"].map((p) => "contracts/interfaces/" + p); 79 | const dependecyPaths = ["access/ownable/Ownable.go", "utils/context/Context.go"].map( 80 | (p) => "@openzeppelin/contracts/" + p, 81 | ); 82 | const allPaths = [...contractPaths, ...interfacePaths, ...dependecyPaths]; 83 | 84 | const testCases = [ 85 | { only: ["@openzeppelin/contracts/access/Ownable.sol"], skip: [] }, 86 | { only: ["contracts"], skip: [] }, 87 | { only: [], skip: ["contracts/Mock.sol"] }, 88 | { only: [], skip: ["@openzeppelin"] }, 89 | { only: ["contracts"], skip: ["@openzeppelin", "contracts/interfaces"] }, 90 | { only: ["any-folder/non-existent-file.txt"], skip: ["any-folder/another-bad-file.go"] }, 91 | ]; 92 | 93 | const caseToString = (index: number): string => { 94 | if (index >= testCases.length) { 95 | throw Error("getTestCase: index argument out of range"); 96 | } 97 | const tc = testCases[index]; 98 | return `onlyFiles=[${tc.only}] and skipFiles=[${tc.skip}]`; 99 | }; 100 | 101 | const assertGenerated = (outdir: string, paths: string[]) => { 102 | assertExists(outdir); 103 | 104 | paths.forEach((p) => assertExists(`${outdir}/${p}`)); 105 | }; 106 | 107 | const assertNotGenerated = (outdir: string, paths: string[]) => { 108 | const wrongPaths = [ 109 | "node_modules", 110 | "contracts/Mock.go", 111 | "contracts/interfaces/IMock.go", 112 | "any-folder/non-existent-file.txt", 113 | "any-folder/another-bad-file.go", 114 | ]; 115 | 116 | paths.concat(wrongPaths).forEach((p) => assertNotExists(`${outdir}/${p}`)); 117 | }; 118 | 119 | it("correctly generates bindings for all contracts", async function () { 120 | await this.env.tasks.getTask("gobind").run(abigenPath); 121 | 122 | assertGenerated(this.outdir, allPaths); 123 | assertNotGenerated(this.outdir, []); 124 | }); 125 | 126 | it(`for ${caseToString(0)} generates only Ownable.go`, async function () { 127 | const ownablePath = "@openzeppelin/contracts/access/ownable/Ownable.go"; 128 | this.env.config.gobind.onlyFiles = testCases[0].only; 129 | 130 | await this.env.tasks.getTask("gobind").run(abigenPath); 131 | 132 | assertGenerated(this.outdir, [ownablePath]); 133 | assertNotGenerated( 134 | this.outdir, 135 | allPaths.filter((p) => p != ownablePath), 136 | ); 137 | }); 138 | 139 | it(`for ${caseToString(1)} generates only contracts and interfaces`, async function () { 140 | this.env.config.gobind.onlyFiles = testCases[1].only; 141 | await this.env.tasks.getTask("gobind").run(abigenPath); 142 | 143 | assertGenerated(this.outdir, [...contractPaths, ...interfacePaths]); 144 | assertNotGenerated(this.outdir, dependecyPaths); 145 | }); 146 | 147 | it(`for ${caseToString(2)} generates for all except Mock1.go and Mock2.go`, async function () { 148 | this.env.config.gobind.skipFiles = testCases[2].skip; 149 | await this.env.tasks.getTask("gobind").run(abigenPath); 150 | 151 | assertGenerated(this.outdir, [...interfacePaths, ...dependecyPaths]); 152 | assertNotGenerated(this.outdir, contractPaths); 153 | }); 154 | 155 | it(`for ${caseToString(3)} generates for all except dependencies`, async function () { 156 | this.env.config.gobind.skipFiles = testCases[3].skip; 157 | await this.env.tasks.getTask("gobind").run(abigenPath); 158 | 159 | assertGenerated(this.outdir, [...contractPaths, ...interfacePaths]); 160 | assertNotGenerated(this.outdir, dependecyPaths); 161 | }); 162 | 163 | it(`for ${caseToString(4)} generates contracts, skips dependencies and interfaces`, async function () { 164 | this.env.config.gobind.onlyFiles = testCases[4].only; 165 | this.env.config.gobind.skipFiles = testCases[4].skip; 166 | await this.env.tasks.getTask("gobind").run(abigenPath); 167 | 168 | assertGenerated(this.outdir, [...contractPaths]); 169 | assertNotGenerated(this.outdir, [...interfacePaths, ...dependecyPaths]); 170 | }); 171 | 172 | it(`for ${caseToString(5)} generates nothing`, async function () { 173 | this.env.config.gobind.onlyFiles = testCases[5].only; 174 | this.env.config.gobind.skipFiles = testCases[5].skip; 175 | 176 | await this.env.tasks.getTask("gobind").run(abigenPath); 177 | 178 | assertNotGenerated(this.outdir, allPaths); 179 | }); 180 | }); 181 | 182 | describe("Misc config fields and flag tests", function () { 183 | useEnvironment("hardhat-project-defined-config"); 184 | 185 | it("generates bindings into the custom outdir", async function () { 186 | const outdir = resolve("go"); 187 | 188 | assertNotExists(outdir); 189 | 190 | this.env.config.gobind.runOnCompile = false; 191 | await this.env.tasks.getTask("gobind").run({ outdir, ...abigenPath }); 192 | 193 | assertContractsGenerated(outdir); 194 | }); 195 | 196 | it("overrides output directory with --outdir", async function () { 197 | const relOutdir = "generated-types/flag-outdir"; 198 | const outdir = resolve(relOutdir); 199 | 200 | assertNotExists(outdir); 201 | 202 | this.env.config.gobind.runOnCompile = false; 203 | await this.env.tasks.getTask("gobind").run({ outdir: relOutdir, ...abigenPath }); 204 | 205 | assertContractsGenerated(outdir); 206 | }); 207 | }); 208 | }); 209 | }); 210 | }); 211 | --------------------------------------------------------------------------------