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) { +/* + * 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 - } - ] - ; - - 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, + var utxos = [{ + address: "mqSjTad2TKbPcKQ3Jq4kgCkKatyN44UMgZ", + txid: "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", + vout: 1, + ts: 1394719301, + scriptPubKey: "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ac", + amount: 0.01, + confirmations: 2 }]; - 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 '+ toAddress + ', change To:' + changeAddressString ) ; + console.log('Unspends Outputs:', utxos); - console.log('VERIFY: ',wKey.privKey.verifySignatureSync(txSigHash, sigRaw)); //TODO - var sigType = new bitcore.Buffer(1); - sigType[0] = anypay ? Transaction.SIGHASH_ANYONECANPAY : Transaction.SIGHASH_ALL; - var sig = bitcore.Buffer.concat([sigRaw, sigType]); + var outs = [{address:toAddress, amount:amt}]; + var keys = [priv]; - var scriptSig = new Script(); - scriptSig.chunks.push(sig); - scriptSig.chunks.push(wKey.privKey.public); - scriptSig.updateBuffer(); - tx.ins[i].s = scriptSig.getBuffer(); - allFound--; - break; - } - } - } + var ret = bitcore.Transaction.createAndSign(utxos, outs, keys, + {remainderAddress: changeAddressString}); - if (allFound !== 0) { - throw new Error('could not find priv key for some inputs'); - } + /* 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); + */ - var txHex = tx.serialize().toString('hex'); + var txHex = ret.tx.serialize().toString('hex'); console.log('TX HEX IS: ', txHex); }; diff --git a/test/test.Transaction.js b/test/test.Transaction.js index 6de6b26..094dd0b 100644 --- a/test/test.Transaction.js +++ b/test/test.Transaction.js @@ -86,8 +86,13 @@ describe('Transaction', function() { it('#create should be able to create instance', function() { var utxos =testdata.dataUnspent; var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; - var tx = Transaction.create(utxos, outs, null, opts); - should.exist(tx); + + var ret = Transaction.create(utxos, outs, opts); + should.exist(ret.tx); + should.exist(ret.selectedUtxos); + ret.selectedUtxos.length.should.equal(2); + + var tx = ret.tx; tx.version.should.equal(1); tx.ins.length.should.equal(2); @@ -105,11 +110,11 @@ describe('Transaction', function() { var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:80}]; Transaction .create - .bind(utxos, outs, null, opts) + .bind(utxos, outs, opts) .should.throw(); var outs2 = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.5}]; - should.exist( Transaction.create(utxos, outs2, null, opts)); + should.exist( Transaction.create(utxos, outs2, opts)); // do not allow unconfirmed Transaction.create.bind(utxos, outs2).should.throw(); @@ -119,66 +124,96 @@ describe('Transaction', function() { it('#create should create same output as bitcoind createrawtransaction ', function() { var utxos =testdata.dataUnspent; var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; - var tx = Transaction.create(utxos, outs, null, opts); - + var ret = Transaction.create(utxos, outs, opts); + var tx = ret.tx; // 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'); + }); + + it('#create should create same output as bitcoind createrawtransaction wo remainder', function() { + var utxos =testdata.dataUnspent; // no remainder - outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; - tx = Transaction.create(utxos, outs, null, {fee:0.03} ); + var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; + var ret = Transaction.create(utxos, outs, {fee:0.03} ); + var tx = ret.tx; // string output generated from: bitcoind createrawtransaction '[{"txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1","vout":1},{"txid":"2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc2","vout":0} ]' '{"mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE":0.08}' // tx.serialize().toString('hex').should.equal('0100000002c1cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0100000000ffffffffc2cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0000000000ffffffff0100127a00000000001976a914774e603bafb717bd3f070e68bbcccfd907c77d1388ac00000000'); }); - it('#sign should sign a tx', function() { + it('#createAndSign should sign a tx', function() { var utxos =testdata.dataUnspentSign.unspent; var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; - var tx = Transaction.create(utxos, outs, testdata.dataUnspentSign.keyStrings, opts); + var ret = Transaction.createAndSign(utxos, outs, testdata.dataUnspentSign.keyStrings, opts); + var tx = ret.tx; 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); + var ret2 = Transaction.createAndSign(utxos, outs2, testdata.dataUnspentSign.keyStrings, opts); + var tx2 = ret2.tx; 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() { + it('#createAndSign should sign an incomplete tx ', function() { var keys = ['cNpW8B7XPAzCdRR9RBWxZeveSNy3meXgHD8GuhcqUyDuy8ptCDzJ']; var utxos =testdata.dataUnspentSign.unspent; var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; - var tx = Transaction.create(utxos, outs, keys, opts); + var ret = Transaction.createAndSign(utxos, outs, keys, opts); + var tx = ret.tx; 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() { - var utxos = Transaction.selectUnspent(testdata.dataUnspentSign.unspent,13, true); + it('#isComplete should return TX signature status', function() { + var keys = ['cNpW8B7XPAzCdRR9RBWxZeveSNy3meXgHD8GuhcqUyDuy8ptCDzJ']; + var utxos =testdata.dataUnspentSign.unspent; var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}]; + var ret = Transaction.createAndSign(utxos, outs, keys, opts); + var tx = ret.tx; + tx.isComplete().should.equal(false); + tx.sign(ret.selectedUtxos, testdata.dataUnspentSign.keyStrings); + tx.isComplete().should.equal(true); + }); + + it('#sign should sign a tx in multiple steps (case1)', function() { + var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:1.08}]; + var ret = Transaction.create(testdata.dataUnspentSign.unspent, outs, opts); + var tx = ret.tx; + var selectedUtxos = ret.selectedUtxos; - var tx = Transaction.prepare(utxos, outs, opts); var k1 = testdata.dataUnspentSign.keyStrings.slice(0,1); + + tx.isComplete().should.equal(false); + + tx.sign(selectedUtxos, k1).should.equal(false); + var k23 = testdata.dataUnspentSign.keyStrings.slice(1,3); - tx.sign(utxos, k1).should.equal(false); - tx.sign(utxos, k23).should.equal(true); + tx.sign(selectedUtxos, k23).should.equal(true); + tx.isComplete().should.equal(true); + }); + + it('#sign should sign a tx in multiple steps (case2)', function() { + var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:16}]; + var ret = Transaction.create(testdata.dataUnspentSign.unspent, outs, opts); + var tx = ret.tx; + var selectedUtxos = ret.selectedUtxos; - var tx2 = Transaction.prepare(utxos, outs, opts); var k1 = testdata.dataUnspentSign.keyStrings.slice(0,1); var k2 = testdata.dataUnspentSign.keyStrings.slice(1,2); var k3 = testdata.dataUnspentSign.keyStrings.slice(2,3); - tx2.sign(utxos, k1).should.equal(false); - tx2.sign(utxos, k2).should.equal(false); - tx2.sign(utxos, k3).should.equal(true); + tx.sign(selectedUtxos, k1).should.equal(false); + tx.sign(selectedUtxos, k2).should.equal(false); + tx.sign(selectedUtxos, k3).should.equal(true); }); - it('#create: should generate dynamic fee and readjust (and not) the selected UTXOs', function() { + it('#createAndSign: 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; @@ -188,7 +223,8 @@ describe('Transaction', function() { outs.push({address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.01}); } - var tx = Transaction.create(utxos, outs, testdata.dataUnspentSign.keyStrings, opts); + var ret = Transaction.createAndSign(utxos, outs, testdata.dataUnspentSign.keyStrings, opts); + var tx = ret.tx; tx.getSize().should.equal(3560); // ins = 11.0101 BTC (2 inputs: 1.0101 + 10 ); @@ -209,7 +245,8 @@ describe('Transaction', function() { outs.push({address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.01}); } - var tx = Transaction.create(utxos, outs, testdata.dataUnspentSign.keyStrings, opts); + var ret = Transaction.createAndSign(utxos, outs, testdata.dataUnspentSign.keyStrings, opts); + var tx = ret.tx; tx.getSize().should.equal(3485); // ins = 1.0101 BTC (1 inputs: 1.0101);