Merge branch 'master' of github.com:bitpay/mystery into feature/p2p-import

Conflicts:
	app/controllers/transactions.js
	app/models/Transaction.js
	config/env/development.js
	lib/Sync.js
	test/model/block.js
This commit is contained in:
Manuel Araoz 2014-01-10 16:12:11 -03:00
commit ead58e1e52
30 changed files with 689 additions and 144 deletions

View File

@ -1,6 +1,16 @@
'use strict';
module.exports = function(grunt) {
//Load NPM tasks
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-mocha-test');
grunt.loadNpmTasks('grunt-nodemon');
grunt.loadNpmTasks('grunt-concurrent');
grunt.loadNpmTasks('grunt-env');
// Project Configuration
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
@ -29,6 +39,11 @@ module.exports = function(grunt) {
options: {
livereload: true
}
},
test: {
// we monitor only app/models/* because we have test for models only now
files: ['test/**/*.js', 'test/*.js','app/models/*.js'],
tasks: ['test'],
}
},
jshint: {
@ -43,7 +58,7 @@ module.exports = function(grunt) {
options: {
reporter: 'spec',
},
src: ['test/*.js']
src: ['test/**/*.js'],
},
nodemon: {
@ -51,9 +66,9 @@ module.exports = function(grunt) {
options: {
file: 'server.js',
args: [],
ignoredFiles: ['public/**'],
ignoredFiles: ['public/**', 'test/**'],
watchedExtensions: ['js'],
nodeArgs: ['--debug'],
// nodeArgs: ['--debug'],
delayTime: 1,
env: {
PORT: 3000
@ -75,14 +90,6 @@ module.exports = function(grunt) {
}
});
//Load NPM tasks
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-mocha-test');
grunt.loadNpmTasks('grunt-nodemon');
grunt.loadNpmTasks('grunt-concurrent');
grunt.loadNpmTasks('grunt-env');
//Making grunt default to force in order not to break the project.
grunt.option('force', true);

View File

@ -45,26 +45,34 @@ $ npm install -g bower
http://localhost:3000
## Prerequisites
Get bitcore from github repository:
$ git clone https://github.com/bitpay/bitcore.git
$ cd bitcore
$ npm install
Run sync from mystery repository:
$ utils/sync.js
check utils/sync.js --help for options.
## API
A REST API is provided at /api. The entry points are:
### Prerequisites
Get bitcore from github repository:
$ git clone https://github.com/bitpay/bitcore.git
$ cd bitcore
$ npm install
Then create a symbolic link from this to your mystery repository. We need to
use bitcore from github, not with npm for now:
$ cd mystery/node_modules
$ rm -R bitcore
$ ln -s <path-to-your-clone-repositoy>/bitcore
Run sync from mystery repository (to save blocks in MongoDB):
$ utils/sync.js
Check utils/sync.js --help for options.
### Blocks
```
/api/block/[:hash]

View File

@ -29,41 +29,56 @@ exports.show = function(req, res) {
res.jsonp(req.block);
};
/**
* List of blocks at HomePage
*/
exports.last_blocks = function(req, res) {
Block.find().sort({time:-1}).limit(7).exec(function(err, blocks) {
if (err) {
res.render('error', {
status: 500
});
} else {
res.jsonp(blocks);
}
});
};
/**
* List of blocks by date
*/
exports.list = function(req, res) {
var findParam = {};
//helper to convert timestamps to yyyy-mm-dd format
var formatTimestamp = function (date) {
var yyyy = date.getUTCFullYear().toString();
var mm = (date.getUTCMonth() + 1).toString(); // getMonth() is zero-based
var dd = date.getUTCDate().toString();
return yyyy + '-' + (mm[1] ? mm : '0' + mm[0]) + '-' + (dd[1] ? dd : '0' + dd[0]); //padding
};
var dateStr;
if (req.query.blockDate) {
findParam = {};
dateStr = req.query.blockDate;
} else {
dateStr = formatTimestamp(new Date());
}
var gte = Math.round((new Date(dateStr)).getTime() / 1000);
//pagination
var lte = gte + 86400;
var prev = formatTimestamp(new Date((gte - 86400) * 1000));
var next = formatTimestamp(new Date(lte * 1000));
Block
.find(findParam)
.limit(5)
.find({
time: {
'$gte': gte,
'$lte': lte
}
})
.exec(function(err, blocks) {
if (err) {
res.render('error', {
status: 500
});
} else {
res.jsonp(blocks);
res.jsonp({
blocks: blocks,
pagination: {
next: next,
prev: prev,
current: dateStr
}
});
}
});
};

View File

@ -16,7 +16,12 @@ var Transaction = require('../models/Transaction');
*/
exports.transaction = function(req, res, next, txid) {
Transaction.fromIdWithInfo(txid, function(err, tx) {
if (err) return next(err);
if (err) {
console.log(err);
res.status(404).send('Not found');
return next();
}
if (!tx) return next(new Error('Failed to load TX ' + txid));
req.transaction = tx.info;
next();
@ -25,9 +30,11 @@ exports.transaction = function(req, res, next, txid) {
/**
* Show block
*/
exports.show = function(req, res) {
res.jsonp(req.transaction);
if (req.transaction) {
res.jsonp(req.transaction);
}
};

View File

@ -57,6 +57,7 @@ BlockSchema.statics.fromHash = function(hash, cb) {
BlockSchema.statics.fromHashWithInfo = function(hash, cb) {
this.fromHash(hash, function(err, block) {
if (err) return cb(err);
if (!block) { return cb(new Error('Block not found')); }
block.getInfo(function(err) { return cb(err,block); } );
});

View File

@ -3,12 +3,18 @@
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
async = require('async'),
RpcClient = require('bitcore/RpcClient').class(),
config = require('../../config/config');
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
async = require('async'),
RpcClient = require('bitcore/RpcClient').class(),
Transaction = require('bitcore/Transaction').class(),
Address = require('bitcore/Address').class(),
networks = require('bitcore/networks'),
util = require('bitcore/util/util'),
bignum = require('bignum'),
config = require('../../config/config');
/**
*/
@ -41,10 +47,15 @@ TransactionSchema.statics.fromId = function(txid, cb) {
};
TransactionSchema.statics.fromIdWithInfo = function(txid, cb) {
// TODO Should we go to mongoDB first? Now, no extra information is stored at mongo.
this.fromId(txid, function(err, tx) {
if (err) return cb(err);
tx.getInfo(function(err) { return cb(err,tx); } );
if (!tx) { return cb(new Error('TX not found')); }
tx.queryInfo(function(err) { return cb(err,tx); } );
});
};
@ -68,18 +79,114 @@ TransactionSchema.statics.createFromArray = function(txs, next) {
};
TransactionSchema.methods.fillInputValues = function (tx, next) {
TransactionSchema.methods.getInfo = function (next) {
if (tx.isCoinBase()) return next();
if (! this.rpc) this.rpc = new RpcClient(config.bitcoind);
var that = this;
var rpc = new RpcClient(config.bitcoind);
async.each(tx.ins, function(i, cb) {
rpc.getRawTransaction(this.txid, 1, function(err, txInfo) {
var outHash = i.getOutpointHash();
var outIndex = i.getOutpointIndex();
var outHashBase64 = outHash.reverse().toString('hex');
var c=0;
that.rpc.getRawTransaction(outHashBase64, function(err, txdata) {
var txin = new Transaction();
if (err || ! txdata.result) return cb( new Error('Input TX '+outHashBase64+' not found'));
var b = new Buffer(txdata.result,'hex');
txin.parse(b);
if ( txin.isCoinBase() ) {
return cb();
}
txin.outs.forEach( function(j) {
// console.log( c + ': ' + util.formatValue(j.v) );
if (c === outIndex) {
i.value = j.v;
}
c++;
});
return cb();
});
},
function(err) {
return next(err);
}
);
};
TransactionSchema.methods.queryInfo = function (next) {
var that = this;
var network = ( config.network === 'testnet') ? networks.testnet : networks.livenet ;
this.rpc = new RpcClient(config.bitcoind);
this.rpc.getRawTransaction(this.txid, 1, function(err, txInfo) {
if (err) return next(err);
that.info = txInfo.result;
//console.log("THAT", that);
return next(null, that.info);
// Transaction parsing
var b = new Buffer(txInfo.result.hex,'hex');
var tx = new Transaction();
tx.parse(b);
that.fillInputValues(tx, function(err) {
// Copy TX relevant values to .info
var c = 0;
var valueIn = bignum(0);
var valueOut = bignum(0);
if ( tx.isCoinBase() ) {
that.info.isCoinBase = true;
}
else {
tx.ins.forEach(function(i) {
that.info.vin[c].value = util.formatValue(i.value);
var n = util.valueToBigInt(i.value).toNumber();
valueIn = valueIn.add( n );
var scriptSig = i.getScript();
var pubKey = scriptSig.simpleInPubKey();
var pubKeyHash = util.sha256ripe160(pubKey);
var addr = new Address(network.addressPubkey, pubKeyHash);
var addrStr = addr.toString();
that.info.vin[c].addr = addrStr;
c++;
});
}
tx.outs.forEach( function(i) {
var n = util.valueToBigInt(i.v).toNumber();
valueOut = valueOut.add(n);
});
that.info.valueOut = valueOut / util.COIN;
if ( !tx.isCoinBase() ) {
that.info.valueIn = valueIn / util.COIN;
that.info.feeds = (valueIn - valueOut) / util.COIN;
}
that.info.size = b.length;
return next(err, that.info);
});
});
};

View File

@ -1,7 +1,7 @@
#footer
.container
p.text-muted Place sticky footer content here.
//script(type='text/javascript', src='/lib/jquery/jquery.min.js')
//script(type='text/javascript', src='/lib/bootstrap/dist/js/bootstrap.min.js')
@ -10,6 +10,8 @@ script(type='text/javascript', src='/lib/angular/angular.js')
script(type='text/javascript', src='/lib/angular-cookies/angular-cookies.js')
script(type='text/javascript', src='/lib/angular-resource/angular-resource.js')
script(type='text/javascript', src='/lib/angular-route/angular-route.js')
script(type='text/javascript', src='/lib/qrcode-generator/js/qrcode.js')
script(type='text/javascript', src='/lib/angular-qrcode/qrcode.js')
//Angular UI
script(type='text/javascript', src='/lib/angular-bootstrap/ui-bootstrap.js')
@ -23,6 +25,7 @@ script(type='text/javascript', src='/js/directives.js')
script(type='text/javascript', src='/js/filters.js')
//Application Services
script(type='text/javascript', src='/js/services/transactions.js')
script(type='text/javascript', src='/js/services/blocks.js')
script(type='text/javascript', src='/js/services/global.js')
script(type='text/javascript', src='/js/services/index.js')
@ -31,4 +34,6 @@ script(type='text/javascript', src='/js/services/index.js')
script(type='text/javascript', src='/js/controllers/index.js')
script(type='text/javascript', src='/js/controllers/header.js')
script(type='text/javascript', src='/js/controllers/blocks.js')
script(type='text/javascript', src='/js/controllers/transactions.js')
script(type='text/javascript', src='/js/controllers/address.js')
script(type='text/javascript', src='/js/init.js')

View File

@ -9,6 +9,7 @@
"angular-route": "latest",
"bootstrap": "3.0.3",
"angular-bootstrap": "0.9.0",
"angular-ui-utils": "0.1.0"
"angular-ui-utils": "0.1.0",
"angular-qrcode": "latest"
}
}
}

View File

@ -11,5 +11,6 @@ module.exports = {
protocol: 'http',
host: process.env.BITCOIND_HOST || '127.0.0.1',
port: process.env.BITCOIND_PORT || '18332',
}
},
network: 'testnet',
}

15
config/env/test.js vendored
View File

@ -1,9 +1,16 @@
'use strict';
module.exports = {
db: "mongodb://localhost/mystery-test",
port: 3001,
db: "mongodb://localhost/mystery-dev",
app: {
name: "Mystery - Test"
}
}
},
bitcoind: {
user: 'mystery',
pass: 'real_mystery',
protocol: 'http',
host: process.env.BITCOIND_HOST || '127.0.0.1',
port: process.env.BITCOIND_PORT || '8332',
},
network: 'testnet',
}

View File

@ -9,12 +9,13 @@ module.exports = function(app) {
//Block routes
var blocks = require('../app/controllers/blocks');
app.get('/api/blocks', blocks.list);
app.get('/api/block/:blockHash', blocks.show);
app.param('blockHash', blocks.block);
app.get('/last_blocks', blocks.last_blocks);
var transactions = require('../app/controllers/transactions');
app.get('/tx/:txid', transactions.show);
app.get('/api/tx/:txid', transactions.show);
app.param('txid', transactions.transaction);

View File

@ -4,7 +4,8 @@ server=1
txindex=1
# Allow connections outsite localhost?
# rpcallowip=192.168.0.*
rpcallowip=192.168.1.*
rpcallowip='192.168.1.*'

View File

@ -43,6 +43,7 @@
"async": "*",
"classtool": "*",
"commander": "*",
"bignum": "*",
"express": "~3.4.7",
"jade": "~1.0.2",
"mongoose": "~3.8.3",

View File

@ -40,6 +40,11 @@ body {
padding-right: 15px;
}
code {
.code {
font-size: 80%;
}
.address {
font-size: 10px;
}

View File

@ -1,7 +1,9 @@
'use strict';
angular.module('mystery', ['ngCookies', 'ngResource', 'ngRoute', 'ui.bootstrap', 'ui.route', 'mystery.system', 'mystery.index', 'mystery.blocks']);
angular.module('mystery', ['ngCookies', 'ngResource', 'ngRoute', 'ui.bootstrap', 'ui.route', 'mystery.system', 'mystery.index', 'mystery.blocks', 'mystery.transactions', 'monospaced.qrcode', 'mystery.address']);
angular.module('mystery.system', []);
angular.module('mystery.index', []);
angular.module('mystery.blocks', []);
angular.module('mystery.transactions', []);
angular.module('mystery.address', []);

View File

@ -7,6 +7,9 @@ angular.module('mystery').config(['$routeProvider',
when('/block/:blockHash', {
templateUrl: 'views/block.html'
}).
when('/tx/:txId', {
templateUrl: 'views/transaction.html'
}).
when('/', {
templateUrl: 'views/index.html'
}).
@ -14,7 +17,10 @@ angular.module('mystery').config(['$routeProvider',
templateUrl: 'views/blocks/list.html'
}).
when('/blocks-date/:blockDate', {
templateUrl: 'views/blocks/list_date.html'
templateUrl: 'views/blocks/list.html'
}).
when('/address/:address', {
templateUrl: 'views/address.html'
}).
otherwise({
redirectTo: '/'

View File

@ -0,0 +1,20 @@
'use strict';
angular.module('mystery.address').controller('AddressController', ['$scope', function ($scope) {
//example data
$scope.address = '1JmTTDcksW7A6GN7JnxuXkMAXsVN9zmgm1';
$scope.hash160 = '77ad7d08aaa9cf489ea4e468eaeb892b85f71e27';
$scope.transactions = [
{
hash: '49a1d01759690476dbeec4a8efd969c09c6d4269ea2d88f4d9d4f098f021413c',
time: 1234123445,
amount: 0.3
},
{
hash: 'cce948b422a4d485900fb82e64458720eb89f545af3f07ddf7d18660f9f881e9',
time: 1234123445,
amount: 0.1
}
];
}]);

View File

@ -3,17 +3,12 @@
angular.module('mystery.blocks').controller('BlocksController', ['$scope', '$routeParams', '$location', 'Global', 'Block', 'Blocks', function ($scope, $routeParams, $location, Global, Block, Blocks) {
$scope.global = Global;
$scope.list_blocks = function() {
Blocks.query(function(blocks) {
$scope.blocks = blocks;
});
};
$scope.list_blocks_date = function() {
Blocks.query({
$scope.list = function() {
Blocks.get({
blockDate: $routeParams.blockDate
}, function(blocks) {
$scope.blocks = blocks;
}, function(res) {
$scope.blocks = res.blocks;
$scope.pagination = res.pagination;
});
};

View File

@ -2,9 +2,5 @@
angular.module('mystery.system').controller('IndexController', ['$scope', 'Global', 'Index', function ($scope, Global, Index) {
$scope.global = Global;
$scope.last_blocks = function() {
Index.query(function(blocks) {
$scope.blocks = blocks;
});
};
$scope.index = Index;
}]);

View File

@ -0,0 +1,14 @@
'use strict';
angular.module('mystery.transactions').controller('transactionsController', ['$scope', '$routeParams', '$location', 'Global', 'Transaction', function ($scope, $routeParams, $location, Global, Transaction) {
$scope.global = Global;
$scope.findOne = function() {
Transaction.get({
txId: $routeParams.txId
}, function(tx) {
$scope.tx = tx;
});
};
}]);

View File

@ -1,5 +1,5 @@
'use strict';
angular.module('mystery.index').factory('Index', ['$resource', function($resource) {
return $resource('/last_blocks');
return $resource;
}]);

View File

@ -0,0 +1,8 @@
'use strict';
angular.module('mystery.transactions').factory('Transaction', ['$resource', function($resource) {
return $resource('/api/tx/:txId', {
txId: '@txId'
});
}]);

60
public/views/address.html Normal file
View File

@ -0,0 +1,60 @@
<section data-ng-controller="AddressController">
<div class="page-header">
<h1>
Address
<small>{{address}}</small>
</h1>
</div>
<div class="col-lg-9">
<table class="table table-striped">
<tbody>
<tr>
<td>Address</td>
<td><a href="#!/address/{{address}}">{{address}}</a></td>
</tr>
<tr>
<td>Hash160</td>
<td><a href="#!/address/{{hash160}}">{{hash160}}</a></td>
</tr>
<tr>
<td>Total Output</td>
<td>1 BTC</td>
</tr>
<tr>
<td>Total Input</td>
<td>0.2 BTC</td>
</tr>
<tr>
<td>Current balance</td>
<td>10.2 BTC</td>
</tr>
</tbody>
</table>
</div>
<div class="col-lg-3">
<qrcode size="200" data="{{address}}"></qrcode>
</div>
<div class="col-lg-12">
<h3>
Transactions
<small>transactions this address relates to</small>
</h3>
<table class="table table-striped">
<thead>
<tr>
<th>Transaction Hash</th>
<th>Datetime</th>
<th>Transacted amount</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="transaction in transactions">
<td><a href="#!/tx/{{transaction.hash}}">{{transaction.hash}}</a></td>
<td>{{transaction.time | date:'medium'}}</td>
<td>{{transaction.amount}} BTC</td>
</tr>
</tbody>
</table>
</div>
</section>

View File

@ -1,23 +1,114 @@
<section data-ng-controller="BlocksController" data-ng-init="findOne()">
<div class="page-header">
<h1>Block Page</h1>
<h1 data-ng-if="block">Block #{{ block.height }}</h1>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Summary</h3>
</div>
<div class="panel-body">
<table class="table table-striped">
<tbody>
<tr>
<td>Number Of Transactions</td>
<td>--</td>
</tr>
<tr>
<td>Output Total</td>
<td>--</td>
</tr>
<tr>
<td>Estimated Transaction Volume</td>
<td>--</td>
</tr>
<tr>
<td>Transaction Fees</td>
<td>--</td>
</tr>
<tr>
<td>Height</td>
<td>{{block.height}}</td>
</tr>
<tr>
<td>Timestamp</td>
<td>{{block.time * 1000 | date:'medium'}}</td>
</tr>
<tr>
<td>Received Time</td>
<td>--<td>
</tr>
<tr>
<td>Relayed By</td>
<td>--</td>
</tr>
<tr>
<td>Difficulty</td>
<td>{{block.difficulty}}</td>
</tr>
<tr>
<td>Bits</td>
<td>{{block.bits}}</td>
</tr>
<tr>
<td>Size</td>
<td>{{block.size}}</td>
</tr>
<tr>
<td>Version</td>
<td>{{block.version}}</td>
</tr>
<tr>
<td>Nonce</td>
<td>{{block.nonce}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default" data-ng-show="!tx.isCoinBase">
<div class="panel-heading">
<h3 class="panel-title">Hashes</h3>
</div>
<div class="panel-body">
<table class="table table-striped">
<tbody>
<tr>
<td>Hash</td>
<td><a class="address" href="/#!/block/{{block.hash}}">{{block.hash}}</a></td>
</tr>
<tr>
<td>Previous Block</td>
<td><a class="address" href="/#!/block/{{block.previousblockhash}}">{{block.previousblockhash}}</a></td>
</tr>
<tr>
<td>Next Block</td>
<td><a class="address" href="/#!/block/{{block.nextblockhash}}">{{block.nextblockhash}}</a></td>
</tr>
<tr>
<td>Merkle Root</td>
<td class="address">{{block.merkleroot}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<h2>Transactions <small>Transactions contained within this block</small></h2>
<table class="table table-striped">
<thead>
<th>Height</th>
<th>Age</th>
<th>Transactions</th>
<th>Confirmations</th>
<th>Size (kB)</th>
<th>Hash</th>
</thead>
<tbody>
<tr>
<td>{{block.height}}</td>
<td>{{block.time | date:'short'}}</td>
<td>{{block.tx.length }}</td>
<td>{{block.confirmations}}</td>
<td>{{block.size / 1024}}</td>
</tr>
<tr data-ng-repeat="tx in block.tx">
<td><a href="/#!/tx/{{tx}}">{{tx}}</a></td>
</tr>
</tbody>
</table>
</section>
</section>

View File

@ -1,10 +1,26 @@
<section data-ng-controller="BlocksController" data-ng-init="list_blocks()">
<section data-ng-controller="BlocksController" data-ng-init="list()">
<div class="page-header">
<h1>Blocks by Date</h1>
<h1>
<span class="glyphicon glyphicon-calendar"></span>
Blocks
<small>by date</small>
</h1>
<ul class="pagination">
<li><a href="#!/blocks-date/{{pagination.prev}}">&laquo; {{pagination.prev}}</a></li>
<li class="disabled"><a href="#">{{pagination.current}}</a></li>
<li><a href="#!/blocks-date/{{pagination.next}}">{{pagination.next}} &raquo;</a></li>
</ul>
</div>
<ul>
<li data-ng-repeat="block in blocks">
<span>{{block.hash}}</span> {{block.time}}
</li>
</ul>
</section>
<table class="table table-striped">
<thead>
<th>Hash</th>
<th>Solved at</th>
</thead>
<tbody>
<tr data-ng-repeat="block in blocks">
<td><a href="#!/block/{{block.hash}}">{{block.hash}}</a></td>
<td>{{block.time * 1000 | date:'medium'}}</td>
</tr>
</tbody>
</table>
</section>

View File

@ -1,10 +0,0 @@
<section data-ng-controller="BlocksController" data-ng-init="list_blocks_date()">
<div class="page-header">
<h1>Blocks by defined date</h1>
</div>
<ul>
<li data-ng-repeat="block in blocks">
<span>{{block.hash}}</span> {{block.time}}
</li>
</ul>
</section>

View File

@ -1,23 +1,8 @@
<section data-ng-controller="IndexController" data-ng-init="last_blocks()">
<div class="page-header">
<h1>Hello BitPay!</h1>
<div class="jumbotron">
<h1>Hello, BitPay!</h1>
<p>Start here for blocks information</p>
<p><a href="/#!/blocks" class="btn btn-primary btn-lg">List all blocks</a></p>
</div>
<table class="table table-striped">
<thead>
<th>Height</th>
<th>Age</th>
<th>Transactions</th>
<th>Confirmations</th>
<th>Size (kB)</th>
</thead>
<tbody>
<tr data-ng-repeat="block in blocks">
<td><a href="#!/block/{{block.hash}}">{{block.height}}</a></td>
<td>{{block.time | date:'short'}}</td>
<td>{{block.tx.length }}</td>
<td>{{block.confirmations}}</td>
<td>{{block.size / 1024}}</td>
</tr>
</tbody>
</table>
</section>

View File

@ -0,0 +1,104 @@
<section data-ng-controller="transactionsController" data-ng-init="findOne()">
<div class="page-header">
<h1>
Transaction
<small>View information about a bitcoin transaction</small>
</h1>
</div>
<div class="well well-sm">
{{tx.txid}}
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Input</th>
<th>&nbsp;</th>
<th>Output</th>
</tr>
</thead>
<tbody>
<tr>
<td width="45%">
<ul class="list-unstyled" data-ng-repeat="vin in tx.vin" data-ng-show="!tx.isCoinBase">
<li><a href="/#!/address/{{vin.addr}}">{{vin.addr}}</a> <span class="pull-right badge">{{vin.value}} BTC</span></li>
</ul>
<div data-ng-show="tx.isCoinBase">
No Inputs (Newly Generated isCoinBasens)
</div>
</td>
<td width="10%" style="text-align: center;"><span class="glyphicon glyphicon-chevron-right">&nbsp;</span></td>
<td width="45%">
<div data-ng-repeat="vout in tx.vout">
<ul class="list-unstyled" data-ng-repeat="addr in vout.scriptPubKey.addresses">
<li><a href="/#!/address/{{addr}}">{{addr}}</a> <span class="pull-right badge">{{vout.value}} BTC</span></li>
</ul>
</div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3" style="text-align: right;">
<button type="button" class="btn btn-primary">{{tx.confirmations}} Confirmations</button>
<button type="button" class="btn btn-success">{{tx.valueOut}} BTC</button>
</td>
</tr>
</tfoot>
</table>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Summary</h3>
</div>
<div class="panel-body">
<table class="table table-striped">
<tbody>
<tr>
<td>Size</td>
<td>{{tx.size}} (bytes)</td>
</tr>
<tr>
<td>Received Time</td>
<td>{{tx.time * 1000|date:'medium'}}</td>
</tr>
<tr>
<td>Reward From Block</td>
<td><a href="/#!/block/{{tx.blockhash}}">Need height to show (it links to block page)</a></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default" data-ng-show="!tx.isCoinBase">
<div class="panel-heading">
<h3 class="panel-title">Inputs and Outputs</h3>
</div>
<div class="panel-body">
<table class="table table-striped">
<tbody>
<tr>
<td>Total Input</td>
<td>{{tx.valueIn}} BTC</td>
</tr>
<tr>
<td>Total Output</td>
<td>{{tx.valueOut}} BTC</td>
</tr>
<tr>
<td>Fees</td>
<td>{{tx.feeds}} BTC</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</section>

View File

@ -3,7 +3,7 @@
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
var TESTING_BLOCK = '0000000000b6288775bbd326bedf324ca8717a15191da58391535408205aada4';
var TESTING_BLOCK = '000000000185678d3d7ecc9962c96418174431f93fe20bf216d5565272423f74';
var
mongoose= require('mongoose'),
@ -14,12 +14,17 @@ var
mongoose.connection.on('error', function(err) { console.log(err); });
<<<<<<< HEAD
describe('Block getInfo', function(){
=======
describe('Block fromHashWithInfo', function(){
>>>>>>> fd86e6d074c5aa4642172b221b9e6f69f3fd8634
before(function(done) {
mongoose.connect(config.db);
done();
});
<<<<<<< HEAD
after(function(done) {
mongoose.connection.close();
@ -34,12 +39,34 @@ describe('Block getInfo', function(){
done();
});
});
=======
after(function(done) {
mongoose.connection.close();
done();
});
it('should poll block\'s info from mongoose', function(done) {
var block2 = Block.fromHashWithInfo(TESTING_BLOCK, function(err, b2) {
if (err) done(err);
assert.equal(b2.hash, TESTING_BLOCK);
done();
});
});
>>>>>>> fd86e6d074c5aa4642172b221b9e6f69f3fd8634
it('should poll block\'s info from bitcoind', function(done) {
var block2 = Block.fromHashWithInfo(TESTING_BLOCK, function(err, b2) {
if (err) done(err);
assert.equal(b2.info.hash, TESTING_BLOCK);
<<<<<<< HEAD
assert.equal(b2.info.chainwork, '00000000000000000000000000000000000000000000000000446af21d50acd3');
=======
assert.equal(b2.info.chainwork, '000000000000000000000000000000000000000000000000001b6dc969ffe847');
>>>>>>> fd86e6d074c5aa4642172b221b9e6f69f3fd8634
done();
});
});

64
test/model/transaction.js Normal file
View File

@ -0,0 +1,64 @@
#!/usr/bin/env node
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
var
mongoose= require('mongoose'),
assert = require('assert'),
config = require('../../config/config'),
Transaction = require('../../app/models/Transaction');
mongoose.connection.on('error', function(err) { console.log(err); });
describe('Transaction fromIdWithInfo', function(){
before(function(done) {
mongoose.connect(config.db);
done();
});
after(function(done) {
mongoose.connection.close();
done();
});
it('should pool tx\'s object from mongoose', function(done) {
var test_txid = '21798ddc9664ac0ef618f52b151dda82dafaf2e26d2bbef6cdaf55a6957ca237';
Transaction.fromIdWithInfo(test_txid, function(err, tx) {
if (err) done(err);
assert.equal(tx.txid, test_txid);
assert(!tx.info.isCoinBase);
done();
});
});
it('should pool tx\'s info from bitcoind', function(done) {
var test_txid = '21798ddc9664ac0ef618f52b151dda82dafaf2e26d2bbef6cdaf55a6957ca237';
Transaction.fromIdWithInfo(test_txid, function(err, tx) {
if (err) done(err);
assert.equal(tx.info.txid, test_txid);
assert.equal(tx.info.blockhash, '000000000185678d3d7ecc9962c96418174431f93fe20bf216d5565272423f74');
assert.equal(tx.info.valueOut, 1.66174);
assert.equal(tx.info.feeds, 0.0005 );
assert.equal(tx.info.size, 226 );
assert(!tx.info.isCoinBase);
done();
});
});
it('test a coinbase TX', function(done) {
var test_txid2 = '2a104bab1782e9b6445583296d4a0ecc8af304e4769ceb64b890e8219c562399';
Transaction.fromIdWithInfo(test_txid2, function(err, tx) {
if (err) done(err);
assert(tx.info.isCoinBase);
assert.equal(tx.info.txid, test_txid2);
assert(!tx.info.feeds);
done();
});
});
});