Created
May 29, 2021 14:29
-
-
Save Mrtenz/8c2bf244c4ece6818e82a4febfbb9e60 to your computer and use it in GitHub Desktop.
Strictly typed contract interface from a standard JSON ABI interface, without code generation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { ERC20 } from './erc-20'; | |
import type { InputTypeMap, OutputTypeMap, TypeMapper } from './types'; | |
/** | |
* This creates an interface with all ERC-20 functions, strictly typed with TypeScript types. It does not require any | |
* code generation or pre-formatting of the ERC-20 ABI, simply pass in the standard ERC-20 as type, and it will work. | |
* | |
* For example: | |
*/ | |
type ERC20Contract = Contract<ERC20>; | |
declare const contract: ERC20Contract; | |
contract.name(); // () => string | |
contract.approve('0x0', 123); // (string, IntegerLike) => boolean | |
contract.transfer(123, '0x'); // Does not compile: `number` is not assignable to `string` | |
interface ABIFunctionInputType { | |
name: string; | |
type: string; | |
components?: ABIFunctionInputType[]; | |
} | |
interface ABIFunction { | |
type: string; | |
name?: string; | |
inputs?: ABIFunctionInputType[]; | |
outputs?: ABIFunctionInputType[]; | |
} | |
/** | |
* Filters non-functions (constructor, fallback, etc.) from the ABI interface. | |
*/ | |
type ABIFunctionsFilter<Interface extends ABIFunction[]> = { | |
[Key in keyof Interface]: Interface[Key] extends Interface[number] | |
? Interface[Key]['type'] extends 'function' | |
? Interface[Key] | |
: never | |
: never; | |
}; | |
/** | |
* Get the name of a function from an ABI function interface. | |
*/ | |
type ABIFunctionName<Fn extends ABIFunction> = Fn['name'] extends string ? Fn['name'] : never; | |
/** | |
* Maps an array of ABI functions to an object with the ABI function name as key, and the ABI function interface as | |
* value. | |
*/ | |
type ABIFunctionMap<Interface extends ABIFunction[]> = { | |
[Key in keyof Interface as Interface[Key] extends Interface[number] | |
? ABIFunctionName<Interface[Key]> | |
: never]: Interface[Exclude<Key, number>]; | |
}; | |
/** | |
* Maps an array of ABI function inputs (or outputs) to an array of types (as string). | |
*/ | |
type ABIFunctionTypes<P extends ABIFunctionInputType[]> = { | |
[K in keyof P]: P[K] extends P[number] | |
? // Note: it uses `tuple${string}` here to support tuples with a fixed length, e.g. `tuple[2]` | |
P[K]['type'] extends `tuple${string}` | |
? ABIFunctionTuple<P[K]> | |
: P[K]['type'] | |
: never; | |
}; | |
/** | |
* Uses the type above to parse tuple components to an array of types (as string). | |
*/ | |
type ABIFunctionTuple<Input extends ABIFunctionInputType> = Input['components'] extends ABIFunctionInputType[] | |
? ABIFunctionTypes<Input['components']> | |
: never; | |
/** | |
* Maps the array of Solidity types (as string) to an array of TypeScript types, e.g. `bytes` is mapped to | |
* `BytesLike`. | |
*/ | |
type ABIFunctionArgs< | |
Output extends ABIFunctionInputType[] | undefined, | |
TypeMap = InputTypeMap, | |
Types = Output extends ABIFunctionInputType[] ? ABIFunctionTypes<Output> : [], | |
Args = Types extends unknown[] ? TypeMapper<Types, TypeMap> : void | |
> = Args extends [infer Arg] ? Arg : Args; | |
/** | |
* Maps an object of ABI function interfaces to an object of JavaScript functions, with the function arguments and | |
* return value typed. | |
*/ | |
type ABIFunctions<Interface extends Record<string, ABIFunction>> = { | |
[Key in keyof Interface]: Interface[Key]['inputs'] extends ABIFunctionInputType[] | |
? ABIFunctionTypes<Interface[Key]['inputs']> extends unknown[] | |
? ( | |
...args: ABIFunctionArgs<Interface[Key]['inputs']> | |
) => ABIFunctionArgs<Interface[Key]['outputs'], OutputTypeMap> | |
: never | |
: never; | |
}; | |
type Contract<Interface extends ABIFunction[]> = ABIFunctions<ABIFunctionMap<ABIFunctionsFilter<Interface>>>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type ERC20 = [ | |
{ | |
constant: true; | |
inputs: []; | |
name: 'name'; | |
outputs: [ | |
{ | |
name: ''; | |
type: 'string'; | |
} | |
]; | |
payable: false; | |
stateMutability: 'view'; | |
type: 'function'; | |
}, | |
{ | |
constant: false; | |
inputs: [ | |
{ | |
name: '_spender'; | |
type: 'address'; | |
}, | |
{ | |
name: '_value'; | |
type: 'uint256'; | |
} | |
]; | |
name: 'approve'; | |
outputs: [ | |
{ | |
name: ''; | |
type: 'bool'; | |
} | |
]; | |
payable: false; | |
stateMutability: 'nonpayable'; | |
type: 'function'; | |
}, | |
{ | |
constant: true; | |
inputs: []; | |
name: 'totalSupply'; | |
outputs: [ | |
{ | |
name: ''; | |
type: 'uint256'; | |
} | |
]; | |
payable: false; | |
stateMutability: 'view'; | |
type: 'function'; | |
}, | |
{ | |
constant: false; | |
inputs: [ | |
{ | |
name: '_from'; | |
type: 'address'; | |
}, | |
{ | |
name: '_to'; | |
type: 'address'; | |
}, | |
{ | |
name: '_value'; | |
type: 'uint256'; | |
} | |
]; | |
name: 'transferFrom'; | |
outputs: [ | |
{ | |
name: ''; | |
type: 'bool'; | |
} | |
]; | |
payable: false; | |
stateMutability: 'nonpayable'; | |
type: 'function'; | |
}, | |
{ | |
constant: true; | |
inputs: []; | |
name: 'decimals'; | |
outputs: [ | |
{ | |
name: ''; | |
type: 'uint8'; | |
} | |
]; | |
payable: false; | |
stateMutability: 'view'; | |
type: 'function'; | |
}, | |
{ | |
constant: true; | |
inputs: [ | |
{ | |
name: '_owner'; | |
type: 'address'; | |
} | |
]; | |
name: 'balanceOf'; | |
outputs: [ | |
{ | |
name: 'balance'; | |
type: 'uint256'; | |
} | |
]; | |
payable: false; | |
stateMutability: 'view'; | |
type: 'function'; | |
}, | |
{ | |
constant: true; | |
inputs: []; | |
name: 'symbol'; | |
outputs: [ | |
{ | |
name: ''; | |
type: 'string'; | |
} | |
]; | |
payable: false; | |
stateMutability: 'view'; | |
type: 'function'; | |
}, | |
{ | |
constant: false; | |
inputs: [ | |
{ | |
name: '_to'; | |
type: 'address'; | |
}, | |
{ | |
name: '_value'; | |
type: 'uint256'; | |
} | |
]; | |
name: 'transfer'; | |
outputs: [ | |
{ | |
name: ''; | |
type: 'bool'; | |
} | |
]; | |
payable: false; | |
stateMutability: 'nonpayable'; | |
type: 'function'; | |
}, | |
{ | |
constant: true; | |
inputs: [ | |
{ | |
name: '_owner'; | |
type: 'address'; | |
}, | |
{ | |
name: '_spender'; | |
type: 'address'; | |
} | |
]; | |
name: 'allowance'; | |
outputs: [ | |
{ | |
name: ''; | |
type: 'uint256'; | |
} | |
]; | |
payable: false; | |
stateMutability: 'view'; | |
type: 'function'; | |
}, | |
{ | |
payable: true; | |
stateMutability: 'payable'; | |
type: 'fallback'; | |
}, | |
{ | |
anonymous: false; | |
inputs: [ | |
{ | |
indexed: true; | |
name: 'owner'; | |
type: 'address'; | |
}, | |
{ | |
indexed: true; | |
name: 'spender'; | |
type: 'address'; | |
}, | |
{ | |
indexed: false; | |
name: 'value'; | |
type: 'uint256'; | |
} | |
]; | |
name: 'Approval'; | |
type: 'event'; | |
}, | |
{ | |
anonymous: false; | |
inputs: [ | |
{ | |
indexed: true; | |
name: 'from'; | |
type: 'address'; | |
}, | |
{ | |
indexed: true; | |
name: 'to'; | |
type: 'address'; | |
}, | |
{ | |
indexed: false; | |
name: 'value'; | |
type: 'uint256'; | |
} | |
]; | |
name: 'Transfer'; | |
type: 'event'; | |
} | |
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Utility types that map a Solidity ABI type to a TypeScript compatible type. If a type is not supported, it uses | |
* `unknown` instead. Support for dynamic types is limited, something like `bytes[3]` is currently not parsed. | |
* | |
* This is based on the implementation in [`@findeth/abi`](https://github.com/FindETH/abi). | |
*/ | |
/* prettier-disable */ | |
type ByteLength = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32; | |
type IntegerLength = 8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 88 | 96 | 104 | 112 | 120 | 128 | 136 | 144 | 152 | 160 | 168 | 176 | 184 | 192 | 200 | 208 | 216 | 224 | 232 | 240 | 248 | 256; | |
/* prettier-enable */ | |
type Bytes = `bytes${ByteLength}`; | |
type BytesLike = string | Uint8Array; | |
type Integer = `int${IntegerLength}`; | |
type UnsignedInteger = `uint${IntegerLength}`; | |
type IntegerLike = number | bigint; | |
export type Type = keyof OutputTypeMap; | |
export type TypeMapper<I extends unknown[], T = OutputTypeMap> = Mapper<T, I>; | |
/** | |
* An object type with most possible ABI types, and their respective TypeScript type. Note that some dynamic types, like | |
* `<type>[<length>]` and `fixed<M>x<N>` are not supported, and `unknown` is used instead. | |
*/ | |
export type OutputTypeMap = WithArrayTypes<MapToOutput<TypeMap>>; | |
/** | |
* An object type with most possible ABI types, and their respective TypeScript type. Note that some dynamic types, like | |
* `<type>[<length>]` and `fixed<M>x<N>` are not supported, and `unknown` is used instead. | |
* | |
* Accepts multiple input types for certain ABI types, like strings, bytes, numbers. | |
*/ | |
export type InputTypeMap = WithArrayTypes<MapToInput<TypeMap>>; | |
/** | |
* Generic type map which is used to generate the input and output type map. | |
*/ | |
type TypeMap = { | |
address: [string, string]; | |
bool: [boolean, boolean]; | |
bytes: [BytesLike, Uint8Array]; | |
function: [BytesLike, Uint8Array]; | |
int: [IntegerLike, bigint]; | |
string: [string, string]; | |
uint: [IntegerLike, bigint]; | |
} & DynamicType<Bytes, [BytesLike, Uint8Array]> & | |
DynamicType<Integer, [IntegerLike, bigint]> & | |
DynamicType<UnsignedInteger, [IntegerLike, bigint]>; | |
/** | |
* Helper type to generate an object type from a union. | |
*/ | |
type DynamicType<K extends string, T> = { | |
[key in K]: T; | |
}; | |
/** | |
* Helper type that maps the types to the types in the type map. | |
*/ | |
type Mapper<TypeMap, Types extends unknown[]> = { | |
[Key in keyof Types]: Types[Key] extends Types[number] | |
? Types[Key] extends unknown[] | |
? Mapper<TypeMap, Types[Key]> | |
: Types[Key] extends keyof TypeMap | |
? TypeMap[Types[Key]] | |
: unknown | |
: unknown; | |
}; | |
/** | |
* Helper type that maps a tuple to the first element. | |
*/ | |
export type MapToInput<T extends Record<string, [unknown, unknown]>> = { | |
[K in keyof T]: T[K][0]; | |
}; | |
/** | |
* Helper type that maps a tuple to the second element. | |
*/ | |
export type MapToOutput<T extends Record<string, [unknown, unknown]>> = { | |
[K in keyof T]: T[K][1]; | |
}; | |
/** | |
* Helper type that adds an array type for each of the specified keys and types. | |
*/ | |
type WithArrayTypes<T> = T & | |
{ | |
[K in keyof T as `${string & K}[]`]: Array<T[K]>; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment