UNPKG

bitcore-lib

Version:

A pure and powerful JavaScript Bitcoin library.

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