https://github.com/hasparus/typechain-usedapp
https://github.com/hasparus/typechain-usedapp
Last synced: 11 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/hasparus/typechain-usedapp
- Owner: hasparus
- Created: 2021-10-22T12:20:54.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2021-10-25T15:46:57.000Z (over 4 years ago)
- Last Synced: 2024-10-19T05:18:29.249Z (over 1 year ago)
- Language: TypeScript
- Size: 123 KB
- Stars: 1
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
Possible stronger typing for Ethereum React Hooks
- [Problem](#problem)
- [`useContractCalls` examples](#usecontractcalls-examples)
- [**reading token allowance**](#reading-token-allowance)
- [**reading Uniswap pool immutables**](#reading-uniswap-pool-immutables)
- [Nonsolutions](#nonsolutions)
- [Possible Solutions](#possible-solutions)
- [1. _Zero Runtime_: Stronger types for existing useDApp hooks](#1-zero-runtime-stronger-types-for-existing-usedapp-hooks)
- [2. _Zero Codegen_: Ethers.js Contract instance as first argument](#2-zero-codegen-ethersjs-contract-instance-as-first-argument)
- [3. Calls as dictionary](#3-calls-as-dictionary)
- [4. Lots of codegen](#4-lots-of-codegen)
- [**Usage**](#usage)
- [**Potential generated code**](#potential-generated-code)
- [How to run this project?](#how-to-run-this-project)
## Problem
`useDapp` is written in TypeScript, but it doesn't have any information about
types of your contract
`useContractCalls`, useDApps basic function for reading on-chain state, has the
following signature.
```ts
interface ContractCall {
abi: Interface;
address: string;
method: string;
args: any[];
}
declare function useContractCalls(
calls: Array
): Array;
```
It is obviously _powerful_ and allows us to query multiple contracts with one
hook, batching our calls, but it is not very _convenient_.
Expand to see example usage of `useContractCalls`
### `useContractCalls` examples
#### **[reading token allowance](https://usedapp.readthedocs.io/en/latest/guide.html#custom-hooks)**
```ts
function useTokenAllowance(
tokenAddress: string | Falsy,
ownerAddress: string | Falsy,
spenderAddress: string | Falsy
) {
const [allowance] =
useContractCall(
ownerAddress &&
spenderAddress &&
tokenAddress && {
abi: ERC20Interface,
address: tokenAddress,
method: "allowance",
args: [ownerAddress, spenderAddress],
}
) ?? [];
return allowance;
}
```
#### **[reading Uniswap pool immutables](https://docs.uniswap.org/protocol/reference/core/interfaces/pool/IUniswapV3PoolImmutables)**
```ts
function usePoolImmutables(address: string) {
const contract = { abi: new Interface(PoolABI), address };
const [token0, token1, fee, tickSpacing, maxLiquidityPerTick] =
useContractCalls(
["token0", "token1", "fee", "tickSpacing", "maxLiquidityPerTick"].map(
(method) => ({ ...contract, method, args: [] })
)
) as any as
| [[string], [string], [number], [number], BigNumberish]
| undefined[];
}
```
---
## Nonsolutions
Alternatively to useDApp, we could typed ethers contracts generated by TypeChain
and with `react-query` or another library used for widely used for data
fetching.
```ts
import { useQuery } from "react-query";
import { Auction__factory } from "./typechain-generated";
const { data, isLoading, error, status } = useQuery(
"auction-calls",
async () => {
const auction = Auction__factory.connect(address, signer);
const [reward, token, latestBid] = await Promise.all([
auction.reward(),
auction.token(),
auction.latestBid(),
]);
return { reward, token, latestBid };
}
);
```
**[See example on TypeScript Playground](https://www.typescriptlang.org/play?jsx=4&ts=4.5.0-beta#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAORToCGAxjALQCOO+VAsAFC8nIAdk2ARBcAKIAPOuAA26AOrAYACwBK9Jtlx4AFAEpEvAJAB6M3EXo4DCMjkATFBji1GLdrrgB3Farh0NWZURwBrOBgIOC98OAAjOQgGMIZVOmBxTLg6SLwwdFQ6EhsfOjxTCzgAFVUbMHJE9EpgVAAaOHsoWzo5OVRfMSp4eJt4uhg09EcAOkrLTQ84HTjIPoGxUmS0DtRonxtHIfh0gDcbKLg5Vph0cRJoBKSUlDBHCcK5nhM7QVR4JDvGB0DqtAAyEDojkyAHMOrhyFBdsCYGgiHAALwuLAcfQAIjoyBEYmYDF6-TxHToqDwwjghkxAD5jN8fmJ-jkiTBROIsTCggBBLk8wymUxsv7wADatDKUEcHSiYTuHTkH3+ACFgI4ALqYnJlFRwAAK5BArXQM3Jeil4pMhOJghmsro8sMHTtDu5YhmSru7s9wp9atumu1otZOoMvHFtFRUHESBd8sVEGVglV6pgWuchFMhGjfG+ceQCbgQNyAH44AAeBroRkAKQAygB5AByM3+UFhwBI+grMxDhWz2o6ggccg6ACYDDWzPXmQAuOB4iFQ2EzLd43h5nhVZiHo-Hk+ns-nw8xnigSCwPIFFlwOBamHt5AgUZI3hPl9vj+4VpVDab9n2pdAAGExBgKAPGAngn0gwRoI8aoYL+DweTgp9W3OKAe0cQosLgcDyRwhFtUI3dSDNVcgjqKBUDxABuK8qgAcTuXAPmceICGqfIIPSTJeHQKRb3gAiGDVWhbDVVABiFR1AikW5BEcAYNTAxDkKYR9SCER1UBXBAQKfeJw1Mp8Em1AioBXbtYSIqy4FOXocBXX930-QCnKsiBcPwwpKxXMi8IogYADJEGoihgrgBzBBhOAAB8TTNC0awSmFmT3ZyDBXU0KAy7SYKYVC6HQx1GRYotnKkiAMD0Syn388iCNQOLQsCyLorIWL7Og2EUrSoqMEywbEpyyz8pG80xpKlC0KKKqassgjIFQFQ9FasL2riki+i68KZsKub0BrKVPP-KAdWqq9nOHMNHCa+DnM6ALwv20iPva6aCvSsbmrgKUso6K7PzB4BXy83A9SikzXre6zHFsgae0SmqkafVy5Hc58ob-T9Max7kQBHWQwA8gmYagYnnNyqy7tqqzk2enbuq+w6fsKE6AfOy7qeu27VsRyI039dnPpXA65CO9redG-msuFkDCBFsyLNF8yUdwNHHMsnG8fBgDUCAyzJb2kLuZ6pA+pAOKsuG06MqyqbXoVs6awWsqlowsQmZA+rGvN63OutuB4Zi+29cSp2+fG9HsqIECPeKqDSpgcrKu9QQA9e9aGq2i2gul762p5-7FZrY2oDzp9HtHZ6Q-LjrS65luU8rz3LJBia4Xx6HrshwfPzhvTnO11H4r7umrMN9AqZH3BZ6fUnyfARfCeXyyGbrtx0DlNnQ7b2XrdTsaa73v1BG24-iLL3aK9ml2+6ZvdeEk6SbF+Dl+RgRSc4rgZBiZkACeRMSAA)**
Built-in polling would satisfy the need for refreshing new data after block
changes, but we would lose other features already implemented in useDapp. (e.g.
We'd need to reach for [`ethcall`](https://github.com/Destiner/ethcall) for
multicalls.)
## Possible Solutions
### 1. _Zero Runtime_: Stronger types for existing useDApp hooks
_See the code in [**./src/zero-runtime.ts**](./src/zero-runtime.ts) or on
**[TypeScript Playground](https://tsplay.dev/WK80Gw)**_
Wide types are definitely the biggest problem with `useContractCalls` for me, so
let's try writing new types for it, leveraging type info generated by TypeChain,
but without changing the runtime implementation.
```ts
import { useContractCalls } from "@usedapp/core";
import { UseContractCalls } from "./src/zero-runtime.ts";
import { Dai, Dai__factory } from "./src/typechain";
const useDaiCalls = useContractCalls as UseContractCalls;
const daiInterface = Dai__factory.createInterface();
const daiAddress = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
export function useDaiBalance() {
// const results: [[BigNumber] | undefined, [number] | undefined];
const [balance, decimals] = useDaiCalls([
{
abi: daiInterface,
address: daiAddress,
method: "balanceOf",
args: ["0x2e465ddca6d2c6c81ce6f260ab13148d43e93371"],
},
{
abi: daiInterface,
address: daiAddress,
method: "decimals",
args: [],
},
]);
return (
balance?.[0] &&
decimals?.[0] &&
ethers.utils.formatUnits(balance[0], decimals[0])
);
}
```
I'll admit this isn't exactly _zero_-runtime, as we need the following assertion
to narrow `useContractCalls` type.
```ts
const useDaiCalls = useContractCalls as UseContractCalls;
```
Problem is, we can't easily infer parameter types with existing signature. Even
if we pass TypeChain's [`DaiInterface`](./src/typechain/Dai.d.ts) as `abi`
property, `Parameters["encodeFunctionData"]` returns only the
parameters of the first overload.
### 2. _Zero Codegen_: Ethers.js [Contract](https://docs.ethers.io/v5/api/contract/contract/) instance as first argument
If we allowed passing Contract instance with types generated by TypeChain as
first argument, we could infer method names and arguments from it in a similar
way to the above, and we could
```ts
import { Dai__factory } from "./src/typechain";
const daiInstance = Dai__factory.connect(address, signer);
useContractCalls(daiInstance, [
{
method: "balanceOf",
args: ["0x2e465ddca6d2c6c81ce6f260ab13148d43e93371"],
},
]);
```
This solution could be possible implemented inside of useDapp as it doesn't
require knowledge about user's contracts.
### 3. Calls as dictionary
_See the code in [**./src/call-as-dict.ts**](./src/calls-as-dict.ts) or on
**[TypeScript Playground](https://tsplay.dev/mbdjdw)**_
Assuming we'd never call the same contract method twice in one
`useContractCalls` invocation, our hook can accept an object of
`method -> args`.
```ts
const { balanceOf, decimals } = useDaiCalls({
balanceOf: ["0x2e465ddca6d2c6c81ce6f260ab13148d43e93371"],
decimals: [],
});
```
We can easily implement this by mapping over entries, passing them to
`useContractCalls` and collecting received results in a new object.
```ts
const useDaiCalls = (calls) => {
const methods = Object.keys(calls);
const results = useContractCalls(
methods.map((method) => ({
abi: daiInterface,
address: daiAddress,
method,
args: calls[method],
}))
);
return Object.fromEntries(results.map((result, i) => [methods[i], result]));
};
```
### 4. Lots of codegen
On TypeChain's side, we could generate a new hook for each possible call.
#### **Usage**
```ts
import { useDaiBalanceOf } from './typechain-dist';
const App() {
const balance = formatUnits(useDaiBalanceOf(address), 18);
}
```
#### **Potential generated code**
```ts
function useDaiBalanceOf(...args: [string | Falsy]): BigNumber | undefined {
return useContractCall({
abi: DAI_ABI,
addresss: DAI_ADDRESS,
method: "balanceOf",
args: args.some(isFalsy) ? undefined : args,
})?.[0];
}
```
---
## How to run this project?
Reading the types in VSCode should suffice, but to prove that the implementation
works and fiddle with it, you can run the development server.
1. Install dependencies
```sh
pnpm install
```
2. Start Hardhat Node
```
pnpm hardhat:node
```
3. Run development server
```
pnpm dev
```