UNPKG

bitcore-lib-cash

Version:

A pure and powerful JavaScript Bitcoin Cash library.

1,273 lines (1,181 loc) 58 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 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