Merge pull request #237 from matiu/feature/double-spends
Feature/double spends
This commit is contained in:
commit
c4ebfa39ab
|
@ -35,6 +35,13 @@ module.exports = function(grunt) {
|
||||||
livereload: true,
|
livereload: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
js2: {
|
||||||
|
files: ['public/src/**/*.js'],
|
||||||
|
tasks: ['compile'],
|
||||||
|
options: {
|
||||||
|
livereload: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
html: {
|
html: {
|
||||||
files: ['public/views/**'],
|
files: ['public/views/**'],
|
||||||
options: {
|
options: {
|
||||||
|
@ -124,7 +131,7 @@ module.exports = function(grunt) {
|
||||||
script: 'insight.js',
|
script: 'insight.js',
|
||||||
options: {
|
options: {
|
||||||
args: [],
|
args: [],
|
||||||
ignore: ['public/**', 'test/**','util/**'],
|
ignore: ['public/**/*.html','public/**/*.css', 'public/**/*.js', 'test/**/*','util/**/*', ,'dev-util/**/*'],
|
||||||
// nodeArgs: ['--debug'],
|
// nodeArgs: ['--debug'],
|
||||||
delayTime: 1,
|
delayTime: 1,
|
||||||
env: {
|
env: {
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
var Address = require('../models/Address'),
|
var Address = require('../models/Address'),
|
||||||
common = require('./common');
|
common = require('./common');
|
||||||
|
|
||||||
|
|
||||||
exports.address = function(req, res, next, addr) {
|
exports.address = function(req, res, next, addr) {
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var config = require('../config/config'),
|
||||||
|
levelup = require('levelup');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var k = process.argv[2];
|
||||||
|
var v = process.argv[3];
|
||||||
|
var isBlock = process.argv[4] === '1';
|
||||||
|
|
||||||
|
|
||||||
|
var dbPath = config.leveldb + (isBlock ? '/blocks' : '/txs');
|
||||||
|
console.log('DB: ',dbPath); //TODO
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var db = levelup(dbPath );
|
||||||
|
|
||||||
|
|
||||||
|
if (v) {
|
||||||
|
db.put(k,v,function(err) {
|
||||||
|
console.log('[PUT done]',err); //TODO
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
db.del(k,function(err) {
|
||||||
|
console.log('[DEL done]',err); //TODO
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -40,9 +40,6 @@ var walk = function(path) {
|
||||||
|
|
||||||
walk(models_path);
|
walk(models_path);
|
||||||
|
|
||||||
var syncOpts = {
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* p2pSync process
|
* p2pSync process
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -51,7 +51,6 @@ function spec(b) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// TransactionDb.prototype.fromTxIdOne = function(txid, cb) { TODO
|
|
||||||
TransactionDb.prototype.has = function(txid, cb) {
|
TransactionDb.prototype.has = function(txid, cb) {
|
||||||
|
|
||||||
var k = OUTS_PREFIX + txid;
|
var k = OUTS_PREFIX + txid;
|
||||||
|
@ -135,6 +134,27 @@ function spec(b) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
TransactionDb.prototype._fillSpend = function(info, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!info) return cb();
|
||||||
|
|
||||||
|
var k = SPEND_PREFIX + info.txid;
|
||||||
|
db.createReadStream({start: k, end: k + '~'})
|
||||||
|
.on('data', function (data) {
|
||||||
|
var k = data.key.split('-');
|
||||||
|
self._addSpendInfo(info.vout[k[3]], k[4], k[5]);
|
||||||
|
})
|
||||||
|
.on('error', function (err) {
|
||||||
|
return cb(err);
|
||||||
|
})
|
||||||
|
.on('end', function (err) {
|
||||||
|
return cb(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
TransactionDb.prototype._fillOutpoints = function(info, cb) {
|
TransactionDb.prototype._fillOutpoints = function(info, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
@ -142,18 +162,46 @@ function spec(b) {
|
||||||
|
|
||||||
var valueIn = 0;
|
var valueIn = 0;
|
||||||
var incompleteInputs = 0;
|
var incompleteInputs = 0;
|
||||||
|
|
||||||
async.eachLimit(info.vin, CONCURRENCY, function(i, c_in) {
|
async.eachLimit(info.vin, CONCURRENCY, function(i, c_in) {
|
||||||
self.fromTxIdN(i.txid, i.vout, function(err, addr, valueSat) {
|
self.fromTxIdN(i.txid, i.vout, function(err, ret) {
|
||||||
|
//console.log('[TransactionDb.js.154:ret:]',ret); //TODO
|
||||||
|
if (!ret || !ret.addr || !ret.valueSat ) {
|
||||||
if (err || !addr || !valueSat ) {
|
|
||||||
console.log('Could not get TXouts in %s,%d from %s ', i.txid, i.vout, info.txid);
|
console.log('Could not get TXouts in %s,%d from %s ', i.txid, i.vout, info.txid);
|
||||||
|
if (ret) i.unconfirmedInput = ret.unconfirmedInput;
|
||||||
incompleteInputs = 1;
|
incompleteInputs = 1;
|
||||||
return c_in(); // error not scalated
|
return c_in(); // error not scalated
|
||||||
}
|
}
|
||||||
i.addr = addr;
|
|
||||||
i.valueSat = valueSat;
|
i.unconfirmedInput = i.unconfirmedInput;
|
||||||
i.value = valueSat / util.COIN;
|
i.addr = ret.addr;
|
||||||
|
i.valueSat = ret.valueSat;
|
||||||
|
i.value = ret.valueSat / util.COIN;
|
||||||
|
|
||||||
|
// Double spend?
|
||||||
|
if ( ret.multipleSpendAttempt ||
|
||||||
|
!ret.spendTxId ||
|
||||||
|
(ret.spendTxId && ret.spendTxId !== info.txid)
|
||||||
|
) {
|
||||||
|
if (ret.multipleSpendAttempts ) {
|
||||||
|
ret.multipleSpendAttempts.each(function(mul) {
|
||||||
|
if (mul.spendTxId !== info.txid) {
|
||||||
|
i.doubleSpendTxID = ret.spendTxId;
|
||||||
|
i.doubleSpendIndex = ret.spendIndex;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (!ret.spendTxId) {
|
||||||
|
i.dbError = 'Input spend not registered';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
i.doubleSpendTxID = ret.spendTxId;
|
||||||
|
i.doubleSpendIndex = ret.spendIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
i.doubleSpendTxID = null;
|
||||||
|
}
|
||||||
|
|
||||||
valueIn += i.valueSat;
|
valueIn += i.valueSat;
|
||||||
return c_in();
|
return c_in();
|
||||||
|
@ -178,7 +226,9 @@ function spec(b) {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
|
|
||||||
self._fillOutpoints(info, function() {
|
self._fillOutpoints(info, function() {
|
||||||
return next(null, info);
|
self._fillSpend(info, function() {
|
||||||
|
return next(null, info);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -195,15 +245,33 @@ function spec(b) {
|
||||||
};
|
};
|
||||||
|
|
||||||
TransactionDb.prototype.fromTxIdN = function(txid, n, cb) {
|
TransactionDb.prototype.fromTxIdN = function(txid, n, cb) {
|
||||||
|
var self = this;
|
||||||
var k = OUTS_PREFIX + txid + '-' + n;
|
var k = OUTS_PREFIX + txid + '-' + n;
|
||||||
|
|
||||||
db.get(k, function (err,val) {
|
db.get(k, function (err,val) {
|
||||||
if (err && err.notFound) {
|
if (!val || (err && err.notFound) ) {
|
||||||
err = null;
|
return cb(null, { unconfirmedInput: 1} );
|
||||||
}
|
}
|
||||||
var a = val?val.split(':'):[null,null];
|
|
||||||
return cb(err, a[0], parseInt(a[1]));
|
var a = val.split(':');
|
||||||
|
var ret = {
|
||||||
|
addr: a[0],
|
||||||
|
valueSat: parseInt(a[1]),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spend?
|
||||||
|
var k = SPEND_PREFIX + txid + '-' + n;
|
||||||
|
db.createReadStream({start: k, end: k + '~'})
|
||||||
|
.on('data', function (data) {
|
||||||
|
var k = data.key.split('-');
|
||||||
|
self._addSpendInfo(ret, k[4], k[5]);
|
||||||
|
})
|
||||||
|
.on('error', function (error) {
|
||||||
|
return cb(error);
|
||||||
|
})
|
||||||
|
.on('end', function () {
|
||||||
|
return cb(null,ret);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -218,27 +286,18 @@ function spec(b) {
|
||||||
|
|
||||||
if (o.multipleSpendAttempts) {
|
if (o.multipleSpendAttempts) {
|
||||||
|
|
||||||
var isConfirmed = 0;
|
|
||||||
var txid, index;
|
|
||||||
async.each(o.multipleSpendAttempts,
|
async.each(o.multipleSpendAttempts,
|
||||||
function (oi) {
|
function (oi, e_c) {
|
||||||
self.isConfirmed(oi.spendTxId, function(err,is) {
|
self.isConfirmed(oi.spendTxId, function(err,is) {
|
||||||
if (err) return cb(err);
|
if (err) return;
|
||||||
isConfirmed = 1;
|
if (is) {
|
||||||
txid = oi.spendTxId;
|
o.spendTxId = oi.spendTxId;
|
||||||
index = oi.index;
|
o.index = oi.index;
|
||||||
return cb();
|
o.spendIsConfirmed = 1;
|
||||||
|
}
|
||||||
|
return e_c();
|
||||||
});
|
});
|
||||||
},
|
}, cb);
|
||||||
function (err) {
|
|
||||||
// write the spended TXid into main register
|
|
||||||
if (isConfirmed) {
|
|
||||||
o.spendTxId = txid;
|
|
||||||
o.index = index;
|
|
||||||
o.spendIsConfirmed = 1;
|
|
||||||
}
|
|
||||||
return cb(err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
self.isConfirmed(o.spendTxId, function(err,is) {
|
self.isConfirmed(o.spendTxId, function(err,is) {
|
||||||
|
@ -367,7 +426,6 @@ function spec(b) {
|
||||||
TransactionDb.prototype.add = function(tx, blockhash, cb) {
|
TransactionDb.prototype.add = function(tx, blockhash, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var addrs = [];
|
var addrs = [];
|
||||||
var is_new = true;
|
|
||||||
|
|
||||||
if (tx.hash) self.adaptTxObject(tx);
|
if (tx.hash) self.adaptTxObject(tx);
|
||||||
|
|
||||||
|
@ -423,13 +481,8 @@ function spec(b) {
|
||||||
},
|
},
|
||||||
function (err) {
|
function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.message.match(/E11000/)) {
|
console.log('ERR at TX %s: %s', tx.txid, err);
|
||||||
is_new = false;
|
return cb(err);
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log('ERR at TX %s: %s', tx.txid, err);
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return p_c();
|
return p_c();
|
||||||
});
|
});
|
||||||
|
@ -439,7 +492,7 @@ function spec(b) {
|
||||||
return self.setConfirmation(tx.txid,blockhash, true, p_c);
|
return self.setConfirmation(tx.txid,blockhash, true, p_c);
|
||||||
},
|
},
|
||||||
], function(err) {
|
], function(err) {
|
||||||
return cb(err, addrs, is_new);
|
return cb(err, addrs);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,9 @@ function($scope, $rootScope, $routeParams, $location, Global, Transaction, Trans
|
||||||
var tmp = {};
|
var tmp = {};
|
||||||
var u = 0;
|
var u = 0;
|
||||||
|
|
||||||
// TODO multiple output address
|
|
||||||
//
|
|
||||||
for(var i=0; i < l; i++) {
|
for(var i=0; i < l; i++) {
|
||||||
|
|
||||||
var notAddr = false;
|
var notAddr = false;
|
||||||
|
|
||||||
// non standard input
|
// non standard input
|
||||||
if (items[i].scriptSig && !items[i].addr) {
|
if (items[i].scriptSig && !items[i].addr) {
|
||||||
items[i].addr = 'Unparsed address [' + u++ + ']';
|
items[i].addr = 'Unparsed address [' + u++ + ']';
|
||||||
|
@ -55,7 +52,12 @@ function($scope, $rootScope, $routeParams, $location, Global, Transaction, Trans
|
||||||
tmp[addr].addr = addr;
|
tmp[addr].addr = addr;
|
||||||
tmp[addr].items = [];
|
tmp[addr].items = [];
|
||||||
}
|
}
|
||||||
|
tmp[addr].isSpend = items[i].spendTxId;
|
||||||
|
|
||||||
|
tmp[addr].doubleSpendTxID = tmp[addr].doubleSpendTxID || items[i].doubleSpendTxID;
|
||||||
|
tmp[addr].doubleSpendIndex = tmp[addr].doubleSpendIndex || items[i].doubleSpendIndex;
|
||||||
|
tmp[addr].unconfirmedInput += items[i].unconfirmedInput;
|
||||||
|
tmp[addr].dbError = tmp[addr].dbError || items[i].dbError;
|
||||||
tmp[addr].valueSat += items[i].value * COIN;
|
tmp[addr].valueSat += items[i].value * COIN;
|
||||||
tmp[addr].value = items[i].value;
|
tmp[addr].value = items[i].value;
|
||||||
tmp[addr].items.push(items[i]);
|
tmp[addr].items.push(items[i]);
|
||||||
|
|
|
@ -28,6 +28,11 @@
|
||||||
<span class="text-muted" title="Current Bitcoin Address" data-ng-show="vin.addr == $root.currentAddr">{{vin.addr}}</span>
|
<span class="text-muted" title="Current Bitcoin Address" data-ng-show="vin.addr == $root.currentAddr">{{vin.addr}}</span>
|
||||||
<a href="/address/{{vin.addr}}" data-ng-show="!vin.notAddr && vin.addr != $root.currentAddr">{{vin.addr}}</a>
|
<a href="/address/{{vin.addr}}" data-ng-show="!vin.notAddr && vin.addr != $root.currentAddr">{{vin.addr}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div data-ng-show="vin.unconfirmedInput" class="text-danger"> <span class="glyphicon glyphicon-warning-sign"></span> (Input unconfirmed)</div>
|
||||||
|
<div data-ng-show="vin.dbError" class="text-danger"> <span class="glyphicon glyphicon-warning-sign"></span> Incoherence in levelDB detected, please resync</div>
|
||||||
|
<div data-ng-show="vin.doubleSpendTxID" class="text-danger"> <span class="glyphicon glyphicon-warning-sign"></span> Double spend attempt detected. From tx:
|
||||||
|
<a href="/tx/{{vin.doubleSpendTxID}}">{{vin.doubleSpendTxID}},{{vin.doubleSpendIndex}}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-ng-repeat="vin in tx.vin" data-ng-show="itemsExpanded">
|
<div data-ng-repeat="vin in tx.vin" data-ng-show="itemsExpanded">
|
||||||
|
@ -38,6 +43,12 @@
|
||||||
<span data-ng-show="vin.notAddr">{{vin.addr}}</span>
|
<span data-ng-show="vin.notAddr">{{vin.addr}}</span>
|
||||||
<a href="/address/{{vin.addr}}" data-ng-show="!vin.notAddr">{{vin.addr}}</a>
|
<a href="/address/{{vin.addr}}" data-ng-show="!vin.notAddr">{{vin.addr}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div data-ng-show="vin.unconfirmedInput" class="text-danger"> <span class="glyphicon glyphicon-warning-sign"></span> (Input unconfirmed)</div>
|
||||||
|
<div data-ng-show="vin.dbError" class="text-danger"> <span class="glyphicon glyphicon-warning-sign"></span> Incoherence in levelDB detected, please resync</div>
|
||||||
|
<div data-ng-show="vin.doubleSpendTxID" class="text-danger"> <span class="glyphicon glyphicon-warning-sign"></span> Double spend attempt detected. From tx:
|
||||||
|
<a href="/tx/{{vin.doubleSpendTxID}}">{{vin.doubleSpendTxID}},{{vin.doubleSpendIndex}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -62,17 +73,34 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div data-ng-repeat="vout in tx.voutSimple" data-ng-show="!itemsExpanded">
|
<div data-ng-repeat="vout in tx.voutSimple" data-ng-show="!itemsExpanded">
|
||||||
<div class="col-md-12 transaction-vin-vout">
|
<div class="col-md-12 transaction-vin-vout">
|
||||||
<div class="text-muted pull-right btc-value" data-ng-class="{'text-success': $root.currentAddr == vout.addr}"><small>{{$root.currency.getConvertion(vout.value)}}</small></div>
|
<div class="text-muted pull-right btc-value" data-ng-class="{'text-success': $root.currentAddr == vout.addr}">
|
||||||
|
|
||||||
|
<small>
|
||||||
|
{{$root.currency.getConvertion(vout.value)}}
|
||||||
|
<span class="text-danger" data-ng-show="vout.isSpend" tooltip="Output is spend" tooltip-placement="left">(S)</span>
|
||||||
|
<span class="text-success" data-ng-show="!vout.isSpend" tooltip="Output is unspend" tooltip-placement="left">(U)</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ellipsis">
|
<div class="ellipsis">
|
||||||
<span data-ng-show="vout.notAddr">{{vout.addr}}</span>
|
<span data-ng-show="vout.notAddr">{{vout.addr}}</span>
|
||||||
<span class="text-muted" title="Current Bitcoin Address" data-ng-show="address == $root.currentAddr" data-ng-repeat="address in vout.addr.split(',')">{{vout.addr}}</span>
|
<span class="text-muted" title="Current Bitcoin Address" data-ng-show="address == $root.currentAddr" data-ng-repeat="address in vout.addr.split(',')">{{vout.addr}}</span>
|
||||||
<a href="/address/{{address}}" data-ng-show="!vout.notAddr && address != $root.currentAddr" data-ng-repeat="address in vout.addr.split(',')">{{address}}</a>
|
<a href="/address/{{address}}" data-ng-show="!vout.notAddr && address != $root.currentAddr" data-ng-repeat="address in vout.addr.split(',')">{{address}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-ng-repeat="vout in tx.vout" data-ng-show="itemsExpanded">
|
<div data-ng-repeat="vout in tx.vout" data-ng-show="itemsExpanded">
|
||||||
<div class="col-md-12 transaction-vin-vout">
|
<div class="col-md-12 transaction-vin-vout">
|
||||||
<div class="text-muted pull-right btc-value"><small>{{$root.currency.getConvertion(vout.value)}}</small></div>
|
<div class="text-muted pull-right btc-value"><small>{{$root.currency.getConvertion(vout.value)}}
|
||||||
|
<span class="text-success" data-ng-show="!vout.spendTxId" tooltip="Output is unspend" tooltip-placement="left">(U)</span>
|
||||||
|
<a class="glyphicon glyphicon-chevron-right" data-ng-show="vout.spendTxId" href="/#!/tx/{{vout.spendTxId}}" title="Spent at: {{vout.spendTxId}},{{vout.spendIndex}}"></a>
|
||||||
|
|
||||||
|
|
||||||
|
</small>
|
||||||
|
|
||||||
|
</div>
|
||||||
<div class="ellipsis">
|
<div class="ellipsis">
|
||||||
<a href="/address/{{address}}" data-ng-repeat="address in vout.scriptPubKey.addresses">{{address}}</a>
|
<a href="/address/{{address}}" data-ng-repeat="address in vout.scriptPubKey.addresses">{{address}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -108,7 +136,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="line-top row">
|
<div class="line-top row">
|
||||||
<div class="col-xs-4 col-md-6">
|
<div class="col-xs-4 col-md-6">
|
||||||
<span data-ng-show="!tx.isCoinBase" class="label label-default">Fees: {{$root.currency.getConvertion(tx.fees)}}</span>
|
<span data-ng-show="!tx.isCoinBase && tx.feeds" class="label label-default">Fees: {{$root.currency.getConvertion(tx.fees)}}</span>
|
||||||
|
<span data-ng-show="!tx.isCoinBase && !tx.feeds" class="label label-default">Fees: unknown yet</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-8 col-md-6 text-right">
|
<div class="col-xs-8 col-md-6 text-right">
|
||||||
<span data-ng-show="tx.confirmations" class="label label-success">{{tx.confirmations}} Confirmations</span>
|
<span data-ng-show="tx.confirmations" class="label label-success">{{tx.confirmations}} Confirmations</span>
|
||||||
|
|
Loading…
Reference in New Issue