Merge pull request #3 from matiu/master

master to master
This commit is contained in:
Matias Alejo Garcia 2014-01-08 08:11:34 -08:00
commit 2962c4e30f
54 changed files with 1929 additions and 3 deletions

3
.bowerrc Normal file
View File

@ -0,0 +1,3 @@
{
"directory": "public/lib"
}

21
.editorconfig Normal file
View File

@ -0,0 +1,21 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 2
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

10
.gitignore vendored
View File

@ -1,5 +1,4 @@
# from https://github.com/github/gitignore/blob/master/Node.gitignore
lib-cov
*.seed
*.log
@ -8,6 +7,7 @@ lib-cov
*.out
*.pid
*.gz
*.swp
pids
logs
@ -20,4 +20,12 @@ node_modules
*.swp
*~
.project
peerdb.json
npm-debug.log
node_modules
.nodemonignore
.DS_Store
public/lib/*

40
.jshintrc Normal file
View File

@ -0,0 +1,40 @@
{
"node": true, // Enable globals available when code is running inside of the NodeJS runtime environment.
"browser": true, // Standard browser globals e.g. `window`, `document`.
"esnext": true, // Allow ES.next specific features such as `const` and `let`.
"bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.).
"camelcase": false, // Permit only camelcase for `var` and `object indexes`.
"curly": false, // Require {} for every new block or scope.
"eqeqeq": true, // Require triple equals i.e. `===`.
"immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
"latedef": true, // Prohibit variable use before definition.
"newcap": true, // Require capitalization of all constructor functions e.g. `new F()`.
"noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`.
"quotmark": "single", // Define quotes to string values.
"regexp": true, // Prohibit `.` and `[^...]` in regular expressions.
"undef": true, // Require all non-global variables be declared before they are used.
"unused": true, // Warn unused variables.
"strict": true, // Require `use strict` pragma in every file.
"trailing": true, // Prohibit trailing whitespaces.
"smarttabs": false, // Suppresses warnings about mixed tabs and spaces
"globals": { // Globals variables.
"angular": true
},
"predef": [ // Extra globals.
"define",
"require",
"exports",
"module",
"describe",
"before",
"beforeEach",
"after",
"afterEach",
"it",
"inject",
"expect"
],
"indent": 2, // Specify indentation spacing
"devel": true, // Allow development statements e.g. `console.log();`.
"noempty": true // Prohibit use of empty blocks.
}

94
Gruntfile.js Normal file
View File

@ -0,0 +1,94 @@
'use strict';
module.exports = function(grunt) {
// Project Configuration
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
watch: {
jade: {
files: ['app/views/**'],
options: {
livereload: true,
},
},
js: {
files: ['Gruntfile.js', 'server.js', 'app/**/*.js', 'public/js/**'],
tasks: ['jshint'],
options: {
livereload: true,
},
},
html: {
files: ['public/views/**'],
options: {
livereload: true,
},
},
css: {
files: ['public/css/**'],
options: {
livereload: true
}
}
},
jshint: {
all: {
src: ['Gruntfile.js', 'server.js', 'app/**/*.js', 'public/js/**'],
options: {
jshintrc: true
}
}
},
mochaTest: {
options: {
reporter: 'spec',
},
src: ['test/*.js']
},
nodemon: {
dev: {
options: {
file: 'server.js',
args: [],
ignoredFiles: ['public/**'],
watchedExtensions: ['js'],
nodeArgs: ['--debug'],
delayTime: 1,
env: {
PORT: 3000
},
cwd: __dirname
}
}
},
concurrent: {
tasks: ['nodemon', 'watch'],
options: {
logConcurrentOutput: true
}
},
env: {
test: {
NODE_ENV: 'test'
}
}
});
//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);
//Default task(s).
grunt.registerTask('default', ['jshint','concurrent']);
//Test task.
grunt.registerTask('test', ['env:test', 'mochaTest']);
};

213
HeaderDB.js Normal file
View File

@ -0,0 +1,213 @@
require('classtool');
function ClassSpec(b) {
var assert = require('assert');
var fs = require('fs');
var Block = require('bitcore/Block').class();
var Deserialize = require('bitcore/Deserialize');
var Parser = require('bitcore/util/BinaryParser').class();
var coinUtil = require('bitcore/util/util');
function HeaderDB(b) {
this.network = b.network;
this.fd = null;
this.blocks = {};
this.byHeight = [];
this.bestBlock = null;
this.cached_size = 0;
}
HeaderDB.prototype.size = function() {
this.cached_size = Object.keys(this.blocks).length;
return this.cached_size;
};
HeaderDB.prototype.locator = function(block) {
if (!block)
block = this.bestBlock;
var step = 1;
var start = 0;
var loc = [];
// see https://en.bitcoin.it/wiki/Protocol_specification#getblocks
for (var i = block.height; i > 0; i -= step, ++start) {
if (start >= 10)
step *= 2;
loc.push(this.byHeight[i]);
}
assert.equal(this.byHeight[0].toString(),
this.network.genesisBlock.hash.toString());
loc.push(this.byHeight[0]);
console.log("Requesting more headers. Current height: " + block.height );
return loc;
};
HeaderDB.prototype.add = function(block) {
var hash = block.calcHash();
block.hash = hash;
var curWork = Deserialize.intFromCompact(block.bits);
if (hash in this.blocks) {
var old = this.blocks[hash];
throw new Error("duplicate block (was at height " + old.height + ")");
}
var bestChain = false;
var reorg = {
oldBest: null,
conn: 0,
disconn: 0,
};
if (this.cached_size == 0) {
if (this.network.genesisBlock.hash.toString() !=
hash.toString())
throw new Error("Invalid genesis block");
block.height = 0;
block.work = curWork;
bestChain = true;
this.cached_size++;
} else {
var prevBlock = this.blocks[block.prev_hash];
if (!prevBlock)
throw new Error("orphan block; prev not found");
block.height = prevBlock.height + 1;
block.work = prevBlock.work + curWork;
this.cached_size++;
if (block.work > this.bestBlock.work)
bestChain = true;
}
// add to by-hash index
this.blocks[hash] = block;
if (bestChain) {
var oldBest = this.bestBlock;
var newBest = block;
reorg.oldBest = oldBest;
// likely case: new best chain has greater height
if (!oldBest) {
while (newBest) {
newBest = this.blocks[newBest.prev_hash];
reorg.conn++;
}
} else {
while (newBest &&
(newBest.height > oldBest.height)) {
newBest = this.blocks[newBest.prev_hash];
reorg.conn++;
}
}
// unlikely: old best chain has greater height
while (oldBest && newBest &&
(oldBest.height > newBest.height)) {
oldBest = this.blocks[oldBest.prev_hash];
reorg.disconn++;
}
// height matches, but still walking parallel
while (oldBest && newBest && (oldBest != newBest)) {
newBest = this.blocks[newBest.prev_hash];
reorg.conn++;
oldBest = this.blocks[oldBest.prev_hash];
reorg.disconn++;
}
var shuf = (reorg.conn > reorg.disconn) ?
reorg.conn : reorg.disconn;
// reorg analyzed, updated best-chain pointer
this.bestBlock = block;
// update by-height index
var ptr = block;
var updated = [];
for (var idx = block.height;
idx > (block.height - shuf); idx--) {
if (idx < 0)
break;
var update = [ idx, ptr ];
updated.push(update);
ptr = this.blocks[ptr.prev_hash];
}
updated.reverse();
for (var i = 0; i < updated.length; i++) {
var update = updated[i];
var idx = update[0];
var ptr = update[1];
if (idx < this.byHeight.length)
this.byHeight[idx] = ptr.hash;
else
this.byHeight.push(ptr.hash);
}
}
return reorg;
};
HeaderDB.prototype.addBuf = function(buf) {
var block = new Block();
var parser = new Parser(buf);
block.parse(parser, true);
this.add(block);
};
HeaderDB.prototype.readFile = function(filename) {
var fd = fs.openSync(filename, 'r');
var stats = fs.fstatSync(fd);
if (stats.size % 80 != 0)
throw new Error("Corrupted header db");
while (1) {
var buf = new Buffer(80);
var bread = fs.readSync(fd, buf, 0, 80, null);
if (bread < 80)
break;
this.addBuf(buf);
if ( ! ( this.cached_size % 1000 )) {
console.log("\tblock..." + this.cached_size ) ;
}
}
fs.closeSync(fd);
};
HeaderDB.prototype.writeFile = function(filename) {
var block = this.bestBlock;
var data = [];
while (block) {
var s = block.getHeader();
data.push(s);
block = this.blocks[block.prev_hash];
}
data.reverse();
var fd = fs.openSync(filename, 'w');
data.forEach(function(datum) {
fs.writeSync(fd, datum, 0, 80, null);
});
fs.closeSync(fd);
};
return HeaderDB;
};
module.defineClass(ClassSpec);

137
README.md
View File

@ -1,2 +1,135 @@
mystery
=======
# Mystery
Project description.
## Prerequisites
* Node.js - Download and Install [Node.js](http://www.nodejs.org/download/). You can also follow [this gist](https://gist.github.com/isaacs/579814) for a quick and easy way to install Node.js and npm
* MongoDB - Download and Install [MongoDB](http://www.mongodb.org/downloads) - Make sure it's running on the default port (27017).
### Tools Prerequisites
* NPM - Node.js package manager, should be installed when you install node.js.
* Grunt - Download and Install [Grunt](http://gruntjs.com).
* Bower - Web package manager, installing [Bower](http://bower.io/) is simple when you have npm:
```
$ npm install -g bower
```
## Additional Packages
* Express - Defined as npm module in the [package.json](package.json) file.
* Mongoose - Defined as npm module in the [package.json](package.json) file.
* AngularJS - Defined as bower module in the [bower.json](bower.json) file.
* Twitter Bootstrap - Defined as bower module in the [bower.json](bower.json) file.
* UI Bootstrap - Defined as bower module in the [bower.json](bower.json) file.
## Quick Install
The quickest way to get started with MEAN is to clone the project and utilize it like this:
Grunt Command Line Interface:
$ sudo npm -g install grunt-cli
Install dependencies:
$ npm install
We use [Grunt](https://github.com/gruntjs/grunt-cli) to start the server:
$ grunt
When not using grunt you can use (for example in production):
$ node server
Then open a browser and go to:
http://localhost:3000
## API
## 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.
### Blocks
```
/block/[:hash]
/block/00000000a967199a2fad0877433c93df785a8d8ce062e5f9b451cd1397bdbf62
```
### Transactions
```
/tx/[:txid]
/tx/525de308971eabd941b139f46c7198b5af9479325c2395db7f2fb5ae8562556c
```
## Troubleshooting
If you did not get all library during grunt command, please use the follow command:
$ bower install
## Configuration
All configuration is specified in the [config](config/) folder, particularly the [config.js](config/config.js) file and the [env](config/env/) files. Here you will need to specify your application name and database name.
### bitcoind
There is a bitcoind configuration sample at:
```
etc/mystery/bitcoin.conf
```
If you want to use a external bitcoind server set BITCOIND_HOST / BITCOIND_PORT enviroment variables. Make sure that bitcoind is configured to accept incomming connections using 'rpcallowip' decribed in https://en.bitcoin.it/wiki/Running_Bitcoin.
### Environmental Settings
There are three environments provided by default, __development__, __test__, and __production__. Each of these environments has the following configuration options:
* __db__ - This is the name of the MongoDB database to use, and is set by default to __mystery-dev__ for the development environment.
* __app.name__ - This is the name of your app or website, and can be different for each environment. You can tell which environment you are running by looking at the TITLE attribute that your app generates.
To run with a different environment, just specify NODE_ENV as you call grunt:
$ NODE_ENV=test grunt
If you are using node instead of grunt, it is very similar:
$ NODE_ENV=test node server
## Github
[Mystery](https://github.com/bitpay/mystery)
## License
(The MIT License)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

219
Sync.js Normal file
View File

@ -0,0 +1,219 @@
require('classtool');
function spec(b) {
var mongoose = require('mongoose');
var util = require('util');
var RpcClient = require('bitcore/RpcClient').class();
var networks = require('bitcore/networks');
var async = require('async');
var config = require('./config/config');
var Block = require('./app/models/Block');
var Transaction=require('./app/models/Transaction');
function Sync(config) {
this.network = config.networkName == 'testnet' ? networks.testnet : networks.livenet;
}
var progress_bar = function(string, current, total) {
console.log( util.format("\t%s %d/%d [%d%%]",
string, current, total, parseInt(100 * current/total))
);
}
Sync.prototype.getNextBlock = function (blockHash,cb) {
var that = this;
if ( !blockHash ) {
return cb();
}
this.rpc.getBlock(blockHash, function(err, blockInfo) {
if (err) return cb(err);
if ( ! ( blockInfo.result.height % 1000) ) {
var h = blockInfo.result.height,
d = blockInfo.result.confirmations;
progress_bar('height', h, h + d);
}
Block.create( blockInfo.result, function(err, inBlock) {
// E11000 => already exists
if (err && ! err.toString().match(/E11000/)) {
return cb(err);
}
if (inBlock) {
inBlock.explodeTransactions(function (err) {
return that.getNextBlock(blockInfo.result.nextblockhash, cb);
});
}
else
return that.getNextBlock(blockInfo.result.nextblockhash, cb);
});
});
}
Sync.prototype.syncBlocks = function (reindex, cb) {
var that = this;
var genesisHash = this.network.genesisBlock.hash.reverse().toString('hex');
console.log("Syncing Blocks...");
if (reindex)
return this.getNextBlock(genesisHash, cb);
Block.findOne({}, {}, { sort: { 'confirmations' : 1 } }, function(err, block) {
if (err) return cb(err);
var nextHash =
block && block.hash
? block.hash
: genesisHash
;
console.log('\tStarting at hash: ' + nextHash);
return that.getNextBlock(nextHash, cb);
});
}
Sync.prototype.syncTXs = function (reindex, cb) {
var that = this;
console.log("Syncing TXs...");
if (reindex) {
// TODO?
}
Transaction.find({blockhash: null}, function(err, txs) {
if (err) return cb(err);
var read = 0;
var pull = 0;
var write = 0;
var total = txs.length;
console.log("\tneed to pull %d txs", total);
if (!total) return cb();
async.each(txs,
function(tx, next){
if (! tx.txid) {
console.log("NO TXID skipping...", tx);
return next();
}
if ( ! ( read++ % 1000) )
progress_bar('read', read, total);
that.rpc.getRawTransaction(tx.txid, 1, function(err, txInfo) {
if ( ! ( pull++ % 1000) )
progress_bar('\tpull', pull, total);
if (!err && txInfo) {
Transaction.update({txid: tx.txid}, txInfo.result, function(err) {
if (err) return next(err);
if ( ! ( write++ % 1000) )
progress_bar('\t\twrite', write, total);
return next();
});
}
else return next();
});
},
function(err){
if (err) return cb(err);
return cb(err);
}
);
});
}
Sync.prototype.start = function (opts, next) {
mongoose.connect(config.db);
var db = mongoose.connection;
this.rpc = new RpcClient(config.bitcoind);
var that = this;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function (){
async.series([
function(cb){
if (opts.destroy) {
console.log("Deleting Blocks...");
return Block.remove().exec(cb);
}
return cb();
},
function(cb){
if (opts.destroy) {
console.log("Deleting TXs...");
return Transaction.remove().exec(cb);
}
return cb();
},
function(cb) {
if (! opts.skip_blocks) {
that.syncBlocks(opts.reindex, function(err) {
if (err) {
return cb(err);
}
console.log("\tBlocks done.");
return cb();
});
}
else {
return cb();
}
},
function(cb) {
if (! opts.skip_txs) {
that.syncTXs(opts.reindex, function(err) {
if (err) {
return cb(err);
}
return cb();
});
}
else {
return cb();
}
},
function(cb) {
db.close();
return cb();
},
],
function(err) {
if (err) {
db.close();
return next(err);
}
return next();
});
});
}
return Sync;
};
module.defineClass(spec);

69
app/controllers/blocks.js Normal file
View File

@ -0,0 +1,69 @@
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Block = mongoose.model('Block');
//, _ = require('lodash');
/**
* Find block by hash ...
*/
exports.block = function(req, res, next, hash) {
Block.fromHash(hash, function(err, block) {
if (err) return next(err);
if (!block) return next(new Error('Failed to load block ' + hash));
req.block = block;
next();
});
};
/**
* Show block
*/
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 = {};
if (req.query.blockDate) {
findParam = {};
}
Block
.find(findParam)
.limit(5)
.exec(function(err, blocks) {
if (err) {
res.render('error', {
status: 500
});
} else {
res.jsonp(blocks);
}
});
};

5
app/controllers/index.js Normal file
View File

@ -0,0 +1,5 @@
'use strict';
exports.render = function(req, res) {
res.render('index');
};

View File

@ -0,0 +1,33 @@
'use strict';
var Transaction = require('../models/Transaction');
//, _ = require('lodash');
/**
* Module dependencies.
*/
/**
* Find block by hash ...
*/
exports.transaction = function(req, res, next, txid) {
Transaction.fromID(txid, function(err, tx) {
if (err) return next(err);
if (!tx) return next(new Error('Failed to load TX ' + txid));
req.transaction = tx;
next();
});
};
/**
* Show block
*/
exports.show = function(req, res) {
res.jsonp(req.transaction);
};

97
app/models/Block.js Normal file
View File

@ -0,0 +1,97 @@
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var async = require('async');
var Transaction = require('./Transaction');
/**
* Block Schema
*/
var BlockSchema = new Schema({
hash: {
type: String,
index: true,
unique: true,
},
size: Number,
height: Number,
confirmations: Number,
version: Number,
merkleroot: String,
tx: [ String ],
time: Date,
nonce: Number,
bits: String,
difficulty: Number,
chainwork: String,
previousblockhash: {
type: String,
index: true,
unique: true,
},
nextblockhash: {
type: String,
index: true,
unique: true,
},
});
BlockSchema.methods.explodeTransactions = function(next) {
// console.log('exploding %s', this.hash, typeof this.tx);
async.forEach( this.tx,
function(tx, callback) {
// console.log('procesing TX %s', tx);
Transaction.create({ txid: tx }, function(err) {
if (err && ! err.toString().match(/E11000/)) {
return callback();
}
if (err) {
return callback(err);
}
return callback();
});
},
function(err) {
if (err) return next(err);
return next();
}
);
};
/**
* Validations
*/
/*
BlockSchema.path('title').validate(function(title) {
return title.length;
},'Title cannot be blank');
*/
/**
* Statics
*/
BlockSchema.statics.load = function(id, cb) {
this.findOne({
_id: id
}).exec(cb);
};
BlockSchema.statics.fromHash = function(hash, cb) {
this.findOne({
hash: hash,
}).exec(cb);
};
module.exports = mongoose.model('Block', BlockSchema);

64
app/models/Transaction.js Normal file
View File

@ -0,0 +1,64 @@
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
*/
var TransactionSchema = new Schema({
txid: {
type: String,
index: true,
unique: true,
},
version: Number,
locktime: Number,
vin: {
type: Array,
default: [],
},
vout: {
type: Array,
default: [],
},
blockhash: {
type: String,
index: true,
default: null,
},
confirmations: Number,
time: Number,
blocktime: Number,
});
/**
* Statics
*/
TransactionSchema.statics.load = function(id, cb) {
this.findOne({
_id: id
}).exec(cb);
};
TransactionSchema.statics.fromID = function(txid, cb) {
this.findOne({
txid: txid,
}).exec(cb);
};
/*
* virtual
*/
// ugly? new object every call?
TransactionSchema.virtual('date').get(function () {
return new Date(this.time);
});
module.exports = mongoose.model('Transaction', TransactionSchema);

13
app/views/404.jade Executable file
View File

@ -0,0 +1,13 @@
extends layouts/default
block main
h1 Oops something went wrong
br
span 404
block content
#error-message-box
#error-stack-trace
pre
code!= error

12
app/views/500.jade Executable file
View File

@ -0,0 +1,12 @@
extends layouts/default
block main
h1 Oops something went wrong
br
span 500
block content
#error-message-box
#error-stack-trace
pre
code!= error

34
app/views/includes/foot.jade Executable file
View File

@ -0,0 +1,34 @@
#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')
//AngularJS
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')
//Angular UI
script(type='text/javascript', src='/lib/angular-bootstrap/ui-bootstrap.js')
script(type='text/javascript', src='/lib/angular-bootstrap/ui-bootstrap-tpls.js')
script(type='text/javascript', src='/lib/angular-ui-utils/ui-utils.js')
//Application Init
script(type='text/javascript', src='/js/app.js')
script(type='text/javascript', src='/js/config.js')
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/blocks.js')
script(type='text/javascript', src='/js/services/global.js')
script(type='text/javascript', src='/js/services/index.js')
//Application Controllers
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/init.js')

15
app/views/includes/head.jade Executable file
View File

@ -0,0 +1,15 @@
head
meta(charset='utf-8')
meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1')
meta(name='viewport', content='width=device-width,initial-scale=1.0')
title Mystery
meta(http-equiv='Content-type', content='text/html;charset=UTF-8')
meta(name="keywords", content="node.js, express, mongoose, mongodb, angularjs")
meta(name="description", content="Mystery")
link(href='/img/icons/favicon.ico', rel='shortcut icon', type='image/x-icon')
link(rel='stylesheet', href='/lib/bootstrap/dist/css/bootstrap.min.css')
link(rel='stylesheet', href='/css/common.css')

View File

@ -0,0 +1 @@
.navbar.navbar-default.navbar-fixed-top(data-ng-include="'views/header.html'", role='navigation')

4
app/views/index.jade Executable file
View File

@ -0,0 +1,4 @@
extends layouts/default
block content
section.container(data-ng-view)

8
app/views/layouts/default.jade Executable file
View File

@ -0,0 +1,8 @@
doctype html
html(lang='en', xmlns='http://www.w3.org/1999/xhtml')
include ../includes/head
body
#wrap
include ../includes/navbar
block content
include ../includes/foot

14
bower.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "Mystery",
"version": "0.0.1",
"dependencies": {
"angular": "latest",
"angular-resource": "latest",
"angular-cookies": "latest",
"angular-mocks": "latest",
"angular-route": "latest",
"bootstrap": "3.0.3",
"angular-bootstrap": "0.9.0",
"angular-ui-utils": "0.1.0"
}
}

9
config/config.js Normal file
View File

@ -0,0 +1,9 @@
'use strict';
var _ = require('lodash');
// Load app configuration
module.exports = _.extend(
require(__dirname + '/../config/env/all.js'),
require(__dirname + '/../config/env/' + process.env.NODE_ENV + '.js') || {});

10
config/env/all.js vendored Executable file
View File

@ -0,0 +1,10 @@
'use strict';
var path = require('path'),
rootPath = path.normalize(__dirname + '/../..');
module.exports = {
root: rootPath,
port: process.env.PORT || 3000,
db: process.env.MONGOHQ_URL
}

15
config/env/development.js vendored Executable file
View File

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

8
config/env/production.js vendored Executable file
View File

@ -0,0 +1,8 @@
'use strict';
module.exports = {
db: "mongodb://localhost/mystery",
app: {
name: "Mystery - Production"
}
}

9
config/env/test.js vendored Executable file
View File

@ -0,0 +1,9 @@
'use strict';
module.exports = {
db: "mongodb://localhost/mystery-test",
port: 3001,
app: {
name: "Mystery - Test"
}
}

69
config/express.js Normal file
View File

@ -0,0 +1,69 @@
'use strict';
/**
* Module dependencies.
*/
var express = require('express'),
config = require('./config');
module.exports = function(app, passport, db) {
app.set('showStackError', true);
//Prettify HTML
app.locals.pretty = true;
//Should be placed before express.static
app.use(express.compress({
filter: function(req, res) {
return (/json|text|javascript|css/).test(res.getHeader('Content-Type'));
},
level: 9
}));
//Set views path, template engine and default layout
app.set('views', config.root + '/app/views');
app.set('view engine', 'jade');
//Enable jsonp
app.enable("jsonp callback");
app.configure(function() {
//cookieParser should be above session
app.use(express.cookieParser());
// request body parsing middleware should be above methodOverride
app.use(express.urlencoded());
app.use(express.json());
app.use(express.methodOverride());
//routes should be at the last
app.use(app.router);
//Setting the fav icon and static folder
app.use(express.favicon());
app.use(express.static(config.root + '/public'));
//Assume "not found" in the error msgs is a 404. this is somewhat silly, but valid, you can do whatever you like, set properties, use instanceof etc.
app.use(function(err, req, res, next) {
//Treat as 404
if (~err.message.indexOf('not found')) return next();
//Log it
console.error(err.stack);
//Error page
res.status(500).render('500', {
error: err.stack
});
});
//Assume 404 since no middleware responded
app.use(function(req, res, next) {
res.status(404).render('404', {
url: req.originalUrl,
error: 'Not found'
});
});
});
};

21
config/routes.js Normal file
View File

@ -0,0 +1,21 @@
'use strict';
module.exports = function(app) {
//Home route
var index = require('../app/controllers/index');
app.get('/', index.render);
//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.param('txid', transactions.transaction);
};

13
etc/bitcoind/bitcoin.conf Normal file
View File

@ -0,0 +1,13 @@
rpcuser=mystery
rpcpassword=real_mystery
server=1
rpcport=8332
testnet=3
txindex=1
# Allow connections outsite localhost?
# rpcallowip=192.168.0.*

220
p2p.js Executable file
View File

@ -0,0 +1,220 @@
var fs = require('fs');
var HeaderDB = require('./HeaderDB').class();
var Block = require('bitcore/Block').class();
var CoinConst = require('bitcore/const');
var coinUtil = require('bitcore/util/util');
var networks = require('bitcore/networks');
var Parser = require('bitcore/util/BinaryParser').class();
var peerdb_fn = 'peerdb.json';
var peerdb = undefined;
var hdrdb = undefined;
var network = networks.testnet;
var config = {
network : network.name
};
var PeerManager = require('bitcore/PeerManager').createClass({
config : config
});
var Peer = require('bitcore/Peer').class();
var syncState = 'init';
function peerdb_load() {
try {
peerdb = JSON.parse(fs.readFileSync(peerdb_fn));
} catch (d) {
console.warn('Unable to read peer db', peerdb_fn, 'creating new one.');
peerdb = [ {
ipv4 : '127.0.0.1',
port : 18333
}, ];
fs.writeFileSync(peerdb_fn, JSON.stringify(peerdb));
}
}
function hdrdb_load()
{
hdrdb = new HeaderDB({network: network});
}
function get_more_headers(info) {
var conn = info.conn;
var loc = hdrdb.locator();
conn.sendGetHeaders(loc, coinUtil.NULL_HASH);
}
function add_header(info, block) {
var hashStr = coinUtil.formatHashFull(block.calcHash());
try {
hdrdb.add(block);
} catch (e) {
return;
}
}
function handle_headers(info) {
console.log("handle headers");
var conn = info.conn;
var headers = info.message.headers;
headers.forEach(function(hdr) {
add_header(info, hdr);
});
// We persist the header DB after each batch
//hdrdb.writeFile(hdrdb_fn);
// Only one request per batch of headers we receive.
get_more_headers(info);
}
function handle_block(info) {
console.log("handle block")
var block = info.message.block;
add_header(info, block);
if (syncState === 'init') {
syncState = 'headers';
get_more_headers(info);
}
}
function handle_verack(info) {
var inv = {
type : CoinConst.MSG.BLOCK,
hash : network.genesisBlock.hash,
};
var invs = [ inv ];
console.log('p2psync: Asking for the genesis block');
// Asks for the genesis block
info.conn.sendGetData(invs);
}
function handle_connected(data) {
var peerman = data.pm;
var peers_n = peerman.peers.length;
console.log('p2psync: Connected to ' + peers_n + ' peer'
+ (peers_n != 1 ? 's' : ''));
}
function p2psync() {
var peerman = new PeerManager();
peerdb.forEach(function(datum) {
var peer = new Peer(datum.ipv4, datum.port);
peerman.addPeer(peer);
});
peerman.on('connection', function(conn) {
conn.on('verack', handle_verack);
conn.on('block', handle_block);
conn.on('headers', handle_headers);
});
peerman.on('connect', handle_connected);
peerman.start();
}
function filesync_block_buf(blkdir, fn, buf) {
var parser = new Parser(buf);
var block = new Block();
block.parse(parser, true);
var hashStr = coinUtil.formatHashFull(block.calcHash());
try {
hdrdb.add(block);
} catch (e) {
var height = hdrdb.size();
console.log('HeaderDB failed adding block #' + height + ', ' + hashStr);
console.log(' Reason: ' + e);
return;
}
var height = hdrdb.size() - 1;
if ((height % 1000) == 0)
console.log('HeaderDB added block #' + height + ', ' + hashStr);
}
function filesync_open_cb(err, fd, blkdir, fn) {
if (err)
throw err;
var hdrbuf = new Buffer(4 * 2);
while (1) {
// read 2x 32-bit header
var bread = fs.readSync(fd, hdrbuf, 0, 4 * 2, null);
if (bread < (4 * 2)) {
console.log('Short read/EOF, ending scan of ' + fn);
break;
}
// check magic matches
var magic = hdrbuf.slice(0, 4);
if (magic.toString() != network.magic.toString()) {
console.log('Invalid network magic, ending scan of ' + fn);
break;
}
// block size
var blkSize = hdrbuf.readUInt32LE(4);
if (blkSize > (1 * 1024 * 1024))
throw new Error('Invalid block size ' + blkSize);
// read raw block data
var blkBuf = new Buffer(blkSize);
bread = fs.readSync(fd, blkBuf, 0, blkSize, null);
if (bread != blkSize)
throw new Error('Failed to read block');
// process block
filesync_block_buf(blkdir, fn, blkBuf);
}
fs.closeSync(fd);
hdrdb.writeFile(hdrdb_fn);
console.log('Wrote header db');
}
function filesync_block_file(blkdir, fn) {
console.log('Scanning ' + fn + ' for block data.');
var pathname = blkdir + '/' + fn;
fs.open(pathname, 'r', function(err, fd) {
filesync_open_cb(err, fd, blkdir, fn);
});
}
function cmd_filesync_rd(err, files, blkdir) {
if (err)
throw err;
files = files.sort();
var scanned = 0;
files.forEach(function(fn) {
var re = /^blk\d+\.dat$/;
if (fn.match(re)) {
filesync_block_file(blkdir, fn);
scanned++;
}
});
console.log('Scanned ' + scanned + ' of ' + files.length + ' files in '
+ blkdir);
}
function main() {
peerdb_load();
hdrdb_load();
p2psync();
}
main();

66
package.json Normal file
View File

@ -0,0 +1,66 @@
{
"name": "mystery",
"version": "0.0.1",
"private": true,
"author": {
"name": "Ryan X Charles",
"email": "ryan@bitpay.com"
},
"repository": "git://github.com/bitpay/mystery.git",
"contributors": [
{
"name": "Matias Alejo Garcia",
"email": "ematiu@gmail.com"
}
],
"bugs": {
"url": "https://github.com/bitpay/mystery/issues"
},
"homepage": "https://github.com/bitpay/mystery",
"license": "MIT",
"keywords": [
"mystery",
"secret",
"enigma",
"riddle",
"mystification",
"puzzle",
"conundrum"
],
"engines": {
"node": "*"
},
"scripts": {
"start": "node node_modules/grunt-cli/bin/grunt",
"postinstall": "node node_modules/bower/bin/bower install"
},
"dependencies": {
"async": "*",
"classtool": "*",
"commander": "*",
"express": "~3.4.7",
"jade": "~1.0.2",
"mongoose": "~3.8.3",
"lodash": "~2.4.1",
"bower": "~1.2.8",
"bitcore": "*",
"buffertools": "*",
"grunt": "~0.4.2",
"grunt-cli": "~0.1.11",
"grunt-env": "~0.4.1",
"grunt-contrib-jshint": "~0.8.0",
"grunt-contrib-watch": "~0.5.3",
"grunt-concurrent": "~0.4.2",
"grunt-nodemon": "~0.1.2",
"grunt-mocha-test": "~0.8.1",
"should": "~2.1.1"
},
"devDependencies": {
"grunt-contrib-watch": "latest",
"grunt-contrib-jshint": "latest",
"grunt-nodemon": "latest",
"grunt-concurrent": "latest",
"grunt-mocha-test": "latest",
"should": "latest"
}
}

45
public/css/common.css Normal file
View File

@ -0,0 +1,45 @@
/* Sticky footer styles
-------------------------------------------------- */
html,
body {
height: 100%;
/* The html and body elements cannot have any padding or margin. */
}
/* Wrapper for page content to push down footer */
#wrap {
min-height: 100%;
height: auto;
/* Negative indent footer by its height */
margin: 0 auto -60px;
/* Pad bottom by footer height */
padding: 0 0 60px;
}
/* Set the fixed height of the footer here */
#footer {
height: 60px;
background-color: #f5f5f5;
}
/* Custom page CSS
-------------------------------------------------- */
/* Not required for template or sticky footer method. */
#wrap > .container {
padding: 60px 15px 0;
}
.container .text-muted {
margin: 20px 0;
}
#footer > .container {
padding-left: 15px;
padding-right: 15px;
}
code {
font-size: 80%;
}

0
public/img/.gitignore vendored Executable file
View File

7
public/js/app.js Executable file
View File

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

30
public/js/config.js Executable file
View File

@ -0,0 +1,30 @@
'use strict';
//Setting up route
angular.module('mystery').config(['$routeProvider',
function($routeProvider) {
$routeProvider.
when('/block/:blockHash', {
templateUrl: 'views/block.html'
}).
when('/', {
templateUrl: 'views/index.html'
}).
when('/blocks', {
templateUrl: 'views/blocks/list.html'
}).
when('/blocks-date/:blockDate', {
templateUrl: 'views/blocks/list_date.html'
}).
otherwise({
redirectTo: '/'
});
}
]);
//Setting HTML5 Location Mode
angular.module('mystery').config(['$locationProvider',
function($locationProvider) {
$locationProvider.hashPrefix('!');
}
]);

View File

@ -0,0 +1,30 @@
'use strict';
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({
blockDate: $routeParams.blockDate
}, function(blocks) {
$scope.blocks = blocks;
});
};
$scope.findOne = function() {
Block.get({
blockHash: $routeParams.blockHash
}, function(block) {
$scope.block = block;
});
};
// for avoid warning. please remove when you use Blocks
$scope.blocks = Blocks;
}]);

12
public/js/controllers/header.js Executable file
View File

@ -0,0 +1,12 @@
'use strict';
angular.module('mystery.system').controller('HeaderController', ['$scope', 'Global', function ($scope, Global) {
$scope.global = Global;
$scope.menu = [{
'title': 'Blocks',
'link': 'blocks'
}];
$scope.isCollapsed = false;
}]);

10
public/js/controllers/index.js Executable file
View File

@ -0,0 +1,10 @@
'use strict';
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;
});
};
}]);

1
public/js/directives.js Executable file
View File

@ -0,0 +1 @@
'use strict';

1
public/js/filters.js Executable file
View File

@ -0,0 +1 @@
'use strict';

9
public/js/init.js Executable file
View File

@ -0,0 +1,9 @@
'use strict';
angular.element(document).ready(function() {
//Fixing facebook bug with redirect
if (window.location.hash === '#_=_') window.location.hash = '#!';
//Then init the app
angular.bootstrap(document, ['mystery']);
});

View File

@ -0,0 +1,11 @@
'use strict';
angular.module('mystery.blocks').factory('Block', ['$resource', function($resource) {
return $resource('/api/block/:blockHash', {
blockHash: '@blockHash'
});
}]);
angular.module('mystery.blocks').factory('Blocks', ['$resource', function($resource) {
return $resource('/api/blocks');
}]);

14
public/js/services/global.js Executable file
View File

@ -0,0 +1,14 @@
'use strict';
//Global service for global variables
angular.module('mystery.system').factory('Global', [
function() {
var _this = this;
_this._data = {
user: window.user,
authenticated: !! window.user
};
return _this._data;
}
]);

View File

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

23
public/views/block.html Normal file
View File

@ -0,0 +1,23 @@
<section data-ng-controller="BlocksController" data-ng-init="findOne()">
<div class="page-header">
<h1>Block Page</h1>
</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>
<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>
</tbody>
</table>
</section>

View File

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

View File

@ -0,0 +1,10 @@
<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>

18
public/views/header.html Executable file
View File

@ -0,0 +1,18 @@
<div class="container" data-ng-controller="HeaderController">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#!">Mystery</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li data-ng-repeat="item in menu" ui-route="/{{item.link}}" ng-class="{active: $uiRoute}">
<a href="#!/{{item.link}}">{{item.title}}</a>
</li>
</ul>
</div>
</div>

23
public/views/index.html Normal file
View File

@ -0,0 +1,23 @@
<section data-ng-controller="IndexController" data-ng-init="last_blocks()">
<div class="page-header">
<h1>Hello BitPay!</h1>
</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>

55
server.js Normal file
View File

@ -0,0 +1,55 @@
'use strict';
/**
* Module dependencies.
*/
var express = require('express'),
fs = require('fs');
/**
* Main application entry file.
*/
//Load configurations
//Set the node enviornment variable if not set before
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
//Initializing system variables
var config = require('./config/config'),
mongoose = require('mongoose');
//Bootstrap db connection
var db = mongoose.connect(config.db);
//Bootstrap models
var models_path = __dirname + '/app/models';
var walk = function(path) {
fs.readdirSync(path).forEach(function(file) {
var newPath = path + '/' + file;
var stat = fs.statSync(newPath);
if (stat.isFile()) {
if (/(.*)\.(js$|coffee$)/.test(file)) {
require(newPath);
}
} else if (stat.isDirectory()) {
walk(newPath);
}
});
};
walk(models_path);
var app = express();
//express settings
require('./config/express')(app, db);
//Bootstrap routes
require('./config/routes')(app);
//Start the app by listening on <port>
var port = process.env.PORT || config.port;
app.listen(port);
console.log('Express app started on port ' + port);
//expose app
exports = module.exports = app;

4
test/mocha.opts Normal file
View File

@ -0,0 +1,4 @@
--require should
-R spec
--ui bdd

9
test/test.js Normal file
View File

@ -0,0 +1,9 @@
var assert = require("assert")
describe('Array', function(){
describe('#indexOf()', function(){
it('should return -1 when the value is not present', function(){
assert.equal(-1, [1,2,3].indexOf(5));
assert.equal(-1, [1,2,3].indexOf(0));
})
})
})

25
util/get_block.js Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env node
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
var RpcClient = require('../node_modules/bitcore/RpcClient').class();
var config = require('../config/config');
var block_hash = process.argv[2] || '0000000000b6288775bbd326bedf324ca8717a15191da58391535408205aada4';
var rpc = new RpcClient(config.bitcoind);
var block = rpc.getBlock(block_hash, function(err, block) {
console.log("Err:");
console.log(err);
console.log("Block info:");
console.log(block);
});

34
util/sync.js Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env node
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
require('buffertools').extend();
var SYNC_VERSION = '0.1';
var program = require('commander');
var Sync = require('../Sync').class();
program
.version(SYNC_VERSION)
.option('-N --network [livenet]', 'Set bitcoin network [testnet]', 'testnet')
.option('-R --reindex', 'Force reindexing', '0')
.option('-D --destroy', 'Remove current DB', '0')
.option('--skip_blocks', 'Sync blocks')
.option('--skip_txs', 'Sync transactions')
.parse(process.argv);
var sync = new Sync({ networkName: program.network });
if (program.remove) {
}
sync.start( program, function(err){
if (err) {
console.log("CRITICAL ERROR: ", err);
}
else {
console.log('Done!');
}
});