From c20c16e7d905f4e91ce6e2d50c002751c7595379 Mon Sep 17 00:00:00 2001 From: tmm Date: Fri, 19 Jun 2026 14:13:26 -0400 Subject: [PATCH 1/5] feat(abi): move human-readable utils into ox --- .changeset/abi-human-readable-ox.md | 5 + package.json | 5 + site/src/pages/guides/abi.md | 164 ++ src/core/Abi.bench.ts | 30 + src/core/Abi.ts | 89 +- src/core/AbiConstructor.ts | 5 +- src/core/AbiError.ts | 8 +- src/core/AbiEvent.ts | 8 +- src/core/AbiFunction.ts | 8 +- src/core/AbiItem.bench.ts | 19 + src/core/AbiItem.ts | 102 +- src/core/AbiParameter.bench.ts | 35 + src/core/AbiParameter.ts | 172 ++ src/core/AbiParameters.bench.ts | 22 + src/core/AbiParameters.ts | 81 +- src/core/RpcSchema.ts | 2 +- src/core/_test/Abi.test.ts | 4 + src/core/_test/AbiItem.test.ts | 3 + src/core/_test/AbiParameter.test-d.ts | 41 + src/core/_test/AbiParameter.test.ts | 87 + src/core/_test/AbiParameters.test.ts | 7 + src/core/_test/index.test.ts | 1 + .../_snap/formatAbi.test.ts.snap | 74 + .../_snap/parseAbi.test.ts.snap | 2353 +++++++++++++++++ .../internal/human-readable/errors.test.ts | 265 ++ src/core/internal/human-readable/errors.ts | 205 ++ .../human-readable/formatAbi.test-d.ts | 144 + .../internal/human-readable/formatAbi.test.ts | 23 + src/core/internal/human-readable/formatAbi.ts | 37 + .../human-readable/formatAbiItem.test-d.ts | 115 + .../human-readable/formatAbiItem.test.ts | 102 + .../internal/human-readable/formatAbiItem.ts | 136 + .../formatAbiParameter.test-d.ts | 125 + .../human-readable/formatAbiParameter.test.ts | 126 + .../human-readable/formatAbiParameter.ts | 86 + .../formatAbiParameters.test-d.ts | 92 + .../formatAbiParameters.test.ts | 32 + .../human-readable/formatAbiParameters.ts | 46 + .../human-readable/human-readable.bench-d.ts | 59 + .../human-readable/integration.test.ts | 68 + .../human-readable/parseAbi.test-d.ts | 188 ++ .../internal/human-readable/parseAbi.test.ts | 82 + src/core/internal/human-readable/parseAbi.ts | 59 + .../human-readable/parseAbiItem.test-d.ts | 183 ++ .../human-readable/parseAbiItem.test.ts | 264 ++ .../internal/human-readable/parseAbiItem.ts | 91 + .../parseAbiParameter.test-d.ts | 68 + .../human-readable/parseAbiParameter.test.ts | 207 ++ .../human-readable/parseAbiParameter.ts | 95 + .../parseAbiParameters.test-d.ts | 160 ++ .../human-readable/parseAbiParameters.test.ts | 80 + .../human-readable/parseAbiParameters.ts | 123 + src/core/internal/human-readable/regex.ts | 15 + .../internal/human-readable/runtime/cache.ts | 96 + .../human-readable/runtime/signatures.test.ts | 239 ++ .../human-readable/runtime/signatures.ts | 106 + .../human-readable/runtime/structs.test.ts | 211 ++ .../human-readable/runtime/structs.ts | 97 + .../human-readable/runtime/utils.test.ts | 721 +++++ .../internal/human-readable/runtime/utils.ts | 352 +++ src/core/internal/human-readable/types.ts | 63 + .../human-readable/types/signatures.test-d.ts | 255 ++ .../human-readable/types/signatures.ts | 308 +++ .../human-readable/types/structs.test-d.ts | 221 ++ .../internal/human-readable/types/structs.ts | 86 + .../human-readable/types/utils.test-d.ts | 1029 +++++++ .../internal/human-readable/types/utils.ts | 390 +++ src/index.ts | 38 + src/zod/RpcSchema.ts | 10 +- 69 files changed, 10788 insertions(+), 35 deletions(-) create mode 100644 .changeset/abi-human-readable-ox.md create mode 100644 src/core/Abi.bench.ts create mode 100644 src/core/AbiParameter.bench.ts create mode 100644 src/core/AbiParameter.ts create mode 100644 src/core/_test/AbiParameter.test-d.ts create mode 100644 src/core/_test/AbiParameter.test.ts create mode 100644 src/core/internal/human-readable/_snap/formatAbi.test.ts.snap create mode 100644 src/core/internal/human-readable/_snap/parseAbi.test.ts.snap create mode 100644 src/core/internal/human-readable/errors.test.ts create mode 100644 src/core/internal/human-readable/errors.ts create mode 100644 src/core/internal/human-readable/formatAbi.test-d.ts create mode 100644 src/core/internal/human-readable/formatAbi.test.ts create mode 100644 src/core/internal/human-readable/formatAbi.ts create mode 100644 src/core/internal/human-readable/formatAbiItem.test-d.ts create mode 100644 src/core/internal/human-readable/formatAbiItem.test.ts create mode 100644 src/core/internal/human-readable/formatAbiItem.ts create mode 100644 src/core/internal/human-readable/formatAbiParameter.test-d.ts create mode 100644 src/core/internal/human-readable/formatAbiParameter.test.ts create mode 100644 src/core/internal/human-readable/formatAbiParameter.ts create mode 100644 src/core/internal/human-readable/formatAbiParameters.test-d.ts create mode 100644 src/core/internal/human-readable/formatAbiParameters.test.ts create mode 100644 src/core/internal/human-readable/formatAbiParameters.ts create mode 100644 src/core/internal/human-readable/human-readable.bench-d.ts create mode 100644 src/core/internal/human-readable/integration.test.ts create mode 100644 src/core/internal/human-readable/parseAbi.test-d.ts create mode 100644 src/core/internal/human-readable/parseAbi.test.ts create mode 100644 src/core/internal/human-readable/parseAbi.ts create mode 100644 src/core/internal/human-readable/parseAbiItem.test-d.ts create mode 100644 src/core/internal/human-readable/parseAbiItem.test.ts create mode 100644 src/core/internal/human-readable/parseAbiItem.ts create mode 100644 src/core/internal/human-readable/parseAbiParameter.test-d.ts create mode 100644 src/core/internal/human-readable/parseAbiParameter.test.ts create mode 100644 src/core/internal/human-readable/parseAbiParameter.ts create mode 100644 src/core/internal/human-readable/parseAbiParameters.test-d.ts create mode 100644 src/core/internal/human-readable/parseAbiParameters.test.ts create mode 100644 src/core/internal/human-readable/parseAbiParameters.ts create mode 100644 src/core/internal/human-readable/regex.ts create mode 100644 src/core/internal/human-readable/runtime/cache.ts create mode 100644 src/core/internal/human-readable/runtime/signatures.test.ts create mode 100644 src/core/internal/human-readable/runtime/signatures.ts create mode 100644 src/core/internal/human-readable/runtime/structs.test.ts create mode 100644 src/core/internal/human-readable/runtime/structs.ts create mode 100644 src/core/internal/human-readable/runtime/utils.test.ts create mode 100644 src/core/internal/human-readable/runtime/utils.ts create mode 100644 src/core/internal/human-readable/types.ts create mode 100644 src/core/internal/human-readable/types/signatures.test-d.ts create mode 100644 src/core/internal/human-readable/types/signatures.ts create mode 100644 src/core/internal/human-readable/types/structs.test-d.ts create mode 100644 src/core/internal/human-readable/types/structs.ts create mode 100644 src/core/internal/human-readable/types/utils.test-d.ts create mode 100644 src/core/internal/human-readable/types/utils.ts diff --git a/.changeset/abi-human-readable-ox.md b/.changeset/abi-human-readable-ox.md new file mode 100644 index 00000000..bd00fc2e --- /dev/null +++ b/.changeset/abi-human-readable-ox.md @@ -0,0 +1,5 @@ +--- +'ox': minor +--- + +Added human-readable ABI parsing and formatting utilities from `abitype` to ox, including runtime validation and type-level safety for `Abi`, `AbiItem`, `AbiParameters`, and the new `AbiParameter` module. diff --git a/package.json b/package.json index 052915cc..32c4c283 100644 --- a/package.json +++ b/package.json @@ -209,6 +209,11 @@ "types": "./dist/core/AbiItem.d.ts", "default": "./dist/core/AbiItem.js" }, + "./AbiParameter": { + "src": "./src/core/AbiParameter.ts", + "types": "./dist/core/AbiParameter.d.ts", + "default": "./dist/core/AbiParameter.js" + }, "./AbiParameters": { "src": "./src/core/AbiParameters.ts", "types": "./dist/core/AbiParameters.d.ts", diff --git a/site/src/pages/guides/abi.md b/site/src/pages/guides/abi.md index 511905e4..30c23a09 100644 --- a/site/src/pages/guides/abi.md +++ b/site/src/pages/guides/abi.md @@ -9,6 +9,170 @@ description: "Encode, decode, and parse Ethereum ABIs." The **Application Binary Interface (ABI)** is the standardized protocol for interacting with smart contracts in the Ethereum ecosystem. It defines how data is encoded and decoded for **Consumer (e.g. Application, Wallet, Server, etc) ↔ Contract** communication, as well as **Contract → Contract** communication. +## Human-readable ABIs + +Ox can parse Solidity-style human-readable ABI signatures into JSON ABI objects, and format JSON ABI objects back into human-readable signatures. + +```ts twoslash +import { Abi, AbiItem, AbiParameter, AbiParameters } from 'ox' + +const abi = Abi.from([ + 'function approve(address spender, uint256 amount) returns (bool)', + 'event Transfer(address indexed from, address indexed to, uint256 amount)', +]) +abi +//^? +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +const item = AbiItem.from( + 'function approve(address spender, uint256 amount) returns (bool)', +) +item +// ^? +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +const parameters = AbiParameters.from('address spender, uint256 amount') +parameters +// ^? +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +const parameter = AbiParameter.from('address spender') +parameter +// ^? +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +const formatted = Abi.format(abi) +formatted +// ^? +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +``` + +Human-readable signatures support functions, events, errors, constructors, fallback functions, receive functions, structs, and ABI parameters. + +```ts +'function balanceOf(address owner) view returns (uint256)' +'event Transfer(address indexed from, address indexed to, uint256 amount)' +'error Unauthorized(address caller)' +'constructor(address owner) payable' +'fallback() external payable' +'receive() external payable' +'struct Account { address owner; uint256 balance; }' +'address spender' +'address spender, uint256 amount' +``` + +Some syntax rules are enforced for parity between runtime parsing and type-level inference: + +- **Whitespace matters.** For example, `'function name() returns (string)'` is valid, but `'function name()returns(string)'` is not. +- **Semicolons are omitted.** Write `'function name()'`, not `'function name();'`. +- **Named and unnamed parameters are both supported.** For example, `'address owner'` and `'address'` are both valid. +- **Inline tuples are supported.** For example, `'(uint256 id, string name) account'` maps to a `tuple` ABI parameter. +- **Struct signatures can be provided before the signature that uses them.** Recursive structs are not supported. +- **Parameter modifiers are normalized.** Modifiers like `indexed`, `calldata`, `memory`, and `storage` are accepted where Solidity allows them and omitted from the JSON ABI shape when they are not represented there. + ## Encoding and Decoding ABI Parameters To start, let's take a look at how we can encode and decode primitive ABI types and parameters using Ox. While encoding and decoding ABI parameters might not be directly useful in isolation, they form the foundation of interacting with smart contracts, as listed in the [Applications](#applications) section below. diff --git a/src/core/Abi.bench.ts b/src/core/Abi.bench.ts new file mode 100644 index 00000000..83197d6b --- /dev/null +++ b/src/core/Abi.bench.ts @@ -0,0 +1,30 @@ +import { bench, describe } from 'vp/test' +import * as Abi from './Abi.js' + +const signatures = [ + 'function name() view returns (string)', + 'function symbol() view returns (string)', + 'function decimals() view returns (uint8)', + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address owner) view returns (uint256)', + 'function transfer(address to, uint256 amount) returns (bool)', + 'function transferFrom(address from, address to, uint256 amount) returns (bool)', + 'function approve(address spender, uint256 amount) returns (bool)', + 'function allowance(address owner, address spender) view returns (uint256)', + 'event Transfer(address indexed from, address indexed to, uint256 amount)', + 'event Approval(address indexed owner, address indexed spender, uint256 amount)', +] as const + +const abi = Abi.from(signatures) + +describe('Abi.from', () => { + bench('erc20 human-readable ABI', () => { + Abi.from(signatures) + }) +}) + +describe('Abi.format', () => { + bench('erc20 JSON ABI', () => { + Abi.format(abi) + }) +}) diff --git a/src/core/Abi.ts b/src/core/Abi.ts index 4e418c9b..3485ef8e 100644 --- a/src/core/Abi.ts +++ b/src/core/Abi.ts @@ -2,10 +2,19 @@ import * as abitype from 'abitype' import type * as Errors from './Errors.js' import * as internal from './internal/abi.js' import type * as AbiItem_internal from './internal/abiItem.js' +import * as formatAbi from './internal/human-readable/formatAbi.js' +import * as parseAbi from './internal/human-readable/parseAbi.js' /** Root type for an ABI. */ export type Abi = abitype.Abi +export { + InvalidSignatureError, + InvalidStructSignatureError, + UnknownSignatureError, +} from './internal/human-readable/errors.js' +export { CircularReferenceError } from './internal/human-readable/errors.js' + /** @internal */ export function format(abi: abi): format.ReturnType /** @@ -36,6 +45,30 @@ export function format(abi: abi): format.ReturnType * * formatted * // ^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // * ``` * * @param abi - The ABI to format. @@ -44,12 +77,12 @@ export function format(abi: abi): format.ReturnType export function format(abi: Abi | readonly unknown[]): readonly string[] // eslint-disable-next-line jsdoc-js/require-jsdoc export function format(abi: Abi | readonly unknown[]): format.ReturnType { - return abitype.formatAbi(abi) as never + return formatAbi.formatAbi(abi) as never } export declare namespace format { type ReturnType = - abitype.FormatAbi + formatAbi.FormatAbi type ErrorType = Errors.GlobalErrorType } @@ -91,6 +124,30 @@ export function from( * * abi * //^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // * ``` * * @example @@ -105,6 +162,30 @@ export function from( * * abi * //^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // * ``` * * @param abi - The ABI to parse. @@ -113,14 +194,14 @@ export function from( export function from(abi: Abi | readonly string[]): Abi // eslint-disable-next-line jsdoc-js/require-jsdoc export function from(abi: Abi | readonly string[]): from.ReturnType { - if (internal.isSignatures(abi)) return abitype.parseAbi(abi) + if (internal.isSignatures(abi)) return parseAbi.parseAbi(abi) return abi } export declare namespace from { type ReturnType< abi extends Abi | readonly string[] | readonly unknown[] = Abi, - > = abi extends readonly string[] ? abitype.ParseAbi : abi + > = abi extends readonly string[] ? parseAbi.ParseAbi : abi type ErrorType = Errors.GlobalErrorType } diff --git a/src/core/AbiConstructor.ts b/src/core/AbiConstructor.ts index a9b31bcd..1c2a5c41 100644 --- a/src/core/AbiConstructor.ts +++ b/src/core/AbiConstructor.ts @@ -5,6 +5,7 @@ import * as AbiParameters from './AbiParameters.js' import * as Errors from './Errors.js' import * as Hex from './Hex.js' import type * as internal from './internal/abiConstructor.js' +import * as formatAbiItem from './internal/human-readable/formatAbiItem.js' import type { IsNarrowable } from './internal/types.js' /** Root type for an {@link ox#AbiItem.AbiItem} with a `constructor` type. */ @@ -316,12 +317,12 @@ export function format( export function format(abiConstructor: AbiConstructor): string /** @internal */ export function format(abiConstructor: AbiConstructor): format.ReturnType { - return abitype.formatAbiItem(abiConstructor) + return formatAbiItem.formatAbiItem(abiConstructor) } export declare namespace format { type ReturnType = - abitype.FormatAbiItem + formatAbiItem.FormatAbiItem type ErrorType = Errors.GlobalErrorType } diff --git a/src/core/AbiError.ts b/src/core/AbiError.ts index b5aecc83..c5acef14 100644 --- a/src/core/AbiError.ts +++ b/src/core/AbiError.ts @@ -6,6 +6,7 @@ import type * as Errors from './Errors.js' import * as Hex from './Hex.js' import type * as internal from './internal/abiError.js' import type * as AbiItem_internal from './internal/abiItem.js' +import * as formatAbiItem from './internal/human-readable/formatAbiItem.js' import type { IsNarrowable, IsNever } from './internal/types.js' /** Root type for an {@link ox#AbiItem.AbiItem} with an `error` type. */ @@ -510,11 +511,14 @@ export declare namespace encode { */ export function format( abiError: abiError | AbiError, -): abitype.FormatAbiItem { - return abitype.formatAbiItem(abiError) as never +): format.ReturnType { + return formatAbiItem.formatAbiItem(abiError) as never } export declare namespace format { + type ReturnType = + formatAbiItem.FormatAbiItem + type ErrorType = Errors.GlobalErrorType } diff --git a/src/core/AbiEvent.ts b/src/core/AbiEvent.ts index 1b19fe94..a94e2060 100644 --- a/src/core/AbiEvent.ts +++ b/src/core/AbiEvent.ts @@ -11,6 +11,7 @@ import type * as internal from './internal/abiEvent.js' import type * as AbiItem_internal from './internal/abiItem.js' import * as Cursor from './internal/cursor.js' import { prettyPrint } from './internal/errors.js' +import * as formatAbiItem from './internal/human-readable/formatAbiItem.js' import type { Compute, IsNarrowable } from './internal/types.js' /** Root type for an {@link ox#AbiItem.AbiItem} with an `event` type. */ @@ -1334,11 +1335,14 @@ export declare namespace encode { */ export function format( abiEvent: abiEvent | AbiEvent, -): abitype.FormatAbiItem { - return abitype.formatAbiItem(abiEvent) as never +): format.ReturnType { + return formatAbiItem.formatAbiItem(abiEvent) as never } export declare namespace format { + type ReturnType = + formatAbiItem.FormatAbiItem + type ErrorType = Errors.GlobalErrorType } diff --git a/src/core/AbiFunction.ts b/src/core/AbiFunction.ts index e6aac376..e5d55ee0 100644 --- a/src/core/AbiFunction.ts +++ b/src/core/AbiFunction.ts @@ -7,6 +7,7 @@ import * as Hex from './Hex.js' import type * as internal from './internal/abiFunction.js' import type * as AbiItem_internal from './internal/abiItem.js' import type * as AbiParameters_internal from './internal/abiParameters.js' +import * as formatAbiItem from './internal/human-readable/formatAbiItem.js' import type { IsNarrowable } from './internal/types.js' /** Root type for an {@link ox#AbiItem.AbiItem} with a `function` type. */ @@ -864,11 +865,14 @@ export declare namespace encodeResult { */ export function format( abiFunction: abiFunction | AbiFunction, -): abitype.FormatAbiItem { - return abitype.formatAbiItem(abiFunction) as never +): format.ReturnType { + return formatAbiItem.formatAbiItem(abiFunction) as never } export declare namespace format { + type ReturnType = + formatAbiItem.FormatAbiItem + type ErrorType = Errors.GlobalErrorType } diff --git a/src/core/AbiItem.bench.ts b/src/core/AbiItem.bench.ts index 2c235027..1523e0aa 100644 --- a/src/core/AbiItem.bench.ts +++ b/src/core/AbiItem.bench.ts @@ -38,3 +38,22 @@ describe('AbiItem.fromAbi (name)', () => { AbiItem.fromAbi(abi200, 'fn179') }) }) + +describe('AbiItem.from', () => { + bench('function transfer(address to, uint256 amount)', () => { + AbiItem.from('function transfer(address to, uint256 amount) returns (bool)') + }) + + bench('struct Foo', () => { + AbiItem.from([ + 'struct Foo { address spender; uint256 amount; }', + 'function approve(Foo foo) returns (bool)', + ]) + }) +}) + +describe('AbiItem.format', () => { + bench('function transfer(address to, uint256 amount)', () => { + AbiItem.format(lastFn) + }) +}) diff --git a/src/core/AbiItem.ts b/src/core/AbiItem.ts index cf06e30b..680abf19 100644 --- a/src/core/AbiItem.ts +++ b/src/core/AbiItem.ts @@ -1,14 +1,21 @@ -import * as abitype from 'abitype' import type * as Abi from './Abi.js' import * as Errors from './Errors.js' import * as Hash from './Hash.js' import * as Hex from './Hex.js' +import * as formatAbiItem from './internal/human-readable/formatAbiItem.js' +import * as parseAbiItem from './internal/human-readable/parseAbiItem.js' import * as internal from './internal/abiItem.js' import type { UnionCompute } from './internal/types.js' /** Root type for an item on an {@link ox#Abi.Abi}. */ export type AbiItem = Abi.Abi[number] +export { + InvalidAbiItemError, + UnknownSolidityTypeError, + UnknownTypeError, +} from './internal/human-readable/errors.js' + /** * Extracts an {@link ox#AbiItem.AbiItem} item from an {@link ox#Abi.Abi}, given a name. * @@ -89,11 +96,14 @@ export type ExtractNames = Extract< */ export function format( abiItem: abiItem | AbiItem, -): abitype.FormatAbiItem { - return abitype.formatAbiItem(abiItem) as never +): format.ReturnType { + return formatAbiItem.formatAbiItem(abiItem) as never } export declare namespace format { + type ReturnType = + formatAbiItem.FormatAbiItem + type ErrorType = Errors.GlobalErrorType } @@ -125,6 +135,30 @@ export declare namespace format { * * abiItem * //^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // * ``` * * @example @@ -141,6 +175,30 @@ export declare namespace format { * * abiItem * //^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // * ``` * * @example @@ -156,6 +214,30 @@ export declare namespace format { * * abiItem * //^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // * ``` * * @@ -178,9 +260,9 @@ export function from< ): from.ReturnType { const { prepare = true } = options const item = (() => { - if (Array.isArray(abiItem)) return abitype.parseAbiItem(abiItem) + if (Array.isArray(abiItem)) return parseAbiItem.parseAbiItem(abiItem) if (typeof abiItem === 'string') - return abitype.parseAbiItem(abiItem as never) + return parseAbiItem.parseAbiItem(abiItem as never) return abiItem })() as AbiItem return { @@ -202,9 +284,9 @@ export declare namespace from { type ReturnType = abiItem extends string - ? abitype.ParseAbiItem + ? parseAbiItem.ParseAbiItem : abiItem extends readonly string[] - ? abitype.ParseAbiItem + ? parseAbiItem.ParseAbiItem : abiItem type ErrorType = Errors.GlobalErrorType @@ -554,7 +636,7 @@ export function getSignature( })() const signature = (() => { if (typeof abiItem === 'string') return abiItem - return abitype.formatAbiItem(abiItem) + return formatAbiItem.formatAbiItem(abiItem) })() return internal.normalizeSignature(signature) } @@ -692,8 +774,8 @@ export class AmbiguityError extends Errors.BaseError { super('Found ambiguous types in overloaded ABI Items.', { metaMessages: [ // TODO: abitype to add support for signature-formatted ABI items. - `\`${x.type}\` in \`${internal.normalizeSignature(abitype.formatAbiItem(x.abiItem))}\`, and`, - `\`${y.type}\` in \`${internal.normalizeSignature(abitype.formatAbiItem(y.abiItem))}\``, + `\`${x.type}\` in \`${internal.normalizeSignature(formatAbiItem.formatAbiItem(x.abiItem))}\`, and`, + `\`${y.type}\` in \`${internal.normalizeSignature(formatAbiItem.formatAbiItem(y.abiItem))}\``, '', 'These types encode differently and cannot be distinguished at runtime.', 'Remove one of the ambiguous items in the ABI.', diff --git a/src/core/AbiParameter.bench.ts b/src/core/AbiParameter.bench.ts new file mode 100644 index 00000000..a24195cf --- /dev/null +++ b/src/core/AbiParameter.bench.ts @@ -0,0 +1,35 @@ +import { bench, describe } from 'vp/test' +import * as AbiParameter from './AbiParameter.js' + +const parameter = { name: 'spender', type: 'address' } as const +const tuple = { + components: [ + { name: 'spender', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + name: 'foo', + type: 'tuple', +} as const + +describe('AbiParameter.from', () => { + bench('address spender', () => { + AbiParameter.from('address spender') + }) + + bench('struct Foo', () => { + AbiParameter.from([ + 'struct Foo { address spender; uint256 amount; }', + 'Foo foo', + ]) + }) +}) + +describe('AbiParameter.format', () => { + bench('address spender', () => { + AbiParameter.format(parameter) + }) + + bench('(address spender, uint256 amount) foo', () => { + AbiParameter.format(tuple) + }) +}) diff --git a/src/core/AbiParameter.ts b/src/core/AbiParameter.ts new file mode 100644 index 00000000..d40fb77c --- /dev/null +++ b/src/core/AbiParameter.ts @@ -0,0 +1,172 @@ +import type * as abitype from 'abitype' +import type * as Errors from './Errors.js' +import * as formatAbiParameter from './internal/human-readable/formatAbiParameter.js' +import * as parseAbiParameter from './internal/human-readable/parseAbiParameter.js' + +/** Root type for an ABI parameter. */ +export type AbiParameter = abitype.AbiParameter + +/** A parameter on an ABI event. */ +export type AbiEventParameter = abitype.AbiEventParameter + +export { + InvalidAbiParameterError, + InvalidAbiTypeParameterError, + InvalidFunctionModifierError, + InvalidModifierError, + InvalidParameterError, + SolidityProtectedKeywordError, +} from './internal/human-readable/errors.js' +export { InvalidParenthesisError } from './internal/human-readable/errors.js' + +/** + * Formats an {@link ox#AbiParameter.AbiParameter} into a **Human Readable ABI Parameter**. + * + * @example + * ```ts twoslash + * import { AbiParameter } from 'ox' + * + * const formatted = AbiParameter.format({ + * name: 'spender', + * type: 'address' + * }) + * + * formatted + * // ^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * ``` + * + * @param parameter - The ABI Parameter to format. + * @returns The formatted ABI Parameter. + */ +export function format( + parameter: parameter | AbiParameter | AbiEventParameter, +): format.ReturnType { + return formatAbiParameter.formatAbiParameter(parameter as parameter) as never +} + +export declare namespace format { + type ReturnType< + parameter extends AbiParameter | AbiEventParameter = AbiParameter, + > = formatAbiParameter.FormatAbiParameter + + type ErrorType = Errors.GlobalErrorType +} + +/** + * Parses an arbitrary **JSON ABI Parameter** or **Human Readable ABI Parameter** into a typed {@link ox#AbiParameter.AbiParameter}. + * + * @example + * ### JSON Parameters + * + * ```ts twoslash + * import { AbiParameter } from 'ox' + * + * const parameter = AbiParameter.from({ + * name: 'spender', + * type: 'address' + * }) + * + * parameter + * //^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * ``` + * + * @example + * ### Human Readable Parameters + * + * ```ts twoslash + * import { AbiParameter } from 'ox' + * + * const parameter = AbiParameter.from('address spender') + * + * parameter + * //^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * ``` + * + * @example + * It is possible to specify `struct`s along with your definition: + * + * ```ts twoslash + * import { AbiParameter } from 'ox' + * + * const parameter = AbiParameter.from([ + * 'struct Foo { address spender; uint256 amount; }', + * 'Foo foo' + * ]) + * + * parameter + * //^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * ``` + * + * @param parameter - The ABI Parameter to parse. + * @returns The typed ABI Parameter. + */ +export function from< + const parameter extends AbiParameter | string | readonly string[], +>( + parameter: parameter | AbiParameter | string | readonly string[], +): from.ReturnType { + if (Array.isArray(parameter)) + return parseAbiParameter.parseAbiParameter(parameter) as never + if (typeof parameter === 'string') + return parseAbiParameter.parseAbiParameter(parameter) as never + return parameter as never +} + +export declare namespace from { + type ReturnType = + parameter extends string + ? parseAbiParameter.ParseAbiParameter + : parameter extends readonly string[] + ? parseAbiParameter.ParseAbiParameter + : parameter + + type ErrorType = Errors.GlobalErrorType +} diff --git a/src/core/AbiParameters.bench.ts b/src/core/AbiParameters.bench.ts index dc186667..dfa0af20 100644 --- a/src/core/AbiParameters.bench.ts +++ b/src/core/AbiParameters.bench.ts @@ -80,3 +80,25 @@ describe('AbiParameters.decode', () => { AbiParameters.decode(tuple_nested, tuple_nested_encoded) }) }) + +describe('AbiParameters.from', () => { + bench('address spender, uint256 amount', () => { + AbiParameters.from('address spender, uint256 amount') + }) + + bench('struct Foo', () => { + AbiParameters.from([ + 'struct Foo { address spender; uint256 amount; }', + 'Foo foo, address bar', + ]) + }) +}) + +describe('AbiParameters.format', () => { + bench('address spender, uint256 amount', () => { + AbiParameters.format([ + { name: 'spender', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ]) + }) +}) diff --git a/src/core/AbiParameters.ts b/src/core/AbiParameters.ts index 9a6d1846..d2c8c01e 100644 --- a/src/core/AbiParameters.ts +++ b/src/core/AbiParameters.ts @@ -3,6 +3,8 @@ import * as Address from './Address.js' import * as Bytes from './Bytes.js' import * as Errors from './Errors.js' import * as Hex from './Hex.js' +import * as formatAbiParameters from './internal/human-readable/formatAbiParameters.js' +import * as parseAbiParameters from './internal/human-readable/parseAbiParameters.js' import * as internal from './internal/abiParameters.js' import * as Cursor from './internal/cursor.js' import * as Solidity from './Solidity.js' @@ -13,6 +15,16 @@ export type AbiParameters = readonly abitype.AbiParameter[] /** A parameter on an {@link ox#AbiParameters.AbiParameters}. */ export type Parameter = abitype.AbiParameter +export { + InvalidAbiParametersError, + InvalidAbiTypeParameterError, + InvalidFunctionModifierError, + InvalidModifierError, + InvalidParameterError, + SolidityProtectedKeywordError, +} from './internal/human-readable/errors.js' +export { InvalidParenthesisError } from './internal/human-readable/errors.js' + /** A packed ABI type. */ export type PackedAbiType = | abitype.SolidityAddress @@ -386,11 +398,18 @@ export function format< Parameter | abitype.AbiEventParameter, ...(readonly (Parameter | abitype.AbiEventParameter)[]), ], -): abitype.FormatAbiParameters { - return abitype.formatAbiParameters(parameters) +): format.ReturnType { + return formatAbiParameters.formatAbiParameters(parameters) } export declare namespace format { + type ReturnType< + parameters extends readonly [ + Parameter | abitype.AbiEventParameter, + ...(readonly (Parameter | abitype.AbiEventParameter)[]), + ] = readonly [Parameter, ...(readonly Parameter[])], + > = formatAbiParameters.FormatAbiParameters + type ErrorType = Errors.GlobalErrorType } @@ -416,6 +435,22 @@ export declare namespace format { * * parameters * //^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // * ``` * * @example @@ -432,6 +467,22 @@ export declare namespace format { * * parameters * //^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // * ``` * * @example @@ -447,6 +498,22 @@ export declare namespace format { * * parameters * //^? + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // + * // * ``` * * @@ -460,9 +527,9 @@ export function from< parameters: parameters | AbiParameters | string | readonly string[], ): from.ReturnType { if (Array.isArray(parameters) && typeof parameters[0] === 'string') - return abitype.parseAbiParameters(parameters) as never + return parseAbiParameters.parseAbiParameters(parameters) as never if (typeof parameters === 'string') - return abitype.parseAbiParameters(parameters) as never + return parseAbiParameters.parseAbiParameters(parameters) as never return parameters as never } @@ -470,9 +537,9 @@ export declare namespace from { type ReturnType< parameters extends AbiParameters | string | readonly string[], > = parameters extends string - ? abitype.ParseAbiParameters + ? parseAbiParameters.ParseAbiParameters : parameters extends readonly string[] - ? abitype.ParseAbiParameters + ? parseAbiParameters.ParseAbiParameters : parameters type ErrorType = Errors.GlobalErrorType @@ -519,7 +586,7 @@ export class DataSizeTooSmallError extends Errors.BaseError { }) { super(`Data size of ${size} bytes is too small for given parameters.`, { metaMessages: [ - `Params: (${abitype.formatAbiParameters(parameters as readonly [Parameter])})`, + `Params: (${formatAbiParameters.formatAbiParameters(parameters as readonly [Parameter])})`, `Data: ${data} (${size} bytes)`, ], }) diff --git a/src/core/RpcSchema.ts b/src/core/RpcSchema.ts index 98c4ea84..b9323937 100644 --- a/src/core/RpcSchema.ts +++ b/src/core/RpcSchema.ts @@ -289,7 +289,7 @@ export type FromViem = { * Each method's name comes from its key. Both `params` and `ReturnType` are * derived from the Zod schema's input (wire) type, since raw JSON-RPC clients * send and receive wire values. Decode wire results to their native - * representation explicitly via {@link ox#zod/RpcSchema.decodeReturns}. + * representation explicitly via `zod.RpcSchema.decodeReturns`. * * @example * ```ts twoslash diff --git a/src/core/_test/Abi.test.ts b/src/core/_test/Abi.test.ts index 4d5f0277..34a4410a 100644 --- a/src/core/_test/Abi.test.ts +++ b/src/core/_test/Abi.test.ts @@ -117,6 +117,10 @@ describe('from', () => { test('exports', () => { expect(Object.keys(Abi)).toMatchInlineSnapshot(` [ + "InvalidSignatureError", + "InvalidStructSignatureError", + "UnknownSignatureError", + "CircularReferenceError", "format", "from", ] diff --git a/src/core/_test/AbiItem.test.ts b/src/core/_test/AbiItem.test.ts index ba2f8ecb..d93c90a5 100644 --- a/src/core/_test/AbiItem.test.ts +++ b/src/core/_test/AbiItem.test.ts @@ -2107,6 +2107,9 @@ describe('getSignatureHash', () => { test('exports', () => { expect(Object.keys(AbiItem)).toMatchInlineSnapshot(` [ + "InvalidAbiItemError", + "UnknownSolidityTypeError", + "UnknownTypeError", "format", "from", "fromAbi", diff --git a/src/core/_test/AbiParameter.test-d.ts b/src/core/_test/AbiParameter.test-d.ts new file mode 100644 index 00000000..b2cedaa9 --- /dev/null +++ b/src/core/_test/AbiParameter.test-d.ts @@ -0,0 +1,41 @@ +import { AbiParameter } from 'ox' +import { describe, expectTypeOf, test } from 'vp/test' + +describe('AbiParameter.format', () => { + test('infers parameter', () => { + const formatted = AbiParameter.format(value) + expectTypeOf(formatted).toEqualTypeOf<'address spender'>() + }) + + test('not narrowable', () => { + const formatted = AbiParameter.format({} as AbiParameter.AbiParameter) + expectTypeOf(formatted).toEqualTypeOf() + }) + + const value = { + name: 'spender', + type: 'address', + } as const +}) + +describe('AbiParameter.from', () => { + test('infers parameter', () => { + const parameter = AbiParameter.from(value) + expectTypeOf(parameter).toEqualTypeOf(value) + }) + + test('from signature', () => { + const parameter = AbiParameter.from('address spender') + expectTypeOf(parameter).toEqualTypeOf(value) + }) + + test('not narrowable', () => { + const parameter = AbiParameter.from({} as AbiParameter.AbiParameter) + expectTypeOf(parameter).toEqualTypeOf() + }) + + const value = { + name: 'spender', + type: 'address', + } as const +}) diff --git a/src/core/_test/AbiParameter.test.ts b/src/core/_test/AbiParameter.test.ts new file mode 100644 index 00000000..9fe1398d --- /dev/null +++ b/src/core/_test/AbiParameter.test.ts @@ -0,0 +1,87 @@ +import { AbiParameter } from 'ox' +import { describe, expect, test } from 'vp/test' + +describe('from', () => { + test('json parameter', () => { + expect( + AbiParameter.from({ name: 'spender', type: 'address' }), + ).toMatchInlineSnapshot(` + { + "name": "spender", + "type": "address", + } + `) + }) + + test('human-readable parameter', () => { + expect(AbiParameter.from('address spender')).toMatchInlineSnapshot(` + { + "name": "spender", + "type": "address", + } + `) + }) + + test('human-readable parameter with structs', () => { + expect( + AbiParameter.from([ + 'struct Foo { address spender; uint256 amount; }', + 'Foo foo', + ]), + ).toMatchInlineSnapshot(` + { + "components": [ + { + "name": "spender", + "type": "address", + }, + { + "name": "amount", + "type": "uint256", + }, + ], + "name": "foo", + "type": "tuple", + } + `) + }) +}) + +describe('format', () => { + test('parameter', () => { + expect( + AbiParameter.format({ name: 'spender', type: 'address' }), + ).toMatchInlineSnapshot(`"address spender"`) + }) + + test('tuple parameter', () => { + expect( + AbiParameter.format({ + components: [ + { name: 'spender', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + name: 'foo', + type: 'tuple', + }), + ).toMatchInlineSnapshot( + `"(address spender, uint256 amount) foo"`, + ) + }) +}) + +test('exports', () => { + expect(Object.keys(AbiParameter)).toMatchInlineSnapshot(` + [ + "InvalidAbiParameterError", + "InvalidAbiTypeParameterError", + "InvalidFunctionModifierError", + "InvalidModifierError", + "InvalidParameterError", + "SolidityProtectedKeywordError", + "InvalidParenthesisError", + "format", + "from", + ] + `) +}) diff --git a/src/core/_test/AbiParameters.test.ts b/src/core/_test/AbiParameters.test.ts index 13bd194e..873e70e9 100644 --- a/src/core/_test/AbiParameters.test.ts +++ b/src/core/_test/AbiParameters.test.ts @@ -360,6 +360,13 @@ describe('format', () => { test('exports', () => { expect(Object.keys(AbiParameters)).toMatchInlineSnapshot(` [ + "InvalidAbiParametersError", + "InvalidAbiTypeParameterError", + "InvalidFunctionModifierError", + "InvalidModifierError", + "InvalidParameterError", + "SolidityProtectedKeywordError", + "InvalidParenthesisError", "decode", "encode", "encodePacked", diff --git a/src/core/_test/index.test.ts b/src/core/_test/index.test.ts index 278da04a..6da06197 100644 --- a/src/core/_test/index.test.ts +++ b/src/core/_test/index.test.ts @@ -10,6 +10,7 @@ test('exports', () => { "AbiEvent", "AbiFunction", "AbiItem", + "AbiParameter", "AbiParameters", "AccessList", "AccountProof", diff --git a/src/core/internal/human-readable/_snap/formatAbi.test.ts.snap b/src/core/internal/human-readable/_snap/formatAbi.test.ts.snap new file mode 100644 index 00000000..fcf0eeee --- /dev/null +++ b/src/core/internal/human-readable/_snap/formatAbi.test.ts.snap @@ -0,0 +1,74 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`formatAbi 1`] = ` +[ + "constructor(address conduitController)", + "function cancel((address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 counter)[] orders) returns (bool cancelled)", + "function fulfillAdvancedOrder(((address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 totalOriginalConsiderationItems) parameters, uint120 numerator, uint120 denominator, bytes signature, bytes extraData) advancedOrder, (uint256 orderIndex, uint8 side, uint256 index, uint256 identifier, bytes32[] criteriaProof)[] criteriaResolvers, bytes32 fulfillerConduitKey, address recipient) payable returns (bool fulfilled)", + "function fulfillAvailableAdvancedOrders(((address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 totalOriginalConsiderationItems) parameters, uint120 numerator, uint120 denominator, bytes signature, bytes extraData)[] advancedOrders, (uint256 orderIndex, uint8 side, uint256 index, uint256 identifier, bytes32[] criteriaProof)[] criteriaResolvers, (uint256 orderIndex, uint256 itemIndex)[][] offerFulfillments, (uint256 orderIndex, uint256 itemIndex)[][] considerationFulfillments, bytes32 fulfillerConduitKey, address recipient, uint256 maximumFulfilled) payable returns (bool[] availableOrders, ((uint8 itemType, address token, uint256 identifier, uint256 amount, address recipient) item, address offerer, bytes32 conduitKey)[] executions)", + "function fulfillAvailableOrders(((address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 totalOriginalConsiderationItems) parameters, bytes signature)[] orders, (uint256 orderIndex, uint256 itemIndex)[][] offerFulfillments, (uint256 orderIndex, uint256 itemIndex)[][] considerationFulfillments, bytes32 fulfillerConduitKey, uint256 maximumFulfilled) payable returns (bool[] availableOrders, ((uint8 itemType, address token, uint256 identifier, uint256 amount, address recipient) item, address offerer, bytes32 conduitKey)[] executions)", + "function fulfillBasicOrder((address considerationToken, uint256 considerationIdentifier, uint256 considerationAmount, address offerer, address zone, address offerToken, uint256 offerIdentifier, uint256 offerAmount, uint8 basicOrderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 offererConduitKey, bytes32 fulfillerConduitKey, uint256 totalOriginalAdditionalRecipients, (uint256 amount, address recipient)[] additionalRecipients, bytes signature) parameters) payable returns (bool fulfilled)", + "function fulfillBasicOrder_efficient_6GL6yc((address considerationToken, uint256 considerationIdentifier, uint256 considerationAmount, address offerer, address zone, address offerToken, uint256 offerIdentifier, uint256 offerAmount, uint8 basicOrderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 offererConduitKey, bytes32 fulfillerConduitKey, uint256 totalOriginalAdditionalRecipients, (uint256 amount, address recipient)[] additionalRecipients, bytes signature) parameters) payable returns (bool fulfilled)", + "function fulfillOrder(((address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 totalOriginalConsiderationItems) parameters, bytes signature) order, bytes32 fulfillerConduitKey) payable returns (bool fulfilled)", + "function getContractOffererNonce(address contractOfferer) view returns (uint256 nonce)", + "function getCounter(address offerer) view returns (uint256 counter)", + "function getOrderHash((address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 counter) order) view returns (bytes32 orderHash)", + "function getOrderStatus(bytes32 orderHash) view returns (bool isValidated, bool isCancelled, uint256 totalFilled, uint256 totalSize)", + "function incrementCounter() returns (uint256 newCounter)", + "function information() view returns (string version, bytes32 domainSeparator, address conduitController)", + "function matchAdvancedOrders(((address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 totalOriginalConsiderationItems) parameters, uint120 numerator, uint120 denominator, bytes signature, bytes extraData)[] orders, (uint256 orderIndex, uint8 side, uint256 index, uint256 identifier, bytes32[] criteriaProof)[] criteriaResolvers, ((uint256 orderIndex, uint256 itemIndex)[] offerComponents, (uint256 orderIndex, uint256 itemIndex)[] considerationComponents)[] fulfillments, address recipient) payable returns (((uint8 itemType, address token, uint256 identifier, uint256 amount, address recipient) item, address offerer, bytes32 conduitKey)[] executions)", + "function matchOrders(((address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 totalOriginalConsiderationItems) parameters, bytes signature)[] orders, ((uint256 orderIndex, uint256 itemIndex)[] offerComponents, (uint256 orderIndex, uint256 itemIndex)[] considerationComponents)[] fulfillments) payable returns (((uint8 itemType, address token, uint256 identifier, uint256 amount, address recipient) item, address offerer, bytes32 conduitKey)[] executions)", + "function name() view returns (string contractName)", + "function validate(((address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 totalOriginalConsiderationItems) parameters, bytes signature)[] orders) returns (bool validated)", + "error BadContractSignature()", + "error BadFraction()", + "error BadReturnValueFromERC20OnTransfer(address token, address from, address to, uint256 amount)", + "error BadSignatureV(uint8 v)", + "error CannotCancelOrder()", + "error ConsiderationCriteriaResolverOutOfRange()", + "error ConsiderationLengthNotEqualToTotalOriginal()", + "error ConsiderationNotMet(uint256 orderIndex, uint256 considerationIndex, uint256 shortfallAmount)", + "error CriteriaNotEnabledForItem()", + "error ERC1155BatchTransferGenericFailure(address token, address from, address to, uint256[] identifiers, uint256[] amounts)", + "error InexactFraction()", + "error InsufficientNativeTokensSupplied()", + "error Invalid1155BatchTransferEncoding()", + "error InvalidBasicOrderParameterEncoding()", + "error InvalidCallToConduit(address conduit)", + "error InvalidConduit(bytes32 conduitKey, address conduit)", + "error InvalidContractOrder(bytes32 orderHash)", + "error InvalidERC721TransferAmount(uint256 amount)", + "error InvalidFulfillmentComponentData()", + "error InvalidMsgValue(uint256 value)", + "error InvalidNativeOfferItem()", + "error InvalidProof()", + "error InvalidRestrictedOrder(bytes32 orderHash)", + "error InvalidSignature()", + "error InvalidSigner()", + "error InvalidTime(uint256 startTime, uint256 endTime)", + "error MismatchedFulfillmentOfferAndConsiderationComponents(uint256 fulfillmentIndex)", + "error MissingFulfillmentComponentOnAggregation(uint8 side)", + "error MissingItemAmount()", + "error MissingOriginalConsiderationItems()", + "error NativeTokenTransferGenericFailure(address account, uint256 amount)", + "error NoContract(address account)", + "error NoReentrantCalls()", + "error NoSpecifiedOrdersAvailable()", + "error OfferAndConsiderationRequiredOnFulfillment()", + "error OfferCriteriaResolverOutOfRange()", + "error OrderAlreadyFilled(bytes32 orderHash)", + "error OrderCriteriaResolverOutOfRange(uint8 side)", + "error OrderIsCancelled(bytes32 orderHash)", + "error OrderPartiallyFilled(bytes32 orderHash)", + "error PartialFillsNotEnabledForOrder()", + "error TokenTransferGenericFailure(address token, address from, address to, uint256 identifier, uint256 amount)", + "error UnresolvedConsiderationCriteria(uint256 orderIndex, uint256 considerationIndex)", + "error UnresolvedOfferCriteria(uint256 orderIndex, uint256 offerIndex)", + "error UnusedItemParameters()", + "event CounterIncremented(uint256 newCounter, address indexed offerer)", + "event OrderCancelled(bytes32 orderHash, address indexed offerer, address indexed zone)", + "event OrderFulfilled(bytes32 orderHash, address indexed offerer, address indexed zone, address recipient, (uint8 itemType, address token, uint256 identifier, uint256 amount)[] offer, (uint8 itemType, address token, uint256 identifier, uint256 amount, address recipient)[] consideration)", + "event OrderValidated(bytes32 orderHash, (address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 totalOriginalConsiderationItems) orderParameters)", + "event OrdersMatched(bytes32[] orderHashes)", +] +`; diff --git a/src/core/internal/human-readable/_snap/parseAbi.test.ts.snap b/src/core/internal/human-readable/_snap/parseAbi.test.ts.snap new file mode 100644 index 00000000..bf7e6781 --- /dev/null +++ b/src/core/internal/human-readable/_snap/parseAbi.test.ts.snap @@ -0,0 +1,2353 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`parseAbi 1`] = ` +[ + { + "inputs": [ + { + "name": "conduitController", + "type": "address", + }, + ], + "stateMutability": "nonpayable", + "type": "constructor", + }, + { + "inputs": [ + { + "components": [ + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + ], + "name": "offer", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "consideration", + "type": "tuple[]", + }, + { + "name": "orderType", + "type": "uint8", + }, + { + "name": "startTime", + "type": "uint256", + }, + { + "name": "endTime", + "type": "uint256", + }, + { + "name": "zoneHash", + "type": "bytes32", + }, + { + "name": "salt", + "type": "uint256", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + { + "name": "counter", + "type": "uint256", + }, + ], + "name": "orders", + "type": "tuple[]", + }, + ], + "name": "cancel", + "outputs": [ + { + "name": "cancelled", + "type": "bool", + }, + ], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + { + "components": [ + { + "name": "considerationToken", + "type": "address", + }, + { + "name": "considerationIdentifier", + "type": "uint256", + }, + { + "name": "considerationAmount", + "type": "uint256", + }, + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + { + "name": "offerToken", + "type": "address", + }, + { + "name": "offerIdentifier", + "type": "uint256", + }, + { + "name": "offerAmount", + "type": "uint256", + }, + { + "name": "basicOrderType", + "type": "uint8", + }, + { + "name": "startTime", + "type": "uint256", + }, + { + "name": "endTime", + "type": "uint256", + }, + { + "name": "zoneHash", + "type": "bytes32", + }, + { + "name": "salt", + "type": "uint256", + }, + { + "name": "offererConduitKey", + "type": "bytes32", + }, + { + "name": "fulfillerConduitKey", + "type": "bytes32", + }, + { + "name": "totalOriginalAdditionalRecipients", + "type": "uint256", + }, + { + "components": [ + { + "name": "amount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "additionalRecipients", + "type": "tuple[]", + }, + { + "name": "signature", + "type": "bytes", + }, + ], + "name": "parameters", + "type": "tuple", + }, + ], + "name": "fulfillBasicOrder", + "outputs": [ + { + "name": "fulfilled", + "type": "bool", + }, + ], + "stateMutability": "payable", + "type": "function", + }, + { + "inputs": [ + { + "components": [ + { + "name": "considerationToken", + "type": "address", + }, + { + "name": "considerationIdentifier", + "type": "uint256", + }, + { + "name": "considerationAmount", + "type": "uint256", + }, + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + { + "name": "offerToken", + "type": "address", + }, + { + "name": "offerIdentifier", + "type": "uint256", + }, + { + "name": "offerAmount", + "type": "uint256", + }, + { + "name": "basicOrderType", + "type": "uint8", + }, + { + "name": "startTime", + "type": "uint256", + }, + { + "name": "endTime", + "type": "uint256", + }, + { + "name": "zoneHash", + "type": "bytes32", + }, + { + "name": "salt", + "type": "uint256", + }, + { + "name": "offererConduitKey", + "type": "bytes32", + }, + { + "name": "fulfillerConduitKey", + "type": "bytes32", + }, + { + "name": "totalOriginalAdditionalRecipients", + "type": "uint256", + }, + { + "components": [ + { + "name": "amount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "additionalRecipients", + "type": "tuple[]", + }, + { + "name": "signature", + "type": "bytes", + }, + ], + "name": "parameters", + "type": "tuple", + }, + ], + "name": "fulfillBasicOrder_efficient_6GL6yc", + "outputs": [ + { + "name": "fulfilled", + "type": "bool", + }, + ], + "stateMutability": "payable", + "type": "function", + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + ], + "name": "offer", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "consideration", + "type": "tuple[]", + }, + { + "name": "orderType", + "type": "uint8", + }, + { + "name": "startTime", + "type": "uint256", + }, + { + "name": "endTime", + "type": "uint256", + }, + { + "name": "zoneHash", + "type": "bytes32", + }, + { + "name": "salt", + "type": "uint256", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + { + "name": "totalOriginalConsiderationItems", + "type": "uint256", + }, + ], + "name": "parameters", + "type": "tuple", + }, + { + "name": "signature", + "type": "bytes", + }, + ], + "name": "order", + "type": "tuple", + }, + { + "name": "fulfillerConduitKey", + "type": "bytes32", + }, + ], + "name": "fulfillOrder", + "outputs": [ + { + "name": "fulfilled", + "type": "bool", + }, + ], + "stateMutability": "payable", + "type": "function", + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + ], + "name": "offer", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "consideration", + "type": "tuple[]", + }, + { + "name": "orderType", + "type": "uint8", + }, + { + "name": "startTime", + "type": "uint256", + }, + { + "name": "endTime", + "type": "uint256", + }, + { + "name": "zoneHash", + "type": "bytes32", + }, + { + "name": "salt", + "type": "uint256", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + { + "name": "totalOriginalConsiderationItems", + "type": "uint256", + }, + ], + "name": "parameters", + "type": "tuple", + }, + { + "name": "numerator", + "type": "uint120", + }, + { + "name": "denominator", + "type": "uint120", + }, + { + "name": "signature", + "type": "bytes", + }, + { + "name": "extraData", + "type": "bytes", + }, + ], + "name": "advancedOrder", + "type": "tuple", + }, + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "side", + "type": "uint8", + }, + { + "name": "index", + "type": "uint256", + }, + { + "name": "identifier", + "type": "uint256", + }, + { + "name": "criteriaProof", + "type": "bytes32[]", + }, + ], + "name": "criteriaResolvers", + "type": "tuple[]", + }, + { + "name": "fulfillerConduitKey", + "type": "bytes32", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "fulfillAdvancedOrder", + "outputs": [ + { + "name": "fulfilled", + "type": "bool", + }, + ], + "stateMutability": "payable", + "type": "function", + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + ], + "name": "offer", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "consideration", + "type": "tuple[]", + }, + { + "name": "orderType", + "type": "uint8", + }, + { + "name": "startTime", + "type": "uint256", + }, + { + "name": "endTime", + "type": "uint256", + }, + { + "name": "zoneHash", + "type": "bytes32", + }, + { + "name": "salt", + "type": "uint256", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + { + "name": "totalOriginalConsiderationItems", + "type": "uint256", + }, + ], + "name": "parameters", + "type": "tuple", + }, + { + "name": "signature", + "type": "bytes", + }, + ], + "name": "orders", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + "name": "offerFulfillments", + "type": "tuple[][]", + }, + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + "name": "considerationFulfillments", + "type": "tuple[][]", + }, + { + "name": "fulfillerConduitKey", + "type": "bytes32", + }, + { + "name": "maximumFulfilled", + "type": "uint256", + }, + ], + "name": "fulfillAvailableOrders", + "outputs": [ + { + "name": "availableOrders", + "type": "bool[]", + }, + { + "components": [ + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifier", + "type": "uint256", + }, + { + "name": "amount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "item", + "type": "tuple", + }, + { + "name": "offerer", + "type": "address", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + ], + "name": "executions", + "type": "tuple[]", + }, + ], + "stateMutability": "payable", + "type": "function", + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + ], + "name": "offer", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "consideration", + "type": "tuple[]", + }, + { + "name": "orderType", + "type": "uint8", + }, + { + "name": "startTime", + "type": "uint256", + }, + { + "name": "endTime", + "type": "uint256", + }, + { + "name": "zoneHash", + "type": "bytes32", + }, + { + "name": "salt", + "type": "uint256", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + { + "name": "totalOriginalConsiderationItems", + "type": "uint256", + }, + ], + "name": "parameters", + "type": "tuple", + }, + { + "name": "numerator", + "type": "uint120", + }, + { + "name": "denominator", + "type": "uint120", + }, + { + "name": "signature", + "type": "bytes", + }, + { + "name": "extraData", + "type": "bytes", + }, + ], + "name": "advancedOrders", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "side", + "type": "uint8", + }, + { + "name": "index", + "type": "uint256", + }, + { + "name": "identifier", + "type": "uint256", + }, + { + "name": "criteriaProof", + "type": "bytes32[]", + }, + ], + "name": "criteriaResolvers", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + "name": "offerFulfillments", + "type": "tuple[][]", + }, + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + "name": "considerationFulfillments", + "type": "tuple[][]", + }, + { + "name": "fulfillerConduitKey", + "type": "bytes32", + }, + { + "name": "recipient", + "type": "address", + }, + { + "name": "maximumFulfilled", + "type": "uint256", + }, + ], + "name": "fulfillAvailableAdvancedOrders", + "outputs": [ + { + "name": "availableOrders", + "type": "bool[]", + }, + { + "components": [ + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifier", + "type": "uint256", + }, + { + "name": "amount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "item", + "type": "tuple", + }, + { + "name": "offerer", + "type": "address", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + ], + "name": "executions", + "type": "tuple[]", + }, + ], + "stateMutability": "payable", + "type": "function", + }, + { + "inputs": [ + { + "name": "contractOfferer", + "type": "address", + }, + ], + "name": "getContractOffererNonce", + "outputs": [ + { + "name": "nonce", + "type": "uint256", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "components": [ + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + ], + "name": "offer", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "consideration", + "type": "tuple[]", + }, + { + "name": "orderType", + "type": "uint8", + }, + { + "name": "startTime", + "type": "uint256", + }, + { + "name": "endTime", + "type": "uint256", + }, + { + "name": "zoneHash", + "type": "bytes32", + }, + { + "name": "salt", + "type": "uint256", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + { + "name": "counter", + "type": "uint256", + }, + ], + "name": "order", + "type": "tuple", + }, + ], + "name": "getOrderHash", + "outputs": [ + { + "name": "orderHash", + "type": "bytes32", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "name": "orderHash", + "type": "bytes32", + }, + ], + "name": "getOrderStatus", + "outputs": [ + { + "name": "isValidated", + "type": "bool", + }, + { + "name": "isCancelled", + "type": "bool", + }, + { + "name": "totalFilled", + "type": "uint256", + }, + { + "name": "totalSize", + "type": "uint256", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "name": "offerer", + "type": "address", + }, + ], + "name": "getCounter", + "outputs": [ + { + "name": "counter", + "type": "uint256", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "incrementCounter", + "outputs": [ + { + "name": "newCounter", + "type": "uint256", + }, + ], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [], + "name": "information", + "outputs": [ + { + "name": "version", + "type": "string", + }, + { + "name": "domainSeparator", + "type": "bytes32", + }, + { + "name": "conduitController", + "type": "address", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "contractName", + "type": "string", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + ], + "name": "offer", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "consideration", + "type": "tuple[]", + }, + { + "name": "orderType", + "type": "uint8", + }, + { + "name": "startTime", + "type": "uint256", + }, + { + "name": "endTime", + "type": "uint256", + }, + { + "name": "zoneHash", + "type": "bytes32", + }, + { + "name": "salt", + "type": "uint256", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + { + "name": "totalOriginalConsiderationItems", + "type": "uint256", + }, + ], + "name": "parameters", + "type": "tuple", + }, + { + "name": "numerator", + "type": "uint120", + }, + { + "name": "denominator", + "type": "uint120", + }, + { + "name": "signature", + "type": "bytes", + }, + { + "name": "extraData", + "type": "bytes", + }, + ], + "name": "orders", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "side", + "type": "uint8", + }, + { + "name": "index", + "type": "uint256", + }, + { + "name": "identifier", + "type": "uint256", + }, + { + "name": "criteriaProof", + "type": "bytes32[]", + }, + ], + "name": "criteriaResolvers", + "type": "tuple[]", + }, + { + "components": [ + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + "name": "offerComponents", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + "name": "considerationComponents", + "type": "tuple[]", + }, + ], + "name": "fulfillments", + "type": "tuple[]", + }, + ], + "name": "matchAdvancedOrders", + "outputs": [ + { + "components": [ + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifier", + "type": "uint256", + }, + { + "name": "amount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "item", + "type": "tuple", + }, + { + "name": "offerer", + "type": "address", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + ], + "name": "executions", + "type": "tuple[]", + }, + ], + "stateMutability": "payable", + "type": "function", + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + ], + "name": "offer", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "consideration", + "type": "tuple[]", + }, + { + "name": "orderType", + "type": "uint8", + }, + { + "name": "startTime", + "type": "uint256", + }, + { + "name": "endTime", + "type": "uint256", + }, + { + "name": "zoneHash", + "type": "bytes32", + }, + { + "name": "salt", + "type": "uint256", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + { + "name": "totalOriginalConsiderationItems", + "type": "uint256", + }, + ], + "name": "parameters", + "type": "tuple", + }, + { + "name": "signature", + "type": "bytes", + }, + ], + "name": "orders", + "type": "tuple[]", + }, + { + "components": [ + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + "name": "offerComponents", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + "name": "considerationComponents", + "type": "tuple[]", + }, + ], + "name": "fulfillments", + "type": "tuple[]", + }, + ], + "name": "matchOrders", + "outputs": [ + { + "components": [ + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifier", + "type": "uint256", + }, + { + "name": "amount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "item", + "type": "tuple", + }, + { + "name": "offerer", + "type": "address", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + ], + "name": "executions", + "type": "tuple[]", + }, + ], + "stateMutability": "payable", + "type": "function", + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + ], + "name": "offer", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifierOrCriteria", + "type": "uint256", + }, + { + "name": "startAmount", + "type": "uint256", + }, + { + "name": "endAmount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "consideration", + "type": "tuple[]", + }, + { + "name": "orderType", + "type": "uint8", + }, + { + "name": "startTime", + "type": "uint256", + }, + { + "name": "endTime", + "type": "uint256", + }, + { + "name": "zoneHash", + "type": "bytes32", + }, + { + "name": "salt", + "type": "uint256", + }, + { + "name": "conduitKey", + "type": "bytes32", + }, + { + "name": "totalOriginalConsiderationItems", + "type": "uint256", + }, + ], + "name": "parameters", + "type": "tuple", + }, + { + "name": "signature", + "type": "bytes", + }, + ], + "name": "orders", + "type": "tuple[]", + }, + ], + "name": "validate", + "outputs": [ + { + "name": "validated", + "type": "bool", + }, + ], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + { + "name": "newCounter", + "type": "uint256", + }, + { + "name": "offerer", + "type": "address", + }, + ], + "name": "CounterIncremented", + "type": "event", + }, + { + "inputs": [ + { + "name": "orderHash", + "type": "bytes32", + }, + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + ], + "name": "OrderCancelled", + "type": "event", + }, + { + "inputs": [ + { + "name": "orderHash", + "type": "bytes32", + }, + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + { + "name": "recipient", + "type": "address", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifier", + "type": "uint256", + }, + { + "name": "amount", + "type": "uint256", + }, + ], + "name": "offer", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "itemType", + "type": "uint8", + }, + { + "name": "token", + "type": "address", + }, + { + "name": "identifier", + "type": "uint256", + }, + { + "name": "amount", + "type": "uint256", + }, + { + "name": "recipient", + "type": "address", + }, + ], + "name": "consideration", + "type": "tuple[]", + }, + ], + "name": "OrderFulfilled", + "type": "event", + }, + { + "inputs": [ + { + "name": "orderHashes", + "type": "bytes32[]", + }, + ], + "name": "OrdersMatched", + "type": "event", + }, + { + "inputs": [ + { + "name": "orderHash", + "type": "bytes32", + }, + { + "name": "offerer", + "type": "address", + }, + { + "name": "zone", + "type": "address", + }, + ], + "name": "OrderValidated", + "type": "event", + }, + { + "inputs": [], + "name": "BadContractSignature", + "type": "error", + }, + { + "inputs": [], + "name": "BadFraction", + "type": "error", + }, + { + "inputs": [ + { + "name": "token", + "type": "address", + }, + { + "name": "from", + "type": "address", + }, + { + "name": "to", + "type": "address", + }, + { + "name": "amount", + "type": "uint256", + }, + ], + "name": "BadReturnValueFromERC20OnTransfer", + "type": "error", + }, + { + "inputs": [ + { + "name": "v", + "type": "uint8", + }, + ], + "name": "BadSignatureV", + "type": "error", + }, + { + "inputs": [], + "name": "CannotCancelOrder", + "type": "error", + }, + { + "inputs": [], + "name": "ConsiderationCriteriaResolverOutOfRange", + "type": "error", + }, + { + "inputs": [], + "name": "ConsiderationLengthNotEqualToTotalOriginal", + "type": "error", + }, + { + "inputs": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "considerationAmount", + "type": "uint256", + }, + { + "name": "shortfallAmount", + "type": "uint256", + }, + ], + "name": "ConsiderationNotMet", + "type": "error", + }, + { + "inputs": [], + "name": "CriteriaNotEnabledForItem", + "type": "error", + }, + { + "inputs": [ + { + "name": "token", + "type": "address", + }, + { + "name": "from", + "type": "address", + }, + { + "name": "to", + "type": "address", + }, + { + "name": "identifiers", + "type": "uint256[]", + }, + { + "name": "amounts", + "type": "uint256[]", + }, + ], + "name": "ERC1155BatchTransferGenericFailure", + "type": "error", + }, + { + "inputs": [], + "name": "InexactFraction", + "type": "error", + }, + { + "inputs": [], + "name": "InsufficientNativeTokensSupplied", + "type": "error", + }, + { + "inputs": [], + "name": "Invalid1155BatchTransferEncoding", + "type": "error", + }, + { + "inputs": [], + "name": "InvalidBasicOrderParameterEncoding", + "type": "error", + }, + { + "inputs": [ + { + "name": "conduit", + "type": "address", + }, + ], + "name": "InvalidCallToConduit", + "type": "error", + }, + { + "inputs": [ + { + "name": "conduitKey", + "type": "bytes32", + }, + { + "name": "conduit", + "type": "address", + }, + ], + "name": "InvalidConduit", + "type": "error", + }, + { + "inputs": [ + { + "name": "orderHash", + "type": "bytes32", + }, + ], + "name": "InvalidContractOrder", + "type": "error", + }, + { + "inputs": [ + { + "name": "amount", + "type": "uint256", + }, + ], + "name": "InvalidERC721TransferAmount", + "type": "error", + }, + { + "inputs": [], + "name": "InvalidFulfillmentComponentData", + "type": "error", + }, + { + "inputs": [ + { + "name": "value", + "type": "uint256", + }, + ], + "name": "InvalidMsgValue", + "type": "error", + }, + { + "inputs": [], + "name": "InvalidNativeOfferItem", + "type": "error", + }, + { + "inputs": [], + "name": "InvalidProof", + "type": "error", + }, + { + "inputs": [ + { + "name": "orderHash", + "type": "bytes32", + }, + ], + "name": "InvalidRestrictedOrder", + "type": "error", + }, + { + "inputs": [], + "name": "InvalidSignature", + "type": "error", + }, + { + "inputs": [], + "name": "InvalidSigner", + "type": "error", + }, + { + "inputs": [ + { + "name": "startTime", + "type": "uint256", + }, + { + "name": "endTime", + "type": "uint256", + }, + ], + "name": "InvalidTime", + "type": "error", + }, + { + "inputs": [ + { + "name": "fulfillmentIndex", + "type": "uint256", + }, + ], + "name": "MismatchedFulfillmentOfferAndConsiderationComponents", + "type": "error", + }, + { + "inputs": [ + { + "name": "side", + "type": "uint8", + }, + ], + "name": "MissingFulfillmentComponentOnAggregation", + "type": "error", + }, + { + "inputs": [], + "name": "MissingItemAmount", + "type": "error", + }, + { + "inputs": [], + "name": "MissingOriginalConsiderationItems", + "type": "error", + }, + { + "inputs": [ + { + "name": "account", + "type": "address", + }, + { + "name": "amount", + "type": "uint256", + }, + ], + "name": "NativeTokenTransferGenericFailure", + "type": "error", + }, + { + "inputs": [ + { + "name": "account", + "type": "address", + }, + ], + "name": "NoContract", + "type": "error", + }, + { + "inputs": [], + "name": "NoReentrantCalls", + "type": "error", + }, + { + "inputs": [], + "name": "NoSpecifiedOrdersAvailable", + "type": "error", + }, + { + "inputs": [], + "name": "OfferAndConsiderationRequiredOnFulfillment", + "type": "error", + }, + { + "inputs": [], + "name": "OfferCriteriaResolverOutOfRange", + "type": "error", + }, + { + "inputs": [ + { + "name": "orderHash", + "type": "bytes32", + }, + ], + "name": "OrderAlreadyFilled", + "type": "error", + }, + { + "inputs": [ + { + "name": "side", + "type": "uint8", + }, + ], + "name": "OrderCriteriaResolverOutOfRange", + "type": "error", + }, + { + "inputs": [ + { + "name": "orderHash", + "type": "bytes32", + }, + ], + "name": "OrderIsCancelled", + "type": "error", + }, + { + "inputs": [ + { + "name": "orderHash", + "type": "bytes32", + }, + ], + "name": "OrderPartiallyFilled", + "type": "error", + }, + { + "inputs": [], + "name": "PartialFillsNotEnabledForOrder", + "type": "error", + }, + { + "inputs": [ + { + "name": "token", + "type": "address", + }, + { + "name": "from", + "type": "address", + }, + { + "name": "to", + "type": "address", + }, + { + "name": "identifier", + "type": "uint256", + }, + { + "name": "amount", + "type": "uint256", + }, + ], + "name": "TokenTransferGenericFailure", + "type": "error", + }, + { + "inputs": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "considerationIndex", + "type": "uint256", + }, + ], + "name": "UnresolvedConsiderationCriteria", + "type": "error", + }, + { + "inputs": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "offerIndex", + "type": "uint256", + }, + ], + "name": "UnresolvedOfferCriteria", + "type": "error", + }, + { + "inputs": [], + "name": "UnusedItemParameters", + "type": "error", + }, +] +`; diff --git a/src/core/internal/human-readable/errors.test.ts b/src/core/internal/human-readable/errors.test.ts new file mode 100644 index 00000000..ad884590 --- /dev/null +++ b/src/core/internal/human-readable/errors.test.ts @@ -0,0 +1,265 @@ +import { expect, test } from 'vp/test' +import { + CircularReferenceError, + InvalidAbiItemError, + InvalidAbiParameterError, + InvalidAbiParametersError, + InvalidAbiTypeParameterError, + InvalidFunctionModifierError, + InvalidModifierError, + InvalidParameterError, + InvalidParenthesisError, + InvalidSignatureError, + InvalidStructSignatureError, + SolidityProtectedKeywordError, + UnknownSignatureError, + UnknownSolidityTypeError, + UnknownTypeError, +} from './errors.js' + +test('InvalidAbiItemError', () => { + expect( + new InvalidAbiItemError({ signature: 'address' }), + ).toMatchInlineSnapshot(` + [AbiItem.InvalidAbiItemError: Failed to parse ABI item. + + Details: parseAbiItem("address") + See: https://oxlib.sh/api/AbiItem/from] + `) +}) + +test('UnknownTypeError', () => { + expect(new UnknownTypeError({ type: 'Foo' })).toMatchInlineSnapshot(` + [HumanReadableAbi.UnknownTypeError: Unknown type. + + Type "Foo" is not a valid ABI type. Perhaps you forgot to include a struct signature?] + `) +}) + +test('UnknownSolidityTypeError', () => { + expect(new UnknownSolidityTypeError({ type: 'Foo' })).toMatchInlineSnapshot(` + [HumanReadableAbi.UnknownSolidityTypeError: Unknown type. + + Type "Foo" is not a valid ABI type.] + `) +}) + +test('InvalidAbiParamterError', () => { + expect( + new InvalidAbiParameterError({ param: 'address owner' }), + ).toMatchInlineSnapshot(` + [AbiParameter.InvalidAbiParameterError: Failed to parse ABI parameter. + + Details: parseAbiParameter("address owner") + See: https://oxlib.sh/api/AbiParameter/from] + `) +}) + +test('InvalidAbiParamtersError', () => { + expect( + new InvalidAbiParametersError({ params: 'address owner' }), + ).toMatchInlineSnapshot(` + [AbiParameters.InvalidAbiParametersError: Failed to parse ABI parameters. + + Details: parseAbiParameters("address owner") + See: https://oxlib.sh/api/AbiParameters/from] + `) +}) + +test('InvalidParameterError', () => { + expect( + new InvalidParameterError({ + param: 'address', + }), + ).toMatchInlineSnapshot(` + [HumanReadableAbi.InvalidParameterError: Invalid ABI parameter. + + Details: address] + `) +}) + +test('SolidityProtectedKeywordError', () => { + expect( + new SolidityProtectedKeywordError({ + param: 'address', + name: 'address', + }), + ).toMatchInlineSnapshot(` + [HumanReadableAbi.SolidityProtectedKeywordError: Invalid ABI parameter. + + "address" is a protected Solidity keyword. More info: https://docs.soliditylang.org/en/latest/cheatsheet.html + + Details: address] + `) +}) + +test('InvalidModifierError', () => { + expect( + new InvalidModifierError({ + param: 'address', + modifier: 'calldata', + type: 'event', + }), + ).toMatchInlineSnapshot(` + [HumanReadableAbi.InvalidModifierError: Invalid ABI parameter. + + Modifier "calldata" not allowed in "event" type. + + Details: address] + `) + + expect( + new InvalidModifierError({ + param: 'address', + modifier: 'calldata', + }), + ).toMatchInlineSnapshot(` + [HumanReadableAbi.InvalidModifierError: Invalid ABI parameter. + + Modifier "calldata" not allowed. + + Details: address] + `) +}) + +test('InvalidFunctionModifierError', () => { + expect( + new InvalidFunctionModifierError({ + param: 'address', + modifier: 'calldata', + type: 'function', + }), + ).toMatchInlineSnapshot(` + [HumanReadableAbi.InvalidFunctionModifierError: Invalid ABI parameter. + + Modifier "calldata" not allowed in "function" type. + Data location can only be specified for array, struct, or mapping types, but "calldata" was given. + + Details: address] + `) +}) + +test('InvalidAbiTypeParameterError', () => { + expect( + new InvalidAbiTypeParameterError({ + abiParameter: { type: 'address' }, + }), + ).toMatchInlineSnapshot(` + [HumanReadableAbi.InvalidAbiTypeParameterError: Invalid ABI parameter. + + ABI parameter type is invalid. + + Details: { + "type": "address" + }] + `) +}) + +test('InvalidSignatureError', () => { + expect( + new InvalidSignatureError({ + signature: 'function name??()', + type: 'function', + }), + ).toMatchInlineSnapshot(` + [Abi.InvalidSignatureError: Invalid function signature. + + Details: function name??()] + `) + + expect( + new InvalidSignatureError({ + signature: 'function name??()', + type: 'struct', + }), + ).toMatchInlineSnapshot(` + [Abi.InvalidSignatureError: Invalid struct signature. + + Details: function name??()] + `) + + expect( + new InvalidSignatureError({ + signature: 'function name??()', + type: 'error', + }), + ).toMatchInlineSnapshot(` + [Abi.InvalidSignatureError: Invalid error signature. + + Details: function name??()] + `) + + expect( + new InvalidSignatureError({ + signature: 'function name??()', + type: 'event', + }), + ).toMatchInlineSnapshot(` + [Abi.InvalidSignatureError: Invalid event signature. + + Details: function name??()] + `) + + expect( + new InvalidSignatureError({ + signature: 'function name??()', + type: 'constructor', + }), + ).toMatchInlineSnapshot(` + [Abi.InvalidSignatureError: Invalid constructor signature. + + Details: function name??()] + `) +}) + +test('UnknownSignatureError', () => { + expect( + new UnknownSignatureError({ signature: 'invalid' }), + ).toMatchInlineSnapshot(` + [Abi.UnknownSignatureError: Unknown signature. + + Details: invalid] + `) +}) + +test('InvalidStructSignatureError', () => { + expect( + new InvalidStructSignatureError({ signature: 'struct Foo{}' }), + ).toMatchInlineSnapshot(` + [Abi.InvalidStructSignatureError: Invalid struct signature. + + No properties exist. + + Details: struct Foo{}] + `) +}) + +test('InvalidParenthesisError', () => { + expect( + new InvalidParenthesisError({ current: '(Foo))', depth: -1 }), + ).toMatchInlineSnapshot(` + [HumanReadableAbi.InvalidParenthesisError: Unbalanced parentheses. + + "(Foo))" has too many closing parentheses. + + Details: Depth "-1"] + `) + + expect( + new InvalidParenthesisError({ current: '((Foo)', depth: 1 }), + ).toMatchInlineSnapshot(` + [HumanReadableAbi.InvalidParenthesisError: Unbalanced parentheses. + + "((Foo)" has too many opening parentheses. + + Details: Depth "1"] + `) +}) + +test('CircularReferenceError', () => { + expect(new CircularReferenceError({ type: 'Foo' })).toMatchInlineSnapshot(` + [Abi.CircularReferenceError: Circular reference detected. + + Struct "Foo" is a circular reference.] + `) +}) diff --git a/src/core/internal/human-readable/errors.ts b/src/core/internal/human-readable/errors.ts new file mode 100644 index 00000000..888440d3 --- /dev/null +++ b/src/core/internal/human-readable/errors.ts @@ -0,0 +1,205 @@ +import type { AbiItemType, AbiParameter } from 'abitype' +import { BaseError } from '../../Errors.js' +import type { Modifier } from './types/signatures.js' + +export class InvalidAbiItemError extends BaseError { + override name = 'AbiItem.InvalidAbiItemError' + + constructor({ signature }: { signature: string | object }) { + super('Failed to parse ABI item.', { + details: `parseAbiItem(${JSON.stringify(signature, null, 2)})`, + docsPath: '/api/AbiItem/from', + }) + } +} + +export class UnknownTypeError extends BaseError { + override name = 'HumanReadableAbi.UnknownTypeError' + + constructor({ type }: { type: string }) { + super('Unknown type.', { + metaMessages: [ + `Type "${type}" is not a valid ABI type. Perhaps you forgot to include a struct signature?`, + ], + }) + } +} + +export class UnknownSolidityTypeError extends BaseError { + override name = 'HumanReadableAbi.UnknownSolidityTypeError' + + constructor({ type }: { type: string }) { + super('Unknown type.', { + metaMessages: [`Type "${type}" is not a valid ABI type.`], + }) + } +} + +export class InvalidAbiParameterError extends BaseError { + override name = 'AbiParameter.InvalidAbiParameterError' + + constructor({ param }: { param: string | object }) { + super('Failed to parse ABI parameter.', { + details: `parseAbiParameter(${JSON.stringify(param, null, 2)})`, + docsPath: '/api/AbiParameter/from', + }) + } +} + +export class InvalidAbiParametersError extends BaseError { + override name = 'AbiParameters.InvalidAbiParametersError' + + constructor({ params }: { params: string | object }) { + super('Failed to parse ABI parameters.', { + details: `parseAbiParameters(${JSON.stringify(params, null, 2)})`, + docsPath: '/api/AbiParameters/from', + }) + } +} + +export class InvalidParameterError extends BaseError { + override name = 'HumanReadableAbi.InvalidParameterError' + + constructor({ param }: { param: string }) { + super('Invalid ABI parameter.', { + details: param, + }) + } +} + +export class SolidityProtectedKeywordError extends BaseError { + override name = 'HumanReadableAbi.SolidityProtectedKeywordError' + + constructor({ param, name }: { param: string; name: string }) { + super('Invalid ABI parameter.', { + details: param, + metaMessages: [ + `"${name}" is a protected Solidity keyword. More info: https://docs.soliditylang.org/en/latest/cheatsheet.html`, + ], + }) + } +} + +export class InvalidModifierError extends BaseError { + override name = 'HumanReadableAbi.InvalidModifierError' + + constructor({ + param, + type, + modifier, + }: { + param: string + type?: AbiItemType | 'struct' | undefined + modifier: Modifier + }) { + super('Invalid ABI parameter.', { + details: param, + metaMessages: [ + `Modifier "${modifier}" not allowed${ + type ? ` in "${type}" type` : '' + }.`, + ], + }) + } +} + +export class InvalidFunctionModifierError extends BaseError { + override name = 'HumanReadableAbi.InvalidFunctionModifierError' + + constructor({ + param, + type, + modifier, + }: { + param: string + type?: AbiItemType | 'struct' | undefined + modifier: Modifier + }) { + super('Invalid ABI parameter.', { + details: param, + metaMessages: [ + `Modifier "${modifier}" not allowed${ + type ? ` in "${type}" type` : '' + }.`, + `Data location can only be specified for array, struct, or mapping types, but "${modifier}" was given.`, + ], + }) + } +} + +export class InvalidAbiTypeParameterError extends BaseError { + override name = 'HumanReadableAbi.InvalidAbiTypeParameterError' + + constructor({ + abiParameter, + }: { + abiParameter: AbiParameter & { indexed?: boolean | undefined } + }) { + super('Invalid ABI parameter.', { + details: JSON.stringify(abiParameter, null, 2), + metaMessages: ['ABI parameter type is invalid.'], + }) + } +} + +export class InvalidSignatureError extends BaseError { + override name = 'Abi.InvalidSignatureError' + + constructor({ + signature, + type, + }: { + signature: string + type: AbiItemType | 'struct' + }) { + super(`Invalid ${type} signature.`, { + details: signature, + }) + } +} + +export class UnknownSignatureError extends BaseError { + override name = 'Abi.UnknownSignatureError' + + constructor({ signature }: { signature: string }) { + super('Unknown signature.', { + details: signature, + }) + } +} + +export class InvalidStructSignatureError extends BaseError { + override name = 'Abi.InvalidStructSignatureError' + + constructor({ signature }: { signature: string }) { + super('Invalid struct signature.', { + details: signature, + metaMessages: ['No properties exist.'], + }) + } +} + +export class InvalidParenthesisError extends BaseError { + override name = 'HumanReadableAbi.InvalidParenthesisError' + + constructor({ current, depth }: { current: string; depth: number }) { + super('Unbalanced parentheses.', { + metaMessages: [ + `"${current.trim()}" has too many ${ + depth > 0 ? 'opening' : 'closing' + } parentheses.`, + ], + details: `Depth "${depth}"`, + }) + } +} + +export class CircularReferenceError extends BaseError { + override name = 'Abi.CircularReferenceError' + + constructor({ type }: { type: string }) { + super('Circular reference detected.', { + metaMessages: [`Struct "${type}" is a circular reference.`], + }) + } +} diff --git a/src/core/internal/human-readable/formatAbi.test-d.ts b/src/core/internal/human-readable/formatAbi.test-d.ts new file mode 100644 index 00000000..9918967a --- /dev/null +++ b/src/core/internal/human-readable/formatAbi.test-d.ts @@ -0,0 +1,144 @@ +import { expectTypeOf, test } from 'vp/test' + +import type { Abi } from 'abitype' +import { seaportAbi } from '../../../../test/abis/json.js' +import type { FormatAbi } from './formatAbi.js' +import { formatAbi } from './formatAbi.js' + +test('FormatAbi', () => { + expectTypeOf>().toEqualTypeOf() + + expectTypeOf< + FormatAbi< + readonly [ + { + readonly name: 'foo' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [] + readonly outputs: readonly [] + }, + { + readonly name: 'bar' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'name' + readonly type: 'string' + }, + ] + }, + { + readonly type: 'bytes32' + }, + ] + readonly outputs: readonly [] + }, + ] + > + >().toEqualTypeOf< + readonly ['function foo()', 'function bar((string name), bytes32)'] + >() + + expectTypeOf< + FormatAbi< + readonly [ + { + readonly name: 'balanceOf' + readonly type: 'function' + readonly stateMutability: 'view' + readonly inputs: readonly [ + { + readonly name: 'owner' + readonly type: 'address' + }, + ] + readonly outputs: readonly [ + { + readonly type: 'uint256' + }, + ] + }, + { + readonly name: 'Transfer' + readonly type: 'event' + readonly inputs: readonly [ + { + readonly name: 'from' + readonly type: 'address' + readonly indexed: true + }, + { + readonly name: 'to' + readonly type: 'address' + readonly indexed: true + }, + { + readonly name: 'amount' + readonly type: 'uint256' + }, + ] + }, + ] + > + >().toEqualTypeOf< + readonly [ + 'function balanceOf(address owner) view returns (uint256)', + 'event Transfer(address indexed from, address indexed to, uint256 amount)', + ] + >() +}) + +test('formatAbi', () => { + expectTypeOf(formatAbi([])).toEqualTypeOf() + + // Array + const res = formatAbi([ + { + name: 'bar', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'tuple', + components: [ + { + name: 'name', + type: 'string', + }, + ], + }, + { + type: 'bytes32', + }, + ], + outputs: [], + }, + ]) + expectTypeOf().toEqualTypeOf< + readonly ['function bar((string name), bytes32)'] + >() + + const abi2 = [ + { + type: 'function', + name: 'foo', + inputs: [], + outputs: [], + stateMutability: 'view', + }, + ] + expectTypeOf(formatAbi(abi2)).toEqualTypeOf() + + const param = abi2 as Abi + expectTypeOf(formatAbi(param)).toEqualTypeOf() + + const getOrderType = formatAbi(seaportAbi)[10] + expectTypeOf< + typeof getOrderType + >().toEqualTypeOf<'function getOrderHash((address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 counter) order) view returns (bytes32 orderHash)'>() +}) diff --git a/src/core/internal/human-readable/formatAbi.test.ts b/src/core/internal/human-readable/formatAbi.test.ts new file mode 100644 index 00000000..df0169f0 --- /dev/null +++ b/src/core/internal/human-readable/formatAbi.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from 'vp/test' + +import { seaportAbi } from '../../../../test/abis/json.js' +import { formatAbi } from './formatAbi.js' + +const customSolidityErrorsAbi = [ + { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, + { inputs: [], name: 'ApprovalCallerNotOwnerNorApproved', type: 'error' }, + { inputs: [], name: 'ApprovalQueryForNonexistentToken', type: 'error' }, +] as const + +test('formatAbi', () => { + const result = formatAbi(seaportAbi) + expect(result).toMatchSnapshot() + + expect(formatAbi(customSolidityErrorsAbi)).toMatchInlineSnapshot(` + [ + "constructor()", + "error ApprovalCallerNotOwnerNorApproved()", + "error ApprovalQueryForNonexistentToken()", + ] + `) +}) diff --git a/src/core/internal/human-readable/formatAbi.ts b/src/core/internal/human-readable/formatAbi.ts new file mode 100644 index 00000000..be955649 --- /dev/null +++ b/src/core/internal/human-readable/formatAbi.ts @@ -0,0 +1,37 @@ +import type { Abi } from 'abitype' +import { type FormatAbiItem, formatAbiItem } from './formatAbiItem.js' + +/** + * Parses JSON ABI into human-readable ABI + * + * @param abi - ABI + * @returns Human-readable ABI + */ +export type FormatAbi = Abi extends abi + ? readonly string[] + : abi extends readonly [] + ? never + : abi extends Abi + ? { + [key in keyof abi]: FormatAbiItem + } + : readonly string[] + +/** + * Parses JSON ABI into human-readable ABI + * + * @param abi - ABI + * @returns Human-readable ABI + */ +export function formatAbi( + abi: abi, +): FormatAbi { + const signatures = [] + const length = abi.length + for (let i = 0; i < length; i++) { + const abiItem = abi[i]! + const signature = formatAbiItem(abiItem as Abi[number]) + signatures.push(signature) + } + return signatures as unknown as FormatAbi +} diff --git a/src/core/internal/human-readable/formatAbiItem.test-d.ts b/src/core/internal/human-readable/formatAbiItem.test-d.ts new file mode 100644 index 00000000..58b108c7 --- /dev/null +++ b/src/core/internal/human-readable/formatAbiItem.test-d.ts @@ -0,0 +1,115 @@ +import { expectTypeOf, test } from 'vp/test' + +import type { + Abi, + AbiConstructor, + AbiError, + AbiEvent, + AbiFallback, + AbiFunction, + AbiReceive, +} from 'abitype' +import type { FormatAbiItem } from './formatAbiItem.js' +import { formatAbiItem } from './formatAbiItem.js' + +test('FormatAbiItem', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + + expectTypeOf< + FormatAbiItem<{ + readonly name: 'foo' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [] + readonly outputs: readonly [] + }> + >().toEqualTypeOf<'function foo()'>() + + expectTypeOf< + FormatAbiItem<{ + readonly name: 'address' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [] + readonly outputs: readonly [] + }> + >().toEqualTypeOf<'function [Error: "address" is a protected Solidity keyword.]()'>() + + expectTypeOf< + FormatAbiItem<{ + readonly name: 'Transfer' + readonly type: 'event' + readonly inputs: readonly [ + { + readonly name: 'from' + readonly type: 'address' + readonly indexed: true + }, + { + readonly name: 'to' + readonly type: 'address' + readonly indexed: true + }, + { + readonly name: 'amount' + readonly type: 'uint256' + }, + ] + }> + >().toEqualTypeOf<'event Transfer(address indexed from, address indexed to, uint256 amount)'>() +}) + +test('formatAbiItem', () => { + expectTypeOf( + formatAbiItem({ + name: 'foo', + type: 'function', + stateMutability: 'nonpayable', + inputs: [], + outputs: [], + }), + ).toEqualTypeOf<'function foo()'>() + expectTypeOf( + formatAbiItem({ + name: 'foo', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'tuple', + components: [ + { + type: 'string', + }, + ], + }, + { + type: 'address', + }, + ], + outputs: [], + }), + ).toEqualTypeOf<'function foo((string), address)'>() + + const abiItem: Abi[number] = { + type: 'function', + name: 'foo', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + } + expectTypeOf(formatAbiItem(abiItem)).toEqualTypeOf() + + expectTypeOf( + formatAbiItem({ type: 'fallback', stateMutability: 'nonpayable' }), + ).toEqualTypeOf<'fallback() external'>() + expectTypeOf( + formatAbiItem({ type: 'fallback', stateMutability: 'payable' }), + ).toEqualTypeOf<'fallback() external payable'>() +}) diff --git a/src/core/internal/human-readable/formatAbiItem.test.ts b/src/core/internal/human-readable/formatAbiItem.test.ts new file mode 100644 index 00000000..973eeef7 --- /dev/null +++ b/src/core/internal/human-readable/formatAbiItem.test.ts @@ -0,0 +1,102 @@ +import { expect, test } from 'vp/test' + +import { seaportAbi } from '../../../../test/abis/json.js' +import { formatAbiItem } from './formatAbiItem.js' + +test('default', () => { + const result = formatAbiItem(seaportAbi[1]) + expect(result).toMatchInlineSnapshot( + '"function cancel((address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 counter)[] orders) returns (bool cancelled)"', + ) +}) + +test.each([ + { + abiItem: { + type: 'function', + name: 'foo', + inputs: [{ type: 'string' }], + outputs: [], + stateMutability: 'nonpayable', + } as const, + expected: 'function foo(string)', + }, + { + abiItem: { + type: 'event', + name: 'Foo', + inputs: [ + { type: 'address', name: 'from', indexed: true }, + { type: 'address', name: 'to', indexed: true }, + { type: 'uint256', name: 'amount' }, + ], + } as const, + expected: + 'event Foo(address indexed from, address indexed to, uint256 amount)', + }, + { + abiItem: { + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [{ type: 'string' }], + } as const, + expected: 'constructor(string)', + }, + { + abiItem: { + type: 'constructor', + stateMutability: 'payable', + inputs: [{ type: 'string' }], + } as const, + expected: 'constructor(string) payable', + }, + { + abiItem: { + type: 'fallback', + stateMutability: 'nonpayable', + } as const, + expected: 'fallback() external', + }, + { + abiItem: { + type: 'fallback', + stateMutability: 'payable', + } as const, + expected: 'fallback() external payable', + }, + { + abiItem: { + type: 'receive', + stateMutability: 'payable', + } as const, + expected: 'receive() external payable', + }, + { + abiItem: { + type: 'function', + name: 'initWormhole', + inputs: [ + { + type: 'tuple[]', + name: 'configs', + components: [ + { + type: 'uint256', + name: 'chainId', + }, + { + type: 'uint16', + name: 'wormholeChainId', + }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + } as const, + expected: + 'function initWormhole((uint256 chainId, uint16 wormholeChainId)[] configs)', + }, +])('formatAbiItem($expected)', ({ abiItem, expected }) => { + expect(formatAbiItem(abiItem)).toEqual(expected) +}) diff --git a/src/core/internal/human-readable/formatAbiItem.ts b/src/core/internal/human-readable/formatAbiItem.ts new file mode 100644 index 00000000..d9983e48 --- /dev/null +++ b/src/core/internal/human-readable/formatAbiItem.ts @@ -0,0 +1,136 @@ +import type { + Abi, + AbiConstructor, + AbiError, + AbiEvent, + AbiEventParameter, + AbiFallback, + AbiFunction, + AbiParameter, + AbiReceive, + AbiStateMutability, +} from 'abitype' +import { + type FormatAbiParameters as FormatAbiParameters_, + formatAbiParameters, +} from './formatAbiParameters.js' +import type { AssertName } from './types/signatures.js' + +/** + * Formats ABI item (e.g. error, event, function) into human-readable ABI item + * + * @param abiItem - ABI item + * @returns Human-readable ABI item + */ +export type FormatAbiItem = + Abi[number] extends abiItem + ? string + : + | (abiItem extends AbiFunction + ? AbiFunction extends abiItem + ? string + : `function ${AssertName}(${FormatAbiParameters< + abiItem['inputs'] + >})${abiItem['stateMutability'] extends Exclude< + AbiStateMutability, + 'nonpayable' + > + ? ` ${abiItem['stateMutability']}` + : ''}${abiItem['outputs']['length'] extends 0 + ? '' + : ` returns (${FormatAbiParameters})`}` + : never) + | (abiItem extends AbiEvent + ? AbiEvent extends abiItem + ? string + : `event ${AssertName}(${FormatAbiParameters< + abiItem['inputs'] + >})` + : never) + | (abiItem extends AbiError + ? AbiError extends abiItem + ? string + : `error ${AssertName}(${FormatAbiParameters< + abiItem['inputs'] + >})` + : never) + | (abiItem extends AbiConstructor + ? AbiConstructor extends abiItem + ? string + : `constructor(${FormatAbiParameters< + abiItem['inputs'] + >})${abiItem['stateMutability'] extends 'payable' + ? ' payable' + : ''}` + : never) + | (abiItem extends AbiFallback + ? AbiFallback extends abiItem + ? string + : `fallback() external${abiItem['stateMutability'] extends 'payable' + ? ' payable' + : ''}` + : never) + | (abiItem extends AbiReceive + ? AbiReceive extends abiItem + ? string + : 'receive() external payable' + : never) + +type FormatAbiParameters< + abiParameters extends readonly (AbiParameter | AbiEventParameter)[], +> = abiParameters['length'] extends 0 + ? '' + : FormatAbiParameters_< + abiParameters extends readonly [ + AbiParameter | AbiEventParameter, + ...(readonly (AbiParameter | AbiEventParameter)[]), + ] + ? abiParameters + : never + > + +/** + * Formats ABI item (e.g. error, event, function) into human-readable ABI item + * + * @param abiItem - ABI item + * @returns Human-readable ABI item + */ +export function formatAbiItem( + abiItem: abiItem, +): FormatAbiItem { + type Result = FormatAbiItem + type Params = readonly [ + AbiParameter | AbiEventParameter, + ...(readonly (AbiParameter | AbiEventParameter)[]), + ] + + if (abiItem.type === 'function') + return `function ${abiItem.name}(${formatAbiParameters( + abiItem.inputs as Params, + )})${ + abiItem.stateMutability && abiItem.stateMutability !== 'nonpayable' + ? ` ${abiItem.stateMutability}` + : '' + }${ + abiItem.outputs?.length + ? ` returns (${formatAbiParameters(abiItem.outputs as Params)})` + : '' + }` + if (abiItem.type === 'event') + return `event ${abiItem.name}(${formatAbiParameters( + abiItem.inputs as Params, + )})` + if (abiItem.type === 'error') + return `error ${abiItem.name}(${formatAbiParameters( + abiItem.inputs as Params, + )})` + if (abiItem.type === 'constructor') + return `constructor(${formatAbiParameters(abiItem.inputs as Params)})${ + abiItem.stateMutability === 'payable' ? ' payable' : '' + }` + if (abiItem.type === 'fallback') + return `fallback() external${ + abiItem.stateMutability === 'payable' ? ' payable' : '' + }` as Result + return 'receive() external payable' as Result +} diff --git a/src/core/internal/human-readable/formatAbiParameter.test-d.ts b/src/core/internal/human-readable/formatAbiParameter.test-d.ts new file mode 100644 index 00000000..afe18c35 --- /dev/null +++ b/src/core/internal/human-readable/formatAbiParameter.test-d.ts @@ -0,0 +1,125 @@ +import { expectTypeOf, test } from 'vp/test' + +import type { AbiParameter } from 'abitype' +import type { FormatAbiParameter } from './formatAbiParameter.js' +import { formatAbiParameter } from './formatAbiParameter.js' + +test('FormatAbiParameter', () => { + // string + expectTypeOf< + FormatAbiParameter<{ + readonly type: 'address' + readonly name: 'from' + }> + >().toEqualTypeOf<'address from'>() + expectTypeOf< + FormatAbiParameter<{ + readonly type: 'address' + readonly name: 'from' + readonly indexed: true + }> + >().toEqualTypeOf<'address indexed from'>() + expectTypeOf< + FormatAbiParameter<{ + readonly type: 'address' + readonly name: '' + }> + >().toEqualTypeOf<'address'>() + + expectTypeOf< + FormatAbiParameter<{ + type: 'address' + name: 'address' + }> + >().toEqualTypeOf<'address [Error: "address" is a protected Solidity keyword.]'>() + + expectTypeOf< + FormatAbiParameter<{ + type: 'address' + name: '123' + }> + >().toEqualTypeOf<'address [Error: Identifier "123" cannot be a number string.]'>() + + // Array + expectTypeOf< + FormatAbiParameter<{ + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'name' + readonly type: 'string' + }, + ] + }> + >().toEqualTypeOf<'(string name)'>() + + expectTypeOf< + FormatAbiParameter<{ + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'string' + readonly name: 'bar' + }, + ] + readonly name: 'foo' + }> + >().toEqualTypeOf<'(string bar) foo'>() + + expectTypeOf< + FormatAbiParameter<{ + readonly components: [ + { + readonly components: [ + { + readonly type: 'string' + readonly name: 'foo' + }, + ] + readonly type: 'tuple' + }, + ] + readonly type: 'tuple' + }> + >().toEqualTypeOf<'((string foo))'>() + + expectTypeOf< + FormatAbiParameter<{ + readonly components: [ + { + readonly components: [ + { + readonly components: [ + { + readonly components: [ + { + readonly type: 'string' + }, + ] + readonly type: 'tuple' + }, + ] + readonly type: 'tuple' + }, + ] + readonly type: 'tuple' + }, + ] + readonly type: 'tuple' + }> + >().toEqualTypeOf<'((((string))))'>() +}) + +test('formatAbiParameter', () => { + expectTypeOf( + formatAbiParameter({ + type: 'tuple', + components: [{ type: 'string' }], + }), + ).toEqualTypeOf<'(string)'>() + + const param = { type: 'address' } + const param2: AbiParameter = param + expectTypeOf(formatAbiParameter(param)).toEqualTypeOf() + expectTypeOf(formatAbiParameter(param2)).toEqualTypeOf() +}) diff --git a/src/core/internal/human-readable/formatAbiParameter.test.ts b/src/core/internal/human-readable/formatAbiParameter.test.ts new file mode 100644 index 00000000..a29d3b06 --- /dev/null +++ b/src/core/internal/human-readable/formatAbiParameter.test.ts @@ -0,0 +1,126 @@ +import { assertType, expect, expectTypeOf, test } from 'vp/test' + +import { formatAbiParameter } from './formatAbiParameter.js' + +test('default', () => { + const result = formatAbiParameter({ type: 'address', name: 'foo' }) + expect(result).toEqual('address foo') + expectTypeOf(result).toEqualTypeOf<'address foo'>() +}) + +test('tuple', () => { + const result = formatAbiParameter({ + type: 'tuple', + components: [ + { type: 'string', name: 'bar' }, + { type: 'string', name: 'baz' }, + ], + name: 'foo', + }) + expect(result).toMatchInlineSnapshot('"(string bar, string baz) foo"') + expectTypeOf(result).toEqualTypeOf<'(string bar, string baz) foo'>() +}) + +test('tuple[][]', () => { + const result = formatAbiParameter({ + type: 'tuple[123][]', + components: [ + { type: 'string', name: 'bar' }, + { type: 'string', name: 'baz' }, + ], + name: 'foo', + }) + expect(result).toMatchInlineSnapshot('"(string bar, string baz)[123][] foo"') + expectTypeOf(result).toEqualTypeOf<'(string bar, string baz)[123][] foo'>() +}) + +test.each([ + { + abiParameter: { type: 'string' }, + expected: 'string', + }, + { + abiParameter: { name: 'foo', type: 'string' }, + expected: 'string foo', + }, + { + abiParameter: { name: 'foo', type: 'string', indexed: true }, + expected: 'string indexed foo', + }, + { + abiParameter: { type: 'tuple', components: [{ type: 'string' }] }, + expected: '(string)', + }, + { + abiParameter: { + type: 'tuple', + components: [{ name: 'foo', type: 'string' }], + }, + expected: '(string foo)', + }, + { + abiParameter: { + type: 'tuple', + name: 'foo', + components: [{ name: 'bar', type: 'string' }], + }, + expected: '(string bar) foo', + }, + { + abiParameter: { + type: 'tuple', + name: 'foo', + components: [ + { name: 'bar', type: 'string' }, + { name: 'baz', type: 'string' }, + ], + }, + expected: '(string bar, string baz) foo', + }, + { + abiParameter: { type: 'string', indexed: false }, + expected: 'string', + }, + { + abiParameter: { type: 'string', indexed: true }, + expected: 'string indexed', + }, + { + abiParameter: { type: 'string', indexed: true, name: 'foo' }, + expected: 'string indexed foo', + }, +])('formatAbiParameter($abiParameter)', ({ abiParameter, expected }) => { + expect(formatAbiParameter(abiParameter)).toEqual(expected) +}) + +test('nested tuple', () => { + const result = formatAbiParameter({ + components: [ + { + components: [ + { + components: [ + { + components: [ + { + name: 'baz', + type: 'string', + }, + ], + name: 'bar', + type: 'tuple', + }, + ], + name: 'foo', + type: 'tuple[1]', + }, + ], + name: 'boo', + type: 'tuple', + }, + ], + type: 'tuple', + }) + expect(result).toMatchInlineSnapshot('"((((string baz) bar)[1] foo) boo)"') + assertType<'((((string baz) bar)[1] foo) boo)'>(result) +}) diff --git a/src/core/internal/human-readable/formatAbiParameter.ts b/src/core/internal/human-readable/formatAbiParameter.ts new file mode 100644 index 00000000..aafb0055 --- /dev/null +++ b/src/core/internal/human-readable/formatAbiParameter.ts @@ -0,0 +1,86 @@ +import type { AbiEventParameter, AbiParameter } from 'abitype' +import { execTyped } from './regex.js' +import type { IsNarrowable, Join } from './types.js' +import type { AssertName } from './types/signatures.js' + +/** + * Formats `AbiParameter` to human-readable ABI parameter. + * + * @param abiParameter - ABI parameter + * @returns Human-readable ABI parameter + */ +export type FormatAbiParameter< + abiParameter extends AbiParameter | AbiEventParameter, +> = abiParameter extends { + name?: infer name extends string + type: `tuple${infer array}` + components: infer components extends readonly AbiParameter[] + indexed?: infer indexed extends boolean +} + ? FormatAbiParameter< + { + type: `(${Join< + { + [key in keyof components]: FormatAbiParameter< + { + type: components[key]['type'] + } & (IsNarrowable extends true + ? { name: components[key]['name'] } + : unknown) & + (components[key] extends { components: readonly AbiParameter[] } + ? { components: components[key]['components'] } + : unknown) + > + }, + ', ' + >})${array}` + } & (IsNarrowable extends true ? { name: name } : unknown) & + (IsNarrowable extends true + ? { indexed: indexed } + : unknown) + > + : `${abiParameter['type']}${abiParameter extends { indexed: true } + ? ' indexed' + : ''}${abiParameter['name'] extends infer name extends string + ? name extends '' + ? '' + : ` ${AssertName}` + : ''}` + +// https://regexr.com/7f7rv +const tupleRegex = /^tuple(?(\[(\d*)\])*)$/ + +/** + * Formats `AbiParameter` to human-readable ABI parameter. + * + * @param abiParameter - ABI parameter + * @returns Human-readable ABI parameter + */ +export function formatAbiParameter< + const abiParameter extends AbiParameter | AbiEventParameter, +>(abiParameter: abiParameter): FormatAbiParameter { + type Result = FormatAbiParameter + + 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 += formatAbiParameter(component) + if (i < length - 1) type += ', ' + } + const result = execTyped<{ array?: string }>(tupleRegex, abiParameter.type) + type += `)${result?.array || ''}` + return formatAbiParameter({ + ...abiParameter, + type, + }) as Result + } + // Add `indexed` to type if in `abiParameter` + if ('indexed' in abiParameter && abiParameter.indexed) + type = `${type} indexed` + // Return human-readable ABI parameter + if (abiParameter.name) return `${type} ${abiParameter.name}` as Result + return type as Result +} diff --git a/src/core/internal/human-readable/formatAbiParameters.test-d.ts b/src/core/internal/human-readable/formatAbiParameters.test-d.ts new file mode 100644 index 00000000..a5151662 --- /dev/null +++ b/src/core/internal/human-readable/formatAbiParameters.test-d.ts @@ -0,0 +1,92 @@ +import { expectTypeOf, test } from 'vp/test' + +import type { AbiParameter } from 'abitype' + +import type { FormatAbiParameters } from './formatAbiParameters.js' +import { formatAbiParameters } from './formatAbiParameters.js' + +test('FormatAbiParameters', () => { + // @ts-expect-error must have at least one parameter + expectTypeOf>().toEqualTypeOf() + + // string + expectTypeOf< + FormatAbiParameters< + [ + { + readonly type: 'address' + readonly name: 'from' + }, + ] + > + >().toEqualTypeOf<'address from'>() + expectTypeOf< + FormatAbiParameters< + [ + { + readonly type: 'address' + readonly name: 'from' + readonly indexed: true + }, + ] + > + >().toEqualTypeOf<'address indexed from'>() + + // Array + expectTypeOf< + FormatAbiParameters< + [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'name' + readonly type: 'string' + }, + ] + }, + ] + > + >().toEqualTypeOf<'(string name)'>() + + expectTypeOf< + FormatAbiParameters< + [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'string' + readonly name: 'bar' + }, + ] + readonly name: 'foo' + }, + ] + > + >().toEqualTypeOf<'(string bar) foo'>() +}) + +test('formatAbiParameter', () => { + expectTypeOf( + formatAbiParameters([ + { + type: 'tuple', + components: [{ type: 'string' }], + }, + ]), + ).toEqualTypeOf<'(string)'>() + + const param = { type: 'address' } + const param2: AbiParameter = param + + expectTypeOf(formatAbiParameters([param])).toEqualTypeOf() + + expectTypeOf( + formatAbiParameters([param, param]), + ).toEqualTypeOf<`${string}, ${string}`>() + + expectTypeOf( + formatAbiParameters([param2, param2]), + ).toEqualTypeOf<`${string}, ${string}`>() +}) diff --git a/src/core/internal/human-readable/formatAbiParameters.test.ts b/src/core/internal/human-readable/formatAbiParameters.test.ts new file mode 100644 index 00000000..9c1c3460 --- /dev/null +++ b/src/core/internal/human-readable/formatAbiParameters.test.ts @@ -0,0 +1,32 @@ +import { expect, expectTypeOf, test } from 'vp/test' + +import { formatAbiParameters } from './formatAbiParameters.js' + +test('default', () => { + const result = formatAbiParameters([ + { type: 'address', name: 'foo' }, + { type: 'uint256', name: 'bar' }, + ]) + expect(result).toEqual('address foo, uint256 bar') + expectTypeOf(result).toEqualTypeOf<'address foo, uint256 bar'>() +}) + +test('tuple', () => { + const result = formatAbiParameters([ + { + type: 'tuple', + components: [ + { type: 'string', name: 'bar' }, + { type: 'string', name: 'baz' }, + ], + name: 'foo', + }, + { type: 'uint256', name: 'bar' }, + ]) + expect(result).toMatchInlineSnapshot( + '"(string bar, string baz) foo, uint256 bar"', + ) + expectTypeOf( + result, + ).toEqualTypeOf<'(string bar, string baz) foo, uint256 bar'>() +}) diff --git a/src/core/internal/human-readable/formatAbiParameters.ts b/src/core/internal/human-readable/formatAbiParameters.ts new file mode 100644 index 00000000..360073a7 --- /dev/null +++ b/src/core/internal/human-readable/formatAbiParameters.ts @@ -0,0 +1,46 @@ +import type { AbiEventParameter, AbiParameter } from 'abitype' +import type { Join } from './types.js' +import { + type FormatAbiParameter, + formatAbiParameter, +} from './formatAbiParameter.js' + +/** + * Formats `AbiParameter`s to human-readable ABI parameter. + * + * @param abiParameters - ABI parameters + * @returns Human-readable ABI parameters + */ +export type FormatAbiParameters< + abiParameters extends readonly [ + AbiParameter | AbiEventParameter, + ...(readonly (AbiParameter | AbiEventParameter)[]), + ], +> = Join< + { + [key in keyof abiParameters]: FormatAbiParameter + }, + ', ' +> + +/** + * Formats `AbiParameter`s to human-readable ABI parameters. + * + * @param abiParameters - ABI parameters + * @returns Human-readable ABI parameters + */ +export function formatAbiParameters< + const abiParameters extends readonly [ + AbiParameter | AbiEventParameter, + ...(readonly (AbiParameter | AbiEventParameter)[]), + ], +>(abiParameters: abiParameters): FormatAbiParameters { + let params = '' + const length = abiParameters.length + for (let i = 0; i < length; i++) { + const abiParameter = abiParameters[i]! + params += formatAbiParameter(abiParameter) + if (i !== length - 1) params += ', ' + } + return params as FormatAbiParameters +} diff --git a/src/core/internal/human-readable/human-readable.bench-d.ts b/src/core/internal/human-readable/human-readable.bench-d.ts new file mode 100644 index 00000000..99c50d79 --- /dev/null +++ b/src/core/internal/human-readable/human-readable.bench-d.ts @@ -0,0 +1,59 @@ +import { attest } from '@ark/attest' +import { Abi, AbiItem, AbiParameter, AbiParameters } from 'ox' +import { describe, test } from 'vp/test' +import type { seaportHumanReadableAbi } from '../../../../test/abis/human-readable.js' + +describe('human-readable ABI type instantiations', () => { + test('Abi.from.ReturnType: erc20-sized ABI', () => { + type Result = Abi.from.ReturnType< + [ + 'function name() view returns (string)', + 'function symbol() view returns (string)', + 'function decimals() view returns (uint8)', + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address owner) view returns (uint256)', + 'function transfer(address to, uint256 amount) returns (bool)', + 'function transferFrom(address from, address to, uint256 amount) returns (bool)', + 'function approve(address spender, uint256 amount) returns (bool)', + 'function allowance(address owner, address spender) view returns (uint256)', + 'event Transfer(address indexed from, address indexed to, uint256 amount)', + 'event Approval(address indexed owner, address indexed spender, uint256 amount)', + ] + > + attest.instantiations([45_000, 'instantiations']) + attest({} as Result) + }) + + test('Abi.from.ReturnType: seaport human-readable ABI', () => { + type Result = Abi.from.ReturnType + attest.instantiations([5_000_000, 'instantiations']) + attest({} as Result) + }) + + test('AbiItem.from.ReturnType: struct item', () => { + type Result = AbiItem.from.ReturnType< + [ + 'struct Foo { address spender; uint256 amount; }', + 'function approve(Foo foo) returns (bool)', + ] + > + attest.instantiations([15_000, 'instantiations']) + attest({} as Result) + }) + + test('AbiParameters.from.ReturnType: nested tuple parameters', () => { + type Result = AbiParameters.from.ReturnType< + '(uint8 a, uint8[] b, (uint8 x, uint8 y)[] c) s, (uint x, uint y) t, uint256 a' + > + attest.instantiations([20_000, 'instantiations']) + attest({} as Result) + }) + + test('AbiParameter.from.ReturnType: struct parameter', () => { + type Result = AbiParameter.from.ReturnType< + ['struct Foo { address spender; uint256 amount; }', 'Foo foo'] + > + attest.instantiations([10_000, 'instantiations']) + attest({} as Result) + }) +}) diff --git a/src/core/internal/human-readable/integration.test.ts b/src/core/internal/human-readable/integration.test.ts new file mode 100644 index 00000000..7540e491 --- /dev/null +++ b/src/core/internal/human-readable/integration.test.ts @@ -0,0 +1,68 @@ +import { expect, test } from 'vp/test' +import { formatAbiItem } from './formatAbiItem.js' +import { parseAbiItem } from './parseAbiItem.js' + +test.each([ + { + type: 'fallback', + stateMutability: 'payable', + } as const, + { + type: 'fallback', + stateMutability: 'nonpayable', + } as const, + { + type: 'receive', + stateMutability: 'payable', + } as const, + { + type: 'function', + name: 'foo', + inputs: [{ type: 'string' }], + outputs: [], + stateMutability: 'nonpayable', + } as const, + { + type: 'event', + name: 'Foo', + inputs: [ + { type: 'address', name: 'from', indexed: true }, + { type: 'address', name: 'to', indexed: true }, + { type: 'uint256', name: 'amount' }, + ], + } as const, + { + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [{ type: 'string' }], + } as const, + { + type: 'constructor', + stateMutability: 'payable', + inputs: [{ type: 'string' }], + } as const, + { + type: 'function', + name: 'initWormhole', + inputs: [ + { + type: 'tuple[]', + name: 'configs', + components: [ + { + type: 'uint256', + name: 'chainId', + }, + { + type: 'uint16', + name: 'wormholeChainId', + }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + } as const, +])('use of parseAbiItem - formatAbiItem should be reversible', (abiItem) => { + expect(parseAbiItem(formatAbiItem(abiItem))).toEqual(abiItem) +}) diff --git a/src/core/internal/human-readable/parseAbi.test-d.ts b/src/core/internal/human-readable/parseAbi.test-d.ts new file mode 100644 index 00000000..e78446ee --- /dev/null +++ b/src/core/internal/human-readable/parseAbi.test-d.ts @@ -0,0 +1,188 @@ +import { expectTypeOf, test } from 'vp/test' + +import type { Abi } from 'abitype' + +import { seaportHumanReadableAbi } from '../../../../test/abis/human-readable.js' +import type { IsAbi } from 'abitype' +import type { ParseAbi } from './parseAbi.js' +import { parseAbi } from './parseAbi.js' + +test('ParseAbi', () => { + type SeaportAbi = ParseAbi + expectTypeOf>().toEqualTypeOf() + + expectTypeOf>().toEqualTypeOf() + expectTypeOf< + ParseAbi<['struct Foo { string name; }']> + >().toEqualTypeOf() + + expectTypeOf< + ParseAbi< + [ + 'function foo()', + 'function bar(Foo, bytes32)', + 'struct Foo { string name; }', + ] + > + >().toEqualTypeOf< + readonly [ + { + readonly name: 'foo' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [] + readonly outputs: readonly [] + }, + { + readonly name: 'bar' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'name' + readonly type: 'string' + }, + ] + }, + { + readonly type: 'bytes32' + }, + ] + readonly outputs: readonly [] + }, + ] + >() + + expectTypeOf< + ParseAbi< + [ + 'function balanceOf(address owner) view returns (uint256)', + 'event Transfer(address indexed from, address indexed to, uint256 amount)', + ] + > + >().toEqualTypeOf< + readonly [ + { + readonly name: 'balanceOf' + readonly type: 'function' + readonly stateMutability: 'view' + readonly inputs: readonly [ + { + readonly name: 'owner' + readonly type: 'address' + }, + ] + readonly outputs: readonly [ + { + readonly type: 'uint256' + }, + ] + }, + { + readonly name: 'Transfer' + readonly type: 'event' + readonly inputs: readonly [ + { + readonly name: 'from' + readonly type: 'address' + readonly indexed: true + }, + { + readonly name: 'to' + readonly type: 'address' + readonly indexed: true + }, + { + readonly name: 'amount' + readonly type: 'uint256' + }, + ] + }, + ] + >() + + expectTypeOf>().toEqualTypeOf() +}) + +test('parseAbi', () => { + // @ts-expect-error empty array not allowed + expectTypeOf(parseAbi([])).toEqualTypeOf() + expectTypeOf(parseAbi(['struct Foo { string name; }'])).toEqualTypeOf() + + // Array + const res2 = parseAbi([ + 'function bar(Foo, bytes32)', + 'struct Foo { string name; }', + ]) + expectTypeOf().toEqualTypeOf< + readonly [ + { + readonly name: 'bar' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'name' + readonly type: 'string' + }, + ] + }, + { + readonly type: 'bytes32' + }, + ] + readonly outputs: readonly [] + }, + ] + >() + + const abi2 = [ + 'function foo()', + 'function bar(Foo, bytes32)', + 'struct Foo { string name; }', + ] + expectTypeOf(parseAbi(abi2)).toEqualTypeOf() + + // @ts-expect-error invalid signature + expectTypeOf(parseAbi(['function foo ()'])).toEqualTypeOf() + + const param: string[] = abi2 + expectTypeOf(parseAbi(param)).toEqualTypeOf() + + const getOrderType = parseAbi(seaportHumanReadableAbi)[10] + expectTypeOf().toEqualTypeOf<{ + readonly name: 'getOrderStatus' + readonly type: 'function' + readonly stateMutability: 'view' + readonly inputs: readonly [ + { + readonly type: 'bytes32' + readonly name: 'orderHash' + }, + ] + readonly outputs: readonly [ + { + readonly type: 'bool' + readonly name: 'isValidated' + }, + { + readonly type: 'bool' + readonly name: 'isCancelled' + }, + { + readonly type: 'uint256' + readonly name: 'totalFilled' + }, + { + readonly type: 'uint256' + readonly name: 'totalSize' + }, + ] + }>() +}) diff --git a/src/core/internal/human-readable/parseAbi.test.ts b/src/core/internal/human-readable/parseAbi.test.ts new file mode 100644 index 00000000..19e5d4b1 --- /dev/null +++ b/src/core/internal/human-readable/parseAbi.test.ts @@ -0,0 +1,82 @@ +import { expect, test } from 'vp/test' + +import { seaportHumanReadableAbi } from '../../../../test/abis/human-readable.js' +import { parseAbi } from './parseAbi.js' + +const customSolidityErrorsHumanReadableAbi = [ + 'constructor()', + 'error ApprovalCallerNotOwnerNorApproved()', + 'error ApprovalQueryForNonexistentToken()', +] as const + +test('parseAbi', () => { + const result = parseAbi(seaportHumanReadableAbi) + expect(result).toMatchSnapshot() + + expect(parseAbi(customSolidityErrorsHumanReadableAbi)).toMatchInlineSnapshot(` + [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor", + }, + { + "inputs": [], + "name": "ApprovalCallerNotOwnerNorApproved", + "type": "error", + }, + { + "inputs": [], + "name": "ApprovalQueryForNonexistentToken", + "type": "error", + }, + ] + `) +}) + +test('busts cache', () => { + const result1 = parseAbi([ + 'function balanceOf(Baz baz)', + 'struct Baz {uint amount; string role;}', + ]) + expect(result1[0].inputs).toMatchInlineSnapshot(` + [ + { + "components": [ + { + "name": "amount", + "type": "uint256", + }, + { + "name": "role", + "type": "string", + }, + ], + "name": "baz", + "type": "tuple", + }, + ] + `) + const result2 = parseAbi([ + 'function balanceOf(Baz baz)', + 'struct Baz {uint price; string role;}', + ]) + expect(result2[0].inputs).toMatchInlineSnapshot(` + [ + { + "components": [ + { + "name": "price", + "type": "uint256", + }, + { + "name": "role", + "type": "string", + }, + ], + "name": "baz", + "type": "tuple", + }, + ] + `) +}) diff --git a/src/core/internal/human-readable/parseAbi.ts b/src/core/internal/human-readable/parseAbi.ts new file mode 100644 index 00000000..78a76c87 --- /dev/null +++ b/src/core/internal/human-readable/parseAbi.ts @@ -0,0 +1,59 @@ +import type { Abi } from 'abitype' +import type { Error, Filter } from './types.js' +import { isStructSignature } from './runtime/signatures.js' +import { parseStructs } from './runtime/structs.js' +import { parseSignature } from './runtime/utils.js' +import type { Signatures } from './types/signatures.js' +import type { ParseStructs } from './types/structs.js' +import type { ParseSignature } from './types/utils.js' + +/** + * Parses human-readable ABI into JSON `Abi`. + * + * @param signatures - Human-readable ABI + * @returns Parsed `Abi`. + */ +export type ParseAbi = + string[] extends signatures + ? Abi // If `T` was not able to be inferred (e.g. just `string[]`), return `Abi` + : signatures extends readonly string[] + ? signatures extends Signatures // Validate signatures + ? ParseStructs extends infer structs + ? { + [key in keyof signatures]: signatures[key] extends string + ? ParseSignature + : never + } extends infer mapped extends readonly unknown[] + ? Filter extends infer result + ? result extends readonly [] + ? never + : result + : never + : never + : never + : never + : never + +/** + * Parses human-readable ABI into JSON `Abi`. + * + * @param signatures - Human-Readable ABI + * @returns Parsed `Abi`. + */ +export function parseAbi( + signatures: signatures['length'] extends 0 + ? Error<'At least one signature required'> + : Signatures extends signatures + ? signatures + : Signatures, +): ParseAbi { + const structs = parseStructs(signatures as readonly string[]) + const abi = [] + const length = signatures.length as number + for (let i = 0; i < length; i++) { + const signature = (signatures as readonly string[])[i]! + if (isStructSignature(signature)) continue + abi.push(parseSignature(signature, structs)) + } + return abi as unknown as ParseAbi +} diff --git a/src/core/internal/human-readable/parseAbiItem.test-d.ts b/src/core/internal/human-readable/parseAbiItem.test-d.ts new file mode 100644 index 00000000..0e372a17 --- /dev/null +++ b/src/core/internal/human-readable/parseAbiItem.test-d.ts @@ -0,0 +1,183 @@ +import { expectTypeOf, test } from 'vp/test' + +import type { Abi } from 'abitype' +import type { ParseAbiItem } from './parseAbiItem.js' +import { parseAbiItem } from './parseAbiItem.js' + +test('ParseAbiItem', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf< + ParseAbiItem<['struct Foo { string name; }']> + >().toEqualTypeOf() + + // string + expectTypeOf>().toEqualTypeOf<{ + readonly name: 'foo' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [] + readonly outputs: readonly [] + }>() + + // Array + expectTypeOf< + ParseAbiItem<['function bar(Foo, bytes32)', 'struct Foo { string name; }']> + >().toEqualTypeOf<{ + readonly name: 'bar' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'name' + readonly type: 'string' + }, + ] + }, + { + readonly type: 'bytes32' + }, + ] + readonly outputs: readonly [] + }>() + + expectTypeOf< + ParseAbiItem< + [ + 'event Transfer(address indexed from, address indexed to, uint256 amount)', + ] + > + >().toEqualTypeOf<{ + readonly name: 'Transfer' + readonly type: 'event' + readonly inputs: readonly [ + { + readonly name: 'from' + readonly type: 'address' + readonly indexed: true + }, + { + readonly name: 'to' + readonly type: 'address' + readonly indexed: true + }, + { + readonly name: 'amount' + readonly type: 'uint256' + }, + ] + }>() + + const abiItem = ['function bar(Foo, bytes32)', 'struct Foo { string name; }'] + expectTypeOf>().toEqualTypeOf() + + expectTypeOf>().toEqualTypeOf() +}) + +test('parseAbiItem', () => { + // @ts-expect-error empty array not allowed + expectTypeOf(parseAbiItem([])).toEqualTypeOf() + expectTypeOf( + parseAbiItem(['struct Foo { string name; }']), + ).toEqualTypeOf() + + // string + expectTypeOf(parseAbiItem('function foo()')).toEqualTypeOf<{ + readonly name: 'foo' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [] + readonly outputs: readonly [] + }>() + expectTypeOf(parseAbiItem('function foo((string), address)')).toEqualTypeOf<{ + readonly name: 'foo' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'string' + }, + ] + }, + { + readonly type: 'address' + }, + ] + readonly outputs: readonly [] + }>() + + expectTypeOf( + // @ts-expect-error invalid signature + parseAbiItem(''), + ).toEqualTypeOf() + + // Array + const res2 = parseAbiItem([ + 'function bar(Foo, bytes32)', + 'struct Foo { string name; }', + ]) + expectTypeOf().toEqualTypeOf<{ + readonly name: 'bar' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'name' + readonly type: 'string' + }, + ] + }, + { + readonly type: 'bytes32' + }, + ] + readonly outputs: readonly [] + }>() + + const abi2 = [ + 'function foo()', + 'function bar(Foo, bytes32)', + 'struct Foo { string name; }', + ] + expectTypeOf(parseAbiItem(abi2)).toEqualTypeOf() + + // @ts-expect-error invalid signature + expectTypeOf(parseAbiItem(['function foo ()'])).toEqualTypeOf() + + const signature: string = 'function foo()' + expectTypeOf(parseAbiItem(signature)).toEqualTypeOf() + + // fallback + expectTypeOf(parseAbiItem('fallback() external')).toEqualTypeOf<{ + readonly type: 'fallback' + readonly stateMutability: 'nonpayable' + }>() + expectTypeOf(parseAbiItem('fallback() external payable')).toEqualTypeOf<{ + readonly type: 'fallback' + readonly stateMutability: 'payable' + }>() + + // receive + expectTypeOf(parseAbiItem('receive() external payable')).toEqualTypeOf<{ + readonly type: 'receive' + readonly stateMutability: 'payable' + }>() +}) + +test('nested tuples', () => { + const formattedAbiItem = + 'function stepChanges((uint256 characterID, uint64 newPosition, uint24 xp, uint24 epoch, uint8 hp, (int32 x, int32 y, uint8 hp, uint8 kind)[5] monsters, (uint8 monsterIndexPlus1, uint8 attackCardsUsed1, uint8 attackCardsUsed2, uint8 defenseCardsUsed1, uint8 defenseCardsUsed2) battle) stateChanges, uint256 action, bool revetOnInvalidMoves) pure returns ((uint256 characterID, uint64 newPosition, uint24 xp, uint24 epoch, uint8 hp, (int32 x, int32 y, uint8 hp, uint8 kind)[5] monsters, (uint8 monsterIndexPlus1, uint8 attackCardsUsed1, uint8 attackCardsUsed2, uint8 defenseCardsUsed1, uint8 defenseCardsUsed2) battle))' + + const abiItem = parseAbiItem(formattedAbiItem) + expectTypeOf(abiItem.stateMutability).toEqualTypeOf<'pure'>() + expectTypeOf(abiItem.inputs.length).toEqualTypeOf<3>() +}) diff --git a/src/core/internal/human-readable/parseAbiItem.test.ts b/src/core/internal/human-readable/parseAbiItem.test.ts new file mode 100644 index 00000000..9d323eca --- /dev/null +++ b/src/core/internal/human-readable/parseAbiItem.test.ts @@ -0,0 +1,264 @@ +import { expect, test } from 'vp/test' + +import { parseAbiItem } from './parseAbiItem.js' + +test('parseAbiItem', () => { + // @ts-expect-error invalid signature type + expect(() => parseAbiItem('')).toThrowErrorMatchingInlineSnapshot( + `[Abi.UnknownSignatureError: Unknown signature.]`, + ) + // @ts-expect-error invalid signature type + expect(() => parseAbiItem([])).toThrowErrorMatchingInlineSnapshot( + ` + [AbiItem.InvalidAbiItemError: Failed to parse ABI item. + + Details: parseAbiItem([]) + See: https://oxlib.sh/api/AbiItem/from] + `, + ) + expect(() => + parseAbiItem(['struct Foo { string name; }']), + ).toThrowErrorMatchingInlineSnapshot( + ` + [AbiItem.InvalidAbiItemError: Failed to parse ABI item. + + Details: parseAbiItem([ + "struct Foo { string name; }" + ]) + See: https://oxlib.sh/api/AbiItem/from] + `, + ) +}) + +test.each([ + { + signature: ['function foo(string)'], + expected: { + type: 'function', + name: 'foo', + inputs: [{ type: 'string' }], + outputs: [], + stateMutability: 'nonpayable', + }, + }, + { + signature: [ + 'event Foo(address indexed from, address indexed to, uint256 amount)', + ], + expected: { + type: 'event', + name: 'Foo', + inputs: [ + { type: 'address', name: 'from', indexed: true }, + { type: 'address', name: 'to', indexed: true }, + { type: 'uint256', name: 'amount' }, + ], + }, + }, + { + signature: ['fallback() external'], + expected: { + type: 'fallback', + stateMutability: 'nonpayable', + }, + }, + { + signature: ['fallback() external payable'], + expected: { + type: 'fallback', + stateMutability: 'payable', + }, + }, +])('parseAbiItem($signature)', ({ signature, expected }) => { + expect(parseAbiItem(signature)).toEqual(expected) +}) + +test.each([ + { + signature: ['struct Foo { string bar; }', 'function foo(Foo)'], + expected: { + type: 'function', + name: 'foo', + inputs: [ + { type: 'tuple', components: [{ name: 'bar', type: 'string' }] }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + }, +])('parseAbiItem($signature)', ({ signature, expected }) => { + expect(parseAbiItem(signature)).toEqual(expected) +}) + +test('nested tuples', () => { + const formattedAbiItem = + 'function stepChanges((uint256 characterID, uint64 newPosition, uint24 xp, uint24 epoch, uint8 hp, (int32 x, int32 y, uint8 hp, uint8 kind)[5] monsters, (uint8 monsterIndexPlus1, uint8 attackCardsUsed1, uint8 attackCardsUsed2, uint8 defenseCardsUsed1, uint8 defenseCardsUsed2) battle) stateChanges, uint256 action, bool revetOnInvalidMoves) pure returns ((uint256 characterID, uint64 newPosition, uint24 xp, uint24 epoch, uint8 hp, (int32 x, int32 y, uint8 hp, uint8 kind)[5] monsters, (uint8 monsterIndexPlus1, uint8 attackCardsUsed1, uint8 attackCardsUsed2, uint8 defenseCardsUsed1, uint8 defenseCardsUsed2) battle))' + expect(parseAbiItem(formattedAbiItem)).toMatchInlineSnapshot( + ` + { + "inputs": [ + { + "components": [ + { + "name": "characterID", + "type": "uint256", + }, + { + "name": "newPosition", + "type": "uint64", + }, + { + "name": "xp", + "type": "uint24", + }, + { + "name": "epoch", + "type": "uint24", + }, + { + "name": "hp", + "type": "uint8", + }, + { + "components": [ + { + "name": "x", + "type": "int32", + }, + { + "name": "y", + "type": "int32", + }, + { + "name": "hp", + "type": "uint8", + }, + { + "name": "kind", + "type": "uint8", + }, + ], + "name": "monsters", + "type": "tuple[5]", + }, + { + "components": [ + { + "name": "monsterIndexPlus1", + "type": "uint8", + }, + { + "name": "attackCardsUsed1", + "type": "uint8", + }, + { + "name": "attackCardsUsed2", + "type": "uint8", + }, + { + "name": "defenseCardsUsed1", + "type": "uint8", + }, + { + "name": "defenseCardsUsed2", + "type": "uint8", + }, + ], + "name": "battle", + "type": "tuple", + }, + ], + "name": "stateChanges", + "type": "tuple", + }, + { + "name": "action", + "type": "uint256", + }, + { + "name": "revetOnInvalidMoves", + "type": "bool", + }, + ], + "name": "stepChanges", + "outputs": [ + { + "components": [ + { + "name": "characterID", + "type": "uint256", + }, + { + "name": "newPosition", + "type": "uint64", + }, + { + "name": "xp", + "type": "uint24", + }, + { + "name": "epoch", + "type": "uint24", + }, + { + "name": "hp", + "type": "uint8", + }, + { + "components": [ + { + "name": "x", + "type": "int32", + }, + { + "name": "y", + "type": "int32", + }, + { + "name": "hp", + "type": "uint8", + }, + { + "name": "kind", + "type": "uint8", + }, + ], + "name": "monsters", + "type": "tuple[5]", + }, + { + "components": [ + { + "name": "monsterIndexPlus1", + "type": "uint8", + }, + { + "name": "attackCardsUsed1", + "type": "uint8", + }, + { + "name": "attackCardsUsed2", + "type": "uint8", + }, + { + "name": "defenseCardsUsed1", + "type": "uint8", + }, + { + "name": "defenseCardsUsed2", + "type": "uint8", + }, + ], + "name": "battle", + "type": "tuple", + }, + ], + "type": "tuple", + }, + ], + "stateMutability": "pure", + "type": "function", + } + `, + ) +}) diff --git a/src/core/internal/human-readable/parseAbiItem.ts b/src/core/internal/human-readable/parseAbiItem.ts new file mode 100644 index 00000000..cbc6c96a --- /dev/null +++ b/src/core/internal/human-readable/parseAbiItem.ts @@ -0,0 +1,91 @@ +import type { Abi } from 'abitype' +import type { Narrow } from 'abitype' +import type { Error, Filter } from './types.js' +import { InvalidAbiItemError } from './errors.js' +import { isStructSignature } from './runtime/signatures.js' +import { parseStructs } from './runtime/structs.js' +import { parseSignature } from './runtime/utils.js' +import type { Signature, Signatures } from './types/signatures.js' +import type { ParseStructs } from './types/structs.js' +import type { ParseSignature } from './types/utils.js' + +/** + * Parses human-readable ABI item (e.g. error, event, function) into `Abi` item. + * + * @param signature - Human-readable ABI item + * @returns Parsed `Abi` item. + */ +export type ParseAbiItem< + signature extends string | readonly string[] | readonly unknown[], +> = + | (signature extends string + ? string extends signature + ? Abi[number] + : signature extends Signature // Validate signature + ? ParseSignature + : never + : never) + | (signature extends readonly string[] + ? string[] extends signature + ? Abi[number] // Return generic Abi item since type was no inferrable + : signature extends Signatures // Validate signature + ? ParseStructs extends infer structs + ? { + [key in keyof signature]: ParseSignature< + signature[key] extends string ? signature[key] : never, + structs + > + } extends infer mapped extends readonly unknown[] + ? // Filter out `never` since those are structs + Filter[0] extends infer result + ? result extends undefined // convert `undefined` to `never` (e.g. `ParseAbiItem<['struct Foo { string name; }']>`) + ? never + : result + : never + : never + : never + : never + : never) + +/** + * Parses human-readable ABI item (e.g. error, event, function) into `Abi` item. + * + * @param signature - Human-readable ABI item + * @returns Parsed `Abi` item. + */ +export function parseAbiItem< + signature extends string | readonly string[] | readonly unknown[], +>( + signature: Narrow & + ( + | (signature extends string + ? string extends signature + ? unknown + : Signature + : never) + | (signature extends readonly string[] + ? signature extends readonly [] // empty array + ? Error<'At least one signature required.'> + : string[] extends signature + ? unknown + : Signatures + : never) + ), +): ParseAbiItem { + let abiItem: ParseAbiItem | undefined + if (typeof signature === 'string') + abiItem = parseSignature(signature) as ParseAbiItem + else { + const structs = parseStructs(signature as readonly string[]) + const length = signature.length as number + for (let i = 0; i < length; i++) { + const signature_ = (signature as readonly string[])[i]! + if (isStructSignature(signature_)) continue + abiItem = parseSignature(signature_, structs) as ParseAbiItem + break + } + } + + if (!abiItem) throw new InvalidAbiItemError({ signature }) + return abiItem as ParseAbiItem +} diff --git a/src/core/internal/human-readable/parseAbiParameter.test-d.ts b/src/core/internal/human-readable/parseAbiParameter.test-d.ts new file mode 100644 index 00000000..3d65a803 --- /dev/null +++ b/src/core/internal/human-readable/parseAbiParameter.test-d.ts @@ -0,0 +1,68 @@ +import { expectTypeOf, test } from 'vp/test' + +import type { AbiParameter } from 'abitype' +import type { ParseAbiParameter } from './parseAbiParameter.js' +import { parseAbiParameter } from './parseAbiParameter.js' + +test('ParseAbiParameter', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf< + ParseAbiParameter<['struct Foo { string name; }']> + >().toEqualTypeOf() + + // string + expectTypeOf>().toEqualTypeOf<{ + readonly type: 'address' + readonly name: 'from' + }>() + expectTypeOf>().toEqualTypeOf<{ + readonly type: 'address' + readonly name: 'from' + readonly indexed: true + }>() + expectTypeOf>().toEqualTypeOf<{ + readonly type: 'address' + readonly name: 'foo' + }>() + + // Array + expectTypeOf< + ParseAbiParameter<['Foo', 'struct Foo { string name; }']> + >().toEqualTypeOf<{ + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'name' + readonly type: 'string' + }, + ] + }>() + + expectTypeOf>().toEqualTypeOf<{ + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'string' + readonly name: 'bar' + }, + ] + readonly name: 'foo' + }>() +}) + +test('parseAbiParameter', () => { + // @ts-expect-error empty array not allowed + expectTypeOf(parseAbiParameter([])).toEqualTypeOf() + expectTypeOf( + parseAbiParameter(['struct Foo { string name; }']), + ).toEqualTypeOf() + + expectTypeOf(parseAbiParameter('(string)')).toEqualTypeOf<{ + readonly type: 'tuple' + readonly components: readonly [{ readonly type: 'string' }] + }>() + + const param: string = 'address' + expectTypeOf(parseAbiParameter(param)).toEqualTypeOf() +}) diff --git a/src/core/internal/human-readable/parseAbiParameter.test.ts b/src/core/internal/human-readable/parseAbiParameter.test.ts new file mode 100644 index 00000000..814dc3f4 --- /dev/null +++ b/src/core/internal/human-readable/parseAbiParameter.test.ts @@ -0,0 +1,207 @@ +import { assertType, expect, test } from 'vp/test' + +import { parseAbiParameter } from './parseAbiParameter.js' + +test('parseAbiParameter', () => { + // @ts-expect-error invalid signature type + expect(() => parseAbiParameter('')).toThrowErrorMatchingInlineSnapshot( + `[HumanReadableAbi.InvalidParameterError: Invalid ABI parameter.]`, + ) + // @ts-expect-error invalid signature type + expect(() => parseAbiParameter([])).toThrowErrorMatchingInlineSnapshot( + ` + [AbiParameter.InvalidAbiParameterError: Failed to parse ABI parameter. + + Details: parseAbiParameter([]) + See: https://oxlib.sh/api/AbiParameter/from] + `, + ) + expect(() => + parseAbiParameter(['struct Foo { string name; }']), + ).toThrowErrorMatchingInlineSnapshot( + ` + [AbiParameter.InvalidAbiParameterError: Failed to parse ABI parameter. + + Details: parseAbiParameter([ + "struct Foo { string name; }" + ]) + See: https://oxlib.sh/api/AbiParameter/from] + `, + ) + + expect(() => + parseAbiParameter(['struct Foo { string memory bar; }', 'Foo indexed foo']), + ).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.InvalidModifierError: Invalid ABI parameter. + + Modifier "memory" not allowed in "struct" type. + + Details: string memory bar] + `, + ) + + expect([parseAbiParameter('address from')]).toMatchInlineSnapshot(` + [ + { + "name": "from", + "type": "address", + }, + ] +`) +}) + +test.each([ + { signature: 'string', expected: { type: 'string' } }, + { signature: 'string foo', expected: { name: 'foo', type: 'string' } }, + { + signature: 'string indexed foo', + expected: { name: 'foo', type: 'string', indexed: true }, + }, + { + signature: 'string calldata foo', + expected: { name: 'foo', type: 'string' }, + }, + { + signature: '(string)', + expected: { type: 'tuple', components: [{ type: 'string' }] }, + }, + { + signature: '(string foo)', + expected: { type: 'tuple', components: [{ name: 'foo', type: 'string' }] }, + }, + { + signature: '(string bar) foo', + expected: { + type: 'tuple', + name: 'foo', + components: [{ name: 'bar', type: 'string' }], + }, + }, + { + signature: '(string bar, string baz) foo', + expected: { + type: 'tuple', + name: 'foo', + components: [ + { name: 'bar', type: 'string' }, + { name: 'baz', type: 'string' }, + ], + }, + }, + { signature: 'string[]', expected: { type: 'string[]' } }, +])('parseAbiParameter($signature)', ({ signature, expected }) => { + expect(parseAbiParameter(signature)).toEqual(expected) +}) + +test.each([ + { + signatures: ['struct Foo { string bar; }', 'Foo'], + expected: { type: 'tuple', components: [{ name: 'bar', type: 'string' }] }, + }, + { + signatures: ['struct Foo { string bar; }', 'Foo foo'], + expected: { + type: 'tuple', + name: 'foo', + components: [{ name: 'bar', type: 'string' }], + }, + }, + { + signatures: ['struct Foo { string bar; }', 'Foo indexed foo'], + expected: { + type: 'tuple', + name: 'foo', + indexed: true, + components: [{ name: 'bar', type: 'string' }], + }, + }, +])('parseAbiParameter($signatures)', ({ signatures, expected }) => { + expect(parseAbiParameter(signatures)).toEqual(expected) +}) + +test('nested tuple', () => { + const result = parseAbiParameter('((((string baz) bar)[1] foo) boo)') + expect(result).toMatchInlineSnapshot(` + { + "components": [ + { + "components": [ + { + "components": [ + { + "components": [ + { + "name": "baz", + "type": "string", + }, + ], + "name": "bar", + "type": "tuple", + }, + ], + "name": "foo", + "type": "tuple[1]", + }, + ], + "name": "boo", + "type": "tuple", + }, + ], + "type": "tuple", + } + `) + assertType<{ + type: 'tuple' + components: readonly [ + { + type: 'tuple' + components: readonly [ + { + type: 'tuple[1]' + components: readonly [ + { + type: 'tuple' + components: readonly [ + { + type: 'string' + name: 'baz' + }, + ] + name: 'bar' + }, + ] + name: 'foo' + }, + ] + name: 'boo' + }, + ] + }>(result) +}) + +test('struct name collision', () => { + const result1 = parseAbiParameter(['struct Foo { string bar; }', 'Foo']) + expect(result1).toEqual({ + type: 'tuple', + components: [{ name: 'bar', type: 'string' }], + }) + + const result2 = parseAbiParameter(['struct Foo { address bar; }', 'Foo']) + expect(result2).toEqual({ + type: 'tuple', + components: [{ name: 'bar', type: 'address' }], + }) + + const result3 = parseAbiParameter([ + 'struct Foo { uint256 amount; address token; }', + 'Foo', + ]) + expect(result3).toEqual({ + type: 'tuple', + components: [ + { name: 'amount', type: 'uint256' }, + { name: 'token', type: 'address' }, + ], + }) +}) diff --git a/src/core/internal/human-readable/parseAbiParameter.ts b/src/core/internal/human-readable/parseAbiParameter.ts new file mode 100644 index 00000000..8832aa69 --- /dev/null +++ b/src/core/internal/human-readable/parseAbiParameter.ts @@ -0,0 +1,95 @@ +import type { AbiParameter } from 'abitype' +import type { Narrow } from 'abitype' +import type { Error, Filter } from './types.js' +import { InvalidAbiParameterError } from './errors.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 { ParseStructs } from './types/structs.js' +import type { ParseAbiParameter as ParseAbiParameter_ } from './types/utils.js' + +/** + * Parses human-readable ABI parameter into `AbiParameter`. + * + * @param param - Human-readable ABI parameter + * @returns Parsed `AbiParameter`. + */ +export type ParseAbiParameter< + param extends string | readonly string[] | readonly unknown[], +> = + | (param extends string + ? param extends '' + ? never + : string extends param + ? AbiParameter + : ParseAbiParameter_ + : never) + | (param extends readonly string[] + ? string[] extends param + ? AbiParameter // Return generic AbiParameter item since type was no inferrable + : ParseStructs extends infer structs + ? { + [key in keyof param]: param[key] extends string + ? IsStructSignature extends true + ? never + : ParseAbiParameter_< + param[key], + { modifier: Modifier; structs: structs } + > + : never + } extends infer mapped extends readonly unknown[] + ? Filter[0] extends infer result + ? result extends undefined + ? never + : result + : never + : never + : never + : never) + +/** + * Parses human-readable ABI parameter into `AbiParameter`. + * + * @param param - Human-readable ABI parameter + * @returns Parsed `AbiParameter`. + */ +export function parseAbiParameter< + param extends string | readonly string[] | readonly unknown[], +>( + param: Narrow & + ( + | (param extends string + ? param extends '' + ? Error<'Empty string is not allowed.'> + : unknown + : never) + | (param extends readonly string[] + ? param extends readonly [] // empty array + ? Error<'At least one parameter required.'> + : string[] extends param + ? unknown + : unknown // TODO: Validate param string + : never) + ), +): ParseAbiParameter { + let abiParameter: AbiParameter | undefined + if (typeof param === 'string') + abiParameter = parseAbiParameter_(param, { + modifiers, + }) as ParseAbiParameter + else { + const structs = parseStructs(param as readonly string[]) + const length = param.length as number + for (let i = 0; i < length; i++) { + const signature = (param as readonly string[])[i]! + if (isStructSignature(signature)) continue + abiParameter = parseAbiParameter_(signature, { modifiers, structs }) + break + } + } + + if (!abiParameter) throw new InvalidAbiParameterError({ param }) + + return abiParameter as ParseAbiParameter +} diff --git a/src/core/internal/human-readable/parseAbiParameters.test-d.ts b/src/core/internal/human-readable/parseAbiParameters.test-d.ts new file mode 100644 index 00000000..ddcc56d0 --- /dev/null +++ b/src/core/internal/human-readable/parseAbiParameters.test-d.ts @@ -0,0 +1,160 @@ +import { expectTypeOf, test } from 'vp/test' + +import type { AbiParameter } from 'abitype' +import type { ParseAbiParameters } from './parseAbiParameters.js' +import { parseAbiParameters } from './parseAbiParameters.js' + +test('ParseAbiParameters', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf< + ParseAbiParameters<['struct Foo { string name; }']> + >().toEqualTypeOf() + + // string + expectTypeOf< + ParseAbiParameters<'address from, address to, uint256 amount'> + >().toEqualTypeOf< + readonly [ + { + readonly type: 'address' + readonly name: 'from' + }, + { + readonly type: 'address' + readonly name: 'to' + }, + { + readonly type: 'uint256' + readonly name: 'amount' + }, + ] + >() + expectTypeOf< + ParseAbiParameters<'address indexed from, address indexed to, uint256 indexed amount'> + >().toEqualTypeOf< + readonly [ + { + readonly type: 'address' + readonly name: 'from' + readonly indexed: true + }, + { + readonly type: 'address' + readonly name: 'to' + readonly indexed: true + }, + { + readonly type: 'uint256' + readonly name: 'amount' + readonly indexed: true + }, + ] + >() + expectTypeOf< + ParseAbiParameters<'address calldata foo, address memory bar, uint256 storage baz'> + >().toEqualTypeOf< + readonly [ + { + readonly type: 'address' + readonly name: 'foo' + }, + { + readonly type: 'address' + readonly name: 'bar' + }, + { + readonly type: 'uint256' + readonly name: 'baz' + }, + ] + >() + + // Array + expectTypeOf< + ParseAbiParameters<['Foo, bytes32', 'struct Foo { string name; }']> + >().toEqualTypeOf< + readonly [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'name' + readonly type: 'string' + }, + ] + }, + { readonly type: 'bytes32' }, + ] + >() +}) + +test('parseAbiParameters', () => { + // @ts-expect-error empty array not allowed + expectTypeOf(parseAbiParameters([])).toEqualTypeOf() + expectTypeOf( + parseAbiParameters(['struct Foo { string name; }']), + ).toEqualTypeOf() + + expectTypeOf(parseAbiParameters('(string)')).toEqualTypeOf< + readonly [ + { + readonly type: 'tuple' + readonly components: readonly [{ readonly type: 'string' }] + }, + ] + >() + + const param: string = 'address, string' + expectTypeOf(parseAbiParameters(param)).toEqualTypeOf< + readonly AbiParameter[] + >() + + expectTypeOf(parseAbiParameters(['(uint256 a),(uint256 b)'])).toEqualTypeOf< + readonly [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'uint256' + readonly name: 'a' + }, + ] + }, + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'uint256' + readonly name: 'b' + }, + ] + }, + ] + >() + + expectTypeOf( + parseAbiParameters(['(uint256 a)', '(uint256 b)']), + ).toEqualTypeOf< + readonly [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'uint256' + readonly name: 'a' + }, + ] + }, + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'uint256' + readonly name: 'b' + }, + ] + }, + ] + >() +}) diff --git a/src/core/internal/human-readable/parseAbiParameters.test.ts b/src/core/internal/human-readable/parseAbiParameters.test.ts new file mode 100644 index 00000000..6018bf40 --- /dev/null +++ b/src/core/internal/human-readable/parseAbiParameters.test.ts @@ -0,0 +1,80 @@ +import { expect, test } from 'vp/test' + +import { parseAbiParameters } from './parseAbiParameters.js' + +test('parseAbiParameters', () => { + // @ts-expect-error invalid signature type + expect(() => parseAbiParameters('')).toThrowErrorMatchingInlineSnapshot( + ` + [AbiParameters.InvalidAbiParametersError: Failed to parse ABI parameters. + + Details: parseAbiParameters("") + See: https://oxlib.sh/api/AbiParameters/from] + `, + ) + // @ts-expect-error invalid signature type + expect(() => parseAbiParameters([])).toThrowErrorMatchingInlineSnapshot( + ` + [AbiParameters.InvalidAbiParametersError: Failed to parse ABI parameters. + + Details: parseAbiParameters([]) + See: https://oxlib.sh/api/AbiParameters/from] + `, + ) + expect(() => + parseAbiParameters(['struct Foo { string name; }']), + ).toThrowErrorMatchingInlineSnapshot( + ` + [AbiParameters.InvalidAbiParametersError: Failed to parse ABI parameters. + + Details: parseAbiParameters([ + "struct Foo { string name; }" + ]) + See: https://oxlib.sh/api/AbiParameters/from] + `, + ) + + expect(parseAbiParameters('address from')).toMatchInlineSnapshot(` + [ + { + "name": "from", + "type": "address", + }, + ] + `) +}) + +test.each([ + { + signatures: 'string, string', + expected: [{ type: 'string' }, { type: 'string' }], + }, + { + signatures: 'string foo, string bar', + expected: [ + { type: 'string', name: 'foo' }, + { type: 'string', name: 'bar' }, + ], + }, +])('parseAbiParameters($signatures)', ({ signatures, expected }) => { + expect(parseAbiParameters(signatures)).toEqual(expected) +}) + +test.each([ + { + signatures: ['struct Foo { string bar; }', 'Foo, string'], + expected: [ + { type: 'tuple', components: [{ name: 'bar', type: 'string' }] }, + { type: 'string' }, + ], + }, + { + signatures: ['string foo, string bar'], + expected: [ + { name: 'foo', type: 'string' }, + { name: 'bar', type: 'string' }, + ], + }, +])('parseAbiParameters($signatures)', ({ signatures, expected }) => { + expect(parseAbiParameters(signatures)).toEqual(expected) +}) diff --git a/src/core/internal/human-readable/parseAbiParameters.ts b/src/core/internal/human-readable/parseAbiParameters.ts new file mode 100644 index 00000000..6e7b3286 --- /dev/null +++ b/src/core/internal/human-readable/parseAbiParameters.ts @@ -0,0 +1,123 @@ +import type { AbiParameter } from 'abitype' +import type { Narrow } from 'abitype' +import type { Error, Filter } from './types.js' +import { InvalidAbiParametersError } from './errors.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 { ParseStructs } from './types/structs.js' +import type { SplitParameters } from './types/utils.js' +import type { ParseAbiParameters as ParseAbiParameters_ } from './types/utils.js' + +/** + * Parses human-readable ABI parameters into `AbiParameter`s. + * + * @param params - Human-readable ABI parameters + * @returns Parsed `AbiParameter`s. + */ +export type ParseAbiParameters< + params extends string | readonly string[] | readonly unknown[], +> = + | (params extends string + ? params extends '' + ? never + : string extends params + ? readonly AbiParameter[] + : ParseAbiParameters_, { modifier: Modifier }> + : never) + | (params extends readonly string[] + ? string[] extends params + ? AbiParameter // Return generic AbiParameter item since type was no inferrable + : ParseStructs extends infer structs + ? { + [key in keyof params]: params[key] extends string + ? IsStructSignature extends true + ? never + : ParseAbiParameters_< + SplitParameters, + { modifier: Modifier; structs: structs } + > + : never + } extends infer mapped extends readonly unknown[] + ? Filter extends readonly [...infer content] + ? content['length'] extends 0 + ? never + : DeepFlatten + : never + : never + : never + : never) + +/** + * Flatten all members of `T`. + * + * @param T - List of items to flatten + * @param Acc - The accumulator used while recursing + * @returns The flattened array + */ +type DeepFlatten< + T extends readonly unknown[], + Acc extends readonly unknown[] = readonly [], +> = T extends readonly [infer head, ...infer tail] + ? tail extends undefined + ? never + : head extends readonly unknown[] + ? DeepFlatten]> + : DeepFlatten + : Acc + +/** + * Parses human-readable ABI parameters into `AbiParameter`s. + * + * @param params - Human-readable ABI parameters + * @returns Parsed `AbiParameter`s. + */ +export function parseAbiParameters< + params extends string | readonly string[] | readonly unknown[], +>( + params: Narrow & + ( + | (params extends string + ? params extends '' + ? Error<'Empty string is not allowed.'> + : unknown + : never) + | (params extends readonly string[] + ? params extends readonly [] // empty array + ? Error<'At least one parameter required.'> + : string[] extends params + ? unknown + : unknown // TODO: Validate param string + : never) + ), +): ParseAbiParameters { + const abiParameters: AbiParameter[] = [] + if (typeof params === 'string') { + const parameters = splitParameters(params) + const length = parameters.length + for (let i = 0; i < length; i++) { + abiParameters.push(parseAbiParameter_(parameters[i]!, { modifiers })) + } + } else { + const structs = parseStructs(params as readonly string[]) + const length = params.length as number + for (let i = 0; i < length; i++) { + const signature = (params as readonly string[])[i]! + if (isStructSignature(signature)) continue + const parameters = splitParameters(signature) + const length = parameters.length + for (let k = 0; k < length; k++) { + abiParameters.push( + parseAbiParameter_(parameters[k]!, { modifiers, structs }), + ) + } + } + } + + if (abiParameters.length === 0) + throw new InvalidAbiParametersError({ params }) + + return abiParameters as ParseAbiParameters +} diff --git a/src/core/internal/human-readable/regex.ts b/src/core/internal/human-readable/regex.ts new file mode 100644 index 00000000..7b10d817 --- /dev/null +++ b/src/core/internal/human-readable/regex.ts @@ -0,0 +1,15 @@ +/** @internal */ +export function execTyped(regex: RegExp, string: string) { + const match = regex.exec(string) + return match?.groups as type | undefined +} + +/** @internal */ +export const bytesRegex = /^bytes([1-9]|1[0-9]|2[0-9]|3[0-2])?$/ + +/** @internal */ +export const integerRegex = + /^u?int(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)?$/ + +/** @internal */ +export const isTupleRegex = /^\(.+?\).*?$/ diff --git a/src/core/internal/human-readable/runtime/cache.ts b/src/core/internal/human-readable/runtime/cache.ts new file mode 100644 index 00000000..2a065473 --- /dev/null +++ b/src/core/internal/human-readable/runtime/cache.ts @@ -0,0 +1,96 @@ +import type { AbiItemType, AbiParameter } from 'abitype' +import type { StructLookup } from '../types/structs.js' + +/** + * Gets the parameter cache key namespaced by `type` and `structs`. This prevents + * parameters from being accessible to types that don't allow them (e.g. + * `string indexed foo` not allowed outside of `type: 'event'`) and ensures + * different struct definitions with the same name are cached separately. + * @param param ABI parameter string + * @param type ABI parameter type + * @param structs Struct definitions to include in cache key + * @returns Cache key for `parameterCache`. + */ +export function getParameterCacheKey( + param: string, + type?: AbiItemType | 'struct', + structs?: StructLookup, +) { + let structKey = '' + if (structs) + for (const struct of Object.entries(structs)) { + if (!struct) continue + let propertyKey = '' + for (const property of struct[1]) { + propertyKey += `[${property.type}${property.name ? `:${property.name}` : ''}]` + } + structKey += `(${struct[0]}{${propertyKey}})` + } + if (type) return `${type}:${param}${structKey}` + return `${param}${structKey}` +} + +/** + * Basic cache seeded with common ABI parameter strings. + * + * **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 } +>([ + // Unnamed + ['address', { type: 'address' }], + ['bool', { type: 'bool' }], + ['bytes', { type: 'bytes' }], + ['bytes32', { type: 'bytes32' }], + ['int', { type: 'int256' }], + ['int256', { type: 'int256' }], + ['string', { type: 'string' }], + ['uint', { type: 'uint256' }], + ['uint8', { type: 'uint8' }], + ['uint16', { type: 'uint16' }], + ['uint24', { type: 'uint24' }], + ['uint32', { type: 'uint32' }], + ['uint64', { type: 'uint64' }], + ['uint96', { type: 'uint96' }], + ['uint112', { type: 'uint112' }], + ['uint160', { type: 'uint160' }], + ['uint192', { type: 'uint192' }], + ['uint256', { type: 'uint256' }], + + // Named + ['address owner', { type: 'address', name: 'owner' }], + ['address to', { type: 'address', name: 'to' }], + ['bool approved', { type: 'bool', name: 'approved' }], + ['bytes _data', { type: 'bytes', name: '_data' }], + ['bytes data', { type: 'bytes', name: 'data' }], + ['bytes signature', { type: 'bytes', name: 'signature' }], + ['bytes32 hash', { type: 'bytes32', name: 'hash' }], + ['bytes32 r', { type: 'bytes32', name: 'r' }], + ['bytes32 root', { type: 'bytes32', name: 'root' }], + ['bytes32 s', { type: 'bytes32', name: 's' }], + ['string name', { type: 'string', name: 'name' }], + ['string symbol', { type: 'string', name: 'symbol' }], + ['string tokenURI', { type: 'string', name: 'tokenURI' }], + ['uint tokenId', { type: 'uint256', name: 'tokenId' }], + ['uint8 v', { type: 'uint8', name: 'v' }], + ['uint256 balance', { type: 'uint256', name: 'balance' }], + ['uint256 tokenId', { type: 'uint256', name: 'tokenId' }], + ['uint256 value', { type: 'uint256', name: 'value' }], + + // Indexed + [ + 'event:address indexed from', + { type: 'address', name: 'from', indexed: true }, + ], + ['event:address indexed to', { type: 'address', name: 'to', indexed: true }], + [ + 'event:uint indexed tokenId', + { type: 'uint256', name: 'tokenId', indexed: true }, + ], + [ + 'event:uint256 indexed tokenId', + { type: 'uint256', name: 'tokenId', indexed: true }, + ], +]) diff --git a/src/core/internal/human-readable/runtime/signatures.test.ts b/src/core/internal/human-readable/runtime/signatures.test.ts new file mode 100644 index 00000000..79a1a5ff --- /dev/null +++ b/src/core/internal/human-readable/runtime/signatures.test.ts @@ -0,0 +1,239 @@ +import { expect, test } from 'vp/test' + +import { + execConstructorSignature, + execErrorSignature, + execEventSignature, + execFunctionSignature, + execStructSignature, + isConstructorSignature, + isErrorSignature, + isEventSignature, + isFallbackSignature, + isFunctionSignature, + isReceiveSignature, + isStructSignature, +} from './signatures.js' + +test('isErrorSignature', () => { + expect(isErrorSignature('error Name(string)')).toMatchInlineSnapshot('true') + expect(isErrorSignature('error $(string)')).toMatchInlineSnapshot('true') + expect(isErrorSignature('error $_a9(string)')).toMatchInlineSnapshot('true') + expect(isErrorSignature('error _(string)')).toMatchInlineSnapshot('true') + expect(isErrorSignature('error abc$_9(string)')).toMatchInlineSnapshot('true') + expect(isErrorSignature('function name(string)')).toMatchInlineSnapshot( + 'false', + ) + expect(isErrorSignature('error 9abc(string)')).toMatchInlineSnapshot('false') +}) + +test('execErrorSignature', () => { + expect(execErrorSignature('error Name(string)')).toMatchInlineSnapshot(` + { + "name": "Name", + "parameters": "string", + } + `) + expect(execErrorSignature('function name(string)')).toMatchInlineSnapshot( + 'undefined', + ) +}) + +test('isEventSignature', () => { + expect(isEventSignature('event Name(string)')).toMatchInlineSnapshot('true') + expect(isEventSignature('event $(string)')).toMatchInlineSnapshot('true') + expect(isEventSignature('event $_a9(string)')).toMatchInlineSnapshot('true') + expect(isEventSignature('event _(string)')).toMatchInlineSnapshot('true') + expect(isEventSignature('event abc$_9(string)')).toMatchInlineSnapshot('true') + expect(isEventSignature('function name(string)')).toMatchInlineSnapshot( + 'false', + ) + expect(isEventSignature('event 9abc(string)')).toMatchInlineSnapshot('false') +}) + +test('execEventSignature', () => { + expect(execEventSignature('event Name(string)')).toMatchInlineSnapshot(` + { + "name": "Name", + "parameters": "string", + } + `) + expect( + execEventSignature('event Name(string indexed foo)'), + ).toMatchInlineSnapshot(` + { + "name": "Name", + "parameters": "string indexed foo", + } + `) + expect(execEventSignature('function name(string)')).toMatchInlineSnapshot( + 'undefined', + ) +}) + +test('isFunctionSignature', () => { + expect(isFunctionSignature('function name(string)')).toMatchInlineSnapshot( + 'true', + ) + expect( + isFunctionSignature('function name(string) returns (uint256)'), + ).toMatchInlineSnapshot('true') + expect( + isFunctionSignature('function name(string) returns(uint256)'), + ).toMatchInlineSnapshot('true') + expect(isFunctionSignature('function $(string)')).toMatchInlineSnapshot( + 'true', + ) + expect(isFunctionSignature('function $_a9(string)')).toMatchInlineSnapshot( + 'true', + ) + expect(isFunctionSignature('function _(string)')).toMatchInlineSnapshot( + 'true', + ) + expect(isFunctionSignature('function abc$_9(string)')).toMatchInlineSnapshot( + 'true', + ) + expect(isFunctionSignature('struct Name { string; }')).toMatchInlineSnapshot( + 'false', + ) + expect(isFunctionSignature('function 9abc(string)')).toMatchInlineSnapshot( + 'false', + ) +}) + +test('execFunctionSignature', () => { + expect(execFunctionSignature('function name(string)')).toMatchInlineSnapshot(` + { + "name": "name", + "parameters": "string", + "returns": undefined, + "scope": undefined, + "stateMutability": undefined, + } + `) + expect( + execFunctionSignature('function foo() view returns (uint256)'), + ).toMatchInlineSnapshot(` + { + "name": "foo", + "parameters": "", + "returns": "uint256", + "scope": undefined, + "stateMutability": "view", + } + `) + expect( + execFunctionSignature('function foo() view returns(uint256)'), + ).toMatchInlineSnapshot(` + { + "name": "foo", + "parameters": "", + "returns": "uint256", + "scope": undefined, + "stateMutability": "view", + } + `) + expect( + execFunctionSignature('struct Name { string; }'), + ).toMatchInlineSnapshot('undefined') +}) + +test('isStructSignature', () => { + expect(isStructSignature('struct Name { string; }')).toMatchInlineSnapshot( + 'true', + ) + expect(isStructSignature('struct $ { string; }')).toMatchInlineSnapshot( + 'true', + ) + expect(isStructSignature('struct $_a9 { string; }')).toMatchInlineSnapshot( + 'true', + ) + expect(isStructSignature('struct _ { string; }')).toMatchInlineSnapshot( + 'true', + ) + expect(isStructSignature('struct abc$_9 { string; }')).toMatchInlineSnapshot( + 'true', + ) + expect(isStructSignature('function name(string)')).toMatchInlineSnapshot( + 'false', + ) + expect(isStructSignature('struct 9abc { string; }')).toMatchInlineSnapshot( + 'false', + ) +}) + +test('execStructSignature', () => { + expect(execStructSignature('struct Name { string; }')).toMatchInlineSnapshot(` + { + "name": "Name", + "properties": " string; ", + } + `) + expect(execStructSignature('function name(string)')).toMatchInlineSnapshot( + 'undefined', + ) +}) + +test('isConstructorSignature', () => { + expect(isConstructorSignature('constructor(string)')).toMatchInlineSnapshot( + 'true', + ) + expect(isConstructorSignature('function name(string)')).toMatchInlineSnapshot( + 'false', + ) +}) + +test('execConstructorSignature', () => { + expect(execConstructorSignature('constructor(string)')).toMatchInlineSnapshot( + ` + { + "parameters": "string", + "stateMutability": undefined, + } + `, + ) + expect( + execConstructorSignature('constructor(string) payable'), + ).toMatchInlineSnapshot( + ` + { + "parameters": "string", + "stateMutability": "payable", + } + `, + ) + expect( + execConstructorSignature('constructor(string) '), + ).toMatchInlineSnapshot('undefined') + expect( + execConstructorSignature('constructor(string) external'), + ).toMatchInlineSnapshot('undefined') + expect( + execConstructorSignature('constructor(string)external'), + ).toMatchInlineSnapshot('undefined') + expect( + execConstructorSignature('function name(string)'), + ).toMatchInlineSnapshot('undefined') +}) + +test('isFallbackSignature', () => { + expect(isFallbackSignature('fallback() external')).toMatchInlineSnapshot( + 'true', + ) + expect( + isFallbackSignature('fallback() external payable'), + ).toMatchInlineSnapshot('true') + + expect(isFallbackSignature('function name(string)')).toMatchInlineSnapshot( + 'false', + ) +}) + +test('isReceiveSignature', () => { + expect( + isReceiveSignature('receive() external payable'), + ).toMatchInlineSnapshot('true') + expect(isReceiveSignature('function name(string)')).toMatchInlineSnapshot( + 'false', + ) +}) diff --git a/src/core/internal/human-readable/runtime/signatures.ts b/src/core/internal/human-readable/runtime/signatures.ts new file mode 100644 index 00000000..514b1f79 --- /dev/null +++ b/src/core/internal/human-readable/runtime/signatures.ts @@ -0,0 +1,106 @@ +import type { AbiStateMutability } from 'abitype' +import { execTyped } from '../regex.js' +import type { + EventModifier, + FunctionModifier, + Modifier, +} from '../types/signatures.js' + +// https://regexr.com/7gmok +const errorSignatureRegex = + /^error (?[a-zA-Z$_][a-zA-Z0-9$_]*)\((?.*?)\)$/ +export function isErrorSignature(signature: string) { + return errorSignatureRegex.test(signature) +} +export function execErrorSignature(signature: string) { + return execTyped<{ name: string; parameters: string }>( + errorSignatureRegex, + signature, + ) +} + +// https://regexr.com/7gmoq +const eventSignatureRegex = + /^event (?[a-zA-Z$_][a-zA-Z0-9$_]*)\((?.*?)\)$/ +export function isEventSignature(signature: string) { + return eventSignatureRegex.test(signature) +} +export function execEventSignature(signature: string) { + return execTyped<{ name: string; parameters: string }>( + eventSignatureRegex, + signature, + ) +} + +// https://regexr.com/7gmot +const functionSignatureRegex = + /^function (?[a-zA-Z$_][a-zA-Z0-9$_]*)\((?.*?)\)(?: (?external|public{1}))?(?: (?pure|view|nonpayable|payable{1}))?(?: returns\s?\((?.*?)\))?$/ +export function isFunctionSignature(signature: string) { + return functionSignatureRegex.test(signature) +} +export function execFunctionSignature(signature: string) { + return execTyped<{ + name: string + parameters: string + stateMutability?: AbiStateMutability + returns?: string + }>(functionSignatureRegex, signature) +} + +// https://regexr.com/7gmp3 +const structSignatureRegex = + /^struct (?[a-zA-Z$_][a-zA-Z0-9$_]*) \{(?.*?)\}$/ +export function isStructSignature(signature: string) { + return structSignatureRegex.test(signature) +} +export function execStructSignature(signature: string) { + return execTyped<{ name: string; properties: string }>( + structSignatureRegex, + signature, + ) +} + +// https://regexr.com/78u01 +const constructorSignatureRegex = + /^constructor\((?.*?)\)(?:\s(?payable{1}))?$/ +export function isConstructorSignature(signature: string) { + return constructorSignatureRegex.test(signature) +} +export function execConstructorSignature(signature: string) { + return execTyped<{ + parameters: string + stateMutability?: Extract + }>(constructorSignatureRegex, signature) +} + +// https://regexr.com/7srtn +const fallbackSignatureRegex = + /^fallback\(\) external(?:\s(?payable{1}))?$/ +export function isFallbackSignature(signature: string) { + return fallbackSignatureRegex.test(signature) +} +export function execFallbackSignature(signature: string) { + return execTyped<{ + parameters: string + stateMutability?: Extract + }>(fallbackSignatureRegex, signature) +} + +// https://regexr.com/78u1k +const receiveSignatureRegex = /^receive\(\) external payable$/ +export function isReceiveSignature(signature: string) { + return receiveSignatureRegex.test(signature) +} + +export const modifiers = new Set([ + 'memory', + 'indexed', + 'storage', + 'calldata', +]) +export const eventModifiers = new Set(['indexed']) +export const functionModifiers = new Set([ + 'calldata', + 'memory', + 'storage', +]) diff --git a/src/core/internal/human-readable/runtime/structs.test.ts b/src/core/internal/human-readable/runtime/structs.test.ts new file mode 100644 index 00000000..3f759dcc --- /dev/null +++ b/src/core/internal/human-readable/runtime/structs.test.ts @@ -0,0 +1,211 @@ +import { expect, test } from 'vp/test' + +import { parseStructs } from './structs.js' + +test('no structs', () => { + expect(parseStructs([])).toMatchInlineSnapshot('{}') + expect(parseStructs([''])).toMatchInlineSnapshot('{}') + expect(parseStructs(['function foo()', 'event Foo()'])).toMatchInlineSnapshot( + '{}', + ) + expect( + parseStructs(['function addPerson(Person person)']), + ).toMatchInlineSnapshot('{}') +}) + +test('parses basic structs', () => { + expect( + parseStructs([ + 'struct Foo { string bar; address baz; }', + 'struct FulfillmentComponent { uint256 orderIndex; uint256 itemIndex; }', + ]), + ).toMatchInlineSnapshot(` + { + "Foo": [ + { + "name": "bar", + "type": "string", + }, + { + "name": "baz", + "type": "address", + }, + ], + "FulfillmentComponent": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + } + `) +}) + +test('parses valid names', () => { + expect( + parseStructs([ + 'struct $ { string; }', + 'struct $_a9 { string; }', + 'struct _ { string; }', + 'struct abc$_9 { string; }', + ]), + ).toMatchInlineSnapshot(` + { + "$": [ + { + "type": "string", + }, + ], + "$_a9": [ + { + "type": "string", + }, + ], + "_": [ + { + "type": "string", + }, + ], + "abc$_9": [ + { + "type": "string", + }, + ], + } + `) +}) + +test('parses and resolves nested structs', () => { + expect( + parseStructs([ + 'struct Fulfillment { FulfillmentComponent[] offerComponents; FulfillmentComponent[] considerationComponents; }', + 'struct FulfillmentComponent { uint256 orderIndex; uint256 itemIndex; }', + 'struct Foo { Fulfillment fulfillment; }', + ]), + ).toMatchInlineSnapshot(` + { + "Foo": [ + { + "components": [ + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + "name": "offerComponents", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + "name": "considerationComponents", + "type": "tuple[]", + }, + ], + "name": "fulfillment", + "type": "tuple", + }, + ], + "Fulfillment": [ + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + "name": "offerComponents", + "type": "tuple[]", + }, + { + "components": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + "name": "considerationComponents", + "type": "tuple[]", + }, + ], + "FulfillmentComponent": [ + { + "name": "orderIndex", + "type": "uint256", + }, + { + "name": "itemIndex", + "type": "uint256", + }, + ], + } + `) +}) + +test('no properties', () => { + expect(() => + parseStructs(['struct Foo {}']), + ).toThrowErrorMatchingInlineSnapshot( + ` + [Abi.InvalidStructSignatureError: Invalid struct signature. + + No properties exist. + + Details: struct Foo {}] + `, + ) +}) + +test('struct does not exist when resolving', () => { + expect(() => + parseStructs(['struct Foo { Bar bar; }']), + ).toThrowErrorMatchingInlineSnapshot(` + [HumanReadableAbi.UnknownTypeError: Unknown type. + + Type "Bar" is not a valid ABI type. Perhaps you forgot to include a struct signature?] + `) +}) + +test('throws if recursive structs are detected', () => { + expect(() => + parseStructs(['struct Foo { Bar bar; }', 'struct Bar { Foo foo; }']), + ).toThrowErrorMatchingInlineSnapshot( + ` + [Abi.CircularReferenceError: Circular reference detected. + + Struct "Bar" is a circular reference.] + `, + ) +}) + +test.todo('throws if property is missing semicolon', () => { + expect(() => + parseStructs(['struct Foo { string bar; address baz }']), + ).toThrowErrorMatchingInlineSnapshot() +}) diff --git a/src/core/internal/human-readable/runtime/structs.ts b/src/core/internal/human-readable/runtime/structs.ts new file mode 100644 index 00000000..b7a67d28 --- /dev/null +++ b/src/core/internal/human-readable/runtime/structs.ts @@ -0,0 +1,97 @@ +import type { AbiParameter } from 'abitype' +import { execTyped, isTupleRegex } from '../regex.js' +import { UnknownTypeError } from '../errors.js' +import { InvalidAbiTypeParameterError } from '../errors.js' +import { + InvalidSignatureError, + InvalidStructSignatureError, +} from '../errors.js' +import { CircularReferenceError } from '../errors.js' +import type { StructLookup } from '../types/structs.js' +import { execStructSignature, isStructSignature } from './signatures.js' +import { isSolidityType, parseAbiParameter } from './utils.js' + +export function parseStructs(signatures: readonly string[]) { + // Create "shallow" version of each struct (and filter out non-structs or invalid structs) + const shallowStructs: StructLookup = {} + const signaturesLength = signatures.length + for (let i = 0; i < signaturesLength; i++) { + const signature = signatures[i]! + if (!isStructSignature(signature)) continue + + const match = execStructSignature(signature) + if (!match) throw new InvalidSignatureError({ signature, type: 'struct' }) + + const properties = match.properties.split(';') + + const components: AbiParameter[] = [] + const propertiesLength = properties.length + for (let k = 0; k < propertiesLength; k++) { + const property = properties[k]! + const trimmed = property.trim() + if (!trimmed) continue + const abiParameter = parseAbiParameter(trimmed, { + type: 'struct', + }) + components.push(abiParameter) + } + + if (!components.length) throw new InvalidStructSignatureError({ signature }) + shallowStructs[match.name] = components + } + + // Resolve nested structs inside each parameter + const resolvedStructs: StructLookup = {} + const entries = Object.entries(shallowStructs) + const entriesLength = entries.length + for (let i = 0; i < entriesLength; i++) { + const [name, parameters] = entries[i]! + resolvedStructs[name] = resolveStructs(parameters, shallowStructs) + } + + return resolvedStructs +} + +const typeWithoutTupleRegex = + /^(?[a-zA-Z$_][a-zA-Z0-9$_]*)(?(?:\[\d*?\])+?)?$/ + +function resolveStructs( + abiParameters: readonly (AbiParameter & { indexed?: true })[] = [], + structs: StructLookup = {}, + ancestors = new Set(), +) { + const components: AbiParameter[] = [] + const length = abiParameters.length + for (let i = 0; i < length; i++) { + const abiParameter = abiParameters[i]! + const isTuple = isTupleRegex.test(abiParameter.type) + if (isTuple) components.push(abiParameter) + else { + const match = execTyped<{ array?: string; type: string }>( + typeWithoutTupleRegex, + abiParameter.type, + ) + if (!match?.type) throw new InvalidAbiTypeParameterError({ abiParameter }) + + const { array, type } = match + if (type in structs) { + if (ancestors.has(type)) throw new CircularReferenceError({ type }) + + components.push({ + ...abiParameter, + type: `tuple${array ?? ''}`, + components: resolveStructs( + structs[type], + structs, + new Set([...ancestors, type]), + ), + }) + } else { + if (isSolidityType(type)) components.push(abiParameter) + else throw new UnknownTypeError({ type }) + } + } + } + + return components +} diff --git a/src/core/internal/human-readable/runtime/utils.test.ts b/src/core/internal/human-readable/runtime/utils.test.ts new file mode 100644 index 00000000..2d2e02b6 --- /dev/null +++ b/src/core/internal/human-readable/runtime/utils.test.ts @@ -0,0 +1,721 @@ +import { expect, test } from 'vp/test' + +import { functionModifiers } from './signatures.js' +import { + isSolidityKeyword, + isSolidityType, + isValidDataLocation, + parseAbiParameter, + parseConstructorSignature, + parseErrorSignature, + parseEventSignature, + parseFallbackSignature, + parseFunctionSignature, + parseSignature, + splitParameters, +} from './utils.js' + +const baseFunctionExpected = { + name: 'foo', + type: 'function', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', +} +const baseEventExpected = { + name: 'Foo', + type: 'event', + inputs: [], +} + +test.each([ + { + signature: 'function foo()', + expected: baseFunctionExpected, + }, + { + signature: 'function foo(string)', + expected: { + ...baseFunctionExpected, + inputs: [{ type: 'string' }], + }, + }, + { + signature: 'function foo(string) view', + expected: { + ...baseFunctionExpected, + inputs: [{ type: 'string' }], + stateMutability: 'view', + }, + }, + { + signature: 'function foo(string) public view', + expected: { + ...baseFunctionExpected, + inputs: [{ type: 'string' }], + stateMutability: 'view', + }, + }, + { + signature: 'function foo(address payable to) external', + expected: { + ...baseFunctionExpected, + inputs: [{ type: 'address', name: 'to' }], + stateMutability: 'nonpayable', + }, + }, + { + signature: 'function foo(string) public view returns (string)', + expected: { + ...baseFunctionExpected, + inputs: [{ type: 'string' }], + outputs: [{ type: 'string' }], + stateMutability: 'view', + }, + }, + { + signature: 'event Foo()', + expected: baseEventExpected, + }, + { + signature: 'event Foo(string indexed)', + expected: { + ...baseEventExpected, + inputs: [{ type: 'string', indexed: true }], + }, + }, + { + signature: 'event Foo(string indexed foo)', + expected: { + ...baseEventExpected, + inputs: [{ type: 'string', indexed: true, name: 'foo' }], + }, + }, + { + signature: 'error Foo(string foo)', + expected: { + name: 'Foo', + type: 'error', + inputs: [{ type: 'string', name: 'foo' }], + }, + }, + { + signature: 'receive() external payable', + expected: { + type: 'receive', + stateMutability: 'payable', + }, + }, + { + signature: 'fallback() external payable', + expected: { + type: 'fallback', + stateMutability: 'payable', + }, + }, + { + signature: 'fallback() external', + expected: { + type: 'fallback', + stateMutability: 'nonpayable', + }, + }, +])('parseSignature($signature)', ({ signature, expected }) => { + expect(parseSignature(signature)).toEqual(expected) +}) + +test('invalid signature', () => { + expect(() => parseSignature('')).toThrowErrorMatchingInlineSnapshot( + `[Abi.UnknownSignatureError: Unknown signature.]`, + ) + expect(() => + parseSignature('method foo(string) (address)'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [Abi.UnknownSignatureError: Unknown signature. + + Details: method foo(string) (address)] + `, + ) + + expect(() => + parseSignature('error Foo(string memory foo)'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.InvalidModifierError: Invalid ABI parameter. + + Modifier "memory" not allowed in "error" type. + + Details: string memory foo] + `, + ) + + expect(() => + parseSignature('event Foo(string memory foo)'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.InvalidModifierError: Invalid ABI parameter. + + Modifier "memory" not allowed in "event" type. + + Details: string memory foo] + `, + ) + + expect(() => + parseSignature('function 9abc()'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [Abi.UnknownSignatureError: Unknown signature. + + Details: function 9abc()] + `, + ) + + expect(() => + parseFunctionSignature('function foo() invalid'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [Abi.InvalidSignatureError: Invalid function signature. + + Details: function foo() invalid] + `, + ) + + expect(() => + parseEventSignature('event Foo(string memory foo) invalid'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [Abi.InvalidSignatureError: Invalid event signature. + + Details: event Foo(string memory foo) invalid] + `, + ) + + expect(() => { + parseErrorSignature('error Foo(string memory foo) invalid') + }).toThrowErrorMatchingInlineSnapshot( + ` + [Abi.InvalidSignatureError: Invalid error signature. + + Details: error Foo(string memory foo) invalid] + `, + ) + + expect(() => { + parseConstructorSignature('constructor() invalid') + }).toThrowErrorMatchingInlineSnapshot( + ` + [Abi.InvalidSignatureError: Invalid constructor signature. + + Details: constructor() invalid] + `, + ) + + expect(() => { + parseFallbackSignature('fallback() external invalid') + }).toThrowErrorMatchingInlineSnapshot( + ` + [Abi.InvalidSignatureError: Invalid fallback signature. + + Details: fallback() external invalid] + `, + ) +}) + +test('empty string', () => { + expect(() => parseAbiParameter('')).toThrowErrorMatchingInlineSnapshot( + `[HumanReadableAbi.InvalidParameterError: Invalid ABI parameter.]`, + ) + + expect(() => parseAbiParameter('foo ,')).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.InvalidParameterError: Invalid ABI parameter. + + Details: foo ,] + `, + ) +}) + +test('Invalid solidity type', () => { + expect(() => + parseAbiParameter('strings'), + ).toThrowErrorMatchingInlineSnapshot(` + [HumanReadableAbi.UnknownSolidityTypeError: Unknown type. + + Type "strings" is not a valid ABI type.] + `) +}) + +test('Invalid solidity type in tuple', () => { + expect(() => + parseAbiParameter('(strings)'), + ).toThrowErrorMatchingInlineSnapshot(` + [HumanReadableAbi.UnknownSolidityTypeError: Unknown type. + + Type "strings" is not a valid ABI type.] + `) +}) + +test('Invalid solidity type in nested tuple', () => { + expect(() => + parseAbiParameter('((strings))'), + ).toThrowErrorMatchingInlineSnapshot(` + [HumanReadableAbi.UnknownSolidityTypeError: Unknown type. + + Type "strings" is not a valid ABI type.] + `) +}) + +test('Struct type without context', () => { + expect(() => + parseAbiParameter('Demo demo'), + ).toThrowErrorMatchingInlineSnapshot(` + [HumanReadableAbi.UnknownSolidityTypeError: Unknown type. + + Type "Demo" is not a valid ABI type.] + `) +}) + +test('Struct type with context', () => { + expect(parseAbiParameter('Demo demo', { type: 'struct' })).toEqual({ + type: 'Demo', + name: 'demo', + }) +}) + +test('indexed not allowed', () => { + expect(() => + parseAbiParameter('string indexed foo'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.InvalidModifierError: Invalid ABI parameter. + + Modifier "indexed" not allowed. + + Details: string indexed foo] + `, + ) +}) + +test('modifier not allowed', () => { + expect(() => + parseAbiParameter('uint256 calldata foo'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.InvalidModifierError: Invalid ABI parameter. + + Modifier "calldata" not allowed. + + Details: uint256 calldata foo] + `, + ) +}) + +test('valid name', () => { + expect(parseAbiParameter('uint256 $')).toEqual({ + type: 'uint256', + name: '$', + }) + + expect(parseAbiParameter('uint256 $_a9')).toEqual({ + type: 'uint256', + name: '$_a9', + }) + + expect(parseAbiParameter('uint256 _')).toEqual({ + type: 'uint256', + name: '_', + }) + + expect(parseAbiParameter('uint256 abc$_9')).toEqual({ + type: 'uint256', + name: 'abc$_9', + }) +}) + +test('invalid name', () => { + expect(() => + parseAbiParameter('uint256 address'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.SolidityProtectedKeywordError: Invalid ABI parameter. + + "address" is a protected Solidity keyword. More info: https://docs.soliditylang.org/en/latest/cheatsheet.html + + Details: uint256 address] + `, + ) + + expect(() => + parseAbiParameter('uint256 9abc'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.InvalidParameterError: Invalid ABI parameter. + + Details: uint256 9abc] + `, + ) +}) + +test('invalid data location', () => { + expect(() => + parseAbiParameter('uint256 memory foo', { modifiers: functionModifiers }), + ).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.InvalidFunctionModifierError: Invalid ABI parameter. + + Modifier "memory" not allowed. + Data location can only be specified for array, struct, or mapping types, but "memory" was given. + + Details: uint256 memory foo] + `, + ) +}) +test('valid data location', () => { + expect( + parseAbiParameter('uint256[] memory foo', { modifiers: functionModifiers }), + ).toMatchInlineSnapshot(` + { + "name": "foo", + "type": "uint256[]", + } + `) + expect( + parseAbiParameter('string memory foo', { modifiers: functionModifiers }), + ).toMatchInlineSnapshot(` + { + "name": "foo", + "type": "string", + } + `) + expect( + parseAbiParameter('Foo memory foo', { + modifiers: functionModifiers, + structs: { Foo: [{ type: 'string' }] }, + }), + ).toMatchInlineSnapshot(` + { + "components": [ + { + "type": "string", + }, + ], + "name": "foo", + "type": "tuple", + } + `) +}) + +test.each(['address', 'bool', 'bytes32', 'int256', 'string', 'uint256'])( + 'parseAbiParameter($type)', + (type) => { + expect(parseAbiParameter(type)).toEqual({ type }) + }, +) + +test.each([ + 'address indexed', + 'bool indexed', + 'bytes32 indexed', + 'int256 indexed', + 'string indexed', + 'uint256 indexed', +])("parseAbiParameter($type, { modifiers: 'indexed' })", (type) => { + expect( + parseAbiParameter(type, { + modifiers: new Set(['indexed']), + }), + ).toEqual({ + type: type.replace(' indexed', ''), + indexed: true, + }) +}) + +test.each([ + 'address[] calldata', + 'bool[] calldata', + 'bytes32[] calldata', + 'int256[] calldata', + 'string calldata', + 'uint256[] calldata', + 'bytes calldata', +])( + "parseAbiParameter($type, { modifiers: ['calldata', 'memory'] })", + (type) => { + expect( + parseAbiParameter(type, { + modifiers: new Set(['calldata', 'memory']), + }), + ).toEqual({ + type: type.replace(/\scalldata|memory/, ''), + }) + }, +) + +test.each([ + 'address foo', + 'bool foo', + 'bytes32 foo', + 'int256 foo', + 'string foo', + 'uint256 foo', +])('parseAbiParameter($type)', (type) => { + expect(parseAbiParameter(type)).toEqual({ + name: 'foo', + type: type.replace(' foo', ''), + }) +}) + +test('dynamic integer', () => { + expect(parseAbiParameter('int')).toMatchInlineSnapshot(` + { + "type": "int256", + } + `) + expect(parseAbiParameter('uint')).toMatchInlineSnapshot(` + { + "type": "uint256", + } + `) +}) + +test('structs', () => { + expect( + parseAbiParameter('Foo foo', { structs: { Foo: [{ type: 'string' }] } }), + ).toMatchInlineSnapshot(` + { + "components": [ + { + "type": "string", + }, + ], + "name": "foo", + "type": "tuple", + } + `) + expect( + parseAbiParameter('Foo[] foo', { structs: { Foo: [{ type: 'string' }] } }), + ).toMatchInlineSnapshot(` + { + "components": [ + { + "type": "string", + }, + ], + "name": "foo", + "type": "tuple[]", + } + `) +}) + +test('inline tuples', () => { + expect(parseAbiParameter('(string) foo')).toMatchInlineSnapshot(` + { + "components": [ + { + "type": "string", + }, + ], + "name": "foo", + "type": "tuple", + } + `) + expect(parseAbiParameter('(string, string) foo')).toMatchInlineSnapshot(` + { + "components": [ + { + "type": "string", + }, + { + "type": "string", + }, + ], + "name": "foo", + "type": "tuple", + } +`) + expect( + parseAbiParameter('(Foo, address bar) foo', { + structs: { Foo: [{ type: 'string' }] }, + }), + ).toMatchInlineSnapshot(` + { + "components": [ + { + "components": [ + { + "type": "string", + }, + ], + "type": "tuple", + }, + { + "name": "bar", + "type": "address", + }, + ], + "name": "foo", + "type": "tuple", + } + `) +}) + +test.each([ + { params: '', expected: [] }, + { params: 'string', expected: ['string'] }, + { params: 'string indexed foo', expected: ['string indexed foo'] }, + { params: 'string, address', expected: ['string', 'address'] }, + { + params: 'string foo, address bar', + expected: ['string foo', 'address bar'], + }, + { + params: 'string indexed foo, address indexed bar', + expected: ['string indexed foo', 'address indexed bar'], + }, + { + params: + 'address owner, (bool loading, (string[][] names) cats)[] dog, uint tokenId', + expected: [ + 'address owner', + '(bool loading, (string[][] names) cats)[] dog', + 'uint tokenId', + ], + }, + { + params: ' ', + expected: [], + }, +])('splitParameters($params)', ({ params, expected }) => { + expect(splitParameters(params)).toEqual(expected) +}) + +test.each([ + 'address', + 'bool', + 'bytes32', + 'int256', + 'string', + 'uint256', + 'function', +])('isSolidityType($type)', (type) => { + expect(isSolidityType(type)).toEqual(true) +}) +test('isSolidityType', () => { + expect(isSolidityType('foo')).toEqual(false) +}) + +test.each([ + 'address', + 'bool', + 'bytes32', + 'int256', + 'string', + 'uint256', + 'function', + 'view', + 'override', + 'let', + 'var', + 'typeof', + 'promise', + 'in', + 'of', + 'reference', + 'implements', + 'mapping', + 'error', + 'event', + 'struct', + 'alias', + 'byte', + 'case', + 'copyof', + 'final', + 'external', + 'public', + 'internal', + 'pure', + 'match', + 'apply', + 'case', + 'null', + 'mutable', + 'inline', + 'static', + 'partial', + 'relocatable', + 'try', + 'catch', + 'switch', + 'supports', + 'mapping', + 'virtual', + 'return', + 'returns', + 'after', + 'auto', + 'default', + 'defined', + 'typedef', + 'typeof', +])('isInvalidSolidiyName($name)', (name) => { + expect(isSolidityKeyword(name)).toEqual(true) +}) + +test.each(['bytes', 'string', 'tuple'])( + 'isValidDataLocation($type)', + (type) => { + expect(isValidDataLocation(type as any, false)).toEqual(true) + }, +) + +test('Unbalanced Parethesis', () => { + expect(() => + splitParameters('address owner, ((string name)'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.InvalidParenthesisError: Unbalanced parentheses. + + "((string name)" has too many opening parentheses. + + Details: Depth "1"] + `, + ) + + expect(() => + splitParameters('address owner, (((string name)'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.InvalidParenthesisError: Unbalanced parentheses. + + "(((string name)" has too many opening parentheses. + + Details: Depth "2"] + `, + ) + expect(() => + splitParameters('address owner, (string name))'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.InvalidParenthesisError: Unbalanced parentheses. + + "(string name))" has too many closing parentheses. + + Details: Depth "-1"] + `, + ) + + expect(() => + splitParameters('address owner, (string name)))'), + ).toThrowErrorMatchingInlineSnapshot( + ` + [HumanReadableAbi.InvalidParenthesisError: Unbalanced parentheses. + + "(string name)))" has too many closing parentheses. + + Details: Depth "-2"] + `, + ) +}) diff --git a/src/core/internal/human-readable/runtime/utils.ts b/src/core/internal/human-readable/runtime/utils.ts new file mode 100644 index 00000000..7eea9d0b --- /dev/null +++ b/src/core/internal/human-readable/runtime/utils.ts @@ -0,0 +1,352 @@ +import type { + AbiItemType, + AbiType, + SolidityArray, + SolidityBytes, + SolidityString, + SolidityTuple, +} from 'abitype' +import { + bytesRegex, + execTyped, + integerRegex, + isTupleRegex, +} from '../regex.js' +import { UnknownSolidityTypeError } from '../errors.js' +import { + InvalidFunctionModifierError, + InvalidModifierError, + InvalidParameterError, + SolidityProtectedKeywordError, +} from '../errors.js' +import { + InvalidSignatureError, + UnknownSignatureError, +} from '../errors.js' +import { InvalidParenthesisError } from '../errors.js' +import type { FunctionModifier, Modifier } from '../types/signatures.js' +import type { StructLookup } from '../types/structs.js' +import { getParameterCacheKey, parameterCache } from './cache.js' +import { + eventModifiers, + execConstructorSignature, + execErrorSignature, + execEventSignature, + execFallbackSignature, + execFunctionSignature, + functionModifiers, + isConstructorSignature, + isErrorSignature, + isEventSignature, + isFallbackSignature, + isFunctionSignature, + isReceiveSignature, +} from './signatures.js' + +export function parseSignature(signature: string, structs: StructLookup = {}) { + if (isFunctionSignature(signature)) + return parseFunctionSignature(signature, structs) + + if (isEventSignature(signature)) + return parseEventSignature(signature, structs) + + if (isErrorSignature(signature)) + return parseErrorSignature(signature, structs) + + if (isConstructorSignature(signature)) + return parseConstructorSignature(signature, structs) + + if (isFallbackSignature(signature)) return parseFallbackSignature(signature) + + if (isReceiveSignature(signature)) + return { + type: 'receive', + stateMutability: 'payable', + } + + throw new UnknownSignatureError({ signature }) +} + +export function parseFunctionSignature( + signature: string, + structs: StructLookup = {}, +) { + const match = execFunctionSignature(signature) + if (!match) throw new InvalidSignatureError({ signature, type: 'function' }) + + const inputParams = splitParameters(match.parameters) + const inputs = [] + const inputLength = inputParams.length + for (let i = 0; i < inputLength; i++) { + inputs.push( + parseAbiParameter(inputParams[i]!, { + modifiers: functionModifiers, + structs, + type: 'function', + }), + ) + } + + const outputs = [] + if (match.returns) { + const outputParams = splitParameters(match.returns) + const outputLength = outputParams.length + for (let i = 0; i < outputLength; i++) { + outputs.push( + parseAbiParameter(outputParams[i]!, { + modifiers: functionModifiers, + structs, + type: 'function', + }), + ) + } + } + + return { + name: match.name, + type: 'function', + stateMutability: match.stateMutability ?? 'nonpayable', + inputs, + outputs, + } +} + +export function parseEventSignature( + signature: string, + structs: StructLookup = {}, +) { + const match = execEventSignature(signature) + if (!match) throw new InvalidSignatureError({ signature, type: 'event' }) + + const params = splitParameters(match.parameters) + const abiParameters = [] + const length = params.length + for (let i = 0; i < length; i++) + abiParameters.push( + parseAbiParameter(params[i]!, { + modifiers: eventModifiers, + structs, + type: 'event', + }), + ) + return { name: match.name, type: 'event', inputs: abiParameters } +} + +export function parseErrorSignature( + signature: string, + structs: StructLookup = {}, +) { + const match = execErrorSignature(signature) + if (!match) throw new InvalidSignatureError({ signature, type: 'error' }) + + const params = splitParameters(match.parameters) + const abiParameters = [] + const length = params.length + for (let i = 0; i < length; i++) + abiParameters.push( + parseAbiParameter(params[i]!, { structs, type: 'error' }), + ) + return { name: match.name, type: 'error', inputs: abiParameters } +} + +export function parseConstructorSignature( + signature: string, + structs: StructLookup = {}, +) { + const match = execConstructorSignature(signature) + if (!match) + throw new InvalidSignatureError({ signature, type: 'constructor' }) + + const params = splitParameters(match.parameters) + const abiParameters = [] + const length = params.length + for (let i = 0; i < length; i++) + abiParameters.push( + parseAbiParameter(params[i]!, { structs, type: 'constructor' }), + ) + return { + type: 'constructor', + stateMutability: match.stateMutability ?? 'nonpayable', + inputs: abiParameters, + } +} + +export function parseFallbackSignature(signature: string) { + const match = execFallbackSignature(signature) + if (!match) throw new InvalidSignatureError({ signature, type: 'fallback' }) + + return { + type: 'fallback', + stateMutability: match.stateMutability ?? 'nonpayable', + } +} + +const abiParameterWithoutTupleRegex = + /^(?[a-zA-Z$_][a-zA-Z0-9$_]*(?:\spayable)?)(?(?:\[\d*?\])+?)?(?:\s(?calldata|indexed|memory|storage{1}))?(?:\s(?[a-zA-Z$_][a-zA-Z0-9$_]*))?$/ +const abiParameterWithTupleRegex = + /^\((?.+?)\)(?(?:\[\d*?\])+?)?(?:\s(?calldata|indexed|memory|storage{1}))?(?:\s(?[a-zA-Z$_][a-zA-Z0-9$_]*))?$/ +const dynamicIntegerRegex = /^u?int$/ + +type ParseOptions = { + modifiers?: Set + structs?: StructLookup + type?: AbiItemType | 'struct' +} + +export function parseAbiParameter(param: string, options?: ParseOptions) { + // optional namespace cache by `type` + const parameterCacheKey = getParameterCacheKey( + param, + options?.type, + options?.structs, + ) + if (parameterCache.has(parameterCacheKey)) + return parameterCache.get(parameterCacheKey)! + + const isTuple = isTupleRegex.test(param) + const match = execTyped<{ + array?: string + modifier?: Modifier + name?: string + type: string + }>( + isTuple ? abiParameterWithTupleRegex : abiParameterWithoutTupleRegex, + param, + ) + if (!match) throw new InvalidParameterError({ param }) + + if (match.name && isSolidityKeyword(match.name)) + throw new SolidityProtectedKeywordError({ param, name: match.name }) + + const name = match.name ? { name: match.name } : {} + const indexed = match.modifier === 'indexed' ? { indexed: true } : {} + const structs = options?.structs ?? {} + let type: string + let components = {} + if (isTuple) { + type = 'tuple' + const params = splitParameters(match.type) + const components_ = [] + const length = params.length + for (let i = 0; i < length; i++) { + // remove `modifiers` from `options` to prevent from being added to tuple components + components_.push(parseAbiParameter(params[i]!, { structs })) + } + components = { components: components_ } + } else if (match.type in structs) { + type = 'tuple' + components = { components: structs[match.type] } + } else if (dynamicIntegerRegex.test(match.type)) { + type = `${match.type}256` + } else if (match.type === 'address payable') { + type = 'address' + } else { + type = match.type + if (!(options?.type === 'struct') && !isSolidityType(type)) + throw new UnknownSolidityTypeError({ type }) + } + + if (match.modifier) { + // Check if modifier exists, but is not allowed (e.g. `indexed` in `functionModifiers`) + if (!options?.modifiers?.has?.(match.modifier)) + throw new InvalidModifierError({ + param, + type: options?.type, + modifier: match.modifier, + }) + + // Check if resolved `type` is valid if there is a function modifier + if ( + functionModifiers.has(match.modifier as FunctionModifier) && + !isValidDataLocation(type, !!match.array) + ) + throw new InvalidFunctionModifierError({ + param, + type: options?.type, + modifier: match.modifier, + }) + } + + const abiParameter = { + type: `${type}${match.array ?? ''}`, + ...name, + ...indexed, + ...components, + } + parameterCache.set(parameterCacheKey, abiParameter) + return abiParameter +} + +// s/o latika for this +export function splitParameters( + params: string, + result: string[] = [], + current = '', + depth = 0, +): readonly string[] { + const length = params.trim().length + // biome-ignore lint/correctness/noUnreachable: recursive + for (let i = 0; i < length; i++) { + const char = params[i] + const tail = params.slice(i + 1) + switch (char) { + case ',': + return depth === 0 + ? splitParameters(tail, [...result, current.trim()]) + : splitParameters(tail, result, `${current}${char}`, depth) + case '(': + return splitParameters(tail, result, `${current}${char}`, depth + 1) + case ')': + return splitParameters(tail, result, `${current}${char}`, depth - 1) + default: + return splitParameters(tail, result, `${current}${char}`, depth) + } + } + + if (current === '') return result + if (depth !== 0) throw new InvalidParenthesisError({ current, depth }) + + result.push(current.trim()) + return result +} + +export function isSolidityType( + type: string, +): type is Exclude { + return ( + type === 'address' || + type === 'bool' || + type === 'function' || + type === 'string' || + bytesRegex.test(type) || + integerRegex.test(type) + ) +} + +const protectedKeywordsRegex = + /^(?:after|alias|anonymous|apply|auto|byte|calldata|case|catch|constant|copyof|default|defined|error|event|external|false|final|function|immutable|implements|in|indexed|inline|internal|let|mapping|match|memory|mutable|null|of|override|partial|private|promise|public|pure|reference|relocatable|return|returns|sizeof|static|storage|struct|super|supports|switch|this|true|try|typedef|typeof|var|view|virtual)$/ + +/** @internal */ +export function isSolidityKeyword(name: string) { + return ( + name === 'address' || + name === 'bool' || + name === 'function' || + name === 'string' || + name === 'tuple' || + bytesRegex.test(name) || + integerRegex.test(name) || + protectedKeywordsRegex.test(name) + ) +} + +/** @internal */ +export function isValidDataLocation( + type: string, + isArray: boolean, +): type is Exclude< + AbiType, + SolidityString | Extract | SolidityArray +> { + return isArray || type === 'bytes' || type === 'string' || type === 'tuple' +} diff --git a/src/core/internal/human-readable/types.ts b/src/core/internal/human-readable/types.ts new file mode 100644 index 00000000..872715b0 --- /dev/null +++ b/src/core/internal/human-readable/types.ts @@ -0,0 +1,63 @@ +/** @internal */ +export type Error = messages extends string + ? [`Error: ${messages}`] + : { + [key in keyof messages]: messages[key] extends infer message extends string + ? `Error: ${message}` + : never + } + +/** @internal */ +export type Filter< + items extends readonly unknown[], + item, + acc extends readonly unknown[] = [], +> = items extends readonly [infer head, ...infer tail extends readonly unknown[]] + ? [head] extends [item] + ? Filter + : Filter + : readonly [...acc] + +/** @internal */ +export type IsNarrowable = IsUnknown extends true + ? false + : IsNever< + (type extends type2 ? true : false) & + (type2 extends type ? false : true) + > extends true + ? false + : true + +/** @internal */ +export type IsNever = [type] extends [never] ? true : false + +/** @internal */ +export type IsUnknown = unknown extends type ? true : false + +/** @internal */ +export type Join< + array extends readonly unknown[], + separator extends string | number, +> = array extends readonly [infer head, ...infer tail] + ? tail['length'] extends 0 + ? `${head & string}` + : `${head & string}${separator}${Join}` + : never + +/** @internal */ +export type Merge = Omit & object2 + +/** @internal */ +export type Pretty = { [key in keyof type]: type[key] } & unknown + +/** @internal */ +export type Trim = TrimLeft< + TrimRight, + chars +> +type TrimLeft = t extends `${chars}${infer tail}` + ? TrimLeft + : t +type TrimRight = t extends `${infer head}${chars}` + ? TrimRight + : t diff --git a/src/core/internal/human-readable/types/signatures.test-d.ts b/src/core/internal/human-readable/types/signatures.test-d.ts new file mode 100644 index 00000000..e9477c09 --- /dev/null +++ b/src/core/internal/human-readable/types/signatures.test-d.ts @@ -0,0 +1,255 @@ +import { assertType, expectTypeOf, test } from 'vp/test' + +import type { + IsConstructorSignature, + IsErrorSignature, + IsEventSignature, + IsFallbackSignature, + IsFunctionSignature, + IsName, + IsSignature, + IsSolidityKeyword, + IsStructSignature, + IsValidCharacter, + Signature, + Signatures, + SolidityKeywords, + ValidateName, +} from './signatures.js' + +test('IsErrorSignature', () => { + // basic + assertType>(true) + + // params + assertType>(true) + assertType>(true) + assertType>(true) + + // invalid + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) +}) + +test('IsEventSignature', () => { + // basic + assertType>(true) + + // params + assertType>(true) + assertType>(true) + assertType>(true) + + // invalid + assertType>(false) + assertType>(false) + assertType>(false) +}) + +test('IsFunctionSignature', () => { + // basic + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + // combinations + assertType>(true) + assertType>(true) + assertType>(true) + assertType< + IsFunctionSignature<'function foo() public view returns (uint256)'> + >(true) + assertType< + IsFunctionSignature<'function foo() public view returns(uint256)'> + >(true) + // params + assertType>(true) + assertType>( + true, + ) + assertType>( + true, + ) + assertType>(true) + assertType>(true) + assertType< + IsFunctionSignature<'function foo(uint256) view returns (uint256)'> + >(true) + assertType< + IsFunctionSignature<'function foo(uint256) view returns(uint256)'> + >(true) + assertType>(true) + assertType< + IsFunctionSignature<'function foo(uint256) public view returns (uint256)'> + >(true) + assertType< + IsFunctionSignature<'function foo(uint256) public view returns(uint256)'> + >(true) + assertType< + IsFunctionSignature<'function foo(uint256) public view returns (uint256 tokenId)'> + >(true) + assertType< + IsFunctionSignature<'function foo(uint256) public view returns(uint256 tokenId)'> + >(true) + assertType< + IsFunctionSignature<'function foo(uint256) public view returns (uint256 tokenId, uint256 balance)'> + >(true) + assertType< + IsFunctionSignature<'function foo(uint256) public view returns(uint256 tokenId, uint256 balance)'> + >(true) + + // invalid + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) +}) + +test('IsStructSignature', () => { + // basic + assertType>(true) + + // properties + assertType>(true) + assertType>(true) + assertType>( + true, + ) + + // invalid + assertType>(false) + assertType>(false) + assertType>(false) +}) + +test('IsConstructorSignature', () => { + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + assertType>( + true, + ) + assertType>(true) + assertType>( + true, + ) + + assertType>(false) + assertType>(false) +}) + +test('IsFallbackSignature', () => { + assertType>(true) + assertType>(true) + + assertType>(false) + assertType>(false) + assertType>(false) +}) + +test('IsSignature', () => { + // basic + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + + // invalid + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) +}) + +test('Signature', () => { + assertType>('function foo()') + assertType>([ + 'Error: Signature "function foo ()" is invalid.', + ]) + // assertType>([ + // 'Error: Signature "function foo??()" is invalid.', + // ]) +}) + +test('Signatures', () => { + assertType>(['function foo()']) + assertType>([ + ['Error: Signature "function foo ()" is invalid at position 0.'], + ]) +}) + +test('IsName', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + // no whitespace + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + // no solidity keywords + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + // no number strings + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + // no invalid characters + // expectTypeOf>().toEqualTypeOf() + // expectTypeOf>().toEqualTypeOf() +}) + +test('ValidateName', () => { + expectTypeOf>().toEqualTypeOf<'foo'>() + expectTypeOf>().toEqualTypeOf<'foo$'>() + expectTypeOf>().toEqualTypeOf< + ['Error: Identifier "foo bar" cannot contain whitespace.'] + >() + expectTypeOf>().toEqualTypeOf< + ['Error: "alias" is a protected Solidity keyword.'] + >() + expectTypeOf>().toEqualTypeOf< + ['Error: Identifier "123" cannot be a number string.'] + >() + expectTypeOf>().toEqualTypeOf< + ['Error: Identifier "12foo" cannot start with a number.'] + >() + expectTypeOf>().toEqualTypeOf< + ['Error: "foo?" contains invalid character.'] + >() +}) + +test('IsSolidityKeyword', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() +}) + +test('IsValidCharacter', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() +}) diff --git a/src/core/internal/human-readable/types/signatures.ts b/src/core/internal/human-readable/types/signatures.ts new file mode 100644 index 00000000..16c8b30f --- /dev/null +++ b/src/core/internal/human-readable/types/signatures.ts @@ -0,0 +1,308 @@ +import type { AbiStateMutability } from 'abitype' +import type { Error } from '../types.js' + +export type ErrorSignature< + name extends string = string, + parameters extends string = string, +> = `error ${name}(${parameters})` +export type IsErrorSignature = + signature extends ErrorSignature ? IsName : false +export type EventSignature< + name extends string = string, + parameters extends string = string, +> = `event ${name}(${parameters})` +export type IsEventSignature = + signature extends EventSignature ? IsName : false + +export type FunctionSignature< + name extends string = string, + tail extends string = string, +> = `function ${name}(${tail}` +export type IsFunctionSignature = + signature extends FunctionSignature + ? IsName extends true + ? signature extends ValidFunctionSignatures + ? true + : // Check that `Parameters` is not absorbing other types (e.g. `returns`) + signature extends `function ${string}(${infer parameters})` + ? parameters extends InvalidFunctionParameters + ? false + : true + : false + : false + : false +export type Scope = 'public' | 'external' // `internal` or `private` functions wouldn't make it to ABI so can ignore +type Returns = `returns (${string})` | `returns(${string})` +// Almost all valid function signatures, except `function ${string}(${infer parameters})` since `parameters` can absorb returns +type ValidFunctionSignatures = + | `function ${string}()` + // basic + | `function ${string}() ${Returns}` + | `function ${string}() ${AbiStateMutability}` + | `function ${string}() ${Scope}` + // combinations + | `function ${string}() ${AbiStateMutability} ${Returns}` + | `function ${string}() ${Scope} ${Returns}` + | `function ${string}() ${Scope} ${AbiStateMutability}` + | `function ${string}() ${Scope} ${AbiStateMutability} ${Returns}` + // Parameters + | `function ${string}(${string}) ${Returns}` + | `function ${string}(${string}) ${AbiStateMutability}` + | `function ${string}(${string}) ${Scope}` + | `function ${string}(${string}) ${AbiStateMutability} ${Returns}` + | `function ${string}(${string}) ${Scope} ${Returns}` + | `function ${string}(${string}) ${Scope} ${AbiStateMutability}` + | `function ${string}(${string}) ${Scope} ${AbiStateMutability} ${Returns}` + +export type StructSignature< + name extends string = string, + properties extends string = string, +> = `struct ${name} {${properties}}` +export type IsStructSignature = + signature extends StructSignature ? IsName : false + +type ConstructorSignature = `constructor(${tail}` +export type IsConstructorSignature = + signature extends ConstructorSignature + ? signature extends ValidConstructorSignatures + ? true + : false + : false +type ValidConstructorSignatures = + | `constructor(${string})` + | `constructor(${string}) payable` + +export type FallbackSignature< + abiStateMutability extends '' | ' payable' = '' | ' payable', +> = `fallback() external${abiStateMutability}` +export type IsFallbackSignature = signature extends + | FallbackSignature<''> + | FallbackSignature<' payable'> + ? true + : false + +export type ReceiveSignature = 'receive() external payable' + +// TODO: Maybe use this for signature validation one day +// https://twitter.com/devanshj__/status/1610423724708343808 +export type IsSignature = + | (IsErrorSignature extends true ? true : never) + | (IsEventSignature extends true ? true : never) + | (IsFunctionSignature extends true ? true : never) + | (IsStructSignature extends true ? true : never) + | (IsConstructorSignature extends true ? true : never) + | (IsFallbackSignature extends true ? true : never) + | (type extends ReceiveSignature ? true : never) extends infer condition + ? [condition] extends [never] + ? false + : true + : false + +export type Signature< + string1 extends string, + string2 extends string | unknown = unknown, +> = IsSignature extends true + ? string1 + : string extends string1 // if exactly `string` (not narrowed), then pass through as valid + ? string1 + : Error<`Signature "${string1}" is invalid${string2 extends string + ? ` at position ${string2}` + : ''}.`> + +export type Signatures = { + [key in keyof signatures]: Signature +} + +export type Modifier = 'calldata' | 'indexed' | 'memory' | 'payable' | 'storage' +export type FunctionModifier = Extract< + Modifier, + 'calldata' | 'memory' | 'payable' | 'storage' +> +export type EventModifier = Extract + +export type IsName = name extends '' + ? false + : ValidateName extends name + ? true + : false + +export type AssertName = + ValidateName extends infer invalidName extends string[] + ? `[${invalidName[number]}]` + : name + +export type ValidateName< + name extends string, + checkCharacters extends boolean = false, +> = name extends `${string}${' '}${string}` + ? Error<`Identifier "${name}" cannot contain whitespace.`> + : IsSolidityKeyword extends true + ? Error<`"${name}" is a protected Solidity keyword.`> + : name extends `${number}` + ? Error<`Identifier "${name}" cannot be a number string.`> + : name extends `${number}${string}` + ? Error<`Identifier "${name}" cannot start with a number.`> + : checkCharacters extends true + ? IsValidCharacter extends true + ? name + : Error<`"${name}" contains invalid character.`> + : name + +export type IsSolidityKeyword = + type extends SolidityKeywords ? true : false + +export type SolidityKeywords = + | 'after' + | 'alias' + | 'anonymous' + | 'apply' + | 'auto' + | 'byte' + | 'calldata' + | 'case' + | 'catch' + | 'constant' + | 'copyof' + | 'default' + | 'defined' + | 'error' + | 'event' + | 'external' + | 'false' + | 'final' + | 'function' + | 'immutable' + | 'implements' + | 'in' + | 'indexed' + | 'inline' + | 'internal' + | 'let' + | 'mapping' + | 'match' + | 'memory' + | 'mutable' + | 'null' + | 'of' + | 'override' + | 'partial' + | 'private' + | 'promise' + | 'public' + | 'pure' + | 'reference' + | 'relocatable' + | 'return' + | 'returns' + | 'sizeof' + | 'static' + | 'storage' + | 'struct' + | 'super' + | 'supports' + | 'switch' + | 'this' + | 'true' + | 'try' + | 'typedef' + | 'typeof' + | 'var' + | 'view' + | 'virtual' + | `address${`[${string}]` | ''}` + | `bool${`[${string}]` | ''}` + | `string${`[${string}]` | ''}` + | `tuple${`[${string}]` | ''}` + | `bytes${number | ''}${`[${string}]` | ''}` + | `${'u' | ''}int${number | ''}${`[${string}]` | ''}` + +export type IsValidCharacter = + character extends `${ValidCharacters}${infer tail}` + ? tail extends '' + ? true + : IsValidCharacter + : false + +// biome-ignore format: no formatting +type ValidCharacters = + // uppercase letters + | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' + // lowercase letters + | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' + // numbers + | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' + // special characters + | '_' | '$' + +// Template string inference can absorb `returns`: +// type Result = `function foo(string) return s (uint256)` extends `function ${string}(${infer Parameters})` ? Parameters : never +// // ^? type Result = "string ) return s (uint256" +// So we need to validate against `returns` keyword with all combinations of whitespace +type InvalidFunctionParameters = + | `${string}${MangledReturns} (${string}` + | `${string}) ${MangledReturns}${string}` + | `${string})${string}${MangledReturns}${string}(${string}` +// r_e_t_u_r_n_s +type MangledReturns = + // Single + | `r${string}eturns` + | `re${string}turns` + | `ret${string}urns` + | `retu${string}rns` + | `retur${string}ns` + | `return${string}s` + // Double + // `r_e*` + | `r${string}e${string}turns` + | `r${string}et${string}urns` + | `r${string}etu${string}rns` + | `r${string}etur${string}ns` + | `r${string}eturn${string}s` + // `re_t*` + | `re${string}t${string}urns` + | `re${string}tu${string}rns` + | `re${string}tur${string}ns` + | `re${string}turn${string}s` + // `ret_u*` + | `ret${string}u${string}rns` + | `ret${string}ur${string}ns` + | `ret${string}urn${string}s` + // `retu_r*` + | `retu${string}r${string}ns` + | `retu${string}rn${string}s` + // `retur_n*` + | `retur${string}n${string}s` + // Triple + // `r_e_t*` + | `r${string}e${string}t${string}urns` + | `r${string}e${string}tu${string}rns` + | `r${string}e${string}tur${string}ns` + | `r${string}e${string}turn${string}s` + // `re_t_u*` + | `re${string}t${string}u${string}rns` + | `re${string}t${string}ur${string}ns` + | `re${string}t${string}urn${string}s` + // `ret_u_r*` + | `ret${string}u${string}r${string}ns` + | `ret${string}u${string}rn${string}s` + // `retu_r_n*` + | `retu${string}r${string}n${string}s` + // Quadruple + // `r_e_t_u*` + | `r${string}e${string}t${string}u${string}rns` + | `r${string}e${string}t${string}ur${string}ns` + | `r${string}e${string}t${string}urn${string}s` + // `re_t_u_r*` + | `re${string}t${string}u${string}r${string}ns` + | `re${string}t${string}u${string}rn${string}s` + // `ret_u_r_n*` + | `ret${string}u${string}r${string}n${string}s` + // Quintuple + // `r_e_t_u_r*` + | `r${string}e${string}t${string}u${string}r${string}ns` + | `r${string}e${string}t${string}u${string}rn${string}s` + // `re_t_u_r_n*` + | `re${string}t${string}u${string}r${string}n${string}s` + // Sextuple + // `r_e_t_u_r_n_s` + | `r${string}e${string}t${string}u${string}r${string}n${string}s` diff --git a/src/core/internal/human-readable/types/structs.test-d.ts b/src/core/internal/human-readable/types/structs.test-d.ts new file mode 100644 index 00000000..a68b73fa --- /dev/null +++ b/src/core/internal/human-readable/types/structs.test-d.ts @@ -0,0 +1,221 @@ +import { expectTypeOf, test } from 'vp/test' + +import type { + ParseStruct, + ParseStructProperties, + ParseStructs, + ResolveStructs, + StructLookup, +} from './structs.js' + +test('ParseStructs', () => { + type Result = ParseStructs< + [ + 'struct Person { Name name; }', + 'struct Name { Foo foo; }', + 'struct Foo { string bar; }', + 'function addPerson(Person person)', + ] + > + expectTypeOf().toEqualTypeOf<{ + Person: readonly [ + { + readonly name: 'name' + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'foo' + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'string' + readonly name: 'bar' + }, + ] + }, + ] + }, + ] + Foo: readonly [{ readonly type: 'string'; readonly name: 'bar' }] + Name: readonly [ + { + readonly type: 'tuple' + readonly name: 'foo' + readonly components: readonly [ + { readonly type: 'string'; readonly name: 'bar' }, + ] + }, + ] + }>() + + expectTypeOf< + ParseStructs<['struct Foo { Bar bar; }', 'struct Bar { Foo foo; }']> + >().toEqualTypeOf<{ + Foo: readonly [ + { + readonly name: 'bar' + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'foo' + readonly type: 'tuple' + readonly components: readonly [ + [ + 'Error: Circular reference detected. Struct "Bar" is a circular reference.', + ], + ] + }, + ] + }, + ] + Bar: readonly [ + { + readonly name: 'foo' + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'bar' + readonly type: 'tuple' + readonly components: readonly [ + [ + 'Error: Circular reference detected. Struct "Foo" is a circular reference.', + ], + ] + }, + ] + }, + ] + }>() + + expectTypeOf>().toEqualTypeOf<{ + Foo: readonly [ + { + readonly name: 'foo' + readonly type: 'tuple' + readonly components: readonly [ + [ + 'Error: Circular reference detected. Struct "Foo" is a circular reference.', + ], + ] + }, + ] + }>() + + expectTypeOf< + ParseStructs<['struct Person { Name name;']> + >().toEqualTypeOf<{}>() + + expectTypeOf>().toEqualTypeOf<{}>() + expectTypeOf< + ParseStructs<['function addPerson(Person person)']> + >().toEqualTypeOf<{}>() +}) + +test('ParseStruct', () => { + expectTypeOf< + ParseStruct<'struct Foo { string foo; string bar; }'> + >().toEqualTypeOf<{ + readonly name: 'Foo' + readonly components: [ + { readonly type: 'string'; readonly name: 'foo' }, + { readonly type: 'string'; readonly name: 'bar' }, + ] + }>() + expectTypeOf>().toEqualTypeOf<{ + readonly name: 'Foo' + readonly components: [] + }>() + expectTypeOf>().toEqualTypeOf<{ + readonly name: 'Foo' + readonly components: [ + { + readonly type: 'Bar' + readonly name: 'bar' + }, + ] + }>() + + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() +}) + +test('ResolveStructs', () => { + type Result = ResolveStructs< + [{ type: 'Name'; name: 'name' }], + { + Person: [{ type: 'Name'; name: 'name' }] + Name: [{ type: 'Foo'; name: 'foo' }] + Foo: [{ type: 'string'; name: 'bar' }, { type: 'uint16'; name: 'baz' }] + } + > + expectTypeOf().toEqualTypeOf< + readonly [ + { + readonly name: 'name' + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'foo' + readonly type: 'tuple' + readonly components: readonly [ + { type: 'string'; name: 'bar' }, + { type: 'uint16'; name: 'baz' }, + ] + }, + ] + }, + ] + >() + + expectTypeOf>().toEqualTypeOf() + expectTypeOf< + ResolveStructs< + [{ type: 'Foo[]'; name: 'foo' }], + { Foo: [{ type: 'string'; name: 'bar' }] } + > + >().toEqualTypeOf< + readonly [ + { + readonly name: 'foo' + readonly type: 'tuple[]' + readonly components: readonly [ + { + type: 'string' + name: 'bar' + }, + ] + }, + ] + >() +}) + +test('ParseStructProperties', () => { + expectTypeOf>().toEqualTypeOf< + [{ readonly type: 'string' }] + >() + expectTypeOf>().toEqualTypeOf< + [{ readonly type: 'string'; readonly name: 'foo' }] + >() + expectTypeOf< + ParseStructProperties<'string; string;'> extends [ + { readonly type: 'string' }, + { readonly type: 'string' }, + ] + ? true + : false + >().toEqualTypeOf() + expectTypeOf< + ParseStructProperties<'string foo; string bar;'> + >().toEqualTypeOf< + [ + { readonly type: 'string'; readonly name: 'foo' }, + { readonly type: 'string'; readonly name: 'bar' }, + ] + >() + + expectTypeOf>().toEqualTypeOf<[]>() + expectTypeOf>().toEqualTypeOf<[]>() + expectTypeOf>().toEqualTypeOf< + [{ readonly type: 'string' }] + >() +}) diff --git a/src/core/internal/human-readable/types/structs.ts b/src/core/internal/human-readable/types/structs.ts new file mode 100644 index 00000000..1f27984c --- /dev/null +++ b/src/core/internal/human-readable/types/structs.ts @@ -0,0 +1,86 @@ +import type { AbiParameter } from 'abitype' +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 ParseStructs = + // Create "shallow" version of each struct (and filter out non-structs or invalid structs) + { + [signature in signatures[number] as ParseStruct extends infer struct extends + { + name: string + } + ? struct['name'] + : never]: ParseStruct['components'] + } extends infer structs extends Record< + string, + readonly (AbiParameter & { type: string })[] + > + ? // Resolve nested structs inside each struct + { + [structName in keyof structs]: ResolveStructs< + structs[structName], + structs + > + } + : never + +export type ParseStruct< + signature extends string, + structs extends StructLookup | unknown = unknown, +> = signature extends StructSignature + ? { + readonly name: Trim + readonly components: ParseStructProperties + } + : never + +export type ResolveStructs< + abiParameters extends readonly (AbiParameter & { type: string })[], + structs extends Record, + keyReferences extends { [_: string]: unknown } | unknown = unknown, +> = readonly [ + ...{ + [key in keyof abiParameters]: abiParameters[key]['type'] extends `${infer head extends + string & keyof structs}[${infer tail}]` // Struct arrays (e.g. `type: 'Struct[]'`, `type: 'Struct[10]'`, `type: 'Struct[][]'`) + ? head extends keyof keyReferences + ? Error<`Circular reference detected. Struct "${abiParameters[key]['type']}" is a circular reference.`> + : { + readonly name: abiParameters[key]['name'] + readonly type: `tuple[${tail}]` + readonly components: ResolveStructs< + structs[head], + structs, + keyReferences & { [_ in head]: true } + > + } + : // Basic struct (e.g. `type: 'Struct'`) + abiParameters[key]['type'] extends keyof structs + ? abiParameters[key]['type'] extends keyof keyReferences + ? Error<`Circular reference detected. Struct "${abiParameters[key]['type']}" is a circular reference.`> + : { + readonly name: abiParameters[key]['name'] + readonly type: 'tuple' + readonly components: ResolveStructs< + structs[abiParameters[key]['type']], + structs, + keyReferences & { [_ in abiParameters[key]['type']]: true } + > + } + : abiParameters[key] + }, +] + +export type ParseStructProperties< + signature extends string, + structs extends StructLookup | unknown = unknown, + result extends any[] = [], +> = Trim extends `${infer head};${infer tail}` + ? ParseStructProperties< + tail, + structs, + [...result, ParseAbiParameter] + > + : result diff --git a/src/core/internal/human-readable/types/utils.test-d.ts b/src/core/internal/human-readable/types/utils.test-d.ts new file mode 100644 index 00000000..d6332c21 --- /dev/null +++ b/src/core/internal/human-readable/types/utils.test-d.ts @@ -0,0 +1,1029 @@ +import { assertType, expectTypeOf, test } from 'vp/test' + +import type { + ParseAbiParameter, + ParseAbiParameters, + ParseSignature, + SplitParameters, + _ParseFunctionParametersAndStateMutability, + _ParseTuple, + _SplitNameOrModifier, + _UnwrapNameOrModifier, + _ValidateAbiParameter, +} from './utils.js' + +type OptionsWithModifier = { modifier: 'calldata'; structs: unknown } +type OptionsWithIndexed = { modifier: 'indexed'; structs: unknown } +type OptionsWithStructs = { + structs: { + Foo: [{ type: 'address'; name: 'bar' }] + } +} + +test('ParseSignature', () => { + type Structs = { + Baz: [{ type: 'address'; name: 'baz' }] + } + + // Error + assertType>({ + type: 'error', + name: 'Foo', + inputs: [], + }) + assertType>({ + type: 'error', + name: 'Foo', + inputs: [{ type: 'string' }], + }) + assertType>({ + type: 'error', + name: 'Foo', + inputs: [{ type: 'string', name: 'bar' }], + }) + assertType>({ + type: 'error', + name: 'Foo', + inputs: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'address', name: 'baz' }], + }, + ], + }) + + // Events + assertType>({ + type: 'event', + name: 'Foo', + inputs: [], + }) + assertType>({ + type: 'event', + name: 'Foo', + inputs: [{ type: 'string' }], + }) + assertType>({ + type: 'event', + name: 'Foo', + inputs: [{ type: 'string', name: 'bar' }], + }) + assertType>({ + type: 'event', + name: 'Foo', + inputs: [{ type: 'string', indexed: true, name: 'bar' }], + }) + assertType>({ + type: 'event', + name: 'Foo', + inputs: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'address', name: 'baz' }], + }, + ], + }) + assertType>({ + type: 'event', + name: 'Foo', + inputs: [ + { + type: 'tuple', + indexed: true, + name: 'bar', + components: [{ type: 'string' }], + }, + ], + }) + + // Constructor + assertType>({ + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [], + }) + assertType>({ + type: 'constructor', + stateMutability: 'payable', + inputs: [], + }) + assertType>({ + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [{ type: 'string' }], + }) + assertType>({ + type: 'constructor', + stateMutability: 'payable', + inputs: [{ type: 'string' }], + }) + assertType>({ + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [{ type: 'string', name: 'foo' }], + }) + assertType>({ + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [{ type: 'string', name: 'foo' }], + }) + assertType>({ + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [{ type: 'tuple', name: 'foo', components: [{ type: 'string' }] }], + }) + + // Fallback + assertType>({ + type: 'fallback', + stateMutability: 'nonpayable', + }) + assertType>({ + type: 'fallback', + stateMutability: 'payable', + }) + + // Receive + assertType>({ + type: 'receive', + stateMutability: 'payable', + }) + + // Functions + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [], + outputs: [], + }) + + // inputs + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [{ type: 'string' }], + outputs: [], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [{ type: 'string', name: 'bar' }], + outputs: [], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'address', name: 'baz' }], + }, + ], + outputs: [], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'view', + inputs: [{ type: 'string', name: 'bar' }], + outputs: [], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'payable', + inputs: [{ type: 'string', name: 'bar' }], + outputs: [], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [{ type: 'string' }], + outputs: [], + }) + assertType>({ + name: 'foo', + type: 'function', + stateMutability: 'nonpayable', + inputs: [{ type: 'address', name: 'to' }], + outputs: [], + }) + + assertType>({ + name: 'foo', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'string', + name: ['Error: "indexed" is a protected Solidity keyword.'], + }, + ], + outputs: [], + }) + assertType>({ + name: 'foo', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'string', + name: ['Error: Identifier "indexed bar" cannot contain whitespace.'], + }, + ], + outputs: [], + }) + + // outputs + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [], + outputs: [{ type: 'string' }], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [], + outputs: [{ type: 'string', name: 'bar' }], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [], + outputs: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'address', name: 'baz' }], + }, + ], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'view', + inputs: [], + outputs: [{ type: 'string', name: 'bar' }], + }) + assertType< + ParseSignature<'function foo() public payable returns (string bar)'> + >({ + type: 'function', + name: 'foo', + stateMutability: 'payable', + inputs: [], + outputs: [{ type: 'string', name: 'bar' }], + }) + assertType< + ParseSignature<'function foo() public payable returns(string bar)'> + >({ + type: 'function', + name: 'foo', + stateMutability: 'payable', + inputs: [], + outputs: [{ type: 'string', name: 'bar' }], + }) + + // inputs and outputs + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [{ type: 'string' }], + outputs: [{ type: 'string' }], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [{ type: 'string', name: 'bar' }], + outputs: [{ type: 'string', name: 'bar' }], + }) + assertType< + ParseSignature<'function foo(string foo) public payable returns(string bar)'> + >({ + type: 'function', + name: 'foo', + stateMutability: 'payable', + inputs: [{ type: 'string', name: 'foo' }], + outputs: [{ type: 'string', name: 'bar' }], + }) + assertType< + ParseSignature<'function foo(Baz bar) returns (Baz bar)', Structs> + >({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'address', name: 'baz' }], + }, + ], + outputs: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'address', name: 'baz' }], + }, + ], + }) + assertType< + ParseSignature<'function foo(string bar) view returns (string bar)'> + >({ + type: 'function', + name: 'foo', + stateMutability: 'view', + inputs: [{ type: 'string', name: 'bar' }], + outputs: [{ type: 'string', name: 'bar' }], + }) + assertType< + ParseSignature<'function foo(string bar) public payable returns (string bar)'> + >({ + type: 'function', + name: 'foo', + stateMutability: 'payable', + inputs: [{ type: 'string', name: 'bar' }], + outputs: [{ type: 'string', name: 'bar' }], + }) + + assertType< + ParseSignature<'function foo(((string)) calldata) returns (string, (string))'> + >({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'tuple', + components: [{ type: 'tuple', components: [{ type: 'string' }] }], + }, + ], + outputs: [ + { + type: 'string', + }, + { + type: 'tuple', + components: [{ type: 'string' }], + }, + ], + }) +}) + +test('ParseAbiParameters', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf< + readonly [{ readonly type: 'string' }] + >() + expectTypeOf< + ParseAbiParameters<['string', 'string']> extends readonly [ + { readonly type: 'string' }, + { readonly type: 'string' }, + ] + ? true + : false + >().toEqualTypeOf() +}) + +test('ParseAbiParameter', () => { + // `${Type} ${Modifier} ${Name}` format + assertType>({ + type: 'string', + name: 'foo', + }) + assertType>({ + type: 'string', + indexed: true, + name: 'foo', + }) + assertType< + ParseAbiParameter< + 'Foo calldata foo', + OptionsWithModifier & OptionsWithStructs + > + >({ + type: 'tuple', + name: 'foo', + components: [{ type: 'address', name: 'bar' }], + }) + assertType< + ParseAbiParameter< + 'Foo indexed foo', + OptionsWithIndexed & OptionsWithStructs + > + >({ + type: 'tuple', + indexed: true, + name: 'foo', + components: [{ type: 'address', name: 'bar' }], + }) + assertType< + ParseAbiParameter< + 'Foo[][1] indexed foo', + OptionsWithIndexed & OptionsWithStructs + > + >({ + name: 'foo', + type: 'tuple[][1]', + indexed: true, + components: [{ type: 'address', name: 'bar' }], + }) + assertType< + ParseAbiParameter< + 'Foo[][1] calldata foo', + OptionsWithModifier & OptionsWithStructs + > + >({ + name: 'foo', + type: 'tuple[][1]', + components: [{ type: 'address', name: 'bar' }], + }) + + // `${Type} ${NameOrModifier}` format + assertType>({ + type: 'string', + name: 'foo', + }) + assertType>({ + type: 'string', + name: ['Error: "indexed" is a protected Solidity keyword.'], + }) + assertType>({ + type: 'string', + }) + assertType>({ + type: 'string', + indexed: true, + }) + assertType< + ParseAbiParameter<'Foo calldata', OptionsWithModifier & OptionsWithStructs> + >({ + type: 'tuple', + components: [{ type: 'address', name: 'bar' }], + }) + assertType< + ParseAbiParameter<'Foo indexed', OptionsWithIndexed & OptionsWithStructs> + >({ + type: 'tuple', + indexed: true, + components: [{ type: 'address', name: 'bar' }], + }) + assertType>({ + name: 'foo', + type: 'tuple[][1]', + components: [{ type: 'address', name: 'bar' }], + }) + assertType>({ + name: 'foo', + type: 'tuple[1]', + components: [{ type: 'address', name: 'bar' }], + }) + + // `${Type}` format + assertType>({ + type: 'string', + }) + assertType>({ + type: 'tuple', + components: [{ type: 'address', name: 'bar' }], + }) + assertType>({ + type: 'tuple[][1]', + components: [{ type: 'address', name: 'bar' }], + }) + + // tuple format + assertType>({ + type: 'tuple', + components: [{ type: 'string' }], + }) + assertType>({ + type: 'tuple', + components: [{ type: 'string' }, { type: 'string' }], + }) + assertType>({ + type: 'tuple', + components: [ + { type: 'string' }, + { type: 'tuple', components: [{ type: 'string' }] }, + ], + }) + + assertType>({ + type: 'tuple', + components: [ + { + type: 'tuple', + components: [ + { + type: 'tuple[1]', + components: [ + { + type: 'tuple', + components: [ + { + type: 'string', + name: 'baz', + }, + ], + name: 'bar', + }, + ], + name: 'foo', + }, + ], + name: 'boo', + }, + ], + }) + + assertType>({ + type: 'address', + name: ['Error: "alias" is a protected Solidity keyword.'], + }) + // assertType>({ + // type: ['Error: Type "Foo" is not a valid ABI type.'], + // name: 'foo', + // }) + + assertType>({ type: 'int256' }) + assertType>({ type: 'uint256' }) + assertType>({ type: 'uint256[]' }) + assertType>({ type: 'uint256[10][]' }) +}) + +test('SplitParameters', () => { + expectTypeOf>().toEqualTypeOf<[]>() + expectTypeOf>().toEqualTypeOf<['string']>() + expectTypeOf>().toEqualTypeOf< + ['string', 'string'] + >() + expectTypeOf>().toEqualTypeOf< + ['string indexed foo'] + >() + expectTypeOf>().toEqualTypeOf< + ['string foo', 'string bar'] + >() + expectTypeOf< + SplitParameters<'address owner, (bool loading, (string[][] names) cats)[] dog, uint tokenId'> + >().toEqualTypeOf< + [ + 'address owner', + '(bool loading, (string[][] names) cats)[] dog', + 'uint tokenId', + ] + >() + + expectTypeOf>().toEqualTypeOf< + [ + 'Error: Unbalanced parentheses. "((string)" has too many opening parentheses.', + ] + >() + expectTypeOf>().toEqualTypeOf< + [ + 'Error: Unbalanced parentheses. "((((string))" has too many opening parentheses.', + ] + >() + expectTypeOf>().toEqualTypeOf< + [ + 'Error: Unbalanced parentheses. "(string)" has too many closing parentheses.', + ] + >() + expectTypeOf>().toEqualTypeOf< + [ + 'Error: Unbalanced parentheses. "(string)" has too many closing parentheses.', + ] + >() +}) + +test('_ValidateAbiParameter', () => { + expectTypeOf<_ValidateAbiParameter<{ type: 'string' }>>().toEqualTypeOf<{ + type: 'string' + }>() + expectTypeOf< + _ValidateAbiParameter<{ type: 'string'; name: 'foo' }> + >().toEqualTypeOf<{ + type: 'string' + name: 'foo' + }>() + + expectTypeOf<_ValidateAbiParameter<{ type: 'int' }>>().toEqualTypeOf<{ + readonly type: 'int256' + }>() + expectTypeOf<_ValidateAbiParameter<{ type: 'uint' }>>().toEqualTypeOf<{ + readonly type: 'uint256' + }>() + expectTypeOf<_ValidateAbiParameter<{ type: 'uint[]' }>>().toEqualTypeOf<{ + readonly type: 'uint256[]' + }>() + expectTypeOf<_ValidateAbiParameter<{ type: 'uint[10][]' }>>().toEqualTypeOf<{ + readonly type: 'uint256[10][]' + }>() + + // expectTypeOf< + // _ValidateAbiParameter<{ type: 'string'; name: 'f0!' }> + // >().toEqualTypeOf<{ + // type: 'string' + // readonly name: ['Error: "f0!" contains invalid character.'] + // }>() + // expectTypeOf< + // _ValidateAbiParameter<{ type: 'string'; name: 'alias' }> + // >().toEqualTypeOf<{ + // type: 'string' + // readonly name: ['Error: "alias" is a protected Solidity keyword.'] + // }>() + // expectTypeOf< + // _ValidateAbiParameter<{ type: 'Bar'; name: 'foo' }> + // >().toEqualTypeOf<{ + // readonly type: ['Error: Type "Bar" is not a valid ABI type.'] + // name: 'foo' + // }>() +}) + +test('_ParseFunctionParametersAndStateMutability', () => { + expectTypeOf< + _ParseFunctionParametersAndStateMutability<'function foo()'> + >().toEqualTypeOf<{ + Inputs: '' + StateMutability: 'nonpayable' + }>() + + expectTypeOf< + _ParseFunctionParametersAndStateMutability<'function foo(string bar)'> + >().toEqualTypeOf<{ + Inputs: 'string bar' + StateMutability: 'nonpayable' + }>() + + expectTypeOf< + _ParseFunctionParametersAndStateMutability<'function foo() view'> + >().toEqualTypeOf<{ + Inputs: '' + StateMutability: 'view' + }>() + + expectTypeOf< + _ParseFunctionParametersAndStateMutability<'function foo(string bar) view'> + >().toEqualTypeOf<{ + Inputs: 'string bar' + StateMutability: 'view' + }>() + + expectTypeOf< + _ParseFunctionParametersAndStateMutability<'function foo(string bar, uint256) external view'> + >().toEqualTypeOf<{ + Inputs: 'string bar, uint256' + StateMutability: 'view' + }>() + + expectTypeOf< + _ParseFunctionParametersAndStateMutability<'function stepChanges((uint256 characterID, uint64 newPosition, uint24 xp, uint24 epoch, uint8 hp, (int32 x, int32 y, uint8 hp, uint8 kind)[5] monsters, (uint8 monsterIndexPlus1, uint8 attackCardsUsed1, uint8 attackCardsUsed2, uint8 defenseCardsUsed1, uint8 defenseCardsUsed2) battle) stateChanges, uint256 action, bool revetOnInvalidMoves) pure returns ((uint256 characterID, uint64 newPosition, uint24 xp, uint24 epoch, uint8 hp, (int32 x, int32 y, uint8 hp, uint8 kind)[5] monsters, (uint8 monsterIndexPlus1, uint8 attackCardsUsed1, uint8 attackCardsUsed2, uint8 defenseCardsUsed1, uint8 defenseCardsUsed2) battle))'> + >().toEqualTypeOf<{ + Inputs: '(uint256 characterID, uint64 newPosition, uint24 xp, uint24 epoch, uint8 hp, (int32 x, int32 y, uint8 hp, uint8 kind)[5] monsters, (uint8 monsterIndexPlus1, uint8 attackCardsUsed1, uint8 attackCardsUsed2, uint8 defenseCardsUsed1, uint8 defenseCardsUsed2) battle) stateChanges, uint256 action, bool revetOnInvalidMoves' + StateMutability: 'pure' + }>() +}) + +test('_ParseTuple', () => { + // basic tuples + assertType<_ParseTuple<'(string)'>>({ + type: 'tuple', + components: [{ type: 'string' }], + }) + assertType<_ParseTuple<'(string, string)'>>({ + type: 'tuple', + components: [{ type: 'string' }, { type: 'string' }], + }) + assertType<_ParseTuple<'((string, string), string)'>>({ + type: 'tuple', + components: [ + { type: 'tuple', components: [{ type: 'string' }, { type: 'string' }] }, + { type: 'string' }, + ], + }) + assertType<_ParseTuple<'((string))'>>({ + type: 'tuple', + components: [{ type: 'tuple', components: [{ type: 'string' }] }], + }) + assertType<_ParseTuple<'(((string)))'>>({ + type: 'tuple', + components: [ + { + type: 'tuple', + components: [{ type: 'tuple', components: [{ type: 'string' }] }], + }, + ], + }) + assertType<_ParseTuple<'(string calldata)'>>({ + type: 'tuple', + components: [ + { + type: 'string', + name: ['Error: "calldata" is a protected Solidity keyword.'], + }, + ], + }) + assertType<_ParseTuple<'(Foo)', OptionsWithStructs>>({ + type: 'tuple', + components: [ + { type: 'tuple', components: [{ type: 'address', name: 'bar' }] }, + ], + }) + + // named tuple params + assertType<_ParseTuple<'(string foo)'>>({ + type: 'tuple', + components: [{ type: 'string', name: 'foo' }], + }) + assertType<_ParseTuple<'(string bar) foo'>>({ + name: 'foo', + type: 'tuple', + components: [{ type: 'string', name: 'bar' }], + }) + assertType<_ParseTuple<'((string bar) foo)'>>({ + type: 'tuple', + components: [ + { + type: 'tuple', + name: 'foo', + components: [{ type: 'string', name: 'bar' }], + }, + ], + }) + assertType<_ParseTuple<'(Foo) foo', OptionsWithStructs>>({ + type: 'tuple', + name: 'foo', + components: [ + { type: 'tuple', components: [{ type: 'address', name: 'bar' }] }, + ], + }) + assertType<_ParseTuple<'((string)) calldata', OptionsWithModifier>>({ + type: 'tuple', + components: [{ type: 'tuple', components: [{ type: 'string' }] }], + }) + + // mixed basic and named tuple params + assertType<_ParseTuple<'(string, string foo)'>>({ + type: 'tuple', + components: [{ type: 'string' }, { type: 'string', name: 'foo' }], + }) + assertType<_ParseTuple<'(string, string bar) foo'>>({ + name: 'foo', + type: 'tuple', + components: [{ type: 'string' }, { type: 'string', name: 'bar' }], + }) + assertType< + _ParseTuple<'(string baz, string bar) indexed foo', OptionsWithIndexed> + >({ + name: 'foo', + type: 'tuple', + components: [ + { type: 'string', name: 'baz' }, + { type: 'string', name: 'bar' }, + ], + indexed: true, + }) + + // inline tuples of tuples + assertType<_ParseTuple<'(string)[]'>>({ + type: 'tuple[]', + components: [{ type: 'string' }], + }) + assertType<_ParseTuple<'(string, string)[]'>>({ + type: 'tuple[]', + components: [{ type: 'string' }, { type: 'string' }], + }) + assertType<_ParseTuple<'((string))[]'>>({ + type: 'tuple[]', + components: [{ type: 'tuple', components: [{ type: 'string' }] }], + }) + assertType<_ParseTuple<'((string)[])[]'>>({ + type: 'tuple[]', + components: [ + { + type: 'tuple[]', + components: [{ type: 'string' }], + }, + ], + }) + + // inline tuples of tuples with name and/or modifier attached + assertType<_ParseTuple<'(string)[] foo'>>({ + type: 'tuple[]', + name: 'foo', + components: [{ type: 'string' }], + }) + assertType<_ParseTuple<'(string, string bar)[] foo'>>({ + type: 'tuple[]', + name: 'foo', + components: [{ type: 'string' }, { type: 'string', name: 'bar' }], + }) + assertType<_ParseTuple<'((string baz) bar)[] foo'>>({ + type: 'tuple[]', + name: 'foo', + components: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'string', name: 'baz' }], + }, + ], + }) + assertType< + _ParseTuple< + '((string)[])[] indexed foo', + OptionsWithIndexed & OptionsWithStructs + > + >({ + type: 'tuple[]', + name: 'foo', + indexed: true, + components: [ + { + type: 'tuple[]', + components: [{ type: 'string' }], + }, + ], + }) + assertType<_ParseTuple<'((string) foo)[]'>>({ + type: 'tuple[]', + components: [ + { type: 'tuple', name: 'foo', components: [{ type: 'string' }] }, + ], + }) + assertType<_ParseTuple<'(string) indexed bar', OptionsWithIndexed>>({ + type: 'tuple', + name: 'bar', + indexed: true, + components: [{ type: 'string' }], + }) + + assertType<_ParseTuple<'((((string))) bar)'>>({ + type: 'tuple', + components: [ + { + name: 'bar', + type: 'tuple', + components: [ + { + type: 'tuple', + components: [ + { + type: 'tuple', + components: [{ type: 'string' }], + }, + ], + }, + ], + }, + ], + }) + assertType<_ParseTuple<'(((string) baz) bar) foo'>>({ + type: 'tuple', + name: 'foo', + components: [ + { + name: 'bar', + type: 'tuple', + components: [ + { + name: 'baz', + type: 'tuple', + components: [{ type: 'string' }], + }, + ], + }, + ], + }) + assertType<_ParseTuple<'((((string) baz)) bar) foo'>>({ + type: 'tuple', + name: 'foo', + components: [ + { + name: 'bar', + type: 'tuple', + components: [ + { + type: 'tuple', + components: [ + { + name: 'baz', + type: 'tuple', + components: [{ type: 'string' }], + }, + ], + }, + ], + }, + ], + }) + + // Modifiers not converted inside tuples + assertType<_ParseTuple<'(string indexed)[] foo', OptionsWithIndexed>>({ + type: 'tuple[]', + name: 'foo', + components: [ + { + type: 'string', + name: ['Error: "indexed" is a protected Solidity keyword.'], + }, + ], + }) + + assertType<_ParseTuple<'((((string baz) bar)[1] foo) boo)'>>({ + type: 'tuple', + components: [ + { + type: 'tuple', + components: [ + { + type: 'tuple[1]', + components: [ + { + type: 'tuple', + components: [ + { + type: 'string', + name: 'baz', + }, + ], + name: 'bar', + }, + ], + name: 'foo', + }, + ], + name: 'boo', + }, + ], + }) + assertType<_ParseTuple<'(((string baz) bar)[1] foo) boo'>>({ + type: 'tuple', + components: [ + { + type: 'tuple[1]', + components: [ + { + type: 'tuple', + components: [ + { + type: 'string', + name: 'baz', + }, + ], + name: 'bar', + }, + ], + name: 'foo', + }, + ], + name: 'boo', + }) +}) + +test('_SplitNameOrModifier', () => { + expectTypeOf<_SplitNameOrModifier<'foo'>>().toEqualTypeOf<{ + readonly name: 'foo' + }>() + expectTypeOf< + _SplitNameOrModifier<'indexed foo', { modifier: 'indexed' }> + >().toEqualTypeOf<{ + readonly name: 'foo' + readonly indexed: true + }>() + expectTypeOf< + _SplitNameOrModifier<'calldata foo', { modifier: 'calldata' }> + >().toEqualTypeOf<{ + readonly name: 'foo' + }>() +}) + +test('_UnwrapNameOrModifier', () => { + expectTypeOf<_UnwrapNameOrModifier<'bar) foo'>>().toEqualTypeOf<{ + End: 'bar' + nameOrModifier: 'foo' + }>() + expectTypeOf<_UnwrapNameOrModifier<'baz) bar) foo'>>().toEqualTypeOf<{ + End: 'baz) bar' + nameOrModifier: 'foo' + }>() + expectTypeOf<_UnwrapNameOrModifier<'string) calldata foo'>>().toEqualTypeOf<{ + End: 'string' + nameOrModifier: 'calldata foo' + }>() +}) diff --git a/src/core/internal/human-readable/types/utils.ts b/src/core/internal/human-readable/types/utils.ts new file mode 100644 index 00000000..2bd39c75 --- /dev/null +++ b/src/core/internal/human-readable/types/utils.ts @@ -0,0 +1,390 @@ +import type { + AbiParameter, + AbiStateMutability, + AbiType, + SolidityFixedArrayRange, +} from 'abitype' +import type { ResolvedRegister } from 'abitype' +import type { Error, IsUnknown, Merge, Pretty, Trim } from '../types.js' +import type { + ErrorSignature, + EventModifier, + EventSignature, + FallbackSignature, + FunctionModifier, + FunctionSignature, + IsConstructorSignature, + IsErrorSignature, + IsEventSignature, + IsFunctionSignature, + Modifier, + ReceiveSignature, + Scope, + ValidateName, +} from './signatures.js' +import type { StructLookup } from './structs.js' + +export type ParseSignature< + signature extends string, + structs extends StructLookup | unknown = unknown, +> = + | (IsErrorSignature extends true + ? signature extends ErrorSignature + ? { + readonly name: name + readonly type: 'error' + readonly inputs: ParseAbiParameters< + SplitParameters, + { structs: structs } + > + } + : never + : never) + | (IsEventSignature extends true + ? signature extends EventSignature + ? { + readonly name: name + readonly type: 'event' + readonly inputs: ParseAbiParameters< + SplitParameters, + { modifier: EventModifier; structs: structs } + > + } + : never + : never) + | (IsFunctionSignature extends true + ? signature extends FunctionSignature + ? { + readonly name: name + readonly type: 'function' + readonly stateMutability: _ParseFunctionParametersAndStateMutability['StateMutability'] + readonly inputs: ParseAbiParameters< + SplitParameters< + _ParseFunctionParametersAndStateMutability['Inputs'] + >, + { modifier: FunctionModifier; structs: structs } + > + readonly outputs: tail extends + | `${string}returns (${infer returns})` + | `${string}returns(${infer returns})` + ? ParseAbiParameters< + SplitParameters, + { modifier: FunctionModifier; structs: structs } + > + : readonly [] + } + : never + : never) + | (IsConstructorSignature extends true + ? { + readonly type: 'constructor' + readonly stateMutability: _ParseConstructorParametersAndStateMutability['StateMutability'] + readonly inputs: ParseAbiParameters< + SplitParameters< + _ParseConstructorParametersAndStateMutability['Inputs'] + >, + { structs: structs } + > + } + : never) + | (signature extends FallbackSignature + ? { + readonly type: 'fallback' + readonly stateMutability: stateMutability extends `${string}payable` + ? 'payable' + : 'nonpayable' + } + : never) + | (signature extends ReceiveSignature + ? { + readonly type: 'receive' + readonly stateMutability: 'payable' + } + : never) + +type ParseOptions = { + modifier?: Modifier + structs?: StructLookup | unknown +} +type DefaultParseOptions = object + +export type ParseAbiParameters< + signatures extends readonly string[], + options extends ParseOptions = DefaultParseOptions, +> = signatures extends [''] + ? readonly [] + : readonly [ + ...{ + [key in keyof signatures]: ParseAbiParameter + }, + ] + +export type ParseAbiParameter< + signature extends string, + options extends ParseOptions = DefaultParseOptions, +> = ( + signature extends `(${string})${string}` + ? _ParseTuple + : // Convert string to shallow AbiParameter (structs resolved yet) + // Check for `${Type} ${nameOrModifier}` format (e.g. `uint256 foo`, `uint256 indexed`, `uint256 indexed foo`) + signature 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 + : never + : // Must be `${Type}` format (e.g. `uint256`) + { readonly type: signature } +) extends infer shallowParameter extends AbiParameter & { + type: string + indexed?: boolean +} + ? // Resolve struct types + // Starting with plain struct types (e.g. `Foo`) + ( + shallowParameter['type'] extends keyof options['structs'] + ? { + readonly type: 'tuple' + readonly components: options['structs'][shallowParameter['type']] + } & (IsUnknown extends false + ? { readonly name: shallowParameter['name'] } + : object) & + (shallowParameter['indexed'] extends true + ? { readonly indexed: true } + : object) + : // Resolve tuple structs (e.g. `Foo[]`, `Foo[2]`, `Foo[][2]`, etc.) + shallowParameter['type'] extends `${infer type extends string & + keyof options['structs']}[${infer tail}]` + ? { + readonly type: `tuple[${tail}]` + readonly components: options['structs'][type] + } & (IsUnknown extends false + ? { readonly name: shallowParameter['name'] } + : object) & + (shallowParameter['indexed'] extends true + ? { readonly indexed: true } + : object) + : // Not a struct, just return + shallowParameter + ) extends infer Parameter extends AbiParameter & { + type: string + indexed?: boolean + } + ? Pretty<_ValidateAbiParameter> + : never + : never + +export type SplitParameters< + signature extends string, + result extends unknown[] = [], + current extends string = '', + depth extends readonly number[] = [], +> = signature extends '' + ? current extends '' + ? [...result] // empty string was passed in to `SplitParameters` + : depth['length'] extends 0 + ? [...result, Trim] + : Error<`Unbalanced parentheses. "${current}" has too many opening parentheses.`> + : signature extends `${infer char}${infer tail}` + ? char extends ',' + ? depth['length'] extends 0 + ? SplitParameters], ''> + : SplitParameters + : char extends '(' + ? SplitParameters + : char extends ')' + ? depth['length'] extends 0 + ? Error<`Unbalanced parentheses. "${current}" has too many closing parentheses.`> + : SplitParameters> + : SplitParameters + : [] +type Pop = type extends [...infer head, any] + ? head + : [] + +export type _ValidateAbiParameter = + // Validate `name` + ( + abiParameter extends { name: string } + ? ValidateName extends infer name + ? name extends abiParameter['name'] + ? abiParameter + : // Add `Error` as `name` + Merge + : never + : abiParameter + ) extends infer parameter + ? // Validate `type` against `AbiType` + ( + ResolvedRegister['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}]` | ''}` + ? Pretty< + Merge + > + : parameter2 + : never + : never + +export type _ParseFunctionParametersAndStateMutability< + signature extends string, +> = signature extends + | `${infer head}returns (${string})` + | `${infer head}returns(${string})` + ? _ParseFunctionParametersAndStateMutability> + : signature extends `function ${string}(${infer parameters})` + ? { Inputs: parameters; StateMutability: 'nonpayable' } + : signature extends `function ${string}(${infer parameters}) ${infer scopeOrStateMutability extends + | Scope + | AbiStateMutability + | `${Scope} ${AbiStateMutability}`}` + ? { + Inputs: parameters + StateMutability: _ParseStateMutability + } + : signature extends `function ${string}(${infer tail}` + ? _UnwrapNameOrModifier extends { + nameOrModifier: infer scopeOrStateMutability extends string + End: infer parameters + } + ? { + Inputs: parameters + StateMutability: _ParseStateMutability + } + : never + : never + +type _ParseStateMutability = + signature extends `${Scope} ${infer stateMutability extends AbiStateMutability}` + ? stateMutability + : signature extends AbiStateMutability + ? signature + : 'nonpayable' + +type _ParseConstructorParametersAndStateMutability = + signature extends `constructor(${infer parameters}) payable` + ? { Inputs: parameters; StateMutability: 'payable' } + : signature extends `constructor(${infer parameters})` + ? { Inputs: parameters; StateMutability: 'nonpayable' } + : never + +export type _ParseTuple< + signature extends `(${string})${string}`, + options extends ParseOptions = DefaultParseOptions, +> = /** Tuples without name or modifier (e.g. `(string)`, `(string foo)`) */ +signature extends `(${infer parameters})` + ? { + readonly type: 'tuple' + readonly components: ParseAbiParameters< + SplitParameters, + Omit + > + } + : // Array or fixed-length array tuples (e.g. `(string)[]`, `(string)[5]`) + signature extends `(${infer head})[${'' | `${SolidityFixedArrayRange}`}]` + ? signature extends `(${head})[${infer size}]` + ? { + readonly type: `tuple[${size}]` + readonly components: ParseAbiParameters< + SplitParameters, + Omit + > + } + : never + : // Array or fixed-length array tuples with name and/or modifier attached (e.g. `(string)[] foo`, `(string)[5] foo`) + signature extends `(${infer parameters})[${ + | '' + | `${SolidityFixedArrayRange}`}] ${infer nameOrModifier}` + ? signature extends `(${parameters})[${infer size}] ${nameOrModifier}` + ? nameOrModifier extends `${string}) ${string}` + ? _UnwrapNameOrModifier extends infer parts extends { + nameOrModifier: string + End: string + } + ? { + readonly type: 'tuple' + readonly components: ParseAbiParameters< + SplitParameters<`${parameters})[${size}] ${parts['End']}`>, + Omit + > + } & _SplitNameOrModifier + : never + : { + readonly type: `tuple[${size}]` + readonly components: ParseAbiParameters< + SplitParameters, + Omit + > + } & _SplitNameOrModifier + : never + : // Tuples with name and/or modifier attached (e.g. `(string) foo`, `(string bar) foo`) + signature extends `(${infer parameters}) ${infer nameOrModifier}` + ? // Check that `nameOrModifier` didn't get matched to `baz) bar) foo` (e.g. `(((string) baz) bar) foo`) + nameOrModifier extends `${string}) ${string}` + ? _UnwrapNameOrModifier extends infer parts extends { + nameOrModifier: string + End: string + } + ? { + readonly type: 'tuple' + readonly components: ParseAbiParameters< + SplitParameters<`${parameters}) ${parts['End']}`>, + Omit + > + } & _SplitNameOrModifier + : never + : { + readonly type: 'tuple' + readonly components: ParseAbiParameters< + SplitParameters, + Omit + > + } & _SplitNameOrModifier + : never + +// Split name and modifier (e.g. `indexed foo` => `{ name: 'foo', indexed: true }`) +export type _SplitNameOrModifier< + signature extends string, + options extends ParseOptions = DefaultParseOptions, +> = Trim extends infer trimmed + ? options extends { modifier: Modifier } + ? // TODO: Check that modifier is allowed + trimmed extends `${infer mod extends options['modifier']} ${infer name}` + ? Pretty< + { readonly name: Trim } & (mod extends 'indexed' + ? { readonly indexed: true } + : object) + > + : trimmed extends options['modifier'] + ? trimmed extends 'indexed' + ? { readonly indexed: true } + : object + : { readonly name: trimmed } + : { readonly name: trimmed } + : never + +// `baz) bar) foo` (e.g. `(((string) baz) bar) foo`) +export type _UnwrapNameOrModifier< + signature extends string, + current extends string = '', +> = signature extends `${infer head}) ${infer tail}` + ? _UnwrapNameOrModifier< + tail, + `${current}${current extends '' ? '' : ') '}${head}` + > + : { End: Trim; nameOrModifier: Trim } diff --git a/src/index.ts b/src/index.ts index c28fc41b..bdc30201 100644 --- a/src/index.ts +++ b/src/index.ts @@ -455,6 +455,44 @@ export * as AbiFunction from './core/AbiFunction.js' */ export * as AbiItem from './core/AbiItem.js' +/** + * Utilities & types for working with a single [ABI Parameter](https://docs.soliditylang.org/en/latest/abi-spec.html#types). + * + * @example + * ### Instantiating Human Readable ABI Parameters + * + * A Human Readable ABI Parameter can be instantiated by using {@link ox#AbiParameter.(from:function)}: + * + * ```ts twoslash + * import { AbiParameter } from 'ox' + * + * const parameter = AbiParameter.from('address spender') + * + * parameter + * //^? + * ``` + * + * @example + * ### Formatting ABI Parameters + * + * An ABI Parameter can be formatted into a human-readable ABI Parameter by using {@link ox#AbiParameter.(format:function)}: + * + * ```ts twoslash + * import { AbiParameter } from 'ox' + * + * const formatted = AbiParameter.format({ + * name: 'spender', + * type: 'address' + * }) + * + * formatted + * // ^? + * ``` + * + * @category ABI + */ +export * as AbiParameter from './core/AbiParameter.js' + /** * Utilities & types for encoding, decoding, and working with [ABI Parameters](https://docs.soliditylang.org/en/latest/abi-spec.html#types) * diff --git a/src/zod/RpcSchema.ts b/src/zod/RpcSchema.ts index 267401c4..1c49e8f5 100644 --- a/src/zod/RpcSchema.ts +++ b/src/zod/RpcSchema.ts @@ -88,7 +88,7 @@ export declare namespace from { { params: z.ZodMiniType; returns: z.ZodMiniType } > - /** The normalized `RpcSchema.Namespace` derived from a {@link from.Namespace}. */ + /** The normalized `RpcSchema.Namespace` derived from `from.Namespace`. */ type ReturnType = { [method in keyof namespace & string]: Item< method, @@ -167,7 +167,7 @@ function resolveItem(args: readonly unknown[]): [item: Item, value: unknown] { * Decodes (wire → native) the `params` for a method. Use on the receiving side * (e.g. a server) to coerce incoming wire params into their native * representation. Accepts either a namespace + method name, or a resolved - * `RpcSchema.Item` (from {@link parseItem}/{@link from}). + * `RpcSchema.Item` (from `parseItem`/`from`). * * @example * ```ts twoslash @@ -218,7 +218,7 @@ export function decodeParams(...args: readonly unknown[]): unknown { * Encodes (native → wire) the `params` for a method. Use on the sending side * (e.g. a client) to serialize native params into the wire shape a JSON-RPC * endpoint expects. Accepts either a namespace + method name, or a resolved - * `RpcSchema.Item` (from {@link parseItem}/{@link from}). + * `RpcSchema.Item` (from `parseItem`/`from`). * * @example * ```ts twoslash @@ -269,7 +269,7 @@ export function encodeParams(...args: readonly unknown[]): unknown { * Decodes (wire → native) the `returns` value for a method. Use on the * receiving side (e.g. a client) to coerce a wire result into its native * representation. Accepts either a namespace + method name, or a resolved - * `RpcSchema.Item` (from {@link parseItem}/{@link from}). + * `RpcSchema.Item` (from `parseItem`/`from`). * * @example * ```ts twoslash @@ -320,7 +320,7 @@ export function decodeReturns(...args: readonly unknown[]): unknown { * Encodes (native → wire) the `returns` value for a method. Use on the sending * side (e.g. a server) to serialize a native result into the wire shape. * Accepts either a namespace + method name, or a resolved `RpcSchema.Item` - * (from {@link parseItem}/{@link from}). + * (from `parseItem`/`from`). * * @example * ```ts twoslash From 2c71058ac818806cb8546277e9c1a599b147ed43 Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 22 Jun 2026 16:53:25 -0400 Subject: [PATCH 2/5] chore: bump vocs --- pnpm-lock.yaml | 12 +++---- site/package.json | 2 +- src/core/Abi.ts | 72 ---------------------------------------- src/core/AbiItem.ts | 72 ---------------------------------------- src/core/AbiParameter.ts | 48 --------------------------- 5 files changed, 7 insertions(+), 199 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0bdcc6c4..e7f2513d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,8 +331,8 @@ importers: specifier: ^4.3.0 version: 4.3.0 vocs: - specifier: 2.0.14 - version: 2.0.14(@types/node@24.10.2)(@types/react@19.2.16)(esbuild@0.27.7)(jiti@2.7.0)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.7)(rollup@4.61.1)(terser@5.31.5)(typescript@5.5.4)(waku@1.0.0-beta.0(@types/node@24.10.2)(esbuild@0.27.7)(jiti@2.7.0)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.7)(terser@5.31.5)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)) + specifier: 2.0.17 + version: 2.0.17(@types/node@24.10.2)(@types/react@19.2.16)(esbuild@0.27.7)(jiti@2.7.0)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.7)(rollup@4.61.1)(terser@5.31.5)(typescript@5.5.4)(waku@1.0.0-beta.0(@types/node@24.10.2)(esbuild@0.27.7)(jiti@2.7.0)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.7)(terser@5.31.5)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)) waku: specifier: 1.0.0-beta.0 version: 1.0.0-beta.0(@types/node@24.10.2)(esbuild@0.27.7)(jiti@2.7.0)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.7)(terser@5.31.5)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0) @@ -5405,15 +5405,15 @@ packages: vite: optional: true - vocs@2.0.14: - resolution: {integrity: sha512-gisU/6AXY8lZHuns3DcTd/ogw83aTD5ff8cGFc9pTuwW33oZGoYxyszQIQ5AECbTFr8iicQR4KyxzhKeum2v8Q==} + vocs@2.0.17: + resolution: {integrity: sha512-U4iHyLt6e8XR8TtMky6vmzIv5Sckc9y3QL4xrvbAGk4USUXRoS1GUkkQ3+lDCDLZ/2isO/IdAyLjIdupdtPobw==} hasBin: true peerDependencies: '@vocs/twoslash-rust': ^0.1.0 mermaid: ^11 react: ^19 react-dom: ^19 - waku: ^1.0.0-beta.0 + waku: ^1.0.0-beta.3 peerDependenciesMeta: '@vocs/twoslash-rust': optional: true @@ -11218,7 +11218,7 @@ snapshots: optionalDependencies: vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.10.2)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.31.5)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)' - vocs@2.0.14(@types/node@24.10.2)(@types/react@19.2.16)(esbuild@0.27.7)(jiti@2.7.0)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.7)(rollup@4.61.1)(terser@5.31.5)(typescript@5.5.4)(waku@1.0.0-beta.0(@types/node@24.10.2)(esbuild@0.27.7)(jiti@2.7.0)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.7)(terser@5.31.5)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)): + vocs@2.0.17(@types/node@24.10.2)(@types/react@19.2.16)(esbuild@0.27.7)(jiti@2.7.0)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.7)(rollup@4.61.1)(terser@5.31.5)(typescript@5.5.4)(waku@1.0.0-beta.0(@types/node@24.10.2)(esbuild@0.27.7)(jiti@2.7.0)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.7)(terser@5.31.5)(tsx@4.21.0)(typescript@5.5.4)(yaml@2.9.0)): dependencies: '@base-ui/react': 1.4.1(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@codesandbox/sandpack-react': 2.20.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) diff --git a/site/package.json b/site/package.json index f4b68b29..e4649769 100644 --- a/site/package.json +++ b/site/package.json @@ -15,7 +15,7 @@ "react-dom": "^19.2.7", "react-server-dom-webpack": "19.2.7", "tailwindcss": "^4.3.0", - "vocs": "2.0.14", + "vocs": "2.0.17", "waku": "1.0.0-beta.0" }, "devDependencies": { diff --git a/src/core/Abi.ts b/src/core/Abi.ts index 3485ef8e..d699aef5 100644 --- a/src/core/Abi.ts +++ b/src/core/Abi.ts @@ -45,30 +45,6 @@ export function format(abi: abi): format.ReturnType * * formatted * // ^? - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // * ``` * * @param abi - The ABI to format. @@ -124,30 +100,6 @@ export function from( * * abi * //^? - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // * ``` * * @example @@ -162,30 +114,6 @@ export function from( * * abi * //^? - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // * ``` * * @param abi - The ABI to parse. diff --git a/src/core/AbiItem.ts b/src/core/AbiItem.ts index 680abf19..c820a31f 100644 --- a/src/core/AbiItem.ts +++ b/src/core/AbiItem.ts @@ -135,30 +135,6 @@ export declare namespace format { * * abiItem * //^? - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // * ``` * * @example @@ -175,30 +151,6 @@ export declare namespace format { * * abiItem * //^? - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // * ``` * * @example @@ -214,30 +166,6 @@ export declare namespace format { * * abiItem * //^? - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // * ``` * * diff --git a/src/core/AbiParameter.ts b/src/core/AbiParameter.ts index d40fb77c..84c5feb2 100644 --- a/src/core/AbiParameter.ts +++ b/src/core/AbiParameter.ts @@ -33,18 +33,6 @@ export { InvalidParenthesisError } from './internal/human-readable/errors.js' * * formatted * // ^? - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // * ``` * * @param parameter - The ABI Parameter to format. @@ -80,18 +68,6 @@ export declare namespace format { * * parameter * //^? - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // * ``` * * @example @@ -104,18 +80,6 @@ export declare namespace format { * * parameter * //^? - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // * ``` * * @example @@ -131,18 +95,6 @@ export declare namespace format { * * parameter * //^? - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // - * // * ``` * * @param parameter - The ABI Parameter to parse. From 3a25b09184d3abb600c7a13d560f9ce57b9f4d56 Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 22 Jun 2026 17:28:43 -0400 Subject: [PATCH 3/5] chore: up --- site/src/pages/_root.css | 71 +++++++++++++++++++++++ site/src/pages/guides/abi.md | 108 ----------------------------------- 2 files changed, 71 insertions(+), 108 deletions(-) diff --git a/site/src/pages/_root.css b/site/src/pages/_root.css index 41ec1307..ab41ead7 100644 --- a/site/src/pages/_root.css +++ b/site/src/pages/_root.css @@ -269,6 +269,77 @@ body > div:has(> div > .twoslash-completion-list) { z-index: 50; } +/* + * Twoslash popup theme fix. + * + * Vocs renders hover/completion content through popup-specific surfaces, but + * ox's global dashed-border treatment is intentionally broader than Vocs's + * component layer. Keep dashed borders for page chrome and normal code blocks, + * while letting Twoslash popovers read as floating UI. + */ +.twoslash-popup-container, +.twoslash-completion-list { + background-color: var(--background-color-code); + border-color: var(--border-color-primary); + border-style: solid; + box-shadow: 0 12px 32px color-mix(in srgb, black 22%, transparent); +} + +.twoslash-popup-container pre, +.twoslash-popup-container code, +.twoslash-popup-container .line, +.twoslash-completion-list, +.twoslash-completion-list * { + border-style: solid; +} + +.twoslash-popup-container pre[data-v], +.twoslash-popup-container pre { + background-color: transparent; + border: 0; + border-radius: 0; + margin: 0; + padding: 0; +} + +.module-snippet .twoslash-popup-container pre { + padding-block: 0 !important; +} + +.twoslash-popup-container code { + background-color: transparent; +} + +.twoslash-query-persisted.twoslash-popup-container[data-v-twoslash-inline-popup], +.twoslash-query-persisted.twoslash-popup-container[data-v-twoslash-inline-popup] + [data-v-content], +.twoslash-query-persisted.twoslash-popup-container[data-v-twoslash-inline-popup] + [data-v-content] + > *, +.twoslash-query-persisted.twoslash-popup-container[data-v-twoslash-inline-popup] + pre, +.twoslash-query-persisted.twoslash-popup-container[data-v-twoslash-inline-popup] + code { + height: auto !important; + min-height: 0 !important; +} + +.twoslash-query-persisted.twoslash-popup-container[data-v-twoslash-inline-popup] { + top: calc(100% + 1px) !important; +} + +.twoslash-popup-container [data-v-content] { + padding: 0 !important; +} + +.twoslash-popup-container [data-v-content] > * { + padding-block: 0.25rem !important; +} + +.twoslash-popup-container [data-v-content] > *:not(:last-child) { + border-style: solid; +} + /* Content links: heading text color with an accent underline. Skip the blank layout (landing page) which styles its own anchors. */ [data-v-content] a:not([data-v-anchor]) { diff --git a/site/src/pages/guides/abi.md b/site/src/pages/guides/abi.md index 30c23a09..fdda655d 100644 --- a/site/src/pages/guides/abi.md +++ b/site/src/pages/guides/abi.md @@ -22,132 +22,24 @@ const abi = Abi.from([ ]) abi //^? -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// const item = AbiItem.from( 'function approve(address spender, uint256 amount) returns (bool)', ) item // ^? -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// const parameters = AbiParameters.from('address spender, uint256 amount') parameters // ^? -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// const parameter = AbiParameter.from('address spender') parameter // ^? -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// const formatted = Abi.format(abi) formatted // ^? -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// ``` Human-readable signatures support functions, events, errors, constructors, fallback functions, receive functions, structs, and ABI parameters. From 4e941751e911c9a04a6972dc19ad764c5766697e Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 22 Jun 2026 17:34:09 -0400 Subject: [PATCH 4/5] chore: up --- vite.config.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vite.config.ts b/vite.config.ts index efe9f399..7f6f6017 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -64,6 +64,14 @@ export default defineConfig({ 'tsdoc/syntax': 'off', }, }, + { + files: ['src/core/internal/**/*.ts'], + rules: { + 'jsdoc-js/require-jsdoc': 'off', + 'jsdoc-js/require-description': 'off', + 'jsdoc-js/require-example': 'off', + }, + }, { files: ['scripts/**', 'test/**'], rules: { From 97f8b3324f102d820b86746121d74c554e6aaef8 Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 22 Jun 2026 17:34:22 -0400 Subject: [PATCH 5/5] chore: format --- scripts/docgen/utils/model.ts | 3 +- src/core/AbiParameter.ts | 4 +- src/core/_test/AbiParameter.test.ts | 9 +- .../internal/human-readable/errors.test.ts | 35 ++-- .../human-readable/human-readable.bench-d.ts | 5 +- .../human-readable/runtime/signatures.test.ts | 15 +- .../human-readable/runtime/structs.test.ts | 5 +- .../human-readable/runtime/utils.test.ts | 20 +-- .../internal/human-readable/runtime/utils.ts | 12 +- src/core/internal/human-readable/types.ts | 30 ++-- .../human-readable/types/signatures.ts | 81 +++++++-- .../internal/human-readable/types/structs.ts | 28 +-- .../internal/human-readable/types/utils.ts | 164 +++++++++--------- 13 files changed, 229 insertions(+), 182 deletions(-) diff --git a/scripts/docgen/utils/model.ts b/scripts/docgen/utils/model.ts index 893e8268..37263c7b 100644 --- a/scripts/docgen/utils/model.ts +++ b/scripts/docgen/utils/model.ts @@ -113,7 +113,8 @@ export function createDataLookup( token.canonicalReference && // prevent duplicates apiItem.excerpt.tokens.findIndex( - (other) => other.canonicalReference === token.canonicalReference, + (other) => + other.canonicalReference === token.canonicalReference, ) === index, ) .map((token) => ({ diff --git a/src/core/AbiParameter.ts b/src/core/AbiParameter.ts index 84c5feb2..339244a7 100644 --- a/src/core/AbiParameter.ts +++ b/src/core/AbiParameter.ts @@ -38,7 +38,9 @@ export { InvalidParenthesisError } from './internal/human-readable/errors.js' * @param parameter - The ABI Parameter to format. * @returns The formatted ABI Parameter. */ -export function format( +export function format< + const parameter extends AbiParameter | AbiEventParameter, +>( parameter: parameter | AbiParameter | AbiEventParameter, ): format.ReturnType { return formatAbiParameter.formatAbiParameter(parameter as parameter) as never diff --git a/src/core/_test/AbiParameter.test.ts b/src/core/_test/AbiParameter.test.ts index 9fe1398d..9d173a08 100644 --- a/src/core/_test/AbiParameter.test.ts +++ b/src/core/_test/AbiParameter.test.ts @@ -3,9 +3,8 @@ import { describe, expect, test } from 'vp/test' describe('from', () => { test('json parameter', () => { - expect( - AbiParameter.from({ name: 'spender', type: 'address' }), - ).toMatchInlineSnapshot(` + expect(AbiParameter.from({ name: 'spender', type: 'address' })) + .toMatchInlineSnapshot(` { "name": "spender", "type": "address", @@ -64,9 +63,7 @@ describe('format', () => { name: 'foo', type: 'tuple', }), - ).toMatchInlineSnapshot( - `"(address spender, uint256 amount) foo"`, - ) + ).toMatchInlineSnapshot(`"(address spender, uint256 amount) foo"`) }) }) diff --git a/src/core/internal/human-readable/errors.test.ts b/src/core/internal/human-readable/errors.test.ts index ad884590..945165b9 100644 --- a/src/core/internal/human-readable/errors.test.ts +++ b/src/core/internal/human-readable/errors.test.ts @@ -18,9 +18,8 @@ import { } from './errors.js' test('InvalidAbiItemError', () => { - expect( - new InvalidAbiItemError({ signature: 'address' }), - ).toMatchInlineSnapshot(` + expect(new InvalidAbiItemError({ signature: 'address' })) + .toMatchInlineSnapshot(` [AbiItem.InvalidAbiItemError: Failed to parse ABI item. Details: parseAbiItem("address") @@ -45,9 +44,8 @@ test('UnknownSolidityTypeError', () => { }) test('InvalidAbiParamterError', () => { - expect( - new InvalidAbiParameterError({ param: 'address owner' }), - ).toMatchInlineSnapshot(` + expect(new InvalidAbiParameterError({ param: 'address owner' })) + .toMatchInlineSnapshot(` [AbiParameter.InvalidAbiParameterError: Failed to parse ABI parameter. Details: parseAbiParameter("address owner") @@ -56,9 +54,8 @@ test('InvalidAbiParamterError', () => { }) test('InvalidAbiParamtersError', () => { - expect( - new InvalidAbiParametersError({ params: 'address owner' }), - ).toMatchInlineSnapshot(` + expect(new InvalidAbiParametersError({ params: 'address owner' })) + .toMatchInlineSnapshot(` [AbiParameters.InvalidAbiParametersError: Failed to parse ABI parameters. Details: parseAbiParameters("address owner") @@ -213,9 +210,8 @@ test('InvalidSignatureError', () => { }) test('UnknownSignatureError', () => { - expect( - new UnknownSignatureError({ signature: 'invalid' }), - ).toMatchInlineSnapshot(` + expect(new UnknownSignatureError({ signature: 'invalid' })) + .toMatchInlineSnapshot(` [Abi.UnknownSignatureError: Unknown signature. Details: invalid] @@ -223,9 +219,8 @@ test('UnknownSignatureError', () => { }) test('InvalidStructSignatureError', () => { - expect( - new InvalidStructSignatureError({ signature: 'struct Foo{}' }), - ).toMatchInlineSnapshot(` + expect(new InvalidStructSignatureError({ signature: 'struct Foo{}' })) + .toMatchInlineSnapshot(` [Abi.InvalidStructSignatureError: Invalid struct signature. No properties exist. @@ -235,9 +230,8 @@ test('InvalidStructSignatureError', () => { }) test('InvalidParenthesisError', () => { - expect( - new InvalidParenthesisError({ current: '(Foo))', depth: -1 }), - ).toMatchInlineSnapshot(` + expect(new InvalidParenthesisError({ current: '(Foo))', depth: -1 })) + .toMatchInlineSnapshot(` [HumanReadableAbi.InvalidParenthesisError: Unbalanced parentheses. "(Foo))" has too many closing parentheses. @@ -245,9 +239,8 @@ test('InvalidParenthesisError', () => { Details: Depth "-1"] `) - expect( - new InvalidParenthesisError({ current: '((Foo)', depth: 1 }), - ).toMatchInlineSnapshot(` + expect(new InvalidParenthesisError({ current: '((Foo)', depth: 1 })) + .toMatchInlineSnapshot(` [HumanReadableAbi.InvalidParenthesisError: Unbalanced parentheses. "((Foo)" has too many opening parentheses. diff --git a/src/core/internal/human-readable/human-readable.bench-d.ts b/src/core/internal/human-readable/human-readable.bench-d.ts index 99c50d79..0f817343 100644 --- a/src/core/internal/human-readable/human-readable.bench-d.ts +++ b/src/core/internal/human-readable/human-readable.bench-d.ts @@ -42,9 +42,8 @@ describe('human-readable ABI type instantiations', () => { }) test('AbiParameters.from.ReturnType: nested tuple parameters', () => { - type Result = AbiParameters.from.ReturnType< - '(uint8 a, uint8[] b, (uint8 x, uint8 y)[] c) s, (uint x, uint y) t, uint256 a' - > + type Result = + AbiParameters.from.ReturnType<'(uint8 a, uint8[] b, (uint8 x, uint8 y)[] c) s, (uint x, uint y) t, uint256 a'> attest.instantiations([20_000, 'instantiations']) attest({} as Result) }) diff --git a/src/core/internal/human-readable/runtime/signatures.test.ts b/src/core/internal/human-readable/runtime/signatures.test.ts index 79a1a5ff..f8d3738a 100644 --- a/src/core/internal/human-readable/runtime/signatures.test.ts +++ b/src/core/internal/human-readable/runtime/signatures.test.ts @@ -58,9 +58,8 @@ test('execEventSignature', () => { "parameters": "string", } `) - expect( - execEventSignature('event Name(string indexed foo)'), - ).toMatchInlineSnapshot(` + expect(execEventSignature('event Name(string indexed foo)')) + .toMatchInlineSnapshot(` { "name": "Name", "parameters": "string indexed foo", @@ -111,9 +110,8 @@ test('execFunctionSignature', () => { "stateMutability": undefined, } `) - expect( - execFunctionSignature('function foo() view returns (uint256)'), - ).toMatchInlineSnapshot(` + expect(execFunctionSignature('function foo() view returns (uint256)')) + .toMatchInlineSnapshot(` { "name": "foo", "parameters": "", @@ -122,9 +120,8 @@ test('execFunctionSignature', () => { "stateMutability": "view", } `) - expect( - execFunctionSignature('function foo() view returns(uint256)'), - ).toMatchInlineSnapshot(` + expect(execFunctionSignature('function foo() view returns(uint256)')) + .toMatchInlineSnapshot(` { "name": "foo", "parameters": "", diff --git a/src/core/internal/human-readable/runtime/structs.test.ts b/src/core/internal/human-readable/runtime/structs.test.ts index 3f759dcc..c1d57b3a 100644 --- a/src/core/internal/human-readable/runtime/structs.test.ts +++ b/src/core/internal/human-readable/runtime/structs.test.ts @@ -183,9 +183,8 @@ test('no properties', () => { }) test('struct does not exist when resolving', () => { - expect(() => - parseStructs(['struct Foo { Bar bar; }']), - ).toThrowErrorMatchingInlineSnapshot(` + expect(() => parseStructs(['struct Foo { Bar bar; }'])) + .toThrowErrorMatchingInlineSnapshot(` [HumanReadableAbi.UnknownTypeError: Unknown type. Type "Bar" is not a valid ABI type. Perhaps you forgot to include a struct signature?] diff --git a/src/core/internal/human-readable/runtime/utils.test.ts b/src/core/internal/human-readable/runtime/utils.test.ts index 2d2e02b6..4123efad 100644 --- a/src/core/internal/human-readable/runtime/utils.test.ts +++ b/src/core/internal/human-readable/runtime/utils.test.ts @@ -238,9 +238,8 @@ test('empty string', () => { }) test('Invalid solidity type', () => { - expect(() => - parseAbiParameter('strings'), - ).toThrowErrorMatchingInlineSnapshot(` + expect(() => parseAbiParameter('strings')) + .toThrowErrorMatchingInlineSnapshot(` [HumanReadableAbi.UnknownSolidityTypeError: Unknown type. Type "strings" is not a valid ABI type.] @@ -248,9 +247,8 @@ test('Invalid solidity type', () => { }) test('Invalid solidity type in tuple', () => { - expect(() => - parseAbiParameter('(strings)'), - ).toThrowErrorMatchingInlineSnapshot(` + expect(() => parseAbiParameter('(strings)')) + .toThrowErrorMatchingInlineSnapshot(` [HumanReadableAbi.UnknownSolidityTypeError: Unknown type. Type "strings" is not a valid ABI type.] @@ -258,9 +256,8 @@ test('Invalid solidity type in tuple', () => { }) test('Invalid solidity type in nested tuple', () => { - expect(() => - parseAbiParameter('((strings))'), - ).toThrowErrorMatchingInlineSnapshot(` + expect(() => parseAbiParameter('((strings))')) + .toThrowErrorMatchingInlineSnapshot(` [HumanReadableAbi.UnknownSolidityTypeError: Unknown type. Type "strings" is not a valid ABI type.] @@ -268,9 +265,8 @@ test('Invalid solidity type in nested tuple', () => { }) test('Struct type without context', () => { - expect(() => - parseAbiParameter('Demo demo'), - ).toThrowErrorMatchingInlineSnapshot(` + expect(() => parseAbiParameter('Demo demo')) + .toThrowErrorMatchingInlineSnapshot(` [HumanReadableAbi.UnknownSolidityTypeError: Unknown type. Type "Demo" is not a valid ABI type.] diff --git a/src/core/internal/human-readable/runtime/utils.ts b/src/core/internal/human-readable/runtime/utils.ts index 7eea9d0b..0f738952 100644 --- a/src/core/internal/human-readable/runtime/utils.ts +++ b/src/core/internal/human-readable/runtime/utils.ts @@ -6,12 +6,7 @@ import type { SolidityString, SolidityTuple, } from 'abitype' -import { - bytesRegex, - execTyped, - integerRegex, - isTupleRegex, -} from '../regex.js' +import { bytesRegex, execTyped, integerRegex, isTupleRegex } from '../regex.js' import { UnknownSolidityTypeError } from '../errors.js' import { InvalidFunctionModifierError, @@ -19,10 +14,7 @@ import { InvalidParameterError, SolidityProtectedKeywordError, } from '../errors.js' -import { - InvalidSignatureError, - UnknownSignatureError, -} from '../errors.js' +import { InvalidSignatureError, UnknownSignatureError } from '../errors.js' import { InvalidParenthesisError } from '../errors.js' import type { FunctionModifier, Modifier } from '../types/signatures.js' import type { StructLookup } from '../types/structs.js' diff --git a/src/core/internal/human-readable/types.ts b/src/core/internal/human-readable/types.ts index 872715b0..5d887c3b 100644 --- a/src/core/internal/human-readable/types.ts +++ b/src/core/internal/human-readable/types.ts @@ -2,7 +2,8 @@ export type Error = messages extends string ? [`Error: ${messages}`] : { - [key in keyof messages]: messages[key] extends infer message extends string + [key in keyof messages]: messages[key] extends infer message extends + string ? `Error: ${message}` : never } @@ -12,21 +13,25 @@ export type Filter< items extends readonly unknown[], item, acc extends readonly unknown[] = [], -> = items extends readonly [infer head, ...infer tail extends readonly unknown[]] +> = items extends readonly [ + infer head, + ...infer tail extends readonly unknown[], +] ? [head] extends [item] ? Filter : Filter : readonly [...acc] /** @internal */ -export type IsNarrowable = IsUnknown extends true - ? false - : IsNever< - (type extends type2 ? true : false) & - (type2 extends type ? false : true) - > extends true +export type IsNarrowable = + IsUnknown extends true ? false - : true + : IsNever< + (type extends type2 ? true : false) & + (type2 extends type ? false : true) + > extends true + ? false + : true /** @internal */ export type IsNever = [type] extends [never] ? true : false @@ -58,6 +63,7 @@ export type Trim = TrimLeft< type TrimLeft = t extends `${chars}${infer tail}` ? TrimLeft : t -type TrimRight = t extends `${infer head}${chars}` - ? TrimRight - : t +type TrimRight< + t, + chars extends string = ' ', +> = t extends `${infer head}${chars}` ? TrimRight : t diff --git a/src/core/internal/human-readable/types/signatures.ts b/src/core/internal/human-readable/types/signatures.ts index 16c8b30f..62fd1f99 100644 --- a/src/core/internal/human-readable/types/signatures.ts +++ b/src/core/internal/human-readable/types/signatures.ts @@ -101,13 +101,14 @@ export type IsSignature = export type Signature< string1 extends string, string2 extends string | unknown = unknown, -> = IsSignature extends true - ? string1 - : string extends string1 // if exactly `string` (not narrowed), then pass through as valid +> = + IsSignature extends true ? string1 - : Error<`Signature "${string1}" is invalid${string2 extends string - ? ` at position ${string2}` - : ''}.`> + : string extends string1 // if exactly `string` (not narrowed), then pass through as valid + ? string1 + : Error<`Signature "${string1}" is invalid${string2 extends string + ? ` at position ${string2}` + : ''}.`> export type Signatures = { [key in keyof signatures]: Signature @@ -226,13 +227,73 @@ export type IsValidCharacter = // biome-ignore format: no formatting type ValidCharacters = // uppercase letters - | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' + | 'A' + | 'B' + | 'C' + | 'D' + | 'E' + | 'F' + | 'G' + | 'H' + | 'I' + | 'J' + | 'K' + | 'L' + | 'M' + | 'N' + | 'O' + | 'P' + | 'Q' + | 'R' + | 'S' + | 'T' + | 'U' + | 'V' + | 'W' + | 'X' + | 'Y' + | 'Z' // lowercase letters - | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' + | 'a' + | 'b' + | 'c' + | 'd' + | 'e' + | 'f' + | 'g' + | 'h' + | 'i' + | 'j' + | 'k' + | 'l' + | 'm' + | 'n' + | 'o' + | 'p' + | 'q' + | 'r' + | 's' + | 't' + | 'u' + | 'v' + | 'w' + | 'x' + | 'y' + | 'z' // numbers - | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' + | '0' + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9' // special characters - | '_' | '$' + | '_' + | '$' // Template string inference can absorb `returns`: // type Result = `function foo(string) return s (uint256)` extends `function ${string}(${infer Parameters})` ? Parameters : never diff --git a/src/core/internal/human-readable/types/structs.ts b/src/core/internal/human-readable/types/structs.ts index 1f27984c..a9b86a9d 100644 --- a/src/core/internal/human-readable/types/structs.ts +++ b/src/core/internal/human-readable/types/structs.ts @@ -30,12 +30,13 @@ export type ParseStructs = export type ParseStruct< signature extends string, structs extends StructLookup | unknown = unknown, -> = signature extends StructSignature - ? { - readonly name: Trim - readonly components: ParseStructProperties - } - : never +> = + signature extends StructSignature + ? { + readonly name: Trim + readonly components: ParseStructProperties + } + : never export type ResolveStructs< abiParameters extends readonly (AbiParameter & { type: string })[], @@ -77,10 +78,11 @@ export type ParseStructProperties< signature extends string, structs extends StructLookup | unknown = unknown, result extends any[] = [], -> = Trim extends `${infer head};${infer tail}` - ? ParseStructProperties< - tail, - structs, - [...result, ParseAbiParameter] - > - : result +> = + Trim extends `${infer head};${infer tail}` + ? ParseStructProperties< + tail, + structs, + [...result, ParseAbiParameter] + > + : result diff --git a/src/core/internal/human-readable/types/utils.ts b/src/core/internal/human-readable/types/utils.ts index 2bd39c75..3fd84696 100644 --- a/src/core/internal/human-readable/types/utils.ts +++ b/src/core/internal/human-readable/types/utils.ts @@ -250,9 +250,7 @@ export type _ParseFunctionParametersAndStateMutability< : signature extends `function ${string}(${infer parameters})` ? { Inputs: parameters; StateMutability: 'nonpayable' } : signature extends `function ${string}(${infer parameters}) ${infer scopeOrStateMutability extends - | Scope - | AbiStateMutability - | `${Scope} ${AbiStateMutability}`}` + Scope | AbiStateMutability | `${Scope} ${AbiStateMutability}`}` ? { Inputs: parameters StateMutability: _ParseStateMutability @@ -286,97 +284,101 @@ type _ParseConstructorParametersAndStateMutability = export type _ParseTuple< signature extends `(${string})${string}`, options extends ParseOptions = DefaultParseOptions, -> = /** Tuples without name or modifier (e.g. `(string)`, `(string foo)`) */ -signature extends `(${infer parameters})` - ? { - readonly type: 'tuple' - readonly components: ParseAbiParameters< - SplitParameters, - Omit - > - } - : // Array or fixed-length array tuples (e.g. `(string)[]`, `(string)[5]`) - signature extends `(${infer head})[${'' | `${SolidityFixedArrayRange}`}]` - ? signature extends `(${head})[${infer size}]` - ? { - readonly type: `tuple[${size}]` - readonly components: ParseAbiParameters< - SplitParameters, - Omit - > - } - : never - : // Array or fixed-length array tuples with name and/or modifier attached (e.g. `(string)[] foo`, `(string)[5] foo`) - signature extends `(${infer parameters})[${ - | '' - | `${SolidityFixedArrayRange}`}] ${infer nameOrModifier}` - ? signature extends `(${parameters})[${infer size}] ${nameOrModifier}` - ? nameOrModifier extends `${string}) ${string}` - ? _UnwrapNameOrModifier extends infer parts extends { - nameOrModifier: string - End: string - } - ? { - readonly type: 'tuple' +> = + /** Tuples without name or modifier (e.g. `(string)`, `(string foo)`) */ + signature extends `(${infer parameters})` + ? { + readonly type: 'tuple' + readonly components: ParseAbiParameters< + SplitParameters, + Omit + > + } + : // Array or fixed-length array tuples (e.g. `(string)[]`, `(string)[5]`) + signature extends `(${infer head})[${'' | `${SolidityFixedArrayRange}`}]` + ? signature extends `(${head})[${infer size}]` + ? { + readonly type: `tuple[${size}]` + readonly components: ParseAbiParameters< + SplitParameters, + Omit + > + } + : never + : // Array or fixed-length array tuples with name and/or modifier attached (e.g. `(string)[] foo`, `(string)[5] foo`) + signature extends `(${infer parameters})[${ + | '' + | `${SolidityFixedArrayRange}`}] ${infer nameOrModifier}` + ? signature extends `(${parameters})[${infer size}] ${nameOrModifier}` + ? nameOrModifier extends `${string}) ${string}` + ? _UnwrapNameOrModifier extends infer parts extends + { + nameOrModifier: string + End: string + } + ? { + readonly type: 'tuple' + readonly components: ParseAbiParameters< + SplitParameters<`${parameters})[${size}] ${parts['End']}`>, + Omit + > + } & _SplitNameOrModifier + : never + : { + readonly type: `tuple[${size}]` readonly components: ParseAbiParameters< - SplitParameters<`${parameters})[${size}] ${parts['End']}`>, + SplitParameters, Omit > - } & _SplitNameOrModifier - : never - : { - readonly type: `tuple[${size}]` - readonly components: ParseAbiParameters< - SplitParameters, - Omit - > - } & _SplitNameOrModifier - : never - : // Tuples with name and/or modifier attached (e.g. `(string) foo`, `(string bar) foo`) - signature extends `(${infer parameters}) ${infer nameOrModifier}` - ? // Check that `nameOrModifier` didn't get matched to `baz) bar) foo` (e.g. `(((string) baz) bar) foo`) - nameOrModifier extends `${string}) ${string}` - ? _UnwrapNameOrModifier extends infer parts extends { - nameOrModifier: string - End: string - } - ? { + } & _SplitNameOrModifier + : never + : // Tuples with name and/or modifier attached (e.g. `(string) foo`, `(string bar) foo`) + signature extends `(${infer parameters}) ${infer nameOrModifier}` + ? // Check that `nameOrModifier` didn't get matched to `baz) bar) foo` (e.g. `(((string) baz) bar) foo`) + nameOrModifier extends `${string}) ${string}` + ? _UnwrapNameOrModifier extends infer parts extends + { + nameOrModifier: string + End: string + } + ? { + readonly type: 'tuple' + readonly components: ParseAbiParameters< + SplitParameters<`${parameters}) ${parts['End']}`>, + Omit + > + } & _SplitNameOrModifier + : never + : { readonly type: 'tuple' readonly components: ParseAbiParameters< - SplitParameters<`${parameters}) ${parts['End']}`>, + SplitParameters, Omit > - } & _SplitNameOrModifier - : never - : { - readonly type: 'tuple' - readonly components: ParseAbiParameters< - SplitParameters, - Omit - > - } & _SplitNameOrModifier - : never + } & _SplitNameOrModifier + : never // Split name and modifier (e.g. `indexed foo` => `{ name: 'foo', indexed: true }`) export type _SplitNameOrModifier< signature extends string, options extends ParseOptions = DefaultParseOptions, -> = Trim extends infer trimmed - ? options extends { modifier: Modifier } - ? // TODO: Check that modifier is allowed - trimmed extends `${infer mod extends options['modifier']} ${infer name}` - ? Pretty< - { readonly name: Trim } & (mod extends 'indexed' +> = + Trim extends infer trimmed + ? options extends { modifier: Modifier } + ? // TODO: Check that modifier is allowed + trimmed extends `${infer mod extends options['modifier']} ${infer name}` + ? Pretty< + { readonly name: Trim } & (mod extends 'indexed' + ? { readonly indexed: true } + : object) + > + : trimmed extends options['modifier'] + ? trimmed extends 'indexed' ? { readonly indexed: true } - : object) - > - : trimmed extends options['modifier'] - ? trimmed extends 'indexed' - ? { readonly indexed: true } - : object - : { readonly name: trimmed } - : { readonly name: trimmed } - : never + : object + : { readonly name: trimmed } + : { readonly name: trimmed } + : never // `baz) bar) foo` (e.g. `(((string) baz) bar) foo`) export type _UnwrapNameOrModifier<