From 041f06aae1595bcd2e5906731c5b4f59adb3ba64 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 16 Mar 2014 23:47:01 -0300 Subject: [PATCH] dynamic fee --- Transaction.js | 103 +++++++++++++++++++++++++++---------- test/data/unspentSign.json | 2 +- test/test.Transaction.js | 62 +++++++++++++++++++++- 3 files changed, 136 insertions(+), 31 deletions(-) diff --git a/Transaction.js b/Transaction.js index fb7a8f7..c16a252 100644 --- a/Transaction.js +++ b/Transaction.js @@ -16,7 +16,7 @@ var WalletKey = imports.WalletKey || require('./WalletKey'); var PrivateKey = imports.PrivateKey || require('./PrivateKey'); var COINBASE_OP = Buffer.concat([util.NULL_HASH, new Buffer('FFFFFFFF', 'hex')]); -var DEFAULT_FEE = 0.001; +var FEE_PER_1000B_SAT = parseInt(0.0001 * util.COIN); function TransactionIn(data) { if ("object" !== typeof data) { @@ -777,7 +777,7 @@ Transaction._sumOutputs = function(outs) { Transaction.prepare = function (ins, outs, opts) { opts = opts || {}; - var feeSat = opts.feeSat || util.parseValue(opts.fee || DEFAULT_FEE); + var feeSat = opts.feeSat || (opts.fee? util.parseValue(opts.fee) : FEE_PER_1000B_SAT ); var txobj = {}; txobj.version = 1; txobj.lock_time = opts.lockTime || 0; @@ -815,27 +815,27 @@ Transaction.prepare = function (ins, outs, opts) { inv + ' < '+ouv + ' [SAT]'); } - var remainderSat = valueInSat.sub(valueOutSat); - - if (remainderSat.cmp(0)>0) { - var remainderAddress = opts.remainderAddress || ins[0].address; - - outs.push({ - address: remainderAddress, - amountSat: remainderSat, - }); - } - for(var i=0;i0) { + var remainderAddress = opts.remainderAddress || ins[0].address; + var value = util.bigIntToValue(remainderSat); + var script = Transaction._scriptForAddress(remainderAddress); + var txout = { + v: value, + s: script.getBuffer(), + }; txobj.outs.push(txout); } @@ -843,6 +843,31 @@ Transaction.prepare = function (ins, outs, opts) { return new Transaction(txobj); }; +Transaction.prototype.calcSize = function () { + var totalSize = 8; // version + lock_time + totalSize += util.getVarIntSize(this.ins.length); // tx_in count + this.ins.forEach(function (txin) { + totalSize += 36 + util.getVarIntSize(txin.s.length) + + txin.s.length + 4; // outpoint + script_len + script + sequence + }); + + totalSize += util.getVarIntSize(this.outs.length); + this.outs.forEach(function (txout) { + totalSize += util.getVarIntSize(txout.s.length) + + txout.s.length + 8; // script_len + script + value + }); + this.size = totalSize; + return totalSize; +}; + +Transaction.prototype.getSize = function getHash() { + if (!this.size) { + this.size = this.calcSize(); + } + return this.size; +}; + + Transaction.prototype.isComplete = function () { var l = this.ins.length; @@ -1021,23 +1046,45 @@ Transaction.prototype.sign = function (selectedUtxos, keys, opts) { Transaction.create = function (utxos, outs, keys, opts) { - var valueOutSat = Transaction - ._sumOutputs(outs) - .add(DEFAULT_FEE * util.COIN); + //starting size estimation + var size = 500; + var opts = opts || {}; - var selectedUtxos = Transaction - .selectUnspent(utxos,valueOutSat / util.COIN, opts.allowUnconfirmed); - - if (!selectedUtxos) { - throw new Error( - 'the given UTXOs dont sum up the given outputs: ' - + valueOutSat.toString() - + ' SAT' - ); + var givenFeeSat; + if (opts.fee || opts.feeSat) { + givenFeeSat = opts.fee ? opts.fee * util.COIN : opts.feeSat; } - var tx = Transaction.prepare(selectedUtxos, outs, opts); - //TODO interate with the new TX fee + do { + // based on https://en.bitcoin.it/wiki/Transaction_fees + maxSizeK = parseInt(size/1000) + 1; + var feeSat = givenFeeSat + ? givenFeeSat : maxSizeK * FEE_PER_1000B_SAT ; + + var valueOutSat = Transaction + ._sumOutputs(outs) + .add(feeSat); + + var selectedUtxos = Transaction + .selectUnspent(utxos,valueOutSat / util.COIN, opts.allowUnconfirmed); + + if (!selectedUtxos) { + throw new Error( + 'the given UTXOs dont sum up the given outputs: ' + + valueOutSat.toString() + + ' (fee is ' + feeSat + + ' )SAT' + ); + } + var tx = Transaction.prepare(selectedUtxos, outs, { + feeSat: feeSat, + remainderAddress: opts.remainderAddress, + lockTime: opts.lockTime, + }); + + size = tx.getSize(); + } while (size > (maxSizeK+1)*1000 ); + if (keys) tx.sign(selectedUtxos, keys); return tx; }; diff --git a/test/data/unspentSign.json b/test/data/unspentSign.json index f03e40e..5826f4c 100644 --- a/test/data/unspentSign.json +++ b/test/data/unspentSign.json @@ -5,7 +5,7 @@ "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", "scriptPubKey": "76a914fe021bac469a5c49915b2a8ffa7390a9ce5580f988ac", "vout": 1, - "amount": 1.01, + "amount": 1.0101, "confirmations":7 }, { diff --git a/test/test.Transaction.js b/test/test.Transaction.js index f1cfce2..6de6b26 100644 --- a/test/test.Transaction.js +++ b/test/test.Transaction.js @@ -94,8 +94,9 @@ describe('Transaction', function() { tx.outs.length.should.equal(2); util.valueToBigInt(tx.outs[0].v).cmp(8000000).should.equal(0); + // remainder is 0.0299 here because unspent select utxos in order - util.valueToBigInt(tx.outs[1].v).cmp(2900000).should.equal(0); + util.valueToBigInt(tx.outs[1].v).cmp(2990000).should.equal(0); tx.isComplete().should.equal(false); }); @@ -106,6 +107,12 @@ describe('Transaction', function() { .create .bind(utxos, outs, null, opts) .should.throw(); + + var outs2 = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.5}]; + should.exist( Transaction.create(utxos, outs2, null, opts)); + + // do not allow unconfirmed + Transaction.create.bind(utxos, outs2).should.throw(); }); @@ -116,7 +123,7 @@ describe('Transaction', function() { // string output generated from: bitcoind createrawtransaction '[{"txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1","vout":1},{"txid":"2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc2","vout":0} ]' '{"mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE":0.08,"mwZabyZXg8JzUtFX1pkGygsMJjnuqiNhgd":0.0299}' - tx.serialize().toString('hex').should.equal('0100000002c1cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0100000000ffffffffc2cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0000000000ffffffff0200127a00000000001976a914774e603bafb717bd3f070e68bbcccfd907c77d1388ac20402c00000000001976a914b00127584485a7cff0949ef0f6bc5575f06ce00d88ac00000000'); + tx.serialize().toString('hex').should.equal('0100000002c1cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0100000000ffffffffc2cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0000000000ffffffff0200127a00000000001976a914774e603bafb717bd3f070e68bbcccfd907c77d1388acb09f2d00000000001976a914b00127584485a7cff0949ef0f6bc5575f06ce00d88ac00000000'); // no remainder outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; @@ -132,10 +139,14 @@ describe('Transaction', function() { var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; var tx = Transaction.create(utxos, outs, testdata.dataUnspentSign.keyStrings, opts); tx.isComplete().should.equal(true); + tx.ins.length.should.equal(1); + tx.outs.length.should.equal(2); var outs2 = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:16}]; var tx2 = Transaction.create(utxos, outs2, testdata.dataUnspentSign.keyStrings, opts); tx2.isComplete().should.equal(true); + tx2.ins.length.should.equal(3); + tx2.outs.length.should.equal(2); }); it('#sign should sign an incomplete tx ', function() { @@ -143,6 +154,8 @@ describe('Transaction', function() { var utxos =testdata.dataUnspentSign.unspent; var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; var tx = Transaction.create(utxos, outs, keys, opts); + tx.ins.length.should.equal(1); + tx.outs.length.should.equal(2); tx.isComplete().should.equal(false); }); it('#sign should sign a tx in multiple steps', function() { @@ -165,6 +178,51 @@ describe('Transaction', function() { }); + it('#create: should generate dynamic fee and readjust (and not) the selected UTXOs', function() { + //this cases exceeds the input by 1mbtc AFTEr calculating the dynamic fee, + //so, it should trigger adding a new 10BTC utxo + var utxos =testdata.dataUnspentSign.unspent; + var outs = []; + var n =101; + for (var i=0; i