Beyond Solidity, part 2: Generating Typescript typings from ABIs
In the previous tutorial, we created our Monorepo with our contracts with a readme template file to let fellow developers know what we’re up to.
In this piece we’re going to crack onto the bits required to generate a JS API that external developers can use to interact with your smart contracts without understanding the architecture.
await myAPI.someMethod("pass info");
Typescript
In programming, the best tool for the job is the one you should use. While I personally think Typescript is great, it definitely comes with additional complexity and should always be weighed for its benefits.
In our case, we want to ensure that we’re not constantly dragging-and-dropping JSON ABI files into our projects and ensuring the types are correct all the time.
The worst bit is Solidity errors are highly un-descriptive. Invalid Solidity arguments passed
or revert
is the most you’re going to get. Catching errors before your program even compiles is highly useful especially when your errors only show up when you make a blockchain transaction.
Furthermore, integer overflow is a real problem with our API. Most tokens use 18 decimal places. By the time you reach a number such as 100,000 tokens you’re already at 10²³ which is the maximum integer in Javascript. Solidity on the other hand uses 2²⁵⁶ for all its integers.
Types
Here’s when we start using our monorepo to create a share reference for our Typescript types. Create a new folder inside the packages
folder called types
. Inside types add a tsconfig.json
file with the following settings:
Then create a package.json
file with the following code:
Next up, create a src
folder and then create your index.ts
file with the following code:
After you’ve done that run the following command:npm install && npm run build && lerna bootstrap
Now you can reference these types inside your project simply by adding @your-project/types
and ensuring the local symlink is generated by running lerna bootstrap
.
Your repo should now look like:
monorepo/
├── package.json
├── packages
│ └── contracts
│ └── package.json
│ └── types
│ └── package.json
│ └── tsconfig.json
│ └── src/index.ts
Base Contract
Essentially the base contract file is class which is then extended via all the contracts in your code. It provides a public web3 instance, an ABI to reference and the currently instantiated contract address. You can customise it to provide any extra functionality you need.
Start by creating a new folder in your packages
directory called base-contract
. Then create your tsconfig.json
with the following code:
Then create your package.json
file:
Now, inside your src
folder create the index.ts
file with the following:
As you can see in line 6, we’re importing our types we created before hand! Okay, time to wrap this up. Run the following command again:npm install && npm run build && lerna bootstrap
We can now reference our base-contract
file and types
from within our repo.
Your repo should now look like:
monorepo/
├── package.json
├── packages
│ └── contracts
│ └── package.json
│ └── types
│ └── package.json
│ └── tsconfig.json
│ └── src/index.ts
│ └── base-contract
│ └── package.json
│ └── tsconfig.json
│ └── src/index.ts
0x ABI Gen
For those of you that aren’t aware, an ABI file is a JSON file which contains all the functions and event references of the smart contract it references. It’s basically a header file for your contracts. What 0x’s ABI generator tool does is convert the JSON file into a Typescript file which you can use to interact by passing the contract address and an instance of web3/ethers.
Inside your contracts
package folder, create a new folder called contract_templates
and create a new file called contract.mustache.
You’ll notice that we reference our base-contract
and types
repo inside this file.
Inside your contract_templates
folder create a partials
folder. Then create the following files inside it:
By this point your contract_templates
folder should look like this:
contract_templates/
├── contract.mustache
├── partials
│ └── call.mustache
│ └── params.mustache
│ └── return_type.mustache
│ └── tx.mustache
│ └── typed_params.mustache
Getting close!
Okay so now that we have our types
, contract_templates
, and base-contract
we can do the fun stuff.
Inside the package.json
file of your contract
repo, add the following to your devDependencies
:
"@0xproject/abi-gen": "^1.0.7",
"@your-project/types": "^0.2.0",
"rimraf": "^2.6.2",
Then inside your dependencies
add the following:
"bignumber.js": "^4.1.0",
"ethereumjs-abi": "^0.6.5",
"mustache": "^3.0.0",
"types-ethereumjs-util": "^0.0.5",
"typescript": "^2.9.2",
After that, add the following to your scripts
:
"generate-artifacts-typings":
"node_modules/@0xproject/abi-gen/bin/abi-gen.js --abis './build/contracts/**/*.json' --out '../artifacts/src/build/wrappers' --template './contract_templates/contract.mustache' --partials './contract_templates/partials/*.mustache'", "build":
"truffle compile && npm run generate-artifacts-typings",
Run lerna bootstrap
and all your local and external dependencies should install.
Now do a npm build
and a new artifacts
repo should appear in your packages folder with Typescript bindings!
To recap:
We created two new packages, one called types
which contains Typescript types that are going to be shared throughout our project. Next, we created our base-contract
package which would be referenced by our contract-templates
but also our final generated typescript contract typings. When we run npm run build
we’re essentially building our typings by referencing our JSON ABI files and the contract_templates
inside our contracts
repo.
This now gives us the base we need to reference our Typescript typings from our Javascript library. We still have some additional setup to do but we’ll leave it here for this tutorial.
Hope you learned something useful!