// 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: 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 };