import type { AbiParameter, AbiParameterToPrimitiveType, AbiType, AbiTypeToPrimitiveType, ParseAbiParameter, } from 'abitype' import { parseAbiParameter } from 'abitype' import { type IsAddressErrorType, isAddress } from '../address/isAddress.js' import { type ConcatErrorType, concat } from '../data/concat.js' import { type PadHexErrorType, padHex } from '../data/pad.js' import { type SizeErrorType, size } from '../data/size.js' import { type SliceErrorType, slice } from '../data/slice.js' import { type BoolToHexErrorType, type NumberToHexErrorType, type StringToHexErrorType, boolToHex, numberToHex, stringToHex, } from '../encoding/toHex.js' import { AbiEncodingArrayLengthMismatchError, type AbiEncodingArrayLengthMismatchErrorType, AbiEncodingBytesSizeMismatchError, type AbiEncodingBytesSizeMismatchErrorType, AbiEncodingLengthMismatchError, type AbiEncodingLengthMismatchErrorType, InvalidAbiEncodingTypeError, type InvalidAbiEncodingTypeErrorType, InvalidArrayError, type InvalidArrayErrorType, } from '../errors/abi.js' import { InvalidAddressError, type InvalidAddressErrorType, } from '../errors/address.js' import { BaseError } from '../errors/base.js' import type { ErrorType } from '../errors/utils.js' import type { Hex } from '../types/data.js' import type { Prettify } from '../types/utils.js' /** @internal */ export type IsomorphicAbiParameter = AbiParameter | AbiType | (string & {}) /** @internal */ export type IsomorphicAbiParametersToPrimitiveTypes< types extends readonly IsomorphicAbiParameter[], > = Prettify<{ [key in keyof types]: types[key] extends AbiParameter ? AbiParameterToPrimitiveType : types[key] extends AbiType ? AbiParameterToPrimitiveType<{ type: types[key] }> : types[key] extends string | readonly string[] | readonly unknown[] ? AbiParameterToPrimitiveType> : never }> export type EncodeAbiParametersReturnType = Hex export type EncodeAbiParametersErrorType = | AbiEncodingLengthMismatchErrorType | PrepareParametersErrorType | EncodeParametersErrorType | ErrorType /** * @description Encodes a list of primitive values into an ABI-encoded hex value. * * - Docs: https://viem.sh/docs/abi/encodeAbiParameters#encodeabiparameters * * Generates ABI encoded data using the [ABI specification](https://docs.soliditylang.org/en/latest/abi-spec), given a set of ABI parameters (inputs/outputs) and their corresponding values. * * @parameter parameters - a set of ABI Parameters (parameters), that can be in the shape of the inputs or outputs attribute of an ABI Item. * @parameter values - a set of values (values) that correspond to the given parameters. * @example * ```typescript * import { Abi } from 'viem' * * const encodedData = Abi.encode( * [{ type: 'string' }, { type: 'uint' }, { type: 'bool' }], * ['wagmi', 420n, true] * ) * ``` * * You can also pass in Human Readable parameters with the `Abi.parse` utility. * * @example * ```typescript * import { Abi } from 'viem' * * const encodedData = Abi.encode( * Abi.parse('string x, uint y, bool z'), * ['wagmi', 420n, true] * ) * ``` */ export function encodeAbiParameters< const parameters extends | readonly IsomorphicAbiParameter[] | readonly unknown[], >( parameters: parameters, values: parameters extends readonly IsomorphicAbiParameter[] ? IsomorphicAbiParametersToPrimitiveTypes : never, ): EncodeAbiParametersReturnType { if (parameters.length !== values.length) throw new AbiEncodingLengthMismatchError({ expectedLength: parameters.length as number, givenLength: values.length as any, }) // Prepare the parameters to determine dynamic types to encode. const preparedParameters = prepareParameters({ parameters: parameters as readonly AbiParameter[], values: values as any, }) const data = encodeParameters(preparedParameters) if (data.length === 0) return '0x' return data } ///////////////////////////////////////////////////////////////// type PreparedParam = { dynamic: boolean; encoded: Hex } type TupleAbiParameter = AbiParameter & { components: readonly AbiParameter[] } type Tuple = AbiParameterToPrimitiveType type PrepareParametersErrorType = PrepareParameterErrorType | ErrorType function prepareParameters< const parameters extends readonly IsomorphicAbiParameter[], >({ parameters, values, }: { parameters: parameters values: parameters extends readonly IsomorphicAbiParameter[] ? IsomorphicAbiParametersToPrimitiveTypes : never }) { const preparedParameters: PreparedParam[] = [] for (let i = 0; i < parameters.length; i++) { preparedParameters.push( prepareParameter({ parameter: parameters[i], value: values[i] }), ) } return preparedParameters } type PrepareParameterErrorType = | EncodeAddressErrorType | EncodeArrayErrorType | EncodeBytesErrorType | EncodeBoolErrorType | EncodeNumberErrorType | EncodeStringErrorType | EncodeTupleErrorType | GetArrayComponentsErrorType | InvalidAbiEncodingTypeErrorType | ErrorType function prepareParameter({ parameter: parameter_, value, }: { parameter: parameter value: parameter extends AbiParameter ? AbiParameterToPrimitiveType : parameter extends AbiType ? AbiTypeToPrimitiveType : never }): PreparedParam { const parameter = ( typeof parameter_ === 'string' ? parseAbiParameter(parameter_ as string) : parameter_ ) as AbiParameter const arrayComponents = getArrayComponents(parameter.type) if (arrayComponents) { const [length, type] = arrayComponents return encodeArray(value, { length, parameter: { ...(typeof parameter === 'object' ? parameter : {}), type, }, }) } if (parameter.type === 'tuple') { return encodeTuple(value as unknown as Tuple, { parameter: parameter as TupleAbiParameter, }) } if (parameter.type === 'address') { return encodeAddress(value as unknown as Hex) } if (parameter.type === 'bool') { return encodeBool(value as unknown as boolean) } if (parameter.type.startsWith('uint') || parameter.type.startsWith('int')) { const signed = parameter.type.startsWith('int') return encodeNumber(value as unknown as number, { signed }) } if (parameter.type.startsWith('bytes')) { return encodeBytes(value as unknown as Hex, { type: parameter.type }) } if (parameter.type === 'string') { return encodeString(value as unknown as string) } throw new InvalidAbiEncodingTypeError(parameter.type, { docsPath: '/docs/contract/encodeAbiParameters', }) } ///////////////////////////////////////////////////////////////// type EncodeParametersErrorType = | NumberToHexErrorType | SizeErrorType | ErrorType function encodeParameters(preparedParameters: PreparedParam[]): Hex { // 1. Compute the size of the static part of the parameters. let staticSize = 0 for (let i = 0; i < preparedParameters.length; i++) { const { dynamic, encoded } = preparedParameters[i] if (dynamic) staticSize += 32 else staticSize += size(encoded) } // 2. Split the parameters into static and dynamic parts. const staticParameters: Hex[] = [] const dynamicParameters: Hex[] = [] let dynamicSize = 0 for (let i = 0; i < preparedParameters.length; i++) { const { dynamic, encoded } = preparedParameters[i] if (dynamic) { staticParameters.push(numberToHex(staticSize + dynamicSize, { size: 32 })) dynamicParameters.push(encoded) dynamicSize += size(encoded) } else { staticParameters.push(encoded) } } // 3. Concatenate static and dynamic parts. return concat([...staticParameters, ...dynamicParameters]) } ///////////////////////////////////////////////////////////////// type EncodeAddressErrorType = | InvalidAddressErrorType | IsAddressErrorType | ErrorType function encodeAddress(value: Hex): PreparedParam { if (!isAddress(value)) throw new InvalidAddressError({ address: value }) return { dynamic: false, encoded: padHex(value.toLowerCase() as Hex) } } type EncodeArrayErrorType = | AbiEncodingArrayLengthMismatchErrorType | ConcatErrorType | EncodeParametersErrorType | InvalidArrayErrorType | NumberToHexErrorType | ErrorType function encodeArray( value: AbiParameterToPrimitiveType, { length, parameter, }: { length: number | null parameter: parameter }, ): PreparedParam { const dynamic = length === null if (!Array.isArray(value)) throw new InvalidArrayError(value) if (!dynamic && value.length !== length) throw new AbiEncodingArrayLengthMismatchError({ expectedLength: length!, givenLength: value.length, type: `${parameter.type}[${length}]`, }) let dynamicChild = false const preparedParameters: PreparedParam[] = [] for (let i = 0; i < value.length; i++) { const preparedParam = prepareParameter({ parameter, value: value[i] }) if (preparedParam.dynamic) dynamicChild = true preparedParameters.push(preparedParam) } if (dynamic || dynamicChild) { const data = encodeParameters(preparedParameters) if (dynamic) { const length = numberToHex(preparedParameters.length, { size: 32 }) return { dynamic: true, encoded: preparedParameters.length > 0 ? concat([length, data]) : length, } } if (dynamicChild) return { dynamic: true, encoded: data } } return { dynamic: false, encoded: concat(preparedParameters.map(({ encoded }) => encoded)), } } type EncodeBytesErrorType = | AbiEncodingBytesSizeMismatchErrorType | ConcatErrorType | PadHexErrorType | NumberToHexErrorType | SizeErrorType | ErrorType function encodeBytes(value: Hex, { type }: { type: string }): PreparedParam { const [, parametersize] = type.split('bytes') const bytesSize = size(value) if (!parametersize) { let value_ = value // If the size is not divisible by 32 bytes, pad the end // with empty bytes to the ceiling 32 bytes. if (bytesSize % 32 !== 0) value_ = padHex(value_, { dir: 'right', size: Math.ceil((value.length - 2) / 2 / 32) * 32, }) return { dynamic: true, encoded: concat([padHex(numberToHex(bytesSize, { size: 32 })), value_]), } } if (bytesSize !== Number.parseInt(parametersize)) throw new AbiEncodingBytesSizeMismatchError({ expectedSize: Number.parseInt(parametersize), value, }) return { dynamic: false, encoded: padHex(value, { dir: 'right' }) } } type EncodeBoolErrorType = PadHexErrorType | BoolToHexErrorType | ErrorType function encodeBool(value: boolean): PreparedParam { if (typeof value !== 'boolean') throw new BaseError( `Invalid boolean value: "${value}" (type: ${typeof value}). Expected: \`true\` or \`false\`.`, ) return { dynamic: false, encoded: padHex(boolToHex(value)) } } type EncodeNumberErrorType = NumberToHexErrorType | ErrorType function encodeNumber( value: number, { signed }: { signed: boolean }, ): PreparedParam { return { dynamic: false, encoded: numberToHex(value, { size: 32, signed, }), } } type EncodeStringErrorType = | ConcatErrorType | NumberToHexErrorType | PadHexErrorType | SizeErrorType | SliceErrorType | StringToHexErrorType | ErrorType function encodeString(value: string): PreparedParam { const hexValue = stringToHex(value) const partsLength = Math.ceil(size(hexValue) / 32) const parts: Hex[] = [] for (let i = 0; i < partsLength; i++) { parts.push( padHex(slice(hexValue, i * 32, (i + 1) * 32), { dir: 'right', }), ) } return { dynamic: true, encoded: concat([ padHex(numberToHex(size(hexValue), { size: 32 })), ...parts, ]), } } type EncodeTupleErrorType = | ConcatErrorType | EncodeParametersErrorType | ErrorType function encodeTuple< const parameter extends AbiParameter & { components: readonly AbiParameter[] }, >( value: AbiParameterToPrimitiveType, { parameter }: { parameter: parameter }, ): PreparedParam { let dynamic = false const preparedParameters: PreparedParam[] = [] for (let i = 0; i < parameter.components.length; i++) { const param_ = parameter.components[i] const index = Array.isArray(value) ? i : param_.name const preparedParam = prepareParameter({ parameter: param_, value: (value as any)[index!] as readonly unknown[], }) preparedParameters.push(preparedParam) if (preparedParam.dynamic) dynamic = true } return { dynamic, encoded: dynamic ? encodeParameters(preparedParameters) : concat(preparedParameters.map(({ encoded }) => encoded)), } } type GetArrayComponentsErrorType = ErrorType export function getArrayComponents( type: string, ): [length: number | null, innerType: string] | undefined { const matches = type.match(/^(.*)\[(\d+)?\]$/) return matches ? // Return `null` if the array is dynamic. [matches[2] ? Number(matches[2]) : null, matches[1]] : undefined }