Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/abi-human-readable-ox.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 6 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion scripts/docgen/utils/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down
2 changes: 1 addition & 1 deletion site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
71 changes: 71 additions & 0 deletions site/src/pages/_root.css
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down
56 changes: 56 additions & 0 deletions site/src/pages/guides/abi.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,62 @@ 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.
Expand Down
30 changes: 30 additions & 0 deletions src/core/Abi.bench.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
17 changes: 13 additions & 4 deletions src/core/Abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<const abi extends Abi>(abi: abi): format.ReturnType<abi>
/**
Expand Down Expand Up @@ -44,12 +53,12 @@ export function format<const abi extends Abi>(abi: abi): format.ReturnType<abi>
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<abi extends Abi | readonly unknown[] = Abi> =
abitype.FormatAbi<abi>
formatAbi.FormatAbi<abi>

type ErrorType = Errors.GlobalErrorType
}
Expand Down Expand Up @@ -113,14 +122,14 @@ export function from<const abi extends Abi | readonly string[]>(
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
> = abi extends readonly string[] ? parseAbi.ParseAbi<abi> : abi

type ErrorType = Errors.GlobalErrorType
}
5 changes: 3 additions & 2 deletions src/core/AbiConstructor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -316,12 +317,12 @@ export function format<const abiConstructor extends AbiConstructor>(
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<abiConstructor extends AbiConstructor = AbiConstructor> =
abitype.FormatAbiItem<abiConstructor>
formatAbiItem.FormatAbiItem<abiConstructor>

type ErrorType = Errors.GlobalErrorType
}
Expand Down
8 changes: 6 additions & 2 deletions src/core/AbiError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -510,11 +511,14 @@ export declare namespace encode {
*/
export function format<const abiError extends AbiError>(
abiError: abiError | AbiError,
): abitype.FormatAbiItem<abiError> {
return abitype.formatAbiItem(abiError) as never
): format.ReturnType<abiError> {
return formatAbiItem.formatAbiItem(abiError) as never
}

export declare namespace format {
type ReturnType<abiError extends AbiError = AbiError> =
formatAbiItem.FormatAbiItem<abiError>

type ErrorType = Errors.GlobalErrorType
}

Expand Down
8 changes: 6 additions & 2 deletions src/core/AbiEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -1334,11 +1335,14 @@ export declare namespace encode {
*/
export function format<const abiEvent extends AbiEvent>(
abiEvent: abiEvent | AbiEvent,
): abitype.FormatAbiItem<abiEvent> {
return abitype.formatAbiItem(abiEvent) as never
): format.ReturnType<abiEvent> {
return formatAbiItem.formatAbiItem(abiEvent) as never
}

export declare namespace format {
type ReturnType<abiEvent extends AbiEvent = AbiEvent> =
formatAbiItem.FormatAbiItem<abiEvent>

type ErrorType = Errors.GlobalErrorType
}

Expand Down
8 changes: 6 additions & 2 deletions src/core/AbiFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -864,11 +865,14 @@ export declare namespace encodeResult {
*/
export function format<const abiFunction extends AbiFunction>(
abiFunction: abiFunction | AbiFunction,
): abitype.FormatAbiItem<abiFunction> {
return abitype.formatAbiItem(abiFunction) as never
): format.ReturnType<abiFunction> {
return formatAbiItem.formatAbiItem(abiFunction) as never
}

export declare namespace format {
type ReturnType<abiFunction extends AbiFunction = AbiFunction> =
formatAbiItem.FormatAbiItem<abiFunction>

type ErrorType = Errors.GlobalErrorType
}

Expand Down
Loading
Loading