/* 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 * { * 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 * * */ '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); 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, }; }; 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 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; }; 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; }; 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