ox
Version:
Ethereum Standard Library
345 lines (309 loc) • 10.3 kB
text/typescript
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 =
/^(?<type>[a-zA-Z$_][a-zA-Z0-9$_]*(?:\spayable)?)(?<array>(?:\[\d*?\])+?)?(?:\s(?<modifier>calldata|indexed|memory|storage{1}))?(?:\s(?<name>[a-zA-Z$_][a-zA-Z0-9$_]*))?$/
const abiParameterWithTupleRegex =
/^\((?<type>.+?)\)(?<array>(?:\[\d*?\])+?)?(?:\s(?<modifier>calldata|indexed|memory|storage{1}))?(?:\s(?<name>[a-zA-Z$_][a-zA-Z0-9$_]*))?$/
const dynamicIntegerRegex = /^u?int$/
type ParseOptions = {
modifiers?: Set<Modifier>
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<AbiType, SolidityTuple | SolidityArray> {
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<SolidityBytes, 'bytes'> | SolidityArray
> {
return isArray || type === 'bytes' || type === 'string' || type === 'tuple'
}