From 706162e2ba29c094cf42065e967f2e1dbe15abee Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 12:21:59 -0300 Subject: [PATCH] #create for Transaction and tests --- Transaction.js | 87 ++++++++++++++++++++++++++-------------- test/data/unspends.json | 7 +++- test/test.Transaction.js | 64 +++++++++++++++++++++++------ 3 files changed, 112 insertions(+), 46 deletions(-) diff --git a/Transaction.js b/Transaction.js index 5e04e13..4fcccb1 100644 --- a/Transaction.js +++ b/Transaction.js @@ -653,44 +653,23 @@ Transaction.prototype.parse = function (parser) { }; /* - * selectUnspent + * _selectUnspent * * Selects some unspend outputs for later usage in tx inputs * - * @unspentArray: unspent array (UTXO) avaible on the form: - * [{ - * address: "mqSjTad2TKbPcKQ3Jq4kgCkKatyN44UMgZ", - * hash: "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", - * scriptPubKey: "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ac", - * vout: 1, - * amount: 0.01, - * }, [...] - * ] - * This is compatible con insight's /utxo API. - * NOTE that amount is in BTCs! (as returned in insight and bitcoind) - * amountSat can be given to provide amount in satochis. - * + * @unspentArray: unspent array (UTXO) avaible on the form (see selectUnspent) * @totalNeededAmount: output transaction amount in BTC, including fee + * @minConfirmations: 0 by default. * * - * Return the selected outputs or null if there are not enough funds. - * It does not check for confirmations. The unspendArray should be filtered. + * Returns the selected outputs or null if there are not enough funds. + * The utxos are selected in the order they appear in the original array. + * Sorting must be done previusly. * */ -Transaction.selectUnspent = function (unspentArray, totalNeededAmount) { - - // TODO implement bidcoind heristics - // A- - // 1) select utxos with 6+ confirmations - // 2) if not 2) select utxos with 1+ confirmations - // 3) if not select unconfirmed. - // - // B- - // Select smaller utxos first. - // - // - // TODO we could randomize or select the selection - +Transaction._selectUnspent = function (unspentArray, totalNeededAmount, minConfirmations) { + minConfirmations = minConfirmations || 0; + var selected = []; var l = unspentArray.length; var totalSat = bignum(0); @@ -699,6 +678,9 @@ Transaction.selectUnspent = function (unspentArray, totalNeededAmount) { for(var i = 0; i= the desired amount. + * + */ + +Transaction.selectUnspent = function (unspentArray, totalNeededAmount, allowUnconfirmed) { + var answer = Transaction._selectUnspent(unspentArray, totalNeededAmount, 6); + + if (!answer.length) + answer = Transaction._selectUnspent(unspentArray, totalNeededAmount, 1); + + if (!answer.length && allowUnconfirmed) + answer = Transaction._selectUnspent(unspentArray, totalNeededAmount, 0); + + return answer; +} + /* * _scriptForAddress * @@ -805,9 +826,13 @@ Transaction.create = function (ins, outs, opts) { var sat = outs[i].amountSat || util.parseValue(outs[i].amount); valueOutSat = valueOutSat.add(sat); } + valueOutSat = valueOutSat.add(feeSat); if (valueInSat.cmp(valueOutSat)<0) { - throw new Error('transaction inputs sum less than outputs'); + var inv = valueInSat.toString(); + var ouv = valueOutSat.toString(); + throw new Error('transaction input amount is less than outputs: ' + + inv + ' < '+ouv + ' [SAT]'); } var remainderSat = valueInSat.sub(valueOutSat); diff --git a/test/data/unspends.json b/test/data/unspends.json index d9fac53..9c818a8 100644 --- a/test/data/unspends.json +++ b/test/data/unspends.json @@ -4,13 +4,15 @@ "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", "scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ac", "vout": 1, - "amount": 0.01 + "amount": 0.01, + "confirmations":7 }, { "address": "mqSjTad2TKbPcKQ3Jq4kgCkKatyN44UMgZ", "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc2", "scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ad", - "vout": 1, + "vout": 0, + "confirmations": 1, "amount": 0.1 }, { @@ -18,6 +20,7 @@ "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc3", "scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ae", "vout": 3, + "confirmations": 0, "amount": 1 } ] diff --git a/test/test.Transaction.js b/test/test.Transaction.js index 64a7270..bf0671d 100644 --- a/test/test.Transaction.js +++ b/test/test.Transaction.js @@ -34,18 +34,18 @@ describe('Transaction', function() { }); - it('should be able to select utxos', function() { - var u = Transaction.selectUnspent(testdata.dataUnspends,1.0); + it('#_selectUnspent should be able to select utxos', function() { + var u = Transaction._selectUnspent(testdata.dataUnspends,1.0); u.length.should.equal(3); - u = Transaction.selectUnspent(testdata.dataUnspends,0.5); + u = Transaction._selectUnspent(testdata.dataUnspends,0.5); u.length.should.equal(3); - u = Transaction.selectUnspent(testdata.dataUnspends,0.1); + u = Transaction._selectUnspent(testdata.dataUnspends,0.1); u.length.should.equal(2); - u = Transaction.selectUnspent(testdata.dataUnspends,0.05); + u = Transaction._selectUnspent(testdata.dataUnspends,0.05); u.length.should.equal(2); - u = Transaction.selectUnspent(testdata.dataUnspends,0.015); + u = Transaction._selectUnspent(testdata.dataUnspends,0.015); u.length.should.equal(2); - u = Transaction.selectUnspent(testdata.dataUnspends,0.01); + u = Transaction._selectUnspent(testdata.dataUnspends,0.01); u.length.should.equal(1); should.exist(u[0].amount); should.exist(u[0].txid); @@ -53,25 +53,63 @@ describe('Transaction', function() { should.exist(u[0].vout); }); - it('should return null if not enough utxos', function() { + it('#selectUnspent should return null if not enough utxos', function() { var u = Transaction.selectUnspent(testdata.dataUnspends,1.12); u.length.should.equal(0); }); - it('should be able to create instance thru #create', function() { + it('#selectUnspent should check confirmations', function() { + var u = Transaction.selectUnspent(testdata.dataUnspends,0.9); + u.length.should.equal(0); + var u = Transaction.selectUnspent(testdata.dataUnspends,0.9,true); + u.length.should.equal(3); + + var u = Transaction.selectUnspent(testdata.dataUnspends,0.11); + u.length.should.equal(2); + var u = Transaction.selectUnspent(testdata.dataUnspends,0.111); + u.length.should.equal(0); + }); + + + var opts = {remainderAddress: 'mwZabyZXg8JzUtFX1pkGygsMJjnuqiNhgd'}; + it('#create should be able to create instance', function() { var utxos = Transaction.selectUnspent(testdata.dataUnspends,0.1); var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; - var tx = Transaction.create(utxos, outs, - {remainderAddress:'3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou'}); + var tx = Transaction.create(utxos, outs, opts); should.exist(tx); + tx.version.should.equal(1); tx.ins.length.should.equal(2); tx.outs.length.should.equal(2); util.valueToBigInt(tx.outs[0].v).cmp(8000000).should.equal(0); - // TODO remainder is 0.03 here because unspend just select utxos in order - util.valueToBigInt(tx.outs[1].v).cmp(3000000).should.equal(0); + // remainder is 0.0299 here because unspend select utxos in order + util.valueToBigInt(tx.outs[1].v).cmp(2990000).should.equal(0); + }); + + + it('#create should fail if not enough inputs ', function() { + var utxos = Transaction.selectUnspent(testdata.dataUnspends,1); + var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; + Transaction + .create + .bind(utxos, outs, opts) + .should.throw(); + }); + + + it('#create should create same output as bitcoind createrawtransaction ', function() { + var utxos = Transaction.selectUnspent(testdata.dataUnspends,0.1); + var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; + var tx = Transaction.create(utxos, outs, opts); + tx.serialize().toString('hex').should.equal('0100000002c1cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0100000000ffffffffc2cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0000000000ffffffff0200127a00000000001976a914774e603bafb717bd3f070e68bbcccfd907c77d1388acb09f2d00000000001976a914b00127584485a7cff0949ef0f6bc5575f06ce00d88ac00000000'); + + outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; + tx = Transaction.create(utxos, outs, {fee:0.03} ); + tx.serialize().toString('hex').should.equal('0100000002c1cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0100000000ffffffffc2cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0000000000ffffffff0100127a00000000001976a914774e603bafb717bd3f070e68bbcccfd907c77d1388ac00000000'); + }); + // Read tests from test/data/tx_valid.json // Format is an array of arrays