tx creation working. more tests needed
This commit is contained in:
parent
ada92746b7
commit
671d372c19
16
Address.js
16
Address.js
|
@ -1,6 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
var imports = require('soop').imports();
|
var imports = require('soop').imports();
|
||||||
var parent = imports.parent || require('./util/VersionedData');
|
var parent = imports.parent || require('./util/VersionedData');
|
||||||
|
var networks= imports.networks || require('./networks');
|
||||||
|
|
||||||
function Address() {
|
function Address() {
|
||||||
Address.super(this, arguments);
|
Address.super(this, arguments);
|
||||||
|
@ -22,4 +23,19 @@ Address.prototype.isValid = function() {
|
||||||
return answer;
|
return answer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Address.prototype.network = function() {
|
||||||
|
var version = this.version();
|
||||||
|
|
||||||
|
var livenet = networks.livenet;
|
||||||
|
var testnet = networks.testnet;
|
||||||
|
|
||||||
|
var answer;
|
||||||
|
if (version === livenet.addressPubkey || version === livenet.addressScript)
|
||||||
|
answer = livenet;
|
||||||
|
else if (version === testnet.addressPubkey || version === testnet.addressScript)
|
||||||
|
answer = testnet;
|
||||||
|
|
||||||
|
return answer;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = require('soop')(Address);
|
module.exports = require('soop')(Address);
|
||||||
|
|
175
Transaction.js
175
Transaction.js
|
@ -11,8 +11,10 @@ var Parser = imports.Parser || require('./util/BinaryParser');
|
||||||
var Step = imports.Step || require('step');
|
var Step = imports.Step || require('step');
|
||||||
var buffertools = imports.buffertools || require('buffertools');
|
var buffertools = imports.buffertools || require('buffertools');
|
||||||
var error = imports.error || require('./util/error');
|
var error = imports.error || require('./util/error');
|
||||||
|
var networks = imports.networks || require('./networks');
|
||||||
|
|
||||||
var COINBASE_OP = Buffer.concat([util.NULL_HASH, new Buffer('FFFFFFFF', 'hex')]);
|
var COINBASE_OP = Buffer.concat([util.NULL_HASH, new Buffer('FFFFFFFF', 'hex')]);
|
||||||
|
var DEFAULT_FEE = 0.0001;
|
||||||
|
|
||||||
function TransactionIn(data) {
|
function TransactionIn(data) {
|
||||||
if ("object" !== typeof data) {
|
if ("object" !== typeof data) {
|
||||||
|
@ -605,36 +607,7 @@ Transaction.prototype.fromObj = function fromObj(obj) {
|
||||||
txobj.ins = [];
|
txobj.ins = [];
|
||||||
txobj.outs = [];
|
txobj.outs = [];
|
||||||
|
|
||||||
obj.inputs.forEach(function(inputobj) {
|
var tx = new Transaction(txobj);
|
||||||
var txin = new TransactionIn();
|
|
||||||
txin.s = util.EMPTY_BUFFER;
|
|
||||||
txin.q = 0xffffffff;
|
|
||||||
|
|
||||||
var hash = new Buffer(inputobj.txid, 'hex');
|
|
||||||
hash = buffertools.reverse(hash);
|
|
||||||
var vout = parseInt(inputobj.vout);
|
|
||||||
var voutBuf = new Buffer(4);
|
|
||||||
voutBuf.writeUInt32LE(vout, 0);
|
|
||||||
|
|
||||||
txin.o = Buffer.concat([hash, voutBuf]);
|
|
||||||
|
|
||||||
txobj.ins.push(txin);
|
|
||||||
});
|
|
||||||
|
|
||||||
var keys = Object.keys(obj.outputs);
|
|
||||||
keys.forEach(function(addrStr) {
|
|
||||||
var addr = new Address(addrStr);
|
|
||||||
var script = Script.createPubKeyHashOut(addr.payload());
|
|
||||||
|
|
||||||
var valueNum = bignum(obj.outputs[addrStr]);
|
|
||||||
var value = util.bigIntToValue(valueNum);
|
|
||||||
|
|
||||||
var txout = new TransactionOut();
|
|
||||||
txout.v = value;
|
|
||||||
txout.s = script.getBuffer();
|
|
||||||
|
|
||||||
txobj.outs.push(txout);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.lock_time = txobj.lock_time;
|
this.lock_time = txobj.lock_time;
|
||||||
this.version = txobj.version;
|
this.version = txobj.version;
|
||||||
|
@ -694,7 +667,8 @@ Transaction.prototype.parse = function (parser) {
|
||||||
* }, [...]
|
* }, [...]
|
||||||
* ]
|
* ]
|
||||||
* This is compatible con insight's /utxo API.
|
* This is compatible con insight's /utxo API.
|
||||||
* NOTE that amount is in BTCs! (as returned in insight and bitcoind.
|
* NOTE that amount is in BTCs! (as returned in insight and bitcoind)
|
||||||
|
* amountSat can be given to provide amount in satochis.
|
||||||
*
|
*
|
||||||
* @totalNeededAmount: output transaction amount in BTC, including fee
|
* @totalNeededAmount: output transaction amount in BTC, including fee
|
||||||
*
|
*
|
||||||
|
@ -705,17 +679,27 @@ Transaction.prototype.parse = function (parser) {
|
||||||
*/
|
*/
|
||||||
Transaction.selectUnspent = function (unspentArray, totalNeededAmount) {
|
Transaction.selectUnspent = function (unspentArray, totalNeededAmount) {
|
||||||
|
|
||||||
|
// TODO implement bidcoind heristics
|
||||||
|
// A-
|
||||||
|
// 1) select utxos with 6+ confirmations
|
||||||
|
// 2) if not 2) select utxos with 1+ confirmations
|
||||||
|
// 3) if not select unconfirmed.
|
||||||
|
//
|
||||||
|
// B-
|
||||||
|
// Select smaller utxos first.
|
||||||
|
//
|
||||||
|
//
|
||||||
// TODO we could randomize or select the selection
|
// TODO we could randomize or select the selection
|
||||||
|
|
||||||
var selected = [];
|
var selected = [];
|
||||||
var l = unspentArray.length;
|
var l = unspentArray.length;
|
||||||
var totalSat = bignum(0);
|
var totalSat = bignum(0);
|
||||||
var totalNeededAmountSat = bignum(totalNeededAmount * util.COIN);
|
var totalNeededAmountSat = util.parseValue(totalNeededAmount);
|
||||||
var fullfill = false;
|
var fullfill = false;
|
||||||
|
|
||||||
for(var i = 0; i<l; i++) {
|
for(var i = 0; i<l; i++) {
|
||||||
var u = unspentArray[i];
|
var u = unspentArray[i];
|
||||||
var sat = bignum(u.amount * util.COIN);
|
var sat = u.amountSat || util.parseValue(u.amount);
|
||||||
totalSat = totalSat.add(sat);
|
totalSat = totalSat.add(sat);
|
||||||
selected.push(u);
|
selected.push(u);
|
||||||
if(totalSat.cmp(totalNeededAmountSat) >= 0) {
|
if(totalSat.cmp(totalNeededAmountSat) >= 0) {
|
||||||
|
@ -727,7 +711,132 @@ Transaction.selectUnspent = function (unspentArray, totalNeededAmount) {
|
||||||
return selected;
|
return selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _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;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* create
|
||||||
|
*
|
||||||
|
* creates a transaction
|
||||||
|
*
|
||||||
|
* @ins
|
||||||
|
* a selected set of utxos, as described on selectUnspent
|
||||||
|
* @outs
|
||||||
|
* an array of [{
|
||||||
|
* address: xx,
|
||||||
|
* amount:0.001
|
||||||
|
* },...]
|
||||||
|
* @opts
|
||||||
|
* {
|
||||||
|
* remainderAddress: null,
|
||||||
|
* fee: 0.001,
|
||||||
|
* lockTime: null,
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Amounts are in BTC. instead of fee and amount; feeSat and amountSat can be given,
|
||||||
|
* repectively, to provide amounts in satoshis.
|
||||||
|
*
|
||||||
|
* (TODO: should we use uBTC already?)
|
||||||
|
*
|
||||||
|
* If not remainderAddress is given, and there is a remainderAddress
|
||||||
|
* first in address will be used. (TODO: is this is reasoable?)
|
||||||
|
*
|
||||||
|
* TODO add exceptions for validations:
|
||||||
|
* out address - ins amount > out amount - fees < maxFees
|
||||||
|
* + more?
|
||||||
|
*/
|
||||||
|
|
||||||
|
Transaction.create = function (ins, outs, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
|
||||||
|
var feeSat = opts.feeSat || util.parseValue(opts.fee || DEFAULT_FEE);
|
||||||
|
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 = bignum(0);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueInSat.cmp(valueOutSat)<0) {
|
||||||
|
throw new Error('transaction inputs sum less than outputs');
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainderSat = valueInSat.sub(valueOutSat);
|
||||||
|
|
||||||
|
if (remainderSat.cmp(0)>0) {
|
||||||
|
var remainderAddress = opts.remainderAddress || ins[0].address;
|
||||||
|
|
||||||
|
outs.push({
|
||||||
|
address: remainderAddress,
|
||||||
|
amountSat: remainderSat,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return new Transaction(txobj);
|
||||||
|
};
|
||||||
|
|
||||||
var TransactionInputsCache = exports.TransactionInputsCache =
|
var TransactionInputsCache = exports.TransactionInputsCache =
|
||||||
function TransactionInputsCache(tx)
|
function TransactionInputsCache(tx)
|
||||||
|
|
|
@ -10,6 +10,7 @@ var Transaction;
|
||||||
var In;
|
var In;
|
||||||
var Out;
|
var Out;
|
||||||
var Script = bitcore.Script;
|
var Script = bitcore.Script;
|
||||||
|
var util = bitcore.util;
|
||||||
var buffertools = require('buffertools');
|
var buffertools = require('buffertools');
|
||||||
var testdata = testdata || require('./testdata');
|
var testdata = testdata || require('./testdata');
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ describe('Transaction', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should be able to select unspents', function() {
|
it('should be able to select utxos', function() {
|
||||||
var u = Transaction.selectUnspent(testdata.dataUnspends,1.0);
|
var u = Transaction.selectUnspent(testdata.dataUnspends,1.0);
|
||||||
u.length.should.equal(3);
|
u.length.should.equal(3);
|
||||||
u = Transaction.selectUnspent(testdata.dataUnspends,0.5);
|
u = Transaction.selectUnspent(testdata.dataUnspends,0.5);
|
||||||
|
@ -52,11 +53,24 @@ describe('Transaction', function() {
|
||||||
should.exist(u[0].vout);
|
should.exist(u[0].vout);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return null if not enough utxos', function() {
|
||||||
it.skip('should be able to create instance thru #create', function() {
|
var u = Transaction.selectUnspent(testdata.dataUnspends,1.12);
|
||||||
var t = Transaction.create({
|
u.length.should.equal(0);
|
||||||
});
|
});
|
||||||
should.exist(t);
|
|
||||||
|
|
||||||
|
it('should be able to create instance thru #create', function() {
|
||||||
|
var utxos = Transaction.selectUnspent(testdata.dataUnspends,0.1);
|
||||||
|
var outs = [{address:'mrPnbY1yKDBsdgbHbS7kJ8GVm8F66hWHLE', amount:0.08}];
|
||||||
|
var tx = Transaction.create(utxos, outs,
|
||||||
|
{remainderAddress:'3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou'});
|
||||||
|
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);
|
||||||
|
// TODO remainder is 0.03 here because unspend just select utxos in order
|
||||||
|
util.valueToBigInt(tx.outs[1].v).cmp(3000000).should.equal(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Read tests from test/data/tx_valid.json
|
// Read tests from test/data/tx_valid.json
|
||||||
|
|
|
@ -188,6 +188,9 @@ function parseWholeValue(res)
|
||||||
|
|
||||||
exports.parseValue = function parseValue(valueStr)
|
exports.parseValue = function parseValue(valueStr)
|
||||||
{
|
{
|
||||||
|
if (typeof valueStr !== 'string')
|
||||||
|
valueStr = valueStr.toString();
|
||||||
|
|
||||||
var res = valueStr.match(reFullVal);
|
var res = valueStr.match(reFullVal);
|
||||||
if (res)
|
if (res)
|
||||||
return parseFullValue(res);
|
return parseFullValue(res);
|
||||||
|
|
Loading…
Reference in New Issue