bitcore-lib
Version:
A pure and powerful JavaScript Bitcoin library.
1,296 lines (1,201 loc) • 114 kB
JavaScript
'use strict';
/* jshint unused: false */
/* jshint latedef: false */
const should = require('chai').should();
const expect = require('chai').expect;
const _ = require('lodash');
const sinon = require('sinon');
const bitcore = require('../..');
const BN = bitcore.crypto.BN;
const Transaction = bitcore.Transaction;
const Input = bitcore.Transaction.Input;
const Output = bitcore.Transaction.Output;
const PrivateKey = bitcore.PrivateKey;
const Script = bitcore.Script;
const Interpreter = bitcore.Script.Interpreter;
const Address = bitcore.Address;
const Networks = bitcore.Networks;
const Opcode = bitcore.Opcode;
const errors = bitcore.errors;
const transactionVector = require('../data/tx_creation');
const taprootVectors = require('../data/bitcoind/wallet_test_vectors.json');
describe('Transaction', function() {
it('should serialize and deserialize correctly a given transaction', function() {
var transaction = new Transaction(tx_1_hex);
transaction.uncheckedSerialize().should.equal(tx_1_hex);
});
it('should parse the version as a signed integer', function () {
var transaction = Transaction('ffffffff0000ffffffff')
transaction.version.should.equal(-1);
transaction.nLockTime.should.equal(0xffffffff);
});
it('fails if an invalid parameter is passed to constructor', function() {
expect(function() {
return new Transaction(1);
}).to.throw(errors.InvalidArgument);
});
var testScript = 'OP_DUP OP_HASH160 20 0x88d9931ea73d60eaf7e5671efc0552b912911f2a OP_EQUALVERIFY OP_CHECKSIG';
var testScriptHex = '76a91488d9931ea73d60eaf7e5671efc0552b912911f2a88ac';
var testPrevTx = 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458';
var testAmount = 1020000;
var testTransaction = new Transaction()
.from({
'txId': testPrevTx,
'outputIndex': 0,
'script': testScript,
'satoshis': testAmount
})
.to('mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc', testAmount - 10000);
it('can serialize to a plain javascript object', function() {
var object = testTransaction.toObject();
object.inputs[0].output.satoshis.should.equal(testAmount);
object.inputs[0].output.script.should.equal(testScriptHex);
object.inputs[0].prevTxId.should.equal(testPrevTx);
object.inputs[0].outputIndex.should.equal(0);
object.outputs[0].satoshis.should.equal(testAmount - 10000);
});
it('will not accept NaN as an amount', function() {
(function() {
var stringTx = new Transaction().to('mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc', NaN);
}).should.throw('Amount is expected to be a positive integer');
});
it('returns the fee correctly', function() {
testTransaction.getFee().should.equal(10000);
});
it('will return zero as the fee for a coinbase', function() {
// block #2: 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098
var coinbaseTransaction = new Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000');
coinbaseTransaction.getFee().should.equal(0);
});
it('serialize to Object roundtrip', function() {
var a = testTransaction.toObject();
var newTransaction = new Transaction(a);
var b = newTransaction.toObject();
a.should.deep.equal(b);
});
it('toObject/fromObject with signatures and custom fee', function() {
var tx = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to([{address: toAddress, satoshis: 50000}])
.fee(15000)
.change(changeAddress)
.sign(privateKey);
var txData = JSON.stringify(tx);
var tx2 = new Transaction(JSON.parse(txData));
var txData2 = JSON.stringify(tx2);
txData.should.equal(txData2);
});
it('toObject/fromObject with p2sh signatures and custom fee', function() {
var tx = new Transaction()
.from(p2shUtxoWith1BTC, [p2shPublicKey1, p2shPublicKey2, p2shPublicKey3], 2)
.to([{address: toAddress, satoshis: 50000}])
.fee(15000)
.change(changeAddress)
.sign(p2shPrivateKey1)
.sign(p2shPrivateKey2);
var txData = JSON.stringify(tx);
var tx2 = new Transaction(JSON.parse(txData));
var tx2Data = JSON.stringify(tx2);
txData.should.equal(tx2Data);
});
it('toObject/fromObject with p2wsh signatures and custom fee', function() {
var tx = new Transaction()
.from(p2wshUtxoWith1BTC, [p2shPublicKey1, p2shPublicKey2, p2shPublicKey3], 2)
.to([{address: toAddress, satoshis: 50000}])
.fee(15000)
.change(changeAddress)
.sign(p2shPrivateKey1)
.sign(p2shPrivateKey2);
var txData = JSON.stringify(tx);
var tx2 = new Transaction(JSON.parse(txData));
var tx2Data = JSON.stringify(tx2);
txData.should.equal(tx2Data);
});
it('fromObject with pay-to-public-key previous outputs', function() {
var tx = bitcore.Transaction({
hash: '132856bf03d6415562a556437d22ac63c37a4595fd986c796eb8e02dc031aa25',
version: 1,
inputs: [
{
prevTxId: 'e30ac3db24ef28500f023775d8eb06ad8a26241690080260308208a4020012a4',
outputIndex: 0,
sequenceNumber: 4294967294,
script: '473044022024dbcf41ccd4f3fe325bebb7a87d0bf359eefa03826482008e0fe7795586ad440220676f5f211ebbc311cfa631f14a8223a343cbadc6fa97d6d17f8d2531308b533201',
scriptString: '71 0x3044022024dbcf41ccd4f3fe325bebb7a87d0bf359eefa03826482008e0fe7795586ad440220676f5f211ebbc311cfa631f14a8223a343cbadc6fa97d6d17f8d2531308b533201',
output: {
satoshis: 5000000000,
script: '2103b1c65d65f1ff3fe145a4ede692460ae0606671d04e8449e99dd11c66ab55a7feac'
}
}
],
outputs: [
{
satoshis: 3999999040,
script: '76a914fa1e0abfb8d26e494375f47e04b4883c44dd44d988ac'
},
{
satoshis: 1000000000,
script: '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac'
}
],
nLockTime: 139
});
tx.inputs[0].should.be.instanceof(bitcore.Transaction.Input.PublicKey);
tx.inputs[0].output.satoshis.should.equal(5000000000);
tx.inputs[0].output.script.toHex().should.equal('2103b1c65d65f1ff3fe145a4ede692460ae0606671d04e8449e99dd11c66ab55a7feac');
});
it('toObject/fromObject with witness, signatures and custom fee', function() {
var tx = new Transaction()
.from(simpleWitnessUtxoWith1BTC)
.to([{address: toAddress, satoshis: 50000}])
.fee(15000)
.change(changeAddress)
.sign(privateKey);
var txData = JSON.stringify(tx);
var tx2 = new Transaction(JSON.parse(txData));
var txData2 = JSON.stringify(tx2);
txData.should.equal(txData2);
});
it('toObject/fromObject with nested witness, signatures and custom fee', function() {
var tx = new Transaction()
.from(simpleWrappedWitnessUtxoWith1BTC)
.to([{address: toAddress, satoshis: 50000}])
.fee(15000)
.change(changeAddress)
.sign(privateKey);
var txData = JSON.stringify(tx);
var tx2 = new Transaction(JSON.parse(txData));
var txData2 = JSON.stringify(tx2);
txData.should.equal(txData2);
});
it('constructor returns a shallow copy of another transaction', function() {
var transaction = new Transaction(tx_1_hex);
var copy = new Transaction(transaction);
copy.uncheckedSerialize().should.equal(transaction.uncheckedSerialize());
});
it('should display correctly in console', function() {
var transaction = new Transaction(tx_1_hex);
transaction.inspect().should.equal('<Transaction: ' + tx_1_hex + '>');
});
it('standard hash of transaction should be decoded correctly', function() {
var transaction = new Transaction(tx_1_hex);
transaction.id.should.equal(tx_1_id);
});
it('serializes an empty transaction', function() {
var transaction = new Transaction();
transaction.setVersion(1);
transaction.uncheckedSerialize().should.equal(tx_empty_hex);
});
it('serializes an empty transaction v2', function() {
var transaction = new Transaction();
transaction.uncheckedSerialize().should.equal(tx_empty_hexV2);
});
it('serializes and deserializes correctly', function() {
var transaction = new Transaction(tx_1_hex);
transaction.uncheckedSerialize().should.equal(tx_1_hex);
});
// testnet tx 2035ead4a9d0c8e2da1184924abc9034d26f2a7093371183ef12891623b235d1
const taprootTx = '02000000000102c1d8527f83a3061536d394cf50c476c60e885986b047d0d553c59f7a703cab700100000000fdffffffb843817220dc08b9f008207b5ea2591c26ce0ad5b3f842b934d9f0635a252d630000000000fdffffff02a086010000000000225120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c6a720000000000001600141eadc6c059a0485e0f8cfff955be4f5a544f514d024730440220776ecbb80e66ada7fe379c93c790303a33c11e3e888e41c991bcdae7d7531487022022ff85dc93a45941b4941484c46b515a476a2f2ab4ccb7dfd243eaadeed05036012103e9f41161bafb6a4e54a9ad29a68cdb3194e4d98b784a1ebcafa0055eb7310c810247304402206b275c62d21aa152323cac83e037f660865ef2a3bc73cc208bdc275643291b6f0220257249964a0e42ced656f74247683b70249f0d65da50532a3d9a5c4df12a531401210332fe2e5317637bed2153bee395facec6a245b98831e5a5d8f7af091371e67264aa7f1f00';
it('deserializes and serializes a taproot tx', function() {
const tx = new Transaction(taprootTx);
tx.should.exist;
const script = new Script(tx.outputs[0]._scriptBuffer);
const addy = script.toAddress('testnet');
const addyString = addy.toString();
addyString.should.equal('tb1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqp3mvzv');
const reserialized = tx.uncheckedSerialize();
reserialized.should.equal(taprootTx);
});
describe('transaction creation test vector', function() {
this.timeout(5000);
var index = 0;
for (const vector of transactionVector) {
index++;
it('case ' + index, function() {
var i = 0;
var transaction = new Transaction();
transaction.setVersion(1);
while (i < vector.length) {
var command = vector[i];
var args = vector[i + 1];
if (command === 'serialize') {
transaction.serialize().should.equal(args);
} else {
transaction[command].apply(transaction, args);
}
i += 2;
}
});
}
});
// TODO: Migrate this into a test for inputs
var fromAddress = 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1';
var witnessFromAddress = 'tb1q3rvex84884sw4al9vu00cp2jhyffz8e2n2k4wp';
var wrappedWitnessFromAddress = '2N2fk5hPbAPaMUs5No2kwy6xLdFL3CjUXMy';
var simpleUtxoWith100000Satoshis = {
address: fromAddress,
txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458',
outputIndex: 0,
script: Script.buildPublicKeyHashOut(fromAddress).toString(),
satoshis: 100000
};
var simpleUtxoWith1000000Satoshis = {
address: fromAddress,
txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458',
outputIndex: 0,
script: Script.buildPublicKeyHashOut(fromAddress).toString(),
satoshis: 1000000
};
var anyoneCanSpendUTXO = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis));
anyoneCanSpendUTXO.script = new Script().add('OP_TRUE');
var toAddress = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc';
var changeAddress = 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up';
var changeAddressP2SH = '2N7T3TAetJrSCruQ39aNrJvYLhG1LJosujf';
var changeAddressP2WPKH = 'tb1q3rvex84884sw4al9vu00cp2jhyffz8e2n2k4wp';
var changeAddressP2WSH = 'tb1qk0jhwmn65dqmlp755a7cff40fnvzsnhzq290kezrfs9d308an3tqlpjvad';
var privateKey = 'cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY';
var private1 = '6ce7e97e317d2af16c33db0b9270ec047a91bff3eff8558afb5014afb2bb5976';
var private2 = 'c9b26b0f771a0d2dad88a44de90f05f416b3b385ff1d989343005546a0032890';
var public1 = new PrivateKey(private1).publicKey;
var public2 = new PrivateKey(private2).publicKey;
var simpleUtxoWith1BTC = {
address: fromAddress,
txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458',
outputIndex: 1,
script: Script.buildPublicKeyHashOut(fromAddress).toString(),
satoshis: 1e8
};
var simpleWitnessUtxoWith1BTC = {
address: witnessFromAddress,
txId: '7e6b603779c8af58284566cf1b655395fffbefaf1c0a080d9aff43f0af05d873',
outputIndex: 0,
script: Script.fromAddress(witnessFromAddress).toString(),
satoshis: 1e8
};
var simpleWrappedWitnessUtxoWith1BTC = {
address: wrappedWitnessFromAddress,
txId: '825153a4a5d0c7ffd1a89838113a7204e5e4fa79fbac28bab0ea56c575393ed7',
outputIndex: 0,
script: Script.fromAddress(wrappedWitnessFromAddress).toString(),
satoshis: 1e8
};
var tenth = 1e7;
var fourth = 25e6;
var half = 5e7;
var p2shPrivateKey1 = PrivateKey.fromWIF('cNuW8LX2oeQXfKKCGxajGvqwhCgBtacwTQqiCGHzzKfmpHGY4TE9');
var p2shPublicKey1 = p2shPrivateKey1.toPublicKey();
var p2shPrivateKey2 = PrivateKey.fromWIF('cTtLHt4mv6zuJytSnM7Vd6NLxyNauYLMxD818sBC8PJ1UPiVTRSs');
var p2shPublicKey2 = p2shPrivateKey2.toPublicKey();
var p2shPrivateKey3 = PrivateKey.fromWIF('cQFMZ5gP9CJtUZPc9X3yFae89qaiQLspnftyxxLGvVNvM6tS6mYY');
var p2shPublicKey3 = p2shPrivateKey3.toPublicKey();
var p2shAddress = Address.createMultisig([
p2shPublicKey1,
p2shPublicKey2,
p2shPublicKey3
], 2, 'testnet');
var p2shUtxoWith1BTC = {
address: p2shAddress.toString(),
txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458',
outputIndex: 0,
script: Script(p2shAddress).toString(),
satoshis: 1e8
};
var p2wshAddress = Address.createMultisig([
p2shPublicKey1,
p2shPublicKey2,
p2shPublicKey3
], 2, 'testnet', null, Address.PayToWitnessScriptHash);
var p2wshUtxoWith1BTC = {
address: p2wshAddress.toString(),
txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458',
outputIndex: 0,
script: Script(p2wshAddress).toString(),
satoshis: 1e8
};
describe('adding inputs', function() {
it('adds just once one utxo', function() {
var tx = new Transaction();
tx.from(simpleUtxoWith1BTC);
tx.from(simpleUtxoWith1BTC);
tx.inputs.length.should.equal(1);
});
describe('isFullySigned', function() {
it('works for normal p2pkh', function() {
var transaction = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to([{address: toAddress, satoshis: 50000}])
.change(changeAddress)
.sign(privateKey);
transaction.isFullySigned().should.equal(true);
});
it('works for normal p2wpkh', function() {
var transaction = new Transaction()
.from(simpleWitnessUtxoWith1BTC)
.to([{address: toAddress, satoshis: 50000}])
.change(changeAddress)
.sign(privateKey);
transaction.isFullySigned().should.equal(true);
});
it('works for wrapped p2wpkh', function() {
var transaction = new Transaction()
.from(simpleWrappedWitnessUtxoWith1BTC)
.to([{address: toAddress, satoshis: 50000}])
.change(changeAddress)
.sign(privateKey);
transaction.isFullySigned().should.equal(true);
});
it('fails when Inputs are not subclassed and isFullySigned is called', function() {
var tx = new Transaction(tx_1_hex);
expect(function() {
return tx.isFullySigned();
}).to.throw(errors.Transaction.UnableToVerifySignature);
});
it('fails when Inputs are not subclassed and verifySignature is called', function() {
var tx = new Transaction(tx_1_hex);
expect(function() {
return tx.isValidSignature({
inputIndex: 0
});
}).to.throw(errors.Transaction.UnableToVerifySignature);
});
it('passes result of input.isValidSignature', function() {
var tx = new Transaction(tx_1_hex);
tx.from(simpleUtxoWith1BTC);
tx.inputs[0].isValidSignature = sinon.stub().returns(true);
var sig = {
inputIndex: 0
};
tx.isValidSignature(sig).should.equal(true);
});
});
});
describe('change address', function() {
it('can calculate simply the output amount', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1000000Satoshis)
.to(toAddress, 500000)
.change(changeAddress)
.sign(privateKey);
transaction.outputs.length.should.equal(2);
transaction.outputs[1].satoshis.should.equal(477400);
transaction.outputs[1].script.toString()
.should.equal(Script.fromAddress(changeAddress).toString());
var actual = transaction.getChangeOutput().script.toString();
var expected = Script.fromAddress(changeAddress).toString();
actual.should.equal(expected);
});
it('accepts a P2SH address for change', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1000000Satoshis)
.to(toAddress, 500000)
.change(changeAddressP2SH)
.sign(privateKey);
transaction.outputs.length.should.equal(2);
transaction.outputs[1].script.isScriptHashOut().should.equal(true);
});
it('accepts a P2WPKH address for change', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1000000Satoshis)
.to(toAddress, 500000)
.change(changeAddressP2WPKH)
.sign(privateKey);
transaction.outputs.length.should.equal(2);
transaction.outputs[1].script.isWitnessPublicKeyHashOut().should.equal(true);
});
it('accepts a P2WSH address for change', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1000000Satoshis)
.to(toAddress, 500000)
.change(changeAddressP2WSH)
.sign(privateKey);
transaction.outputs.length.should.equal(2);
transaction.outputs[1].script.isWitnessScriptHashOut().should.equal(true);
});
it('can recalculate the change amount', function() {
var transaction = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to(toAddress, 50000)
.change(changeAddress)
.fee(0)
.sign(privateKey);
transaction.getChangeOutput().satoshis.should.equal(50000);
transaction = transaction
.to(toAddress, 20000)
.sign(privateKey);
transaction.outputs.length.should.equal(3);
transaction.outputs[2].satoshis.should.equal(30000);
transaction.outputs[2].script.toString()
.should.equal(Script.fromAddress(changeAddress).toString());
});
it('adds no fee if no change is available', function() {
var transaction = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to(toAddress, 99000)
.sign(privateKey);
transaction.outputs.length.should.equal(1);
});
it('adds no fee if no money is available', function() {
var transaction = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to(toAddress, 100000)
.change(changeAddress)
.sign(privateKey);
transaction.outputs.length.should.equal(1);
});
it('adds no change if fee less than DUST_AMOUNT', function () {
var transaction = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to(toAddress, 100000 - Transaction.DUST_AMOUNT)
.change(changeAddress)
.sign(privateKey);
transaction.outputs.length.should.equal(1);
});
it('fee can be set up manually', function() {
var transaction = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to(toAddress, 80000)
.fee(10000)
.change(changeAddress)
.sign(privateKey);
transaction.outputs.length.should.equal(2);
transaction.outputs[1].satoshis.should.equal(10000);
});
it('fee per kb can be set up manually', function() {
var inputs = new Array(10).fill(0).map(function(_, i) {
var utxo = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis));
utxo.outputIndex = i;
return utxo;
});
var transaction = new Transaction()
.from(inputs)
.to(toAddress, 950000)
.feePerKb(8000)
.change(changeAddress)
.sign(privateKey);
transaction._estimateSize().should.be.within(1000, 1999);
transaction.outputs.length.should.equal(2);
transaction.outputs[1].satoshis.should.equal(37536);
});
it('fee per byte (low fee) can be set up manually', function () {
var inputs = new Array(10).fill(0).map(function(_, i) {
var utxo = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis));
utxo.outputIndex = i;
return utxo;
});
var transaction = new Transaction()
.from(inputs)
.to(toAddress, 950000)
.feePerByte(1)
.change(changeAddress)
.sign(privateKey);
transaction._estimateSize().should.be.within(1000, 1999);
transaction.outputs.length.should.equal(2);
transaction.outputs[1].satoshis.should.be.within(48001, 49000);
});
it('fee per byte (high fee) can be set up manually', function () {
var inputs = new Array(10).fill(0).map(function(_, i) {
var utxo = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis));
utxo.outputIndex = i;
return utxo;
});
var transaction = new Transaction()
.from(inputs)
.to(toAddress, 950000)
.feePerByte(2)
.change(changeAddress)
.sign(privateKey);
transaction._estimateSize().should.be.within(1000, 1999);
transaction.outputs.length.should.equal(2);
transaction.outputs[1].satoshis.should.be.within(46002, 48000);
});
it('fee per byte can be set up manually', function () {
var inputs = new Array(10).fill(0).map(function(_, i) {
var utxo = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis));
utxo.outputIndex = i;
return utxo;
});
var transaction = new Transaction()
.from(inputs)
.to(toAddress, 950000)
.feePerByte(13)
.change(changeAddress)
.sign(privateKey);
transaction._estimateSize().should.be.within(1000, 1999);
transaction.outputs.length.should.equal(2);
transaction.outputs[1].satoshis.should.be.within(24013, 37000);
});
it('fee per byte not enough for change', function () {
var inputs = new Array(10).fill(0).map(function(_, i) {
var utxo = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis));
utxo.outputIndex = i;
return utxo;
});
var transaction = new Transaction()
.from(inputs)
.to(toAddress, 999999)
.feePerByte(1)
.change(changeAddress)
.sign(privateKey);
transaction._estimateSize().should.be.within(1000, 1999);
transaction.outputs.length.should.equal(1);
});
it('if satoshis are invalid', function() {
var transaction = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to(toAddress, 99999)
.change(changeAddress)
.sign(privateKey);
transaction.outputs[0]._satoshis = 100;
transaction.outputs[0]._satoshisBN = new BN(101, 10);
expect(function() {
return transaction.serialize();
}).to.throw(errors.Transaction.InvalidSatoshis);
});
it('if fee is too small, fail serialization', function() {
var transaction = new Transaction({disableDustOutputs: true})
.from(simpleUtxoWith100000Satoshis)
.to(toAddress, 99999)
.change(changeAddress)
.sign(privateKey);
expect(function() {
return transaction.serialize();
}).to.throw(errors.Transaction.FeeError.TooSmall);
});
it('on second call to sign, change is not recalculated', function() {
var transaction = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to(toAddress, 100000)
.change(changeAddress)
.sign(privateKey)
.sign(privateKey);
transaction.outputs.length.should.equal(1);
});
it('getFee() returns the difference between inputs and outputs if no change address set', function() {
var transaction = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to(toAddress, 1000);
transaction.getFee().should.equal(99000);
});
it('should not under calculate fee', function () {
var inputs = Array(10).fill(0).map(function (_, i) {
var utxo = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis));
utxo.outputIndex = i;
return utxo;
});
const feeRate = 1;
var transaction = new Transaction()
.from(inputs)
.to(toAddress, 950000)
.feePerByte(feeRate)
.change(changeAddress)
.sign(privateKey);
// actual fee rate should never be *less* than the given
// fee rate and should only be over by 1% at most.
(transaction.getFee() / transaction.size).should.be.within(feeRate * 1, feeRate * 1.01);
});
});
describe('serialization', function() {
it('stores the change address correctly', function() {
var serialized = new Transaction()
.change(changeAddress)
.toObject();
var deserialized = new Transaction(serialized);
expect(deserialized._changeScript.toString()).to.equal(Script.fromAddress(changeAddress).toString());
expect(deserialized.getChangeOutput()).to.equal(null);
});
it('can avoid checked serialize', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC)
.to(fromAddress, 1);
expect(function() {
return transaction.serialize();
}).to.throw();
expect(function() {
return transaction.serialize(true);
}).to.not.throw();
});
it('stores the fee set by the user', function() {
var fee = 1000000;
var serialized = new Transaction()
.fee(fee)
.toObject();
var deserialized = new Transaction(serialized);
expect(deserialized._fee).to.equal(fee);
});
});
describe('checked serialize', function() {
it('fails if no change address was set', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC)
.to(toAddress, 1);
expect(function() {
return transaction.serialize();
}).to.throw(errors.Transaction.ChangeAddressMissing);
});
it('fails if a high fee was set', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC)
.change(changeAddress)
.fee(50000000)
.to(toAddress, 40000000);
expect(function() {
return transaction.serialize();
}).to.throw(errors.Transaction.FeeError.TooLarge);
});
it('fails if a dust output is created', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC)
.to(toAddress, 545)
.change(changeAddress)
.sign(privateKey);
expect(function() {
return transaction.serialize();
}).to.throw(errors.Transaction.DustOutputs);
});
it('doesn\'t fail if a dust output is not dust', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC)
.to(toAddress, 546)
.change(changeAddress)
.sign(privateKey);
expect(function() {
return transaction.serialize();
}).to.not.throw(errors.Transaction.DustOutputs);
});
it('doesn\'t fail if a dust output is an op_return', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC)
.addData('not dust!')
.change(changeAddress)
.sign(privateKey);
expect(function() {
return transaction.serialize();
}).to.not.throw(errors.Transaction.DustOutputs);
});
it('fails when outputs and fee don\'t add to total input', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC)
.to(toAddress, 99900000)
.fee(99999)
.sign(privateKey);
expect(function() {
return transaction.serialize();
}).to.throw(errors.Transaction.FeeError.Different);
});
it('checks output amount before fee errors', function() {
var transaction = new Transaction();
transaction.from(simpleUtxoWith1BTC);
transaction
.to(toAddress, 10000000000000)
.change(changeAddress)
.fee(5);
expect(function() {
return transaction.serialize();
}).to.throw(errors.Transaction.InvalidOutputAmountSum);
});
it('will throw fee error with disableMoreOutputThanInput enabled (but not triggered)', function() {
var transaction = new Transaction();
transaction.from(simpleUtxoWith1BTC);
transaction
.to(toAddress, 84000000)
.change(changeAddress)
.fee(16000000);
expect(function() {
return transaction.serialize({
disableMoreOutputThanInput: true
});
}).to.throw(errors.Transaction.FeeError.TooLarge);
});
describe('skipping checks', function() {
var buildSkipTest = function(builder, check, expectedError) {
return function() {
var transaction = new Transaction();
transaction.from(simpleUtxoWith1BTC);
builder(transaction);
var options = {};
options[check] = true;
expect(function() {
return transaction.serialize(options);
}).not.to.throw();
expect(function() {
return transaction.serialize();
}).to.throw(expectedError);
};
};
it('can skip the check for too much fee', buildSkipTest(
function(transaction) {
return transaction
.fee(50000000)
.change(changeAddress)
.sign(privateKey);
}, 'disableLargeFees', errors.Transaction.FeeError.TooLarge
));
it('can skip the check for a fee that is too small', buildSkipTest(
function(transaction) {
return transaction
.fee(1)
.change(changeAddress)
.sign(privateKey);
}, 'disableSmallFees', errors.Transaction.FeeError.TooSmall
));
it('can skip the check that prevents dust outputs', buildSkipTest(
function(transaction) {
return transaction
.to(toAddress, 100)
.change(changeAddress)
.sign(privateKey);
}, 'disableDustOutputs', errors.Transaction.DustOutputs
));
it('can skip the check that prevents unsigned outputs', buildSkipTest(
function(transaction) {
return transaction
.to(toAddress, 10000)
.change(changeAddress);
}, 'disableIsFullySigned', errors.Transaction.MissingSignatures
));
it('can skip the check that avoids spending more bitcoins than the inputs for a transaction', buildSkipTest(
function(transaction) {
return transaction
.to(toAddress, 10000000000000)
.change(changeAddress)
.sign(privateKey);
}, 'disableMoreOutputThanInput', errors.Transaction.InvalidOutputAmountSum
));
});
});
describe('#verify', function() {
it('not if _satoshis and _satoshisBN have different values', function() {
var tx = new Transaction()
.from({
'txId': testPrevTx,
'outputIndex': 0,
'script': testScript,
'satoshis': testAmount
})
.to('mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc', testAmount - 10000);
tx.outputs[0]._satoshis = 100;
tx.outputs[0]._satoshisBN = new BN('fffffffffffffff', 16);
var verify = tx.verify();
verify.should.equal('transaction txout 0 satoshis is invalid');
});
it('not if _satoshis is negative', function() {
var tx = new Transaction()
.from({
'txId': testPrevTx,
'outputIndex': 0,
'script': testScript,
'satoshis': testAmount
})
.to('mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc', testAmount - 10000);
tx.outputs[0]._satoshis = -100;
tx.outputs[0]._satoshisBN = new BN(-100, 10);
var verify = tx.verify();
verify.should.equal('transaction txout 0 satoshis is invalid');
});
it('not if transaction is greater than max block size', function() {
var tx = new Transaction()
.from({
'txId': testPrevTx,
'outputIndex': 0,
'script': testScript,
'satoshis': testAmount
})
.to('mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc', testAmount - 10000);
tx.toBuffer = sinon.stub().returns({
length: 10000000
});
var verify = tx.verify();
verify.should.equal('transaction over the maximum block size');
});
it('not if has null input (and not coinbase)', function() {
var tx = new Transaction()
.from({
'txId': testPrevTx,
'outputIndex': 0,
'script': testScript,
'satoshis': testAmount
})
.to('mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc', testAmount - 10000);
tx.isCoinbase = sinon.stub().returns(false);
tx.inputs[0].isNull = sinon.stub().returns(true);
var verify = tx.verify();
verify.should.equal('transaction input 0 has null input');
});
});
describe('to and from JSON', function() {
it('takes a string that is a valid JSON and deserializes from it', function() {
var simple = new Transaction();
expect(new Transaction(simple.toJSON()).uncheckedSerialize()).to.equal(simple.uncheckedSerialize());
var complex = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to(toAddress, 50000)
.change(changeAddress)
.sign(privateKey);
var cj = complex.toJSON();
var ctx = new Transaction(cj);
expect(ctx.uncheckedSerialize()).to.equal(complex.uncheckedSerialize());
});
it('serializes the `change` information', function() {
var transaction = new Transaction();
transaction.change(changeAddress);
expect(transaction.toJSON().changeScript).to.equal(Script.fromAddress(changeAddress).toString());
expect(new Transaction(transaction.toJSON()).uncheckedSerialize()).to.equal(transaction.uncheckedSerialize());
});
it('serializes correctly p2sh multisig signed tx', function() {
var t = new Transaction(tx2hex);
expect(t.toString()).to.equal(tx2hex);
var r = new Transaction(t);
expect(r.toString()).to.equal(tx2hex);
var j = new Transaction(t.toObject());
expect(j.toString()).to.equal(tx2hex);
});
});
describe('serialization of inputs', function() {
it('can serialize and deserialize a P2PKH input', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC);
var deserialized = new Transaction(transaction.toObject());
expect(deserialized.inputs[0] instanceof Transaction.Input.PublicKeyHash).to.equal(true);
});
it('can serialize and deserialize a P2SH input', function() {
var transaction = new Transaction()
.from({
txId: '0000', // Not relevant
outputIndex: 0,
script: Script.buildMultisigOut([public1, public2], 2).toScriptHashOut(),
satoshis: 10000
}, [public1, public2], 2);
var deserialized = new Transaction(transaction.toObject());
expect(deserialized.inputs[0] instanceof Transaction.Input.MultiSigScriptHash).to.equal(true);
});
it('can serialize and deserialize a P2PWKH input', function() {
var transaction = new Transaction()
.from(simpleWitnessUtxoWith1BTC);
var deserialized = new Transaction(transaction.toObject());
expect(deserialized.inputs[0] instanceof Transaction.Input.PublicKeyHash).to.equal(true);
});
it('can serialize and deserialize a wrapped P2PWKH input', function() {
var transaction = new Transaction()
.from(simpleWrappedWitnessUtxoWith1BTC);
var deserialized = new Transaction(transaction.toObject());
expect(deserialized.inputs[0] instanceof Transaction.Input.PublicKeyHash).to.equal(true);
});
it('can serialize and deserialize a P2WSH input', function() {
var transaction = new Transaction()
.from({
txId: '0000', // Not relevant
outputIndex: 0,
script: Script.buildWitnessMultisigOutFromScript(Script.buildMultisigOut([public1, public2], 2)),
satoshis: 10000
}, [public1, public2], 2);
var deserialized = new Transaction(transaction.toObject());
expect(deserialized.inputs[0] instanceof Transaction.Input.MultiSigScriptHash).to.equal(true);
});
});
describe('checks on adding inputs', function() {
var transaction = new Transaction();
it('fails if no output script is provided', function() {
expect(function() {
transaction.addInput(new Transaction.Input());
}).to.throw(errors.Transaction.NeedMoreInfo);
});
it('fails if no satoshi amount is provided', function() {
var input = new Transaction.Input();
expect(function() {
transaction.addInput(input);
}).to.throw(errors.Transaction.NeedMoreInfo);
expect(function() {
transaction.addInput(new Transaction.Input(), Script.empty());
}).to.throw(errors.Transaction.NeedMoreInfo);
});
it('allows output and transaction to be feed as arguments', function() {
expect(function() {
transaction.addInput(new Transaction.Input(), Script.empty(), 0);
}).to.not.throw();
});
it('does not allow a threshold number greater than the amount of public keys', function() {
expect(function() {
transaction = new Transaction();
return transaction.from({
txId: '0000000000000000000000000000000000000000000000000000000000000000',
outputIndex: 0,
script: Script(),
satoshis: 10000
}, [], 1);
}).to.throw('Number of required signatures must be greater than the number of public keys');
});
it('will add an empty script if not supplied', function() {
transaction = new Transaction();
var outputScriptString = 'OP_2 21 0x038282263212c609d9ea2a6e3e172de238d8c39' +
'cabd5ac1ca10646e23fd5f51508 21 0x038282263212c609d9ea2a6e3e172de23' +
'8d8c39cabd5ac1ca10646e23fd5f51508 OP_2 OP_CHECKMULTISIG OP_EQUAL';
transaction.addInput(new Transaction.Input({
prevTxId: '0000000000000000000000000000000000000000000000000000000000000000',
outputIndex: 0,
script: new Script()
}), outputScriptString, 10000);
transaction.inputs[0].output.script.should.be.instanceof(bitcore.Script);
transaction.inputs[0].output.script.toString().should.equal(outputScriptString);
});
});
describe('removeInput and removeOutput', function() {
it('can remove an input by index', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC);
transaction.inputs.length.should.equal(1);
transaction.inputAmount.should.equal(simpleUtxoWith1BTC.satoshis);
transaction.removeInput(0);
transaction.inputs.length.should.equal(0);
transaction.inputAmount.should.equal(0);
});
it('can remove an input by transaction id', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC);
transaction.inputs.length.should.equal(1);
transaction.inputAmount.should.equal(simpleUtxoWith1BTC.satoshis);
transaction.removeInput(simpleUtxoWith1BTC.txId, simpleUtxoWith1BTC.outputIndex);
transaction.inputs.length.should.equal(0);
transaction.inputAmount.should.equal(0);
});
it('fails if the index provided is invalid', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC);
expect(function() {
transaction.removeInput(2);
}).to.throw(errors.Transaction.InvalidIndex);
});
it('an output can be removed by index', function() {
var transaction = new Transaction()
.to([
{address: toAddress, satoshis: 40000000},
{address: toAddress, satoshis: 40000000}
])
transaction.outputs.length.should.equal(2);
transaction.outputAmount.should.equal(80000000);
transaction.removeOutput(0);
transaction.outputs.length.should.equal(1);
transaction.outputAmount.should.equal(40000000);
});
});
describe('handling the nLockTime', function() {
var MILLIS_IN_SECOND = 1000;
var timestamp = 1423504946;
var blockHeight = 342734;
var date = new Date(timestamp * MILLIS_IN_SECOND);
it('handles a null locktime', function() {
var transaction = new Transaction();
expect(transaction.getLockTime()).to.equal(null);
});
it('handles a simple example', function() {
var future = new Date(2025, 10, 30); // Sun Nov 30 2025
var transaction = new Transaction()
.lockUntilDate(future);
transaction.nLockTime.should.equal(future.getTime() / 1000);
transaction.getLockTime().should.deep.equal(future);
});
it('accepts a date instance', function() {
var transaction = new Transaction()
.lockUntilDate(date);
transaction.nLockTime.should.equal(timestamp);
transaction.getLockTime().should.deep.equal(date);
});
it('accepts a number instance with a timestamp', function() {
var transaction = new Transaction()
.lockUntilDate(timestamp);
transaction.nLockTime.should.equal(timestamp);
transaction.getLockTime().should.deep.equal(new Date(timestamp * 1000));
});
it('accepts a block height', function() {
var transaction = new Transaction()
.lockUntilBlockHeight(blockHeight);
transaction.nLockTime.should.equal(blockHeight);
transaction.getLockTime().should.deep.equal(blockHeight);
});
it('fails if the block height is too high', function() {
expect(function() {
return new Transaction().lockUntilBlockHeight(5e8);
}).to.throw(errors.Transaction.BlockHeightTooHigh);
});
it('fails if the date is too early', function() {
expect(function() {
return new Transaction().lockUntilDate(1);
}).to.throw(errors.Transaction.LockTimeTooEarly);
expect(function() {
return new Transaction().lockUntilDate(499999999);
}).to.throw(errors.Transaction.LockTimeTooEarly);
});
it('fails if the block height is negative', function() {
expect(function() {
return new Transaction().lockUntilBlockHeight(-1);
}).to.throw(errors.Transaction.NLockTimeOutOfRange);
});
it('has a non-max sequenceNumber for effective date locktime tx', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC)
.lockUntilDate(date);
transaction.inputs[0].sequenceNumber
.should.equal(Transaction.Input.DEFAULT_LOCKTIME_SEQNUMBER);
});
it('has a non-max sequenceNumber for effective blockheight locktime tx', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC)
.lockUntilBlockHeight(blockHeight);
transaction.inputs[0].sequenceNumber
.should.equal(Transaction.Input.DEFAULT_LOCKTIME_SEQNUMBER);
});
it('should serialize correctly for date locktime ', function() {
var transaction= new Transaction()
.from(simpleUtxoWith1BTC)
.lockUntilDate(date);
var serialized_tx = transaction.uncheckedSerialize();
var copy = new Transaction(serialized_tx);
serialized_tx.should.equal(copy.uncheckedSerialize());
copy.inputs[0].sequenceNumber
.should.equal(Transaction.Input.DEFAULT_LOCKTIME_SEQNUMBER)
});
it('should serialize correctly for a block height locktime', function() {
var transaction= new Transaction()
.from(simpleUtxoWith1BTC)
.lockUntilBlockHeight(blockHeight);
var serialized_tx = transaction.uncheckedSerialize();
var copy = new Transaction(serialized_tx);
serialized_tx.should.equal(copy.uncheckedSerialize());
copy.inputs[0].sequenceNumber
.should.equal(Transaction.Input.DEFAULT_LOCKTIME_SEQNUMBER)
});
});
it('handles anyone-can-spend utxo', function() {
var transaction = new Transaction()
.from(anyoneCanSpendUTXO)
.to(toAddress, 50000);
should.exist(transaction);
});
it('handles unsupported utxo in tx object', function() {
var transaction = new Transaction();
transaction.fromObject.bind(transaction, JSON.parse(unsupportedTxObj))
.should.throw('Unsupported input script type: OP_1 OP_ADD OP_2 OP_EQUAL');
});
it('will error if object hash does not match transaction hash', function() {
var tx = new Transaction(tx_1_hex);
var txObj = tx.toObject();
txObj.hash = 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458';
(function() {
var tx2 = new Transaction(txObj);
}).should.throw('Hash in object does not match transaction hash');
});
describe('inputAmount + outputAmount', function() {
it('returns correct values for simple transaction', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC)
.to(toAddress, 40000000);
transaction.inputAmount.should.equal(100000000);
transaction.outputAmount.should.equal(40000000);
});
it('returns correct values for transaction with change', function() {
var transaction = new Transaction()
.from(simpleUtxoWith1BTC)
.change(changeAddress)
.to(toAddress, 1000);
transaction.inputAmount.should.equal(100000000);
transaction.outputAmount.should.equal(99977400);
});
it('returns correct values for coinjoin transaction', function() {
// see livenet tx c16467eea05f1f30d50ed6dbc06a38539d9bb15110e4b7dc6653046a3678a718
var transaction = new Transaction(txCoinJoinHex);
transaction.outputAmount.should.equal(4191290961);
expect(function() {
var ia = transaction.inputAmount;
}).to.throw('No previous output information');
});
});
describe('output ordering', function() {
var transaction, out1, out2, out3, out4;
beforeEach(function() {
transaction = new Transaction()
.from(simpleUtxoWith1BTC)
.to([
{address: toAddress, satoshis: tenth},
{address: toAddress, satoshis: fourth}
])
.to(toAddress, half)
.change(changeAddress);
out1 = transaction.outputs[0];
out2 = transaction.outputs[1];
out3 = transaction.outputs[2];
out4 = transaction.outputs[3];
});
it('allows the user to sort outputs according to a criteria', function() {
var sorting = function(array) {
return [array[3], array[2], array[1], array[0]];
};
transaction.sortOutputs(sorting);
transaction.outputs[0].should.equal(out4);
transaction.outputs[1].should.equal(out3);
transaction.outputs[2].should.equal(out2);
transaction.outputs[3].should.equal(out1);
});
it('allows the user to randomize the output order', function() {
var shuffle = sinon.stub(_, 'shuffle');
shuffle.onFirstCall().returns([out2, out1, out4, out3]);
transaction._changeIndex.should.equal(3);
transaction.shuffleOutputs();
transaction.outputs[0].should.equal(out2);
transaction.outputs[1].should.equal(out1);
transaction.outputs[2].should.equal(out4);
transaction.outputs[3].should.equal(out3);
transaction._changeIndex.should.equal(2);
_.shuffle.restore();
});
it('fails if the provided function does not work as expected', function() {
var sorting = function(array) {
return [array[0], array[1], array[2]];
};
expect(function() {
transaction.sortOutputs(sorting);
}).to.throw(errors.Transaction.InvalidSorting);
});
it('shuffle without change', function() {
var tx = new Transaction(transaction.toObject()).to(toAddress, half);
expect(tx.getChangeOutput()).to.be.null;
expect(function() {
tx.shuffleOutputs();
}).to.not.throw(errors.Transaction.InvalidSorting);
})
});
describe('clearOutputs', function() {
it('removes all outputs and maintains the transaction in order', function() {
var tx = new Transaction()
.from(simpleUtxoWith1BTC)
.to(toAddress, tenth)
.to([
{address: toAddress, satoshis: fourth},
{address: toAddress, satoshis: half}
])
.change(changeAddress);
tx.clearOutputs();
tx.outputs.length.should.equal(1);
tx.to(toAddress, tenth);
tx.outputs.length.should.equal(2);
tx.outputs[0].satoshis.should.equal(10000000);
tx.outputs[0].script.toAddress().toString().should.equal(toAddress);
tx.outputs[1].satoshis.should.equal(89977400);
tx.outputs[1].script.toAddress().toString().should.equal(changeAddress);
});
});
describe('BIP69 Sorting', function() {
it('sorts inputs correctly', function() {
var from1 = {
txId: '0000000000000000000000000000000000000000000000000000000000000000',
outputIndex: 0,
script: Script.buildPublicKeyHashOut(fromAddress).toString(),
satoshis: 100000
};
var from2 = {
txId: '0000000000000000000000000000000000000000000000000000000000000001',
outputIndex: 0,
script: Script.buildPublicKeyHashOut(fromAddress).toString(),
satoshis: 100000
};
var from3 = {
txId: '0000000000000000000000000000000000000000000000000000000000000001',
outputIndex: