UNPKG

viem

Version:

TypeScript Interface for Ethereum

387 lines (327 loc) • 12.2 kB
import type { AbiParameter, AbiParametersToPrimitiveTypes } from 'abitype' import type { ByteArray, Hex } from '../../types/misc.js' import { AbiDecodingDataSizeTooSmallError, AbiDecodingZeroDataError, InvalidAbiDecodingTypeError, type InvalidAbiDecodingTypeErrorType, } from '../../errors/abi.js' import type { ErrorType } from '../../errors/utils.js' import { type ChecksumAddressErrorType, checksumAddress, } from '../address/getAddress.js' import { type CreateCursorErrorType, type Cursor, createCursor, } from '../cursor.js' import { type SizeErrorType, size } from '../data/size.js' import { type SliceBytesErrorType, sliceBytes } from '../data/slice.js' import { type TrimErrorType, trim } from '../data/trim.js' import { type BytesToBigIntErrorType, type BytesToBoolErrorType, type BytesToNumberErrorType, type BytesToStringErrorType, bytesToBigInt, bytesToBool, bytesToNumber, bytesToString, } from '../encoding/fromBytes.js' import { type HexToBytesErrorType, hexToBytes } from '../encoding/toBytes.js' import { type BytesToHexErrorType, bytesToHex } from '../encoding/toHex.js' import { getArrayComponents } from './encodeAbiParameters.js' export type DecodeAbiParametersReturnType< params extends readonly AbiParameter[] = readonly AbiParameter[], > = AbiParametersToPrimitiveTypes< params extends readonly AbiParameter[] ? params : AbiParameter[] > export type DecodeAbiParametersErrorType = | HexToBytesErrorType | BytesToHexErrorType | DecodeParameterErrorType | SizeErrorType | CreateCursorErrorType | ErrorType export function decodeAbiParameters< const params extends readonly AbiParameter[], >( params: params, data: ByteArray | Hex, ): DecodeAbiParametersReturnType<params> { const bytes = typeof data === 'string' ? hexToBytes(data) : data const cursor = createCursor(bytes) if (size(bytes) === 0 && params.length > 0) throw new AbiDecodingZeroDataError() if (size(data) && size(data) < 32) throw new AbiDecodingDataSizeTooSmallError({ data: typeof data === 'string' ? data : bytesToHex(data), params: params as readonly AbiParameter[], size: size(data), }) let consumed = 0 const values = [] for (let i = 0; i < params.length; ++i) { const param = params[i] cursor.setPosition(consumed) const [data, consumed_] = decodeParameter(cursor, param, { staticPosition: 0, }) consumed += consumed_ values.push(data) } return values as DecodeAbiParametersReturnType<params> } type DecodeParameterErrorType = | DecodeArrayErrorType | DecodeTupleErrorType | DecodeAddressErrorType | DecodeBoolErrorType | DecodeBytesErrorType | DecodeNumberErrorType | DecodeStringErrorType | InvalidAbiDecodingTypeErrorType function decodeParameter( cursor: Cursor, param: AbiParameter, { staticPosition }: { staticPosition: number }, ) { const arrayComponents = getArrayComponents(param.type) if (arrayComponents) { const [length, type] = arrayComponents return decodeArray(cursor, { ...param, type }, { length, staticPosition }) } if (param.type === 'tuple') return decodeTuple(cursor, param as TupleAbiParameter, { staticPosition }) if (param.type === 'address') return decodeAddress(cursor) if (param.type === 'bool') return decodeBool(cursor) if (param.type.startsWith('bytes')) return decodeBytes(cursor, param, { staticPosition }) if (param.type.startsWith('uint') || param.type.startsWith('int')) return decodeNumber(cursor, param) if (param.type === 'string') return decodeString(cursor, { staticPosition }) throw new InvalidAbiDecodingTypeError(param.type, { docsPath: '/docs/contract/decodeAbiParameters', }) } //////////////////////////////////////////////////////////////////// // Type Decoders const sizeOfLength = 32 const sizeOfOffset = 32 type DecodeAddressErrorType = | ChecksumAddressErrorType | BytesToHexErrorType | SliceBytesErrorType | ErrorType function decodeAddress(cursor: Cursor) { const value = cursor.readBytes(32) return [checksumAddress(bytesToHex(sliceBytes(value, -20))), 32] } type DecodeArrayErrorType = BytesToNumberErrorType | ErrorType function decodeArray( cursor: Cursor, param: AbiParameter, { length, staticPosition }: { length: number | null; staticPosition: number }, ) { // If the length of the array is not known in advance (dynamic array), // this means we will need to wonder off to the pointer and decode. if (!length) { // Dealing with a dynamic type, so get the offset of the array data. const offset = bytesToNumber(cursor.readBytes(sizeOfOffset)) // Start is the static position of current slot + offset. const start = staticPosition + offset const startOfData = start + sizeOfLength // Get the length of the array from the offset. cursor.setPosition(start) const length = bytesToNumber(cursor.readBytes(sizeOfLength)) // Check if the array has any dynamic children. const dynamicChild = hasDynamicChild(param) let consumed = 0 const value: unknown[] = [] for (let i = 0; i < length; ++i) { // If any of the children is dynamic, then all elements will be offset pointer, thus size of one slot (32 bytes). // Otherwise, elements will be the size of their encoding (consumed bytes). cursor.setPosition(startOfData + (dynamicChild ? i * 32 : consumed)) const [data, consumed_] = decodeParameter(cursor, param, { staticPosition: startOfData, }) consumed += consumed_ value.push(data) } // As we have gone wondering, restore to the original position + next slot. cursor.setPosition(staticPosition + 32) return [value, 32] } // If the length of the array is known in advance, // and the length of an element deeply nested in the array is not known, // we need to decode the offset of the array data. if (hasDynamicChild(param)) { // Dealing with dynamic types, so get the offset of the array data. const offset = bytesToNumber(cursor.readBytes(sizeOfOffset)) // Start is the static position of current slot + offset. const start = staticPosition + offset const value: unknown[] = [] for (let i = 0; i < length; ++i) { // Move cursor along to the next slot (next offset pointer). cursor.setPosition(start + i * 32) const [data] = decodeParameter(cursor, param, { staticPosition: start, }) value.push(data) } // As we have gone wondering, restore to the original position + next slot. cursor.setPosition(staticPosition + 32) return [value, 32] } // If the length of the array is known in advance and the array is deeply static, // then we can just decode each element in sequence. let consumed = 0 const value: unknown[] = [] for (let i = 0; i < length; ++i) { const [data, consumed_] = decodeParameter(cursor, param, { staticPosition: staticPosition + consumed, }) consumed += consumed_ value.push(data) } return [value, consumed] } type DecodeBoolErrorType = BytesToBoolErrorType | ErrorType function decodeBool(cursor: Cursor) { return [bytesToBool(cursor.readBytes(32), { size: 32 }), 32] } type DecodeBytesErrorType = | BytesToNumberErrorType | BytesToHexErrorType | ErrorType function decodeBytes( cursor: Cursor, param: AbiParameter, { staticPosition }: { staticPosition: number }, ) { const [_, size] = param.type.split('bytes') if (!size) { // Dealing with dynamic types, so get the offset of the bytes data. const offset = bytesToNumber(cursor.readBytes(32)) // Set position of the cursor to start of bytes data. cursor.setPosition(staticPosition + offset) const length = bytesToNumber(cursor.readBytes(32)) // If there is no length, we have zero data. if (length === 0) { // As we have gone wondering, restore to the original position + next slot. cursor.setPosition(staticPosition + 32) return ['0x', 32] } const data = cursor.readBytes(length) // As we have gone wondering, restore to the original position + next slot. cursor.setPosition(staticPosition + 32) return [bytesToHex(data), 32] } const value = bytesToHex(cursor.readBytes(Number.parseInt(size), 32)) return [value, 32] } type DecodeNumberErrorType = | BytesToNumberErrorType | BytesToBigIntErrorType | ErrorType function decodeNumber(cursor: Cursor, param: AbiParameter) { const signed = param.type.startsWith('int') const size = Number.parseInt(param.type.split('int')[1] || '256') const value = cursor.readBytes(32) return [ size > 48 ? bytesToBigInt(value, { signed }) : bytesToNumber(value, { signed }), 32, ] } type TupleAbiParameter = AbiParameter & { components: readonly AbiParameter[] } type DecodeTupleErrorType = BytesToNumberErrorType | ErrorType function decodeTuple( cursor: Cursor, param: TupleAbiParameter, { staticPosition }: { staticPosition: number }, ) { // Tuples can have unnamed components (i.e. they are arrays), so we must // determine whether the tuple is named or unnamed. In the case of a named // tuple, the value will be an object where each property is the name of the // component. In the case of an unnamed tuple, the value will be an array. const hasUnnamedChild = param.components.length === 0 || param.components.some(({ name }) => !name) // Initialize the value to an object or an array, depending on whether the // tuple is named or unnamed. const value: any = hasUnnamedChild ? [] : {} let consumed = 0 // If the tuple has a dynamic child, we must first decode the offset to the // tuple data. if (hasDynamicChild(param)) { // Dealing with dynamic types, so get the offset of the tuple data. const offset = bytesToNumber(cursor.readBytes(sizeOfOffset)) // Start is the static position of referencing slot + offset. const start = staticPosition + offset for (let i = 0; i < param.components.length; ++i) { const component = param.components[i] cursor.setPosition(start + consumed) const [data, consumed_] = decodeParameter(cursor, component, { staticPosition: start, }) consumed += consumed_ value[hasUnnamedChild ? i : component?.name!] = data } // As we have gone wondering, restore to the original position + next slot. cursor.setPosition(staticPosition + 32) return [value, 32] } // If the tuple has static children, we can just decode each component // in sequence. for (let i = 0; i < param.components.length; ++i) { const component = param.components[i] const [data, consumed_] = decodeParameter(cursor, component, { staticPosition, }) value[hasUnnamedChild ? i : component?.name!] = data consumed += consumed_ } return [value, consumed] } type DecodeStringErrorType = | BytesToNumberErrorType | BytesToStringErrorType | TrimErrorType | ErrorType function decodeString( cursor: Cursor, { staticPosition }: { staticPosition: number }, ) { // Get offset to start of string data. const offset = bytesToNumber(cursor.readBytes(32)) // Start is the static position of current slot + offset. const start = staticPosition + offset cursor.setPosition(start) const length = bytesToNumber(cursor.readBytes(32)) // If there is no length, we have zero data (empty string). if (length === 0) { cursor.setPosition(staticPosition + 32) return ['', 32] } const data = cursor.readBytes(length, 32) const value = bytesToString(trim(data)) // As we have gone wondering, restore to the original position + next slot. cursor.setPosition(staticPosition + 32) return [value, 32] } function hasDynamicChild(param: AbiParameter) { const { type } = param if (type === 'string') return true if (type === 'bytes') return true if (type.endsWith('[]')) return true if (type === 'tuple') return (param as any).components?.some(hasDynamicChild) const arrayComponents = getArrayComponents(param.type) if ( arrayComponents && hasDynamicChild({ ...param, type: arrayComponents[1] } as AbiParameter) ) return true return false }