UNPKG

@ledgerhq/hw-app-eth

Version:
234 lines (204 loc) • 7.32 kB
import BigNumber from "bignumber.js"; import { EIP712MessageTypesEntry } from "@ledgerhq/types-live"; import { hexBuffer, intAsHexBytes } from "../../utils"; /** * @ignore for the README * * A Map of helpers to get the wanted binary value for * each type of array possible in a type definition */ enum EIP712_ARRAY_TYPE_VALUE { DYNAMIC = 0, FIXED = 1, } /** * @ignore for the README * * A Map of helpers to get the id and size to return for each * type that can be used in EIP712 */ export const EIP712_TYPE_PROPERTIES: Record< string, { key: (size?: number) => number; sizeInBits: (size?: number) => number | null; } > = { CUSTOM: { key: () => 0, sizeInBits: () => null, }, INT: { key: () => 1, sizeInBits: size => Number(size) / 8, }, UINT: { key: () => 2, sizeInBits: size => Number(size) / 8, }, ADDRESS: { key: () => 3, sizeInBits: () => null, }, BOOL: { key: () => 4, sizeInBits: () => null, }, STRING: { key: () => 5, sizeInBits: () => null, }, BYTES: { key: size => (typeof size !== "undefined" ? 6 : 7), sizeInBits: size => (typeof size !== "undefined" ? Number(size) : null), }, }; /** * @ignore for the README * * A Map of encoders to transform a value to formatted buffer */ export const EIP712_TYPE_ENCODERS = { INT(value: string | null, sizeInBits = 256): Buffer { const failSafeValue = value ?? "0"; if (typeof failSafeValue === "string" && failSafeValue?.startsWith("0x")) { return hexBuffer(failSafeValue); } let valueAsBN = new BigNumber(failSafeValue); // If negative we'll use `two's complement` method to // "reversibly convert a positive binary number into a negative binary number with equivalent (but negative) value". // thx wikipedia if (valueAsBN.lt(0)) { const sizeInBytes = sizeInBits / 8; // Creates BN from a buffer serving as a mask filled by maximum value 0xff const maskAsBN = new BigNumber(`0x${Buffer.alloc(sizeInBytes, 0xff).toString("hex")}`); // two's complement version of value valueAsBN = maskAsBN.plus(valueAsBN).plus(1); } const paddedHexString = valueAsBN.toString(16).length % 2 ? "0" + valueAsBN.toString(16) : valueAsBN.toString(16); return Buffer.from(paddedHexString, "hex"); }, UINT(value: string): Buffer { return this.INT(value); }, BOOL(value: number | string | boolean | null): Buffer { return this.INT(typeof value === "boolean" ? Number(value).toString() : value); }, ADDRESS(value: string | null): Buffer { // Only sending the first 10 bytes (why ?) return hexBuffer(value ?? "").slice(0, 20); }, STRING(value: string | null): Buffer { return Buffer.from(value ?? "", "utf-8"); }, BYTES(value: string | null, sizeInBits?: number): Buffer { const failSafeValue = value ?? ""; // Why slice again ? return hexBuffer(failSafeValue).slice(0, sizeInBits ?? (failSafeValue?.length - 2) / 2); }, }; /** * @ignore for the README * * Helper parsing an EIP712 Type name to return its type and size(s) * if it's an array or nested arrays * * @see EIP712MessageTypes * * @example "uint8[2][][4]" => [{name: "uint", bits: 8}, [2, null, 4]] * @example "bool" => [{name: "bool", bits: null}, []] * * @param {String} typeName * @returns {[{ name: string; bits: Number | null }, Array<Number | null | undefined>]} */ export const destructTypeFromString = ( typeName?: string, ): [{ name: string; bits: number | undefined } | null, Array<number | null>] => { // Will split "any[][1][10]" in "any", "[][1][10]" const splitNameAndArraysRegex = new RegExp(/^([^[\]]*)(\[.*\])*/g); // Will match all numbers (or null) inside each array. [0][10][] => [0,10,null] const splitArraysRegex = new RegExp(/\[(\d*)\]/g); // Will separate the the name from the potential bits allocation. uint8 => [uint,8] const splitNameAndNumberRegex = new RegExp(/(\D*)(\d*)/); const [, type, maybeArrays] = splitNameAndArraysRegex.exec(typeName || "") || []; const [, name, bits] = splitNameAndNumberRegex.exec(type || "") || []; const typeDescription = name ? { name, bits: bits ? Number(bits) : undefined } : null; const arrays = maybeArrays ? [...maybeArrays.matchAll(splitArraysRegex)] : []; // Parse each size to either a Number or null const arraySizes = arrays.map(([, size]) => (size ? Number(size) : null)); return [typeDescription, arraySizes]; }; /** * @ignore for the README * * Helper to construct the hexadecimal ByteString for the description * of a field in an EIP712 Message * * @param isArray * @param typeSize * @param typeValue * @returns {String} HexByteString */ export const constructTypeDescByteString = ( isArray: boolean, typeSize: number | null | undefined, typeValue: number, ): string => { if (typeValue >= 16) { throw new Error( "Eth utils - constructTypeDescByteString - Cannot accept a typeValue >= 16 because the typeValue can only be 4 bits in binary" + { isArray, typeSize, typeValue }, ); } // 1 is array, 0 is not array const isArrayBit = isArray ? "1" : "0"; // 1 has type size, 0 has no type size const hasTypeSize = typeof typeSize === "number" ? "1" : "0"; // 2 unused bits const unusedBits = "00"; // type key as 4 bits const typeValueBits = typeValue.toString(2).padStart(4, "0"); return intAsHexBytes(parseInt(isArrayBit + hasTypeSize + unusedBits + typeValueBits, 2), 1); }; /** * @ignore for the README * * Helper to create the buffer to describe an EIP712 types' entry structure * * @param {EIP712MessageTypesEntry} entry * @returns {Buffer} */ export const makeTypeEntryStructBuffer = ({ name, type }: EIP712MessageTypesEntry): Buffer => { const [typeDescription, arrSizes] = destructTypeFromString(type as string); const isTypeAnArray = Boolean(arrSizes.length); const typeProperties = EIP712_TYPE_PROPERTIES[typeDescription?.name?.toUpperCase() || ""] || EIP712_TYPE_PROPERTIES.CUSTOM; const typeKey = typeProperties.key(typeDescription?.bits); const typeSizeInBits = typeProperties.sizeInBits(typeDescription?.bits); const typeDescData = constructTypeDescByteString(isTypeAnArray, typeSizeInBits, typeKey); const bufferArray: Buffer[] = [Buffer.from(typeDescData, "hex")]; if (typeProperties === EIP712_TYPE_PROPERTIES.CUSTOM) { bufferArray.push(Buffer.from(intAsHexBytes(typeDescription?.name?.length ?? 0, 1), "hex")); bufferArray.push(Buffer.from(typeDescription?.name ?? "", "utf-8")); } if (typeof typeSizeInBits === "number") { bufferArray.push(Buffer.from(intAsHexBytes(typeSizeInBits, 1), "hex")); } if (isTypeAnArray) { bufferArray.push(Buffer.from(intAsHexBytes(arrSizes.length, 1), "hex")); arrSizes.forEach(size => { if (typeof size === "number") { bufferArray.push( Buffer.from(intAsHexBytes(EIP712_ARRAY_TYPE_VALUE.FIXED, 1), "hex"), Buffer.from(intAsHexBytes(size, 1), "hex"), ); } else { bufferArray.push(Buffer.from(intAsHexBytes(EIP712_ARRAY_TYPE_VALUE.DYNAMIC, 1), "hex")); } }); } bufferArray.push(Buffer.from(intAsHexBytes(name.length, 1), "hex"), Buffer.from(name, "utf-8")); return Buffer.concat(bufferArray); };