diff --git a/.changeset/cuddly-donkeys-protect.md b/.changeset/cuddly-donkeys-protect.md new file mode 100644 index 00000000..fe988945 --- /dev/null +++ b/.changeset/cuddly-donkeys-protect.md @@ -0,0 +1,6 @@ +--- +"abitype": minor +--- + +Adds parameter validation to `parseAbiParameter` and `parseAbiParameters`. +Changes `StrictAbiType` to `Strict`. diff --git a/docs/api/human.md b/docs/api/human.md index c44e8d3e..dd519558 100644 --- a/docs/api/human.md +++ b/docs/api/human.md @@ -8,6 +8,7 @@ title: 'Human-Readable ABI' Human-Readable ABIs compress [JSON ABIs](https://docs.soliditylang.org/en/latest/abi-spec.html#json) into signatures that are nicer to read and less verbose to write. For example: ::: code-group + ```ts [human-readable.ts] const abi = [ 'constructor()', @@ -51,6 +52,7 @@ const abi = [ { inputs: [], name: 'ApprovalCallerNotOwnerNorApproved', type: 'error' }, ] as const ``` + ::: ABIType contains parallel [type-level](/api/human#types) and [runtime](/api/human#utilities) utilities for parsing and formatting human-readable ABIs, ABI items, and ABI parameters. @@ -477,6 +479,10 @@ const abiParameterStruct = parseAbiParameter([ ]) ``` +::: info PARAMETER VALIDATION +When passing a string array as an argument typescript will verify if the parameter strings are valid to safeguard you from the runtime behavior. When strict is disabled the type-checking will be more lax. If you enable strict it will check if the Solidity types are valid. +::: + ### `parseAbiParameters` Parses human-readable ABI parameters into [`AbiParameter`s](/api/types#abiparameter). @@ -503,6 +509,13 @@ const abiParametersStruct = parseAbiParameters([ ]) ``` +<<<<<<< HEAD +::: info PARAMETER VALIDATION +When passing a string array as an argument typescript will verify if the parameter strings are valid to safeguard you from the runtime behavior. When strict is disabled the type-checking will be more lax. If you enable strict it will check if the Solidity types are valid. +::: + +||||||| 24256d0 +======= ### `formatAbi` Formats [`Abi`](/api/types#abi) into human-readable ABI. @@ -602,6 +615,7 @@ const result = formatAbiParameters([ ``` +>>>>>>> main ## Errors ```ts twoslash diff --git a/docs/config.md b/docs/config.md index 7433f490..182aa737 100644 --- a/docs/config.md +++ b/docs/config.md @@ -151,9 +151,9 @@ declare module 'abitype' { } ``` -### `StrictAbiType` +### `Strict` -When set, validates `AbiParameter`'s `type` against `AbiType`. +When set, validates `AbiParameter`'s `type` against `AbiType` and enforces stricter checks on human readable ABIs. - Type `boolean` - Default `false` @@ -161,7 +161,7 @@ When set, validates `AbiParameter`'s `type` against `AbiType`. ```ts twoslash declare module 'abitype' { export interface Config { - StrictAbiType: false + Strict: false } } ``` diff --git a/src/abi.ts b/src/abi.ts index 440c3a2b..b35b5707 100644 --- a/src/abi.ts +++ b/src/abi.ts @@ -9,14 +9,14 @@ export type Address = ResolvedConfig['AddressType'] // Could use `Range`, but listed out for zero overhead // rome-ignore format: no formatting export type MBytes = - | '' | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 + | '' | 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 // rome-ignore format: no formatting export type MBits = - | '' | 8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 | 72 - | 80 | 88 | 96 | 104 | 112 | 120 | 128 | 136 | 144 | 152 + | '' | 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 @@ -84,16 +84,16 @@ export type AbiType = | SolidityInt | SolidityString | SolidityTuple -type ResolvedAbiType = ResolvedConfig['StrictAbiType'] extends true - ? AbiType +type ResolvedAbiType = ResolvedConfig['Strict'] extends true + ? AbiType & string : string export type AbiInternalType = | ResolvedAbiType | `address ${string}` | `contract ${string}` - | `enum ${string}` - | `struct ${string}` + | `enum${string}` + | `struct${string}` export type AbiParameter = Pretty< { @@ -247,3 +247,103 @@ export type TypedData = Pretty< [_ in TypedDataType]?: never | undefined } > + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Inferred Abi Types + +export type InferredAbiParameter = Pretty< + { + type: string + name?: string | undefined + /** Representation used by Solidity compiler */ + internalType?: string | undefined + } & ( + | { type: string } + | { + type: 'tuple' | `tuple[${string}]` + components: readonly InferredAbiParameter[] + } + ) +> + +export type InferredAbiEventParameter = Pretty< + InferredAbiParameter & { indexed?: boolean | undefined } +> + +/** Typescript inferred ABI ["function"](https://docs.soliditylang.org/en/latest/abi-spec.html#json) type */ +export type InferredAbiFunction = { + type: string + /** + * @deprecated use `pure` or `view` from {@link AbiStateMutability} instead + * @see https://github.com/ethereum/solidity/issues/992 + */ + constant?: boolean | undefined + /** + * @deprecated Vyper used to provide gas estimates + * @see https://github.com/vyperlang/vyper/issues/2151 + */ + gas?: number | undefined + inputs: readonly InferredAbiParameter[] + name: string + outputs: readonly InferredAbiParameter[] + /** + * @deprecated use `payable` or `nonpayable` from {@link AbiStateMutability} instead + * @see https://github.com/ethereum/solidity/issues/992 + */ + payable?: boolean | undefined + stateMutability: string +} + +/** Typescript inferred ABI ["constructor"](https://docs.soliditylang.org/en/latest/abi-spec.html#json) type */ +export type InferredAbiConstructor = { + type: string + inputs: readonly InferredAbiParameter[] + /** + * @deprecated use `payable` or `nonpayable` from {@link AbiStateMutability} instead + * @see https://github.com/ethereum/solidity/issues/992 + */ + payable?: boolean | undefined + stateMutability: string +} + +/** Typescript inferred ABI ["fallback"](https://docs.soliditylang.org/en/latest/abi-spec.html#json) type */ +export type InferredAbiFallback = { + type: string + inputs?: [] | undefined + /** + * @deprecated use `payable` or `nonpayable` from {@link AbiStateMutability} instead + * @see https://github.com/ethereum/solidity/issues/992 + */ + payable?: boolean | undefined + stateMutability: string +} + +/** Typescript inferred ABI ["receive"](https://docs.soliditylang.org/en/latest/contracts.html#receive-ether-function) type */ +export type InferredAbiReceive = { + type: string + stateMutability: string +} + +/** Typescript inferred ABI ["event"](https://docs.soliditylang.org/en/latest/abi-spec.html#events) type */ +export type InferredAbiEvent = { + type: string + anonymous?: boolean | undefined + inputs: readonly InferredAbiEventParameter[] + name: string +} + +/** Typescript inferred ABI ["error"](https://docs.soliditylang.org/en/latest/abi-spec.html#errors) type */ +export type InferredAbiError = { + type: string + inputs: readonly InferredAbiParameter[] + name: string +} + +export type InferredAbi = readonly ( + | InferredAbiReceive + | InferredAbiFallback + | InferredAbiConstructor + | InferredAbiError + | InferredAbiEvent + | InferredAbiFunction +)[] diff --git a/src/config.test-d.ts b/src/config.test-d.ts index 8339c8be..b9a02382 100644 --- a/src/config.test-d.ts +++ b/src/config.test-d.ts @@ -3,9 +3,9 @@ import { assertType, test } from 'vitest' import type { ResolvedConfig } from './config.js' // For testing updates to config properties: -// declare module './config' { +// declare module './config.js' { // export interface Config { -// FixedArrayMaxLength: 6 +// Strict: true // } // } @@ -29,6 +29,6 @@ test('Config', () => { type BigIntType = ResolvedConfig['BigIntType'] assertType(123n) - type StrictAbiType = ResolvedConfig['StrictAbiType'] - assertType(false) + type Strict = ResolvedConfig['Strict'] + assertType(false) }) diff --git a/src/config.ts b/src/config.ts index c576b14c..c36eaaa1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -39,8 +39,8 @@ export interface DefaultConfig { /** TypeScript type to use for `int` and `uint` values, where `M <= 48` */ IntType: number - /** When set, validates {@link AbiParameter}'s `type` against {@link AbiType} */ - StrictAbiType: false + /** When set, validates {@link AbiParameter}'s `type` against {@link AbiType} and enforces stricter checks on human readable ABIs */ + Strict: false } /** @@ -110,14 +110,14 @@ export interface ResolvedConfig { : Config['IntType'] /** - * When set, validates {@link AbiParameter}'s `type` against {@link AbiType} + * When set, validates {@link AbiParameter}'s `type` against {@link AbiType} and enforces stricter checks on human readable ABIs * * Note: You probably only want to set this to `true` if parsed types are returning as `unknown` - * and you want to figure out why. + * and you want to figure out why or if you want stricter validation on your types. * * @default false */ - StrictAbiType: Config['StrictAbiType'] extends true - ? Config['StrictAbiType'] - : DefaultConfig['StrictAbiType'] + Strict: Config['Strict'] extends true + ? Config['Strict'] + : DefaultConfig['Strict'] } diff --git a/src/human-readable/errors/abiParameter.test.ts b/src/human-readable/errors/abiParameter.test.ts index 88f54b8a..4917dc18 100644 --- a/src/human-readable/errors/abiParameter.test.ts +++ b/src/human-readable/errors/abiParameter.test.ts @@ -115,7 +115,7 @@ test('InvalidFunctionModifierError', () => { test('InvalidAbiTypeParameterError', () => { expect( new InvalidAbiTypeParameterError({ - abiParameter: { type: 'addres' }, + abiParameter: { type: 'address' }, }), ).toMatchInlineSnapshot(` [InvalidAbiTypeParameterError: Invalid ABI parameter. @@ -123,7 +123,7 @@ test('InvalidAbiTypeParameterError', () => { ABI parameter type is invalid. Details: { - "type": "addres" + "type": "address" } Version: abitype@x.y.z] `) diff --git a/src/human-readable/errors/abiParameter.ts b/src/human-readable/errors/abiParameter.ts index e91ada88..9ffb85b0 100644 --- a/src/human-readable/errors/abiParameter.ts +++ b/src/human-readable/errors/abiParameter.ts @@ -1,4 +1,4 @@ -import type { AbiItemType, AbiParameter } from '../../abi.js' +import type { AbiItemType, InferredAbiEventParameter } from '../../abi.js' import { BaseError } from '../../errors.js' import type { Modifier } from '../types/signatures.js' @@ -100,7 +100,7 @@ export class InvalidAbiTypeParameterError extends BaseError { constructor({ abiParameter, }: { - abiParameter: AbiParameter & { indexed?: boolean | undefined } + abiParameter: InferredAbiEventParameter }) { super('Invalid ABI parameter.', { details: JSON.stringify(abiParameter, null, 2), diff --git a/src/human-readable/formatAbiParameter.test-d.ts b/src/human-readable/formatAbiParameter.test-d.ts index 145bc32e..b8c4e506 100644 --- a/src/human-readable/formatAbiParameter.test-d.ts +++ b/src/human-readable/formatAbiParameter.test-d.ts @@ -1,6 +1,6 @@ import { expectTypeOf, test } from 'vitest' -import type { AbiParameter } from '../abi.js' +import type { InferredAbiParameter } from '../abi.js' import type { FormatAbiParameter } from './formatAbiParameter.js' import { formatAbiParameter } from './formatAbiParameter.js' @@ -57,7 +57,7 @@ test('formatAbiParameter', () => { ).toEqualTypeOf<'(string)'>() const param = { type: 'address' } - const param2: AbiParameter = param + const param2: InferredAbiParameter = param expectTypeOf(formatAbiParameter(param)).toEqualTypeOf() expectTypeOf(formatAbiParameter(param2)).toEqualTypeOf() }) diff --git a/src/human-readable/formatAbiParameter.ts b/src/human-readable/formatAbiParameter.ts index 98a42348..444b2e95 100644 --- a/src/human-readable/formatAbiParameter.ts +++ b/src/human-readable/formatAbiParameter.ts @@ -1,4 +1,8 @@ -import type { AbiEventParameter, AbiParameter } from '../abi.js' +import type { + AbiParameter, + InferredAbiEventParameter, + InferredAbiParameter, +} from '../abi.js' import { execTyped } from '../regex.js' import type { Join } from '../types.js' @@ -13,7 +17,7 @@ import type { Join } from '../types.js' * // ^? type Result = 'address from' */ export type FormatAbiParameter< - TAbiParameter extends AbiParameter | AbiEventParameter, + TAbiParameter extends InferredAbiParameter | InferredAbiParameter, > = TAbiParameter extends { name?: infer Name extends string type: `tuple${infer Array}` @@ -51,7 +55,7 @@ const tupleRegex = /^tuple(?(\[(\d*)\])*)$/ * // ^? const result: 'address from' */ export function formatAbiParameter< - const TAbiParameter extends AbiParameter | AbiEventParameter, + const TAbiParameter extends InferredAbiParameter | InferredAbiEventParameter, >(abiParameter: TAbiParameter): FormatAbiParameter { type Result = FormatAbiParameter diff --git a/src/human-readable/formatAbiParameters.test-d.ts b/src/human-readable/formatAbiParameters.test-d.ts index 873e7221..f861c0a0 100644 --- a/src/human-readable/formatAbiParameters.test-d.ts +++ b/src/human-readable/formatAbiParameters.test-d.ts @@ -1,6 +1,6 @@ import { expectTypeOf, test } from 'vitest' -import type { AbiParameter } from '../abi.js' +import type { InferredAbiParameter } from '../abi.js' import type { FormatAbiParameters } from './formatAbiParameters.js' import { formatAbiParameters } from './formatAbiParameters.js' @@ -78,7 +78,7 @@ test('formatAbiParameter', () => { ).toEqualTypeOf<'(string)'>() const param = { type: 'address' } - const param2: AbiParameter = param + const param2: InferredAbiParameter = param expectTypeOf(formatAbiParameters([param])).toEqualTypeOf() diff --git a/src/human-readable/formatAbiParameters.ts b/src/human-readable/formatAbiParameters.ts index 28d1a341..5765d1d6 100644 --- a/src/human-readable/formatAbiParameters.ts +++ b/src/human-readable/formatAbiParameters.ts @@ -1,4 +1,4 @@ -import type { AbiEventParameter, AbiParameter } from '../abi.js' +import type { InferredAbiEventParameter, InferredAbiParameter } from '../abi.js' import type { Join } from '../types.js' import { type FormatAbiParameter, @@ -20,8 +20,8 @@ import { */ export type FormatAbiParameters< TAbiParameters extends readonly [ - AbiParameter | AbiEventParameter, - ...(readonly (AbiParameter | AbiEventParameter)[]), + InferredAbiParameter | InferredAbiEventParameter, + ...(readonly (InferredAbiParameter | InferredAbiEventParameter)[]), ], > = Join< { @@ -45,8 +45,8 @@ export type FormatAbiParameters< */ export function formatAbiParameters< const TAbiParameters extends readonly [ - AbiParameter | AbiEventParameter, - ...(readonly (AbiParameter | AbiEventParameter)[]), + InferredAbiParameter | InferredAbiEventParameter, + ...(readonly (InferredAbiParameter | InferredAbiEventParameter)[]), ], >(abiParameters: TAbiParameters): FormatAbiParameters { let params = '' diff --git a/src/human-readable/parseAbiParameter.test-d.ts b/src/human-readable/parseAbiParameter.test-d.ts index 0606b1e6..aa6b030b 100644 --- a/src/human-readable/parseAbiParameter.test-d.ts +++ b/src/human-readable/parseAbiParameter.test-d.ts @@ -1,10 +1,36 @@ import { expectTypeOf, test } from 'vitest' import type { AbiParameter } from '../abi.js' - -import type { ParseAbiParameter } from './parseAbiParameter.js' +import type { Flatten } from '../types.js' +import type { + ParseAbiParameter, + ValidateAbiParameter, +} from './parseAbiParameter.js' import { parseAbiParameter } from './parseAbiParameter.js' +test('ValidateAbiParameters', () => { + expectTypeOf>().toEqualTypeOf< + [ + 'Error: No Struct signature found. Please provide valid struct signatures.', + ] + >() + expectTypeOf< + ValidateAbiParameter<['struct Foo { string name; }', 'address']> + >().toEqualTypeOf<[]>() + expectTypeOf< + Flatten< + ValidateAbiParameter<['struct Foo { string name; }', 'address, address']> + > + >().toEqualTypeOf< + [ + 'Error: Invalid Parameter "address, address". Please use "parseAbiParameters" for comma seperated strings.', + ] + >() + // expectTypeOf< + // Flatten> + // >().toEqualTypeOf<["Error: Invalid Parameter \"Bar\". No valid type."]>(); +}) + test('ParseAbiParameter', () => { expectTypeOf>().toEqualTypeOf() expectTypeOf>().toEqualTypeOf() @@ -22,6 +48,7 @@ test('ParseAbiParameter', () => { readonly name: 'from' readonly indexed: true }>() + // This will fail in strict mode expectTypeOf>().toEqualTypeOf<{ readonly type: 'address' readonly name: 'foo' @@ -56,9 +83,24 @@ test('parseAbiParameter', () => { // @ts-expect-error empty array not allowed expectTypeOf(parseAbiParameter([])).toEqualTypeOf() expectTypeOf( + // @ts-expect-error no parameter passed parseAbiParameter(['struct Foo { string name; }']), ).toEqualTypeOf() + expectTypeOf( + parseAbiParameter(['struct Foo { string name; }', 'address']), + ).toEqualTypeOf<{ readonly type: 'address' }>() + + expectTypeOf( + // This will fail in strict mode + parseAbiParameter(['struct Foo { string name; }', 'Bar']), + ).toEqualTypeOf<{ readonly type: 'Bar' }>() + + // expectTypeOf( + // // @ts-expect-error not a valid type + // parseAbiParameter(["struct Foo { string name; }", "Bar"]) + // ).toEqualTypeOf(); + expectTypeOf(parseAbiParameter('(string)')).toEqualTypeOf<{ readonly type: 'tuple' readonly components: readonly [{ readonly type: 'string' }] diff --git a/src/human-readable/parseAbiParameter.test.ts b/src/human-readable/parseAbiParameter.test.ts index c53091df..a37f142c 100644 --- a/src/human-readable/parseAbiParameter.test.ts +++ b/src/human-readable/parseAbiParameter.test.ts @@ -22,6 +22,7 @@ test('parseAbiParameter', () => { `, ) expect(() => + // @ts-expect-error no parameter passed parseAbiParameter(['struct Foo { string name; }']), ).toThrowErrorMatchingInlineSnapshot( ` diff --git a/src/human-readable/parseAbiParameter.ts b/src/human-readable/parseAbiParameter.ts index 15d813cd..970e3e18 100644 --- a/src/human-readable/parseAbiParameter.ts +++ b/src/human-readable/parseAbiParameter.ts @@ -1,14 +1,59 @@ import type { AbiParameter } from '../abi.js' import { InvalidAbiParameterError } from '../index.js' import type { Narrow } from '../narrow.js' -import type { Error, Filter } from '../types.js' +import type { Error, Filter, Flatten, IsEmptyObject } from '../types.js' import { isStructSignature, modifiers } from './runtime/signatures.js' import { parseStructs } from './runtime/structs.js' import { parseAbiParameter as parseAbiParameter_ } from './runtime/utils.js' -import type { IsStructSignature, Modifier } from './types/signatures.js' +import type { + CountStructSignatures, + IsStructSignature, + Modifier, + StructSignature, + ValidateParameterString, +} from './types/signatures.js' import type { ParseStructs } from './types/structs.js' import type { ParseAbiParameter as ParseAbiParameter_ } from './types/utils.js' +/** + * Validates human-readable ABI parameter string that contains struct signatures. + * If strict mode is set to true this will also perform type checks on the provided strings. + * @param TParams - Human-readable ABI parameter with struct signatures + * @returns[] if all params are valid. Otherwise returns an error message. + * + * @example + * type Result = ValidateAbiParameter< + * // ^? type Result = [] + * ['Baz bar', 'struct Baz { string name; }'] + * > + */ +export type ValidateAbiParameter = Flatten< + ParseStructs extends infer ParsedStructs extends object + ? IsEmptyObject extends true + ? Error<'No Struct signature found. Please provide valid struct signatures.'> + : CountStructSignatures extends Filter< + TParams, + StructSignature + >['length'] + ? { + [K in keyof TParams]: IsStructSignature extends true + ? never + : TParams[K] extends `(${string})${string}` + ? never + : TParams[K] extends `${string},${string}` + ? Error<`Invalid Parameter "${TParams[K]}". Please use "parseAbiParameters" for comma seperated strings.`> + : ValidateParameterString< + TParams[K], + keyof ParsedStructs + > extends infer ValidatedParam + ? ValidatedParam extends true + ? never + : ValidatedParam + : unknown + } + : Error<'Missmatch between struct signatures and arguments. Not all parameter strings will be parsed.'> + : [unknown] +> /** * Parses human-readable ABI parameter into {@link AbiParameter} * @@ -38,21 +83,25 @@ export type ParseAbiParameter< | (TParam extends readonly string[] ? string[] extends TParam ? AbiParameter // Return generic AbiParameter item since type was no inferrable - : ParseStructs extends infer Structs - ? { - [K in keyof TParam]: TParam[K] extends string - ? IsStructSignature extends true - ? never - : ParseAbiParameter_< - TParam[K], - { Modifier: Modifier; Structs: Structs } - > + : ValidateAbiParameter extends infer Validated + ? Validated extends never[] + ? ParseStructs extends infer Structs + ? { + [K in keyof TParam]: TParam[K] extends string + ? IsStructSignature extends true + ? never + : ParseAbiParameter_< + TParam[K], + { Modifier: Modifier; Structs: Structs } + > + : never + } extends infer Mapped extends readonly unknown[] + ? Filter[0] extends infer Result + ? Result extends undefined + ? never + : Result + : never : never - } extends infer Mapped extends readonly unknown[] - ? Filter[0] extends infer Result - ? Result extends undefined - ? never - : Result : never : never : never @@ -87,10 +136,16 @@ export function parseAbiParameter< : never) | (TParam extends readonly string[] ? TParam extends readonly [] // empty array - ? Error<'At least one parameter required.'> + ? 'Error: At least one parameter required.' : string[] extends TParam ? unknown - : unknown // TODO: Validate param string + : ValidateAbiParameter extends infer ValidatedParams extends readonly unknown[] + ? ValidatedParams extends readonly [] + ? unknown + : ValidatedParams extends string[] + ? ValidatedParams[number] + : never + : never : never) ), ): ParseAbiParameter { diff --git a/src/human-readable/parseAbiParameters.test-d.ts b/src/human-readable/parseAbiParameters.test-d.ts index 5190a069..7a7eafd0 100644 --- a/src/human-readable/parseAbiParameters.test-d.ts +++ b/src/human-readable/parseAbiParameters.test-d.ts @@ -1,10 +1,29 @@ import { expectTypeOf, test } from 'vitest' import type { AbiParameter } from '../abi.js' - -import type { ParseAbiParameters } from './parseAbiParameters.js' +import type { Flatten } from '../types.js' +import type { + ParseAbiParameters, + ValidateAbiParameters, +} from './parseAbiParameters.js' import { parseAbiParameters } from './parseAbiParameters.js' +test('ValidateAbiParameters', () => { + expectTypeOf>().toEqualTypeOf< + [ + 'Error: No Struct signature found. Please provide valid struct signatures.', + ] + >() + expectTypeOf< + ValidateAbiParameters<['struct Foo { string name; }', 'address']> + >().toEqualTypeOf<[]>() + expectTypeOf< + Flatten> + >().toEqualTypeOf<[]>() + // expectTypeOf< + // Flatten> + // >().toEqualTypeOf<["Error: Invalid Parameter \"Bar\". No valid type."]>(); +}) test('ParseAbiParameters', () => { expectTypeOf>().toEqualTypeOf() expectTypeOf>().toEqualTypeOf() @@ -53,19 +72,19 @@ test('ParseAbiParameters', () => { ] >() expectTypeOf< - ParseAbiParameters<'address calldata foo, address memory bar, uint256 storage baz'> + ParseAbiParameters<'address[] calldata foo, address[] memory bar, uint256[] storage baz'> >().toEqualTypeOf< readonly [ { - readonly type: 'address' + readonly type: 'address[]' readonly name: 'foo' }, { - readonly type: 'address' + readonly type: 'address[]' readonly name: 'bar' }, { - readonly type: 'uint256' + readonly type: 'uint256[]' readonly name: 'baz' }, ] @@ -94,9 +113,24 @@ test('parseAbiParameters', () => { // @ts-expect-error empty array not allowed expectTypeOf(parseAbiParameters([])).toEqualTypeOf() expectTypeOf( + // @ts-expect-error invalid no parameter given parseAbiParameters(['struct Foo { string name; }']), ).toEqualTypeOf() + expectTypeOf( + parseAbiParameters(['struct Foo { string name; }', 'address']), + ).toEqualTypeOf() + + expectTypeOf( + // This will fail in strict mode + parseAbiParameters(['struct Foo { string name; }', 'Bar']), + ).toEqualTypeOf() + + // expectTypeOf( + // // @ts-expect-error not a valid type + // parseAbiParameters(["struct Foo { string name; }", "Bar"]) + // ).toEqualTypeOf(); + expectTypeOf(parseAbiParameters('(string)')).toEqualTypeOf< readonly [ { diff --git a/src/human-readable/parseAbiParameters.test.ts b/src/human-readable/parseAbiParameters.test.ts index 3c9f9ddf..e49d1e36 100644 --- a/src/human-readable/parseAbiParameters.test.ts +++ b/src/human-readable/parseAbiParameters.test.ts @@ -24,6 +24,7 @@ test('parseAbiParameters', () => { `, ) expect(() => + // @ts-expect-error invalid no parameter given parseAbiParameters(['struct Foo { string name; }']), ).toThrowErrorMatchingInlineSnapshot( ` diff --git a/src/human-readable/parseAbiParameters.ts b/src/human-readable/parseAbiParameters.ts index 4c1c97ce..923646e6 100644 --- a/src/human-readable/parseAbiParameters.ts +++ b/src/human-readable/parseAbiParameters.ts @@ -1,16 +1,67 @@ -import type { AbiParameter } from '../abi.js' +import type { AbiParameter, InferredAbiParameter } from '../abi.js' import { InvalidAbiParametersError } from '../index.js' import type { Narrow } from '../narrow.js' -import type { Error, Filter } from '../types.js' +import type { Error, Filter, Flatten, IsEmptyObject } from '../types.js' import { isStructSignature, modifiers } from './runtime/signatures.js' import { parseStructs } from './runtime/structs.js' import { splitParameters } from './runtime/utils.js' import { parseAbiParameter as parseAbiParameter_ } from './runtime/utils.js' -import type { IsStructSignature, Modifier } from './types/signatures.js' +import type { + CountStructSignatures, + IsStructSignature, + Modifier, + StructSignature, + ValidateParameterString, +} from './types/signatures.js' import type { ParseStructs } from './types/structs.js' import type { SplitParameters } from './types/utils.js' import type { ParseAbiParameters as ParseAbiParameters_ } from './types/utils.js' +/** + * Validates human-readable ABI parameter string that contains struct signatures. + * If strict mode is set to true this will also perform type checks on the provided strings. + * @param TParams - Human-readable ABI parameter with struct signatures + * @returns [] if all params are valid. Otherwise returns an error message. + * + * @example + * type Result = ValidateAbiParameter< + * // ^? type Result = [] + * ['Baz bar', 'struct Baz { string name; }'] + * > + */ +export type ValidateAbiParameters = Flatten< + ParseStructs extends infer ParsedStructs extends object + ? IsEmptyObject extends true + ? Error<'No Struct signature found. Please provide valid struct signatures.'> + : CountStructSignatures extends Filter< + TParams, + StructSignature + >['length'] + ? { + [K in keyof TParams]: SplitParameters< + TParams[K] + > extends infer Splited extends string[] + ? { + [K2 in keyof Splited]: IsStructSignature< + Splited[K2] + > extends true + ? never + : Splited[K2] extends `(${string})${string}` + ? never + : ValidateParameterString< + Splited[K2], + keyof ParsedStructs + > extends infer ValidatedParam + ? ValidatedParam extends true + ? never + : ValidatedParam + : unknown + } + : unknown + } + : Error<'Missmatch between struct signatures and arguments. Not all parameter strings will be parsed.'> + : [unknown] +> /** * Parses human-readable ABI parameters into {@link AbiParameter}s * @@ -40,21 +91,23 @@ export type ParseAbiParameters< | (TParams extends readonly string[] ? string[] extends TParams ? AbiParameter // Return generic AbiParameter item since type was no inferrable - : ParseStructs extends infer Structs - ? { - [K in keyof TParams]: TParams[K] extends string - ? IsStructSignature extends true + : Flatten> extends readonly [] + ? ParseStructs extends infer Structs + ? { + [K in keyof TParams]: TParams[K] extends string + ? IsStructSignature extends true + ? never + : ParseAbiParameters_< + SplitParameters, + { Modifier: Modifier; Structs: Structs } + > + : never + } extends infer Mapped extends readonly unknown[] + ? Filter[0] extends infer Result + ? Result extends undefined ? never - : ParseAbiParameters_< - SplitParameters, - { Modifier: Modifier; Structs: Structs } - > + : Result : never - } extends infer Mapped extends readonly unknown[] - ? Filter[0] extends infer Result - ? Result extends undefined - ? never - : Result : never : never : never @@ -92,11 +145,17 @@ export function parseAbiParameters< ? Error<'At least one parameter required.'> : string[] extends TParams ? unknown - : unknown // TODO: Validate param string + : ValidateAbiParameters extends infer Parsed extends readonly unknown[] + ? Parsed extends readonly [] + ? unknown + : Parsed extends readonly string[] + ? Parsed[number] + : never + : never : never) ), ): ParseAbiParameters { - const abiParameters: AbiParameter[] = [] + const abiParameters: InferredAbiParameter[] = [] if (typeof params === 'string') { const parameters = splitParameters(params) const length = parameters.length diff --git a/src/human-readable/runtime/cache.ts b/src/human-readable/runtime/cache.ts index ffa0efca..45dfb00b 100644 --- a/src/human-readable/runtime/cache.ts +++ b/src/human-readable/runtime/cache.ts @@ -1,4 +1,4 @@ -import type { AbiItemType, AbiParameter } from '../../abi.js' +import type { AbiItemType, InferredAbiEventParameter } from '../../abi.js' /** * Gets {@link parameterCache} cache key namespaced by {@link type}. This prevents parameters from being accessible to types that don't allow them (e.g. `string indexed foo` not allowed outside of `type: 'event'`). @@ -19,10 +19,7 @@ export function getParameterCacheKey( * * **Note: When seeding more parameters, make sure you benchmark performance. The current number is the ideal balance between performance and having an already existing cache.** */ -export const parameterCache = new Map< - string, - AbiParameter & { indexed?: boolean } ->([ +export const parameterCache = new Map([ // Unnamed ['address', { type: 'address' }], ['bool', { type: 'bool' }], diff --git a/src/human-readable/runtime/structs.ts b/src/human-readable/runtime/structs.ts index e9c306f4..9b1e02ba 100644 --- a/src/human-readable/runtime/structs.ts +++ b/src/human-readable/runtime/structs.ts @@ -1,4 +1,4 @@ -import type { AbiParameter } from '../../abi.js' +import type { InferredAbiParameter } from '../../abi.js' import { execTyped, isTupleRegex } from '../../regex.js' import { UnknownTypeError } from '../errors/abiItem.js' import { InvalidAbiTypeParameterError } from '../errors/abiParameter.js' @@ -24,7 +24,7 @@ export function parseStructs(signatures: readonly string[]) { const properties = match.properties.split(';') - const components: AbiParameter[] = [] + const components: InferredAbiParameter[] = [] const propertiesLength = properties.length for (let k = 0; k < propertiesLength; k++) { const property = properties[k]! @@ -56,11 +56,11 @@ const typeWithoutTupleRegex = /^(?[a-zA-Z0-9_]+?)(?(?:\[\d*?\])+?)?$/ function resolveStructs( - abiParameters: readonly (AbiParameter & { indexed?: true })[], + abiParameters: readonly (InferredAbiParameter & { indexed?: true })[], structs: StructLookup, ancestors = new Set(), ) { - const components: AbiParameter[] = [] + const components: InferredAbiParameter[] = [] const length = abiParameters.length for (let i = 0; i < length; i++) { const abiParameter = abiParameters[i]! diff --git a/src/human-readable/types/signatures-test-d.ts b/src/human-readable/types/signatures-test-d.ts index 417e6bd5..5ada3722 100644 --- a/src/human-readable/types/signatures-test-d.ts +++ b/src/human-readable/types/signatures-test-d.ts @@ -1,19 +1,21 @@ import { assertType, expectTypeOf, test } from 'vitest' -import type { - IsConstructorSignature, - IsErrorSignature, - IsEventSignature, - IsFunctionSignature, - IsName, - IsSignature, - IsSolidityKeyword, - IsStructSignature, - IsValidCharacter, - Signature, - Signatures, - SolidityKeywords, - ValidateName, +import { + type IsConstructorSignature, + type IsErrorSignature, + type IsEventSignature, + type IsFunctionSignature, + type IsName, + type IsSignature, + type IsSolidityKeyword, + type IsStructSignature, + type IsValidCharacter, + type Signature, + type Signatures, + type SolidityKeywords, + type ValidateModifier, + type ValidateName, + type ValidateType, } from './signatures.js' test('IsErrorSignature', () => { @@ -211,6 +213,43 @@ test('ValidateName', () => { >() }) +test('ValidateType', () => { + // This will fail in strict mode + assertType>(true) + assertType>(true) + assertType>(false) +}) + +test('ValidateModifier', () => { + // This will fail in strict mode + assertType>(true) + // This will fail in strict mode + assertType>(true) + // This will fail in strict mode + assertType>(true) + // This will fail in strict mode + assertType>(true) + assertType>( + true, + ) + // assertType>([ + // "Error: Invalid modifier. calldata not allowed in address type.", + // ]); + // assertType>([ + // "Error: Invalid modifier. storage not allowed in address type.", + // ]); + // assertType>([ + // "Error: Invalid modifier. memory not allowed in address type.", + // ]); + // assertType>([ + // "Error: Invalid modifier. memory not allowed in Foo type.", + // ]); + assertType>( + true, + ) + assertType>(true) +}) + test('IsSolidityKeyword', () => { expectTypeOf>().toEqualTypeOf() expectTypeOf>().toEqualTypeOf() diff --git a/src/human-readable/types/signatures.ts b/src/human-readable/types/signatures.ts index 822e0497..4e9a1313 100644 --- a/src/human-readable/types/signatures.ts +++ b/src/human-readable/types/signatures.ts @@ -1,5 +1,13 @@ -import type { AbiStateMutability } from '../../abi.js' -import type { Error } from '../../types.js' +import type { + AbiStateMutability, + AbiType, + SolidityArray, + SolidityString, + SolidityTuple, +} from '../../abi.js' +import type { ResolvedConfig } from '../../config.js' +import type { Error, Filter, IsArrayString } from '../../types.js' +import type { StructLookup } from './structs.js' export type ErrorSignature< TName extends string = string, @@ -130,7 +138,7 @@ export type IsName = TName extends '' : false export type ValidateName< TName extends string, - CheckCharacters extends boolean = false, + CheckCharacters extends boolean = ResolvedConfig['Strict'], > = TName extends `${string}${' '}${string}` ? Error<`Name "${TName}" cannot contain whitespace.`> : IsSolidityKeyword extends true @@ -141,6 +149,53 @@ export type ValidateName< : Error<`"${TName}" contains invalid character.`> : TName +export type ValidateType< + TType extends string, + Strict extends boolean = ResolvedConfig['Strict'], +> = Strict extends true ? (TType extends AbiType ? true : false) : true + +export type ValidateModifier< + TModifer extends Modifier, + Options extends { type?: string; Structs?: StructLookup | unknown }, +> = Options extends { type: string } + ? ResolvedConfig['Strict'] extends true + ? TModifer extends Exclude + ? Options['type'] extends + | SolidityArray + | SolidityString + | 'bytes' + | SolidityTuple + ? true + : Options['type'] extends keyof Options['Structs'] + ? true + : Error<`Invalid modifier. ${TModifer} not allowed in ${Options['type']} type.`> + : true + : true + : unknown + +export type ValidateParameterString< + ParamString extends string, + StructKeys extends string | number | symbol, +> = ParamString extends `${infer Type} ${string}` + ? ValidateType> extends true + ? true + : IsArrayString extends StructKeys + ? true + : Error<`Invalid Parameter "${ParamString}". No valid type.`> + : ValidateType> extends true + ? true + : IsArrayString extends StructKeys + ? true + : Error<`Invalid Parameter "${ParamString}". No valid type.`> + +export type CountStructSignatures = + Filter< + { + [K in keyof Signatures]: Signatures[K] extends StructSignature ? 0 : never + }, + never + >['length'] + export type IsSolidityKeyword = T extends SolidityKeywords ? true : false diff --git a/src/human-readable/types/structs.ts b/src/human-readable/types/structs.ts index 4a04b24a..b5c1450e 100644 --- a/src/human-readable/types/structs.ts +++ b/src/human-readable/types/structs.ts @@ -1,9 +1,9 @@ -import type { AbiParameter } from '../../abi.js' +import type { InferredAbiParameter } from '../../abi.js' import type { Error, Trim } from '../../types.js' import type { StructSignature } from './signatures.js' import type { ParseAbiParameter } from './utils.js' -export type StructLookup = Record +export type StructLookup = Record export type ParseStructs = // Create "shallow" version of each struct (and filter out non-structs or invalid structs) @@ -16,7 +16,7 @@ export type ParseStructs = : never]: ParseStruct['components'] } extends infer Structs extends Record< string, - readonly (AbiParameter & { type: string })[] + readonly (InferredAbiParameter & { type: string })[] > ? // Resolve nested structs inside each struct { @@ -38,8 +38,11 @@ export type ParseStruct< : never export type ResolveStructs< - TAbiParameters extends readonly (AbiParameter & { type: string })[], - TStructs extends Record, + TAbiParameters extends readonly (InferredAbiParameter & { type: string })[], + TStructs extends Record< + string, + readonly (InferredAbiParameter & { type: string })[] + >, TKeyReferences extends { [_: string]: unknown } | unknown = unknown, > = readonly [ ...{ @@ -82,6 +85,6 @@ export type ParseStructProperties< ? ParseStructProperties< Tail, TStructs, - [...Result, ParseAbiParameter] + [...Result, ParseAbiParameter] > : Result diff --git a/src/human-readable/types/utils.test-d.ts b/src/human-readable/types/utils.test-d.ts index 799ff77f..ef5696da 100644 --- a/src/human-readable/types/utils.test-d.ts +++ b/src/human-readable/types/utils.test-d.ts @@ -394,6 +394,12 @@ test('ParseAbiParameter', () => { type: 'string', name: 'foo', }) + + // assertType>({ + // type: 'address', + // name: ["Error: Invalid modifier. calldata not allowed in address type."], + // }) + assertType>({ type: 'string', indexed: true, @@ -540,6 +546,40 @@ test('ParseAbiParameter', () => { ], }) + assertType< + ParseAbiParameter< + '((((string baz) bar)[1] foo) boo) calldata buu', + OptionsWithModifier + > + >({ + type: 'tuple', + components: [ + { + type: 'tuple', + components: [ + { + type: 'tuple[1]', + components: [ + { + type: 'tuple', + components: [ + { + type: 'string', + name: 'baz', + }, + ], + name: 'bar', + }, + ], + name: 'foo', + }, + ], + name: 'boo', + }, + ], + name: 'buu', + }) + assertType>({ type: 'address', name: ['Error: "alias" is a protected Solidity keyword.'], @@ -971,20 +1011,36 @@ test('_ParseTuple', () => { }) test('_SplitNameOrModifier', () => { - expectTypeOf<_SplitNameOrModifier<'foo'>>().toEqualTypeOf<{ + expectTypeOf< + _SplitNameOrModifier<'foo', { type: 'address' }> + >().toEqualTypeOf<{ readonly name: 'foo' }>() expectTypeOf< - _SplitNameOrModifier<'indexed foo', { Modifier: 'indexed' }> + _SplitNameOrModifier< + 'indexed foo', + { Modifier: 'indexed'; type: 'address' } + > >().toEqualTypeOf<{ readonly name: 'foo' readonly indexed: true }>() expectTypeOf< - _SplitNameOrModifier<'calldata foo', { Modifier: 'calldata' }> + _SplitNameOrModifier< + 'calldata foo', + { Modifier: 'calldata'; type: 'string' } + > >().toEqualTypeOf<{ readonly name: 'foo' }>() + // expectTypeOf< + // _SplitNameOrModifier< + // 'calldata foo', + // { Modifier: 'calldata'; type: 'address' } + // > + // >().toEqualTypeOf<{ + // readonly name: "Error: Invalid modifier. calldata not allowed in address type." + // }>() }) test('_UnwrapNameOrModifier', () => { diff --git a/src/human-readable/types/utils.ts b/src/human-readable/types/utils.ts index 2d74ad04..2113cb91 100644 --- a/src/human-readable/types/utils.ts +++ b/src/human-readable/types/utils.ts @@ -1,11 +1,18 @@ import type { - AbiParameter, AbiStateMutability, - AbiType, + InferredAbiParameter, SolidityFixedArrayRange, } from '../../abi.js' import type { ResolvedConfig } from '../../config.js' -import type { Error, IsUnknown, Merge, Pretty, Trim } from '../../types.js' +import type { + Error, + IsArrayString, + IsUnknown, + Merge, + Pop, + Pretty, + Trim, +} from '../../types.js' import type { ErrorSignature, EventModifier, @@ -20,7 +27,9 @@ import type { Modifier, ReceiveSignature, Scope, + ValidateModifier, ValidateName, + ValidateType, } from './signatures.js' import type { StructLookup } from './structs.js' @@ -103,10 +112,13 @@ export type ParseSignature< : never) export type ParseOptions = { - Modifier?: Modifier + Modifier?: Modifier | undefined Structs?: StructLookup | unknown + Strict?: boolean | undefined +} +export type DefaultParseOptions = object & { + Strict: ResolvedConfig['Strict'] } -export type DefaultParseOptions = object export type ParseAbiParameters< T extends readonly string[], @@ -130,12 +142,22 @@ export type ParseAbiParameter< T extends `${infer Type} ${infer Tail}` ? Trim extends infer Trimmed extends string - ? // TODO: data location modifiers only allowed for struct/array types - { readonly type: Trim } & _SplitNameOrModifier + ? { readonly type: Trim } & _SplitNameOrModifier< + Trimmed, + Options & { + type: IsArrayString< + Trim + > extends infer ArrayType extends string + ? ArrayType extends keyof Options['Structs'] + ? ArrayType + : Trim + : never + } + > : never : // Must be `${Type}` format (e.g. `uint256`) { readonly type: T } -) extends infer ShallowParameter extends AbiParameter & { +) extends infer ShallowParameter extends InferredAbiParameter & { type: string indexed?: boolean } @@ -166,11 +188,18 @@ export type ParseAbiParameter< : object) : // Not a struct, just return ShallowParameter - ) extends infer Parameter extends AbiParameter & { + ) extends infer Parameter extends InferredAbiParameter & { type: string indexed?: boolean } - ? Pretty<_ValidateAbiParameter> + ? Pretty< + _ValidateAbiParameter< + Parameter, + Options['Strict'] extends boolean + ? Options['Strict'] + : ResolvedConfig['Strict'] + > + > : never : never @@ -198,45 +227,40 @@ export type SplitParameters< : SplitParameters> : SplitParameters : [] -type Pop = T extends [...infer R, any] ? R : [] -export type _ValidateAbiParameter = - // Validate `name` - ( - TAbiParameter extends { name: string } - ? ValidateName extends infer Name - ? Name extends TAbiParameter['name'] - ? TAbiParameter - : // Add `Error` as `name` - Merge - : never - : TAbiParameter - ) extends infer Parameter - ? // Validate `type` against `AbiType` - ( - ResolvedConfig['StrictAbiType'] extends true - ? Parameter extends { type: AbiType } - ? Parameter - : Merge< - Parameter, - { - readonly type: Error<`Type "${Parameter extends { - type: string - } - ? Parameter['type'] - : string}" is not a valid ABI type.`> - } - > - : Parameter - ) extends infer Parameter2 extends { type: unknown } - ? // Convert `(u)int` to `(u)int256` - Parameter2['type'] extends `${infer Prefix extends - | 'u' - | ''}int${infer Suffix extends `[${string}]` | ''}` - ? Merge - : Parameter2 +export type _ValidateAbiParameter< + TAbiParameter extends InferredAbiParameter & { type: string }, + Strict extends boolean = ResolvedConfig['Strict'], +> = ( // Validate `name` + TAbiParameter extends { name: string } + ? TAbiParameter['name'] extends `Error:${string}` + ? Merge + : ValidateName extends infer Name + ? Name extends TAbiParameter['name'] + ? TAbiParameter + : // Add `Error` as `name` + Merge : never + : TAbiParameter +) extends infer Parameter extends { type: string } + ? ( + ValidateType extends true + ? Parameter + : Merge< + Parameter, + { + readonly type: Error<`Type "${Parameter['type']}" is not a valid ABI type.`> + } + > + ) extends infer Parameter2 extends { type: unknown } + ? // Convert `(u)int` to `(u)int256` + Parameter2['type'] extends `${infer Prefix extends + | 'u' + | ''}int${infer Suffix extends `[${string}]` | ''}` + ? Merge + : Parameter2 : never + : never export type _ParseFunctionParametersAndStateMutability< TSignature extends string, @@ -307,7 +331,10 @@ T extends `(${infer Parameters})` SplitParameters<`${Parameters})[${Size}] ${Parts['End']}`>, Omit > - } & _SplitNameOrModifier + } & _SplitNameOrModifier< + Parts['NameOrModifier'], + Options & { type: 'tuple' } + > : never : { readonly type: `tuple[${Size}]` @@ -315,7 +342,10 @@ T extends `(${infer Parameters})` SplitParameters, Omit > - } & _SplitNameOrModifier + } & _SplitNameOrModifier< + NameOrModifier, + Options & { type: `tuple[${Size}]` } + > : never : // Tuples with name and/or modifier attached (e.g. `(string) foo`, `(string bar) foo`) T extends `(${infer Parameters}) ${infer NameOrModifier}` @@ -331,7 +361,10 @@ T extends `(${infer Parameters})` SplitParameters<`${Parameters}) ${Parts['End']}`>, Omit > - } & _SplitNameOrModifier + } & _SplitNameOrModifier< + Parts['NameOrModifier'], + Options & { type: 'tuple' } + > : never : { readonly type: 'tuple' @@ -339,22 +372,26 @@ T extends `(${infer Parameters})` SplitParameters, Omit > - } & _SplitNameOrModifier + } & _SplitNameOrModifier : never // Split name and modifier (e.g. `indexed foo` => `{ name: 'foo', indexed: true }`) export type _SplitNameOrModifier< T extends string, - Options extends ParseOptions = DefaultParseOptions, + Options extends ParseOptions & { type: string }, > = Trim extends infer Trimmed ? Options extends { Modifier: Modifier } - ? // TODO: Check that modifier is allowed - Trimmed extends `${infer Mod extends Options['Modifier']} ${infer Name}` - ? { readonly name: Trim } & (Mod extends 'indexed' - ? { readonly indexed: true } - : object) + ? Trimmed extends `${infer Mod extends Options['Modifier']} ${infer Name}` + ? ValidateModifier extends infer Validated extends string[] + ? { readonly name: Validated[0] } + : { readonly name: Trim } & (Mod extends 'indexed' + ? { readonly indexed: true } + : // This is safe since this will get squashed by the intersection + {}) : Trimmed extends Options['Modifier'] - ? Trimmed extends 'indexed' + ? ValidateModifier extends infer Val extends string[] + ? { readonly name: Val[0] } + : Trimmed extends 'indexed' ? { readonly indexed: true } : object : { readonly name: Trimmed } diff --git a/src/index.ts b/src/index.ts index 525c5165..83eaa189 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,15 @@ export type { AbiStateMutability, AbiType, Address, + InferredAbi, + InferredAbiConstructor, + InferredAbiError, + InferredAbiEvent, + InferredAbiEventParameter, + InferredAbiFallback, + InferredAbiFunction, + InferredAbiParameter, + InferredAbiReceive, SolidityAddress, SolidityArray, SolidityArrayWithoutTuple, diff --git a/src/types.test-d.ts b/src/types.test-d.ts index 9f1b64e5..d5c55743 100644 --- a/src/types.test-d.ts +++ b/src/types.test-d.ts @@ -3,12 +3,16 @@ import { assertType, expectTypeOf, test } from 'vitest' import type { Error, Filter, + Flatten, + IsArrayString, + IsEmptyObject, IsNarrowable, IsNever, IsUnknown, Join, Merge, OneOf, + Pop, Range, Trim, Tuple, @@ -97,3 +101,34 @@ test('Tuple', () => { readonly [string | number, string | number] >() }) + +test('Flatten', () => { + assertType>([1, 2, 3]) + assertType>([1, 2, 3]) + assertType>([1, 3]) + assertType>([1, '2']) +}) + +test('IsEmptyObject', () => { + assertType>(true) + assertType>(true) + assertType>(false) +}) + +test('IsArrayString', () => { + assertType>('Foo') + assertType>('Foo') + assertType>('Foo') +}) + +test('IsEmptyObject', () => { + assertType>([1, 2]) + assertType>([1, 2, 3, 4]) +}) + +test('IsNever', () => { + assertType>(true) + assertType>(false) + assertType>(false) + assertType>(false) +}) diff --git a/src/types.ts b/src/types.ts index 1e01d30d..8f7ebe3e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -217,3 +217,68 @@ type _TupleOf< > = R['length'] extends TSize ? R : _TupleOf + +/** + * Check if a given object has no keys. + * + * @param T - Object to check + * @param AllKeys - Keys to check. Default to keyof {@link T} + * @returns true if empty false if not + * + * @example + * type Result = IsEmptyObject<{}> + * // ^? type Result = true + */ +export type IsEmptyObject< + T extends object, + AllKeys extends keyof T = keyof T, +> = IsNever + +/** + * Flattens array of nested arrays into a single array with all of the elements. + * This also filters any `never` elements that the arrays might have. + * + * @param T - Array with nested arrays. + * @returns an array with all the elements of the nested arrays. + * + * @example + * type Result = Flatten<[1, [2], [[3]]]> + * // ^? type Result = [1, 2, 3] + */ +export type Flatten< + T extends readonly unknown[], + Result extends readonly unknown[] = [], +> = T extends readonly [infer Head, ...infer Rest extends readonly unknown[]] + ? [Head] extends [never] + ? Flatten<[...Rest], Result> + : Head extends readonly any[] + ? Flatten<[...Head, ...Rest], Result> + : Flatten<[...Rest], [...Result, Head]> + : Result + +/** + * Checks if a string literal is an array. + * + * @param T - String to check. + * @returns The extracted value if an array or {@link T} if not. + * + * @example + * type Result = IsArrayString<"Foo[]"> + * // ^? type Result = "Foo" + */ +export type IsArrayString = + T extends `${infer Name}[${string}]` ? Name : T + +/** + * Pops the last elements of an array of numbers. + * + * @param T - Array with numbers. + * @returns the array without the last element. + * + * @example + * type Result = Pop<[1, 2, 3]> + * // ^? type Result = [1, 2] + */ +export type Pop = T extends [...infer R, any] + ? R + : [] diff --git a/src/utils.test-d.ts b/src/utils.test-d.ts index b5029e91..6e0994c3 100644 --- a/src/utils.test-d.ts +++ b/src/utils.test-d.ts @@ -24,6 +24,7 @@ import type { ExtractAbiFunction, ExtractAbiFunctionNames, ExtractAbiFunctions, + ExtractAbiParseErrors, IsAbi, IsTypedData, TypedDataToPrimitiveTypes, @@ -390,6 +391,7 @@ test('AbiParameterToPrimitiveType', () => { name: 'data' type: 'foo' }> + // This will fail in strict mode assertType(null) }) @@ -399,6 +401,7 @@ test('AbiParameterToPrimitiveType', () => { type: 'foo[2][2]' }> assertType([ + // This will fail in strict mode [null, null], [null, null], ]) @@ -878,3 +881,20 @@ test('IsTypedData', () => { }> assertType(false) }) + +test('ExtractAbiParseErrors', () => { + assertType< + ExtractAbiParseErrors<[{ type: 'address'; name: ['Error: Foo'] }]> + >(['Error: Foo']) + assertType< + ExtractAbiParseErrors< + [ + { + type: 'address' + name: ['Error: Foo'] + components: [{ type: 'address'; name: ['Error: Bar'] }] + }, + ] + > + >(['Error: Bar']) +}) diff --git a/src/utils.ts b/src/utils.ts index 15a11310..9a052a6a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -20,7 +20,7 @@ import type { TypedDataType, } from './abi.js' import type { ResolvedConfig } from './config.js' -import type { Error, Merge, Pretty, Tuple } from './types.js' +import type { Error, Flatten, Merge, Pretty, Tuple } from './types.js' /** * Converts {@link AbiType} to corresponding TypeScript primitive type. @@ -82,7 +82,9 @@ type BitsTypeLookup = { * @returns TypeScript primitive type */ export type AbiParameterToPrimitiveType< - TAbiParameter extends AbiParameter | { name: string; type: unknown }, + TAbiParameter extends + | AbiParameter + | { name?: string | undefined; type: unknown }, TAbiParameterKind extends AbiParameterKind = AbiParameterKind, > = TAbiParameter['type'] extends Exclude< // 1. Check to see if type is basic (not tuple or array) and can be looked up immediately. @@ -156,7 +158,7 @@ export type AbiParameterToPrimitiveType< : // 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 `Config['ArrayMaxDepth']`, etc. - ResolvedConfig['StrictAbiType'] extends true + ResolvedConfig['Strict'] extends true ? TAbiParameter['type'] extends infer TAbiType extends string ? Error<`Unknown type '${TAbiType}'.`> : never @@ -315,6 +317,22 @@ export type ExtractAbiError< TErrorName extends ExtractAbiErrorNames, > = Extract, { name: TErrorName }> +/** + * Extract all errors from a parsed {@link Abi} in form of a union type if any exist. + * + * @param TAbi - {@link Abi} to check. + * @returns [] if the abi has no errors. Otherwise returns an array with the union of the errors. + */ +export type ExtractAbiParseErrors = Flatten<{ + [K in keyof TAbi]: { + [K2 in keyof TAbi[K]]: TAbi[K][K2] extends readonly string[] + ? TAbi[K][K2] + : TAbi[K][K2] extends readonly unknown[] + ? ExtractAbiParseErrors + : never + }[keyof TAbi[K]] +}> + //////////////////////////////////////////////////////////////////////////////////////////////////// // Typed Data diff --git a/src/zod.ts b/src/zod.ts index 3c55ab6c..6036a0f8 100644 --- a/src/zod.ts +++ b/src/zod.ts @@ -4,9 +4,9 @@ import type { AbiConstructor as AbiConstructorType, AbiFallback as AbiFallbackType, AbiFunction as AbiFunctionType, - AbiParameter as AbiParameterType, AbiReceive as AbiReceiveType, Address as AddressType, + InferredAbiParameter as AbiParameterType, } from './abi.js' import { bytesRegex, integerRegex } from './regex.js'