Skip to content

Commit 26dc309

Browse files
committed
feat: support hardhat
1 parent 67b9fd2 commit 26dc309

File tree

6 files changed

+211
-35
lines changed

6 files changed

+211
-35
lines changed

README.md

+69-6
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,19 @@ A CLI tool which allows you to convert an ABI json file into fully loaded interf
3434
- Web3 1.x and 2.x
3535
- Ethers 5.x
3636
- Ethers 4.x
37+
- Hardhat
3738

3839
## ethereum-abi-types-generator vs TypeChain
3940

4041
The first question I normally get is “have you seen TypeChain”, yes I have of course and it is a great tool but it was missing and did a few things which I didn't want as a developer. The main differences with this ethereum-abi-types-generator vs typechain are:
4142

4243
### No bundle size at all added
4344

44-
With TypeChain you have a class factory you have to connect to adding size into the final bundle. This package is all interfaces meaning nothing is added to your final bundle size.
45+
With TypeChain you have a class factory you have to connect to adding size into the final bundle. This package is all interfaces meaning nothing is added to your final bundle size.
4546

4647
### Exposes proper typed interfaces meaning you can use them in your application
4748

48-
TypeChain has dynamic interfaces aka ```public contractCall(): Promise<{ foo: BigNumber }>``` so if you wanted to use that interface somewhere in your app its not exported so can not be used. This lib generates response interfaces which are exported aka:
49+
TypeChain has dynamic interfaces aka `public contractCall(): Promise<{ foo: BigNumber }>` so if you wanted to use that interface somewhere in your app its not exported so can not be used. This lib generates response interfaces which are exported aka:
4950

5051
```ts
5152
export interface ContractCallResponse {
@@ -66,12 +67,12 @@ export interface FooRequest {
6667
public contractCall(request: FooRequest): Promise<ContractCallResponse>
6768
```
6869

69-
If you have worked with dynamic interfaces you understand the pain it brings having to recreate everytime.
70+
If you have worked with dynamic interfaces you understand the pain it brings having to recreate everytime.
7071

7172
### Use your provider interface your use too
7273

73-
TypeChain you have to connect to the factory then use the contract that way. With this lib you just use web3 or ethers interface for every contract call meaning you don't have to get use to another process it just works and zero code changes just cast and you got compile time errors for contracts.
74-
74+
TypeChain you have to connect to the factory then use the contract that way. With this lib you just use web3 or ethers interface for every contract call meaning you don't have to get use to another process it just works and zero code changes just cast and you got compile time errors for contracts.
75+
7576
## Motivation
7677

7778
Blockchain development in JavaScript is already super hard. You have all these tools like `truffle,` `ethers`, `web3` (the list goes on) which you have to get use to and the learning curve is already quite high. On top of this, you have loads of other tools to get things to work as you need. TypeScript allows you to bring runtime errors in the compiler but on contract calls most developers have to either build their own types meaning maintaining them and easily getting out of sync or have no compile type errors using the dreaded `any` hoping and praying you don't break anything. The idea was to not have to make the developer wrap any kind of `web3` or `ethers` instance or use a new tool to get this working but with a simple 1 line change you can use all the same libraries interfaces as what the developer is use to but with `types` `auto-generated` for you to bring back compile-time errors on any contract calls with super ease.
@@ -100,6 +101,8 @@ If you get compile time errors due to it waiting `web3` dependencies when using
100101

101102
## CLI usage
102103

104+
### Web3 1.x and 2.x & Ethers 5.x & Ethers 4.x
105+
103106
```ts
104107
$ abi-types-generator <abiFileLocation>
105108
$ abi-types-generator <abiFileLocation> --name=ABI_NAME
@@ -119,6 +122,12 @@ $ abi-types-generator <abiFileLocation> --output=PATH_DIRECTORY --name=ABI_NAME
119122
$ abi-types-generator <abiFileLocation> --output=PATH_DIRECTORY --name=ABI_NAME --provider=web3|ethers|ethers_v5 --watch
120123
```
121124

125+
#### Hardhat
126+
127+
```ts
128+
$ abi-types-generator hardhat
129+
```
130+
122131
We suggest running these within the `script` commands in npm or yarn this way you will not lose your commands and can be run on build agents as well. Also you will not get confused with sharing the script and others running in the wrong paths. Examples below:
123132

124133
```json
@@ -142,7 +151,8 @@ We suggest running these within the `script` commands in npm or yarn this way yo
142151
"ethers-v5-token-abi": "abi-types-generator './abi-examples/token-abi.json' --output='./ethers_v5/uniswap-example/generated-typings' --name=token-contract --provider=ethers_v5",
143152
"ethers-v5-uniswap-exchange-abi": "abi-types-generator './abi-examples/uniswap-exchange-abi.json' --output='./ethers_v5/uniswap-example/generated-typings' --name=uniswap-exchange-contract --provider=ethers_v5",
144153
"ethers-v5-uniswap-factory-abi": "abi-types-generator './abi-examples/uniswap-factory-abi.json' --output='./ethers_v5/uniswap-example/generated-typings' --name=uniswap-factory-contract --provider=ethers_v5",
145-
"ethers-v5-uniswap": "npm run ethers-token-abi && npm run ethers-uniswap-exchange-abi && npm run ethers-uniswap-factory-abi"
154+
"ethers-v5-uniswap": "npm run ethers-token-abi && npm run ethers-uniswap-exchange-abi && npm run ethers-uniswap-factory-abi",
155+
"hardhat-example": "abi-types-generator hardhat"
146156
}
147157
}
148158
```
@@ -275,6 +285,59 @@ We use `prettier` to format all files, to make sure it matches your coding style
275285

276286
Right now the package does not try to find any of your `tslint.json` settings. It will support this soon. For now if you get any `tslint` errors when running the linter it's best to ignore any generated file in the `linterOptions` > `exclude` of the `tslint.json`. I tend to put all my generated files in 1 place so I can ignore the entire folder.
277287

288+
### Using with hardhat
289+
290+
First you create a script in your `package.json` that runs the `abi-types-generator` script after it compiles everytime.
291+
292+
```json
293+
{
294+
"scripts": {
295+
"compile": "npx hardhat compile && abi-types-generator hardhat"
296+
}
297+
}
298+
```
299+
300+
If your contracts are ready to compile run:
301+
302+
```bash
303+
$ npm run compile
304+
```
305+
306+
You types are now created within the root of your hardhat project in a folder called `ethereum-abi-types` and you can use them throughout your tests/scripts or anything `ts` related.
307+
308+
### Test example
309+
310+
```ts
311+
import { expect } from 'chai';
312+
import { ethers } from 'hardhat';
313+
import {
314+
ContractContext as MyVeryFirstContract,
315+
GetFooResponse,
316+
GetFooRequest,
317+
} from '../ethereum-abi-types/MyVeryFirstContract';
318+
319+
describe('Example test', function () {
320+
let contract: NftMetadataHelper;
321+
beforeEach(async () => {
322+
const contractFactory = await ethers.getContractFactory(
323+
'MyVeryFirstContract'
324+
);
325+
326+
// thats it you now have full typings on your contract
327+
contract =
328+
(await contractFactory.deploy()) as unknown as MyVeryFirstContract;
329+
});
330+
331+
it('I love to write unit tests', async () => {
332+
const foo: GetFooRequest = { fooBoo: true };
333+
const result = await contract.getFoo(foo);
334+
335+
expect(result).to.equal(
336+
{ fooResponse: 'boo' }
337+
});
338+
});
339+
```
340+
278341
### Using with web3 and ethers
279342
280343
#### Web3 - https://www.npmjs.com/package/web3

abi-types-generator/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ethereum-abi-types-generator",
3-
"version": "1.1.9",
3+
"version": "1.2.0",
44
"description": "Generate types from an ethereum ABI json file.",
55
"main": "dist/index.js",
66
"scripts": {

abi-types-generator/src/commands/generate.ts

+21-14
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ProgramOptions } from '../common/models/program-options';
44
import { ConverterType } from '../converters/enums/converter-type';
55
import AbiGenerator from '../converters/typescript/abi-generator';
66
import { Provider } from '../converters/typescript/enums/provider';
7-
import { GenerateResponse } from '../converters/typescript/models/generate-response';
7+
import { HardhatFactory } from '../converters/typescript/hardhat-factory';
88
import { CommandTypes } from './enums/command-types';
99

1010
const help = Helpers.getHelpMessageByCommandType(CommandTypes.generate);
@@ -17,18 +17,29 @@ export = {
1717

1818
const language = cmd.options.lang || ConverterType.ts;
1919

20-
let generateResponse: GenerateResponse;
21-
2220
try {
2321
switch (language) {
2422
case ConverterType.ts:
25-
generateResponse = new AbiGenerator({
26-
provider: (cmd.options.provider as Provider) || Provider.web3,
27-
abiFileLocation: cmd.command,
28-
outputPathDirectory: cmd.options.output,
29-
name: cmd.options.name,
30-
watch: cmd.options.watch !== undefined,
31-
}).generate();
23+
if (cmd.command === 'hardhat') {
24+
const response = await new HardhatFactory().generate();
25+
if (response) {
26+
Logger.log(
27+
`successfully created typings for all contracts for hardhat, these are saved in ${response}`
28+
);
29+
}
30+
} else {
31+
const generateResponse = new AbiGenerator({
32+
provider: (cmd.options.provider as Provider) || Provider.web3,
33+
abiFileLocation: cmd.command,
34+
outputPathDirectory: cmd.options.output,
35+
name: cmd.options.name,
36+
watch: cmd.options.watch !== undefined,
37+
}).generate();
38+
39+
Logger.log(
40+
`successfully created typings for abi file ${generateResponse.abiJsonFileLocation} saved in ${generateResponse.outputLocation}`
41+
);
42+
}
3243
break;
3344
default:
3445
Logger.error(
@@ -40,9 +51,5 @@ export = {
4051
Logger.error(error.message);
4152
return;
4253
}
43-
44-
Logger.log(
45-
`successfully created typings for abi file ${generateResponse.abiJsonFileLocation} saved in ${generateResponse.outputLocation}`
46-
);
4754
},
4855
};

abi-types-generator/src/converters/typescript/abi-generator.ts

+9-14
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default class AbiGenerator {
3333
*/
3434
public generate(): GenerateResponse {
3535
this.clearAllQuotesFromContextInfo();
36-
if (!this.isDirectory(this.getOutputPathDirectory())) {
36+
if (!TypeScriptHelpers.isDirectory(this.getOutputPathDirectory())) {
3737
throw new Error('output path must be a directory');
3838
}
3939

@@ -258,11 +258,14 @@ export default class AbiGenerator {
258258
}
259259

260260
try {
261-
const result: AbiItem[] = JSON.parse(
262-
fs.readFileSync(abiFileFullPath, 'utf8')
263-
);
261+
// tslint:disable-next-line: no-any
262+
const result: any = JSON.parse(fs.readFileSync(abiFileFullPath, 'utf8'));
263+
264+
if (result.abi) {
265+
return result.abi;
266+
}
264267

265-
return result;
268+
return result as AbiItem[];
266269
} catch (error) {
267270
throw new Error(
268271
`Abi file ${abiFileFullPath} is not a json file. Abi must be a json file.`
@@ -281,15 +284,7 @@ export default class AbiGenerator {
281284
* Build the executing path
282285
*/
283286
private buildExecutingPath(joinPath: string): string {
284-
return path.resolve(process.cwd(), joinPath);
285-
}
286-
287-
/**
288-
* Check is a path is a directory
289-
* @param pathValue The path value
290-
*/
291-
private isDirectory(pathValue: string): boolean {
292-
return fs.existsSync(pathValue) && fs.lstatSync(pathValue).isDirectory();
287+
return TypeScriptHelpers.buildExecutingPath(joinPath);
293288
}
294289

295290
/**

abi-types-generator/src/converters/typescript/common/helpers.ts

+17
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import fs from 'fs-extra';
2+
import path from 'path';
13
import { AbiInput, AbiOutput, SolidityType } from '../../../abi-properties';
24
import Helpers from '../../../common/helpers';
35
import { Provider } from '../enums/provider';
@@ -352,4 +354,19 @@ export default class TypeScriptHelpers {
352354

353355
return `export type ${typeName} = ${result};`;
354356
}
357+
358+
/**
359+
* Check is a path is a directory
360+
* @param pathValue The path value
361+
*/
362+
public static isDirectory(pathValue: string): boolean {
363+
return fs.existsSync(pathValue) && fs.lstatSync(pathValue).isDirectory();
364+
}
365+
366+
/**
367+
* Build the executing path
368+
*/
369+
public static buildExecutingPath(joinPath: string): string {
370+
return path.resolve(process.cwd(), joinPath);
371+
}
355372
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import fs from 'fs-extra';
2+
import path from 'path';
3+
import { Logger } from '../../common/logger';
4+
import AbiGenerator from './abi-generator';
5+
import TypeScriptHelpers from './common/helpers';
6+
import { Provider } from './enums/provider';
7+
8+
interface AbiFilePathContext {
9+
path: string;
10+
contractName?: string | undefined;
11+
}
12+
13+
export class HardhatFactory {
14+
/**
15+
* Generate all hardhat contract typings
16+
*/
17+
public async generate(): Promise<string | undefined> {
18+
const contracts = TypeScriptHelpers.buildExecutingPath(
19+
'./artifacts/contracts'
20+
);
21+
if (!TypeScriptHelpers.isDirectory(contracts)) {
22+
throw new Error(
23+
'can not find the artifacts > contracts directory please make sure you run this command on the route of the project and have compiled your smart contracts'
24+
);
25+
}
26+
27+
const abiFilesPaths = await this.buildAbiFilesPathContext(contracts);
28+
if (abiFilesPaths.length === 0) {
29+
Logger.log(
30+
'No contracts found in artifacts > contracts please make sure you have compiled your smart contracts'
31+
);
32+
return undefined;
33+
}
34+
35+
const saveTypingsFolder = TypeScriptHelpers.buildExecutingPath(
36+
'./ethereum-abi-types'
37+
);
38+
39+
if (!fs.existsSync(saveTypingsFolder)) {
40+
fs.mkdirSync(saveTypingsFolder);
41+
}
42+
43+
for (let i = 0; i < abiFilesPaths.length; i++) {
44+
const generateResponse = new AbiGenerator({
45+
provider: Provider.ethers_v5,
46+
abiFileLocation: abiFilesPaths[i].path,
47+
outputPathDirectory: saveTypingsFolder,
48+
name: abiFilesPaths[i].contractName,
49+
}).generate();
50+
51+
Logger.log(
52+
`successfully created typings for abi file ${generateResponse.abiJsonFileLocation} saved in ${generateResponse.outputLocation}`
53+
);
54+
}
55+
56+
return saveTypingsFolder;
57+
}
58+
59+
/**
60+
* Build abi files path context
61+
* @param directoryPath The directory path
62+
* @param abiFiles The abi files
63+
*/
64+
private async buildAbiFilesPathContext(
65+
directoryPath: string,
66+
abiFiles: AbiFilePathContext[] = []
67+
): Promise<AbiFilePathContext[]> {
68+
const folder = await fs.promises.readdir(directoryPath);
69+
for (let i = 0; i < folder.length; i++) {
70+
const item = folder[i];
71+
const itemPath = path.join(directoryPath, item);
72+
// console.log(item, itemPath);
73+
if (TypeScriptHelpers.isDirectory(itemPath)) {
74+
await this.buildAbiFilesPathContext(itemPath, abiFiles);
75+
} else {
76+
if (item.includes('.json')) {
77+
try {
78+
const metadata = JSON.parse(fs.readFileSync(itemPath, 'utf8'));
79+
if (metadata.abi && Array.isArray(metadata.abi)) {
80+
abiFiles.push({
81+
path: itemPath,
82+
contractName: metadata.contractName,
83+
});
84+
}
85+
} catch (error) {
86+
// mute it
87+
}
88+
}
89+
}
90+
}
91+
92+
return abiFiles;
93+
}
94+
}

0 commit comments

Comments
 (0)