Merge branch 'feature/TransactionBuilder'

This commit is contained in:
Ryan X. Charles 2014-03-30 21:02:22 -04:00
commit 93050e3e92
12 changed files with 993 additions and 720 deletions

View File

@ -50,6 +50,6 @@ module.exports = function(grunt) {
});
grunt.registerTask('default', ['watch']);
grunt.registerTask('default', ['shell','watch']);
};

View File

@ -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');

View File

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

438
TransactionBuilder.js Normal file
View File

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

View File

@ -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');

View File

@ -45,6 +45,7 @@ var modules = [
'ScriptInterpreter',
'Sign',
'Transaction',
'TransactionBuilder',
'Wallet',
'WalletKey',
'config',

View File

@ -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();
}

74
examples/CreateScript.js Normal file
View File

@ -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();
}

View File

@ -14,9 +14,9 @@
<script type="text/javascript">
var Address = require('bitcore').Address;
print = function(s){
print = function(s,s2,s3){
var div = document.getElementById('content');
div.innerHTML += s + '<br />';
div.innerHTML += s + (s2||'') + (s3||'') + '<br />';
};
print('<hr> <h1>Address</h1>' );
@ -39,21 +39,16 @@
}
});
print('<hr> <h1>KeyModule</h1>' );
print('<hr> <h1>Key</h1>' );
/*
Using bitcore root module
*/
var bitcore = require('bitcore');
var k = bitcore.KeyModule.Key.generateSync();
var k = bitcore.Key.generateSync();
print ('Generate Key Pair:');
print ('Private:' + bitcore.buffertools.toHex(k.private));
print ('Public:' + bitcore.buffertools.toHex(k.public));
print('<hr> <h1>PeerManager</h1>' );
var p = new bitcore.PeerManager();
print('<hr> <h1>Util</h1>' );
var coinUtil = bitcore.util;
@ -81,6 +76,7 @@
var WalletKey = bitcore.WalletKey;
var networks = bitcore.networks;
var priv = 'L4cEVwoNDeYdCQfFJAGkGKPnE2TmqLEuBn4znQChD2ojjQRJVKpU';
var s = new WalletKey({
network: networks.livenet
@ -91,11 +87,9 @@
print("Public: " + o.pub);
print("Addr: " + o.addr);
var module = { exports: {} };
print('<hr><h1>TransactionBuilder</h1>');
console.log = print;
</script>
<script src="./CreateAndSignTx.js"></script>
<script>
run();
</script>
</body>
</html>

View File

@ -36,6 +36,7 @@
<script src="test.SIN.js"></script>
<script src="test.SINKey.js"></script>
<script src="test.Transaction.js"></script>
<script src="test.TransactionBuilder.js"></script>
<script src="test.util.js"></script>
<script src="test.VersionedData.js"></script>
<script src="test.Wallet.js"></script>

View File

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

View File

@ -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);
});
});