@fluent-wallet/hw-app-conflux
Version:
Ledger Hardware Wallet Conflux Application API
281 lines • 9.87 kB
JavaScript
const EIP712_DOMAIN = "EIP712Domain";
const CIP23_DOMAIN = "CIP23Domain";
/**
* Prepares EIP712 typed data for signing by extracting field definitions
* and building implementation entries for transmission to the device.
*/
export function prepareEIP712Payload(typedData) {
const { definitions, metaMap } = buildDefinitions(typedData.types);
const implementation = buildImplementation(metaMap, typedData);
return { definitions, implementation };
}
function buildDefinitions(types) {
const definitions = [];
const metaMap = {};
const sortedEntries = Object.entries(types).sort(([a], [b]) => a.localeCompare(b));
sortedEntries.forEach(([structName, fields]) => {
const parsedFields = [];
const metaFields = [];
fields.forEach((field) => {
const parsed = parseTypeDescriptor(field.type);
const definition = {
name: field.name,
type: parsed.fieldType,
};
if (parsed.arrayLevels.length > 0) {
definition.arrayLevels = parsed.arrayLevels.map((level) => level === null
? { kind: "dynamic" }
: { kind: "fixed", length: level });
}
parsedFields.push(definition);
metaFields.push({
name: field.name,
kind: parsed.kind,
size: parsed.size,
structName: parsed.structName,
arrayLevels: parsed.arrayLevels,
});
});
definitions.push({ name: structName, fields: parsedFields });
metaMap[structName] = metaFields;
});
return { definitions, metaMap };
}
function buildImplementation(metaMap, typedData) {
const entries = [];
const hasEip712Domain = Boolean(metaMap[EIP712_DOMAIN]);
const hasCip23Domain = Boolean(metaMap[CIP23_DOMAIN]);
if (hasEip712Domain && hasCip23Domain) {
throw new Error("Ambiguous domain: both EIP712Domain and CIP23Domain are present");
}
const domainRoot = hasEip712Domain
? EIP712_DOMAIN
: hasCip23Domain
? CIP23_DOMAIN
: null;
if (domainRoot) {
entries.push({ type: "root", name: domainRoot });
entries.push(...encodeStruct(metaMap, domainRoot, typedData.domain));
}
const { primaryType } = typedData;
if (!metaMap[primaryType]) {
throw new Error(`Unknown primaryType ${primaryType}`);
}
entries.push({ type: "root", name: primaryType });
entries.push(...encodeStruct(metaMap, primaryType, typedData.message));
return entries;
}
function parseTypeDescriptor(rawType) {
const { baseType, arrayLevels } = extractArrayLevels(rawType);
const { typeName, typeSize } = extractTypeName(baseType);
let fieldType;
let kind = "custom";
let size;
let structName;
switch (typeName) {
case "int":
case "uint": {
if (typeof typeSize !== "number" || typeSize % 8 !== 0) {
throw new Error(`${typeName} must specify a size multiple of 8`);
}
const byteSize = typeSize / 8;
if (byteSize < 1 || byteSize > 32) {
throw new Error(`${typeName} size must be between 8 and 256 bits`);
}
fieldType = { kind: typeName, size: byteSize };
kind = typeName;
size = byteSize;
break;
}
case "bytes": {
if (typeof typeSize === "number") {
if (typeSize < 1 || typeSize > 32) {
throw new Error("bytesN size must be between 1 and 32");
}
fieldType = { kind: "fixed-bytes", size: typeSize };
kind = "fixed-bytes";
size = typeSize;
}
else {
fieldType = { kind: "dynamic-bytes" };
kind = "dynamic-bytes";
}
break;
}
case "address":
case "bool":
case "string": {
fieldType = { kind: typeName };
kind = typeName;
break;
}
default: {
fieldType = { kind: "custom", structName: typeName };
structName = typeName;
break;
}
}
return { fieldType, kind, size, structName, arrayLevels };
}
function extractArrayLevels(type) {
const arrayLevels = [];
let remaining = type;
const pattern = /(.*)\[([0-9]*)\]$/;
while (true) {
const match = remaining.match(pattern);
if (!match)
break;
remaining = match[1];
const level = match[2].length ? parseInt(match[2], 10) : null;
arrayLevels.unshift(level);
}
return { baseType: remaining, arrayLevels };
}
function extractTypeName(type) {
const match = type.match(/^(\w+?)(\d*)$/);
if (!match) {
throw new Error(`Invalid type descriptor: ${type}`);
}
const typeName = match[1];
const typeSize = match[2].length ? parseInt(match[2], 10) : undefined;
return { typeName, typeSize };
}
function encodeStruct(metaMap, structName, data) {
const metaFields = metaMap[structName];
if (!metaFields) {
throw new Error(`Unknown struct ${structName}`);
}
const entries = [];
metaFields.forEach((field) => {
const value = data[field.name];
if (typeof value === "undefined") {
throw new Error(`Missing value for field ${field.name} in ${structName}`);
}
entries.push(...encodeField(metaMap, field, value));
});
return entries;
}
function encodeField(metaMap, field, value) {
if (field.arrayLevels.length > 0) {
if (!Array.isArray(value)) {
throw new Error(`Field ${field.name} expects an array`);
}
const [currentLevel, ...rest] = field.arrayLevels;
if (currentLevel !== null && value.length !== currentLevel) {
throw new Error(`Array ${field.name} expects length ${currentLevel} but received ${value.length}`);
}
const entries = [
{ type: "array", size: value.length },
];
value.forEach((element) => {
entries.push(...encodeField(metaMap, {
...field,
arrayLevels: rest,
}, element));
});
return entries;
}
if (field.kind === "custom") {
if (!field.structName || typeof value !== "object" || value === null) {
throw new Error(`Field ${field.name} expected struct ${field.structName}`);
}
return encodeStruct(metaMap, field.structName, value);
}
const buffer = encodePrimitiveValue(field.kind, field.size, value);
const entry = {
type: "field",
value: buffer,
};
return [entry];
}
function encodePrimitiveValue(kind, size, value) {
switch (kind) {
case "int":
case "uint": {
if (typeof size !== "number") {
throw new Error(`${kind} requires a size`);
}
return encodeInteger(value, size * 8);
}
case "address":
return encodeFixedHex(value, 20);
case "bool":
return encodeBoolean(value);
case "string":
if (value == null)
return Buffer.alloc(0);
if (typeof value !== "string") {
throw new Error("String value must be a string");
}
return Buffer.from(value, "utf8");
case "fixed-bytes":
if (typeof size !== "number") {
throw new Error("fixed-bytes requires a size");
}
return encodeFixedHex(value, size);
case "dynamic-bytes":
return encodeDynamicHex(value);
default:
throw new Error(`Unsupported primitive type: ${kind}`);
}
}
function padHexString(str) {
return str.length % 2 ? "0" + str : str;
}
function hexBuffer(str) {
if (!str)
return Buffer.alloc(0);
const withoutPrefix = str.startsWith("0x") ? str.slice(2) : str;
return Buffer.from(padHexString(withoutPrefix), "hex");
}
function encodeInteger(value, sizeInBits = 256) {
const failSafeValue = value ?? "0";
if (typeof failSafeValue === "string" && failSafeValue.startsWith("0x")) {
return hexBuffer(failSafeValue);
}
let numericValue;
if (typeof failSafeValue === "string") {
numericValue = BigInt(failSafeValue);
}
else if (typeof failSafeValue === "number") {
if (!Number.isSafeInteger(failSafeValue)) {
throw new Error("Unsafe integer number; use string or bigint");
}
numericValue = BigInt(failSafeValue);
}
else if (typeof failSafeValue === "bigint") {
numericValue = failSafeValue;
}
else if (typeof failSafeValue === "boolean") {
numericValue = failSafeValue ? 1n : 0n;
}
else {
throw new Error("Integer value must be a number, bigint, boolean or string");
}
if (numericValue < 0n) {
const modulus = 1n << BigInt(sizeInBits);
numericValue = (modulus + (numericValue % modulus)) % modulus;
}
const hex = padHexString(numericValue.toString(16));
return Buffer.from(hex, "hex");
}
function encodeFixedHex(value, byteLength) {
if (value == null)
return Buffer.alloc(0);
if (typeof value !== "string") {
throw new Error("Expected hex string value");
}
return hexBuffer(value).subarray(0, byteLength);
}
function encodeDynamicHex(value) {
if (value == null)
return Buffer.alloc(0);
if (typeof value !== "string") {
throw new Error("Dynamic bytes must be a hex string");
}
return hexBuffer(value);
}
function encodeBoolean(value) {
return encodeInteger(value, 256);
}
//# sourceMappingURL=typedData.js.map