UNPKG

lotus-sdk

Version:

Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem

1,017 lines (1,016 loc) 36.6 kB
import { Preconditions } from '../util/preconditions.js'; import { JSUtil } from '../util/js.js'; import { BufferReader } from '../encoding/bufferreader.js'; import { BufferWriter } from '../encoding/bufferwriter.js'; import { Hash } from '../crypto/hash.js'; import { Signature } from '../crypto/signature.js'; import { verify } from './sighash.js'; import { BitcoreError } from '../errors.js'; import { Address } from '../address.js'; import { UnspentOutput } from './unspentoutput.js'; import { Input, MultisigInput, MultisigScriptHashInput, PublicKeyInput, PublicKeyHashInput, TaprootInput, MuSigTaprootInput, } from './input.js'; import { Output } from './output.js'; import { Script } from '../script.js'; import { PrivateKey } from '../privatekey.js'; import { BN } from '../crypto/bn.js'; import { sighash as computeSighash } from './sighash.js'; import { Interpreter } from '../script/interpreter.js'; const CURRENT_VERSION = 2; const DEFAULT_NLOCKTIME = 0; const MAX_BLOCK_SIZE = 32_000_000; const DUST_AMOUNT = 546; const FEE_SECURITY_MARGIN = 150; const MAX_MONEY = 2_100_000_000_000_000; const NLOCKTIME_BLOCKHEIGHT_LIMIT = 5e8; const NLOCKTIME_MAX_VALUE = 4294967295; const FEE_PER_KB = 1_000; const CHANGE_OUTPUT_MAX_SIZE = 20 + 4 + 34 + 4; const MAXIMUM_EXTRA_SIZE = 4 + 9 + 9 + 4; const NULL_HASH = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); export class Transaction { static DUST_AMOUNT = DUST_AMOUNT; static FEE_SECURITY_MARGIN = FEE_SECURITY_MARGIN; static NLOCKTIME_BLOCKHEIGHT_LIMIT = NLOCKTIME_BLOCKHEIGHT_LIMIT; static NLOCKTIME_MAX_VALUE = NLOCKTIME_MAX_VALUE; static FEE_PER_KB = FEE_PER_KB; static CHANGE_OUTPUT_MAX_SIZE = CHANGE_OUTPUT_MAX_SIZE; static MAXIMUM_EXTRA_SIZE = MAXIMUM_EXTRA_SIZE; static NULL_HASH = NULL_HASH; inputs = []; outputs = []; _version = CURRENT_VERSION; nLockTime = DEFAULT_NLOCKTIME; _inputAmount; _outputAmount; _changeScript; _changeIndex; _fee; _feePerKb; _feePerByte; _hash; _txid; get spentOutputs() { if (!this.inputs.every(input => input.output)) { return undefined; } return this.inputs.map(input => input.output); } constructor(serialized) { if (serialized instanceof Transaction) { return Transaction.shallowCopy(serialized); } else if (typeof serialized === 'string' && JSUtil.isHexa(serialized)) { this.fromString(serialized); } else if (Buffer.isBuffer(serialized)) { this.fromBuffer(serialized); } else if (serialized && typeof serialized === 'object') { this.fromObject(serialized); } else { this._newTransaction(); } } static create(serialized) { return new Transaction(serialized); } static shallowCopy(transaction) { const copy = new Transaction(transaction.toBuffer()); return copy; } static fromBuffer(buffer) { return new Transaction(buffer); } static fromBufferReader(reader) { return new Transaction().fromBufferReader(reader); } static fromObject(arg) { return new Transaction(arg); } static fromString(str) { return new Transaction(str); } get hash() { if (!this._hash) { const hashBuffer = this._getHash(); const reader = new BufferReader(hashBuffer); this._hash = reader.readReverse(32).toString('hex'); } return this._hash; } get id() { return this.txid; } get txid() { if (!this._txid) { const txidBuffer = this._getTxid(); const reader = new BufferReader(txidBuffer); this._txid = reader.readReverse(32).toString('hex'); } return this._txid; } get inputAmount() { return this._getInputAmount(); } get outputAmount() { return this._getOutputAmount(); } get version() { return this._version; } feePerByte(feePerByte) { this._feePerByte = feePerByte; this._updateChangeOutput(); } fee(amount) { this._fee = amount; this._updateChangeOutput(); } feePerKb(amount) { this._feePerKb = amount; this._updateChangeOutput(); } set version(version) { this._version = version; } _getHash() { return Hash.sha256sha256(this.toBuffer()); } _getTxid() { const writer = new BufferWriter(); writer.writeInt32LE(this.version); const inputHashes = this._getTxInputHashes(); const outputHashes = this._getTxOutputHashes(); const inputMerkleRootAndHeight = this._computeMerkleRoot(inputHashes); const outputMerkleRootAndHeight = this._computeMerkleRoot(outputHashes); writer.write(inputMerkleRootAndHeight.root); writer.writeUInt8(inputMerkleRootAndHeight.height); writer.write(outputMerkleRootAndHeight.root); writer.writeUInt8(outputMerkleRootAndHeight.height); writer.writeUInt32LE(this.nLockTime); return Hash.sha256sha256(writer.toBuffer()); } _getTxInputHashes() { const hashes = []; if (this.inputs.length === 0) { return [Transaction.NULL_HASH]; } for (let i = 0; i < this.inputs.length; i++) { const input = this.inputs[i]; const writer = new BufferWriter(); writer.writeReverse(input.prevTxId); writer.writeUInt32LE(input.outputIndex); writer.writeUInt32LE(Number(input.sequenceNumber)); const hash = Hash.sha256sha256(writer.toBuffer()); hashes.push(hash); } return hashes; } _getTxOutputHashes() { const hashes = []; if (this.outputs.length === 0) { return [Transaction.NULL_HASH]; } for (let i = 0; i < this.outputs.length; i++) { const output = this.outputs[i]; const writer = new BufferWriter(); writer.writeUInt64LEBN(new BN(output.satoshis)); writer.writeVarLengthBuffer(output.scriptBuffer); const hash = Hash.sha256sha256(writer.toBuffer()); hashes.push(hash); } return hashes; } _computeMerkleRoot(hashes) { if (hashes.length === 0) { return { root: Transaction.NULL_HASH, height: 0, }; } let j = 0; let height = 1; for (let size = hashes.length; size > 1; size = Math.floor(size / 2)) { height += 1; if (size % 2 === 1) { hashes.push(Transaction.NULL_HASH); size += 1; } for (let i = 0; i < size; i += 2) { const buf = Buffer.concat([hashes[j + i], hashes[j + i + 1]]); hashes.push(Hash.sha256sha256(buf)); } j += size; } return { root: hashes[hashes.length - 1], height: height, }; } _getInputAmount() { if (this._inputAmount !== undefined) { return this._inputAmount; } let total = 0; for (const input of this.inputs) { if (input.output && input.output.satoshis) { total += input.output.satoshis; } } this._inputAmount = total; return total; } _getOutputAmount() { if (this._outputAmount !== undefined) { return this._outputAmount; } let total = 0; for (const output of this.outputs) { total += output.satoshis; } this._outputAmount = total; return total; } _newTransaction() { this.version = CURRENT_VERSION; this.nLockTime = DEFAULT_NLOCKTIME; } serialize(unsafe) { if (unsafe === true || (unsafe && unsafe.disableAll)) { return this.uncheckedSerialize(); } else { return this.checkedSerialize(unsafe); } } uncheckedSerialize() { return this.toBuffer().toString('hex'); } checkedSerialize(opts) { const serializationError = this.getSerializationError(opts); if (serializationError) { serializationError.message += ' - For more information please see: ' + 'https://bitcore.io/api/lib/transaction#serialization-checks'; throw serializationError; } return this.uncheckedSerialize(); } getSerializationError(opts) { const dustError = this._hasDustOutputs(opts); if (dustError) return dustError; const sigError = this._isMissingSignatures(opts); if (sigError) return sigError; if (this._hasInvalidSatoshis()) { return new BitcoreError('Invalid satoshis in outputs'); } return null; } _hasDustOutputs(opts) { if (opts && opts.disableDustOutputs) { return null; } for (const output of this.outputs) { if (output.satoshis < Transaction.DUST_AMOUNT && !output.isOpReturn()) { return new BitcoreError('Dust outputs not allowed'); } } return null; } _isMissingSignatures(opts) { if (opts && opts.disableIsFullySigned) { return null; } if (!this.isFullySigned()) { return new BitcoreError('Transaction is not fully signed'); } return null; } _hasInvalidSatoshis() { for (const output of this.outputs) { if (output.satoshis < 0) { return true; } } return false; } isFullySigned() { for (const input of this.inputs) { if (!input.script || input.script.chunks.length === 0) { return false; } } return true; } toBuffer() { const writer = new BufferWriter(); return this.toBufferWriter(writer).toBuffer(); } toBufferWriter(writer) { if (!writer) { writer = new BufferWriter(); } writer.writeInt32LE(this.version); writer.writeVarintNum(this.inputs.length); for (const input of this.inputs) { input.toBufferWriter(writer); } writer.writeVarintNum(this.outputs.length); for (const output of this.outputs) { writer.writeUInt64LEBN(new BN(output.satoshis)); writer.writeVarLengthBuffer(output.scriptBuffer); } writer.writeUInt32LE(this.nLockTime); return writer; } fromBuffer(buffer) { const reader = new BufferReader(buffer); return this.fromBufferReader(reader); } fromBufferReader(reader) { Preconditions.checkArgument(!reader.finished(), 'No transaction data received'); this.version = reader.readInt32LE(); const sizeTxIns = reader.readVarintNum(); for (let i = 0; i < sizeTxIns; i++) { const input = Input.fromBufferReader(reader); this.inputs.push(input); } const sizeTxOuts = reader.readVarintNum(); for (let i = 0; i < sizeTxOuts; i++) { const output = Output.fromBufferReader(reader); this.outputs.push(output); } this.nLockTime = reader.readUInt32LE(); return this; } fromString(str) { return this.fromBuffer(Buffer.from(str, 'hex')); } toObject() { const inputs = this.inputs.map(input => input.toObject()); const outputs = this.outputs.map(output => output.toObject()); const obj = { txid: this.txid, hash: this.hash, version: this.version, inputs: inputs, outputs: outputs, nLockTime: this.nLockTime, }; if (this._changeScript) { obj.changeScript = this._changeScript.toString(); obj.changeAsm = this._changeScript.toASM(); } if (this._changeIndex !== undefined) { obj.changeIndex = this._changeIndex; } if (this._fee !== undefined) { obj.fee = this._fee; } return obj; } toJSON = this.toObject; fromObject(arg) { Preconditions.checkArgument(typeof arg === 'object' && arg !== null, 'Must provide an object to deserialize a transaction'); let transaction; if (arg instanceof Transaction) { const obj = arg.toObject(); transaction = { version: obj.version, nLockTime: obj.nLockTime, inputs: obj.inputs, outputs: obj.outputs, changeScript: obj.changeScript, changeIndex: obj.changeIndex, fee: obj.fee, }; } else { transaction = arg; } this.inputs = []; this.outputs = []; this.version = transaction.version || CURRENT_VERSION; this.nLockTime = transaction.nLockTime || DEFAULT_NLOCKTIME; if (transaction.inputs) { for (const inputData of transaction.inputs) { const input = new Input({ prevTxId: inputData.prevTxId, outputIndex: inputData.outputIndex, sequenceNumber: inputData.sequenceNumber, script: inputData.script || undefined, scriptBuffer: inputData.scriptBuffer, output: inputData.output, }); this.inputs.push(input); } } if (transaction.outputs) { for (const outputData of transaction.outputs) { const output = new Output({ satoshis: outputData.satoshis, script: outputData.script, }); this.outputs.push(output); } } if (transaction.changeScript) { this._changeScript = typeof transaction.changeScript === 'string' ? Script.fromString(transaction.changeScript) : transaction.changeScript; } if (transaction.changeIndex !== undefined) { this._changeIndex = transaction.changeIndex; } if (transaction.fee !== undefined) { this._fee = transaction.fee; } return this; } addInput(input) { this.inputs.push(input); this._inputAmount = undefined; return this; } addOutput(output) { this._addOutput(output); this._updateChangeOutput(); return this; } clone() { return Transaction.shallowCopy(this); } toString() { return this.uncheckedSerialize(); } inspect() { return '<Transaction: ' + this.uncheckedSerialize() + '>'; } from(utxos, pubkeys, threshold, opts) { if (Array.isArray(utxos)) { for (const utxo of utxos) { this.from(utxo, pubkeys, threshold, opts); } return this; } const exists = this.inputs.some(input => input.prevTxId.toString('hex') === utxos.txId && input.outputIndex === utxos.outputIndex); if (exists) { return this; } const utxo = utxos instanceof UnspentOutput ? utxos : new UnspentOutput(utxos); if (pubkeys && threshold) { this._fromMultisigUtxo(utxo, pubkeys, threshold, opts); } else { this._fromNonP2SH(utxo); } return this; } change(address) { this._changeScript = Script.fromAddress(address); this._updateChangeOutput(); return this; } to(address, amount) { if (Array.isArray(address)) { for (const to of address) { this.to(to.address, to.satoshis); } return this; } Preconditions.checkArgument(JSUtil.isNaturalNumber(amount), 'Amount is expected to be a positive integer'); this.addOutput(new Output({ script: Script.fromAddress(new Address(address)), satoshis: amount, })); return this; } sign(privateKey, sigtype, signingMethod) { const privKeys = Array.isArray(privateKey) ? privateKey : [privateKey]; const sigtypeDefault = sigtype || Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID; for (const privKey of privKeys) { const signatures = this.getSignatures(privKey, sigtypeDefault, signingMethod); for (const signature of signatures) { this.applySignature(signature, signingMethod); } } return this; } signSchnorr(privateKey) { return this.sign(privateKey, Signature.SIGHASH_ALL | Signature.SIGHASH_LOTUS, 'schnorr'); } getMuSig2Inputs() { return this.inputs.filter(input => input instanceof MuSigTaprootInput); } getMuSig2Sighash(inputIndex) { const input = this.inputs[inputIndex]; if (!(input instanceof MuSigTaprootInput)) { throw new Error(`Input ${inputIndex} is not a MuSigTaprootInput`); } if (!input.output) { throw new Error(`Input ${inputIndex} is missing output information`); } const sigtype = Signature.SIGHASH_ALL | Signature.SIGHASH_LOTUS; return computeSighash(this, sigtype, inputIndex, input.output.script, new BN(input.output.satoshis)); } addMuSig2Nonce(inputIndex, signerIndex, nonce) { const input = this.inputs[inputIndex]; if (!(input instanceof MuSigTaprootInput)) { throw new Error(`Input ${inputIndex} is not a MuSigTaprootInput`); } input.addPublicNonce(signerIndex, nonce); return this; } addMuSig2PartialSignature(inputIndex, signerIndex, partialSig) { const input = this.inputs[inputIndex]; if (!(input instanceof MuSigTaprootInput)) { throw new Error(`Input ${inputIndex} is not a MuSigTaprootInput`); } input.addPartialSignature(signerIndex, partialSig); return this; } finalizeMuSig2Signatures() { const musigInputs = this.getMuSig2Inputs(); for (let i = 0; i < this.inputs.length; i++) { const input = this.inputs[i]; if (input instanceof MuSigTaprootInput) { if (!input.hasAllPartialSignatures()) { throw new Error(`MuSig2 input ${i} is missing partial signatures. ` + `Has ${input.partialSignatures?.size || 0} of ${input.keyAggContext?.pubkeys.length || 0}`); } const sighash = this.getMuSig2Sighash(i); input.finalizeMuSigSignature(this, sighash); } } return this; } getSignatures(privKey, sigtype, signingMethod) { const privateKey = new PrivateKey(privKey); const sigtypeDefault = sigtype || Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID; const results = []; const hashData = Hash.sha256ripemd160(privateKey.publicKey.toBuffer()); for (let index = 0; index < this.inputs.length; index++) { const input = this.inputs[index]; const signatures = input.getSignatures(this, privateKey, index, sigtypeDefault, hashData, signingMethod); for (const signature of signatures) { results.push(signature); } } return results; } applySignature(signature, signingMethod) { this.inputs[signature.inputIndex].addSignature(this, signature, signingMethod); return this; } isValidSignature(sig) { const input = this.inputs[sig.inputIndex]; return input.isValidSignature(this, sig); } verifySignature(sig, pubkey, nin, subscript, satoshisBN, flags, signingMethod) { return verify(this, sig, pubkey, nin, subscript, satoshisBN, flags, signingMethod); } lockUntilDate(time) { Preconditions.checkArgument(!!time, 'time is required'); if (typeof time === 'number' && time < Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) { throw new Error('Lock time too early'); } if (time instanceof Date) { time = time.getTime() / 1000; } for (let i = 0; i < this.inputs.length; i++) { if (this.inputs[i].sequenceNumber === Input.DEFAULT_SEQNUMBER) { this.inputs[i].sequenceNumber = Input.DEFAULT_LOCKTIME_SEQNUMBER; } } this.nLockTime = time; return this; } lockUntilBlockHeight(height) { Preconditions.checkArgument(typeof height === 'number', 'height must be a number'); if (height >= Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) { throw new Error('Block height too high'); } if (height < 0) { throw new Error('NLockTime out of range'); } for (let i = 0; i < this.inputs.length; i++) { if (this.inputs[i].sequenceNumber === Input.DEFAULT_SEQNUMBER) { this.inputs[i].sequenceNumber = Input.DEFAULT_LOCKTIME_SEQNUMBER; } } this.nLockTime = height; return this; } getLockTime() { if (!this.nLockTime) { return null; } if (this.nLockTime < Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) { return this.nLockTime; } return new Date(1000 * this.nLockTime); } hasAllUtxoInfo() { return this.inputs.every(input => !!input.output); } addData(value) { this.addOutput(new Output({ script: Script.buildDataOut(value), satoshis: 0, })); return this; } clearOutputs() { this.outputs = []; this._clearSignatures(); this._outputAmount = undefined; this._changeIndex = undefined; this._updateChangeOutput(); return this; } removeOutput(index) { this._removeOutput(index); this._updateChangeOutput(); } sort() { this.sortInputs(inputs => { const copy = [...inputs]; let i = 0; copy.forEach(x => { ; x.i = i++; }); copy.sort((first, second) => { const prevTxIdCompare = Buffer.compare(first.prevTxId, second.prevTxId); if (prevTxIdCompare !== 0) return prevTxIdCompare; const outputIndexCompare = first.outputIndex - second.outputIndex; if (outputIndexCompare !== 0) return outputIndexCompare; return (first.i - second.i); }); return copy; }); this.sortOutputs(outputs => { const copy = [...outputs]; let i = 0; copy.forEach(x => { ; x.i = i++; }); copy.sort((first, second) => { const satoshisCompare = first.satoshis - second.satoshis; if (satoshisCompare !== 0) return satoshisCompare; const scriptCompare = Buffer.compare(first.scriptBuffer, second.scriptBuffer); if (scriptCompare !== 0) return scriptCompare; return (first.i - second.i); }); return copy; }); return this; } sortInputs(sortingFunction) { this.inputs = sortingFunction(this.inputs); return this; } sortOutputs(sortingFunction) { const sortedOutputs = sortingFunction(this.outputs); return this._newOutputOrder(sortedOutputs); } shuffleOutputs() { return this.sortOutputs(outputs => { const shuffled = [...outputs]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; }); } removeInput(index) { this.inputs.splice(index, 1); this._inputAmount = undefined; } getFee() { if (this.isCoinbase()) { return 0; } if (this._fee !== undefined) { return this._fee; } if (!this._changeScript) { return this._getUnspentValue(); } return this._estimateFee(); } getChangeOutput() { if (this._changeIndex !== undefined) { return this.outputs[this._changeIndex]; } return null; } verify() { if (this.inputs.length === 0) { return 'transaction inputs empty'; } if (this.outputs.length === 0) { return 'transaction outputs empty'; } if (this.toBuffer().length > MAX_BLOCK_SIZE) { return 'transaction over the maximum block size'; } let totalOutput = 0; for (let i = 0; i < this.outputs.length; i++) { const output = this.outputs[i]; if (output.satoshis < 0) { return 'transaction output ' + i + ' satoshis is negative'; } if (output.satoshis > MAX_MONEY) { return 'transaction output ' + i + ' greater than MAX_MONEY'; } totalOutput += output.satoshis; if (totalOutput > MAX_MONEY) { return ('transaction output ' + i + ' total output greater than MAX_MONEY'); } } if (this.isCoinbase()) { const coinbaseScript = this.inputs[0].scriptBuffer; if (!coinbaseScript || coinbaseScript.length < 2 || coinbaseScript.length > 100) { return 'coinbase transaction script size invalid'; } return true; } if (!this.hasAllUtxoInfo()) { return 'Missing previous output information'; } if (this.inputAmount < this.outputAmount) { return 'transaction input amount is less than output amount'; } const actualFee = this.inputAmount - this.outputAmount; const txSize = this.toBuffer().length; const feeRatePerByte = Transaction.FEE_PER_KB / 1000; const minRequiredFee = Math.ceil(txSize * feeRatePerByte); if (actualFee < minRequiredFee) { return `transaction fee too low: ${actualFee} < ${minRequiredFee} (minimum ${feeRatePerByte} satoshi/byte)`; } const inputSet = new Set(); for (let i = 0; i < this.inputs.length; i++) { const input = this.inputs[i]; if (input.prevTxId.equals(Transaction.NULL_HASH)) { return 'transaction input ' + i + ' has null input'; } const inputId = input.prevTxId.toString('hex') + ':' + input.outputIndex.toString(); if (inputSet.has(inputId)) { return 'transaction input ' + i + ' duplicate input'; } inputSet.add(inputId); } const scriptVerification = this._verifyScripts(); if (!scriptVerification.success) { return scriptVerification.error || 'Script verification failed'; } return true; } _verifyScripts(flags) { if (this.isCoinbase()) { return { success: true }; } if (!this.hasAllUtxoInfo()) { return { success: false, error: 'Missing UTXO (output) information for script verification', }; } for (let i = 0; i < this.inputs.length; i++) { const input = this.inputs[i]; if (!input.script || !input.output?.script) { return { success: false, error: `Input ${i} script verification failed: missing script`, }; } try { const verifyFlags = flags !== undefined ? flags : Interpreter.SCRIPT_VERIFY_P2SH | Interpreter.SCRIPT_VERIFY_STRICTENC | Interpreter.SCRIPT_VERIFY_DERSIG | Interpreter.SCRIPT_VERIFY_LOW_S | Interpreter.SCRIPT_VERIFY_NULLFAIL | Interpreter.SCRIPT_ENABLE_SIGHASH_FORKID | Interpreter.SCRIPT_ENABLE_SCHNORR_MULTISIG; const interpreter = new Interpreter(); const isValid = interpreter.verify(input.script, input.output.script, this, i, verifyFlags, BigInt(input.output.satoshis)); if (!isValid) { return { success: false, error: `Input ${i} script verification failed: ${interpreter.errstr}`, }; } } catch (error) { return { success: false, error: `Input ${i} script verification error: ${error instanceof Error ? error.message : String(error)}`, }; } } return { success: true }; } isCoinbase() { return (this.inputs.length === 1 && this.inputs[0].prevTxId.equals(Transaction.NULL_HASH) && this.inputs[0].outputIndex === 0xffffffff); } uncheckedAddInput(input) { Preconditions.checkArgument(input instanceof Input, 'input must be an Input'); this.inputs.push(input); this._inputAmount = undefined; this._updateChangeOutput(); return this; } _newOutputOrder(newOutputs) { const isInvalidSorting = this.outputs.length !== newOutputs.length || this.outputs.some((output, index) => output !== newOutputs[index]); if (isInvalidSorting) { throw new BitcoreError('Invalid sorting: outputs must contain the same elements'); } if (this._changeIndex !== undefined) { const changeOutput = this.outputs[this._changeIndex]; this._changeIndex = newOutputs.findIndex(output => output === changeOutput); } this.outputs = newOutputs; return this; } _fromNonP2SH(utxo) { let clazz; const unspentOutput = new UnspentOutput(utxo); if (unspentOutput.script.isPayToTaproot()) { if (unspentOutput.keyAggContext && unspentOutput.mySignerIndex !== undefined) { clazz = MuSigTaprootInput; const input = new MuSigTaprootInput({ output: new Output({ script: unspentOutput.script, satoshis: unspentOutput.satoshis, }), prevTxId: unspentOutput.txId, outputIndex: unspentOutput.outputIndex, script: new Script(), keyAggContext: unspentOutput.keyAggContext, mySignerIndex: unspentOutput.mySignerIndex, }); this.addInput(input); return; } clazz = TaprootInput; const taprootInput = new TaprootInput({ output: new Output({ script: unspentOutput.script, satoshis: unspentOutput.satoshis, }), prevTxId: unspentOutput.txId, outputIndex: unspentOutput.outputIndex, script: new Script(), internalPubKey: unspentOutput.internalPubKey, merkleRoot: unspentOutput.merkleRoot, }); this.addInput(taprootInput); return; } else if (unspentOutput.script.isPayToPublicKeyHash()) { clazz = PublicKeyHashInput; } else if (unspentOutput.script.isPublicKeyOut()) { clazz = PublicKeyInput; } else { clazz = Input; } this.addInput(new clazz({ output: new Output({ script: unspentOutput.script, satoshis: unspentOutput.satoshis, }), prevTxId: unspentOutput.txId, outputIndex: unspentOutput.outputIndex, script: new Script(), })); } _fromMultisigUtxo(utxo, pubkeys, threshold, opts) { Preconditions.checkArgument(threshold <= pubkeys.length, 'Number of required signatures must be greater than the number of public keys'); const unspentOutput = new UnspentOutput(utxo); if (unspentOutput.script.isMultisigOut()) { this.addInput(new MultisigInput(new Input({ output: new Output({ script: unspentOutput.script, satoshis: unspentOutput.satoshis, }), prevTxId: unspentOutput.txId, outputIndex: unspentOutput.outputIndex, script: new Script(), }), pubkeys, threshold, undefined, opts)); } else if (unspentOutput.script.isPayToScriptHash()) { this.addInput(new MultisigScriptHashInput(new Input({ output: new Output({ script: unspentOutput.script, satoshis: unspentOutput.satoshis, }), prevTxId: unspentOutput.txId, outputIndex: unspentOutput.outputIndex, script: new Script(), }), pubkeys, threshold, undefined, opts)); } else { throw new Error('Unsupported script type'); } } _updateChangeOutput() { if (!this._changeScript) { return; } this._clearSignatures(); if (this._changeIndex !== undefined) { this._removeOutput(this._changeIndex); } const available = this._getUnspentValue(); const fee = this.getFee(); const changeAmount = available - fee; if (changeAmount >= Transaction.DUST_AMOUNT) { this._changeIndex = this.outputs.length; this._addOutput(new Output({ script: this._changeScript, satoshis: changeAmount, })); } else { this._changeIndex = undefined; } } _getUnspentValue() { return this._getInputAmount() - this._getOutputAmount(); } _clearSignatures() { for (const input of this.inputs) { input.clearSignatures(); } } _estimateFee() { const estimatedSize = this._estimateSize(); const available = this._getUnspentValue(); const feeRate = this._feePerByte || (this._feePerKb || Transaction.FEE_PER_KB) / 1000; const getFee = (size) => size * feeRate; const fee = Math.ceil(getFee(estimatedSize)); const feeWithChange = Math.ceil(getFee(estimatedSize) + getFee(Transaction.CHANGE_OUTPUT_MAX_SIZE)); if (!this._changeScript || available <= feeWithChange) { return fee; } return feeWithChange; } static _getVarintSize(n) { if (n < 253) return 1; if (n < 0x10000) return 3; if (n < 0x100000000) return 5; return 9; } _estimateSize() { let result = 4; result += Transaction._getVarintSize(this.inputs.length); for (const input of this.inputs) { result += 40; const scriptSigLen = input._estimateSize(); result += Transaction._getVarintSize(scriptSigLen); result += scriptSigLen; } result += Transaction._getVarintSize(this.outputs.length); for (const output of this.outputs) { result += output.getSize(); } result += 4; return result; } _removeOutput(index) { this.outputs.splice(index, 1); this._outputAmount = undefined; } _addOutput(output) { this.outputs.push(output); this._outputAmount = undefined; } }