UNPKG

ox

Version:

Ethereum Standard Library

465 lines 17.9 kB
import * as AbiParameters from '../AbiParameters.js'; import * as Address from '../Address.js'; import * as Bytes from '../Bytes.js'; import * as Errors from '../Errors.js'; import * as Hex from '../Hex.js'; import { integerRegex } from '../Solidity.js'; /** @internal */ export function decodeParameter(cursor, param, options) { const { checksumAddress, staticPosition } = options; const arrayComponents = getArrayComponents(param.type); if (arrayComponents) { const [length, type] = arrayComponents; return decodeArray(cursor, { ...param, type }, { checksumAddress, length, staticPosition }); } if (param.type === 'tuple') return decodeTuple(cursor, param, { checksumAddress, staticPosition, }); if (param.type === 'address') return decodeAddress(cursor, { checksum: checksumAddress }); 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 AbiParameters.InvalidTypeError(param.type); } const sizeOfLength = 32; const sizeOfOffset = 32; /** @internal */ export function decodeAddress(cursor, options = {}) { const { checksum = false } = options; const value = cursor.readBytes(32); const wrap = (address) => checksum ? Address.checksum(address) : address; return [wrap(Hex.fromBytes(Bytes.slice(value, -20))), 32]; } /** @internal */ export function decodeArray(cursor, param, options) { const { checksumAddress, length, staticPosition } = options; // 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 = Bytes.toNumber(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 = Bytes.toNumber(cursor.readBytes(sizeOfLength)); // Check if the array has any dynamic children. const dynamicChild = hasDynamicChild(param); let consumed = 0; const value = []; 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, { checksumAddress, 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 = Bytes.toNumber(cursor.readBytes(sizeOfOffset)); // Start is the static position of current slot + offset. const start = staticPosition + offset; const value = []; 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, { checksumAddress, 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 = []; for (let i = 0; i < length; ++i) { const [data, consumed_] = decodeParameter(cursor, param, { checksumAddress, staticPosition: staticPosition + consumed, }); consumed += consumed_; value.push(data); } return [value, consumed]; } /** @internal */ export function decodeBool(cursor) { return [Bytes.toBoolean(cursor.readBytes(32), { size: 32 }), 32]; } /** @internal */ export function decodeBytes(cursor, param, { staticPosition }) { const [_, size] = param.type.split('bytes'); if (!size) { // Dealing with dynamic types, so get the offset of the bytes data. const offset = Bytes.toNumber(cursor.readBytes(32)); // Set position of the cursor to start of bytes data. cursor.setPosition(staticPosition + offset); const length = Bytes.toNumber(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 [Hex.fromBytes(data), 32]; } const value = Hex.fromBytes(cursor.readBytes(Number.parseInt(size), 32)); return [value, 32]; } /** @internal */ export function decodeNumber(cursor, param) { const signed = param.type.startsWith('int'); const size = Number.parseInt(param.type.split('int')[1] || '256'); const value = cursor.readBytes(32); return [ size > 48 ? Bytes.toBigInt(value, { signed }) : Bytes.toNumber(value, { signed }), 32, ]; } /** @internal */ export function decodeTuple(cursor, param, options) { const { checksumAddress, staticPosition } = options; // 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 = 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 = Bytes.toNumber(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, { checksumAddress, 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, { checksumAddress, staticPosition, }); value[hasUnnamedChild ? i : component?.name] = data; consumed += consumed_; } return [value, consumed]; } /** @internal */ export function decodeString(cursor, { staticPosition }) { // Get offset to start of string data. const offset = Bytes.toNumber(cursor.readBytes(32)); // Start is the static position of current slot + offset. const start = staticPosition + offset; cursor.setPosition(start); const length = Bytes.toNumber(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 = Bytes.toString(Bytes.trimLeft(data)); // As we have gone wondering, restore to the original position + next slot. cursor.setPosition(staticPosition + 32); return [value, 32]; } /** @internal */ export function prepareParameters({ checksumAddress, parameters, values, }) { const preparedParameters = []; for (let i = 0; i < parameters.length; i++) { preparedParameters.push(prepareParameter({ checksumAddress, parameter: parameters[i], value: values[i], })); } return preparedParameters; } /** @internal */ export function prepareParameter({ checksumAddress = false, parameter: parameter_, value, }) { const parameter = parameter_; const arrayComponents = getArrayComponents(parameter.type); if (arrayComponents) { const [length, type] = arrayComponents; return encodeArray(value, { checksumAddress, length, parameter: { ...parameter, type, }, }); } if (parameter.type === 'tuple') { return encodeTuple(value, { checksumAddress, parameter: parameter, }); } if (parameter.type === 'address') { return encodeAddress(value, { checksum: checksumAddress, }); } if (parameter.type === 'bool') { return encodeBoolean(value); } if (parameter.type.startsWith('uint') || parameter.type.startsWith('int')) { const signed = parameter.type.startsWith('int'); const [, , size = '256'] = integerRegex.exec(parameter.type) ?? []; return encodeNumber(value, { signed, size: Number(size), }); } if (parameter.type.startsWith('bytes')) { return encodeBytes(value, { type: parameter.type }); } if (parameter.type === 'string') { return encodeString(value); } throw new AbiParameters.InvalidTypeError(parameter.type); } /** @internal */ export function encode(preparedParameters) { // 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 += Hex.size(encoded); } // 2. Split the parameters into static and dynamic parts. const staticParameters = []; const dynamicParameters = []; let dynamicSize = 0; for (let i = 0; i < preparedParameters.length; i++) { const { dynamic, encoded } = preparedParameters[i]; if (dynamic) { staticParameters.push(Hex.fromNumber(staticSize + dynamicSize, { size: 32 })); dynamicParameters.push(encoded); dynamicSize += Hex.size(encoded); } else { staticParameters.push(encoded); } } // 3. Concatenate static and dynamic parts. return Hex.concat(...staticParameters, ...dynamicParameters); } /** @internal */ export function encodeAddress(value, options) { const { checksum = false } = options; Address.assert(value, { strict: checksum }); return { dynamic: false, encoded: Hex.padLeft(value.toLowerCase()), }; } /** @internal */ export function encodeArray(value, options) { const { checksumAddress, length, parameter } = options; const dynamic = length === null; if (!Array.isArray(value)) throw new AbiParameters.InvalidArrayError(value); if (!dynamic && value.length !== length) throw new AbiParameters.ArrayLengthMismatchError({ expectedLength: length, givenLength: value.length, type: `${parameter.type}[${length}]`, }); let dynamicChild = false; const preparedParameters = []; for (let i = 0; i < value.length; i++) { const preparedParam = prepareParameter({ checksumAddress, parameter, value: value[i], }); if (preparedParam.dynamic) dynamicChild = true; preparedParameters.push(preparedParam); } if (dynamic || dynamicChild) { const data = encode(preparedParameters); if (dynamic) { const length = Hex.fromNumber(preparedParameters.length, { size: 32 }); return { dynamic: true, encoded: preparedParameters.length > 0 ? Hex.concat(length, data) : length, }; } if (dynamicChild) return { dynamic: true, encoded: data }; } return { dynamic: false, encoded: Hex.concat(...preparedParameters.map(({ encoded }) => encoded)), }; } /** @internal */ export function encodeBytes(value, { type }) { const [, parametersize] = type.split('bytes'); const bytesSize = Hex.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_ = Hex.padRight(value_, Math.ceil((value.length - 2) / 2 / 32) * 32); return { dynamic: true, encoded: Hex.concat(Hex.padLeft(Hex.fromNumber(bytesSize, { size: 32 })), value_), }; } if (bytesSize !== Number.parseInt(parametersize)) throw new AbiParameters.BytesSizeMismatchError({ expectedSize: Number.parseInt(parametersize), value, }); return { dynamic: false, encoded: Hex.padRight(value) }; } /** @internal */ export function encodeBoolean(value) { if (typeof value !== 'boolean') throw new Errors.BaseError(`Invalid boolean value: "${value}" (type: ${typeof value}). Expected: \`true\` or \`false\`.`); return { dynamic: false, encoded: Hex.padLeft(Hex.fromBoolean(value)) }; } /** @internal */ export function encodeNumber(value, { signed, size }) { 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 Hex.IntegerOutOfRangeError({ max: max.toString(), min: min.toString(), signed, size: size / 8, value: value.toString(), }); } return { dynamic: false, encoded: Hex.fromNumber(value, { size: 32, signed, }), }; } /** @internal */ export function encodeString(value) { const hexValue = Hex.fromString(value); const partsLength = Math.ceil(Hex.size(hexValue) / 32); const parts = []; for (let i = 0; i < partsLength; i++) { parts.push(Hex.padRight(Hex.slice(hexValue, i * 32, (i + 1) * 32))); } return { dynamic: true, encoded: Hex.concat(Hex.padRight(Hex.fromNumber(Hex.size(hexValue), { size: 32 })), ...parts), }; } /** @internal */ export function encodeTuple(value, options) { const { checksumAddress, parameter } = options; let dynamic = false; const preparedParameters = []; 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({ checksumAddress, parameter: param_, value: value[index], }); preparedParameters.push(preparedParam); if (preparedParam.dynamic) dynamic = true; } return { dynamic, encoded: dynamic ? encode(preparedParameters) : Hex.concat(...preparedParameters.map(({ encoded }) => encoded)), }; } /** @internal */ 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; } /** @internal */ export function hasDynamicChild(param) { const { type } = param; if (type === 'string') return true; if (type === 'bytes') return true; if (type.endsWith('[]')) return true; if (type === 'tuple') return param.components?.some(hasDynamicChild); const arrayComponents = getArrayComponents(param.type); if (arrayComponents && hasDynamicChild({ ...param, type: arrayComponents[1], })) return true; return false; } //# sourceMappingURL=abiParameters.js.map