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
JavaScript
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;
}
}