UNPKG

viem

Version:

TypeScript Interface for Ethereum

268 lines • 9.41 kB
import { AbiEncodingArrayLengthMismatchError, AbiEncodingBytesSizeMismatchError, AbiEncodingLengthMismatchError, InvalidAbiEncodingTypeError, InvalidArrayError, } from '../../errors/abi.js'; import { InvalidAddressError, } from '../../errors/address.js'; import { BaseError } from '../../errors/base.js'; import { IntegerOutOfRangeError } from '../../errors/encoding.js'; import { isAddress } from '../address/isAddress.js'; import { concat } from '../data/concat.js'; import { padHex } from '../data/pad.js'; import { size } from '../data/size.js'; import { slice } from '../data/slice.js'; import { boolToHex, numberToHex, stringToHex, } from '../encoding/toHex.js'; import { integerRegex } from '../regex.js'; /** * @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. * * @param params - a set of ABI Parameters (params), that can be in the shape of the inputs or outputs attribute of an ABI Item. * @param values - a set of values (values) that correspond to the given params. * @example * ```typescript * import { encodeAbiParameters } from 'viem' * * const encodedData = encodeAbiParameters( * [ * { name: 'x', type: 'string' }, * { name: 'y', type: 'uint' }, * { name: 'z', type: 'bool' } * ], * ['wagmi', 420n, true] * ) * ``` * * You can also pass in Human Readable parameters with the parseAbiParameters utility. * * @example * ```typescript * import { encodeAbiParameters, parseAbiParameters } from 'viem' * * const encodedData = encodeAbiParameters( * parseAbiParameters('string x, uint y, bool z'), * ['wagmi', 420n, true] * ) * ``` */ export function encodeAbiParameters(params, values) { if (params.length !== values.length) throw new AbiEncodingLengthMismatchError({ expectedLength: params.length, givenLength: values.length, }); // Prepare the parameters to determine dynamic types to encode. const preparedParams = prepareParams({ params: params, values: values, }); const data = encodeParams(preparedParams); if (data.length === 0) return '0x'; return data; } function prepareParams({ params, values, }) { const preparedParams = []; for (let i = 0; i < params.length; i++) { preparedParams.push(prepareParam({ param: params[i], value: values[i] })); } return preparedParams; } function prepareParam({ param, value, }) { const arrayComponents = getArrayComponents(param.type); if (arrayComponents) { const [length, type] = arrayComponents; return encodeArray(value, { length, param: { ...param, type } }); } if (param.type === 'tuple') { return encodeTuple(value, { param: param, }); } if (param.type === 'address') { return encodeAddress(value); } if (param.type === 'bool') { return encodeBool(value); } if (param.type.startsWith('uint') || param.type.startsWith('int')) { const signed = param.type.startsWith('int'); const [, , size = '256'] = integerRegex.exec(param.type) ?? []; return encodeNumber(value, { signed, size: Number(size), }); } if (param.type.startsWith('bytes')) { return encodeBytes(value, { param }); } if (param.type === 'string') { return encodeString(value); } throw new InvalidAbiEncodingTypeError(param.type, { docsPath: '/docs/contract/encodeAbiParameters', }); } function encodeParams(preparedParams) { // 1. Compute the size of the static part of the parameters. let staticSize = 0; for (let i = 0; i < preparedParams.length; i++) { const { dynamic, encoded } = preparedParams[i]; if (dynamic) staticSize += 32; else staticSize += size(encoded); } // 2. Split the parameters into static and dynamic parts. const staticParams = []; const dynamicParams = []; let dynamicSize = 0; for (let i = 0; i < preparedParams.length; i++) { const { dynamic, encoded } = preparedParams[i]; if (dynamic) { staticParams.push(numberToHex(staticSize + dynamicSize, { size: 32 })); dynamicParams.push(encoded); dynamicSize += size(encoded); } else { staticParams.push(encoded); } } // 3. Concatenate static and dynamic parts. return concat([...staticParams, ...dynamicParams]); } function encodeAddress(value) { if (!isAddress(value)) throw new InvalidAddressError({ address: value }); return { dynamic: false, encoded: padHex(value.toLowerCase()) }; } function encodeArray(value, { length, param, }) { 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: `${param.type}[${length}]`, }); let dynamicChild = false; const preparedParams = []; for (let i = 0; i < value.length; i++) { const preparedParam = prepareParam({ param, value: value[i] }); if (preparedParam.dynamic) dynamicChild = true; preparedParams.push(preparedParam); } if (dynamic || dynamicChild) { const data = encodeParams(preparedParams); if (dynamic) { const length = numberToHex(preparedParams.length, { size: 32 }); return { dynamic: true, encoded: preparedParams.length > 0 ? concat([length, data]) : length, }; } if (dynamicChild) return { dynamic: true, encoded: data }; } return { dynamic: false, encoded: concat(preparedParams.map(({ encoded }) => encoded)), }; } function encodeBytes(value, { param }) { const [, paramSize] = param.type.split('bytes'); const bytesSize = size(value); if (!paramSize) { 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(paramSize)) throw new AbiEncodingBytesSizeMismatchError({ expectedSize: Number.parseInt(paramSize), value, }); return { dynamic: false, encoded: padHex(value, { dir: 'right' }) }; } function encodeBool(value) { 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)) }; } function encodeNumber(value, { signed, size = 256 }) { if (typeof size === 'number') { const max = 2n ** (BigInt(size) - (signed ? 1n : 0n)) - 1n; const min = signed ? -max - 1n : 0n; if (value > max || value < min) throw new IntegerOutOfRangeError({ max: max.toString(), min: min.toString(), signed, size: size / 8, value: value.toString(), }); } return { dynamic: false, encoded: numberToHex(value, { size: 32, signed, }), }; } function encodeString(value) { const hexValue = stringToHex(value); const partsLength = Math.ceil(size(hexValue) / 32); const parts = []; 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, ]), }; } function encodeTuple(value, { param }) { let dynamic = false; const preparedParams = []; for (let i = 0; i < param.components.length; i++) { const param_ = param.components[i]; const index = Array.isArray(value) ? i : param_.name; const preparedParam = prepareParam({ param: param_, value: value[index], }); preparedParams.push(preparedParam); if (preparedParam.dynamic) dynamic = true; } return { dynamic, encoded: dynamic ? encodeParams(preparedParams) : concat(preparedParams.map(({ encoded }) => encoded)), }; } export function getArrayComponents(type) { const matches = type.match(/^(.*)\[(\d+)?\]$/); return matches ? // Return `null` if the array is dynamic. [matches[2] ? Number(matches[2]) : null, matches[1]] : undefined; } //# sourceMappingURL=encodeAbiParameters.js.map