diff --git a/.gitignore b/.gitignore index 56651542..23e926c3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ node_modules tsconfig*.tsbuildinfo bench playgrounds/performance/out +# temporary type-testing cache +.attest # local env files .env diff --git a/package.json b/package.json index 26bac6c0..c342041d 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "version:update": "bun .scripts/updateVersion.ts" }, "devDependencies": { + "@arktype/attest": "0.0.4", "@biomejs/biome": "1.2.2", "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.0", diff --git a/packages/abitype/package.json b/packages/abitype/package.json index 43eb2c9c..4ca2fb97 100644 --- a/packages/abitype/package.json +++ b/packages/abitype/package.json @@ -10,7 +10,8 @@ "build:esm+types": "tsc --project tsconfig.build.json --module es2020 --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types && echo > ./dist/esm/package.json '{\"type\":\"module\",\"sideEffects\":false}'", "clean": "rm -rf dist tsconfig.tsbuildinfo abis zod", "test:build": "publint --strict", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "typeperf": "tsc --noEmit --extendedDiagnostics --composite false --incremental false" }, "files": [ "dist", @@ -48,12 +49,8 @@ }, "typesVersions": { "*": { - "abis": [ - "./dist/types/exports/abis.d.ts" - ], - "zod": [ - "./dist/types/exports/zod.d.ts" - ] + "abis": ["./dist/types/exports/abis.d.ts"], + "zod": ["./dist/types/exports/zod.d.ts"] } }, "peerDependencies": { diff --git a/packages/abitype/src/types.ts b/packages/abitype/src/types.ts index 33dde0b2..0c2aa799 100644 --- a/packages/abitype/src/types.ts +++ b/packages/abitype/src/types.ts @@ -141,6 +141,18 @@ type KeyofUnion = T extends T ? keyof T : never */ export type Pretty = { [K in keyof T]: T[K] } & unknown +/** + * Check that a type is a subtype of another. + * + * Useful for ensuring more complex types conform to a base pattern, e.g. by + * defining a set of keys. + * + * @param Base - The type that T must extend. + * @param T - The type to check. + * @returns T + */ +export type Satisfy = T + /** * Creates range between two positive numbers using [tail recursion](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#tail-recursion-elimination-on-conditional-types). * diff --git a/packages/abitype/src/utils.bench.ts b/packages/abitype/src/utils.bench.ts new file mode 100644 index 00000000..cd66be46 --- /dev/null +++ b/packages/abitype/src/utils.bench.ts @@ -0,0 +1,34 @@ +import { bench } from '@arktype/attest' +import type { TypedDataToPrimitiveTypes } from './utils.js' + +bench('recursive', () => { + const types3 = { + Foo: [{ name: 'bar', type: 'Bar[]' }], + Bar: [{ name: 'foo', type: 'Foo[]' }], + } as const + return {} as TypedDataToPrimitiveTypes +}).types([12, 'instantiations']) + +bench('deep', () => { + const types = { + Contributor: [ + { name: 'name', type: 'string' }, + { name: 'address', type: 'address' }, + ], + Website: [ + { name: 'domain', type: 'string' }, + { name: 'webmaster', type: 'Contributor' }, + ], + Project: [ + { name: 'name', type: 'string' }, + { name: 'contributors', type: 'Contributor[2]' }, + { name: 'website', type: 'Website' }, + ], + Organization: [ + { name: 'name', type: 'string' }, + { name: 'projects', type: 'Project[]' }, + { name: 'website', type: 'Website' }, + ], + } as const + return {} as TypedDataToPrimitiveTypes +}).types([44, 'instantiations']) diff --git a/packages/abitype/src/utils.test.ts b/packages/abitype/src/utils.test.ts new file mode 100644 index 00000000..7a3d621d --- /dev/null +++ b/packages/abitype/src/utils.test.ts @@ -0,0 +1,20 @@ +import { attest } from '@arktype/attest' +import { test } from 'vitest' +import type { TypedDataToPrimitiveTypes } from './utils.js' + +test('self-referencing', () => { + type Result = TypedDataToPrimitiveTypes<{ + Name: [{ name: 'first'; type: 'Name' }, { name: 'last'; type: 'string' }] + }> + attest< + { + Name: { + first: [ + "Error: Cannot convert self-referencing struct 'Name' to primitive type.", + ] + last: string + } + }, + Result + >() +}) diff --git a/packages/abitype/src/utils.ts b/packages/abitype/src/utils.ts index 4ad1816d..90d78091 100644 --- a/packages/abitype/src/utils.ts +++ b/packages/abitype/src/utils.ts @@ -5,15 +5,11 @@ import type { AbiStateMutability, AbiType, MBits, - SolidityAddress, SolidityArray, - SolidityBool, SolidityBytes, SolidityFixedArrayRange, SolidityFixedArraySizeLookup, - SolidityFunction, SolidityInt, - SolidityString, SolidityTuple, TypedData, TypedDataParameter, @@ -34,44 +30,45 @@ import type { Error, Merge, Pretty, Tuple } from './types.js' export type AbiTypeToPrimitiveType< TAbiType extends AbiType, TAbiParameterKind extends AbiParameterKind = AbiParameterKind, -> = PrimitiveTypeLookup[TAbiType] +> = TAbiType extends SolidityBytes + ? // If PrimitiveTypeLookup is missing key values from AbiType, + // there will be an error on this property access + PrimitiveTypeLookup[TAbiType][TAbiParameterKind] + : PrimitiveTypeLookup[TAbiType] -// Using a map to look up types is faster, than nested conditional types -// s/o https://twitter.com/SeaRyanC/status/1538971176357113858 -type PrimitiveTypeLookup< - TAbiType extends AbiType, - TAbiParameterKind extends AbiParameterKind = AbiParameterKind, -> = { - [_ in SolidityAddress]: ResolvedRegister['AddressType'] -} & { - [_ in SolidityBool]: boolean -} & { - [_ in SolidityBytes]: ResolvedRegister['BytesType'][TAbiParameterKind] -} & { - [_ in SolidityFunction]: `${ResolvedRegister['AddressType']}${string}` -} & { - [_ in SolidityInt]: TAbiType extends `${'u' | ''}int${infer TBits}` - ? TBits extends keyof BitsTypeLookup - ? BitsTypeLookup[TBits] - : Error<'Unknown bits value.'> - : Error<`Unknown 'SolidityInt' format.`> -} & { - [_ in SolidityString]: string -} & { - [_ in SolidityTuple]: Record -} & { - [_ in SolidityArray]: readonly unknown[] +interface PrimitiveTypeLookup + extends SolidityIntMap, + SolidityByteMap, + SolidityArrayMap { + address: ResolvedRegister['AddressType'] + bool: boolean + function: `${ResolvedRegister['AddressType']}${string}` + string: string + tuple: Record +} + +type SolidityIntMap = { + [_ in SolidityInt]: _ extends `${ + | 'u' + | ''}int${infer TBits extends keyof BitsTypeLookup}` + ? BitsTypeLookup[TBits] + : never } -type GreaterThan48Bits = Exclude -type LessThanOrEqualTo48Bits = Exclude -type NoBits = Exclude -type BitsTypeLookup = { - [_ in `${LessThanOrEqualTo48Bits}`]: ResolvedRegister['IntType'] -} & { - [_ in `${GreaterThan48Bits}`]: ResolvedRegister['BigIntType'] -} & { - [_ in NoBits]: ResolvedRegister['BigIntType'] +type SolidityByteMap = { + [_ in SolidityBytes]: ResolvedRegister['BytesType'] +} + +type SolidityArrayMap = { [_ in SolidityArray]: readonly unknown[] } + +type GreaterThan48Bits = Exclude +type LessThanOrEqualTo48Bits = Exclude +type NoBits = '' + +export type BitsTypeLookup = { + [K in MBits]: ResolvedRegister[K extends LessThanOrEqualTo48Bits + ? 'IntType' + : 'BigIntType'] } /** @@ -84,100 +81,103 @@ type BitsTypeLookup = { export type AbiParameterToPrimitiveType< TAbiParameter extends AbiParameter | { name: string; 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. - AbiType, - SolidityTuple | SolidityArray -> +> = TAbiParameter['type'] extends AbiBasicType ? AbiTypeToPrimitiveType : // 2. Check if type is tuple and covert each component TAbiParameter extends { type: SolidityTuple components: infer TComponents extends readonly AbiParameter[] } - ? TComponents extends readonly [] - ? [] - : _HasUnnamedAbiParameter extends true - ? // Has unnamed tuple parameters so return as array - readonly [ - ...{ - [K in keyof TComponents]: AbiParameterToPrimitiveType< - TComponents[K], - TAbiParameterKind - > - }, - ] - : // All tuple parameters are named so return as object - { - [Component in - TComponents[number] as Component extends { - name: string - } - ? Component['name'] - : never]: AbiParameterToPrimitiveType - } + ? AbiComponentsToPrimitiveType : // 3. Check if type is array. - /** - * First, infer `Head` against a known size type (either fixed-length array value or `""`). - * - * | Input | Head | - * | --------------- | ------------ | - * | `string[]` | `string` | - * | `string[][][3]` | `string[][]` | - */ - TAbiParameter['type'] extends `${infer Head}[${ - | '' - | `${SolidityFixedArrayRange}`}]` - ? /** - * Then, infer in the opposite direction, using the known `Head` to infer the exact `Size` value. - * - * | Input | Size | - * | ------------ | ---- | - * | `${Head}[]` | `""` | - * | `${Head}[3]` | `3` | - */ - TAbiParameter['type'] extends `${Head}[${infer Size}]` - ? // Check if size is within range for fixed-length arrays, if so create a tuple. - // Otherwise, create an array. Tuples and arrays are created with `[${Size}]` popped off the end - // and passed back into the function to continue reduing down to the basic types found in Step 1. - Size extends keyof SolidityFixedArraySizeLookup - ? Tuple< - AbiParameterToPrimitiveType< - Merge, - TAbiParameterKind - >, - SolidityFixedArraySizeLookup[Size] - > - : readonly AbiParameterToPrimitiveType< - Merge, - TAbiParameterKind - >[] - : never + MaybeExtractArrayParameterType extends [ + infer Head extends string, + infer Size, + ] + ? AbiArrayToPrimitiveType : // 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. ResolvedRegister['StrictAbiType'] extends true - ? TAbiParameter['type'] extends infer TAbiType extends string - ? Error<`Unknown type '${TAbiType}'.`> - : never + ? Error<`Unknown type '${TAbiParameter['type'] & string}'.`> : // 5. If we've gotten this far, let's check for errors in tuple components. // (Happens for recursive tuple typed data types.) TAbiParameter extends { components: Error } ? TAbiParameter['components'] : unknown -// TODO: Speed up by returning immediately as soon as named parameter is found. -type _HasUnnamedAbiParameter = - TAbiParameters extends readonly [ - infer Head extends AbiParameter, - ...infer Tail extends readonly AbiParameter[], - ] - ? Head extends { name: string } - ? Head['name'] extends '' - ? true - : _HasUnnamedAbiParameter - : true - : false +type AbiBasicType = Exclude + +type AbiComponentsToPrimitiveType< + Components extends readonly AbiParameter[], + TAbiParameterKind extends AbiParameterKind, +> = Components extends readonly [] + ? [] + : // Compare the original set of names to a "validated" + // set where each name is coerced to a string and undefined|"" are excluded + Components[number]['name'] extends Exclude< + Components[number]['name'] & string, + undefined | '' + > + ? // If all the original names are present, all tuple parameters are named so return as object + { + [Component in + Components[number] as Component['name'] & {}]: AbiParameterToPrimitiveType< + Component, + TAbiParameterKind + > + } + : // Otherwise, has unnamed tuple parameters so return as array + { + [I in keyof Components]: AbiParameterToPrimitiveType< + Components[I], + TAbiParameterKind + > + } + +type MaybeExtractArrayParameterType = + /** + * First, infer `Head` against a known size type (either fixed-length array value or `""`). + * + * | Input | Head | + * | --------------- | ------------ | + * | `string[]` | `string` | + * | `string[][][3]` | `string[][]` | + */ + T extends `${infer Head}[${'' | `${SolidityFixedArrayRange}`}]` + ? // * Then, infer in the opposite direction, using the known `Head` to infer the exact `Size` value. + // * + // * | Input | Size | + // * | ------------ | ---- | + // * | `${Head}[]` | `""` | + // * | `${Head}[3]` | `3` | + // */ + T extends `${Head}[${infer Size}]` + ? [Head, Size] + : undefined + : undefined + +type AbiArrayToPrimitiveType< + TAbiParameter extends AbiParameter | { name: string; type: unknown }, + TAbiParameterKind extends AbiParameterKind, + Head extends string, + Size, +> = Size extends keyof SolidityFixedArraySizeLookup + ? // Check if size is within range for fixed-length arrays, if so create a tuple. + Tuple< + AbiParameterToPrimitiveType< + Merge, + TAbiParameterKind + >, + SolidityFixedArraySizeLookup[Size] + > + : // Otherwise, create an array. Tuples and arrays are created with `[${Size}]` popped off the end + // and passed back into the function to continue reducing down to the basic types found in Step 1. + readonly AbiParameterToPrimitiveType< + Merge, + TAbiParameterKind + >[] /** * Converts array of {@link AbiParameter} to corresponding TypeScript primitive types. @@ -397,7 +397,8 @@ export type TypedDataToPrimitiveTypes< ? AbiParameterToPrimitiveType : Error<`Cannot convert unknown type '${K2['type']}' to primitive type.`> } -} + // Ensure the result is "Prettied" +} & unknown type _TypedDataParametersToAbiParameters< TTypedDataParameters extends readonly TypedDataParameter[], diff --git a/packages/abitype/test/globalSetup.ts b/packages/abitype/test/globalSetup.ts new file mode 100644 index 00000000..bd2ac4a4 --- /dev/null +++ b/packages/abitype/test/globalSetup.ts @@ -0,0 +1,6 @@ +import { cleanup, setup } from '@arktype/attest' + +export default () => { + setup() + return cleanup +} diff --git a/packages/abitype/test/vitest.workspace.ts b/packages/abitype/test/vitest.workspace.ts index 6009e332..1adff125 100644 --- a/packages/abitype/test/vitest.workspace.ts +++ b/packages/abitype/test/vitest.workspace.ts @@ -13,6 +13,7 @@ export default defineWorkspace([ name: 'abitype', environment: 'node', setupFiles: ['./packages/abitype/test/setup.ts'], + globalSetup: ['./packages/abitype/test/globalSetup.ts'], include: ['./packages/abitype/**/*.test.ts'], }, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed902246..59c71a54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,9 @@ importers: .: devDependencies: + '@arktype/attest': + specifier: 0.0.4 + version: 0.0.4(typescript@5.0.4) '@biomejs/biome': specifier: 1.2.2 version: 1.2.2 @@ -132,6 +135,28 @@ packages: '@jridgewell/trace-mapping': 0.3.18 dev: true + /@arktype/attest@0.0.4(typescript@5.0.4): + resolution: {integrity: sha512-9abICJ9UBeMLkqvXBzPMkR5wFqnXiutiBmiT3wZX+P5S0iQc1R/DPpH4fgcGe12oTSCN0k7OlsT+0uzpUzLEow==} + peerDependencies: + typescript: '*' + dependencies: + '@arktype/fs': 0.0.3 + '@arktype/util': 0.0.2 + '@typescript/vfs': 1.5.0 + arktype: 1.0.27-alpha + typescript: 5.0.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@arktype/fs@0.0.3: + resolution: {integrity: sha512-7jOCMs3Q95/Cn1QEFfrG99/QEcRNeLYZqEkLQ1grHuM/uefhw+7KZpswazjHlQe+6AZ9RVijy2Zdh69Xjyqmyg==} + dev: true + + /@arktype/util@0.0.2: + resolution: {integrity: sha512-pEq0vTCl1jytMDbNb0anfUL8pZ0UeR6/xLQ7KGUfapYKifEMUaoegZ/Hp4UKXguzSvZOMcwb/5f0ilYdDbBtLQ==} + dev: true + /@babel/code-frame@7.21.4: resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} engines: {node: '>=6.9.0'} @@ -2733,6 +2758,14 @@ packages: - supports-color dev: true + /@typescript/vfs@1.5.0: + resolution: {integrity: sha512-AJS307bPgbsZZ9ggCT3wwpg3VbTKMFNHfaY/uF0ahSkYYrPF2dSSKDNIDIQAHm9qJqbLvCsSJH7yN4Vs/CsMMg==} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + /@vitest/coverage-v8@0.34.5(vitest@0.34.5): resolution: {integrity: sha512-97xjhRTSdmeeHCm2nNHhT3hLsMYkAhHXm/rwj6SZ3voka8xiCJrwgtfIjoZIFEL4OO0KezGmVuHWQXcMunULIA==} peerDependencies: @@ -2929,6 +2962,10 @@ packages: resolution: {integrity: sha512-fExL2kFDC1Q2DUOx3whE/9KoN66IzkY4b4zUHUBFM1ojEYjZZYDcUW3bek/ufGionX9giIKDC5redH2IlGqcQQ==} dev: true + /arktype@1.0.27-alpha: + resolution: {integrity: sha512-IGoOTif9W6y2folO1+JoPEffZwO56NSuxb7n4C/iYymKluGek1Z1W6Rsi2P3L27SuQpke3DdsjUxW2ABBhH4hQ==} + dev: true + /array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} dependencies: