Skip to content

Instantly share code, notes, and snippets.

@Mrtenz
Created May 29, 2021 14:29
Show Gist options
  • Save Mrtenz/8c2bf244c4ece6818e82a4febfbb9e60 to your computer and use it in GitHub Desktop.
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
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>>>;
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';
}
];
/**
* 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