Standalone ABI encoding, decoding, and TypeScript inference for EVM — zero dependencies.
Encode function calls, decode results, parse events, decode errors, and compute selectors without importing ethers, viem, or web3. One package, 24 KB, works everywhere.
- Full ABI codec — encodeFunctionData, decodeFunctionResult, decodeFunctionData
- Event support — encodeEventTopics, decodeEventLog with indexed parameter handling
- Error decoding — Error(string), Panic(uint256), and custom errors
- encodePacked — Solidity
abi.encodePackedequivalent - Human-readable ABI — parseAbi / formatAbi
- TypeScript inference — function names, input/output types inferred from ABI
- Selector caching — keccak256 computed once per signature, cached for subsequent calls
- keccak256 built-in — pure implementation, no crypto dependencies
- Zero dependencies — ~24 KB bundled, ESM + CJS
- Works everywhere — Node.js, Deno, Bun, browsers
npm install @pulsadev/abi-utils
# or
pnpm add @pulsadev/abi-utils
# or
yarn add @pulsadev/abi-utilsimport {
encodeFunctionData,
decodeFunctionResult,
parseAbi,
} from '@pulsadev/abi-utils'
// Define ABI (JSON or human-readable)
const abi = parseAbi([
'function balanceOf(address account) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
])
// Encode calldata
const data = encodeFunctionData(abi, 'balanceOf', [
'0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
])
// '0x70a08231000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045'
// Decode result
const result = decodeFunctionResult(abi, 'balanceOf', returnData)
console.log(result.values[0]) // bigintimport {
encodeFunctionData,
decodeFunctionResult,
decodeFunctionData,
getSelector,
} from '@pulsadev/abi-utils'
// Encode a function call
const calldata = encodeFunctionData(abi, 'transfer', [to, amount])
// Decode a return value
const result = decodeFunctionResult(abi, 'transfer', returnData)
console.log(result.values[0]) // bool
console.log(result.named) // { '': true }
// Decode calldata back to function name + args
const { functionName, args } = decodeFunctionData(abi, calldata)
// Get function selector
const selector = getSelector(abi, 'transfer') // '0xa9059cbb'import {
encodeEventTopics,
decodeEventLog,
getEventTopic,
} from '@pulsadev/abi-utils'
// Get event topic hash
const topic = getEventTopic(abi, 'Transfer')
// '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
// Encode topics for filtering
const topics = encodeEventTopics(abi, 'Transfer')
// Decode a log entry
const { eventName, args } = decodeEventLog(abi, log.topics, log.data)
console.log(args.from) // '0x...'
console.log(args.to) // '0x...'
console.log(args.value) // bigintimport { decodeError } from '@pulsadev/abi-utils'
// Decode revert data
const { errorName, args } = decodeError(abi, revertData)
// Built-in Error(string)
// → { errorName: 'Error', args: { reason: 'Insufficient balance' } }
// Built-in Panic(uint256)
// → { errorName: 'Panic', args: { code: 0x11n, reason: 'Arithmetic overflow/underflow' } }
// Custom errors
// → { errorName: 'InsufficientBalance', args: { account: '0x...', balance: 0n } }import { encodePacked } from '@pulsadev/abi-utils'
// Equivalent to Solidity's abi.encodePacked
const packed = encodePacked(
['address', 'uint256'],
['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 1000000n],
)import { encodeParameters, decodeParameters } from '@pulsadev/abi-utils'
// Encode raw parameters
const encoded = encodeParameters(
[{ type: 'address', name: 'to' }, { type: 'uint256', name: 'amount' }],
['0xd8dA...6045', 1000000n],
)
// Decode raw parameters
const values = decodeParameters(
[{ type: 'address', name: 'to' }, { type: 'uint256', name: 'amount' }],
encoded,
)import { parseAbi, formatAbi } from '@pulsadev/abi-utils'
// Parse human-readable signatures
const abi = parseAbi([
'function transfer(address to, uint256 amount) returns (bool)',
'event Transfer(address indexed from, address indexed to, uint256 value)',
'error InsufficientBalance(address account, uint256 balance)',
])
// Format JSON ABI back to human-readable
const signatures = formatAbi(abi)import { keccak256, selector, eventTopic } from '@pulsadev/abi-utils'
// Keccak-256 hash
const hash = keccak256('transfer(address,uint256)')
// Function selector (first 4 bytes)
const sel = selector('transfer(address,uint256)') // '0xa9059cbb'
// Event topic (full 32-byte hash)
const topic = eventTopic('Transfer(address,address,uint256)')Function names are autocompleted and validated at compile time when using as const ABI:
const abi = [
{
type: 'function',
name: 'transfer',
inputs: [
{ type: 'address', name: 'to' },
{ type: 'uint256', name: 'amount' },
],
outputs: [{ type: 'bool', name: '' }],
stateMutability: 'nonpayable',
},
] as const
// Function name is autocompleted — 'transfer' is the only valid option
encodeFunctionData(abi, 'transfer', [to, amount])
// Type error: 'foo' is not a valid function name
encodeFunctionData(abi, 'foo', []) // TS error
// Type utilities
type Names = ExtractFunctionNames<typeof abi> // 'transfer'
type Mapped = SolidityToTs<'uint256'> // bigint| Solidity Type | TypeScript Type | Encode | Decode | Packed |
|---|---|---|---|---|
address |
`0x${string}` |
✅ | ✅ | ✅ |
bool |
boolean |
✅ | ✅ | ✅ |
uint8—uint256 |
bigint |
✅ | ✅ | ✅ |
int8—int256 |
bigint |
✅ | ✅ | ✅ |
bytes1—bytes32 |
`0x${string}` |
✅ | ✅ | ✅ |
bytes |
`0x${string}` |
✅ | ✅ | ✅ |
string |
string |
✅ | ✅ | ✅ |
T[] |
T[] |
✅ | ✅ | — |
T[N] |
T[] |
✅ | ✅ | — |
tuple |
Record |
✅ | ✅ | — |
Benchmarked on a dedicated VPS (Dallas, 4 vCPU, 8 GB RAM). 10,000 iterations.
| Operation | ops/sec |
|---|---|
encodeFunctionData(transfer) |
507,803 |
encodeFunctionData(balanceOf) |
1,011,704 |
encodeFunctionData(totalSupply) |
2,834,397 |
decodeFunctionResult |
1,073,016 |
encodeParameters(address, uint256) |
1,036,642 |
encodePacked(address, uint256) |
1,560,759 |
parseAbi(3 signatures) |
315,376 |
Selectors are cached after first computation — subsequent calls skip keccak256 entirely.
| @pulsadev/abi-utils | ox (wevm) | abitype | ethers | |
|---|---|---|---|---|
| Runtime encode/decode | ✅ | ✅ | ❌ | ✅ |
| Standalone install | ✅ | ❌ 10 deps | ✅ | ❌ |
| ESM + CJS | ✅ | ❌ ESM only | ✅ | ✅ |
| Bundle size | 24 KB | large | 15 KB | large |
| encodePacked | ✅ | ✅ | ❌ | ✅ |
| Event decode | ✅ | ✅ | ❌ | ✅ |
| Error decode | ✅ | ✅ | ❌ | ✅ |
| TypeScript inference | ✅ | ✅ | ✅ | ❌ |
| Selector caching | ✅ | ❌ | — | ❌ |
| keccak256 built-in | ✅ | ❌ | ❌ | ❌ |
MIT © Yuto Nakamura