@okxweb3/coin-ethereum
Version:
An Ethereum SDK for building Web3 wallets and applications.
414 lines • 16.1 kB
JavaScript
;
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