bitcore-lib-cash
Version:
A pure and powerful JavaScript Bitcoin Cash library.
1,273 lines (1,181 loc) • 58 kB
JavaScript
'use strict';
/* jshint unused: false */
/* jshint latedef: false */
var should = require('chai').should();
var expect = require('chai').expect;
var _ = require('lodash');
var sinon = require('sinon');
var bitcore = require('../..');
var BN = bitcore.crypto.BN;
var Transaction = bitcore.Transaction;
var Signature = bitcore.Signature;
var Input = bitcore.Transaction.Input;
var Output = bitcore.Transaction.Output;
var PrivateKey = bitcore.PrivateKey;
var Script = bitcore.Script;
var Address = bitcore.Address;
var Networks = bitcore.Networks;
var Opcode = bitcore.Opcode;
var errors = bitcore.errors;
var transactionVector = require('../data/tx_creation');
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('bchtest:qpuzrs9rw692n5dr0ctv7asq9th4xul34qes9nf4x6', 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('bchtest:qpuzrs9rw692n5dr0ctv7asq9th4xul34qes9nf4x6', 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('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('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.uncheckedSerialize().should.equal(tx_empty_hex);
});
it('serializes and deserializes correctly', function() {
var transaction = new Transaction(tx_1_hex);
transaction.uncheckedSerialize().should.equal(tx_1_hex);
});
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()).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 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 = 'bchtest:qpuzrs9rw692n5dr0ctv7asq9th4xul34qes9nf4x6';
var changeAddress = 'bchtest:qqrnkl4w9q37lg6fuwu32ku2wd2jv336pul3f7qz0a';
var changeAddressP2SH = 'bchtest:pzdumagr7ru8w46s8alws4lzruv2c75vsucd9e2rz0';
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 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
};
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('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('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('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 = Array(10).fill(0).map(function (_, i) {
var utxo = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis));
utxo.outputIndex = i;
return utxo;
});
const feeRate = 8000;
var transaction = new Transaction()
.from(inputs)
.to(toAddress, 950000)
.feePerKb(feeRate)
.change(changeAddress)
.sign(privateKey);
transaction._estimateSize().should.be.within(1000, 1999);
(transaction.getFee() / transaction.size).should.be.within(feeRate / 1000 * 1, feeRate / 1000 * 1.01);
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 = 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);
transaction._estimateSize().should.be.within(1000, 1999);
(transaction.getFee() / transaction.size).should.be.within(feeRate * 1, feeRate * 1.01);
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 = Array(10).fill(0).map(function (_, i) {
var utxo = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis));
utxo.outputIndex = i;
return utxo;
});
const feeRate = 2
var transaction = new Transaction()
.from(inputs)
.to(toAddress, 950000)
.feePerByte(feeRate)
.change(changeAddress)
.sign(privateKey);
transaction._estimateSize().should.be.within(1000, 1999);
(transaction.getFee() / transaction.size).should.be.within(feeRate * 1, feeRate * 1.01);
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 = Array(10).fill(0).map(function (_, i) {
var utxo = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis));
utxo.outputIndex = i;
return utxo;
});
const feeRate = 13;
var transaction = new Transaction()
.from(inputs)
.to(toAddress, 950000)
.feePerByte(feeRate)
.change(changeAddress)
.sign(privateKey);
transaction._estimateSize().should.be.within(1000, 1999);
(transaction.getFee() / transaction.size).should.be.within(feeRate * 1, feeRate * 1.01);
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 = 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('bchtest:qpuzrs9rw692n5dr0ctv7asq9th4xul34qes9nf4x6', 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('bchtest:qpuzrs9rw692n5dr0ctv7asq9th4xul34qes9nf4x6', 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('bchtest:qpuzrs9rw692n5dr0ctv7asq9th4xul34qes9nf4x6', 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);
});
});
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('isZceSecured', () => {
const zceRawTx = '0100000004ff8abba28b308eb0c1e6bd10b29f30b957dad3be871b40553d8324a79ad1bdea010000006441c4f26e20f40e01f186c70cf89c4003c07870fa4b4370999ed14b4700425a39f89bdc8324abd5245db2df5b9f63f1232a7907e87f05843c2db5062b35dc7b77b9412103978af6aff2c8b93b8776fbb6e33b532316326667ec79f21704589576548f3dcaffffffff85ee380cc3329273aa667d3c29ea9fc81167e8a06ed0feac4df12809f60a064100000000644171dccb5867d2dc9fe9278c2de3916260994a32fc4c724663fb1b9f0fe3f10d8d46a20f3881487c0d391b45419b57d301db4811174ba3fc05c6d16865f4e4f136412103351c7e4cc8e77ccab99b23de6c1585defc72b99da42d2878d6cb64a4186bb0c7ffffffffbcdf29fde37c27c291f585cb83af678dc54ce950f8c5bc7e1d25413b240c94ea000000006441092d7d0dc3b785d13c8e910d8533272a33812b7a61479002ffb7d5cf620fd813c6f59c70f874e78ac65ed720afe1ff6b09a299c03ebb4f8f14f4e7370db852f5412103f4d946e03044e8314e5d87b674154b21c5c7a72f26b96c8a734b1b4ded7263ebffffffff9dc766918e1deed380ddce878c4239f5dd434498f5b65ac3520980dadf3375c3000000006441ae19b88f8503b6beb9d9508d46984486d01ace007fceb90c1b813be51b88ab7e576ba1711e52fcfd8487f22373acc9550a107d5565b9f07a2b004368247358bb412103de52c25690cf07f3f95ba76129c43b9099b51fe151789bc96ac75515b3d48df9ffffffff033c7a0300000000001976a9140157345215a85cbdd767cf947e0c3503e349bfbf88acf63b0000000000001976a914754f61f94a91042b40e5eb804d4db5d766239f4488ac832c00000000000017a914ad6306ad48a63fd28884974ae1ef579af4279a6b8700000000';
const zceTx = new Transaction(zceRawTx);
const reclaimRawTx = '0100000001b5389a3c3ddcdf02569f7d44442923ddaa7ac6bc889983dff8a1823d7f33169402000000b44135429e569603d928f65e808a3163b59431317c2032f8b611cc1a5a8c7bc6e577b0697000d85950a5a29b7544abdcda3467cc0b5f513e3f7bb616f8ad257badf04121035fb4cada6334e9179b4ed1294a510bcaf8e6f8622030f301e1a76856649924194c4e76a914f92b0b93d58cbb77b51e4f76de22fe47d5000e0f8763ac676b6b5279a96c637c687ea96c637c687ea914c319f2ebff371fd5fcff74768e8200d446fb803188785479879169766bbb6cba68ffffffff017a2b0000000000001976a91425707fbb5f80cd52cc86e88a456a95bbdb4f0c8788ac00000000';
const instantAcceptanceEscrow = 10721;
const requiredFeeRate = 1;
it('should return true for a properly constructed escrowReclaimTx', () => {
const zceSecured = zceTx.isZceSecured(reclaimRawTx, instantAcceptanceEscrow, requiredFeeRate);
zceSecured.should.equal(true);
});
it('should return false if insufficient escrow provided', () => {
const zceSecured = zceTx.isZceSecured(reclaimRawTx, instantAcceptanceEscrow + 1, requiredFeeRate);
zceSecured.should.equal(false);
});
it('should return false if insufficient fee rate used', () => {
const zceSecured = zceTx.isZceSecured(reclaimRawTx, instantAcceptanceEscrow, requiredFeeRate + 1);
zceSecured.should.equal(false);
});
it('should not crash and return false if invalid escrowReclaimTx provided', () => {
const zceSecured = zceTx.isZceSecured('invalidhex', instantAcceptanceEscrow, requiredFeeRate);
zceSecured.should.equal(false);
});
});
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: 1,
script: Script.buildPublicKeyHashOut(fromAddress).toString(),
satoshis: 100000
};
var tx = new Transaction()
.from(from3)
.from(from2)
.from(from1);
tx.sort();
tx.inputs[0].prevTxId.toString('hex').should.equal(from1.txId);
tx.inputs[1].prevTxId.toString('hex').should.equal(from2.txId);
tx.inputs[2].prevTxId.toString('hex').should.equal(from3.txId);
tx.inputs[0].outputIndex.should.equal(from1.outputIndex);
tx.inputs[1].outputIndex.should.equal(from2.outputIndex);
tx.inputs[2].outputIndex.should.equal(from3.outputIndex);
});
it('sorts outputs correctly', function() {
var tx = new Transaction()
.addOutput(new Transaction.Output({
script: new Script().add(Opcode(0)),
satoshis: 2
}))
.addOutput(new Transaction.Output({
script: new Script().add(Opcode(1)),
satoshis: 2
}))
.addOutput(new Transaction.Output({
script: new Script().add(Opcode(0)),
satoshis: 1
}));
tx.sort();
tx.outputs[0].satoshis.should.equal(1);
tx.outputs[1].satoshis.should.equal(2);
tx.outputs[2].satoshis.should.equal(2);
tx.outputs[0].script.toString().should.equal('OP_0');
tx.outputs[1].script.toString().should.equal('OP_0');
tx.outputs[2].script.toString().should.equal('0x01');
});
describe('bitcoinjs fixtures', function() {
var fixture = require('../data/bip69.json');
// returns index-based order of sorted against original
var getIndexOrder = function(original, sorted) {
return sorted.map(function(value) {
return original.indexOf(value);
});
};
for (const inputSet of fixture.inputs) {
it(inputSet.description, function() {
var tx = new Transaction();
inputSet.inputs = inputSet.inputs.map(function(input) {
var input = new Input({
prevTxId: input.txId,
outputIndex: input.vout,
script: new Script(),
output: new Output({
script: new Script(),
satoshis: 0
})
});
input.clearSignatures = function() {};
return input;
});
tx.inputs = inputSet.inputs;
tx.sort();
getIndexOrder(inputSet.inputs, tx.inputs).should.deep.equal(inputSet.expected);
});
}
for (const outputSet of fixture.outputs) {
it(outputSet.description, function() {
var tx = new Transaction();
outputSet.outputs = outputSet.outputs.map(function(output) {
return new Output({
script: new Script(output.script),
satoshis: output.value
});
});
tx.outputs = outputSet.outputs;
tx.sort();
getIndexOrder(outputSet.outputs, tx.outputs).should.deep.equal(outputSet.expected);
});
}
});
});
});
var tx_empty_hex = '02000000000000000000';
/* jshint maxlen: 1000 */
var tx_1_hex = '01000000015884e5db9de218238671572340b207ee85b628074e7e467096c267266baf77a4000000006a473044022013fa3089327b50263029265572ae1b022a91d10ac80eb4f32f291c914533670b02200d8a5ed5f62634a7e1a0dc9188a3cc460a986267ae4d58faf50c79105431327501210223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5effffffff0150690f00000000001976a9147821c0a3768aa9d1a37e16cf76002aef5373f1a888ac00000000';
var tx_1_id = '779a3e5b3c2c452c8533