Skip to content

Commit 3a5c377

Browse files
Add generic type argument for useContract (#198)
* Add generic type argument for useContract * Remove `undefined` from type definition * Add guide to README for `useContract` generic type argument * Add example usage on example project * Update type definition * Update README * Update README.md * Update README.md * Reset README changes Co-authored-by: Dhaiwat Pandya <[email protected]>
1 parent 0c5209b commit 3a5c377

File tree

7 files changed

+328
-24
lines changed

7 files changed

+328
-24
lines changed

example/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
22

3+
types
4+
35
# dependencies
46
/node_modules
57
/.pnp

example/abis/Greeter.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
[
2+
{
3+
"inputs": [
4+
{
5+
"internalType": "string",
6+
"name": "_greeting",
7+
"type": "string"
8+
}
9+
],
10+
"stateMutability": "nonpayable",
11+
"type": "constructor"
12+
},
13+
{
14+
"inputs": [],
15+
"name": "greet",
16+
"outputs": [
17+
{
18+
"internalType": "string",
19+
"name": "",
20+
"type": "string"
21+
}
22+
],
23+
"stateMutability": "view",
24+
"type": "function"
25+
},
26+
{
27+
"inputs": [
28+
{
29+
"internalType": "string",
30+
"name": "_greeting",
31+
"type": "string"
32+
}
33+
],
34+
"name": "setGreeting",
35+
"outputs": [],
36+
"stateMutability": "nonpayable",
37+
"type": "function"
38+
},
39+
{
40+
"inputs": [
41+
{
42+
"internalType": "address payable",
43+
"name": "_to",
44+
"type": "address"
45+
}
46+
],
47+
"name": "transferTo",
48+
"outputs": [],
49+
"stateMutability": "payable",
50+
"type": "function"
51+
}
52+
]

example/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@
66
"dev": "next dev",
77
"build": "next build",
88
"start": "next start",
9-
"lint": "next lint"
9+
"lint": "next lint",
10+
"postinstall": "yarn typechain",
11+
"typechain": "typechain --target=ethers-v5 abis/*.json --out-dir=types/contracts"
1012
},
1113
"dependencies": {
1214
"@chakra-ui/react": "^1.7.3",
15+
"@typechain/ethers-v5": "^8.0.5",
1316
"@web3-ui/components": "^0.4.1",
1417
"@web3-ui/core": "^0.4.0",
18+
"@web3-ui/hooks": "^0.8.1",
1519
"next": "12.0.7",
1620
"react": "17.0.2",
17-
"react-dom": "17.0.2"
21+
"react-dom": "17.0.2",
22+
"typechain": "^6.1.0"
1823
},
1924
"devDependencies": {
2025
"eslint": "8.4.1",

example/pages/index.tsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import React, { useEffect, useState } from 'react';
2-
3-
import { NFTGallery } from '@web3-ui/components';
4-
import { Stack, Input, Button, Heading, Text } from '@chakra-ui/react';
2+
import {
3+
Container,
4+
Stack,
5+
Input,
6+
Button,
7+
Heading,
8+
Text
9+
} from '@chakra-ui/react';
510
import { useWallet, ConnectWallet } from '@web3-ui/core';
6-
import { Container } from '@chakra-ui/react';
11+
import { NFTGallery } from '@web3-ui/components';
12+
import { useContract } from '@web3-ui/hooks';
13+
import { Greeter } from '../types/contracts';
14+
import GreeterABI from '../abis/Greeter.json';
15+
716
export default function Home() {
817
const [address, setAddress] = useState('');
918
const [nftGallery, setNftGallery] = useState(null);
@@ -13,17 +22,44 @@ export default function Home() {
1322
connected,
1423
provider
1524
} = useWallet();
25+
const [greeterContract, isReady] = useContract<Greeter>(
26+
// Rinkeby
27+
'0x7e1D33FcF1C6b6fd301e0B7305dD40E543CF7135',
28+
GreeterABI
29+
);
1630

1731
useEffect(() => {
1832
console.log('correctNetwork', correctNetwork);
1933
}, [correctNetwork]);
2034

35+
async function setGreeting() {
36+
console.log(greeterContract);
37+
38+
const response = await greeterContract.setGreeting('Hello World');
39+
40+
console.log('setGreeting', response);
41+
}
42+
43+
async function greet() {
44+
const response = await greeterContract.greet();
45+
46+
console.log('greet', response);
47+
}
48+
2149
return (
2250
<Container>
2351
<ConnectWallet />
2452
{!correctNetwork && (
2553
<Button onClick={switchToCorrectNetwork}>Switch to Mainnet.</Button>
2654
)}
55+
{isReady ? (
56+
<Stack my={5}>
57+
<Button onClick={setGreeting}>Set Greeting</Button>
58+
<Button onClick={greet}>Greet</Button>
59+
</Stack>
60+
) : (
61+
<> </>
62+
)}
2763
<Stack p={3}>
2864
<Heading>Demo</Heading>
2965
<Text>Type in an address to view their NFTs</Text>

packages/hooks/README.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,81 @@ import { useContract } from '@web3-ui/hooks';
7979

8080
const [contract, isReady] = useContract('CONTRACT_ADDRESS', 'CONTRACT_ABI');
8181

82-
//check that the contract has been loaded
82+
// check that the contract has been loaded
8383
if (isReady) {
8484
await contract.greeting();
8585
}
8686
```
8787

88+
A generic type argument can be passed down to the hook to create the type definitions based on the ABIs stored in a directory. To auto-generate the types it is highly recommended to use [typechain](https://www.npmjs.com/package/typechain) package.
89+
90+
Install [typechain](https://www.npmjs.com/package/typechain)
91+
92+
```bash
93+
yarn add typechain @typechain/ethers-v5 --dev # or `npm i -D typechain @typechain/ethers-v5`
94+
```
95+
96+
Add a "typechain" script to your `package.json` file as well as a "postinstall" script that executes the script after installing dependencies.
97+
98+
```json
99+
"scripts": {
100+
"postinstall": "yarn typechain",
101+
"typechain": "typechain --target=ethers-v5 <ABI_DIRECTORY_PATH> --out-dir=<OUTPUT_DIRECTORY_PATH>",
102+
}
103+
```
104+
105+
- The `<ABI_DIRECTORY_PATH>` should be the path to where all of the ABIs are stored. e.g. `src/abis/*.json` (This depends on your preferred file structure)
106+
- The `<OUTPUT_DIRECTORY_PATH>` will be your preferred path to where the generated type definitions should be placed. e.g. `src/types/contracts`
107+
- For the `<OUTPUT_DIRECTORY_PATH>` it is also recommended to add the directory path on `.gitignore` since these can be generated via `typechain` script
108+
109+
For an actual example check below,
110+
111+
```json
112+
"scripts": {
113+
"postinstall": "yarn typechain",
114+
"typechain": "typechain --target=ethers-v5 src/abis/**/*.json --out-dir=src/types/contracts",
115+
}
116+
```
117+
118+
Next is to put all of your ABI JSON files stored to the defined `ABI_DIRECTORY_PATH` directory.
119+
120+
- `src/abis/ERC20Token/ERC20Token.json`
121+
- `src/abis/CoolProtocol/CoolProtocolLendingPool.json`
122+
123+
Then finally run the script to generate the type definitions.
124+
125+
```bash
126+
yarn typechain # or `npm run typechain`
127+
```
128+
129+
Example usage in utilizing the generic type argument for `useContract` hook
130+
131+
```tsx
132+
import React from 'react';
133+
import { useContract } from '@web3-ui/hooks';
134+
import { ERC20Token } from 'types/contracts/ERC20Token';
135+
import ERC20TokenABI from 'abis/ERC20Token/ERC20Token.json';
136+
137+
function App() {
138+
const [contract, isReady] = useContract<ERC20Token>(
139+
'CONTRACT_ADDRESS',
140+
ERC20TokenABI
141+
);
142+
143+
async function checkBalance() {
144+
const response = await contract.balanceOf('0x...');
145+
146+
console.log('checkBalance', response);
147+
}
148+
149+
return (
150+
<>{isReady ? <button onClick={checkBalance}></button> : 'Connect Wallet'}</>
151+
);
152+
}
153+
154+
export default App;
155+
```
156+
88157
---
89158

90159
### useTransaction

packages/hooks/src/hooks/useContract.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,38 @@
11
import React from 'react';
22
import { Web3Context } from '../Provider';
3-
import { Contract } from 'ethers';
3+
import { Contract, ContractInterface } from 'ethers';
44

5-
export function useContract(address: string, abi) {
5+
/**
6+
* @description
7+
* Defines the contract instance on `useState` hook
8+
*/
9+
export type ContractInstance<T extends Contract> = T | null;
10+
11+
/**
12+
* @description
13+
* The return type of the `useContract` hook
14+
*/
15+
export type UseContractHook<T extends Contract> = [
16+
ContractInstance<T> | null,
17+
boolean
18+
];
19+
20+
export function useContract<T extends Contract>(
21+
address: string,
22+
abi: ContractInterface
23+
): UseContractHook<T> {
624
const context = React.useContext(Web3Context);
7-
const [contract, setContract] = React.useState({});
25+
const [contract, setContract] = React.useState<ContractInstance<T>>(null);
826
const [isReady, setIsReady] = React.useState(false);
927
React.useEffect(() => {
1028
if (context?.connected) {
1129
const newContract = new Contract(
1230
address,
1331
abi,
1432
context.signer || undefined
15-
);
16-
const contractInterface = Object.values(
17-
newContract.interface.functions
18-
).reduce((accumulator, funcFragment) => {
19-
return {
20-
...accumulator,
21-
[funcFragment.name]: newContract[funcFragment.name]
22-
};
23-
}, {});
24-
setContract(contractInterface);
33+
) as T;
34+
35+
setContract(newContract);
2536
setIsReady(true);
2637
}
2738
}, [context]);

0 commit comments

Comments
 (0)