Dev typings for all your ethereum ABI contract methods and events with 1 liner integrations with web3
and ethers
👏👏👏👏. Never have to have a runtime error again and bring them into compile time errors in 2 minutes.
Demo gif can be seen on the github readme
A CLI tool which allows you to convert an ABI json file into fully loaded interfaces types.
🚀 Compile time errors, never make easy dynamic mistakes again.
🚀 Easy 1 line solution to get working
🚀 Use the same interfaces as the provider your use to, no changes in how you develop
🚀 Works with just a simple ABI json file
🚀 Supports ethers
and web3
out the box
🚀 0 bundle increase, its all dev dependencies so get all the benefit with no negative impact on your build size
🚀 Nice easy CLI to allow you to generate these however you like, build scripts, watch file change events its up to you
🚀 Supported multidimensional return types aka bytes32[4] >
[string, string, string, string, string]`
🚀 Automatic documentation generated on each method outputting all the details about it which is on the ABI - no jumping back and forth
🚀 Generated event typings
- Web3 1.x and 2.x
- Ethers 5.x
- Ethers 4.x
- Hardhat
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:
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.
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:
export interface ContractCallResponse {
foo: BigNumber
}
public contractCall(): Promise<ContractCallResponse>
This means you can use this interface anywhere in your app as its just exported for you. The naming for this is ${contractCallMethodName}Response
aka if a method was called HelloWorld the response interface would be HelloWorldResponse
. This also follows suit on the request interfaces aka:
export interface FooRequest {
foo: BigNumber,
boo: string;
}
public contractCall(request: FooRequest): Promise<ContractCallResponse>
If you have worked with dynamic interfaces you understand the pain it brings having to recreate everytime.
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.
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.
The ABI file is the source of truth for all contract calls so by building types from this file we can be assured our types correct.
$ npm install ethereum-abi-types-generator --save-dev
$ yarn add ethereum-abi-types-generator --dev
You can install this globally as well but you must make sure wherever the --output
location is which generates the typings file has ethereum-abi-types-generator
installed in that project, as it uses imports from this package to map the ContractContext
to make your life easier handling the generic type build up automatically. We suggest always running this tool inside a project context.
If you get compile time errors due to it waiting web3
dependencies when using ethers please set skipLibCheck
: true in the tsconfig.json compiler options and this should fix that issue.
$ abi-types-generator <abiFileLocation>
$ abi-types-generator <abiFileLocation> --name=ABI_NAME
$ abi-types-generator <abiFileLocation> --watch
$ abi-types-generator <abiFileLocation> --name=ABI_NAME --watch
$ abi-types-generator <abiFileLocation> --output=PATH_DIRECTORY
$ abi-types-generator <abiFileLocation> --output=PATH_DIRECTORY --watch
$ abi-types-generator <abiFileLocation> --output=PATH_DIRECTORY --name=ABI_NAME
$ abi-types-generator <abiFileLocation> --output=PATH_DIRECTORY --name=ABI_NAME --watch
$ abi-types-generator <abiFileLocation> --provider=web3|ethers|ethers_v5
$ abi-types-generator <abiFileLocation> --provider=web3|ethers|ethers_v5 --watch
$ abi-types-generator <abiFileLocation> --name=ABI_NAME --provider=web3|ethers|ethers_v5
$ abi-types-generator <abiFileLocation> --name=ABI_NAME --provider=web3|ethers|ethers_v5 --watch
$ abi-types-generator <abiFileLocation> --output=PATH_DIRECTORY --provider=web3|ethers|ethers_v5
$ abi-types-generator <abiFileLocation> --output=PATH_DIRECTORY --provider=web3|ethers|ethers_v5 --watch
$ abi-types-generator <abiFileLocation> --output=PATH_DIRECTORY --name=ABI_NAME --provider=web3|ethers|ethers_v5
$ abi-types-generator <abiFileLocation> --output=PATH_DIRECTORY --name=ABI_NAME --provider=web3|ethers|ethers_v5 --watch
$ abi-types-generator hardhat
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:
{
"name": "examples",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"web3-example": "abi-types-generator './abi-examples/fake-contract-abi.json' --output='./web3/fake-contract-example/generated-typings' --name=fake-contract",
"web3-token-abi": "abi-types-generator './abi-examples/token-abi.json' --output='./web3/uniswap-example/generated-typings' --name=token-contract",
"web3-uniswap-exchange-abi": "abi-types-generator './abi-examples/uniswap-exchange-abi.json' --output='./web3/uniswap-example/generated-typings' --name=uniswap-exchange-contract",
"web3-uniswap-factory-abi": "abi-types-generator './abi-examples/uniswap-factory-abi.json' --output='./web3/uniswap-example/generated-typings' --name=uniswap-factory-contract",
"web3-uniswap": "npm run web3-token-abi && npm run web3-uniswap-exchange-abi && npm run web3-uniswap-factory-abi",
"ethers-example": "abi-types-generator './abi-examples/fake-contract-abi.json' --output='./ethers/fake-contract-example/generated-typings' --name=fake-contract --provider=ethers",
"ethers-token-abi": "abi-types-generator './abi-examples/token-abi.json' --output='./ethers/uniswap-example/generated-typings' --name=token-contract --provider=ethers",
"ethers-uniswap-exchange-abi": "abi-types-generator './abi-examples/uniswap-exchange-abi.json' --output='./ethers/uniswap-example/generated-typings' --name=uniswap-exchange-contract --provider=ethers",
"ethers-uniswap-factory-abi": "abi-types-generator './abi-examples/uniswap-factory-abi.json' --output='./ethers/uniswap-example/generated-typings' --name=uniswap-factory-contract --provider=ethers",
"ethers-uniswap": "npm run ethers-token-abi && npm run ethers-uniswap-exchange-abi && npm run ethers-uniswap-factory-abi",
"ethers-v5-example": "abi-types-generator './abi-examples/fake-contract-abi.json' --output='./ethers_v5/fake-contract-example/generated-typings' --name=fake-contract --provider=ethers_v5",
"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",
"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",
"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",
"ethers-v5-uniswap": "npm run ethers-token-abi && npm run ethers-uniswap-exchange-abi && npm run ethers-uniswap-factory-abi",
"hardhat-example": "abi-types-generator hardhat"
}
}
The ABI json file path location. Must be a JSON file path.
The ABI ts file you want it to be named as, this will also name interface and enums as this. It will remove any -
and .
for the ts
interfaces and enum names.
If not supplied it will take the file name and use that as the abi name throughout.
Example:
$ abi-types-generator ./examples/abi-examples/uniswap-factory-abi.json
generates: ./examples/abi-examples/uniswap-factory-abi.ts
Example:
$ abi-types-generator ./examples/abi-examples/uniswap-factory-abi.json --name=foo-abi
generates: ./examples/abi-examples/foo-abi.ts
$ abi-types-generator ./examples/abi-examples/uniswap-factory-abi.json --name=foo.abi
generates: ./examples/abi-examples/foo.abi.ts
$ abi-types-generator ./examples/abi-examples/uniswap-factory-abi.json --name=foo
generates: ./examples/abi-examples/foo.ts
Where you want the .ts
to be generated and saved to.
If not supplied it will use the directory of the <abiFileLocation>
and generate it there.
Example:
$ abi-types-generator ./examples/abi-examples/uniswap-factory-abi.json
generates in: ./examples/abi-examples/uniswap-factory-abi.ts
Example:
$ abi-types-generator ./examples/abi-examples/uniswap-factory-abi.json --output=./examples/ethers/generated-typings
generates in: ./examples/ethers/generated-typings/uniswap-factory-abi.ts
$ abi-types-generator ./examples/abi-examples/uniswap-factory-abi.json --output=./examples/ethers/generated-typings --name=foo
generates: ./examples/ethers/generated-typings/foo.ts
What contract
support you want to generate. This is the library you are using on your dapp and the lib you use to call any contract calls.
export enum Provider {
web3 = 'web3',
ethers = 'ethers',
ethers_v5 = 'ethers_v5',
}
If not supplied it will fallback to web3
Example:
$ abi-types-generator ./examples/abi-examples/uniswap-factory-abi.json
generates the web3 contract typings based on the web3 lib.
Example:
$ abi-types-generator ./examples/abi-examples/uniswap-factory-abi.json --provider=ethers
generates the ethers contract typings based on the ethers 4 lib.
Example:
$ abi-types-generator ./examples/abi-examples/uniswap-factory-abi.json --provider=ethers_v5
generates the ethers contract typings based on the ethers 5 lib.
This will watch the <abiFileLocation>
for changes and if anything does change it will regenerate the typings and resave them in the --output
defined or use the default one if one is not defined.
We use prettier
to format all files, to make sure it matches your coding style just make sure you have a .prettierrc
defined in the root of your project and it will use that. If it can not find any it will take these default prettier:
{
parser: 'typescript',
trailingComma: 'es5',
singleQuote: true,
bracketSpacing: true,
printWidth: 80,
}
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.
First you create a script in your package.json
that runs the abi-types-generator
script after it compiles everytime.
{
"scripts": {
"compile": "npx hardhat compile && abi-types-generator hardhat"
}
}
If your contracts are ready to compile run:
$ npm run compile
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.
import { expect } from 'chai';
import { ethers } from 'hardhat';
import {
ContractContext as MyVeryFirstContract,
GetFooResponse,
GetFooRequest,
} from '../ethereum-abi-types/MyVeryFirstContract';
describe('Example test', function () {
let contract: MyVeryFirstContract;
beforeEach(async () => {
const contractFactory = await ethers.getContractFactory(
'MyVeryFirstContract'
);
// thats it you now have full typings on your contract
contract =
(await contractFactory.deploy()) as unknown as MyVeryFirstContract;
});
it('I love to write unit tests', async () => {
const foo: GetFooRequest = { fooBoo: true };
const result: GetFooResponse = await contract.getFoo(foo);
expect(result).to.equal(
{ fooResponse: 'boo' }
});
});
Below is just a fake contract example just so you can understand how the typings improve your development.
The cli tool will generate all your typings for you and expose them in the generated file. Its super easy to start using strongly typed interfaces for all your ABI calls.
Lets say i run the cli command:
$ abi-types-generator ./abi-examples/fake-contract-abi.json --output=./generated-typings --name=fake-contract
This will generate an ts
file of ./generated-typings/fake-contract.ts
which has all your strongly typed methods and events.
All you meed to do is cast your new web3.eth.Contract
code to an ContractContext
which is exposed in where you defined the --output
path to. In this example it is ./generated-typings/fake-contract.ts
import Web3 from 'web3';
import { AbiExamples } from '../../abi-examples';
import { ContractContext } from './generated-typings/fake-contract';
const web3 = new Web3(
'https://mainnet.infura.io/v3/280bb5b627394709938a7cc0b71a4a58'
);
// Has to cast to unknown as we have made some typings changes to the
// contract interfaces which conflicts with `web3` typings.
// This all work great but the compiler gets confused.
// Casting to unknown first then the `ContractContext` solves this.
const contract = new web3.eth.Contract(
AbiExamples.YOUR_ABI,
AbiExamples.YOUR_CONTRACT_ADDRESS
) as unknown as ContractContext;
Easy as that 🔥🔥
import Web3 from 'web3';
import { AbiExamples } from '../../abi-examples';
import {
ContractContext,
TupleInputOnlyRequest,
TupleNoInputNamesResponse,
} from './generated-typings/fake-contract';
const example = async () => {
const mockEthereumAddress = '0x419D0d8BdD9aF5e606Ae2232ed285Aff190E711b';
const web3 = new Web3(
'https://mainnet.infura.io/v3/280bb5b627394709938a7cc0b71a4a58'
);
// Has to cast to unknown as we have made some typings changes to the
// contract interfaces which conflicts with `web3` typings.
// This all work great but the compiler gets confused.
// Casting to unknown first then the `ContractContext` solves this.
const contract = new web3.eth.Contract(
AbiExamples.YOUR_ABI,
AbiExamples.YOUR_CONTRACT_ADDRESS
) as unknown as ContractContext;
// you now have full typings on `contract.methods` which has generated docs
const simpleCall = await contract.methods
.easyExample(true, mockEthereumAddress, new Date().getTime())
.call();
console.log(simpleCall);
// build up a proper typed request object with the interface importable
// from the typings file generated
const tupleExampleRequest: TupleInputOnlyRequest = {
address: mockEthereumAddress,
timestamps: [
new Date().getTime(),
new Date().getTime(),
new Date().getTime(),
],
};
// encode abi method all exposed
const data = contract.methods.tupleInputOnly(tupleExampleRequest).encodeABI();
console.log(data);
// any none constant methods will have the correct interface on them as well
// aka you cant call `.call()` here and the compile will show you this.
// will also expose the event emitters for your typings to still work with web3
contract.methods
.tupleInputOnly(tupleExampleRequest)
.send({ from: mockEthereumAddress })
.on('transactionHash', (hash) => {
console.log(hash);
});
const result: TupleNoInputNamesResponse = await contract.methods
.tupleNoInputNames(mockEthereumAddress, mockEthereumAddress)
.call();
console.log(result);
// full typings on your events with even the filter indexs which will
// not compile if supply it incorrectly and only expose the correct ones for you
contract.events
.Event1({ filter: { token: '0x00' } })
.on('changed', (event) => {
console.log(event);
});
// can any past events only allowing you to query events which actually exist
const event = await contract.getPastEvents('Event1', {
filter: { token: '0x00' },
});
console.log(event);
};
example();
If the ABI changes and I run the CLI command again or have a --watch on the file, when you try to compile it will flag any errors with your typings for you.
Ethers - https://www.npmjs.com/package/ethers
Below is just a fake contract example just so you can understand how the typings improve your development.
The cli tool will generate all your typings for you and expose them in the generated file. Its super easy to start using strongly typed interfaces for all your ABI calls.
Lets say i run the cli command:
Ethers v4
$ abi-types-generator ./abi-examples/fake-contract-abi.json --output=./generated-typings --name=fake-contract --provider=ethers
Ethers v5
$ abi-types-generator ./abi-examples/fake-contract-abi.json --output=./generated-typings --name=fake-contract --provider=ethers_v5
This will generate an ts
file of ./generated-typings/fake-contract.ts
which has all your strongly typed methods and events.
All you meed to do is cast your new ethers.Contract
code to an ContractContext
which is exposed in where you defined the --output
path to. In this example it is ./generated-typings/fake-contract.ts
import { ethers } from 'ethers';
import { AbiExamples } from '../../abi-examples';
import { ContractContext } from './generated-typings/fake-contract';
// Connect to the network
const customHttpProvider = new ethers.providers.JsonRpcProvider(
'https://mainnet.infura.io/v3/280bb5b627394709938a7cc0b71a4a58'
);
// Has to cast to unknown as we have made some typings changes to the
// contract interfaces which conflicts with `ethers` typings.
// This all work great but the compiler gets confused.
// Casting to unknown first then the `ContractContext` solves this.
const contract = new ethers.Contract(
AbiExamples.YOUR_CONTRACT_ADDRESS,
AbiExamples.YOUR_ABI,
customHttpProvider
) as unknown as ContractContext;
Easy as that 🔥🔥
import { ethers, utils } from 'ethers';
import { AbiExamples } from '../../abi-examples';
import {
ContractContext,
TupleInputOnlyRequest,
TupleNoInputNamesResponse,
} from './generated-typings/fake-contract';
const example = async () => {
const mockEthereumAddress = '0x419D0d8BdD9aF5e606Ae2232ed285Aff190E711b';
// Connect to the network
const customHttpProvider = new ethers.providers.JsonRpcProvider(
'https://mainnet.infura.io/v3/280bb5b627394709938a7cc0b71a4a58'
);
// Has to cast to unknown as we have made some typings changes to the
// contract interfaces which conflicts with `ethers` typings.
// This all work great but the compiler gets confused.
// Casting to unknown first then the `ContractContext` solves this.
const contract = new ethers.Contract(
AbiExamples.YOUR_CONTRACT_ADDRESS,
AbiExamples.YOUR_ABI,
customHttpProvider
) as unknown as ContractContext;
// you now have full typings on `contract.x` which has generated docs
const simpleCall = await contract.easyExample(
true,
mockEthereumAddress,
new Date().getTime()
);
console.log(simpleCall);
// you can use the same etherjs flows to send and sign transactions
// `contract.connect` will return a `ContractContext` so will still have
// all the typings exposed for you
const privateKey =
'0x0123456789012345678901234567890123456789012345678901234567890123';
const wallet = new ethers.Wallet(privateKey, customHttpProvider);
// Create a new instance of the Contract with a Signer, which allows
// update methods
const contractWithSigner = contract.connect(wallet);
// build up a proper typed request object with the interface importable
// from the typings file generated
const tupleExampleRequest: TupleInputOnlyRequest = {
address: mockEthereumAddress,
timestamps: [
new Date().getTime(),
new Date().getTime(),
new Date().getTime(),
],
};
// strongly typed optional overrides as well for both `calls` and `transactions`
const tx = await contractWithSigner.tupleInputOnly(tupleExampleRequest, {
// The maximum units of gas for the transaction to use
gasLimit: 23000,
// The price (in wei) per unit of gas
gasPrice: utils.parseUnits('9.0', 'gwei'),
// The nonce to use in the transaction
nonce: 123,
// The amount to send with the transaction (i.e. msg.value)
value: utils.parseEther('1.0'),
// The chain ID (or network ID) to use
chainId: 1,
});
console.log(tx.hash);
// "0xaf0068dcf728afa5accd02172867627da4e6f946dfb8174a7be31f01b11d5364"
// The operation is NOT complete yet; we must wait until it is mined
await tx.wait();
const result: TupleNoInputNamesResponse = await contract.tupleNoInputNames(
mockEthereumAddress,
mockEthereumAddress
);
console.log(result);
// full typings on your events
contract.on(
'Event1',
(author: string, oldValue: string, newValue: string, event: any) => {
// Called when anyone changes the value
console.log(author);
// "0x14791697260E4c9A71f18484C9f997B308e59325"
console.log(oldValue);
// "Hello World"
console.log(newValue);
// "I like turtles."
console.log(event.blockNumber);
// 4115004
}
);
// filter that matches my signer as the author
const filter = contract.filters.Event1(wallet.address);
// full typings on filter interfaces as well
contract.filters.Event1(
filter,
(author: string, oldValue: string, newValue: string, event: any) => {
// Called ONLY when your account changes the value
console.log(author);
// "0x14791697260E4c9A71f18484C9f997B308e59325"
console.log(oldValue);
// "Hello World"
console.log(newValue);
// "I like turtles."
console.log(event.blockNumber);
// 4115004
}
);
};
example();
If the ABI changes and I run the CLI command again or have a --watch on the file, when you try to compile it will flag any errors with your typings for you.
Please raise any issues in the below link.
https://github.com/joshstevens19/ethereum-abi-types-generator/issues
This package is brought to you by Josh Stevens. My aim is to be able to keep creating these awesome packages to help the Ethereum space grow with easier-to-use tools to allow the learning curve to get involved with blockchain development easier and making Ethereum ecosystem better. If you want to help with that vision and allow me to invest more time into creating cool packages or if this package has saved you a lot of development time donations are welcome, every little helps. By donating, you are supporting me to be able to maintain existing packages, extend existing packages (as Ethereum matures), and allowing me to build more packages for Ethereum due to being able to invest more time into it. Thanks, everyone!
Direct donations any token accepted - Eth address > 0x699c2daD091ffcF18f3cd9E8495929CA3a64dFe1
sponsor me via github using fiat money