UNPKG

@okxweb3/coin-ethereum

Version:

An Ethereum SDK for building Web3 wallets and applications.

414 lines 16.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.typedSignatureHash = exports.TypedDataUtils = exports.TYPED_MESSAGE_SCHEMA = exports.getLength = exports.getErrorStack = exports.padStart = exports.bigIntToBytes = exports.remove0x = exports.add0x = exports.hexToBytes = exports.isStrictHexString = exports.assertIsHexString = exports.concatBytes = exports.valueToBytes = exports.isBytes = exports.stringToBytes = exports.numberToBytes = exports.assert = exports.SignTypedDataVersion = exports.StrictHexStruct = void 0; const ethereumjs_util_1 = require("../ethereumjs-util"); const index_1 = require("./index"); const coin_base_1 = require("@okxweb3/coin-base"); const superstruct_1 = require("superstruct"); const HEX_MINIMUM_NUMBER_CHARACTER = 48; const HEX_MAXIMUM_NUMBER_CHARACTER = 58; const HEX_CHARACTER_OFFSET = 87; const NUMBER_REGEX = /^u?int(?<length>[0-9]*)?$/u; const BUFFER_WIDTH = 32; exports.StrictHexStruct = (0, superstruct_1.pattern)((0, superstruct_1.string)(), /^0x[0-9a-f]+$/iu); var SignTypedDataVersion; (function (SignTypedDataVersion) { SignTypedDataVersion["V1"] = "V1"; SignTypedDataVersion["V3"] = "V3"; SignTypedDataVersion["V4"] = "V4"; })(SignTypedDataVersion = exports.SignTypedDataVersion || (exports.SignTypedDataVersion = {})); function assert(condition, errorMsg) { if (!condition) { throw new Error(errorMsg); } } exports.assert = assert; function numberToBytes(value) { assert(typeof value === 'number', 'Value must be a number.'); assert(value >= 0, 'Value must be a non-negative number.'); assert(Number.isSafeInteger(value), 'Value is not a safe integer. Use `bigIntToBytes` instead.'); const hexadecimal = value.toString(16); return hexToBytes(hexadecimal); } exports.numberToBytes = numberToBytes; function stringToBytes(value) { assert(typeof value === 'string', 'Value must be a string.'); return new TextEncoder().encode(value); } exports.stringToBytes = stringToBytes; function isBytes(value) { return value instanceof Uint8Array; } exports.isBytes = isBytes; function valueToBytes(value) { if (typeof value === 'bigint') { return bigIntToBytes(value); } if (typeof value === 'number') { return numberToBytes(value); } if (typeof value === 'string') { if (value.startsWith('0x')) { return hexToBytes(value); } return stringToBytes(value); } if (isBytes(value)) { return value; } throw new TypeError(`Unsupported value type: "${typeof value}".`); } exports.valueToBytes = valueToBytes; function concatBytes(values) { const normalizedValues = new Array(values.length); let byteLength = 0; for (let i = 0; i < values.length; i++) { const value = valueToBytes(values[i]); normalizedValues[i] = value; byteLength += value.length; } const bytes = new Uint8Array(byteLength); for (let i = 0, offset = 0; i < normalizedValues.length; i++) { bytes.set(normalizedValues[i], offset); offset += normalizedValues[i].length; } return bytes; } exports.concatBytes = concatBytes; function assertIsHexString(value) { assert((0, ethereumjs_util_1.isHexString)(value), 'Value must be a hexadecimal string.'); } exports.assertIsHexString = assertIsHexString; function isStrictHexString(value) { return (0, superstruct_1.is)(value, exports.StrictHexStruct); } exports.isStrictHexString = isStrictHexString; function hexToBytes(value) { if (value?.toLowerCase?.() === '0x') { return new Uint8Array(); } assertIsHexString(value); const strippedValue = remove0x(value).toLowerCase(); const normalizedValue = strippedValue.length % 2 === 0 ? strippedValue : `0${strippedValue}`; const bytes = new Uint8Array(normalizedValue.length / 2); for (let i = 0; i < bytes.length; i++) { const c1 = normalizedValue.charCodeAt(i * 2); const c2 = normalizedValue.charCodeAt(i * 2 + 1); const n1 = c1 - (c1 < HEX_MAXIMUM_NUMBER_CHARACTER ? HEX_MINIMUM_NUMBER_CHARACTER : HEX_CHARACTER_OFFSET); const n2 = c2 - (c2 < HEX_MAXIMUM_NUMBER_CHARACTER ? HEX_MINIMUM_NUMBER_CHARACTER : HEX_CHARACTER_OFFSET); bytes[i] = n1 * 16 + n2; } return bytes; } exports.hexToBytes = hexToBytes; function add0x(hexadecimal) { if (hexadecimal.startsWith('0x')) { return hexadecimal; } if (hexadecimal.startsWith('0X')) { return `0x${hexadecimal.substring(2)}`; } return `0x${hexadecimal}`; } exports.add0x = add0x; function remove0x(hexadecimal) { if (hexadecimal.startsWith('0x') || hexadecimal.startsWith('0X')) { return hexadecimal.substring(2); } return hexadecimal; } exports.remove0x = remove0x; function bigIntToBytes(value) { assert(typeof value === 'bigint', 'Value must be a bigint.'); assert(value >= BigInt(0), 'Value must be a non-negative bigint.'); const hexadecimal = value.toString(16); return hexToBytes(hexadecimal); } exports.bigIntToBytes = bigIntToBytes; function padStart(buffer, length = BUFFER_WIDTH) { const padding = new Uint8Array(Math.max(length - buffer.length, 0)).fill(0x00); return concatBytes([padding, buffer]); } exports.padStart = padStart; ; const getErrorStack = (error) => { if (error instanceof Error) { return error.stack; } return undefined; }; exports.getErrorStack = getErrorStack; const getLength = (type) => { if (type === 'int' || type === 'uint') { return 256; } const match = type.match(NUMBER_REGEX); assert(match?.groups?.length, `Invalid number type. Expected a number type, but received "${type}".`); const length = parseInt(match.groups.length, 10); assert(length >= 8 && length <= 256, `Invalid number length. Expected a number between 8 and 256, but received "${type}".`); assert(length % 8 === 0, `Invalid number length. Expected a multiple of 8, but received "${type}".`); return length; }; exports.getLength = getLength; exports.TYPED_MESSAGE_SCHEMA = { type: 'object', properties: { types: { type: 'object', additionalProperties: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, type: { type: 'string', enum: getSolidityTypes() }, }, required: ['name', 'type'], }, }, }, primaryType: { type: 'string' }, domain: { type: 'object' }, message: { type: 'object' }, }, required: ['types', 'primaryType', 'domain', 'message'], }; function getSolidityTypes() { const types = ['bool', 'address', 'string', 'bytes']; const ints = Array.from(new Array(32)).map((_, index) => `int${(index + 1) * 8}`); const uints = Array.from(new Array(32)).map((_, index) => `uint${(index + 1) * 8}`); const bytes = Array.from(new Array(32)).map((_, index) => `bytes${index + 1}`); return [...types, ...ints, ...uints, ...bytes]; } function validateVersion(version, allowedVersions) { if (!Object.keys(SignTypedDataVersion).includes(version)) { throw new Error(`Invalid version: '${version}'`); } else if (allowedVersions && !allowedVersions.includes(version)) { throw new Error(`SignTypedDataVersion not allowed: '${version}'. Allowed versions are: ${allowedVersions.join(', ')}`); } } function reallyStrangeAddressToBytes(address) { let addressValue = BigInt(0); for (let i = 0; i < address.length; i++) { const character = BigInt(address.charCodeAt(i) - 48); addressValue *= BigInt(10); if (character >= 49) { addressValue += character - BigInt(49) + BigInt(0xa); } else if (character >= 17) { addressValue += character - BigInt(17) + BigInt(0xa); } else { addressValue += character; } } return padStart(bigIntToBytes(addressValue), 20); } function parseNumber(type, value) { assert(value !== null, `Unable to encode value: Invalid number. Expected a valid number value, but received "${value}".`); const bigIntValue = BigInt(value); const length = (0, exports.getLength)(type); const maxValue = BigInt(2) ** BigInt(length) - BigInt(1); assert(bigIntValue >= -maxValue && bigIntValue <= maxValue, `Unable to encode value: Number "${value}" is out of range for type "${type}".`); return bigIntValue; } function encodeField(types, name, type, value, version) { validateVersion(version, [SignTypedDataVersion.V3, SignTypedDataVersion.V4]); if (types[type] !== undefined) { return [ 'bytes32', version === SignTypedDataVersion.V4 && value == null ? '0x0000000000000000000000000000000000000000000000000000000000000000' : (0, ethereumjs_util_1.keccak)(encodeData(type, value, types, version)), ]; } if (type === 'function') { throw new Error('Unsupported or invalid type: "function"'); } if (value === undefined) { throw new Error(`missing value for field ${name} of type ${type}`); } if (type === 'address') { if (typeof value === 'number') { return ['address', padStart(numberToBytes(value), 20)]; } else if (isStrictHexString(value)) { return ['address', add0x(value)]; } else if (typeof value === 'string') { return ['address', reallyStrangeAddressToBytes(value).subarray(0, 20)]; } } if (type === 'bool') { return ['bool', Boolean(value)]; } if (type === 'bytes') { if (typeof value === 'number') { value = numberToBytes(value); } else if (isStrictHexString(value)) { value = hexToBytes(value); } else if (typeof value === 'string') { value = stringToBytes(value); } return ['bytes32', (0, ethereumjs_util_1.keccak)(value)]; } if (type.startsWith('bytes') && type !== 'bytes' && !type.includes('[')) { if (typeof value === 'number') { if (value < 0) { return ['bytes32', new Uint8Array(32)]; } return ['bytes32', bigIntToBytes(BigInt(value))]; } else if (isStrictHexString(value)) { return ['bytes32', hexToBytes(value)]; } return ['bytes32', value]; } if (type.startsWith('int') && !type.includes('[')) { const bigIntValue = parseNumber(type, value); if (bigIntValue >= BigInt(0)) { return ['uint256', bigIntValue]; } return ['int256', bigIntValue]; } if (type === 'string') { if (typeof value === 'number') { value = numberToBytes(value); } else { value = stringToBytes(value ?? ''); } return ['bytes32', (0, ethereumjs_util_1.keccak)(value)]; } if (type.endsWith(']')) { if (version === SignTypedDataVersion.V3) { throw new Error('Arrays are unimplemented in encodeData; use V4 extension'); } const parsedType = type.slice(0, type.lastIndexOf('[')); const typeValuePairs = value.map((item) => encodeField(types, name, parsedType, item, version)); const k = typeValuePairs.map(([t]) => t); const v = typeValuePairs.map(([, v]) => v); return ['bytes32', (0, ethereumjs_util_1.keccak)(coin_base_1.abi.RawEncode(k, v))]; } return [type, value]; } function encodeData(primaryType, data, types, version) { validateVersion(version, [SignTypedDataVersion.V3, SignTypedDataVersion.V4]); const encodedTypes = ['bytes32']; const encodedValues = [hashType(primaryType, types)]; for (const field of types[primaryType]) { if (version === SignTypedDataVersion.V3 && data[field.name] === undefined) { continue; } const [type, value] = encodeField(types, field.name, field.type, data[field.name], version); encodedTypes.push(type); encodedValues.push(value); } return coin_base_1.abi.RawEncode(encodedTypes, encodedValues); } function encodeType(primaryType, types) { let result = ''; const unsortedDeps = findTypeDependencies(primaryType, types); unsortedDeps.delete(primaryType); const deps = [primaryType, ...Array.from(unsortedDeps).sort()]; for (const type of deps) { const children = types[type]; if (!children) { throw new Error(`No type definition specified: ${type}`); } result += `${type}(${types[type] .map(({ name, type: t }) => `${t} ${name}`) .join(',')})`; } return result; } function findTypeDependencies(primaryType, types, results = new Set()) { [primaryType] = primaryType.match(/^\w*/u); if (results.has(primaryType) || types[primaryType] === undefined) { return results; } results.add(primaryType); for (const field of types[primaryType]) { findTypeDependencies(field.type, types, results); } return results; } function hashStruct(primaryType, data, types, version) { validateVersion(version, [SignTypedDataVersion.V3, SignTypedDataVersion.V4]); return (0, ethereumjs_util_1.keccak)(encodeData(primaryType, data, types, version)); } function hashType(primaryType, types) { return (0, ethereumjs_util_1.keccak)(Buffer.from(encodeType(primaryType, types))); } function sanitizeData(data) { const sanitizedData = {}; for (const key in exports.TYPED_MESSAGE_SCHEMA.properties) { if (data[key]) { sanitizedData[key] = data[key]; } } if ('types' in sanitizedData) { sanitizedData.types = { EIP712Domain: [], ...sanitizedData.types }; } return sanitizedData; } function eip712Hash(typedData, version) { validateVersion(version, [SignTypedDataVersion.V3, SignTypedDataVersion.V4]); const sanitizedData = sanitizeData(typedData); const parts = [Buffer.from('1901', 'hex')]; parts.push(hashStruct('EIP712Domain', sanitizedData.domain, sanitizedData.types, version)); if (sanitizedData.primaryType !== 'EIP712Domain') { parts.push(hashStruct(sanitizedData.primaryType, sanitizedData.message, sanitizedData.types, version)); } return (0, ethereumjs_util_1.keccak)(Buffer.concat(parts)); } exports.TypedDataUtils = { encodeData, encodeType, hashType, eip712Hash, }; function typedSignatureHash(typedData) { const hashBuffer = _typedSignatureHash(typedData); return (0, ethereumjs_util_1.bufferToHex)(hashBuffer); } exports.typedSignatureHash = typedSignatureHash; function _typedSignatureHash(typedData) { const error = new Error('Expect argument to be non-empty array'); if (typeof typedData !== 'object' || !('length' in typedData) || !typedData.length) { throw error; } const data = typedData.map(function (e) { if (e.type !== 'bytes') { return e.value; } return (0, index_1.legacyToBuffer)(e.value); }); const types = typedData.map(function (e) { return e.type; }); const schema = typedData.map(function (e) { if (!e.name) { throw error; } return `${e.type} ${e.name}`; }); const temp1 = coin_base_1.abi.ABI.solidityPack(new Array(typedData.length).fill('string'), schema); const temp2 = coin_base_1.abi.ABI.solidityPack(types, data); const hash1 = (0, ethereumjs_util_1.keccak256)(temp1); const hash2 = (0, ethereumjs_util_1.keccak256)(temp2); const temp = coin_base_1.abi.ABI.solidityPack(['bytes32', 'bytes32'], [hash1, hash2]); return (0, ethereumjs_util_1.keccak256)(temp); } //# sourceMappingURL=sign-typed-data.js.map