96 lines
4 KiB
JavaScript
96 lines
4 KiB
JavaScript
|
|
// 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 };
|