Matias Alejo Garcia
11 years ago
5 changed files with 853 additions and 675 deletions
@ -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); |
|||
|
@ -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