Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add custom struct type matching to Register via structTypeMatches #251

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sour-buckets-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"abitype": patch
---

Introduce `structTypeMatches` to `Register`, allowing user defined types to be matched against structs before they are processed as primitives.
10 changes: 10 additions & 0 deletions packages/abitype/src/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ export type SolidityArrayWithoutTuple = _BuildArrayTypes<
export type SolidityArrayWithTuple = _BuildArrayTypes<SolidityTuple>
export type SolidityArray = SolidityArrayWithoutTuple | SolidityArrayWithTuple

////////////////////////////////////////////////////////////////////////////////////////////////////
// User Defined Struct Types

type ResolvedStructTypeMatches = ResolvedRegister['structTypeMatches']
export type ResolvedStructMatchesMap = {
[P in keyof ResolvedStructTypeMatches &
string as `struct ${P}`]: ResolvedStructTypeMatches[P]
}
export type ResolvedStructMatchesUnion = keyof ResolvedStructMatchesMap

////////////////////////////////////////////////////////////////////////////////////////////////////
// Abi Types

Expand Down
2 changes: 2 additions & 0 deletions packages/abitype/src/register.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ test('ResolvedRegister', () => {

type StrictAbiType = ResolvedRegister['strictAbiType']
assertType<StrictAbiType>(false)

assertType<ResolvedRegister['structTypeMatches']>({})
})
15 changes: 15 additions & 0 deletions packages/abitype/src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ export type ResolvedRegister = {
? type
: DefaultRegister['fixedArrayMaxLength']

/**
* Custom Struct types matched against `AbiParameter.internalType`
*
* Note: format is Record<struct_name, matched_type>
* @default {}
*/
structTypeMatches: Register extends {
structTypeMatches: infer type extends Record<string, any>
}
? type
: DefaultRegister['structTypeMatches']

/**
* When set, validates {@link AbiParameter}'s `type` against {@link AbiType}
*
Expand Down Expand Up @@ -142,6 +154,9 @@ export type DefaultRegister = {
/** TypeScript type to use for `int<M>` and `uint<M>` values, where `M <= 48` */
intType: number

/** Typescript types to use for custom structs */
structTypeMatches: {}

/** When set, validates {@link AbiParameter}'s `type` against {@link AbiType} */
strictAbiType: false

Expand Down
53 changes: 30 additions & 23 deletions packages/abitype/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type {
AbiStateMutability,
AbiType,
MBits,
ResolvedStructMatchesMap,
ResolvedStructMatchesUnion,
SolidityArray,
SolidityBytes,
SolidityFixedArrayRange,
Expand Down Expand Up @@ -79,33 +81,38 @@ type BitsTypeLookup = {
* @returns TypeScript primitive type
*/
export type AbiParameterToPrimitiveType<
abiParameter extends AbiParameter | { name: string; type: unknown },
abiParameter extends
| AbiParameter
| { name: string; type: unknown; internalType?: unknown },
abiParameterKind extends AbiParameterKind = AbiParameterKind,
// 1. Check to see if type is basic (not tuple or array) and can be looked up immediately.
> = abiParameter['type'] extends AbiBasicType
? AbiTypeToPrimitiveType<abiParameter['type'], abiParameterKind>
: // 2. Check if type is tuple and covert each component
abiParameter extends {
type: SolidityTuple
components: infer components extends readonly AbiParameter[]
}
? AbiComponentsToPrimitiveType<components, abiParameterKind>
: // 3. Check if type is array.
MaybeExtractArrayParameterType<abiParameter['type']> extends [
infer head extends string,
infer size,
]
? AbiArrayToPrimitiveType<abiParameter, abiParameterKind, head, size>
: // 4. If type is not basic, tuple, or array, we don't know what the type is.
// This can happen when a fixed-length array is out of range (`Size` doesn't exist in `SolidityFixedArraySizeLookup`),
// the array has depth greater than `ResolvedRegister['arrayMaxDepth']`, etc.
ResolvedRegister['strictAbiType'] extends true
? Error<`Unknown type '${abiParameter['type'] & string}'.`>
: // 5. If we've gotten this far, let's check for errors in tuple components.
// (Happens for recursive tuple typed data types.)
abiParameter extends { components: Error<string> }
? abiParameter['components']
: unknown
: // 2. Check if internalType matches user defined struct matches
abiParameter['internalType'] extends ResolvedStructMatchesUnion
? ResolvedStructMatchesMap[abiParameter['internalType']]
: // 3. Check if type is tuple and covert each component
abiParameter extends {
type: SolidityTuple
components: infer components extends readonly AbiParameter[]
}
? AbiComponentsToPrimitiveType<components, abiParameterKind>
: // 4. Check if type is array.
MaybeExtractArrayParameterType<abiParameter['type']> extends [
infer head extends string,
infer size,
]
? AbiArrayToPrimitiveType<abiParameter, abiParameterKind, head, size>
: // 5. If type is not basic, tuple, or array, we don't know what the type is.
// This can happen when a fixed-length array is out of range (`Size` doesn't exist in `SolidityFixedArraySizeLookup`),
// the array has depth greater than `ResolvedRegister['arrayMaxDepth']`, etc.
ResolvedRegister['strictAbiType'] extends true
? Error<`Unknown type '${abiParameter['type'] & string}'.`>
: // 6. If we've gotten this far, let's check for errors in tuple components.
// (Happens for recursive tuple typed data types.)
abiParameter extends { components: Error<string> }
? abiParameter['components']
: unknown

type AbiBasicType = Exclude<AbiType, SolidityTuple | SolidityArray>

Expand Down
57 changes: 57 additions & 0 deletions packages/register-tests/default/src/structTypeMatches.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { AbiParametersToPrimitiveTypes, ResolvedRegister } from 'abitype'
import { assertType, expectTypeOf, test } from 'vitest'

type EncryptedUint8 = {
data: `0x${string}`
}
type EncryptedUint16 = {
data: `0x${string}`
}

declare module 'abitype' {
interface Register {
structTypeMatches: {
eUint8: EncryptedUint8
eUint16: EncryptedUint16
}
}
}

test('Register structTypeMatches', () => {
type StructTypeMatches = ResolvedRegister['structTypeMatches']
assertType<StructTypeMatches>({
eUint8: { data: '0xfoobarbaz' },
eUint16: { data: '0xfoobarbaz' },
})

type Result = AbiParametersToPrimitiveTypes<
[
{
components: [
{
internalType: 'bytes'
name: 'data'
type: 'bytes'
},
]
internalType: 'struct eUint8'
name: 'encryptedA8'
type: 'tuple'
},
{
components: [
{
internalType: 'bytes'
name: 'data'
type: 'bytes'
},
]
internalType: 'struct eUint16'
name: 'encryptedB16'
type: 'tuple'
},
]
>
assertType<Result>([{ data: '0xfoobarbaz' }, { data: '0xfoobarbaz' }])
expectTypeOf<Result>().toEqualTypeOf<[EncryptedUint8, EncryptedUint16]>()
})