theseus/bundled-addons/aegis/lib/wc-sign.js

96 lines
4 KiB
JavaScript
Raw Normal View History

feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect Aegis Wallet 0.4.4 → 0.6.1: - Vault lifecycle from the wallet gate. The locked / not-yet-created states now show a master-password form (with optional BIP39 mnemonic on setup) instead of redirecting users to Settings › Passwords. New api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by the existing "vault-derive" capability. api.openSettings(section) also added; settings.html honours a #section hash on open. - Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44 path or a WIF; the cashaddr is derived in the add-on, the signer material goes to a separate wallet-imports.enc via api.vault.imports {list, add, remove, signer}. Argus password-vault gains createImports / unlockImports / saveImports with its own KDF salt so the imports key is disjoint from the passwords key. lib/chain-bch-imported.js is a single-address Electrum adapter; spend support is deferred to M.1b. - Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted in add-on storage. Fiat lines under balances, in the wallet picker, and a portfolio total when 2+ wallets are open. Settings tab is now reachable while the vault is locked so the toggle is always available. - WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js). @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay on the right side of LGPL §4d. Sign requests go through approvalModal and are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS. - DGB adapter load is now soft-fail: when Aegis runs from userData/addons the bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of taking the whole add-on down.
2026-09-09 10:33:21 +02:00
// WizardConnect transaction signing for Aegis.
//
// The dapp hands us a full BCH transaction plus its source outputs. Per the
// WC protocol, we must sign every input with SIGHASH_ALL | FORKID | UTXOS.
// Any other sighash flag combination MUST be rejected (protocol/security).
//
// This module supports P2PKH inputs only. Contract inputs (a source output
// carrying a `contract` field) are rejected with a clear error — they need
// script-aware signing that Aegis's BCH runtime doesn't do today.
// SIGHASH byte required for this protocol: SIGHASH_ALL | SIGHASH_FORKID | SIGHASH_UTXOS
// = 0x01 | 0x40 | 0x20 = 0x61.
const REQUIRED_SIGHASH = 0x61;
function toHex(u8) { let s = ""; for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, "0"); return s; }
function fromHex(h) {
const s = String(h || "").replace(/^0x/i, "");
const out = new Uint8Array(s.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.substr(i * 2, 2), 16);
return out;
}
function ensureTransaction(txOrHex, libauth) {
if (typeof txOrHex === "string") {
const dec = libauth.decodeTransactionCommon
? libauth.decodeTransactionCommon(fromHex(txOrHex))
: libauth.decodeTransaction(fromHex(txOrHex));
if (typeof dec === "string") throw new Error(`wc-sign: bad tx hex — ${dec}`);
return dec;
}
return txOrHex;
}
async function signTx({ request, account, branches, libauth, secp256k1 }) {
const {
generateSigningSerializationBCH,
hash256, encodeTransaction,
} = libauth;
const tx = ensureTransaction(request.transaction, libauth);
const sourceOutputs = (request.sourceOutputs || []).map((o, i) => {
if (o.contract) throw new Error(`wc-sign: input ${i} spends a contract — unsupported`);
return {
lockingBytecode: o.lockingBytecode instanceof Uint8Array ? o.lockingBytecode : fromHex(o.lockingBytecode),
valueSatoshis: typeof o.valueSatoshis === "bigint" ? o.valueSatoshis : BigInt(o.valueSatoshis),
};
});
if (sourceOutputs.length !== tx.inputs.length) {
throw new Error(`wc-sign: sourceOutputs (${sourceOutputs.length}) ≠ inputs (${tx.inputs.length})`);
}
const inputPathMap = new Map(); // inputIndex -> { branch, addressIndex }
for (const [inputIndex, pathName, addressIndex] of (request.inputPaths || [])) {
inputPathMap.set(Number(inputIndex), { pathName: String(pathName), addressIndex: Number(addressIndex) });
}
const signedInputs = tx.inputs.map((inp, i) => ({ ...inp }));
for (let i = 0; i < tx.inputs.length; i++) {
const hint = inputPathMap.get(i);
if (!hint) throw new Error(`wc-sign: no path for input ${i}`);
const branch = branches[hint.pathName];
if (!branch) throw new Error(`wc-sign: unknown path "${hint.pathName}"`);
const node = branch.deriveChild(hint.addressIndex);
const preimage = generateSigningSerializationBCH({
inputIndex: i,
signingSerializationType: new Uint8Array([REQUIRED_SIGHASH]),
sourceOutputs,
transaction: { ...tx, inputs: signedInputs },
});
const digest = hash256(preimage);
const sig = secp256k1.sign(digest, node.privateKey, { prehash: false, lowS: true, format: "der" });
// signature || sighashType byte
const sigWithHash = new Uint8Array(sig.length + 1);
sigWithHash.set(sig, 0); sigWithHash[sig.length] = REQUIRED_SIGHASH;
// P2PKH unlocking: <sig+hashtype> <pubkey>
const pushSig = new Uint8Array(1 + sigWithHash.length);
pushSig[0] = sigWithHash.length;
pushSig.set(sigWithHash, 1);
const pushPk = new Uint8Array(1 + node.publicKey.length);
pushPk[0] = node.publicKey.length;
pushPk.set(node.publicKey, 1);
const unlocking = new Uint8Array(pushSig.length + pushPk.length);
unlocking.set(pushSig, 0); unlocking.set(pushPk, pushSig.length);
signedInputs[i].unlockingBytecode = unlocking;
}
const encoded = encodeTransaction({ ...tx, inputs: signedInputs });
return { signedTransaction: toHex(encoded) };
}
module.exports = { signTx, REQUIRED_SIGHASH };