diff --git a/Gruntfile.js b/Gruntfile.js index f6c0b2fe7..f7d6193ef 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -50,6 +50,6 @@ module.exports = function(grunt) { }); - grunt.registerTask('default', ['watch']); + grunt.registerTask('default', ['shell','watch']); }; diff --git a/README.md b/README.md index f686ce08a..f17f1d0e3 100644 --- a/README.md +++ b/README.md @@ -124,19 +124,23 @@ rpc.getBlock(hash, function(err, ret) { Check the list of all supported RPC call at [RpcClient.js](RpcClient.js) ## Creating and sending a Transaction through P2P -For this example you need a running bitcoind instance with RPC enabled. + +The fee of the transaction can be given in `opts` or it will be determined +by the transaction size. Documentation on the paramets of `TransactionBuilder` +can be found on the source file. + ```js var bitcore = require('bitcore'); var networks = bitcore.networks; var Peer = bitcore.Peer; -var Transaction = bitcore.Transaction; +var TransactionBuilder = bitcore.TransactionBuilder; var PeerManager = require('soop').load('../PeerManager', { network: networks.testnet }); // this can be get from insight.bitcore.io API o blockchain.info -var utxos = { - "unspent": [ + +var unspent = [ { "address": "n4g2TFaQo8UgedwpkYdcQFF6xE2Ei9Czvy", "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", @@ -153,29 +157,15 @@ var utxos = { "confirmations": 1, "amount": 10 }, -}; +]; -//private keys in WIF format (see Transaction.js for other options) +//private keys in WIF format (see TransactionBuilder.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)); @@ -183,7 +173,36 @@ peerman.addPeer(new Peer('127.0.0.1', 18333)); peerman.on('connect', function() { var conn = peerman.getActiveConnection(); if (conn) { - conn.sendTx(createTx()); + var outs = [{address:toAddress, amount:amt}]; + var opts = {remainderAddress: changeAddressString}; + var Builder = bitcore.TransactionBuilder; + + var tx = new Builder(opts) + .setUnspent(Unspent) + .setOutputs(outs) + .sign(keys) + .build(); + + /* create and signing can be done in multiple steps using: + * + * var builder = new bitcore.TransactionBuilder(opts) + * .setUnspent(utxos) + * .setOutputs(outs); + * //later + * builder.sign(key1); + * // get partially signed tx + * var tx = builder.build(); + * + * //later + * builder.sign(key2); + * if (builder.isFullySigned()){ + * var tx = builder.build(); + * } + * + * The selected Unspent Outputs for the transaction can be retrieved with: + * var selectedUnspent = build.getSelectedUnspent(); + */ + conn.sendTx(tx.serialize().toString('hex')); } conn.on('reject', function() { console.log('Transaction Rejected'); diff --git a/Transaction.js b/Transaction.js index ac82be1f9..8d5b64280 100644 --- a/Transaction.js +++ b/Transaction.js @@ -302,9 +302,6 @@ Transaction.prototype.hashForSignature = "(" + this.ins.length + " inputs)"); } - // Clone transaction - var txTmp = new Transaction(this); - // In case concatenating two scripts ends up with two codeseparators, // or an extra one at the end, this prevents all those possible // incompatibilities. @@ -355,7 +352,7 @@ Transaction.prototype.hashForSignature = } else { var outsLen; if (hashTypeMode === SIGHASH_SINGLE) { - if (inIndex >= txTmp.outs.length) { + if (inIndex >= this.outs.length) { // bug present in bitcoind which must be also present in bitcore // see https://bitcointalk.org/index.php?topic=260595 // Transaction.hashForSignature(): SIGHASH_SINGLE @@ -530,173 +527,6 @@ Transaction.prototype.parse = function(parser) { -/* - * 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 < l; i++) { - var u = utxos[i]; - - var c = u.confirmations || 0; - - if (c < minConfirmations || (maxConfirmations && c >= 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; i < outs.length; i++) { - var sat = outs[i].amountSat || util.parseValue(outs[i].amount); - valueOutSat = valueOutSat.add(sat); - } - return valueOutSat; -} - -/* - * 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 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; i < l; i++) { - valueInSat = valueInSat.add(util.parseValue(ins[i].amount)); - - var txin = {}; - txin.s = util.EMPTY_BUFFER; - txin.q = 0xffffffff; - - var hash = new Buffer(ins[i].txid, 'hex'); - var hashReversed = buffertools.reverse(hash); - - var vout = parseInt(ins[i].vout); - var voutBuf = new Buffer(4); - voutBuf.writeUInt32LE(vout, 0); - - txin.o = Buffer.concat([hashReversed, voutBuf]); - txobj.ins.push(txin); - } - - var valueOutSat = Transaction._sumOutputs(outs); - valueOutSat = valueOutSat.add(feeSat); - - if (valueInSat.cmp(valueOutSat) < 0) { - var inv = valueInSat.toString(); - var ouv = valueOutSat.toString(); - throw new Error('transaction input amount is less than outputs: ' + - inv + ' < ' + ouv + ' [SAT]'); - } - - for (var i = 0; i < outs.length; i++) { - var amountSat = outs[i].amountSat || util.parseValue(outs[i].amount); - var value = util.bigIntToValue(amountSat); - var script = Transaction._scriptForAddress(outs[i].address); - var txout = { - v: value, - s: script.getBuffer(), - }; - txobj.outs.push(txout); - } - - // add remainder (without modifiying outs[]) - var remainderSat = valueInSat.sub(valueOutSat); - if (remainderSat.cmp(0) > 0) { - 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 @@ -722,7 +552,6 @@ Transaction.prototype.getSize = function getHash() { return this.size; }; - Transaction.prototype.isComplete = function() { var l = this.ins.length; @@ -737,235 +566,4 @@ Transaction.prototype.isComplete = function() { }; -/* - * sign - * - * signs the transaction - * - * @ utxos - * @keypairs - * @opts - * signhash: Transaction.SIGHASH_ALL - * - * Return the 'completeness' status of the tx (i.e, if all inputs are signed). - * - */ - -Transaction.prototype.sign = function(selectedUtxos, keys, opts) { - var self = this; - var complete = false; - var m = keys.length; - opts = opts || {}; - var signhash = opts.signhash || SIGHASH_ALL; - - if (selectedUtxos.length !== self.ins.length) - throw new Error('given selectedUtxos do not match tx inputs'); - - var inputMap = []; - var l = selectedUtxos.length; - for (var i = 0; i < l; i++) { - inputMap[i] = { - address: selectedUtxos[i].address, - scriptPubKey: selectedUtxos[i].scriptPubKey - }; - } - - //prepare keys - var walletKeyMap = {}; - var l = keys.length; - var wk; - for (var i = 0; i < l; i++) { - var k = keys[i]; - - if (typeof k === 'string') { - var pk = new PrivateKey(k); - wk = new WalletKey({ - network: pk.network() - }); - wk.fromObj({ - priv: k - }); - } else if (k instanceof WalletKey) { - wk = k; - } else { - throw new Error('argument must be an array of strings (WIF format) or WalletKey objects'); - } - walletKeyMap[wk.storeObj().addr] = wk; - } - - var inputSigned = 0; - l = self.ins.length; - for (var i = 0; i < l; i++) { - var aIn = self.ins[i]; - var wk = walletKeyMap[inputMap[i].address]; - - if (typeof wk === 'undefined') { - if (buffertools.compare(aIn.s, util.EMPTY_BUFFER) !== 0) - inputSigned++; - continue; - } - var scriptBuf = new Buffer(inputMap[i].scriptPubKey, 'hex'); - var s = new Script(scriptBuf); - if (s.classify() !== Script.TX_PUBKEYHASH) { - throw new Error('input:' + i + ' script type:' + s.getRawOutType() + ' not supported yet'); - } - - 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 && triesLeft--); - - if (!triesLeft) { - log.debug('could not sign input:' + i + ' verification failed'); - continue; - } - - var sigType = new Buffer(1); - sigType[0] = signhash; - var sig = Buffer.concat([sigRaw, sigType]); - - var scriptSig = new Script(); - scriptSig.chunks.push(sig); - scriptSig.chunks.push(wk.privKey.public); - scriptSig.updateBuffer(); - 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 - }; -}; - - -/* - * 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; -}; - module.exports = require('soop')(Transaction); diff --git a/TransactionBuilder.js b/TransactionBuilder.js new file mode 100644 index 000000000..083decb49 --- /dev/null +++ b/TransactionBuilder.js @@ -0,0 +1,438 @@ + +/* + var tx = (new TransactionBuilder(opts)) + .setUnspent(utxos) + .setOutputs(outs) + .sign(keys) + .build(); + + + var builder = (new TransactionBuilder(opts)) + .setUnspent(spent) + .setOutputs(outs); + + // Uncomplete tx (no signed or partially signed) + var tx = builder.build(); + + ..later.. + + builder.sign(keys); + while ( builder.isFullySigned() ) { + + ... get new keys ... + + builder.sign(keys); + } + + var tx = builder.build(); + broadcast(tx.serialize()); + + To get selected unspent outputs: + var selectedUnspent = builder.getSelectedUnspent(); + + + @unspent + * 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, + * spendUnconfirmed: false, + * signhash: SIGHASH_ALL + * } + * 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?) + * + */ + + +'use strict'; + +var imports = require('soop').imports(); +var Address = imports.Address || require('./Address'); +var Script = imports.Script || require('./Script'); +var util = imports.util || require('./util/util'); +var bignum = imports.bignum || require('bignum'); +var buffertools = imports.buffertools || require('buffertools'); +var networks = imports.networks || require('./networks'); +var WalletKey = imports.WalletKey || require('./WalletKey'); +var PrivateKey = imports.PrivateKey || require('./PrivateKey'); + +var Transaction = imports.Transaction || require('./Transaction'); +var FEE_PER_1000B_SAT = parseInt(0.0001 * util.COIN); + +function TransactionBuilder(opts) { + var opts = opts || {}; + this.txobj = {}; + this.txobj.version = 1; + this.txobj.lock_time = opts.lockTime || 0; + this.txobj.ins = []; + this.txobj.outs = []; + + this.spendUnconfirmed = opts.spendUnconfirmed || false; + + if (opts.fee || opts.feeSat) { + this.givenFeeSat = opts.fee ? opts.fee * util.COIN : opts.feeSat; + } + this.remainderAddress = opts.remainderAddress; + this.signhash = opts.signhash || Transaction.SIGHASH_ALL; + + this.tx = {}; + this.inputsSigned= 0; + + return this; +} + +/* + * _scriptForAddress + * + * Returns a scriptPubKey for the given address type + */ + +TransactionBuilder._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; +}; + +TransactionBuilder.prototype.setUnspent = function(utxos) { + this.utxos = utxos; + return this; +}; + +TransactionBuilder.prototype._setInputMap = function() { + var inputMap = []; + + var l = this.selectedUtxos.length; + for (var i = 0; i < l; i++) { + var s = this.selectedUtxos[i]; + + inputMap.push({ + address: s.address, + scriptPubKey: s.scriptPubKey + }); + } + this.inputMap = inputMap; + return this; +}; + +TransactionBuilder.prototype.getSelectedUnspent = function(neededAmountSat) { + return this.selectedUtxos; +}; + +/* _selectUnspent + * TODO(?): sort sel (at the end) 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. + */ + +TransactionBuilder.prototype._selectUnspent = function(neededAmountSat) { + + if (!this.utxos || !this.utxos.length) + throw new Error('unspent not set'); + + var minConfirmationSteps = [6, 1]; + if (this.spendUnconfirmed) minConfirmationSteps.push(0); + + var sel = [], + totalSat = bignum(0), + fulfill = false, + maxConfirmations = null, + l = this.utxos.length; + + do { + var minConfirmations = minConfirmationSteps.shift(); + for (var i = 0; i < l; i++) { + var u = this.utxos[i]; + var c = u.confirmations || 0; + + if (c < minConfirmations || (maxConfirmations && c >= maxConfirmations)) + continue; + + var sat = u.amountSat || util.parseValue(u.amount); + totalSat = totalSat.add(sat); + sel.push(u); + if (totalSat.cmp(neededAmountSat) >= 0) { + fulfill = true; + break; + } + } + maxConfirmations = minConfirmations; + } while (!fulfill && minConfirmationSteps.length); + + if (!fulfill) + throw new Error('no enough unspent to fulfill totalNeededAmount'); + + this.selectedUtxos = sel; + this._setInputMap(); + return this; +}; + +TransactionBuilder.prototype._setInputs = function() { + var ins = this.selectedUtxos; + var l = ins.length; + var valueInSat = bignum(0); + + this.txobj.ins=[]; + for (var i = 0; i < l; i++) { + valueInSat = valueInSat.add(util.parseValue(ins[i].amount)); + + var txin = {}; + txin.s = util.EMPTY_BUFFER; + txin.q = 0xffffffff; + + var hash = new Buffer(ins[i].txid, 'hex'); + var hashReversed = buffertools.reverse(hash); + + var vout = parseInt(ins[i].vout); + var voutBuf = new Buffer(4); + voutBuf.writeUInt32LE(vout, 0); + + txin.o = Buffer.concat([hashReversed, voutBuf]); + this.txobj.ins.push(txin); + } + this.valueInSat = valueInSat; + return this; +}; + +TransactionBuilder.prototype._setFee = function(feeSat) { + if ( typeof this.valueOutSat === 'undefined') + throw new Error('valueOutSat undefined'); + + + var valueOutSat = this.valueOutSat.add(feeSat); + + if (this.valueInSat.cmp(valueOutSat) < 0) { + var inv = this.valueInSat.toString(); + var ouv = valueOutSat.toString(); + throw new Error('transaction input amount is less than outputs: ' + + inv + ' < ' + ouv + ' [SAT]'); + } + this.feeSat = feeSat; + return this; +}; + +TransactionBuilder.prototype._setRemainder = function(remainderIndex) { + + if ( typeof this.valueInSat === 'undefined' || + typeof this.valueOutSat === 'undefined') + throw new Error('valueInSat / valueOutSat undefined'); + + // add remainder (without modifying outs[]) + var remainderSat = this.valueInSat.sub(this.valueOutSat).sub(this.feeSat); + var l =this.txobj.outs.length; + this.remainderSat = bignum(0); + + //remove old remainder? + if (l > remainderIndex) { + this.txobj.outs.pop(); + } + + if (remainderSat.cmp(0) > 0) { + var remainderAddress = this.remainderAddress || this.selectedUtxos[0].address; + var value = util.bigIntToValue(remainderSat); + var script = TransactionBuilder._scriptForAddress(remainderAddress); + var txout = { + v: value, + s: script.getBuffer(), + }; + this.txobj.outs.push(txout); + this.remainderSat = remainderSat; + } + + return this; +}; + +TransactionBuilder.prototype._setFeeAndRemainder = function() { + + //starting size estimation + var size = 500, maxSizeK, remainderIndex = this.txobj.outs.length; + + do { + // based on https://en.bitcoin.it/wiki/Transaction_fees + maxSizeK = parseInt(size / 1000) + 1; + + var feeSat = this.givenFeeSat ? + this.givenFeeSat : maxSizeK * FEE_PER_1000B_SAT; + + var neededAmountSat = this.valueOutSat.add(feeSat); + + this._selectUnspent(neededAmountSat) + ._setInputs() + ._setFee(feeSat) + ._setRemainder(remainderIndex); + + + size = new Transaction(this.txobj).getSize(); + } while (size > (maxSizeK + 1) * 1000); + return this; +}; + +TransactionBuilder.prototype.setOutputs = function(outs) { + var valueOutSat = bignum(0); + + this.txobj.outs = []; + var l =outs.length; + + for (var i = 0; i < l; i++) { + var amountSat = outs[i].amountSat || util.parseValue(outs[i].amount); + var value = util.bigIntToValue(amountSat); + var script = TransactionBuilder._scriptForAddress(outs[i].address); + var txout = { + v: value, + s: script.getBuffer(), + }; + this.txobj.outs.push(txout); + + var sat = outs[i].amountSat || util.parseValue(outs[i].amount); + valueOutSat = valueOutSat.add(sat); + } + + this.valueOutSat = valueOutSat; + + this._setFeeAndRemainder(); + + this.tx = new Transaction(this.txobj); + return this; +}; + +TransactionBuilder._mapKeys = function(keys) { + + //prepare keys + var walletKeyMap = {}; + var l = keys.length; + var wk; + for (var i = 0; i < l; i++) { + var k = keys[i]; + + if (typeof k === 'string') { + var pk = new PrivateKey(k); + wk = new WalletKey({ network: pk.network() }); + wk.fromObj({ priv: k }); + } + else if (k instanceof WalletKey) { + wk = k; + } + else { + throw new Error('argument must be an array of strings (WIF format) or WalletKey objects'); + } + walletKeyMap[wk.storeObj().addr] = wk; + } + return walletKeyMap; +}; + +TransactionBuilder._checkSupportedScriptType = function (s) { + if (s.classify() !== Script.TX_PUBKEYHASH) { + throw new Error('scriptSig type:' + s.getRawOutType() + + ' not supported yet'); + } +}; + + +TransactionBuilder._signHashAndVerify = function(wk, txSigHash) { + var triesLeft = 10, sigRaw; + + do { + sigRaw = wk.privKey.signSync(txSigHash); + } while (wk.privKey.verifySignatureSync(txSigHash, sigRaw) === false && + triesLeft--); + + if (triesLeft<0) + throw new Error('could not sign input: verification failed'); + + return sigRaw; +}; + +TransactionBuilder.prototype._checkTx = function() { + if (! this.tx || !this.tx.ins.length || !this.tx.outs.length) + throw new Error('tx is not defined'); +}; + + +TransactionBuilder.prototype.sign = function(keys) { + this._checkTx(); + + var tx = this.tx, + ins = tx.ins, + l = ins.length; + + var walletKeyMap = TransactionBuilder._mapKeys(keys); + + for (var i = 0; i < l; i++) { + var im = this.inputMap[i]; + if (typeof im === 'undefined') continue; + var wk = walletKeyMap[im.address]; + if (!wk) continue; + + var scriptBuf = new Buffer(im.scriptPubKey, 'hex'); + +//TODO: support p2sh + var s = new Script(scriptBuf); + TransactionBuilder._checkSupportedScriptType(s); + + var txSigHash = this.tx.hashForSignature(s, i, this.signhash); + var sigRaw = TransactionBuilder._signHashAndVerify(wk, txSigHash); + var sigType = new Buffer(1); + sigType[0] = this.signhash; + var sig = Buffer.concat([sigRaw, sigType]); + + var scriptSig = new Script(); + scriptSig.chunks.push(sig); + scriptSig.chunks.push(wk.privKey.public); + scriptSig.updateBuffer(); + tx.ins[i].s = scriptSig.getBuffer(); + this.inputsSigned++; + } + return this; +}; + +TransactionBuilder.prototype.isFullySigned = function() { + return this.inputsSigned === this.tx.ins.length; +}; + +TransactionBuilder.prototype.build = function() { + this._checkTx(); + return this.tx; +}; + +module.exports = require('soop')(TransactionBuilder); + diff --git a/bitcore.js b/bitcore.js index bcd0ea78c..93e1a8dd4 100644 --- a/bitcore.js +++ b/bitcore.js @@ -29,6 +29,7 @@ requireWhenAccessed('Point', './Point'); requireWhenAccessed('Opcode', './Opcode'); requireWhenAccessed('Script', './Script'); requireWhenAccessed('Transaction', './Transaction'); +requireWhenAccessed('TransactionBuilder', './TransactionBuilder'); requireWhenAccessed('Connection', './Connection'); requireWhenAccessed('Peer', './Peer'); requireWhenAccessed('Block', './Block'); diff --git a/browser/build.js b/browser/build.js index 545219f39..43ae3505a 100644 --- a/browser/build.js +++ b/browser/build.js @@ -45,6 +45,7 @@ var modules = [ 'ScriptInterpreter', 'Sign', 'Transaction', + 'TransactionBuilder', 'Wallet', 'WalletKey', 'config', diff --git a/examples/CreateAndSignTx.js b/examples/CreateAndSignTx.js index 0e3425268..1c2273e6c 100644 --- a/examples/CreateAndSignTx.js +++ b/examples/CreateAndSignTx.js @@ -25,23 +25,44 @@ var run = function() { var outs = [{address:toAddress, amount:amt}]; var keys = [priv]; + var opts = {remainderAddress: changeAddressString}; + var Builder = bitcore.TransactionBuilder; - var ret = bitcore.Transaction.createAndSign(utxos, outs, keys, - {remainderAddress: changeAddressString}); + var tx = new Builder(opts) + .setUnspent(utxos) + .setOutputs(outs) + .sign(keys) + .build(); - /* 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); + /* create and signing can be done in multiple steps using: + * + * var builder = new bitcore.TransactionBuilder(opts) + * .setUnspent(utxos) + * .setOutputs(outs); + * + * builder.sign(key1); + * builder.sign(key2); + * ... + * if (builder.isFullySigned()){ + * var tx = builder.build(); + * } + * + * The selected Unspent Outputs for the transaction can be retrieved with: + * + * var selectedUnspent = build.getSelectedUnspent(); */ - var txHex = ret.tx.serialize().toString('hex'); + var txHex = tx.serialize().toString('hex'); console.log('TX HEX IS: ', txHex); }; - -module.exports.run = run; -if (require.main === module) { +// This is just for browser & mocha compatibility +if (typeof module !== 'undefined') { + module.exports.run = run; + if (require.main === module) { + run(); + } +} else { run(); } diff --git a/examples/CreateScript.js b/examples/CreateScript.js new file mode 100644 index 000000000..f2ac9aee9 --- /dev/null +++ b/examples/CreateScript.js @@ -0,0 +1,74 @@ +'use strict'; + +var run = function() { + // replace '../bitcore' with 'bitcore' if you use this code elsewhere. + var bitcore = require('../bitcore'); + var networks = require('../networks'); + var Script = bitcore.Script; + var WalletKey = bitcore.WalletKey; + var buffertools = bitcore.buffertools; + var Address = bitcore.Address; + var util = bitcore.util; + var opts = {network: networks.livenet}; + + var p = console.log; + + var wk = new WalletKey(opts); + wk.generate(); + var wkObj = wk.storeObj(); + + var s = Script.createPubKeyOut(wk.privKey.public); + p('\nScript PubKey:'); + p('\tHex : ' + buffertools.toHex(s.buffer)); + p('\tHuman : ' + s.toHumanReadable()); + p('\tKey -------------------------------'); + console.log ('\tPrivate: ' + wkObj.priv); + console.log ('\tPublic : ' + wkObj.pub); + console.log ('\tAddr : ' + wkObj.addr); + + s = Script.createPubKeyHashOut(wk.privKey.public); + p('\nScript PubKeyHash:'); + p('\tHex : ' + buffertools.toHex(s.buffer)); + p('\tHuman : ' + s.toHumanReadable()); + p('\tKey -------------------------------'); + console.log ('\tPrivate: ' + wkObj.priv); + console.log ('\tPublic : ' + wkObj.pub); + console.log ('\tAddr : ' + wkObj.addr); + + var wks=[]; + var pubs = []; + for (var i =0; i<5; i++) { + wks[i] = new WalletKey(opts); + wks[i].generate(); + pubs.push(wks[i].privKey.public); + } + + s = Script.createMultisig(3,pubs); + p('\nScript MultiSig (3 out of 5 required signatures):'); + p('\tHex : ' + buffertools.toHex(s.buffer)); + p('\tHuman : ' + s.toHumanReadable()); + + for (i =0; i<5; i++) { + wkObj = wks[i].storeObj(); + p('\tKey ['+i+'] -------------------------------'); + console.log ('\tPrivate: ' + wkObj.priv); + console.log ('\tPublic : ' + wkObj.pub); + console.log ('\tAddr : ' + wkObj.addr); + } + + var hash = util.sha256ripe160(s.buffer); + + s = Script.createP2SH(hash); + p('\nScript P2SH:'); + p('\tHex : ' + buffertools.toHex(s.buffer)); + p('\tHuman : ' + s.toHumanReadable()); + p('\tScript Hash: ' + buffertools.toHex(hash)); + var a = new Address(networks.livenet.addressScript,hash); + p('\tp2sh Addr: ' + a.toString()); + +}; + +module.exports.run = run; +if (require.main === module) { + run(); +} diff --git a/examples/example.html b/examples/example.html index 3fdd08932..74a1f641f 100644 --- a/examples/example.html +++ b/examples/example.html @@ -14,9 +14,9 @@ - diff --git a/test/index.html b/test/index.html index 668712ab2..f0ecc27ab 100644 --- a/test/index.html +++ b/test/index.html @@ -36,6 +36,7 @@ + diff --git a/test/test.Transaction.js b/test/test.Transaction.js index b7f611caf..8e37f5381 100644 --- a/test/test.Transaction.js +++ b/test/test.Transaction.js @@ -62,278 +62,6 @@ describe('Transaction', function() { should.exist(t); }); - - it('#selectUnspent should be able to select utxos', function() { - var u = Transaction.selectUnspent(testdata.dataUnspent, 1.0, true); - u.length.should.equal(3); - - 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); - should.not.exist(u); - }); - - - it('#selectUnspent should check confirmations', function() { - var u = Transaction.selectUnspent(testdata.dataUnspent, 0.9); - should.not.exist(u); - u = Transaction.selectUnspent(testdata.dataUnspent, 0.9, true); - u.length.should.equal(3); - - u = Transaction.selectUnspent(testdata.dataUnspent, 0.11); - u.length.should.equal(2); - u = Transaction.selectUnspent(testdata.dataUnspent, 0.111); - should.not.exist(u); - }); - - - var opts = { - remainderAddress: 'mwZabyZXg8JzUtFX1pkGygsMJjnuqiNhgd', - allowUnconfirmed: true, - }; - - it('#create should be able to create instance', function() { - var utxos = testdata.dataUnspent; - var outs = [{ - address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', - amount: 0.08 - }]; - - 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); - 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(2990000).should.equal(0); - tx.isComplete().should.equal(false); - }); - - it('#create should fail if not enough inputs ', function() { - var utxos = testdata.dataUnspent; - var outs = [{ - address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', - amount: 80 - }]; - Transaction - .create - .bind(utxos, outs, opts) - .should. - throw (); - - var outs2 = [{ - address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', - amount: 0.5 - }]; - should.exist(Transaction.create(utxos, outs2, opts)); - - // do not allow unconfirmed - Transaction.create.bind(utxos, outs2).should. - throw (); - }); - - - it('#create should create same output as bitcoind createrawtransaction ', function() { - var utxos = testdata.dataUnspent; - var outs = [{ - address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', - amount: 0.08 - }]; - 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 - 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('#createAndSign should sign a tx', function() { - var utxos = testdata.dataUnspentSign.unspent; - var outs = [{ - address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', - amount: 0.08 - }]; - 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 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('#createAndSign should sign an incomplete tx ', 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.ins.length.should.equal(1); - tx.outs.length.should.equal(2); - }); - 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 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(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 k1 = testdata.dataUnspentSign.keyStrings.slice(0, 1); - var k2 = testdata.dataUnspentSign.keyStrings.slice(1, 2); - var k3 = testdata.dataUnspentSign.keyStrings.slice(2, 3); - tx.sign(selectedUtxos, k1).should.equal(false); - tx.sign(selectedUtxos, k2).should.equal(false); - tx.sign(selectedUtxos, k3).should.equal(true); - - }); - - 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; - var outs = []; - var n = 101; - for (var i = 0; i < n; i++) { - outs.push({ - address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', - amount: 0.01 - }); - } - - 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 ); - tx.ins.length.should.equal(2); - // outs = 101 outs: - // 101 * 0.01 = 1.01BTC; + 0.0004 fee = 1.0104btc - // remainder = 11.0101-1.0104 = 9.9997 - tx.outs.length.should.equal(102); - util.valueToBigInt(tx.outs[n].v).cmp(999970000).should.equal(0); - tx.isComplete().should.equal(true); - - - //this is the complementary case, it does not trigger a new utxo - utxos = testdata.dataUnspentSign.unspent; - outs = []; - n = 100; - for (i = 0; i < n; i++) { - outs.push({ - address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', - amount: 0.01 - }); - } - - ret = Transaction.createAndSign(utxos, outs, testdata.dataUnspentSign.keyStrings, opts); - tx = ret.tx; - tx.getSize().should.equal(3485); - - // ins = 1.0101 BTC (1 inputs: 1.0101); - tx.ins.length.should.equal(1); - // outs = 100 outs: - // 100 * 0.01 = 1BTC; + 0.0004 fee = 1.0004btc - // remainder = 1.0101-1.0004 = 0.0097 - tx.outs.length.should.equal(101); - util.valueToBigInt(tx.outs[n].v).cmp(970000).should.equal(0); - tx.isComplete().should.equal(true); - }); - - - - /* * Bitcoin core transaction tests */ diff --git a/test/test.TransactionBuilder.js b/test/test.TransactionBuilder.js new file mode 100644 index 000000000..dd5a4721a --- /dev/null +++ b/test/test.TransactionBuilder.js @@ -0,0 +1,398 @@ +'use strict'; + +var chai = chai || require('chai'); +chai.Assertion.includeStack = true; +var bitcore = bitcore || require('../bitcore'); + +var should = chai.should(); + +var Transaction = bitcore.Transaction; +var TransactionBuilder = bitcore.TransactionBuilder; +var In; +var Out; +var Script = bitcore.Script; +var util = bitcore.util; +var buffertools = require('buffertools'); +var testdata = testdata || require('./testdata'); + +describe('TransactionBuilder', function() { + it('should initialze the main object', function() { + should.exist(TransactionBuilder); + }); + + + it('should be able to create instance', function() { + var t = new TransactionBuilder(); + should.exist(t); + }); + + it('should be able to create instance with params', function() { + var t = new TransactionBuilder({spendUnconfirmed: true, lockTime: 10}); + should.exist(t); + should.exist(t.txobj.version); + t.spendUnconfirmed.should.equal(true); + t.txobj.lock_time.should.equal(10); + }); + + + var getBuilder = function (spendUnconfirmed) { + var t = new TransactionBuilder({spendUnconfirmed: spendUnconfirmed}) + .setUnspent(testdata.dataUnspent); + + return t; + }; + + function f(amount, spendUnconfirmed) { + spendUnconfirmed = typeof spendUnconfirmed === 'undefined'?true:false; + return getBuilder(spendUnconfirmed) + ._selectUnspent(amount * util.COIN).selectedUtxos; + } + + it('#_selectUnspent should be able to select utxos', function() { + var u = f(1); + u.length.should.equal(3); + + should.exist(u[0].amount); + should.exist(u[0].txid); + should.exist(u[0].scriptPubKey); + should.exist(u[0].vout); + + f(0.5).length.should.equal(3); + f(0.1).length.should.equal(2); + f(0.05).length.should.equal(2); + f(0.015).length.should.equal(2); + f(0.001).length.should.equal(1); + }); + + /*jshint -W068 */ + it('#_selectUnspent should return null if not enough utxos', function() { + (function() { f(1.12); }).should.throw(); + }); + + + it('#_selectUnspent should check confirmations', function() { + (function() { f(0.9,false); }).should.throw(); + f(0.9).length.should.equal(3); + + f(0.11,false).length.should.equal(2); + (function() { f(0.111,false); }).should.throw(); + }); + + + + it('#_setInputs sets inputs', function() { + var b = getBuilder() + .setUnspent(testdata.dataUnspent) + ._selectUnspent(0.1 * util.COIN) + ._setInputs(); + + should.exist(b.txobj.ins[0].s); + should.exist(b.txobj.ins[0].q); + should.exist(b.txobj.ins[0].o); + }); + + it('#_setInputMap set inputMap', function() { + var b = getBuilder() + .setUnspent(testdata.dataUnspent) + ._selectUnspent(0.1 * util.COIN) + ._setInputs() + ._setInputMap(); + + should.exist(b.inputMap); + b.inputMap.length.should.equal(2); + }); + + var getBuilder2 = function (fee) { + var opts = { + remainderAddress: 'mwZabyZXg8JzUtFX1pkGygsMJjnuqiNhgd', + spendUnconfirmed: true, + }; + + if (fee) opts.fee = fee; + + var outs = [{ + address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', + amount: 0.08 + }]; + + return new TransactionBuilder(opts) + .setUnspent(testdata.dataUnspent) + .setOutputs(outs); + }; + + + it('should fail to create tx', function() { + + (function() { + getBuilder() + .setUnspent(testdata.dataUnspent) + .build(); + }).should.throw(); + }); + + it('should fail if not enough inputs ', function() { + var utxos = testdata.dataUnspent; + + var opts = { + remainderAddress: 'mwZabyZXg8JzUtFX1pkGygsMJjnuqiNhgd', + spendUnconfirmed: true, + }; + var outs = [{ + address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', + amount: 80 + }]; + + (function() { + new TransactionBuilder(opts) + .setUnspent(testdata.dataUnspent) + .setOutputs(outs); + }).should.throw(); + + var outs2 = [{ + address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', + amount: 0.5 + }]; + + should.exist( + new TransactionBuilder(opts) + .setUnspent(testdata.dataUnspent) + .setOutputs(outs2) + ); + + // do not allow unconfirmed + opts.spendUnconfirmed = false; + (function() { + new TransactionBuilder(opts) + .setUnspent(testdata.dataUnspent) + .setOutputs(outs2); + }).should.throw(); + }); + + it('should be able to create a tx', function() { + var b = getBuilder2(); + + b.isFullySigned().should.equal(false); + b.getSelectedUnspent().length.should.equal(2); + + var tx = b.build(); + 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); + + // remainder is 0.0299 here because unspent select utxos in order + util.valueToBigInt(tx.outs[1].v).cmp(2990000).should.equal(0); + }); + + + it('should create same output as bitcoind createrawtransaction ', function() { + var tx = getBuilder2().build(); + + // 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('should create same output as bitcoind createrawtransaction wo remainder', function() { + + //no remainder + var tx = getBuilder2(0.03).build(); + + // string output generated from: bitcoind createrawtransaction '[{"txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1","vout":1},{"txid":"2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc2","vout":0} ]' '{"mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE":0.08}' + // + tx.serialize().toString('hex').should.equal('0100000002c1cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0100000000ffffffffc2cf12ab89729d19d3cdec8ae531b5038d56c741006a105d532b3a7afa65c12a0000000000ffffffff0100127a00000000001976a914774e603bafb717bd3f070e68bbcccfd907c77d1388ac00000000'); + }); + + + + var getBuilder3 = function (outs) { + + var opts = { + remainderAddress: 'mwZabyZXg8JzUtFX1pkGygsMJjnuqiNhgd', + spendUnconfirmed: true, + }; + + var outs = outs || [{ + address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', + amount: 0.08 + }]; + +//console.log('[test.TransactionBuilder.js.216:outs:]',outs, outs.length); //TODO + return new TransactionBuilder(opts) + .setUnspent(testdata.dataUnspentSign.unspent) + .setOutputs(outs); + }; + + it('should sign a tx (case 1)', function() { + var b = getBuilder3(); + b.isFullySigned().should.equal(false); + + b.sign(testdata.dataUnspentSign.keyStrings); + + b.isFullySigned().should.equal(true); + + var tx = b.build(); + tx.isComplete().should.equal(true); + tx.ins.length.should.equal(1); + tx.outs.length.should.equal(2); + }); + + it('should sign a tx (case 2)', function() { + var b = getBuilder3([{ + address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', + amount: 16 + }]) + .sign(testdata.dataUnspentSign.keyStrings); + + b.isFullySigned().should.equal(true); + var tx = b.build(); + tx.isComplete().should.equal(true); + tx.ins.length.should.equal(3); + tx.outs.length.should.equal(2); + }); + + it('should sign an incomplete tx', function() { + var keys = ['cNpW8B7XPAzCdRR9RBWxZeveSNy3meXgHD8GuhcqUyDuy8ptCDzJ']; + var outs = [{ + address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', + amount: 0.08 + }]; + + var b = new TransactionBuilder() + .setUnspent(testdata.dataUnspentSign.unspent) + .setOutputs(outs) + .sign(keys); + + b.isFullySigned().should.equal(false); + + var tx = b.build(); + tx.ins.length.should.equal(1); + tx.outs.length.should.equal(2); + tx.isComplete().should.equal(false); + }); + + + it('should sign a tx in multiple steps (case1)', function() { + + var b = getBuilder3([{ + address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', + amount: 16 + }]); + + b.isFullySigned().should.equal(false); + (b.build()).isComplete().should.equal(false); + + var k1 = testdata.dataUnspentSign.keyStrings.slice(0, 1); + b.sign(k1); + b.isFullySigned().should.equal(false); + (b.build()).isComplete().should.equal(false); + + var k23 = testdata.dataUnspentSign.keyStrings.slice(1, 3); + b.sign(k23); + b.isFullySigned().should.equal(true); + (b.build()).isComplete().should.equal(true); + }); + + it('#sign should sign a tx in multiple steps (case2)', function() { + var b = getBuilder3([{ + address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', + amount: 16 + }]); + + b.isFullySigned().should.equal(false); + (b.build()).isComplete().should.equal(false); + + var k1 = testdata.dataUnspentSign.keyStrings.slice(0, 1); + b.sign(k1); + b.isFullySigned().should.equal(false); + (b.build()).isComplete().should.equal(false); + + var k2 = testdata.dataUnspentSign.keyStrings.slice(1, 2); + b.sign(k2); + b.isFullySigned().should.equal(false); + (b.build()).isComplete().should.equal(false); + + var k3 = testdata.dataUnspentSign.keyStrings.slice(2, 3); + b.sign(k3); + b.isFullySigned().should.equal(true); + (b.build()).isComplete().should.equal(true); + }); + + it('should generate dynamic fee and readjust (and not) the selected UTXOs (case1)', function() { + //this cases exceeds the input by 1mbtc AFTEr calculating the dynamic fee, + //so, it should trigger adding a new 10BTC utxo + // + + var outs = []; + var N = 101; + for (var i = 0; i < N; i++) { + outs.push({ + address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', + amount: 0.01 + }); + } + var b = getBuilder3(outs); + var tx = b.build(); + + tx.getSize().should.equal(3560); + + // ins = 11.0101 BTC (2 inputs: 1.0101 + 10 ); + parseInt(b.valueInSat.toString()).should.equal(11.0101 * util.COIN); + tx.ins.length.should.equal(2); + + // outs = 101 outs + 1 remainder + tx.outs.length.should.equal(N+1); + + + // 3560 bytes tx -> 0.0004 + b.feeSat.should.equal(0.0004 * util.COIN); + + // 101 * 0.01 = 1.01BTC; + 0.0004 fee = 1.0104btc + // remainder = 11.0101-1.0104 = 9.9997 + + parseInt(b.remainderSat.toString()).should.equal(parseInt(9.9997 * util.COIN)); + + util.valueToBigInt(tx.outs[N].v).cmp(999970000).should.equal(0); + tx.isComplete().should.equal(false); + }); + + it('should generate dynamic fee and readjust (and not) the selected UTXOs(case2)', function() { + //this is the complementary case, it does not trigger a new utxo + var outs = []; + var N = 100; + for (var i = 0; i < N; i++) { + outs.push({ + address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', + amount: 0.01 + }); + } + var b = getBuilder3(outs); + var tx = b.build(); + + tx.getSize().should.equal(3485); + + // ins = 1.0101 BTC (1 inputs: 1.0101 ); + parseInt(b.valueInSat.toString()).should.equal(1.0101 * util.COIN); + tx.ins.length.should.equal(1); + + // outs = 100 outs: + // 100 * 0.01 = 1BTC; + 0.0004 fee = 1.0004btc + // remainder = 1.0101-1.0004 = 0.0097 + + // outs = 101 outs + 1 remainder + tx.outs.length.should.equal(N+1); + + + // 3560 bytes tx -> 0.0004 + b.feeSat.should.equal(0.0004 * util.COIN); + + // 101 * 0.01 = 1.01BTC; + 0.0004 fee = 1.0104btc + // remainder = 11.0101-1.0104 = 9.9997 + parseInt(b.remainderSat.toString()).should.equal(parseInt(0.0097 * util.COIN)); + util.valueToBigInt(tx.outs[N].v).cmp(970000).should.equal(0); + tx.isComplete().should.equal(false); + }); +});