From ca1ca168bb71e66d80a9d3e3038f8d470dc40df6 Mon Sep 17 00:00:00 2001 From: Sebastien Guillemot Date: Fri, 25 Oct 2024 20:25:25 +0900 Subject: [PATCH] feat: Improve return type for toSignature --- .changeset/silly-geckos-agree.md | 5 + src/utils/hash/normalizeSignature.test-d.ts | 139 ++++++++++++++++++++ src/utils/hash/normalizeSignature.ts | 43 +++++- src/utils/hash/toSignature.ts | 18 ++- 4 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 .changeset/silly-geckos-agree.md create mode 100644 src/utils/hash/normalizeSignature.test-d.ts diff --git a/.changeset/silly-geckos-agree.md b/.changeset/silly-geckos-agree.md new file mode 100644 index 0000000000..02fca35af9 --- /dev/null +++ b/.changeset/silly-geckos-agree.md @@ -0,0 +1,5 @@ +--- +"viem": minor +--- + +Improved return type for toSignature diff --git a/src/utils/hash/normalizeSignature.test-d.ts b/src/utils/hash/normalizeSignature.test-d.ts new file mode 100644 index 0000000000..804014984d --- /dev/null +++ b/src/utils/hash/normalizeSignature.test-d.ts @@ -0,0 +1,139 @@ +import { expectTypeOf, test } from 'vitest' +import { normalizeSignature } from './normalizeSignature.js' + +/** + * Cases where we should be able to get proper types for this, but where the feature just isn't implemented in Viem + */ +type TodoType = string + +test('foo()', () => { + expectTypeOf(normalizeSignature('event foo()')).toMatchTypeOf<'foo()'>() +}) + +test('bar(uint foo)', () => { + expectTypeOf(normalizeSignature('bar(uint foo)')).toMatchTypeOf() +}) + +test('processAccount(uint256 , address )', () => { + expectTypeOf( + normalizeSignature('processAccount(uint256 , address )'), + ).toMatchTypeOf() +}) + +test('function bar()', () => { + expectTypeOf(normalizeSignature('function bar()')).toMatchTypeOf<'bar()'>() +}) + +test('function bar()', () => { + expectTypeOf(normalizeSignature('function bar( )')).toMatchTypeOf() +}) + +test('event Bar()', () => { + expectTypeOf(normalizeSignature('event Bar()')).toMatchTypeOf<'Bar()'>() +}) + +test('function bar(uint foo)', () => { + expectTypeOf( + normalizeSignature('function bar(uint foo)'), + ).toMatchTypeOf<'bar(uint256)'>() +}) + +test('function bar(uint foo, address baz)', () => { + expectTypeOf( + normalizeSignature('function bar(uint foo, address baz)'), + ).toMatchTypeOf<'bar(uint256,address)'>() +}) + +test('event Barry(uint foo)', () => { + expectTypeOf( + normalizeSignature('event Barry(uint foo)'), + ).toMatchTypeOf<'Barry(uint256)'>() +}) + +test('Barry(uint indexed foo)', () => { + expectTypeOf( + normalizeSignature('Barry(uint indexed foo)'), + ).toMatchTypeOf() +}) + +test('event Barry(uint indexed foo)', () => { + expectTypeOf( + normalizeSignature('event Barry(uint indexed foo)'), + ).toMatchTypeOf<'Barry(uint256)'>() +}) + +test('function _compound(uint256 a, uint256 b, uint256 c)', () => { + expectTypeOf( + normalizeSignature('function _compound(uint256 a, uint256 b, uint256 c)'), + ).toMatchTypeOf<'_compound(uint256,uint256,uint256)'>() +}) + +test('bar(uint foo, (uint baz))', () => { + expectTypeOf( + normalizeSignature('bar(uint foo, (uint baz))'), + ).toMatchTypeOf() +}) + +test('bar(uint foo, (uint baz, bool x))', () => { + expectTypeOf( + normalizeSignature('bar(uint foo, (uint baz, bool x))'), + ).toMatchTypeOf() +}) + +test('bar(uint foo, (uint baz, bool x) foo)', () => { + expectTypeOf( + normalizeSignature('bar(uint foo, (uint baz, bool x) foo)'), + ).toMatchTypeOf() +}) + +test('bar(uint[] foo, (uint baz, bool x))', () => { + expectTypeOf( + normalizeSignature('bar(uint[] foo, (uint baz, bool x))'), + ).toMatchTypeOf() +}) + +test('bar(uint[] foo, (uint baz, bool x)[])', () => { + expectTypeOf( + normalizeSignature('bar(uint[] foo, (uint baz, bool x)[])'), + ).toMatchTypeOf() +}) + +test('foo(uint bar)', () => { + expectTypeOf( + normalizeSignature('foo(uint bar) payable returns (uint)'), + ).toMatchTypeOf() +}) + +test('function submitBlocksWithCallbacks(bool isDataCompressed, bytes calldata data, ((uint16,(uint16,uint16,uint16,bytes)[])[], address[]) calldata config)', () => { + expectTypeOf( + normalizeSignature( + 'function submitBlocksWithCallbacks(bool isDataCompressed, bytes calldata data, ((uint16,(uint16,uint16,uint16,bytes)[])[], address[]) calldata config)', + ), + ).toMatchTypeOf<'submitBlocksWithCallbacks(bool,bytes,((uint16,(uint16,uint16,uint16,bytes)[])[],address[]))'>( + 'submitBlocksWithCallbacks(bool,bytes,((uint16,(uint16,uint16,uint16,bytes)[])[],address[]))', + ) +}) + +test('function createEdition(string name, string symbol, uint64 editionSize, uint16 royaltyBPS, address fundsRecipient, address defaultAdmin, (uint104 publicSalePrice, uint32 maxSalePurchasePerAddress, uint64 publicSaleStart, uint64 publicSaleEnd, uint64 presaleStart, uint64 presaleEnd, bytes32 presaleMerkleRoot) saleConfig, string description, string animationURI, string imageURI) returns (address)', () => { + expectTypeOf( + normalizeSignature( + 'function createEdition(string name, string symbol, uint64 editionSize, uint16 royaltyBPS, address fundsRecipient, address defaultAdmin, (uint104 publicSalePrice, uint32 maxSalePurchasePerAddress, uint64 publicSaleStart, uint64 publicSaleEnd, uint64 presaleStart, uint64 presaleEnd, bytes32 presaleMerkleRoot) saleConfig, string description, string animationURI, string imageURI) returns (address)', + ), + ).toMatchTypeOf<'createEdition(string,string,uint64,uint16,address,address,(uint104,uint32,uint64,uint64,uint64,uint64,bytes32),string,string,string)'>() +}) + +test('trim spaces', () => { + expectTypeOf( + normalizeSignature( + 'function createEdition(string name,string symbol, uint64 editionSize , uint16 royaltyBPS, address fundsRecipient, address defaultAdmin, ( uint104 publicSalePrice , uint32 maxSalePurchasePerAddress, uint64 publicSaleStart, uint64 publicSaleEnd, uint64 presaleStart, uint64 presaleEnd, bytes32 presaleMerkleRoot) saleConfig , string description, string animationURI, string imageURI ) returns (address)', + ), + ).toMatchTypeOf() +}) + +test('error: invalid signatures', () => { + // TODO: these should be errors, but they aren't yet + // assertType(normalizeSignature('bar')) + // assertType(normalizeSignature('bar(uint foo')) + // assertType(normalizeSignature('baruint foo)')) + // assertType(normalizeSignature('bar(uint foo, (uint baz)')) +}) diff --git a/src/utils/hash/normalizeSignature.ts b/src/utils/hash/normalizeSignature.ts index 8e04062673..2381d4ca47 100644 --- a/src/utils/hash/normalizeSignature.ts +++ b/src/utils/hash/normalizeSignature.ts @@ -1,13 +1,44 @@ +import type { + AbiEvent, + AbiFunction, + ParseAbiItem, + SignatureAbiItem, +} from 'abitype' import { BaseError } from '../../errors/base.js' import type { ErrorType } from '../../errors/utils.js' -type NormalizeSignatureParameters = string -type NormalizeSignatureReturnType = string +/** + * TODO: ParseAbiItem is too strict here since + * 1. normalizeSignature allows loose formatting (ex: spacing) + * 2. normalization doesn't require the string to conform to abitype's human-readable encoding + * ex: 'function bar( )' should still results in `bar()` as the result + * ex: `event foo(tuple(uint16, uint16))` results in a broken type response + * for now, we return`string` in cases where abitype fails + * but this doesn't handle error cases where abitype is just wrong like + * TODO: Some obvious errors get the return type `string` + * this is because we fallback to `string` if `ParseAbiItem` fails + * but it could fail because not because it's too strict, but because the string is plain incorrect + * ex: normalizeSignature('bar') falls back to `string` as the response type + * since it can't tell if this is wrong since `ParseAbiItem` is too strict or if it really is invalid + * TODO: Some normalizations are missing + * ex 1: (JS wrong, type correct) 'event Barry(uint foo)' results in type `Barry(uint256)`, but the real return value is `Barry(uint)`. + * ex 2: (Both wrong) tuples doesn't get removed `foo(tuple(uint16, uint16))` does not get normalized to 'foo((uint16, uint16))' + */ +export type ToSignature = + ParseAbiItem extends never + ? string + : ParseAbiItem extends AbiFunction | AbiEvent + ? SignatureAbiItem> + : never + +type NormalizeSignatureParameters = Signature +type NormalizeSignatureReturnType = + ToSignature export type NormalizeSignatureErrorType = ErrorType -export function normalizeSignature( - signature: NormalizeSignatureParameters, -): NormalizeSignatureReturnType { +export function normalizeSignature( + signature: NormalizeSignatureParameters, +): NormalizeSignatureReturnType { let active = true let current = '' let level = 0 @@ -60,5 +91,5 @@ export function normalizeSignature( if (!valid) throw new BaseError('Unable to normalize signature.') - return result + return result as NormalizeSignatureReturnType } diff --git a/src/utils/hash/toSignature.ts b/src/utils/hash/toSignature.ts index 0a4902dffc..01fe74b49e 100644 --- a/src/utils/hash/toSignature.ts +++ b/src/utils/hash/toSignature.ts @@ -1,8 +1,14 @@ -import { type AbiEvent, type AbiFunction, formatAbiItem } from 'abitype' +import { + type AbiEvent, + type AbiFunction, + type SignatureAbiItem, + formatAbiItem, +} from 'abitype' import type { ErrorType } from '../../errors/utils.js' import { type NormalizeSignatureErrorType, + type ToSignature, normalizeSignature, } from './normalizeSignature.js' @@ -25,10 +31,16 @@ export type ToSignatureErrorType = NormalizeSignatureErrorType | ErrorType * }) * // 'ownerOf(uint256)' */ -export const toSignature = (def: string | AbiFunction | AbiEvent) => { +export const toSignature = ( + def: Def, +): Def extends AbiFunction | AbiEvent + ? SignatureAbiItem + : Def extends string + ? ToSignature + : never => { const def_ = (() => { if (typeof def === 'string') return def return formatAbiItem(def) })() - return normalizeSignature(def_) + return normalizeSignature(def_) as ToSignature }