TransactionBuiler working with test
This commit is contained in:
parent
4ceacec9e8
commit
cb1a2d9b48
404
Transaction.js
404
Transaction.js
|
@ -302,9 +302,6 @@ Transaction.prototype.hashForSignature =
|
||||||
"(" + this.ins.length + " inputs)");
|
"(" + this.ins.length + " inputs)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone transaction
|
|
||||||
var txTmp = new Transaction(this);
|
|
||||||
|
|
||||||
// In case concatenating two scripts ends up with two codeseparators,
|
// In case concatenating two scripts ends up with two codeseparators,
|
||||||
// or an extra one at the end, this prevents all those possible
|
// or an extra one at the end, this prevents all those possible
|
||||||
// incompatibilities.
|
// incompatibilities.
|
||||||
|
@ -355,7 +352,7 @@ Transaction.prototype.hashForSignature =
|
||||||
} else {
|
} else {
|
||||||
var outsLen;
|
var outsLen;
|
||||||
if (hashTypeMode === SIGHASH_SINGLE) {
|
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
|
// bug present in bitcoind which must be also present in bitcore
|
||||||
// see https://bitcointalk.org/index.php?topic=260595
|
// see https://bitcointalk.org/index.php?topic=260595
|
||||||
// Transaction.hashForSignature(): SIGHASH_SINGLE
|
// 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() {
|
Transaction.prototype.calcSize = function() {
|
||||||
var totalSize = 8; // version + lock_time
|
var totalSize = 8; // version + lock_time
|
||||||
|
@ -722,7 +552,6 @@ Transaction.prototype.getSize = function getHash() {
|
||||||
return this.size;
|
return this.size;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
Transaction.prototype.isComplete = function() {
|
Transaction.prototype.isComplete = function() {
|
||||||
var l = this.ins.length;
|
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);
|
module.exports = require('soop')(Transaction);
|
||||||
|
|
|
@ -0,0 +1,445 @@
|
||||||
|
|
||||||
|
/*
|
||||||
|
var tx = TransactionBuilder.init(opts)
|
||||||
|
.setUnspent(utxos)
|
||||||
|
.setOutputs(outs)
|
||||||
|
.sign(keys)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
var builder = TransactionBuilder.init(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() {
|
||||||
|
this.txobj = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _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.init = function(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;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
|
@ -27,6 +27,7 @@ requireWhenAccessed('Address', './Address');
|
||||||
requireWhenAccessed('Opcode', './Opcode');
|
requireWhenAccessed('Opcode', './Opcode');
|
||||||
requireWhenAccessed('Script', './Script');
|
requireWhenAccessed('Script', './Script');
|
||||||
requireWhenAccessed('Transaction', './Transaction');
|
requireWhenAccessed('Transaction', './Transaction');
|
||||||
|
requireWhenAccessed('TransactionBuilder', './TransactionBuilder');
|
||||||
requireWhenAccessed('Connection', './Connection');
|
requireWhenAccessed('Connection', './Connection');
|
||||||
requireWhenAccessed('Peer', './Peer');
|
requireWhenAccessed('Peer', './Peer');
|
||||||
requireWhenAccessed('Block', './Block');
|
requireWhenAccessed('Block', './Block');
|
||||||
|
|
|
@ -62,278 +62,6 @@ describe('Transaction', function() {
|
||||||
should.exist(t);
|
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
|
* Bitcoin core transaction tests
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,406 @@
|
||||||
|
'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 init', function() {
|
||||||
|
var t = new TransactionBuilder();
|
||||||
|
t.init({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();
|
||||||
|
t.init( {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())
|
||||||
|
.init(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))
|
||||||
|
.init(opts)
|
||||||
|
.setUnspent(testdata.dataUnspent)
|
||||||
|
.setOutputs(outs);
|
||||||
|
}).should.throw();
|
||||||
|
|
||||||
|
var outs2 = [{
|
||||||
|
address: 'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE',
|
||||||
|
amount: 0.5
|
||||||
|
}];
|
||||||
|
|
||||||
|
should.exist(
|
||||||
|
(new TransactionBuilder(opts))
|
||||||
|
.init(opts)
|
||||||
|
.setUnspent(testdata.dataUnspent)
|
||||||
|
.setOutputs(outs2)
|
||||||
|
);
|
||||||
|
|
||||||
|
// do not allow unconfirmed
|
||||||
|
opts.spendUnconfirmed = false;
|
||||||
|
(function() {
|
||||||
|
(new TransactionBuilder(opts))
|
||||||
|
.init(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())
|
||||||
|
.init(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())
|
||||||
|
.init()
|
||||||
|
.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);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue