Skip to content

Commit 69d14db

Browse files
committed
feat: add human-readable abi*
1 parent 8c68d23 commit 69d14db

File tree

17 files changed

+3298
-3
lines changed

17 files changed

+3298
-3
lines changed

src/core/AbiParameter.ts

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import type * as abitype from 'abitype'
2+
3+
import * as Errors from './Errors.js'
4+
import type { AssertName } from './internal/humanReadable/types/signatures.js'
5+
import type { Modifier } from './internal/humanReadable/types/signatures.js'
6+
import * as internal_regex from './internal/regex.js'
7+
import type { IsNarrowable, Join } from './internal/types.js'
8+
9+
/** Root type for ABI parameters. */
10+
export type AbiParameter = abitype.AbiParameter
11+
12+
/**
13+
* Decodes ABI-encoded data into its respective primitive values based on ABI Parameters.
14+
*
15+
* @example
16+
* ```ts twoslash
17+
* import { AbiParameters } from 'ox'
18+
*
19+
* const data = AbiParameters.decode(
20+
* AbiParameters.from(['string', 'uint', 'bool']),
21+
* '0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a4000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000057761676d69000000000000000000000000000000000000000000000000000000',
22+
* )
23+
* // @log: ['wagmi', 420n, true]
24+
* ```
25+
*
26+
* @example
27+
* ### JSON Parameters
28+
*
29+
* You can pass **JSON ABI** Parameters:
30+
*
31+
* ```ts twoslash
32+
* import { AbiParameters } from 'ox'
33+
*
34+
* const data = AbiParameters.decode(
35+
* [
36+
* { name: 'x', type: 'string' },
37+
* { name: 'y', type: 'uint' },
38+
* { name: 'z', type: 'bool' },
39+
* ],
40+
* '0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a4000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000057761676d69000000000000000000000000000000000000000000000000000000',
41+
* )
42+
* // @log: ['wagmi', 420n, true]
43+
* ```
44+
*
45+
* @param parameters - The set of ABI parameters to decode, in the shape of the `inputs` or `outputs` attribute of an ABI Item. These parameters must include valid [ABI types](https://docs.soliditylang.org/en/latest/types.html).
46+
* @param data - ABI encoded data.
47+
* @param options - Decoding options.
48+
* @returns Array of decoded values.
49+
*/
50+
export function format<
51+
const abiParameter extends AbiParameter | abitype.AbiEventParameter,
52+
>(abiParameter: abiParameter): format.ReturnType<abiParameter>
53+
54+
// eslint-disable-next-line jsdoc/require-jsdoc
55+
export function format(abiParameter: AbiParameter): string {
56+
let type = abiParameter.type
57+
if (
58+
internal_regex.tupleAbiParameterType.test(abiParameter.type) &&
59+
'components' in abiParameter
60+
) {
61+
type = '('
62+
const length = abiParameter.components.length as number
63+
for (let i = 0; i < length; i++) {
64+
const component = abiParameter.components[i]!
65+
type += format(component)
66+
if (i < length - 1) type += ', '
67+
}
68+
const result = internal_regex.execTyped<{ array?: string }>(
69+
internal_regex.tupleAbiParameterType,
70+
abiParameter.type,
71+
)
72+
type += `)${result?.array ?? ''}`
73+
return format({
74+
...abiParameter,
75+
type,
76+
})
77+
}
78+
// Add `indexed` to type if in `abiParameter`
79+
if ('indexed' in abiParameter && abiParameter.indexed)
80+
type = `${type} indexed`
81+
// Return human-readable ABI parameter
82+
if (abiParameter.name) return `${type} ${abiParameter.name}`
83+
return type
84+
}
85+
86+
export declare namespace format {
87+
type ReturnType<
88+
abiParameter extends AbiParameter | abitype.AbiEventParameter,
89+
> = abiParameter extends {
90+
name?: infer name extends string
91+
type: `tuple${infer array}`
92+
components: infer components extends readonly AbiParameter[]
93+
indexed?: infer indexed extends boolean
94+
}
95+
? format.ReturnType<
96+
{
97+
type: `(${Join<
98+
{
99+
[key in keyof components]: format.ReturnType<
100+
{
101+
type: components[key]['type']
102+
} & (IsNarrowable<components[key]['name'], string> extends true
103+
? { name: components[key]['name'] }
104+
: unknown) &
105+
(components[key] extends {
106+
components: readonly AbiParameter[]
107+
}
108+
? { components: components[key]['components'] }
109+
: unknown)
110+
>
111+
},
112+
', '
113+
>})${array}`
114+
} & (IsNarrowable<name, string> extends true
115+
? { name: name }
116+
: unknown) &
117+
(IsNarrowable<indexed, boolean> extends true
118+
? { indexed: indexed }
119+
: unknown)
120+
>
121+
: `${abiParameter['type']}${abiParameter extends { indexed: true }
122+
? ' indexed'
123+
: ''}${abiParameter['name'] extends infer name extends string
124+
? name extends ''
125+
? ''
126+
: ` ${AssertName<name>}`
127+
: ''}`
128+
129+
type ErrorType = Errors.GlobalErrorType
130+
}
131+
132+
export class InvalidAbiParameterError extends Errors.BaseError {
133+
override readonly name = 'AbiParameter.InvalidAbiParameterError'
134+
135+
constructor({ param }: { param: string | object }) {
136+
super('Failed to parse ABI parameter.', {
137+
details: `parseAbiParameter(${JSON.stringify(param, null, 2)})`,
138+
})
139+
}
140+
}
141+
142+
export class InvalidAbiParametersError extends Errors.BaseError {
143+
override readonly name = 'AbiParameter.InvalidAbiParametersError'
144+
145+
constructor({ params }: { params: string | object }) {
146+
super('Failed to parse ABI parameters.', {
147+
details: `parseAbiParameters(${JSON.stringify(params, null, 2)})`,
148+
})
149+
}
150+
}
151+
152+
export class InvalidParameterError extends Errors.BaseError {
153+
override readonly name = 'AbiParameter.InvalidParameterError'
154+
155+
constructor({ param }: { param: string }) {
156+
super('Invalid ABI parameter.', {
157+
details: param,
158+
})
159+
}
160+
}
161+
162+
export class SolidityProtectedKeywordError extends Errors.BaseError {
163+
override readonly name = 'AbiParameter.SolidityProtectedKeywordError'
164+
165+
constructor({ param, name }: { param: string; name: string }) {
166+
super('Invalid ABI parameter.', {
167+
details: param,
168+
metaMessages: [
169+
`"${name}" is a protected Solidity keyword. More info: https://docs.soliditylang.org/en/latest/cheatsheet.html`,
170+
],
171+
})
172+
}
173+
}
174+
175+
export class InvalidModifierError extends Errors.BaseError {
176+
override readonly name = 'AbiParameter.InvalidModifierError'
177+
178+
constructor({
179+
param,
180+
type,
181+
modifier,
182+
}: {
183+
param: string
184+
type?: abitype.AbiItemType | 'struct' | undefined
185+
modifier: Modifier
186+
}) {
187+
super('Invalid ABI parameter.', {
188+
details: param,
189+
metaMessages: [
190+
`Modifier "${modifier}" not allowed${
191+
type ? ` in "${type}" type` : ''
192+
}.`,
193+
],
194+
})
195+
}
196+
}
197+
198+
export class InvalidFunctionModifierError extends Errors.BaseError {
199+
override readonly name = 'AbiParameter.InvalidFunctionModifierError'
200+
201+
constructor({
202+
param,
203+
type,
204+
modifier,
205+
}: {
206+
param: string
207+
type?: abitype.AbiItemType | 'struct' | undefined
208+
modifier: Modifier
209+
}) {
210+
super('Invalid ABI parameter.', {
211+
details: param,
212+
metaMessages: [
213+
`Modifier "${modifier}" not allowed${
214+
type ? ` in "${type}" type` : ''
215+
}.`,
216+
`Data location can only be specified for array, struct, or mapping types, but "${modifier}" was given.`,
217+
],
218+
})
219+
}
220+
}
221+
222+
export class InvalidAbiTypeParameterError extends Errors.BaseError {
223+
override readonly name = 'AbiParameter.InvalidAbiTypeParameterError'
224+
225+
constructor({
226+
abiParameter,
227+
}: {
228+
abiParameter: AbiParameter & { indexed?: boolean | undefined }
229+
}) {
230+
super('Invalid ABI parameter.', {
231+
details: JSON.stringify(abiParameter, null, 2),
232+
metaMessages: ['ABI parameter type is invalid.'],
233+
})
234+
}
235+
}

src/core/Address.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import * as Caches from './Caches.js'
44
import * as Errors from './Errors.js'
55
import * as Hash from './Hash.js'
66
import * as PublicKey from './PublicKey.js'
7-
8-
const addressRegex = /*#__PURE__*/ /^0x[a-fA-F0-9]{40}$/
7+
import * as internal_regex from './internal/regex.js'
98

109
/** Root type for Address. */
1110
export type Address = abitype_Address
@@ -37,7 +36,7 @@ export function assert(
3736
): asserts value is Address {
3837
const { strict = true } = options
3938

40-
if (!addressRegex.test(value))
39+
if (!internal_regex.address.test(value))
4140
throw new InvalidAddressError({
4241
address: value,
4342
cause: new InvalidInputError(),

src/core/_test/AbiParameter.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { AbiParameter } from 'ox'
2+
import { describe, expect, test } from 'vitest'
3+
4+
describe('format', () => {
5+
test('default', () => {
6+
const formatted = AbiParameter.format({
7+
name: 'spender',
8+
type: 'address',
9+
})
10+
expect(formatted).toMatchInlineSnapshot(`"address spender"`)
11+
})
12+
})
13+
14+
test('exports', () => {
15+
expect(Object.keys(AbiParameter)).toMatchInlineSnapshot(`
16+
[
17+
"format",
18+
]
19+
`)
20+
})

0 commit comments

Comments
 (0)