UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

372 lines (371 loc) 14.3 kB
import { crypto as bitcoinCrypto, opcodes, PaymentType, Psbt, script, toXOnly, } from '@btc-vision/bitcoin'; import { TransactionBuilder } from './TransactionBuilder.js'; import { TransactionType } from '../enums/TransactionType.js'; import { MultiSignGenerator } from '../../generators/builders/MultiSignGenerator.js'; import { EcKeyPair } from '../../keypair/EcKeyPair.js'; export class MultiSignTransaction extends TransactionBuilder { constructor(parameters) { if (!parameters.refundVault) { throw new Error('Refund vault is required'); } if (!parameters.requestedAmount) { throw new Error('Requested amount is required'); } if (!parameters.receiver) { throw new Error('Receiver is required'); } super({ ...parameters, signer: EcKeyPair.fromPrivateKey(bitcoinCrypto.sha256(Buffer.from('aaaaaaaa', 'utf-8'))), priorityFee: 0n, gasSatFee: 0n, }); this.type = TransactionType.MULTI_SIG; this.targetScriptRedeem = null; this.leftOverFundsScriptRedeem = null; this.originalInputCount = 0; this.sighashTypes = MultiSignTransaction.signHashTypesArray; this.customFinalizer = (_inputIndex, input) => { if (!this.tapLeafScript) { throw new Error('Tap leaf script is required'); } const scriptSolution = this.getScriptSolution(input); const witness = scriptSolution .concat(this.tapLeafScript.script) .concat(this.tapLeafScript.controlBlock); return { finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witness), }; }; if (!parameters.pubkeys) { throw new Error('Pubkeys are required'); } if (parameters.psbt) { this.log(`Using provided PSBT.`); this.transaction = parameters.psbt; this.originalInputCount = this.transaction.data.inputs.length; } this.refundVault = parameters.refundVault; this.requestedAmount = parameters.requestedAmount; this.receiver = parameters.receiver; this.publicKeys = parameters.pubkeys; this.minimumSignatures = parameters.minimumSignatures; this.compiledTargetScript = MultiSignGenerator.compile(parameters.pubkeys, this.minimumSignatures); this.scriptTree = this.getScriptTree(); this.internalInit(); } static fromBase64(params) { const psbt = Psbt.fromBase64(params.psbt, { network: params.network }); return new MultiSignTransaction({ ...params, psbt, }); } static verifyIfSigned(psbt, signerPubKey) { let alreadySigned = false; for (let i = 1; i < psbt.data.inputs.length; i++) { const input = psbt.data.inputs[i]; if (!input.finalScriptWitness) { continue; } const decoded = TransactionBuilder.readScriptWitnessToWitnessStack(input.finalScriptWitness); if (decoded.length < 3) { continue; } for (let j = 0; j < decoded.length - 2; j += 3) { const pubKey = decoded[j + 2]; if (pubKey.equals(signerPubKey)) { alreadySigned = true; break; } } } return alreadySigned; } static signPartial(psbt, signer, originalInputCount, minimums) { let signed = false; let final = true; for (let i = originalInputCount; i < psbt.data.inputs.length; i++) { const input = psbt.data.inputs[i]; if (!input.tapInternalKey) { input.tapInternalKey = toXOnly(MultiSignTransaction.numsPoint); } const partialSignatures = []; if (input.finalScriptWitness) { const decoded = TransactionBuilder.readScriptWitnessToWitnessStack(input.finalScriptWitness); input.tapLeafScript = [ { leafVersion: 192, script: decoded[decoded.length - 2], controlBlock: decoded[decoded.length - 1], }, ]; for (let j = 0; j < decoded.length - 2; j += 3) { partialSignatures.push({ signature: decoded[j], leafHash: decoded[j + 1], pubkey: decoded[j + 2], }); } input.tapScriptSig = (input.tapScriptSig || []).concat(partialSignatures); } delete input.finalScriptWitness; const signHashTypes = MultiSignTransaction.signHashTypesArray ? [MultiSignTransaction.calculateSignHash(MultiSignTransaction.signHashTypesArray)] : []; try { MultiSignTransaction.signInput(psbt, input, i, signer, signHashTypes); signed = true; } catch (e) { console.log(e); } if (signed) { if (!input.tapScriptSig) throw new Error('No new signatures for input'); if (input.tapScriptSig.length !== minimums[i - originalInputCount]) { final = false; } } } return { signed, final: !signed ? false : final, }; } static dedupeSignatures(original, partial) { const signatures = new Map(); for (const sig of original) { signatures.set(sig.pubkey.toString('hex'), sig); } for (const sig of partial) { if (!signatures.has(sig.pubkey.toString('hex'))) { signatures.set(sig.pubkey.toString('hex'), sig); } } return Array.from(signatures.values()); } static attemptFinalizeInputs(psbt, startIndex, orderedPubKeys, isFinal) { let finalizedInputs = 0; for (let i = startIndex; i < psbt.data.inputs.length; i++) { try { const input = psbt.data.inputs[i]; if (!input.tapInternalKey) { input.tapInternalKey = toXOnly(MultiSignTransaction.numsPoint); } const partialSignatures = []; if (input.finalScriptWitness) { const decoded = TransactionBuilder.readScriptWitnessToWitnessStack(input.finalScriptWitness); for (let j = 0; j < decoded.length - 2; j += 3) { partialSignatures.push({ signature: decoded[j], leafHash: decoded[j + 1], pubkey: decoded[j + 2], }); } input.tapLeafScript = [ { leafVersion: 192, script: decoded[decoded.length - 2], controlBlock: decoded[decoded.length - 1], }, ]; input.tapScriptSig = MultiSignTransaction.dedupeSignatures(input.tapScriptSig || [], partialSignatures); } delete input.finalScriptWitness; psbt.finalizeInput(i, (inputIndex, input) => { return MultiSignTransaction.partialFinalizer(inputIndex, input, [], orderedPubKeys[i - startIndex], isFinal); }); finalizedInputs++; } catch (e) { } } return finalizedInputs === psbt.data.inputs.length - startIndex; } finalizeTransactionInputs() { let finalized = false; try { for (let i = this.originalInputCount; i < this.transaction.data.inputs.length; i++) { this.transaction.finalizeInput(i, this.customFinalizer.bind(this)); } finalized = true; } catch (e) { this.error(`Error finalizing transaction inputs: ${e.stack}`); } return finalized; } async signPSBT() { if (await this.signTransaction()) { return this.transaction; } throw new Error('Could not sign transaction'); } async buildTransaction() { const selectedRedeem = this.targetScriptRedeem; if (!selectedRedeem) { throw new Error('Left over funds script redeem is required'); } if (!selectedRedeem.redeemVersion) { throw new Error('Left over funds script redeem version is required'); } if (!selectedRedeem.output) { throw new Error('Left over funds script redeem output is required'); } this.tapLeafScript = { leafVersion: selectedRedeem.redeemVersion, script: selectedRedeem.output, controlBlock: this.getWitness(), }; this.addInputsFromUTXO(); const outputLeftAmount = this.calculateOutputLeftAmountFromVaults(this.utxos); if (outputLeftAmount < 0) { throw new Error(`Output value left is negative ${outputLeftAmount}.`); } this.addOutput({ address: this.refundVault, value: Number(outputLeftAmount), }); this.addOutput({ address: this.receiver, value: Number(this.requestedAmount), }); } async internalBuildTransaction(transaction, checkPartialSigs = false) { const inputs = this.getInputs(); const outputs = this.getOutputs(); transaction.setMaximumFeeRate(this._maximumFeeRate); transaction.addInputs(inputs, checkPartialSigs); for (let i = 0; i < this.updateInputs.length; i++) { transaction.updateInput(i, this.updateInputs[i]); } transaction.addOutputs(outputs); try { await this.signInputs(transaction); return this.finalizeTransactionInputs(); } catch (e) { const err = e; this.error(`[internalBuildTransaction] Something went wrong while getting building the transaction: ${err.stack}`); } return false; } async signInputs(_transaction) { } generateScriptAddress() { return { internalPubkey: toXOnly(MultiSignTransaction.numsPoint), network: this.network, scriptTree: this.scriptTree, name: PaymentType.P2TR, }; } generateTapData() { const selectedRedeem = this.targetScriptRedeem; if (!selectedRedeem) { throw new Error('Left over funds script redeem is required'); } if (!this.scriptTree) { throw new Error('Script tree is required'); } return { internalPubkey: toXOnly(MultiSignTransaction.numsPoint), network: this.network, scriptTree: this.scriptTree, redeem: selectedRedeem, name: PaymentType.P2TR, }; } getScriptSolution(input) { if (!input.tapScriptSig) { return []; } return input.tapScriptSig.map((sig) => { return sig.signature; }); } getScriptTree() { this.generateRedeemScripts(); return [ { output: this.compiledTargetScript, version: 192, }, { output: MultiSignTransaction.LOCK_LEAF_SCRIPT, version: 192, }, ]; } getTotalOutputAmount(utxos) { let total = BigInt(0); for (const utxo of utxos) { total += BigInt(utxo.value); } return total; } calculateOutputLeftAmountFromVaults(utxos) { const total = this.getTotalOutputAmount(utxos); return total - this.requestedAmount; } generateRedeemScripts() { this.targetScriptRedeem = { name: PaymentType.P2TR, output: this.compiledTargetScript, redeemVersion: 192, }; this.leftOverFundsScriptRedeem = { name: PaymentType.P2TR, output: MultiSignTransaction.LOCK_LEAF_SCRIPT, redeemVersion: 192, }; } } MultiSignTransaction.LOCK_LEAF_SCRIPT = script.compile([ opcodes.OP_XOR, opcodes.OP_NOP, opcodes.OP_CODESEPARATOR, ]); MultiSignTransaction.signHashTypesArray = []; MultiSignTransaction.numsPoint = Buffer.from('50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0', 'hex'); MultiSignTransaction.partialFinalizer = (inputIndex, input, partialSignatures, orderedPubKeys, isFinal) => { if (!input.tapLeafScript || !input.tapLeafScript[0].script || !input.tapLeafScript[0].controlBlock) { throw new Error('Tap leaf script is required'); } if (!input.tapScriptSig) { throw new Error(`No new signatures for input ${inputIndex}.`); } let scriptSolution = []; if (!isFinal) { scriptSolution = input.tapScriptSig .map((sig) => { return [sig.signature, sig.leafHash, sig.pubkey]; }) .flat(); } else { for (const pubKey of orderedPubKeys) { let found = false; for (const sig of input.tapScriptSig) { if (sig.pubkey.equals(toXOnly(pubKey))) { scriptSolution.push(sig.signature); found = true; } } if (!found) { scriptSolution.push(Buffer.alloc(0)); } } scriptSolution = scriptSolution.reverse(); } if (partialSignatures.length > 0) { scriptSolution = scriptSolution.concat(partialSignatures); } const witness = scriptSolution .concat(input.tapLeafScript[0].script) .concat(input.tapLeafScript[0].controlBlock); return { finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witness), }; };