// Creates a bitcore Transaction object // // // Synopsis // -------- // ``` // 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()); // // //Searialize it and pass it around... // var string = JSON.serialize(builder.toObj()); // // then... // var builder = TransactionBuilder.fromObj(JSON.parse(str); // builder.sign(keys); // // Also // var builder2 = TransactionBuilder.fromObj(JSON.parse(str2); // builder2.merge(builder); // Will merge signatures for p2sh mulsig txs. // // // ``` // // // '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'); 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 Key = imports.Key || require('./Key'); var log = imports.log || require('../util/log'); var Transaction = imports.Transaction || require('./Transaction'); var FEE_PER_1000B_SAT = parseInt(0.0001 * util.COIN); // Methods // ------- // // TransactionBuilder // ------------------ // Creates a TransactionBuilder instance // `opts` // ``` // { // remainderOut: 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 remainderOut is given, and there are remainder coins, the // first IN out will be used to return the coins. remainderOut has the form: // ``` // remainderOut = { address: 1xxxxx} // ``` // or // ``` // remainderOut = { pubkeys: ['hex1','hex2',...} for multisig // ``` function TransactionBuilder(opts) { opts = opts || {}; this.lockTime = opts.lockTime || 0; this.spendUnconfirmed = opts.spendUnconfirmed || false; if (opts.fee || opts.feeSat) { this.givenFeeSat = opts.fee ? opts.fee * util.COIN : opts.feeSat; } this.remainderOut = opts.remainderOut; this.signhash = opts.signhash || Transaction.SIGHASH_ALL; this.tx = {}; this.inputsSigned= 0; this.signaturesAdded= 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.addressVersion || version === testnet.addressVersion) script = Script.createPubKeyHashOut(address.payload()); else if (version === livenet.P2SHVersion || version === testnet.P2SHVersion) script = Script.createP2SH(address.payload()); else throw new Error('invalid output address'); return script; }; TransactionBuilder._scriptForPubkeys = function(out) { var l = out.pubkeys.length; var pubKeyBuf=[]; for (var i=0; i 1) ret = this._scriptForPubkeys(out); else throw new Error('unknown out type'); return ret; }; TransactionBuilder.infoForP2sh = function(opts, networkName) { var script = this._scriptForOut(opts); var hash = util.sha256ripe160(script.getBuffer()); var version = networkName === 'testnet' ? networks.testnet.P2SHVersion : networks.livenet.P2SHVersion; var addr = new Address(version, hash); var addrStr = addr.as('base58'); return { script: script, scriptBufHex: script.getBuffer().toString('hex'), hash: hash, address: addrStr, }; }; // setUnspent // ---------- // Sets the `unspent` available for the transaction. Some (or all) // of them to fullfil the transaction's outputs and fee. // The expected format is: // ``` // [{ // 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. TransactionBuilder.prototype.setUnspent = function(unspent) { this.utxos = unspent; return this; }; TransactionBuilder.prototype._setInputMap = function() { var inputMap = []; var l = this.selectedUtxos.length; for (var i = 0; i < l; i++) { var utxo = this.selectedUtxos[i]; var scriptBuf = new Buffer(utxo.scriptPubKey, 'hex'); var scriptPubKey = new Script(scriptBuf); var scriptType = scriptPubKey.classify(); if (scriptType === Script.TX_UNKNOWN) throw new Error('unkown output type at:' + i + ' Type:' + scriptPubKey.getRawOutType()); inputMap.push({ address: utxo.address, scriptPubKey: scriptPubKey, scriptType: scriptType, i: i, }); } this.inputMap = inputMap; return this; }; // getSelectedUnspent // ------------------ // // Returns the selected unspent outputs, to be used in the transaction. TransactionBuilder.prototype.getSelectedUnspent = function() { 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 [SAT]:' + neededAmountSat); this.selectedUtxos = sel; this._setInputMap(); return this; }; TransactionBuilder.prototype._setInputs = function(txobj) { var ins = this.selectedUtxos; var l = ins.length; var valueInSat = bignum(0); 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]); 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(txobj, 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 =txobj.outs.length; this.remainderSat = bignum(0); /*remove old remainder? */ if (l > remainderIndex) { txobj.outs.pop(); } if (remainderSat.cmp(0) > 0) { var remainderOut = this.remainderOut || this.selectedUtxos[0]; var value = util.bigIntToValue(remainderSat); var script = TransactionBuilder._scriptForOut(remainderOut); var txout = { v: value, s: script.getBuffer(), }; txobj.outs.push(txout); this.remainderSat = remainderSat; } return this; }; TransactionBuilder.prototype._setFeeAndRemainder = function(txobj) { /* starting size estimation */ var size = 500, maxSizeK, remainderIndex = 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(txobj) ._setFee(feeSat) ._setRemainder(txobj, remainderIndex); size = new Transaction(txobj).getSize(); } while (size > (maxSizeK + 1) * 1000); return this; }; // setOutputs // ---------- // Sets the outputs for the transaction. Format is: // ``` // an array of [{ // address: xx, // amount:0.001 // },...] // ``` // // Note that only some of this outputs will be selected // to create the transaction. The selected ones can be checked // after calling `setOutputs`, with `.getSelectedUnspent` // TransactionBuilder.prototype.setOutputs = function(outs) { var valueOutSat = bignum(0); var txobj = {}; txobj.version = 1; txobj.lock_time = this.lockTime || 0; txobj.ins = []; 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._scriptForOut(outs[i]); var txout = { v: value, s: script.getBuffer(), }; txobj.outs.push(txout); var sat = outs[i].amountSat || util.parseValue(outs[i].amount); valueOutSat = valueOutSat.add(sat); } this.valueOutSat = valueOutSat; this._setFeeAndRemainder(txobj); this.tx = new Transaction(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._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._multiFindKey = function(walletKeyMap,pubKeyHash) { var wk; [ networks.livenet, networks.testnet].forEach(function(n) { [ n.addressVersion, n.P2SHVersion].forEach(function(v) { var a = new Address(v,pubKeyHash); if (!wk && walletKeyMap[a]) { wk = walletKeyMap[a]; } }); }); return wk; }; TransactionBuilder.prototype._findWalletKey = function(walletKeyMap, input) { var wk; if (input.address) { wk = walletKeyMap[input.address]; } else if (input.pubKeyHash) { wk = this._multiFindKey(walletKeyMap, input.pubKeyHash); } else if (input.pubKeyBuf) { var pubKeyHash = util.sha256ripe160(input.pubKeyBuf); wk = this._multiFindKey(walletKeyMap, pubKeyHash); } else { throw new Error('no infomation at input to find keys'); } return wk; }; TransactionBuilder.prototype._signPubKey = function(walletKeyMap, input, txSigHash) { if (this.tx.ins[input.i].s.length > 0) return {}; var wk = this._findWalletKey(walletKeyMap, input); if (!wk) return; 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.updateBuffer(); return {inputFullySigned: true, signaturesAdded: 1, script: scriptSig.getBuffer()}; }; TransactionBuilder.prototype._signPubKeyHash = function(walletKeyMap, input, txSigHash) { if (this.tx.ins[input.i].s.length > 0) return {}; var wk = this._findWalletKey(walletKeyMap, input); if (!wk) return; 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(); return {inputFullySigned: true, signaturesAdded: 1, script: scriptSig.getBuffer()}; }; /* FOR TESTING var _dumpChunks = function (scriptSig, label) { console.log('## DUMP: ' + label + ' ##'); for(var i=0; i2) || (l1<2 && l0>2 ))) throw new Error('TX sig types mismatch in merge'); if ((!l0 && !l1) || ( l0 && !l1) || (!l0 && l1)) return s1buf; // Get the pubkeys var input = this.inputMap[index]; var type = input.scriptPubKey.classify(); //p2pubkey or p2pubkeyhash if (type === Script.TX_PUBKEYHASH || type === Script.TX_PUBKEY) { log.debug('Merging two signed inputs type:' + input.scriptPubKey.getRawOutType() + '. Signatures differs. Using the first version.'); return s0buf; } else if (type!== Script.TX_SCRIPTHASH) { // No support for normal multisig or strange txs. throw new Error('Script type:'+input.scriptPubKey.getRawOutType()+'not supported at #merge'); } return this._mergeInputSigP2sh(input,s0, s1); }; TransactionBuilder.prototype._mergeTx = function(tx, ignoreConflictingSignatures) { var v0 = this.tx; var v1 = tx; var l = v0.ins.length; if (l !== v1.ins.length) throw new Error('TX in length mismatch in merge'); this.inputsSigned =0; for(var i=0; i