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 @@
-