bitcore-matrixpay-lib
Version:
A pure and powerful JavaScript MatrixPay library.
1,286 lines (1,190 loc) • 56.3 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 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('yXGeNPQXYFXhLAN1ZKrAjxzzBnZ2JZNKnh', 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('yXGeNPQXYFXhLAN1ZKrAjxzzBnZ2JZNKnh', 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;
transactionVector.forEach(function(vector) {
index++;
it('case ' + index, function() {
var i = 0;
var transaction = new Transaction();
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 = 'yYo3PeSBv2rMnJeyLUCCzx4Y8VhPppZKkC';
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 = 'yXGeNPQXYFXhLAN1ZKrAjxzzBnZ2JZNKnh';
var changeAddress = 'yLygrKXHautSYwRAF3TUGJvidxKqXwLA23';
var changeAddressP2SH = '8tdHAwttdvR87BihpKRSUjN6HyQNVZsqBv';
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(simpleUtxoWith100000Satoshis)
.to(toAddress, 50000)
.change(changeAddress)
.sign(privateKey);
transaction.outputs.length.should.equal(2);
transaction.outputs[1].satoshis.should.equal(40000);
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 = _.map(_.range(10), function(i) {
var utxo = _.clone(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(34000);
});
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()
.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, 10000);
transaction.getFee().should.equal(90000);
});
});
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, 5459)
.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, 5460)
.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, 90000000)
.change(changeAddress)
.fee(10000000);
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('yXGeNPQXYFXhLAN1ZKrAjxzzBnZ2JZNKnh', 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('yXGeNPQXYFXhLAN1ZKrAjxzzBnZ2JZNKnh', 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('yXGeNPQXYFXhLAN1ZKrAjxzzBnZ2JZNKnh', 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('yXGeNPQXYFXhLAN1ZKrAjxzzBnZ2JZNKnh', 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, 10000);
transaction.inputAmount.should.equal(100000000);
transaction.outputAmount.should.equal(99990000);
});
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(89990000);
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: 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);
});
};
fixture.inputs.forEach(function(inputSet) {
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);
});
});
fixture.outputs.forEach(function(outputSet) {
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);
});
});
});
});
describe('Replace-by-fee', function() {
describe('#enableRBF', function() {
it('only enable inputs not already enabled (0xffffffff)', function() {
var tx = new Transaction()
.from(simpleUtxoWith1BTC)
.from(simpleUtxoWith100000Satoshis)
.to([{address: toAddress, satoshis: 50000}])
.fee(15000)
.change(changeAddress)
.sign(privateKey);
tx.inputs[0].sequenceNumber = 0x00000000;
tx.enableRBF();
tx.inputs[0].sequenceNumber.should.equal(0x00000000);
tx.inputs[1].sequenceNumber.should.equal(0xfffffffd);
});
it('enable for inputs with 0xffffffff and 0xfffffffe', function() {
var tx = new Transaction()
.from(simpleUtxoWith1BTC)
.from(simpleUtxoWith100000Satoshis)
.to([{address: toAddress, satoshis: 50000}])
.fee(15000)
.change(changeAddress)
.sign(privateKey);
tx.inputs[0].sequenceNumber = 0xffffffff;
tx.inputs[1].sequenceNumber = 0xfffffffe;
tx.enableRBF();
tx.inputs[0].sequenceNumber.should.equal(0xfffffffd);
tx.inputs[1].sequenceNumber.should.equal(0xfffffffd);
});
});
describe('#isRBF', function() {
it('enable and determine opt-in', function() {
var tx = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to([{address: toAddress, satoshis: 50000}])
.fee(15000)
.change(changeAddress)
.enableRBF()
.sign(privateKey);
tx.isRBF().should.equal(true);
});
it('determine opt-out with default sequence number', function() {
var tx = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to([{address: toAddress, satoshis: 50000}])
.fee(15000)
.change(changeAddress)
.sign(privateKey);
tx.isRBF().should.equal(false);
});
it('determine opt-out with 0xfffffffe', function() {
var tx = new Transaction()
.from(simpleUtxoWith1BTC)
.from(simpleUtxoWith100000Satoshis)
.to([{address: toAddress, satoshis: 50000 + 1e8}])
.fee(15000)
.change(changeAddress)
.sign(privateKey);
tx.inputs[0].sequenceNumber = 0xfffffffe;
tx.inputs[1].sequenceNumber = 0xfffffffe;
tx.isRBF().should.equal(false);
});
it('determine opt-out with 0xffffffff', function() {
var tx = new Transaction()
.from(simpleUtxoWith1BTC)
.from(simpleUtxoWith100000Satoshis)
.to([{address: toAddress, satoshis: 50000 + 1e8}])
.fee(15000)
.change(changeAddress)
.sign(privateKey);
tx.inputs[0].sequenceNumber = 0xffffffff;
tx.inputs[1].sequenceNumber = 0xffffffff;
tx.isRBF().should.equal(false);
});
it('determine opt-in with 0xfffffffd (first input)', function() {
var tx = new Transaction()
.from(simpleUtxoWith1BTC)
.from(simpleUtxoWith100000Satoshis)
.to([{address: toAddress, satoshis: 50000 + 1e8}])
.fee(15000)
.change(changeAddress)
.sign(privateKey);
tx.inputs[0].sequenceNumber = 0xfffffffd;
tx.inputs[1].sequenceNumber = 0xffffffff;
tx.isRBF().should.equal(true);
});
it('determine opt-in with 0xfffffffd (second input)', function() {
var tx = new Transaction()
.from(simpleUtxoWith1BTC)
.from(simpleUtxoWith100000Satoshis)
.to([{address: toAddress, satoshis: 50000 + 1e8}])
.fee(15000)
.change(changeAddress)
.sign(privateKey);
tx.inputs[0].sequenceNumber = 0xffffffff;
tx.inputs[1].sequenceNumber = 0xfffffffd;
tx.isRBF().should.equal(true);
});
});
});
describe('fromObject', function () {
it('Should copy transaction when passing instance of Transaction as arg', 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
});
var copiedTransaction = bitcore.Transaction().fromObject(tx);
expect(copiedTransaction).to.be.an.instanceof(bitcore.Transaction);
});
});
});
var tx_empty_hex = '01000000000000000000';
/* jshint maxlen: 1000 */
var tx_1_hex = '01000000015884e5db9de218238671572340b207ee85b628074e7e467096c267266baf77a4000000006a473044022013fa3089327b50263029265572ae1b022a91d10ac80eb4f32f291c914533670b02200d8a5ed5f62634a7e1a0dc9188a3cc460a986267ae4d58faf50c79105431327501210223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5effffffff0150690f00000000001976a9147821c0a3768aa9d1a37e16cf76002aef5373f1a888ac00000000';
var tx_1_id = '779a3e5b3c2c452c85333d8521f804c1a52800e60f4b7c3bbe36f4bab350b72c';
var tx2hex = '0100000001e07d8090f4d4e6fcba6a2819e805805517eb19e669e9d2f856b41d4277953d640000000091004730440220248bc60bb309dd0215fbde830b6371e3fdc55685d11daa9a3c43828892e26ce202205f10cd4011f3a43657260a211f6c4d1fa81b6b6bdd6577263ed097cc22f4e5b50147522102fa38420cec94843ba963684b771ba3ca7ce1728dc2c7e7cade0bf298324d6b942103f948a83c20b2e7228ca9f3b71a96c2f079d9c32164cd07f08fbfdb483427d2ee52aeffffffff01180fe200000000001976a914ccee7ce8e8b91ec0bc23e1cfb6324461429e6b0488ac00000000';
var unsupportedTxObj = '{"version":1,"inputs":[{"prevTxId":"a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458","outputIndex":0,"sequenceNumber":4294967295,"script":"OP_1","output":{"satoshis":1020000,"script":"OP_1 OP_ADD OP_2 OP_EQUAL"}}],"outputs":[{"satoshis":1010000,"script":"OP_DUP OP_HASH160 20 0x7821c0a3768aa9d1a37e16cf76002aef5373f1a8 OP_EQUALVERIFY OP_CHECKSIG"}],"nLockTime":0}';
var txCoinJoinHex = '0100000013440a4e2471a0afd66c9db54db7d414507981eb3db35970dadf722453f08bdc8d0c0000006a47304402200098a7f838ff267969971f5d9d4b2c1db11b8e39c81eebf3c8fe22dd7bf0018302203fa16f0aa3559752462c20ddd8a601620eb176b4511507d11a361a7bb595c57c01210343ead2c0e2303d880bf72dfc04fc9c20d921fc53949c471e22b3c68c0690b828ffffffff0295eef5ad85c9b6b91a3d77bce015065dc64dab526b2f27fbe56f51149bb67f100000006b483045022100c46d6226167e6023e5a058b1ae541c5ca4baf4a69afb65adbfce2cc276535a6a022006320fdc8a438009bbfebfe4ab63e415ee231456a0137d167ee2113677f8e3130121032e38a3e15bee5ef272eaf71033a054637f7b74a51882e659b0eacb8db3e417a9ffffffffee0a35737ab56a0fdb84172c985f1597cffeb33c1d8e4adf3b3b4cc6d430d9b50a0000006b483045022100d02737479b676a35a5572bfd027ef9713b