From ada92746b7084d3bf181b1f477e0eaf385dd6676 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 14 Mar 2014 17:38:42 -0300 Subject: [PATCH 01/22] selectUnspent function and tests --- Transaction.js | 50 ++++++++++++++++++++++++++++++++++++++++ test/data/unspends.json | 24 +++++++++++++++++++ test/test.Transaction.js | 29 +++++++++++++++++++++++ test/testdata.js | 3 +++ 4 files changed, 106 insertions(+) create mode 100644 test/data/unspends.json diff --git a/Transaction.js b/Transaction.js index 37d2ad8..1da4964 100644 --- a/Transaction.js +++ b/Transaction.js @@ -679,6 +679,56 @@ 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: + * [{ + * 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. + * + * @totalNeededAmount: output transaction amount in BTC, including fee + * + * + * Return the selected outputs or null if there are not enough funds. + * It does not check for confirmations. The unspendArray should be filtered. + * + */ +Transaction.selectUnspent = function (unspentArray, totalNeededAmount) { + + // TODO we could randomize or select the selection + + var selected = []; + var l = unspentArray.length; + var totalSat = bignum(0); + var totalNeededAmountSat = bignum(totalNeededAmount * util.COIN); + var fullfill = false; + + for(var i = 0; i= 0) { + fullfill = true; + break; + } + } + if (!fullfill) return []; + return selected; +} + + + var TransactionInputsCache = exports.TransactionInputsCache = function TransactionInputsCache(tx) { diff --git a/test/data/unspends.json b/test/data/unspends.json new file mode 100644 index 0000000..d9fac53 --- /dev/null +++ b/test/data/unspends.json @@ -0,0 +1,24 @@ +[ + { + "address": "mqSjTad2TKbPcKQ3Jq4kgCkKatyN44UMgZ", + "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", + "scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ac", + "vout": 1, + "amount": 0.01 + }, + { + "address": "mqSjTad2TKbPcKQ3Jq4kgCkKatyN44UMgZ", + "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc2", + "scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ad", + "vout": 1, + "amount": 0.1 + }, + { + "address": "mqSjTad2TKbPcKQ3Jq4kgCkKatyN44UMgZ", + "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc3", + "scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ae", + "vout": 3, + "amount": 1 + } +] + diff --git a/test/test.Transaction.js b/test/test.Transaction.js index ff2dc4b..c8bf0b0 100644 --- a/test/test.Transaction.js +++ b/test/test.Transaction.js @@ -25,11 +25,40 @@ describe('Transaction', function() { should.exist(In); should.exist(Out); }); + + it('should be able to create instance', function() { var t = new Transaction(); should.exist(t); }); + + it('should be able to select unspents', function() { + var u = Transaction.selectUnspent(testdata.dataUnspends,1.0); + u.length.should.equal(3); + u = Transaction.selectUnspent(testdata.dataUnspends,0.5); + u.length.should.equal(3); + u = Transaction.selectUnspent(testdata.dataUnspends,0.1); + u.length.should.equal(2); + u = Transaction.selectUnspent(testdata.dataUnspends,0.05); + u.length.should.equal(2); + u = Transaction.selectUnspent(testdata.dataUnspends,0.015); + u.length.should.equal(2); + u = Transaction.selectUnspent(testdata.dataUnspends,0.01); + u.length.should.equal(1); + should.exist(u[0].amount); + should.exist(u[0].txid); + should.exist(u[0].scriptPubKey); + should.exist(u[0].vout); + }); + + + it.skip('should be able to create instance thru #create', function() { + var t = Transaction.create({ + }); + should.exist(t); + }); + // Read tests from test/data/tx_valid.json // Format is an array of arrays // Inner arrays are either [ "comment" ] diff --git a/test/testdata.js b/test/testdata.js index dd05be7..6eef35a 100644 --- a/test/testdata.js +++ b/test/testdata.js @@ -7,6 +7,8 @@ var dataTxValid = JSON.parse(fs.readFileSync('test/data/tx_valid.json')); var dataTxInvalid = JSON.parse(fs.readFileSync('test/data/tx_invalid.json')); var dataScriptValid = JSON.parse(fs.readFileSync('test/data/script_valid.json')); var dataScriptInvalid = JSON.parse(fs.readFileSync('test/data/script_invalid.json')); +var dataUnspends = JSON.parse(fs.readFileSync('test/data/unspends.json')); + module.exports.dataValid = dataValid; module.exports.dataInvalid = dataInvalid; @@ -16,3 +18,4 @@ module.exports.dataTxInvalid = dataTxInvalid; module.exports.dataScriptValid = dataScriptValid; module.exports.dataScriptInvalid = dataScriptInvalid; module.exports.dataScriptAll = dataScriptValid.concat(dataScriptInvalid); +module.exports.dataUnspends = dataUnspends; From 671d372c199d94b75e66fb18ba8ef48c27843f30 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 03:27:06 -0300 Subject: [PATCH 02/22] tx creation working. more tests needed --- Address.js | 16 ++++ Transaction.js | 175 +++++++++++++++++++++++++++++++-------- test/test.Transaction.js | 24 ++++-- util/util.js | 3 + 4 files changed, 180 insertions(+), 38 deletions(-) diff --git a/Address.js b/Address.js index 07333da..f8dcf8e 100644 --- a/Address.js +++ b/Address.js @@ -1,6 +1,7 @@ 'use strict'; var imports = require('soop').imports(); var parent = imports.parent || require('./util/VersionedData'); +var networks= imports.networks || require('./networks'); function Address() { Address.super(this, arguments); @@ -22,4 +23,19 @@ Address.prototype.isValid = function() { return answer; }; +Address.prototype.network = function() { + var version = this.version(); + + var livenet = networks.livenet; + var testnet = networks.testnet; + + var answer; + if (version === livenet.addressPubkey || version === livenet.addressScript) + answer = livenet; + else if (version === testnet.addressPubkey || version === testnet.addressScript) + answer = testnet; + + return answer; +}; + module.exports = require('soop')(Address); diff --git a/Transaction.js b/Transaction.js index 1da4964..5e04e13 100644 --- a/Transaction.js +++ b/Transaction.js @@ -11,8 +11,10 @@ var Parser = imports.Parser || require('./util/BinaryParser'); var Step = imports.Step || require('step'); var buffertools = imports.buffertools || require('buffertools'); var error = imports.error || require('./util/error'); +var networks = imports.networks || require('./networks'); var COINBASE_OP = Buffer.concat([util.NULL_HASH, new Buffer('FFFFFFFF', 'hex')]); +var DEFAULT_FEE = 0.0001; function TransactionIn(data) { if ("object" !== typeof data) { @@ -605,36 +607,7 @@ Transaction.prototype.fromObj = function fromObj(obj) { txobj.ins = []; txobj.outs = []; - obj.inputs.forEach(function(inputobj) { - var txin = new TransactionIn(); - txin.s = util.EMPTY_BUFFER; - txin.q = 0xffffffff; - - var hash = new Buffer(inputobj.txid, 'hex'); - hash = buffertools.reverse(hash); - var vout = parseInt(inputobj.vout); - var voutBuf = new Buffer(4); - voutBuf.writeUInt32LE(vout, 0); - - txin.o = Buffer.concat([hash, voutBuf]); - - txobj.ins.push(txin); - }); - - var keys = Object.keys(obj.outputs); - keys.forEach(function(addrStr) { - var addr = new Address(addrStr); - var script = Script.createPubKeyHashOut(addr.payload()); - - var valueNum = bignum(obj.outputs[addrStr]); - var value = util.bigIntToValue(valueNum); - - var txout = new TransactionOut(); - txout.v = value; - txout.s = script.getBuffer(); - - txobj.outs.push(txout); - }); + var tx = new Transaction(txobj); this.lock_time = txobj.lock_time; this.version = txobj.version; @@ -694,7 +667,8 @@ Transaction.prototype.parse = function (parser) { * }, [...] * ] * This is compatible con insight's /utxo API. - * NOTE that amount is in BTCs! (as returned in insight and bitcoind. + * NOTE 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 * @@ -705,17 +679,27 @@ Transaction.prototype.parse = function (parser) { */ 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 = bignum(totalNeededAmount * util.COIN); + var totalNeededAmountSat = util.parseValue(totalNeededAmount); var fullfill = false; for(var i = 0; i= 0) { @@ -727,7 +711,132 @@ Transaction.selectUnspent = function (unspentArray, totalNeededAmount) { return selected; } +/* + * _scriptForAddress + * + * Returns a scriptPubKey for the given address type + */ +Transaction._scriptForAddress = function (addressString) { + + var livenet = networks.livenet; + var testnet = networks.testnet; + var address = new Address(addressString); + + var version = address.version(); + var script; + if (version == livenet.addressPubkey || version == testnet.addressPubkey) + script = Script.createPubKeyHashOut(address.payload()); + else if (version == livenet.addressScript || version == testnet.addressScript) + script = Script.createP2SH(address.payload()); + else + throw new Error('invalid output address'); + + return script; +}; + +/* + * create + * + * creates a transaction + * + * @ins + * a selected set of utxos, as described on selectUnspent + * @outs + * an array of [{ + * address: xx, + * amount:0.001 +* },...] + * @opts + * { + * remainderAddress: null, + * fee: 0.001, + * lockTime: null, + * } + * + * Amounts are in BTC. instead of fee and amount; feeSat and amountSat can be given, + * repectively, to provide amounts in satoshis. + * + * (TODO: should we use uBTC already?) + * + * If not remainderAddress is given, and there is a remainderAddress + * first in address will be used. (TODO: is this is reasoable?) + * + * TODO add exceptions for validations: + * out address - ins amount > out amount - fees < maxFees + * + more? + */ + +Transaction.create = function (ins, outs, opts) { + opts = opts || {}; + + var feeSat = opts.feeSat || util.parseValue(opts.fee || DEFAULT_FEE); + var txobj = {}; + txobj.version = 1; + txobj.lock_time = opts.lockTime || 0; + txobj.ins = []; + txobj.outs = []; + + var l = ins.length; + var valueInSat = bignum(0); + for(var i=0; i0) { + var remainderAddress = opts.remainderAddress || ins[0].address; + + outs.push({ + address: remainderAddress, + amountSat: remainderSat, + }); + } + + for(var i=0;i Date: Sat, 15 Mar 2014 12:21:59 -0300 Subject: [PATCH 03/22] #create for Transaction and tests --- Transaction.js | 93 +++++++++++++++++++++++++--------------- test/data/unspends.json | 7 ++- test/test.Transaction.js | 64 +++++++++++++++++++++------ 3 files changed, 115 insertions(+), 49 deletions(-) diff --git a/Transaction.js b/Transaction.js index 5e04e13..4fcccb1 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 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,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" ] From 35f5c9c57015a0910927de0411cedac14dcd91ca Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 12:22:15 -0300 Subject: [PATCH 04/22] #create for Transaction and tests --- test/test.Transaction.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test.Transaction.js b/test/test.Transaction.js index bf0671d..b6fa09a 100644 --- a/test/test.Transaction.js +++ b/test/test.Transaction.js @@ -104,6 +104,7 @@ describe('Transaction', function() { var tx = Transaction.create(utxos, outs, opts); tx.serialize().toString('hex').should.equal('0100000002c1cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0100000000ffffffffc2cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0000000000ffffffff0200127a00000000001976a914774e603bafb717bd3f070e68bbcccfd907c77d1388acb09f2d00000000001976a914b00127584485a7cff0949ef0f6bc5575f06ce00d88ac00000000'); + // no remainder outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; tx = Transaction.create(utxos, outs, {fee:0.03} ); tx.serialize().toString('hex').should.equal('0100000002c1cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0100000000ffffffffc2cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0000000000ffffffff0100127a00000000001976a914774e603bafb717bd3f070e68bbcccfd907c77d1388ac00000000'); From 128662ceeeaaecd5a325d2ba96b916d48d2f29b7 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 12:22:36 -0300 Subject: [PATCH 05/22] #create for Transaction and tests --- test/test.Transaction.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test.Transaction.js b/test/test.Transaction.js index b6fa09a..47dd752 100644 --- a/test/test.Transaction.js +++ b/test/test.Transaction.js @@ -108,7 +108,6 @@ describe('Transaction', function() { outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; tx = Transaction.create(utxos, outs, {fee:0.03} ); tx.serialize().toString('hex').should.equal('0100000002c1cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0100000000ffffffffc2cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0000000000ffffffff0100127a00000000001976a914774e603bafb717bd3f070e68bbcccfd907c77d1388ac00000000'); - }); From ced6c91b69bdda002bfcbd55317591fe237846c5 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 17:44:29 -0300 Subject: [PATCH 06/22] add comment to generate hardcoded dara --- test/test.Transaction.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/test.Transaction.js b/test/test.Transaction.js index 47dd752..bed43d9 100644 --- a/test/test.Transaction.js +++ b/test/test.Transaction.js @@ -102,11 +102,17 @@ describe('Transaction', function() { var utxos = Transaction.selectUnspent(testdata.dataUnspends,0.1); var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; var tx = Transaction.create(utxos, outs, opts); + + + // 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('0100000002c1cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0100000000ffffffffc2cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0000000000ffffffff0200127a00000000001976a914774e603bafb717bd3f070e68bbcccfd907c77d1388acb09f2d00000000001976a914b00127584485a7cff0949ef0f6bc5575f06ce00d88ac00000000'); // no remainder outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; tx = Transaction.create(utxos, outs, {fee:0.03} ); + + // string output generated from: bitcoind createrawtransaction '[{"txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1","vout":1},{"txid":"2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc2","vout":0} ]' '{"mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE":0.08}' + // tx.serialize().toString('hex').should.equal('0100000002c1cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0100000000ffffffffc2cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0000000000ffffffff0100127a00000000001976a914774e603bafb717bd3f070e68bbcccfd907c77d1388ac00000000'); }); From d473b400deea730e970bfdf0a8c97eafb6193dd1 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 17:58:38 -0300 Subject: [PATCH 07/22] remove outdated example --- examples/createTx.js | 205 --------------------------------------- test/test.Transaction.js | 9 +- 2 files changed, 8 insertions(+), 206 deletions(-) delete mode 100644 examples/createTx.js diff --git a/examples/createTx.js b/examples/createTx.js deleted file mode 100644 index e9cc25a..0000000 --- a/examples/createTx.js +++ /dev/null @@ -1,205 +0,0 @@ - - - -var bitcore = require('../bitcore'); - -var priv = 'cTgGUrcro89yUtKeG6gHBAS14r3qp25KwTTxG9d4kEzcFxecuZDm'; -var amt = '0.005'; -var toAddress = 'myuAQcCc1REUgXGsCTiYhZvPPc3XxZ36G1'; -var changeAddressString = 'moDz3jEo9q7CxjBDjmb13sL4SKkgo2AACE'; -var feeString = '0.0001'; - -var safeUnspent = [ -{ -address: "mqSjTad2TKbPcKQ3Jq4kgCkKatyN44UMgZ", -hash: "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", -vout: 1, -ts: 1394719301, -scriptPubKey: "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ac", -amount: 0.01, -confirmations: 2 -} -] -; - -console.log('TX Data: BTC:' + amt + ' => '+ toAddress + ', change To:' + changeAddressString ) ; -console.log('Unspends:', safeUnspent); - -var wk = new bitcore.WalletKey({ - network: bitcore.networks.testnet -}); -wk.fromObj({ priv: priv, }); - -var wkObj= wk.storeObj(); -var keyPairs = [{ - key: wkObj.priv, - address: wkObj.addr, -}]; -console.log('KEY DB IS:', keyPairs); - -var Address = bitcore.Address; -var Transaction = bitcore.Transaction; -var Script = bitcore.Script; -var nets = bitcore.networks; -var z = bitcore.bignum(0); -var amt = bitcore.util.parseValue(amt); - -if(z.cmp(amt) === 0 ) - throw "spend amount must be greater than zero"; - -if(!changeAddressString) - throw "change address was not provided"; - -var fee = bitcore.util.parseValue(feeString || '0'); -var total = bitcore.bignum(0).add(amt).add(fee); -var address = new Address(toAddress); -var sendTx = new Transaction(); -var i; - -var unspent = []; -var unspentAmt = bitcore.bignum(0); - - -for(i=0;i -1, we have enough to send the requested amount - if(unspentAmt.cmp(total) > -1) { - break; - } - } - - if(unspentAmt.cmp(total) < 0) { - throw "you do not have enough bitcoins to send this amount"; - } - - var txobj = {}; - txobj.version = 1; - txobj.lock_time = 0; - txobj.ins = []; - txobj.outs = []; - - for(i=0;i Date: Sat, 15 Mar 2014 18:10:46 -0300 Subject: [PATCH 08/22] add example to createKey --- examples/CreateKey.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 examples/CreateKey.js diff --git a/examples/CreateKey.js b/examples/CreateKey.js new file mode 100644 index 0000000..91f5aa9 --- /dev/null +++ b/examples/CreateKey.js @@ -0,0 +1,42 @@ +'use strict'; + + +var run = function() { + // Replace '../bitcore' with 'bitcore' if you use this code elsewhere. + var bitcore = require('../bitcore'); + var networks = require('../networks'); + var WalletKey = bitcore.WalletKey; + + + function print(wk) { + + console.log('\n## Network: ' + wk.network.name); + console.log ('\t * Hex Representation'); + console.log ('\tPrivate: ' + bitcore.buffertools.toHex(wk.privKey.private)); + console.log ('\tPublic : ' + bitcore.buffertools.toHex(wk.privKey.public)); + console.log ('\tPublic Compressed : ' + (wk.privKey.compressed?'Yes':'No')); + + var wkObj = wk.storeObj(); + console.log ('\n * WalletKey Store Object'); + console.log ('\tPrivate: ' + wkObj.priv); + console.log ('\tPublic : ' + wkObj.pub); + console.log ('\tAddr : ' + wkObj.addr); + }; + + //Generate a new one + var wk = new WalletKey({network: networks.testnet}); + wk.generate(); + print(wk); + + //Generate from private Key WIF + var wk2 = new WalletKey({network: networks.testnet}); + wk2.fromObj({priv:'cS62Ej4SobZnpFQYN1PEEBr2KWf5sgRYYnELtumcG6WVCfxno39V'}); + print(wk2); + + +}; + +module.exports.run = run; +if (require.main === module) { + run(); +} From b6e6ad28eb681ac5739b6d070c4a12ba9ceb31f4 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 18:21:59 -0300 Subject: [PATCH 09/22] fix variable names --- test/data/{unspends.json => unspent.json} | 0 test/data/unspentSign.json | 34 +++++++++++++++++++++++ test/test.Transaction.js | 32 ++++++++++----------- test/testdata.js | 6 ++-- 4 files changed, 54 insertions(+), 18 deletions(-) rename test/data/{unspends.json => unspent.json} (100%) create mode 100644 test/data/unspentSign.json diff --git a/test/data/unspends.json b/test/data/unspent.json similarity index 100% rename from test/data/unspends.json rename to test/data/unspent.json diff --git a/test/data/unspentSign.json b/test/data/unspentSign.json new file mode 100644 index 0000000..ce2fa69 --- /dev/null +++ b/test/data/unspentSign.json @@ -0,0 +1,34 @@ +{ + "unspent": [ + { + "address": "n4g2TFaQo8UgedwpkYdcQFF6xE2Ei9Czvy", + "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", + "scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ac", + "vout": 1, + "amount": 1.01, + "confirmations":7 + }, + { + "address": "mhNCT9TwZAGF1tLPpZdqfkTmtBkY282YDW", + "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc2", + "scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ad", + "vout": 0, + "confirmations": 1, + "amount": 10 + }, + { + "address": "n44hn28zAooZpn8mpWKzATbabqaHDK9oNJ", + "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc3", + "scriptPubKey": "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ae", + "vout": 3, + "confirmations": 0, + "amount": 5 + } + ], + "keyStrings": [ + "cSq7yo4fvsbMyWVN945VUGUWMaSazZPWqBVJZyoGsHmNq6W4HVBV", + "cPa87VgwZfowGZYaEenoQeJgRfKW6PhZ1R65EHTkN1K19cSvc92G", + "cPQ9DSbBRLva9av5nqeF5AGrh3dsdW8p2E5jS4P8bDWZAoQTeeKB" + ] +} + diff --git a/test/test.Transaction.js b/test/test.Transaction.js index 835b918..926bf1a 100644 --- a/test/test.Transaction.js +++ b/test/test.Transaction.js @@ -35,17 +35,17 @@ describe('Transaction', function() { it('#_selectUnspent should be able to select utxos', function() { - var u = Transaction._selectUnspent(testdata.dataUnspends,1.0); + var u = Transaction._selectUnspent(testdata.dataUnspent,1.0); u.length.should.equal(3); - u = Transaction._selectUnspent(testdata.dataUnspends,0.5); + u = Transaction._selectUnspent(testdata.dataUnspent,0.5); u.length.should.equal(3); - u = Transaction._selectUnspent(testdata.dataUnspends,0.1); + u = Transaction._selectUnspent(testdata.dataUnspent,0.1); u.length.should.equal(2); - u = Transaction._selectUnspent(testdata.dataUnspends,0.05); + u = Transaction._selectUnspent(testdata.dataUnspent,0.05); u.length.should.equal(2); - u = Transaction._selectUnspent(testdata.dataUnspends,0.015); + u = Transaction._selectUnspent(testdata.dataUnspent,0.015); u.length.should.equal(2); - u = Transaction._selectUnspent(testdata.dataUnspends,0.01); + u = Transaction._selectUnspent(testdata.dataUnspent,0.01); u.length.should.equal(1); should.exist(u[0].amount); should.exist(u[0].txid); @@ -54,27 +54,27 @@ describe('Transaction', function() { }); it('#selectUnspent should return null if not enough utxos', function() { - var u = Transaction.selectUnspent(testdata.dataUnspends,1.12); + var u = Transaction.selectUnspent(testdata.dataUnspent,1.12); u.length.should.equal(0); }); it('#selectUnspent should check confirmations', function() { - var u = Transaction.selectUnspent(testdata.dataUnspends,0.9); + var u = Transaction.selectUnspent(testdata.dataUnspent,0.9); u.length.should.equal(0); - var u = Transaction.selectUnspent(testdata.dataUnspends,0.9,true); + var u = Transaction.selectUnspent(testdata.dataUnspent,0.9,true); u.length.should.equal(3); - var u = Transaction.selectUnspent(testdata.dataUnspends,0.11); + var u = Transaction.selectUnspent(testdata.dataUnspent,0.11); u.length.should.equal(2); - var u = Transaction.selectUnspent(testdata.dataUnspends,0.111); + var u = Transaction.selectUnspent(testdata.dataUnspent,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 utxos = Transaction.selectUnspent(testdata.dataUnspent,0.1); var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; var tx = Transaction.create(utxos, outs, opts); should.exist(tx); @@ -83,13 +83,13 @@ describe('Transaction', function() { tx.ins.length.should.equal(2); tx.outs.length.should.equal(2); util.valueToBigInt(tx.outs[0].v).cmp(8000000).should.equal(0); - // remainder is 0.0299 here because unspend select utxos in order + // remainder is 0.0299 here because unspent 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 utxos = Transaction.selectUnspent(testdata.dataUnspent,1); var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; Transaction .create @@ -99,7 +99,7 @@ describe('Transaction', function() { it('#create should create same output as bitcoind createrawtransaction ', function() { - var utxos = Transaction.selectUnspent(testdata.dataUnspends,0.1); + var utxos = Transaction.selectUnspent(testdata.dataUnspent,0.1); var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; var tx = Transaction.create(utxos, outs, opts); @@ -117,7 +117,7 @@ describe('Transaction', function() { }); it.skip('#sign should sign a tx', function() { - var utxos = Transaction.selectUnspent(testdata.dataUnspends,0.1); + var utxos = Transaction.selectUnspent(testdata.dataUnspentign.unspent,0.1); var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; var tx = Transaction.create(utxos, outs, opts); }); diff --git a/test/testdata.js b/test/testdata.js index 6eef35a..6460c23 100644 --- a/test/testdata.js +++ b/test/testdata.js @@ -7,7 +7,8 @@ var dataTxValid = JSON.parse(fs.readFileSync('test/data/tx_valid.json')); var dataTxInvalid = JSON.parse(fs.readFileSync('test/data/tx_invalid.json')); var dataScriptValid = JSON.parse(fs.readFileSync('test/data/script_valid.json')); var dataScriptInvalid = JSON.parse(fs.readFileSync('test/data/script_invalid.json')); -var dataUnspends = JSON.parse(fs.readFileSync('test/data/unspends.json')); +var dataUnspent = JSON.parse(fs.readFileSync('test/data/unspent.json')); +var dataUnspentSign = JSON.parse(fs.readFileSync('test/data/unspentSign.json')); module.exports.dataValid = dataValid; @@ -18,4 +19,5 @@ module.exports.dataTxInvalid = dataTxInvalid; module.exports.dataScriptValid = dataScriptValid; module.exports.dataScriptInvalid = dataScriptInvalid; module.exports.dataScriptAll = dataScriptValid.concat(dataScriptInvalid); -module.exports.dataUnspends = dataUnspends; +module.exports.dataUnspent = dataUnspent; +module.exports.dataUnspentSign = dataUnspentSign; From 7d1df2602ce7922bb1871547aa0ebddf29227e40 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 18:39:51 -0300 Subject: [PATCH 10/22] add test for #network in Address --- test/test.Address.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/test.Address.js b/test/test.Address.js index 21d1534..3e0441f 100644 --- a/test/test.Address.js +++ b/test/test.Address.js @@ -47,4 +47,24 @@ describe('Address', function() { s.should.equal(a.toString()); // check that validation doesn't change data }); }); + it('should be able to detect network from an address', function() { + var a = new Address('1KfyjCgBSMsLqiCbakfSdeoBUqMqLUiu3T'); + a.network().name.should.equal('livenet'); + var a = new Address('1dice8EMZmqKvrGE4Qc9bUFf9PX3xaYDp'); + a.network().name.should.equal('livenet'); + //p2sh + var a = new Address('3QRhucKtEn5P9i7YPxzXCqBtPJTPbRFycn'); + a.network().name.should.equal('livenet'); + + //testnet + var a = new Address('mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE'); + a.network().name.should.equal('testnet'); + var a = new Address('n2ekxibY5keRiMaoKFGfiNfXQCS4zTUpct'); + a.network().name.should.equal('testnet'); + + //p2sh + var a = new Address('2NBSBcf2KfjPEEqVusmrWdmUeNHRiUTS3Li'); + a.network().name.should.equal('testnet'); + }); + }); From 5bf652dcb69870cb38bc595570209d0a52680d44 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 18:46:59 -0300 Subject: [PATCH 11/22] add uncompressed key generation example --- examples/CreateKey.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/examples/CreateKey.js b/examples/CreateKey.js index 91f5aa9..5bc7196 100644 --- a/examples/CreateKey.js +++ b/examples/CreateKey.js @@ -1,12 +1,14 @@ 'use strict'; + var run = function() { // Replace '../bitcore' with 'bitcore' if you use this code elsewhere. var bitcore = require('../bitcore'); var networks = require('../networks'); var WalletKey = bitcore.WalletKey; + var opts = {network: networks.livenet}; function print(wk) { @@ -17,19 +19,26 @@ var run = function() { console.log ('\tPublic Compressed : ' + (wk.privKey.compressed?'Yes':'No')); var wkObj = wk.storeObj(); - console.log ('\n * WalletKey Store Object'); + console.log ('\n\t * WalletKey Store Object'); console.log ('\tPrivate: ' + wkObj.priv); console.log ('\tPublic : ' + wkObj.pub); console.log ('\tAddr : ' + wkObj.addr); }; - //Generate a new one - var wk = new WalletKey({network: networks.testnet}); + //Generate a new one (compressed public key, compressed WIK flag) + var wk = new WalletKey(opts); wk.generate(); print(wk); + //Generate a new one (uncompressed public key, uncompressed WIK flag) + var wk = new WalletKey(opts); + wk.generate(); + wk.privKey.compressed = false; + wk.privKey.regenerateSync(); + print(wk); + //Generate from private Key WIF - var wk2 = new WalletKey({network: networks.testnet}); + var wk2 = new WalletKey(opts); wk2.fromObj({priv:'cS62Ej4SobZnpFQYN1PEEBr2KWf5sgRYYnELtumcG6WVCfxno39V'}); print(wk2); From 807a72666ceee93d81c9824d0e0e5a6d61ae5c54 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 18:49:48 -0300 Subject: [PATCH 12/22] add network detection in private key and tests --- PrivateKey.js | 16 ++++++++++++++++ test/test.PrivateKey.js | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/PrivateKey.js b/PrivateKey.js index c9a811c..9e2eac6 100644 --- a/PrivateKey.js +++ b/PrivateKey.js @@ -1,6 +1,7 @@ var imports = require('soop').imports(); var parent = imports.parent || require('./util/VersionedData'); +var networks= imports.networks || require('./networks'); //compressed is true if public key is compressed; false otherwise function PrivateKey(version, buf, compressed) { @@ -61,4 +62,19 @@ PrivateKey.prototype.compressed = function(compressed) { } }; +PrivateKey.prototype.network = function() { + var version = this.version(); + + var livenet = networks.livenet; + var testnet = networks.testnet; + + var answer; + if (version === livenet.keySecret) + answer = livenet; + else if (version === testnet.keySecret) + answer = testnet; + + return answer; +}; + module.exports = require('soop')(PrivateKey); diff --git a/test/test.PrivateKey.js b/test/test.PrivateKey.js index f6508e5..3d41de7 100644 --- a/test/test.PrivateKey.js +++ b/test/test.PrivateKey.js @@ -29,4 +29,24 @@ describe('PrivateKey', function() { privkey.as('base58').should.equal('cTpB4YiyKiBcPxnefsDpbnDxFDffjqJob8wGCEDXxgQ7zQoMXJdH'); }); + + it('should be able to detect network from privatekey', function() { + var a = new PrivateKey('cMu64LfQqrPC83SjJqde4mZ5jzC48zyeKbUZbTjQdh6pa1h48TdM'); + a.network().name.should.equal('testnet'); + var a = new PrivateKey('cS62Ej4SobZnpFQYN1PEEBr2KWf5sgRYYnELtumcG6WVCfxno39V'); + a.network().name.should.equal('testnet'); + + //compress flag = on + var a = new PrivateKey('KwHXRTLNWKzxy2NUnnhFtxricC3Dod4Dd3D7RKzVkKDtWrZhuDHs'); + a.network().name.should.equal('livenet'); + var a = new PrivateKey('KwaLX8oyJNNCL9tcyYakQHJDTnrPAmZ2M1YK7NhEcT9j55LWqMZz'); + a.network().name.should.equal('livenet'); + + //compress flag = off + var a = new PrivateKey('5KS4jw2kT3VoEFUfzgSpX3GVi7qRYkTfwTBU7qxPKyvbGuiVj33'); + a.network().name.should.equal('livenet'); + var a = new PrivateKey('5JZsbYcnYN8Dz2YeSLZr6aswrVevedMUSFWxpie6SPpYRb2E4Gi'); + a.network().name.should.equal('livenet'); + }); + }); From a6463a3835d0003d61372192a03e47cc489261ec Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 21:04:56 -0300 Subject: [PATCH 13/22] ignore tags file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 44f3737..4c15f13 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules/ *~ .project README.html +tags From a2041d5790a2f9157fc6d439ef386f61c703c51c Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 21:12:23 -0300 Subject: [PATCH 14/22] add TX signing. Support to p2pubkeyhash --- Transaction.js | 117 +++++++++++++++++++++++++++++++++++-- test/data/unspentSign.json | 6 +- test/test.Transaction.js | 33 ++++++++++- 3 files changed, 145 insertions(+), 11 deletions(-) diff --git a/Transaction.js b/Transaction.js index 4fcccb1..3b58233 100644 --- a/Transaction.js +++ b/Transaction.js @@ -12,6 +12,8 @@ var Step = imports.Step || require('step'); var buffertools = imports.buffertools || require('buffertools'); var error = imports.error || require('./util/error'); var networks = imports.networks || require('./networks'); +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.0001; @@ -780,12 +782,12 @@ Transaction._scriptForAddress = function (addressString) { * * (TODO: should we use uBTC already?) * - * If not remainderAddress is given, and there is a remainderAddress - * first in address will be used. (TODO: is this is reasoable?) + * If no remainderAddress is given, and there is a remainderAddress + * first in address will be used. (TODO: is this is reasonable?) + * + * The address from the inputs will be added to the Transaction object + * for latter signing * - * TODO add exceptions for validations: - * out address - ins amount > out amount - fees < maxFees - * + more? */ Transaction.create = function (ins, outs, opts) { @@ -798,6 +800,8 @@ Transaction.create = function (ins, outs, opts) { txobj.ins = []; txobj.outs = []; + var inputMap = []; + var l = ins.length; var valueInSat = bignum(0); for(var i=0; i Date: Sat, 15 Mar 2014 21:15:10 -0300 Subject: [PATCH 15/22] add limit to try-verify loop --- Transaction.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Transaction.js b/Transaction.js index 3b58233..874183d 100644 --- a/Transaction.js +++ b/Transaction.js @@ -943,10 +943,15 @@ Transaction.prototype.sign = function (keys, opts) { var txSigHash = self.hashForSignature(s, i, signhash); var sigRaw; + var triesLeft = 10; do { sigRaw = wk.privKey.signSync(txSigHash); - } while ( wk.privKey.verifySignatureSync(txSigHash, sigRaw) === false ); + } while ( wk.privKey.verifySignatureSync(txSigHash, sigRaw) === false && triesLeft-- ); + if (!triesLeft) { + log.debug('could not sign input:'+i +' verification failed'); + continue; + } var sigType = new Buffer(1); sigType[0] = signhash; From 242f4381ae117ba9fd3271a6eb4e63a30d807d74 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 15 Mar 2014 22:00:58 -0300 Subject: [PATCH 16/22] fix error in commit --- Transaction.js | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Transaction.js b/Transaction.js index 874183d..1c55c78 100644 --- a/Transaction.js +++ b/Transaction.js @@ -609,7 +609,36 @@ Transaction.prototype.fromObj = function fromObj(obj) { txobj.ins = []; txobj.outs = []; - var tx = new Transaction(txobj); + obj.inputs.forEach(function(inputobj) { + var txin = new TransactionIn(); + txin.s = util.EMPTY_BUFFER; + txin.q = 0xffffffff; + + var hash = new Buffer(inputobj.txid, 'hex'); + hash = buffertools.reverse(hash); + var vout = parseInt(inputobj.vout); + var voutBuf = new Buffer(4); + voutBuf.writeUInt32LE(vout, 0); + + txin.o = Buffer.concat([hash, voutBuf]); + + txobj.ins.push(txin); + }); + + var keys = Object.keys(obj.outputs); + keys.forEach(function(addrStr) { + var addr = new Address(addrStr); + var script = Script.createPubKeyHashOut(addr.payload()); + + var valueNum = bignum(obj.outputs[addrStr]); + var value = util.bigIntToValue(valueNum); + + var txout = new TransactionOut(); + txout.v = value; + txout.s = script.getBuffer(); + + txobj.outs.push(txout); + }); this.lock_time = txobj.lock_time; this.version = txobj.version; From aac13a8817bdd6ec844a7c81177adab6445318ee Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 16 Mar 2014 07:09:46 -0300 Subject: [PATCH 17/22] remove uncompressed key example --- examples/CreateKey.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/examples/CreateKey.js b/examples/CreateKey.js index 5bc7196..8b162cf 100644 --- a/examples/CreateKey.js +++ b/examples/CreateKey.js @@ -25,19 +25,12 @@ var run = function() { console.log ('\tAddr : ' + wkObj.addr); }; - //Generate a new one (compressed public key, compressed WIK flag) + //Generate a new one (compressed public key, compressed WIF flag) var wk = new WalletKey(opts); wk.generate(); print(wk); - //Generate a new one (uncompressed public key, uncompressed WIK flag) - var wk = new WalletKey(opts); - wk.generate(); - wk.privKey.compressed = false; - wk.privKey.regenerateSync(); - print(wk); - - //Generate from private Key WIF + //Generate from private Key WIF. Compressed status taken from WIF. var wk2 = new WalletKey(opts); wk2.fromObj({priv:'cS62Ej4SobZnpFQYN1PEEBr2KWf5sgRYYnELtumcG6WVCfxno39V'}); print(wk2); From 42d30f44a844e9609fd481e4fed17b71e447bf8d Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 16 Mar 2014 19:18:46 -0300 Subject: [PATCH 18/22] better utxo selection, combining inputs different confimations steps --- Transaction.js | 91 ++++++++++++++++++---------------------- test/test.Transaction.js | 36 +++++++++------- 2 files changed, 61 insertions(+), 66 deletions(-) diff --git a/Transaction.js b/Transaction.js index 1c55c78..3393f36 100644 --- a/Transaction.js +++ b/Transaction.js @@ -683,51 +683,12 @@ 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 * - * Selects some unspend outputs for later usage in tx inputs + * Selects some unspent outputs for later usage in tx inputs * * @unspentArray: unspent array (UTXO) avaible on the form: * [{ @@ -739,28 +700,56 @@ Transaction._selectUnspent = function (unspentArray, totalNeededAmount, minConfi * confirmations: 3 * }, [...] * ] - * This is compatible con insight's /utxo API. - * That amount is in BTCs. (as returned in insight and bitcoind) - * amountSat can be given to provide amount in satochis. + * This is compatible con insight's utxo API. + * That amount is in BTCs (as returned in insight and bitcoind). + * amountSat (instead of amount) can be given to provide amount in satochis. * * @totalNeededAmount: output transaction amount in BTC, including fee - * @allowUnconfirmed:false (allow selecting unconfirmed utxos) - * + * @allowUnconfirmed: false (allow selecting unconfirmed utxos) * * Note that the sum of the selected unspent is >= the desired amount. + * Returns the selected unspent outputs if the totalNeededAmount was reach. + * 'null' if not. + * + * TODO: utxo selection is not optimized to minimize mempool usage. * */ Transaction.selectUnspent = function (unspentArray, totalNeededAmount, allowUnconfirmed) { - var answer = Transaction._selectUnspent(unspentArray, totalNeededAmount, 6); - if (!answer.length) - answer = Transaction._selectUnspent(unspentArray, totalNeededAmount, 1); + var minConfirmationSteps = [6,1]; + if (allowUnconfirmed) minConfirmationSteps.push(0); - if (!answer.length && allowUnconfirmed) - answer = Transaction._selectUnspent(unspentArray, totalNeededAmount, 0); + var ret = []; + var l = unspentArray.length; + var totalSat = bignum(0); + var totalNeededAmountSat = util.parseValue(totalNeededAmount); + var fulfill = false; + var maxConfirmations = null; - return answer; + do { + var minConfirmations = minConfirmationSteps.shift(); + for(var i = 0; i=maxConfirmations) ) + continue; + + + var sat = u.amountSat || util.parseValue(u.amount); + totalSat = totalSat.add(sat); + ret.push(u); + if(totalSat.cmp(totalNeededAmountSat) >= 0) { + fulfill = true; + break; + } + } + maxConfirmations = minConfirmations; + } while( !fulfill && minConfirmationSteps.length); + + return fulfill ? ret : null; } /* diff --git a/test/test.Transaction.js b/test/test.Transaction.js index 8755592..3631f04 100644 --- a/test/test.Transaction.js +++ b/test/test.Transaction.js @@ -34,41 +34,47 @@ describe('Transaction', function() { }); - it('#_selectUnspent should be able to select utxos', function() { - var u = Transaction._selectUnspent(testdata.dataUnspent,1.0); + it('#selectUnspent should be able to select utxos', function() { + var u = Transaction.selectUnspent(testdata.dataUnspent,1.0, true); u.length.should.equal(3); - u = Transaction._selectUnspent(testdata.dataUnspent,0.5); - u.length.should.equal(3); - u = Transaction._selectUnspent(testdata.dataUnspent,0.1); - u.length.should.equal(2); - u = Transaction._selectUnspent(testdata.dataUnspent,0.05); - u.length.should.equal(2); - u = Transaction._selectUnspent(testdata.dataUnspent,0.015); - u.length.should.equal(2); - u = Transaction._selectUnspent(testdata.dataUnspent,0.01); - u.length.should.equal(1); + should.exist(u[0].amount); should.exist(u[0].txid); should.exist(u[0].scriptPubKey); should.exist(u[0].vout); + + u = Transaction.selectUnspent(testdata.dataUnspent,0.5, true); + u.length.should.equal(3); + + u = Transaction.selectUnspent(testdata.dataUnspent,0.1, true); + u.length.should.equal(2); + + u = Transaction.selectUnspent(testdata.dataUnspent,0.05, true); + u.length.should.equal(2); + + u = Transaction.selectUnspent(testdata.dataUnspent,0.015, true); + u.length.should.equal(2); + + u = Transaction.selectUnspent(testdata.dataUnspent,0.01, true); + u.length.should.equal(1); }); it('#selectUnspent should return null if not enough utxos', function() { var u = Transaction.selectUnspent(testdata.dataUnspent,1.12); - u.length.should.equal(0); + should.not.exist(u); }); it('#selectUnspent should check confirmations', function() { var u = Transaction.selectUnspent(testdata.dataUnspent,0.9); - u.length.should.equal(0); + should.not.exist(u); var u = Transaction.selectUnspent(testdata.dataUnspent,0.9,true); u.length.should.equal(3); var u = Transaction.selectUnspent(testdata.dataUnspent,0.11); u.length.should.equal(2); var u = Transaction.selectUnspent(testdata.dataUnspent,0.111); - u.length.should.equal(0); + should.not.exist(u); }); From 0c83ecf5fb8e1eda5e675bc83576172dd85f7f9f Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 16 Mar 2014 20:50:49 -0300 Subject: [PATCH 19/22] new interfase for create TX (select + prepare + sign) --- Transaction.js | 203 ++++++++++++++++++++++++--------------- test/test.Transaction.js | 66 +++++++------ 2 files changed, 163 insertions(+), 106 deletions(-) diff --git a/Transaction.js b/Transaction.js index 3393f36..fb7a8f7 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.0001; +var DEFAULT_FEE = 0.001; function TransactionIn(data) { if ("object" !== typeof data) { @@ -690,20 +690,7 @@ Transaction.prototype.parse = function (parser) { * * Selects some unspent 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, - * confirmations: 3 - * }, [...] - * ] - * This is compatible con insight's utxo API. - * That amount is in BTCs (as returned in insight and bitcoind). - * amountSat (instead of amount) can be given to provide amount in satochis. - * + * @utxos * @totalNeededAmount: output transaction amount in BTC, including fee * @allowUnconfirmed: false (allow selecting unconfirmed utxos) * @@ -715,13 +702,13 @@ Transaction.prototype.parse = function (parser) { * */ -Transaction.selectUnspent = function (unspentArray, totalNeededAmount, allowUnconfirmed) { +Transaction.selectUnspent = function (utxos, totalNeededAmount, allowUnconfirmed) { var minConfirmationSteps = [6,1]; if (allowUnconfirmed) minConfirmationSteps.push(0); var ret = []; - var l = unspentArray.length; + var l = utxos.length; var totalSat = bignum(0); var totalNeededAmountSat = util.parseValue(totalNeededAmount); var fulfill = false; @@ -730,7 +717,7 @@ Transaction.selectUnspent = function (unspentArray, totalNeededAmount, allowUnco do { var minConfirmations = minConfirmationSteps.shift(); for(var i = 0; i Date: Sun, 16 Mar 2014 23:47:01 -0300 Subject: [PATCH 20/22] 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 Date: Sun, 16 Mar 2014 23:51:35 -0300 Subject: [PATCH 21/22] add comment regarding coins selection --- Transaction.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Transaction.js b/Transaction.js index c16a252..f9c8e69 100644 --- a/Transaction.js +++ b/Transaction.js @@ -736,6 +736,10 @@ Transaction.selectUnspent = function (utxos, totalNeededAmount, allowUnconfirmed maxConfirmations = minConfirmations; } while( !fulfill && minConfirmationSteps.length); + //TODO(?): sort ret and check is some inputs can be avoided. + //If the initial utxos are sorted, this step would be necesary only if + //utxos were selected from different minConfirmationSteps. + return fulfill ? ret : null; } From aab52ad2291c5cb9a2d8ad9a196907e4c29f8844 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 18 Mar 2014 11:32:31 -0300 Subject: [PATCH 22/22] updated interfase to create: create and createAndSign --- README.md | 83 ++++++++------- Transaction.js | 145 +++++++++++++++----------- examples/CreateAndSignTx.js | 201 ++++-------------------------------- test/test.Transaction.js | 91 +++++++++++----- 4 files changed, 212 insertions(+), 308 deletions(-) diff --git a/README.md b/README.md index de7b466..4802d4e 100644 --- a/README.md +++ b/README.md @@ -130,54 +130,53 @@ var bitcore = require('bitcore'); var networks = bitcore.networks; var Peer = bitcore.Peer; var Transaction = bitcore.Transaction; -var Address = bitcore.Address; -var Script = bitcore.Script; -var coinUtil = bitcore.util; var PeerManager = require('soop').load('../PeerManager', { network: networks.testnet }); -var createTx = function() { - var TXIN = 'd05f35e0bbc495f6dcab03e599c8f5e32a07cdb4bc76964de201d06a2a7d8265'; - var TXIN_N = 0; - var ADDR = 'muHct3YZ9Nd5Pq7uLYYhXRAxeW4EnpcaLz'; - var VAL = '0.001'; - - var txobj = { - version: 1, - lock_time: 0, - ins: [], - outs: [] - }; - - var txin = { - s: coinUtil.EMPTY_BUFFER, // Add signature - q: 0xffffffff - }; - - var hash = new Buffer(TXIN.split('').reverse(), 'hex'); - var vout = parseInt(TXIN_N); - var voutBuf = new Buffer(4); - - voutBuf.writeUInt32LE(vout, 0); - txin.o = Buffer.concat([hash, voutBuf]); - txobj.ins.push(txin); - - var addr = new Address(ADDR); - var script = Script.createPubKeyHashOut(addr.payload()); - var valueNum = coinUtil.parseValue(VAL); - var value = coinUtil.bigIntToValue(valueNum); - - var txout = { - v: value, - s: script.getBuffer(), - }; - txobj.outs.push(txout); - - return new Transaction(txobj); - +// this can be get from insight.bitcore.io API o blockchain.info +var utxos = { + "unspent": [ + { + "address": "n4g2TFaQo8UgedwpkYdcQFF6xE2Ei9Czvy", + "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", + "scriptPubKey": "76a914fe021bac469a5c49915b2a8ffa7390a9ce5580f988ac", + "vout": 1, + "amount": 1.0101, + "confirmations":7 + }, + { + "address": "mhNCT9TwZAGF1tLPpZdqfkTmtBkY282YDW", + "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc2", + "scriptPubKey": "76a9141448534cb1a1ec44665b0eb2326e570814afe3f188ac", + "vout": 0, + "confirmations": 1, + "amount": 10 + }, }; +//private keys in WIF format (see Transaction.js for other options) +var keys = [ + "cSq7yo4fvsbMyWVN945VUGUWMaSazZPWqBVJZyoGsHmNq6W4HVBV", + "cPa87VgwZfowGZYaEenoQeJgRfKW6PhZ1R65EHTkN1K19cSvc92G", + "cPQ9DSbBRLva9av5nqeF5AGrh3dsdW8p2E5jS4P8bDWZAoQTeeKB" +]; + +function createTx() { + var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; + + var ret = Transaction.createAndSign(utxos, outs, keys); + + / * create and signing can be done in 2 steps using: + * var ret = Transaction.create(utxos,outs); + * and later: + * ret.tx.sign(ret.tx.selectedUtxos, outs, keys); + */ + + return ret.tx.serialize().toString('hex'); +}; + + var peerman = new PeerManager(); peerman.addPeer(new Peer('127.0.0.1', 18333)); diff --git a/Transaction.js b/Transaction.js index f9c8e69..6d28741 100644 --- a/Transaction.js +++ b/Transaction.js @@ -778,10 +778,16 @@ Transaction._sumOutputs = function(outs) { return valueOutSat; } -Transaction.prepare = function (ins, outs, opts) { - opts = opts || {}; +/* + * createWithFee + * Create a TX given ins (selected already), outs, and a FIXED fee + * details on the input on .create + */ + +Transaction.createWithFee = function (ins, outs, feeSat, opts) { + opts = opts || {}; + feeSat = feeSat || 0; - 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; @@ -981,18 +987,74 @@ Transaction.prototype.sign = function (selectedUtxos, keys, opts) { self.ins[i].s = scriptSig.getBuffer(); inputSigned++; } - var complete = inputSigned === l; - return complete; }; +/* + * create + * + * creates a transaction without signing it. + * + * @utxos + * @outs + * @opts + * + * See createAndSign for documentation on the inputs + * + * Returns: + * { tx: {}, selectedUtxos: []} + * see createAndSign for details + * + */ +Transaction.create = function (utxos, outs, opts) { + //starting size estimation + var size = 500; + var opts = opts || {}; + + var givenFeeSat; + if (opts.fee || opts.feeSat) { + givenFeeSat = opts.fee ? opts.fee * util.COIN : opts.feeSat; + } + + var selectedUtxos; + 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); + + 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.createWithFee(selectedUtxos, outs, feeSat, { + remainderAddress: opts.remainderAddress, + lockTime: opts.lockTime, + }); + + size = tx.getSize(); + } while (size > (maxSizeK+1)*1000 ); + + return {tx: tx, selectedUtxos: selectedUtxos}; +}; /* - * create + * createAndSign * * creates and signs a transaction * @@ -1031,66 +1093,33 @@ Transaction.prototype.sign = function (selectedUtxos, keys, opts) { * signhash: SIGHASH_ALL * } * + * + * Retuns: + * { + * tx: The new created transaction, + * selectedUtxos: The UTXOs selected as inputs for this transaction + * } + * * Amounts are in BTC. instead of fee and amount; feeSat and amountSat can be given, * repectively, to provide amounts in satoshis. * - * If no remainderAddress is given, and there is a remainderAddress - * first in address will be used. (TODO: is this is reasonable?) + * If no remainderAddress is given, and there are remainder coins, the + * first IN address will be used to return the coins. (TODO: is this is reasonable?) * - * if not keys are provided, the transaction will no be signed. .sign can be used to - * sign it later. - * - * The Transaction creation is handled in 3 steps: - * .selectUnspent - * .prepare + * The Transaction creation is handled in 2 steps: + * .create + * .selectUnspent + * .createWithFee * .sign * + * If you need just to create a TX and not sign it, use .create + * */ - -Transaction.create = function (utxos, outs, keys, opts) { - - //starting size estimation - var size = 500; - var opts = opts || {}; - - var givenFeeSat; - if (opts.fee || opts.feeSat) { - givenFeeSat = opts.fee ? opts.fee * util.COIN : opts.feeSat; - } - - 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; +Transaction.createAndSign = function (utxos, outs, keys, opts) { + var ret = Transaction.create(utxos, outs, opts); + ret.tx.sign(ret.selectedUtxos, keys); + return ret; }; var TransactionInputsCache = exports.TransactionInputsCache = diff --git a/examples/CreateAndSignTx.js b/examples/CreateAndSignTx.js index aa8a746..0e34252 100644 --- a/examples/CreateAndSignTx.js +++ b/examples/CreateAndSignTx.js @@ -8,195 +8,34 @@ var run = function() { var amt = '0.005'; var toAddress = 'myuAQcCc1REUgXGsCTiYhZvPPc3XxZ36G1'; var changeAddressString = 'moDz3jEo9q7CxjBDjmb13sL4SKkgo2AACE'; - var feeString = '0.0001'; - var safeUnspent = [ - { - address: "mqSjTad2TKbPcKQ3Jq4kgCkKatyN44UMgZ", - hash: "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", - vout: 1, - ts: 1394719301, - scriptPubKey: "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ac", - amount: 0.01, - confirmations: 2 - } - ] - ; + var utxos = [{ + address: "mqSjTad2TKbPcKQ3Jq4kgCkKatyN44UMgZ", + txid: "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", + vout: 1, + ts: 1394719301, + scriptPubKey: "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ac", + amount: 0.01, + confirmations: 2 + }]; console.log('TX Data: BTC:' + amt + ' => '+ toAddress + ', change To:' + changeAddressString ) ; - console.log('Unspends:', safeUnspent); - - var wk = new bitcore.WalletKey({ - network: bitcore.networks.testnet - }); - wk.fromObj({ priv: priv, }); - - var wkObj= wk.storeObj(); - var keyPairs = [{ - key: wkObj.priv, - address: wkObj.addr, - }]; - console.log('KEY DB IS:', keyPairs); - - var Address = bitcore.Address; - var Transaction = bitcore.Transaction; - var Script = bitcore.Script; - var nets = bitcore.networks; - var z = bitcore.bignum(0); - var amt = bitcore.util.parseValue(amt); - - if(z.cmp(amt) === 0 ) - throw "spend amount must be greater than zero"; - - if(!changeAddressString) - throw "change address was not provided"; - - var fee = bitcore.util.parseValue(feeString || '0'); - var total = bitcore.bignum(0).add(amt).add(fee); - var address = new Address(toAddress); - var sendTx = new Transaction(); - var i; - - var unspent = []; - var unspentAmt = bitcore.bignum(0); + console.log('Unspends Outputs:', utxos); - for(i=0;i -1, we have enough to send the requested amount - if(unspentAmt.cmp(total) > -1) { - break; - } - } - - if(unspentAmt.cmp(total) < 0) { - throw "you do not have enough bitcoins to send this amount"; - } - - var txobj = {}; - txobj.version = 1; - txobj.lock_time = 0; - txobj.ins = []; - txobj.outs = []; - - for(i=0;i