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: signature from ABI item #255

Open
wants to merge 1 commit 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/silly-pugs-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"abitype": minor
---

construct signatures from ABI
27 changes: 27 additions & 0 deletions docs/pages/api/human.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,33 @@ Runtime functions for parsing and formatting human-readable ABIs.
These functions throw [errors](#errors-1) for invalid inputs. Make sure you handle errors appropriately.
:::

### `signatureAbiItem`

Formats [`Abi`](/api/types#abi) into the corresponding signature used to create selectors for both [functions](https://docs.soliditylang.org/en/develop/abi-spec.html#function-selector) and [events](https://docs.soliditylang.org/en/develop/abi-spec.html#events).

| Name | Description | Type |
| ------------ | ------------- | ----------------------------|
| `abiItem` | ABI item | `AbiFunction` \| `AbiEvent` |
| returns | signature | `string` (inferred) |

#### Example

```ts twoslash
import { signatureAbiItem } from 'abitype'

const result = signatureAbiItem({
// ^?



name: 'balanceOf',
type: 'function',
stateMutability: 'view',
inputs: [{ type: 'address', name: 'owner' }],
outputs: [{ type: 'uint256' }],
})
```

### `parseAbi`

Parses human-readable ABI into JSON [`Abi`](/api/types#abi).
Expand Down
1 change: 1 addition & 0 deletions packages/abitype/src/exports/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ it('exports', () => {
"parseAbiItem",
"parseAbiParameter",
"parseAbiParameters",
"signatureAbiItem",
"UnknownTypeError",
"InvalidAbiItemError",
"UnknownSolidityTypeError",
Expand Down
5 changes: 5 additions & 0 deletions packages/abitype/src/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ export {
type ParseAbiParameters,
} from '../human-readable/parseAbiParameters.js'

export {
signatureAbiItem,
type signatureAbiItem,
} from '../human-readable/signatureAbiItem.js'

export {
UnknownTypeError,
InvalidAbiItemError,
Expand Down
2 changes: 1 addition & 1 deletion packages/abitype/src/human-readable/formatAbiParameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export type FormatAbiParameter<
: ''}`

// https://regexr.com/7f7rv
const tupleRegex = /^tuple(?<array>(\[(\d*)\])*)$/
export const tupleRegex = /^tuple(?<array>(\[(\d*)\])*)$/

/**
* Formats {@link AbiParameter} to human-readable ABI parameter.
Expand Down
47 changes: 47 additions & 0 deletions packages/abitype/src/human-readable/signatureAbiItem.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Fragment as FragmentV5 } from '@ethersproject/abi'
import { Fragment as FragmentV6 } from 'ethers'
import { bench, describe } from 'vitest'

import { signatureAbiItem } from './signatureAbiItem.js'

describe('Generate Function ABI signature', () => {
const basic = {
name: 'balanceOf',
type: 'function',
stateMutability: 'view',
inputs: [{ type: 'address' }, { type: 'address' }],
outputs: [{ name: 'balance', type: 'uint256' }],
} as const

bench('abitype', () => {
signatureAbiItem(basic)
})

bench('ethers@6', () => {
FragmentV6.from(basic).format('sighash')
})

bench('ethers@5', () => {
FragmentV5.fromObject(basic).format('sighash')
})
})

describe('Generate Event ABI signature', () => {
const basic = {
name: 'foo',
type: 'event',
inputs: [{ type: 'uint256' }, { type: 'uint256' }],
} as const

bench('abitype', () => {
signatureAbiItem(basic)
})

bench('ethers@6', () => {
FragmentV6.from(basic).format('sighash')
})

bench('ethers@5', () => {
FragmentV5.fromObject(basic).format('sighash')
})
})
79 changes: 79 additions & 0 deletions packages/abitype/src/human-readable/signatureAbiItem.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { expectTypeOf, test } from 'vitest'

import type { SignatureAbiItem } from './signatureAbiItem.js'

test('SignatureAbiItem: function', () => {
expectTypeOf<
SignatureAbiItem<{
name: 'balanceOf'
type: 'function'
stateMutability: 'view'
inputs: [{ type: 'address' }, { type: 'address' }]
outputs: [{ name: 'balance'; type: 'uint256' }]
}>
>().toEqualTypeOf<'balanceOf(address,address)'>()
expectTypeOf<
SignatureAbiItem<{
name: 'balanceOf'
type: 'function'
stateMutability: 'view'
inputs: [{ type: 'address' }]
outputs: [{ name: 'balance'; type: 'uint256' }]
}>
>().toEqualTypeOf<'balanceOf(address)'>()
expectTypeOf<
SignatureAbiItem<{
name: 'balanceOf'
type: 'function'
stateMutability: 'view'
inputs: []
outputs: [{ name: 'balance'; type: 'uint256' }]
}>
>().toEqualTypeOf<'balanceOf()'>()
expectTypeOf<
SignatureAbiItem<{
type: 'function'
name: 'foo'
stateMutability: 'view'
inputs: [
{
type: 'tuple'
name: 'config'
components: [{ type: 'uint16'; name: '' }, { type: 'bool'; name: '' }]
},
]
outputs: []
}>
>().toEqualTypeOf<'foo((uint16,bool))'>()
})

test('SignatureAbiItem: event', () => {
expectTypeOf<
SignatureAbiItem<{
readonly name: 'foo'
readonly type: 'event'
readonly inputs: [{ type: 'uint256' }, { type: 'uint256' }]
}>
>().toEqualTypeOf<'foo(uint256,uint256)'>()
expectTypeOf<
SignatureAbiItem<{
readonly name: 'foo'
readonly type: 'event'
readonly inputs: [{ type: 'uint256' }]
}>
>().toEqualTypeOf<'foo(uint256)'>()
expectTypeOf<
SignatureAbiItem<{
readonly name: 'foo'
readonly type: 'event'
readonly inputs: []
}>
>().toEqualTypeOf<'foo()'>()
expectTypeOf<
SignatureAbiItem<{
name: 'foo'
type: 'event'
inputs: [{ type: 'tuple'; components: [{ type: 'uint256' }] }]
}>
>().toEqualTypeOf<'foo((uint256))'>()
})
93 changes: 93 additions & 0 deletions packages/abitype/src/human-readable/signatureAbiItem.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { expect, test } from 'vitest'
import { signatureAbiItem } from './signatureAbiItem.js'

test.each([
{
signature: signatureAbiItem({
name: 'balanceOf',
type: 'function',
stateMutability: 'view',
inputs: [{ type: 'address' }, { type: 'address' }],
outputs: [{ name: 'balance', type: 'uint256' }],
}),
expected: 'balanceOf(address,address)',
},
{
signature: signatureAbiItem({
name: 'balanceOf',
type: 'function',
stateMutability: 'view',
inputs: [{ type: 'address' }],
outputs: [{ name: 'balance', type: 'uint256' }],
}),
expected: 'balanceOf(address)',
},
{
signature: signatureAbiItem({
name: 'balanceOf',
type: 'function',
stateMutability: 'view',
inputs: [],
outputs: [{ name: 'balance', type: 'uint256' }],
}),
expected: 'balanceOf()',
},
{
signature: signatureAbiItem({
type: 'function',
name: 'foo',
stateMutability: 'view',
inputs: [
{
type: 'tuple',
name: 'config',
components: [
{ type: 'uint16', name: '' },
{ type: 'bool', name: '' },
],
},
],
outputs: [],
}),
expected: 'foo((uint16,bool))',
},
])('signatureAbiItem: function', (t) => {
expect(t.signature).toEqual(t.expected)
})

test.each([
{
signature: signatureAbiItem({
name: 'foo',
type: 'event',
inputs: [{ type: 'uint256' }, { type: 'uint256' }],
}),
expected: 'foo(uint256,uint256)',
},
{
signature: signatureAbiItem({
name: 'foo',
type: 'event',
inputs: [{ type: 'uint256' }],
}),
expected: 'foo(uint256)',
},
{
signature: signatureAbiItem({
name: 'foo',
type: 'event',
inputs: [],
}),
expected: 'foo()',
},
{
signature: signatureAbiItem({
name: 'foo',
type: 'event',
inputs: [{ type: 'tuple', components: [{ type: 'uint256' }] }],
}),
expected: 'foo((uint256))',
},
])('signatureAbiItem: event', (t) => {
expect(t.signature).toEqual(t.expected)
})
105 changes: 105 additions & 0 deletions packages/abitype/src/human-readable/signatureAbiItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type {
AbiEvent,
AbiEventParameter,
AbiFunction,
AbiParameter,
} from '../abi.js'
import type { AssertName } from '../human-readable/types/signatures.js'
import { execTyped } from '../regex.js'
import { tupleRegex } from './formatAbiParameter.js'

export type TypesToCSV<
T extends readonly (AbiParameter | AbiEventParameter)[],
> = T extends readonly [
infer First extends AbiParameter | AbiEventParameter,
...infer Rest extends readonly (AbiParameter | AbiEventParameter)[],
]
? `${First extends {
components: infer Component extends readonly (
| AbiParameter
| AbiEventParameter
)[]
}
? `(${TypesToCSV<Component>})`
: First['type']}${Rest['length'] extends 0 ? '' : `,${TypesToCSV<Rest>}`}`
: ''

/**
* Formats an ABI item into a signature
*
* @param abiItem - ABI item
* @returns A signature
*
* @example
* type Result = SignatureAbiParameter({ name: 'foo', type: 'event', inputs: [{ type: 'uint256' }, { type: 'uint256' }] })
* // ^? type Result: foo(uint256,uint256)
*/
export type SignatureAbiItem<abiItem extends AbiFunction | AbiEvent> =
| AbiFunction
| AbiEvent extends abiItem
? string
: `${AssertName<abiItem['name']>}(${TypesToCSV<abiItem['inputs']>})`

/**
* Formats an ABI function into a signature
* Function: https://docs.soliditylang.org/en/develop/abi-spec.html#function-selector
* Event: https://docs.soliditylang.org/en/develop/abi-spec.html#events
*
* @param abiItem - ABI item
* @returns A signature
*
* @example
* const signatureAbi = signatureAbiItem({ name: 'foo', type: 'event', inputs: [{ type: 'uint256' }, { type: 'uint256' }] })
* // ^? const signatureAbiItem: foo(uint256,uint256)
*/
export function signatureAbiItem<const abiItem extends AbiFunction | AbiEvent>(
abiItem: abiItem,
): SignatureAbiItem<abiItem> {
return `${abiItem.name}(${abiItem.inputs.map((param) => signatureAbiParameter(param)).join(',')})` as SignatureAbiItem<abiItem>
}

/**
* Formats an ABI parameter into a piece of a signature
*
* @param abiParameter - ABI parameter
* @returns A piece of a signature
*
* @example
* type Result = SignatureAbiParameter({ type: 'uint256' })
* // ^? type Result: uint256
*/
export type SignatureAbiParameter<
abiParameter extends AbiParameter | AbiEventParameter,
> = TypesToCSV<[abiParameter]>[0]

/**
* Formats an ABI parameter into a piece of a signature
*
* @param abiParameter - ABI parameter
* @returns A piece of a signature
*
* @example
* const signatureAbi = signatureAbiParameter({ type: 'uint256' })
* // ^? const signatureAbiItem: uint256
*/
export function signatureAbiParameter<
const abiParameter extends AbiParameter | AbiEventParameter,
>(abiParameter: abiParameter): SignatureAbiParameter<abiParameter> {
let type = abiParameter.type
if (tupleRegex.test(abiParameter.type) && 'components' in abiParameter) {
type = '('
const length = abiParameter.components.length as number
for (let i = 0; i < length; i++) {
const component = abiParameter.components[i]!
type += signatureAbiParameter(component)
if (i < length - 1) type += ','
}
const result = execTyped<{ array?: string }>(tupleRegex, abiParameter.type)
type += `)${result?.array ?? ''}`
return signatureAbiParameter({
...abiParameter,
type,
}) as SignatureAbiParameter<abiParameter>
}
return abiParameter.type
}