diff --git a/bundled-addons/bchwallet/lib/chain-btc.js b/bundled-addons/bchwallet/lib/chain-btc.js index db0532f..539cc2d 100644 --- a/bundled-addons/bchwallet/lib/chain-btc.js +++ b/bundled-addons/bchwallet/lib/chain-btc.js @@ -79,7 +79,7 @@ module.exports = function makeBtcAdapter({ // Legacy P2PKH. Signing needs the full previous transaction // (nonWitnessUtxo) — one extra electrum call per input at send time. const p = payments.p2pkh({ pubkey, network }); - return { family: "bip44", address: p.address, output: Buffer.from(p.output), send: "p2pkh" }; + return { family: "bip44", address: p.address, output: Buffer.from(p.output), send: "p2pkh", needsPrevTx: true }; } if (purpose === 49) { // P2SH-wrapped SegWit. PSBT needs the redeem script (the inner p2wpkh @@ -89,12 +89,12 @@ module.exports = function makeBtcAdapter({ return { family: "bip49", address: p.address, output: Buffer.from(p.output), redeem: Buffer.from(redeem.output), send: "p2sh-p2wpkh" }; } if (purpose === 86) { - // BIP86 Taproot key-path. Signing needs the tap-tweaked signer; - // ECPair from the current bitcoinjs stack doesn't tweak natively, so - // send is deferred — receive still works. - const internalPubkey = Buffer.from(pubkey.subarray(1, 33)); // x-only + // BIP86 Taproot key-path. Signing goes through a tap-tweaked ECPair + // (see signerFor + signAndBroadcast); the internal 32-byte x-only + // pubkey is captured here so the PSBT input can carry it. + const internalPubkey = Buffer.from(pubkey.subarray(1, 33)); const p = payments.p2tr({ internalPubkey, network }); - return { family: "bip86", address: p.address, output: Buffer.from(p.output), internalPubkey, send: null }; + return { family: "bip86", address: p.address, output: Buffer.from(p.output), internalPubkey, send: "p2tr" }; } // Default: BIP84 native SegWit. const p = payments.p2wpkh({ pubkey, network }); @@ -362,14 +362,8 @@ module.exports = function makeBtcAdapter({ const dest = String(to || ""); try { bitcoinjs.address.toOutputScript(dest, this._bjsNet); } catch (e) { throw new Error(`bad Bitcoin address: ${e?.message || dest}`); } - // Family-aware refusal: this rev signs BIP84 + BIP49 natively. BIP44 - // needs previous-tx fetches (nonWitnessUtxo) and BIP86 needs Taproot - // key-path tweaking — both are on the roadmap; error early so the - // user isn't surprised at broadcast time. const cur = this.current(); - if (!cur.sendKind) { - throw new Error(`Sending from ${cur.family.toUpperCase()} addresses isn't wired up yet in this Aegis rev. Receiving works; to spend, switch this wallet's address family to BIP84 (Native SegWit) in Settings and sweep funds there.`); - } + if (!cur.sendKind) throw new Error(`no sender for ${cur.family} — registry bug`); const spendable = this._state.utxos.slice().sort((a, b) => (b.height > 0) - (a.height > 0)); const change = this._changeEntry(); const kind = cur.sendKind; @@ -408,15 +402,28 @@ module.exports = function makeBtcAdapter({ } async signAndBroadcast(plan) { + // BIP44 inputs need the whole previous transaction (nonWitnessUtxo) + // so PSBT can compute a legacy sighash; fetch each one in parallel + // before assembling the PSBT. + const needsPrev = plan._chosen.filter((u) => u.entry.family === "bip44"); + const prevHex = new Map(); + if (needsPrev.length) { + const results = await Promise.all(needsPrev.map((u) => + this._client.call("blockchain.transaction.get", [u.txid, false]) + )); + needsPrev.forEach((u, i) => prevHex.set(u.txid, String(results[i]))); + } const psbt = new Psbt({ network: this._bjsNet }); for (const u of plan._chosen) { - const inp = { - hash: u.txid, index: u.vout, - witnessUtxo: { script: u.entry.script, value: u.value }, - }; - // BIP49: the redeem script (inner P2WPKH output) is required so - // bitcoinjs-lib can finalize the P2SH-wrapped SegWit witness. - if (u.entry.family === "bip49" && u.entry.redeemScript) inp.redeemScript = u.entry.redeemScript; + const inp = { hash: u.txid, index: u.vout }; + const fam = u.entry.family; + if (fam === "bip44") { + inp.nonWitnessUtxo = Buffer.from(prevHex.get(u.txid), "hex"); + } else { + inp.witnessUtxo = { script: u.entry.script, value: u.value }; + if (fam === "bip49" && u.entry.redeemScript) inp.redeemScript = u.entry.redeemScript; + if (fam === "bip86" && u.entry.tapInternalKey) inp.tapInternalKey = u.entry.tapInternalKey; + } psbt.addInput(inp); } const outputs = [{ address: plan._to, value: plan._sendMax ? plan.recipients[0].value : plan._value }]; @@ -425,8 +432,21 @@ module.exports = function makeBtcAdapter({ } for (const o of outputs) psbt.addOutput(o); for (let i = 0; i < plan._chosen.length; i++) { - const signer = this._keys.signerFor(plan._chosen[i].entry); - psbt.signInput(i, signer); + const entry = plan._chosen[i].entry; + // Taproot key-path: bitcoinjs-lib matches the signer's publicKey + // against the tweaked output key, so the signer has to be the + // internal ECPair tweaked with sha256("TapTweak" || internalPubkey). + // ECPair.tweak() from the ecpair package does exactly that (its + // internal state becomes the tap-tweaked keypair) and its + // signSchnorr is what PSBT calls for a key-path spend. + if (entry.family === "bip86") { + const raw = ECPair.fromPrivateKey(Buffer.from(entry._node.privateKey), { network: this._bjsNet }); + const tweak = bitcoinjs.crypto.taggedHash("TapTweak", entry.tapInternalKey); + const tweaked = raw.tweak(tweak); + psbt.signInput(i, tweaked); + } else { + psbt.signInput(i, this._keys.signerFor(entry)); + } } psbt.finalizeAllInputs(); const tx = psbt.extractTransaction();