UNPKG

bitcore-matrixpay-lib

Version:
1,286 lines (1,190 loc) 56.3 kB
'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