Setup 5d15508bba929f1f074c052ac933863eadf6eb8e56984ebd5a1af75e80626643 Portable a5d346b97f5a13d85fa3bd301a72075ddb82fe636d7b1a51840ffd5a16d879f4 Bundled since 0.3.27: 32d4b75 - Aegis (bchwallet) gains its own update card in Settings > General beside Ariadne. Check for updates hits the same signed OTA endpoint the boot timer uses; Restart to apply appears when a signed newer version is staged. Uses the existing addons-check-updates + a new app-restart IPC. New Aegis versions ship without a Theseus release. 32d4b75 (same commit) - DevTools (F12 / Ctrl+Shift+I) opens docked to the right of the tab (mode: 'right') instead of a detached window. Matches stock Chrome. Users who prefer detached can drag out via the DevTools own toolbar. b71c925 - Search-engine favicons in Settings > Search now use Google's /s2/favicons service — DuckDuckGo's ip3 source returned 404 for enough hosts (Brave, Bing, Yandex, etc.) that half the list was falling through to the emoji placeholder. Deployed. Verified LIVE 0.3.28.
143 lines
6.1 KiB
JavaScript
143 lines
6.1 KiB
JavaScript
// EIP-712 typed-data hashing (personal_sign's structured cousin). Produces
|
|
// the 32-byte digest that eth_signTypedData_v4 signs with the wallet's
|
|
// secp256k1 key.
|
|
//
|
|
// Reference: https://eips.ethereum.org/EIPS/eip-712
|
|
// Digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct(primaryType, message))
|
|
// - domainSeparator = hashStruct("EIP712Domain", typedData.domain)
|
|
// - hashStruct(type, data) = keccak256(typeHash(type) || encodeData(type, data))
|
|
// - typeHash(type) = keccak256(encodeType(type))
|
|
// - encodeType is the canonical string form; sub-types are appended in
|
|
// alphabetical order once, without recursion into themselves twice.
|
|
//
|
|
// This is enough for every mainstream EIP-712 payload — Permit / EIP-2612,
|
|
// OpenSea order signatures, WalletConnect handshakes, Snapshot votes. Not
|
|
// implemented: fixed-size arrays of atomic types wider than a byte (rare
|
|
// enough that no shipping dapp we care about uses them).
|
|
|
|
module.exports = function makeEip712({ keccak_256 }) {
|
|
const enc = new TextEncoder();
|
|
const concat = (...ps) => {
|
|
const n = ps.reduce((a, p) => a + p.length, 0);
|
|
const out = new Uint8Array(n); let k = 0;
|
|
for (const p of ps) { out.set(p, k); k += p.length; }
|
|
return out;
|
|
};
|
|
const hex2bytes = (h) => {
|
|
const s = String(h).replace(/^0x/i, "");
|
|
if (s.length % 2) throw new Error("hex: odd length");
|
|
const out = new Uint8Array(s.length / 2);
|
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
|
return out;
|
|
};
|
|
const bytesToBig = (b) => {
|
|
let v = 0n; for (const x of b) v = (v << 8n) | BigInt(x); return v;
|
|
};
|
|
const bigToBe32 = (v, signed) => {
|
|
let n = BigInt(v);
|
|
if (n < 0n) {
|
|
if (!signed) throw new Error("negative value for unsigned type");
|
|
// two's complement to 256 bits
|
|
n = (1n << 256n) + n;
|
|
}
|
|
const out = new Uint8Array(32);
|
|
for (let i = 31; i >= 0; i--) { out[i] = Number(n & 0xffn); n >>= 8n; }
|
|
return out;
|
|
};
|
|
|
|
// encodeType walker — resolves the primary type + every struct it
|
|
// transitively references, then emits "Primary(...)Sub1(...)Sub2(...)"
|
|
// with sub-types in alphabetical order per the spec.
|
|
function findDependencies(primaryType, types, found = new Set()) {
|
|
if (found.has(primaryType) || !types[primaryType]) return found;
|
|
found.add(primaryType);
|
|
for (const f of types[primaryType]) {
|
|
const base = f.type.replace(/\[.*\]$/, "");
|
|
if (types[base]) findDependencies(base, types, found);
|
|
}
|
|
return found;
|
|
}
|
|
function encodeType(primaryType, types) {
|
|
const deps = [...findDependencies(primaryType, types)].filter((t) => t !== primaryType).sort();
|
|
const all = [primaryType, ...deps];
|
|
return all.map((t) => `${t}(${types[t].map((f) => `${f.type} ${f.name}`).join(",")})`).join("");
|
|
}
|
|
function typeHash(primaryType, types) {
|
|
return keccak_256(enc.encode(encodeType(primaryType, types)));
|
|
}
|
|
|
|
// Encode one field value per its declared type. Struct + array types
|
|
// hash themselves to 32 bytes; atomics land in a 32-byte slot each.
|
|
function encodeValue(type, value, types) {
|
|
// Array types: `Type[]` (dynamic) or `Type[N]` (fixed) — both encode
|
|
// as keccak256(concat(encodeValue(baseType, element)...)) per EIP-712.
|
|
const arr = /^(.+)\[(\d*)\]$/.exec(type);
|
|
if (arr) {
|
|
const baseType = arr[1];
|
|
const items = Array.isArray(value) ? value : [];
|
|
const encoded = items.map((v) => encodeValue(baseType, v, types));
|
|
return keccak_256(concat(...encoded));
|
|
}
|
|
// Struct types: hashStruct recursion.
|
|
if (types[type]) return hashStruct(type, value, types);
|
|
// Atomic types.
|
|
if (type === "string") return keccak_256(enc.encode(String(value ?? "")));
|
|
if (type === "bytes") {
|
|
const b = typeof value === "string" ? hex2bytes(value) : Uint8Array.from(value || []);
|
|
return keccak_256(b);
|
|
}
|
|
if (type === "address") {
|
|
const h = hex2bytes(String(value || "0x0").replace(/^0x/, ""));
|
|
if (h.length !== 20) throw new Error("address must be 20 bytes");
|
|
const out = new Uint8Array(32);
|
|
out.set(h, 12);
|
|
return out;
|
|
}
|
|
if (type === "bool") {
|
|
const out = new Uint8Array(32);
|
|
out[31] = value ? 1 : 0;
|
|
return out;
|
|
}
|
|
// bytesN (fixed): left-aligned in a 32-byte word.
|
|
const bytesN = /^bytes(\d+)$/.exec(type);
|
|
if (bytesN) {
|
|
const n = Number(bytesN[1]);
|
|
if (n < 1 || n > 32) throw new Error("bytesN out of range");
|
|
const b = typeof value === "string" ? hex2bytes(value) : Uint8Array.from(value || []);
|
|
if (b.length !== n) throw new Error(`${type} expects ${n} bytes, got ${b.length}`);
|
|
const out = new Uint8Array(32);
|
|
out.set(b, 0);
|
|
return out;
|
|
}
|
|
// uint* / int*: encode as 32-byte big-endian.
|
|
const uintM = /^uint(\d*)$/.exec(type);
|
|
if (uintM) return bigToBe32(value, false);
|
|
const intM = /^int(\d*)$/.exec(type);
|
|
if (intM) return bigToBe32(value, true);
|
|
throw new Error("unsupported EIP-712 type: " + type);
|
|
}
|
|
|
|
function encodeData(primaryType, data, types) {
|
|
const fields = types[primaryType];
|
|
if (!fields) throw new Error("unknown type: " + primaryType);
|
|
const encoded = fields.map((f) => encodeValue(f.type, data ? data[f.name] : undefined, types));
|
|
return concat(...encoded);
|
|
}
|
|
function hashStruct(primaryType, data, types) {
|
|
return keccak_256(concat(typeHash(primaryType, types), encodeData(primaryType, data, types)));
|
|
}
|
|
|
|
// Full EIP-712 digest, ready for secp256k1.sign(digest, key).
|
|
function digest(typedData) {
|
|
const td = typedData && typeof typedData === "object" ? typedData : {};
|
|
const types = td.types || {};
|
|
if (!types.EIP712Domain) throw new Error("typedData.types.EIP712Domain missing");
|
|
const primary = String(td.primaryType || "");
|
|
if (!primary || !types[primary]) throw new Error(`typedData.primaryType "${primary}" not in types`);
|
|
const domainSeparator = hashStruct("EIP712Domain", td.domain || {}, types);
|
|
const messageHash = hashStruct(primary, td.message || {}, types);
|
|
return keccak_256(concat(Uint8Array.from([0x19, 0x01]), domainSeparator, messageHash));
|
|
}
|
|
|
|
return { digest, encodeType, typeHash, hashStruct };
|
|
};
|