diff --git a/Transaction.js b/Transaction.js index 5e04e1385..4fcccb135 100644 --- a/Transaction.js +++ b/Transaction.js @@ -652,6 +652,47 @@ Transaction.prototype.parse = function (parser) { this.calcHash(); }; +/* + * _selectUnspent + * + * Selects some unspend outputs for later usage in tx inputs + * + * @unspentArray: unspent array (UTXO) avaible on the form (see selectUnspent) + * @totalNeededAmount: output transaction amount in BTC, including fee + * @minConfirmations: 0 by default. + * + * + * 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, minConfirmations) { + minConfirmations = minConfirmations || 0; + + var selected = []; + var l = unspentArray.length; + var totalSat = bignum(0); + var totalNeededAmountSat = util.parseValue(totalNeededAmount); + var fullfill = false; + + for(var i = 0; i= 0) { + fullfill = true; + break; + } + } + if (!fullfill) return []; + return selected; +} + /* * selectUnspent * @@ -664,51 +705,31 @@ Transaction.prototype.parse = function (parser) { * scriptPubKey: "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ac", * vout: 1, * amount: 0.01, + * confirmations: 3 * }, [...] * ] * This is compatible con insight's /utxo API. - * NOTE that amount is in BTCs! (as returned in insight and bitcoind) + * That amount is in BTCs. (as returned in insight and bitcoind) * amountSat can be given to provide amount in satochis. * * @totalNeededAmount: output transaction amount in BTC, including fee + * @allowUnconfirmed:false (allow selecting unconfirmed utxos) * * - * Return the selected outputs or null if there are not enough funds. - * It does not check for confirmations. The unspendArray should be filtered. + * Note that the sum of the selected unspent is >= the desired amount. * */ -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 - - var selected = []; - var l = unspentArray.length; - var totalSat = bignum(0); - var totalNeededAmountSat = util.parseValue(totalNeededAmount); - var fullfill = false; +Transaction.selectUnspent = function (unspentArray, totalNeededAmount, allowUnconfirmed) { + var answer = Transaction._selectUnspent(unspentArray, totalNeededAmount, 6); - for(var i = 0; i= 0) { - fullfill = true; - break; - } - } - if (!fullfill) return []; - return selected; + if (!answer.length) + answer = Transaction._selectUnspent(unspentArray, totalNeededAmount, 1); + + if (!answer.length && allowUnconfirmed) + answer = Transaction._selectUnspent(unspentArray, totalNeededAmount, 0); + + return answer; } /* @@ -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 d9fac53bc..9c818a804 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 64a727024..bf0671d84 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,26 +53,64 @@ 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 // Inner arrays are either [ "comment" ]