diff --git a/.gitignore b/.gitignore index 44f3737..4c15f13 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules/ *~ .project README.html +tags 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/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/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 37d2ad8..6d28741 100644 --- a/Transaction.js +++ b/Transaction.js @@ -11,8 +11,12 @@ 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 WalletKey = imports.WalletKey || require('./WalletKey'); +var PrivateKey = imports.PrivateKey || require('./PrivateKey'); var COINBASE_OP = Buffer.concat([util.NULL_HASH, new Buffer('FFFFFFFF', 'hex')]); +var FEE_PER_1000B_SAT = parseInt(0.0001 * util.COIN); function TransactionIn(data) { if ("object" !== typeof data) { @@ -679,6 +683,445 @@ Transaction.prototype.parse = function (parser) { this.calcHash(); }; + + +/* + * selectUnspent + * + * Selects some unspent outputs for later usage in tx inputs + * + * @utxos + * @totalNeededAmount: output transaction amount in BTC, including fee + * @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 (utxos, totalNeededAmount, allowUnconfirmed) { + + var minConfirmationSteps = [6,1]; + if (allowUnconfirmed) minConfirmationSteps.push(0); + + var ret = []; + var l = utxos.length; + var totalSat = bignum(0); + var totalNeededAmountSat = util.parseValue(totalNeededAmount); + var fulfill = false; + var maxConfirmations = null; + + 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); + + //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; +} + +/* + * _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; +}; + +Transaction._sumOutputs = function(outs) { + var valueOutSat = bignum(0); + var l = outs.length; + + 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); + } + + + 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; + + var ret = true; + for (var i=0; i (maxSizeK+1)*1000 ); + + return {tx: tx, selectedUtxos: selectedUtxos}; +}; + + +/* + * createAndSign + * + * creates and signs a transaction + * + * @utxos + * unspent outputs array (UTXO), using the following format: + * [{ + * 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. + * + + * @outs + * an array of [{ + * address: xx, + * amount:0.001 + * },...] + * + * @keys + * an array of strings representing private keys to sign the + * transaction in WIF private key format OR WalletKey objects + * + * @opts + * { + * remainderAddress: null, + * fee: 0.001, + * lockTime: null, + * allowUnconfirmed: false, + * 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 are remainder coins, the + * first IN address will be used to return the coins. (TODO: is this is reasonable?) + * + * 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.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 = function TransactionInputsCache(tx) { 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/examples/CreateKey.js b/examples/CreateKey.js new file mode 100644 index 0000000..8b162cf --- /dev/null +++ b/examples/CreateKey.js @@ -0,0 +1,44 @@ +'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) { + + 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\t * WalletKey Store Object'); + console.log ('\tPrivate: ' + wkObj.priv); + console.log ('\tPublic : ' + wkObj.pub); + console.log ('\tAddr : ' + wkObj.addr); + }; + + //Generate a new one (compressed public key, compressed WIF flag) + var wk = new WalletKey(opts); + wk.generate(); + print(wk); + + //Generate from private Key WIF. Compressed status taken from WIF. + var wk2 = new WalletKey(opts); + wk2.fromObj({priv:'cS62Ej4SobZnpFQYN1PEEBr2KWf5sgRYYnELtumcG6WVCfxno39V'}); + print(wk2); + + +}; + +module.exports.run = run; +if (require.main === module) { + run(); +} 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