├── db ├── empty └── testnet │ └── empty ├── test ├── mocha.opts ├── integration │ ├── nodecheck.js │ ├── utxo.json │ ├── spent.json │ ├── status.js │ ├── blocklist.js │ ├── block.js │ ├── 02-transactionouts.js │ ├── txitems.json │ ├── blockExtractor.js │ ├── addr.js │ ├── 01-transactionouts.js │ ├── addr.json │ └── 99-sync.js.descructive-test └── lib │ └── PeerSync.js ├── etc ├── bitcoind │ ├── bitcoin-livenet.conf │ └── bitcoin-testnet.conf └── minersPoolStrings.json ├── app ├── controllers │ ├── common.js │ ├── index.js │ ├── addresses.js │ ├── status.js │ ├── socket.js │ ├── currency.js │ ├── transactions.js │ └── blocks.js └── models │ ├── Status.js │ └── Address.js ├── util ├── p2p.js └── sync.js ├── dev-util ├── sync-level.js ├── status_info.js ├── block-level.js ├── level.js ├── get_tx.js ├── level-put.js ├── read_block.js ├── get_block.js ├── find_ref.sh ├── get_outs_addr.js ├── get_outs.js ├── explode_tx.js └── stats ├── .gitignore ├── lib ├── PoolMatch.js ├── Rpc.js ├── PeerSync.js ├── BlockExtractor.js ├── BlockDb.js ├── Sync.js ├── HistoricSync.js └── TransactionDb.js ├── config ├── routes.js ├── express.js └── config.js ├── .jshintrc ├── insight.js ├── package.json ├── Gruntfile.js └── README.md /db/empty: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /db/testnet/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | -R spec 3 | --ui bdd 4 | --recursive 5 | -------------------------------------------------------------------------------- /etc/bitcoind/bitcoin-livenet.conf: -------------------------------------------------------------------------------- 1 | rpcuser=user 2 | rpcpassword=pass 3 | server=1 4 | txindex=1 5 | 6 | # Allow connections outsite localhost? 7 | rpcallowip=192.168.1.* 8 | 9 | rpcport=8332 10 | 11 | -------------------------------------------------------------------------------- /etc/bitcoind/bitcoin-testnet.conf: -------------------------------------------------------------------------------- 1 | rpcuser=user 2 | rpcpassword=pass 3 | server=1 4 | txindex=1 5 | 6 | # Allow connections outsite localhost? 7 | rpcallowip=192.168.1.* 8 | 9 | rpcport=18332 10 | testnet=3 11 | -------------------------------------------------------------------------------- /app/controllers/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | exports.handleErrors = function (err, res) { 5 | if (err) { 6 | if (err.code) { 7 | res.status(400).send(err.message + '. Code:' + err.code); 8 | } 9 | else { 10 | res.status(503).send(err.message); 11 | } 12 | } 13 | else { 14 | res.status(404).send('Not found'); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /util/p2p.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 'use strict'; 3 | 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | var PeerSync = require('../lib/PeerSync').class(); 7 | 8 | var PROGRAM_VERSION = '0.1'; 9 | var program = require('commander'); 10 | 11 | program 12 | .version(PROGRAM_VERSION) 13 | .parse(process.argv); 14 | 15 | var ps = new PeerSync(); 16 | ps.run(); 17 | 18 | -------------------------------------------------------------------------------- /dev-util/sync-level.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var Sync = require('../lib/Sync').class(); 5 | 6 | 7 | var s = new Sync(); 8 | 9 | 10 | s.setOrphan( 11 | '0000000000c2b1e8dab92a72741289e5ef0d4f375fd1b26f729da2ba979c028a', 12 | '000000000228f9d02654459e09998c7557afa9082784c11226853f5feb805df9', 13 | function (err) { 14 | console.log('[sync-level.js.15]',err); //TODO 15 | }); 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/integration/nodecheck.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var BlockDb = require('../../lib/BlockDb').class(); 4 | var height_needed = 180000; 5 | var bDb = new BlockDb(); 6 | 7 | var expect = require('chai').expect; 8 | 9 | describe('Node check', function() { 10 | it('should contain block ' + height_needed, function(done) { 11 | bDb.blockIndex(height_needed, function(err, b) { 12 | expect(err).to.equal(null); 13 | expect(b).to.not.equal(null); 14 | done(); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /dev-util/status_info.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 3 | 4 | var RpcClient = require('../node_modules/bitcore/RpcClient').class(); 5 | 6 | var config = require('../config/config'); 7 | 8 | var rpc = new RpcClient(config.bitcoind); 9 | 10 | var block = rpc.getInfo(function(err, block) { 11 | if (err) { 12 | console.log("Err:"); 13 | console.log(err); 14 | } 15 | 16 | console.log("Block info:"); 17 | console.log(block); 18 | }); 19 | 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # from https://github.com/github/gitignore/blob/master/Node.gitignore 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | *.swp 11 | tags 12 | pids 13 | logs 14 | results 15 | build 16 | node_modules 17 | 18 | # extras 19 | *.swp 20 | *.swo 21 | *~ 22 | .project 23 | peerdb.json 24 | 25 | npm-debug.log 26 | .nodemonignore 27 | 28 | .DS_Store 29 | db/txs/* 30 | db/txs 31 | db/testnet/txs/* 32 | db/testnet/txs 33 | db/blocks/* 34 | db/blocks 35 | db/testnet/blocks/* 36 | db/testnet/blocks 37 | 38 | README.html 39 | -------------------------------------------------------------------------------- /app/controllers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = require('../../config/config'); 4 | 5 | var _getVersion = function() { 6 | var pjson = require('../../package.json'); 7 | return pjson.version; 8 | }; 9 | 10 | exports.render = function(req, res) { 11 | if (config.publicPath) { 12 | return res.sendfile(config.publicPath + '/index.html'); 13 | } 14 | 15 | var version = _getVersion(); 16 | res.send('insight API v' + version); 17 | }; 18 | 19 | exports.version = function(req, res) { 20 | var version = _getVersion(); 21 | res.json({ version: version }); 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /dev-util/block-level.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var 4 | config = require('../config/config'), 5 | levelup = require('levelup'); 6 | 7 | 8 | db = levelup(config.leveldb + '/blocks'); 9 | 10 | db.createReadStream({start: 'b-'}) 11 | .on('data', function (data) { 12 | console.log('[block-level.js.11:data:]',data); //TODO 13 | if (data==false) c++; 14 | }) 15 | .on('error', function (err) { 16 | return cb(err); 17 | }) 18 | .on('close', function () { 19 | return cb(null); 20 | }) 21 | .on('end', function () { 22 | return cb(null); 23 | }); 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/integration/utxo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "addr": "muyg1K5WsHkfMVCkUXU2y7Xp5ZD6RGzCeH", 4 | "length": 1, 5 | "tx0id": "eeabc70063d3f266e190e8735bc4599c811d3a79d138da1364e88502069b029c", 6 | "tx0scriptPubKey": "76a9149e9f6515c70db535abdbbc983c7d8d1bff6c20cd88ac", 7 | "tx0amount": 0.38571339 8 | }, 9 | { 10 | "addr": "mgKY35SXqxFpcKK3Dq9mW9919N7wYXvcFM", 11 | "length": 1, 12 | "tx0id": "91800d80bb4c69b238c9bfd94eb5155ab821e6b25cae5c79903d12853bbb4ed5", 13 | "tx0scriptPubKey": "76a91408cf4ceb2b7278043fcc7f545e6e6e73ef9a644f88ac", 14 | "tx0amount": 0.01979459 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /dev-util/level.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var config = require('../config/config'), 5 | levelup = require('levelup'); 6 | 7 | 8 | 9 | var s = process.argv[2]; 10 | var isBlock = process.argv[3] === '1'; 11 | 12 | 13 | var dbPath = config.leveldb + (isBlock ? '/blocks' : '/txs'); 14 | console.log('DB: ',dbPath); //TODO 15 | 16 | 17 | 18 | var db = levelup(dbPath ); 19 | 20 | 21 | db.createReadStream({start: s, end: s+'~'}) 22 | .on('data', function (data) { 23 | console.log(data.key + ' => ' + data.value); //TODO 24 | }) 25 | .on('error', function () { 26 | }) 27 | .on('end', function () { 28 | }); 29 | 30 | 31 | -------------------------------------------------------------------------------- /dev-util/get_tx.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var util = require('util'); 5 | var T = require('../lib/TransactionDb').class(); 6 | 7 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 8 | 9 | 10 | // var hash = process.argv[2] || '0000000000b6288775bbd326bedf324ca8717a15191da58391535408205aada4'; 11 | var hash = process.argv[2] || 'e2253359458db3e732c82a43fc62f56979ff59928f25a2df34dfa443e9a41160'; 12 | 13 | 14 | var t = new T(); 15 | t.fromIdWithInfo(hash, function(err, ret) { 16 | 17 | console.log('Err:'); 18 | console.log(err); 19 | 20 | 21 | console.log('Ret:'); 22 | console.log(util.inspect(ret,{depth:null})); 23 | }); 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /dev-util/level-put.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var config = require('../config/config'), 5 | levelup = require('levelup'); 6 | 7 | 8 | 9 | var k = process.argv[2]; 10 | var v = process.argv[3]; 11 | var isBlock = process.argv[4] === '1'; 12 | 13 | 14 | var dbPath = config.leveldb + (isBlock ? '/blocks' : '/txs'); 15 | console.log('DB: ',dbPath); //TODO 16 | 17 | 18 | 19 | var db = levelup(dbPath ); 20 | 21 | 22 | if (v) { 23 | db.put(k,v,function(err) { 24 | console.log('[PUT done]',err); //TODO 25 | }); 26 | } 27 | else { 28 | db.del(k,function(err) { 29 | console.log('[DEL done]',err); //TODO 30 | }); 31 | } 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /dev-util/read_block.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | var assert = require('assert'), 7 | config = require('../config/config'), 8 | BlockExtractor = require('../lib/BlockExtractor').class(), 9 | networks = require('bitcore/networks'), 10 | util = require('bitcore/util/util'); 11 | 12 | var be = new BlockExtractor(config.bitcoind.dataDir, config.network); 13 | var network = config.network === 'testnet' ? networks.testnet: networks.livenet; 14 | // console.log('[read_block.js.13]', be.nextFile() ); 15 | 16 | var c=0; 17 | while (c++ < 100) { 18 | be.getNextBlock(function(err, b) { 19 | console.log('[read_block.js.14]',err, c, b?util.formatHashAlt(b.hash):''); //TODO 20 | }); 21 | } 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /dev-util/get_block.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var util = require('util'); 5 | 6 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 7 | 8 | var RpcClient = require('../node_modules/bitcore/RpcClient').class(); 9 | 10 | var config = require('../config/config'); 11 | 12 | 13 | var hash = process.argv[2] || '0000000000b6288775bbd326bedf324ca8717a15191da58391535408205aada4'; 14 | //var hash = process.argv[2] || 'f6c2901f39fd07f2f2e503183d76f73ecc1aee9ac9216fde58e867bc29ce674e'; 15 | 16 | //hash = 'e2253359458db3e732c82a43fc62f56979ff59928f25a2df34dfa443e9a41160'; 17 | 18 | var rpc = new RpcClient(config.bitcoind); 19 | 20 | rpc.getBlock( hash, function(err, ret) { 21 | 22 | console.log('Err:'); 23 | console.log(err); 24 | 25 | 26 | console.log('Ret:'); 27 | console.log(util.inspect(ret, { depth: 10} )); 28 | }); 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /dev-util/find_ref.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | FIND='find'; 4 | 5 | ##if [[ "$OSTYPE" =~ "darwin" ]] 6 | ##then 7 | ## FIND='gfind' 8 | ##fi 9 | 10 | 11 | if [ -z "$1" ] 12 | then 13 | echo "$0 : find functions references " 14 | echo "Usage $0 function_name " 15 | exit; 16 | fi 17 | 18 | EXTRA='' 19 | 20 | 21 | CMD="grep -rnH" 22 | 23 | if [ "$2" != '--nocolor' ] 24 | then 25 | CMD="$CMD --color=always" 26 | fi 27 | 28 | 29 | $FIND -L . -name \*.json -not -wholename \*node_modules\* -not -wholename \*public/lib\* -exec $CMD "$1" {} + \ 30 | -o -name \*.html -not -wholename \*node_modules\* -not -wholename \*public/lib\* -exec $CMD "$1" {} + \ 31 | -o -name \*.jade -not -wholename \*node_modules\* -not -wholename \*public/lib\* -exec $CMD "$1" {} + \ 32 | -o -name \*.js -not -wholename \*node_modules\* -not -wholename \*public/lib\* -exec $CMD "$1" {} + 33 | 34 | -------------------------------------------------------------------------------- /lib/PoolMatch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('classtool'); 4 | 5 | function spec(b) { 6 | 7 | var fs = require('fs'); 8 | var buffertools = require('buffertools'); 9 | var db = b.db || JSON.parse( fs.readFileSync(b.poolMatchFile || './poolMatchFile.json')); 10 | 11 | var PoolMatch = function() { 12 | var self = this; 13 | 14 | self.strings = {}; 15 | db.forEach(function(pool) { 16 | pool.searchStrings.forEach(function(s) { 17 | self.strings[s] = { 18 | poolName: pool.poolName, 19 | url: pool.url 20 | }; 21 | }); 22 | }); 23 | }; 24 | 25 | 26 | PoolMatch.prototype.match = function(buffer) { 27 | var self = this; 28 | for(var k in self.strings) { 29 | if (buffertools.indexOf(buffer, k) >= 0) { 30 | return self.strings[k]; 31 | } 32 | } 33 | }; 34 | 35 | return PoolMatch; 36 | } 37 | module.defineClass(spec); 38 | 39 | -------------------------------------------------------------------------------- /dev-util/get_outs_addr.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var util = require('util'); 5 | var mongoose= require('mongoose'), 6 | config = require('../config/config'); 7 | 8 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 9 | 10 | var T = require('../app/models/TransactionOut'); 11 | 12 | 13 | // var hash = process.argv[2] || '0000000000b6288775bbd326bedf324ca8717a15191da58391535408205aada4'; 14 | var hash = process.argv[2] || 'mp3Rzxx9s1A21SY3sjJ3CQoa2Xjph7e5eS'; 15 | 16 | 17 | 18 | 19 | mongoose.connect(config.db); 20 | 21 | mongoose.connection.on('error', function(err) { console.log(err); }); 22 | 23 | 24 | mongoose.connection.on('open', function() { 25 | 26 | T.find({addr: hash}, function(err, ret) { 27 | 28 | console.log('Err:'); 29 | console.log(err); 30 | 31 | 32 | console.log('Ret:'); 33 | console.log(util.inspect(ret,{depth:null})); 34 | mongoose.connection.close(); 35 | }); 36 | }); 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/integration/spent.json: -------------------------------------------------------------------------------- 1 | { 2 | "21798ddc9664ac0ef618f52b151dda82dafaf2e26d2bbef6cdaf55a6957ca237": [ 3 | { 4 | "txid": "bcd8da8ee847da377f8aaca92502c05e5f914c6a2452753146013b0e642a25a0", 5 | "n": 0 6 | }, 7 | { 8 | "txid": "deb7bddc67e936ae49b97a97885d29e60afc6f6784f6d871f2904614a67250f5", 9 | "n": 0 10 | } 11 | ], 12 | "b633a6249d4a2bc123e7f8a151cae2d4afd17aa94840009f8697270c7818ceee": [ 13 | { 14 | "txid": "c0c46d6be0183f52c88afe2d649800ecdaa7594ee390c77bafbd06322e6c823d", 15 | "n": 11 16 | }, 17 | { 18 | "txid": "d60e980419c5a8abd629fdea5032d561678b62e23b3fdba62b42f410c5a29560", 19 | "n": 1 20 | } 21 | ], 22 | "ca2f42e44455b8a84434de139efea1fe2c7d71414a8939e0a20f518849085c3b": [ 23 | { 24 | "txid": "aa21822f1a69bc54e5a4ab60b25c09503702a821379fd2dfbb696b8ada4ce5b9", 25 | "n": 0 26 | }, 27 | { 28 | "txid": "a33bd24a47ab6f23758ed09e05716f809614f2e280e5a05a317ec6d839e81225", 29 | "n": 1 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /dev-util/get_outs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var util = require('util'); 5 | var mongoose= require('mongoose'), 6 | config = require('../config/config'); 7 | 8 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 9 | 10 | var T = require('../app/models/TransactionOut'); 11 | 12 | 13 | // var hash = process.argv[2] || '0000000000b6288775bbd326bedf324ca8717a15191da58391535408205aada4'; 14 | var hash = process.argv[2] || 'e2253359458db3e732c82a43fc62f56979ff59928f25a2df34dfa443e9a41160'; 15 | 16 | 17 | 18 | 19 | mongoose.connect(config.db); 20 | 21 | mongoose.connection.on('error', function(err) { console.log(err); }); 22 | 23 | 24 | mongoose.connection.on('open', function() { 25 | 26 | var b = new Buffer(hash,'hex'); 27 | 28 | T.find({txidBuf: b}, function(err, ret) { 29 | 30 | console.log('Err:'); 31 | console.log(err); 32 | 33 | 34 | console.log('Ret:'); 35 | console.log(util.inspect(ret,{depth:null})); 36 | mongoose.connection.close(); 37 | }); 38 | }); 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /dev-util/explode_tx.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var util = require('util'); 5 | var mongoose= require('mongoose'), 6 | config = require('../config/config'); 7 | 8 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 9 | 10 | var T = require('../app/models/TransactionOut'); 11 | 12 | 13 | // var hash = process.argv[2] || '0000000000b6288775bbd326bedf324ca8717a15191da58391535408205aada4'; 14 | var hash = process.argv[2] || '6749762ae220c10705556799dcec9bb6a54a7b881eb4b961323a3363b00db518'; 15 | 16 | 17 | 18 | 19 | mongoose.connect(config.db); 20 | 21 | mongoose.connection.on('error', function(err) { console.log(err); }); 22 | 23 | 24 | mongoose.connection.on('open', function() { 25 | 26 | var b = new Buffer(hash,'hex'); 27 | 28 | T.createFromTxs([hash], function(err, ret) { 29 | 30 | console.log('Err:'); 31 | console.log(err); 32 | 33 | 34 | console.log('Ret:'); 35 | console.log(util.inspect(ret,{depth:null})); 36 | mongoose.connection.close(); 37 | }); 38 | }); 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/integration/status.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | var assert = require('assert'), 7 | Status = require('../../app/models/Status').class(); 8 | 9 | describe('Status', function(){ 10 | 11 | it('getInfo', function(done) { 12 | var d = new Status(); 13 | 14 | d.getInfo(function(err) { 15 | if (err) done(err); 16 | assert.equal('number', typeof d.info.difficulty); 17 | done(); 18 | }); 19 | }); 20 | 21 | it('getDifficulty', function(done) { 22 | var d = new Status(); 23 | 24 | d.getDifficulty(function(err) { 25 | if (err) done(err); 26 | assert.equal('number', typeof d.difficulty); 27 | done(); 28 | }); 29 | }); 30 | 31 | it('getLastBlockHash', function(done) { 32 | var d = new Status(); 33 | 34 | d.getLastBlockHash(function(err) { 35 | if (err) done(err); 36 | assert.equal('string', typeof d.lastblockhash); 37 | done(); 38 | }); 39 | }); 40 | 41 | 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /test/integration/blocklist.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | var TESTING_BLOCK0 = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'; 7 | var TESTING_BLOCK1 = '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943'; 8 | var START_TS = 1; 9 | var END_TS = '1296688928~'; // 2/2/2011 23:23PM 10 | 11 | var assert = require('assert'), 12 | BlockDb = require('../../lib/BlockDb').class(); 13 | 14 | var bDb; 15 | 16 | describe('BlockDb getBlocksByDate', function(){ 17 | 18 | 19 | before(function(c) { 20 | bDb = new BlockDb(); 21 | return c(); 22 | }); 23 | 24 | it('Get Hash by Date', function(done) { 25 | 26 | bDb.getBlocksByDate(START_TS, END_TS, function(err, list) { 27 | if (err) done(err); 28 | assert(list, 'returns list'); 29 | assert.equal(list.length,2, 'list has 2 items'); 30 | assert.equal(list[0].hash, TESTING_BLOCK0); 31 | assert.equal(list[1].hash, TESTING_BLOCK1); 32 | done(); 33 | }); 34 | }); 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /app/controllers/addresses.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var Address = require('../models/Address'), 8 | common = require('./common'); 9 | 10 | var getAddr = function(req, res, next) { 11 | var a; 12 | try { 13 | var addr = req.param('addr'); 14 | a = Address.new(addr); 15 | } catch (e) { 16 | common.handleErrors({message: 'Invalid address:' + e.message, code: 1}, res, next); 17 | return null; 18 | } 19 | return a; 20 | }; 21 | 22 | exports.show = function(req, res, next) { 23 | var a = getAddr(req, res, next); 24 | 25 | if (a) 26 | a.update(function(err) { 27 | if (err) { 28 | return common.handleErrors(err, res); 29 | } 30 | else { 31 | return res.jsonp(a); 32 | } 33 | }); 34 | }; 35 | 36 | 37 | 38 | exports.utxo = function(req, res, next) { 39 | var a = getAddr(req, res, next); 40 | 41 | if (a) 42 | a.getUtxo(function(err, utxo) { 43 | if (err) 44 | return common.handleErrors(err, res); 45 | else { 46 | return res.jsonp(utxo); 47 | } 48 | }); 49 | }; 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /util/sync.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 4 | 'use strict'; 5 | 6 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 7 | 8 | var SYNC_VERSION = '0.1'; 9 | var program = require('commander'); 10 | var HistoricSync = require('../lib/HistoricSync').class(); 11 | var async = require('async'); 12 | 13 | program 14 | .version(SYNC_VERSION) 15 | .option('-D --destroy', 'Remove current DB (and start from there)', 0) 16 | .option('-S --startfile', 'Number of file from bitcoind to start(default=0)') 17 | .option('-R --rpc', 'Force sync with RPC') 18 | .option('-v --verbose', 'Verbose 0/1', 0) 19 | .parse(process.argv); 20 | 21 | var historicSync = new HistoricSync({ 22 | shouldBroadcastSync: true, 23 | }); 24 | 25 | 26 | async.series([ 27 | function(cb) { 28 | if (!program.destroy) return cb(); 29 | console.log('Deleting Sync DB...'); 30 | historicSync.sync.destroy(cb); 31 | }, 32 | function(cb) { 33 | historicSync.start({ 34 | forceStartFile: program.startfile, 35 | forceRPC: program.rpc, 36 | },cb); 37 | }, 38 | ], 39 | function(err) { 40 | historicSync.close(); 41 | if (err) console.log('CRITICAL ERROR: ', historicSync.info()); 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /test/lib/PeerSync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chai = require('chai'), 3 | expect = chai.expect, 4 | sinon = require('sinon'); 5 | 6 | var PeerSync = require('../../lib/PeerSync.js').class(); 7 | describe('PeerSync', function() { 8 | var ps; 9 | 10 | beforeEach(function(done) { 11 | ps = new PeerSync(); 12 | done(); 13 | }); 14 | afterEach(function() { 15 | ps.close(); 16 | }); 17 | 18 | 19 | describe('#handleInv()', function() { 20 | var inv_info = { 21 | message: { 22 | invs: [] 23 | }, 24 | conn: { 25 | sendGetData: sinon.spy() 26 | } 27 | }; 28 | it('should return with no errors', function() { 29 | expect(function() { 30 | ps.handleInv(inv_info); 31 | }).not.to. 32 | throw (Error); 33 | }); 34 | it('should call sendGetData', function() { 35 | ps.handleInv(inv_info); 36 | expect(inv_info.conn.sendGetData.calledTwice).to.be.ok; 37 | }); 38 | }); 39 | 40 | describe('#run()', function() { 41 | it('should setup peerman', function() { 42 | var startSpy = sinon.spy(ps.peerman, 'start'); 43 | var onSpy = sinon.spy(ps.peerman, 'on'); 44 | ps.run(); 45 | 46 | expect(startSpy.called).to.be.ok; 47 | expect(onSpy.called).to.be.ok; 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/integration/block.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 6 | 7 | 8 | var TESTING_BLOCK = '000000000185678d3d7ecc9962c96418174431f93fe20bf216d5565272423f74'; 9 | 10 | var 11 | assert = require('assert'), 12 | // config = require('../../config/config'), 13 | BlockDb = require('../../lib/BlockDb').class(); 14 | 15 | var bDb; 16 | 17 | describe('BlockDb fromHashWithInfo', function() { 18 | 19 | before(function(c) { 20 | bDb = new BlockDb(); 21 | return c(); 22 | }); 23 | 24 | it('should poll block\'s info from bitcoind', function(done) { 25 | bDb.fromHashWithInfo(TESTING_BLOCK, function(err, b2) { 26 | if (err) done(err); 27 | assert.equal(b2.hash, TESTING_BLOCK, 'hash'); 28 | assert.equal(b2.info.hash, TESTING_BLOCK, 'info.hash'); 29 | assert.equal(b2.info.height, 71619); 30 | assert.equal(b2.info.nonce, 3960980741); 31 | assert.equal(b2.info.bits, '1c018c14'); 32 | assert.equal(b2.info.merkleroot, '9a326cb524aa2e5bc926b8c1f6de5b01257929ee02158054b55aae93a55ec9dd'); 33 | assert.equal(b2.info.nextblockhash, '000000000121941b3b10d76fbe67b35993df91eb3398e9153e140b4f6213cb84'); 34 | done(); 35 | }); 36 | }); 37 | it('return true in has', function(done) { 38 | bDb.has(TESTING_BLOCK, function(err, has) { 39 | assert.equal(has, true); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /app/controllers/status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var Status = require('../models/Status'), 8 | common = require('./common'); 9 | 10 | /** 11 | * Status 12 | */ 13 | exports.show = function(req, res) { 14 | 15 | if (! req.query.q) { 16 | res.status(400).send('Bad Request'); 17 | } 18 | else { 19 | var option = req.query.q; 20 | var statusObject = Status.new(); 21 | 22 | var returnJsonp = function (err) { 23 | if (err || ! statusObject) 24 | return common.handleErrors(err, res); 25 | else { 26 | res.jsonp(statusObject); 27 | } 28 | }; 29 | 30 | switch(option) { 31 | case 'getInfo': 32 | statusObject.getInfo(returnJsonp); 33 | break; 34 | case 'getDifficulty': 35 | statusObject.getDifficulty(returnJsonp); 36 | break; 37 | case 'getTxOutSetInfo': 38 | statusObject.getTxOutSetInfo(returnJsonp); 39 | break; 40 | case 'getLastBlockHash': 41 | statusObject.getLastBlockHash(returnJsonp); 42 | break; 43 | default: 44 | res.status(400).send('Bad Request'); 45 | } 46 | } 47 | }; 48 | 49 | exports.sync = function(req, res) { 50 | if (req.historicSync) 51 | res.jsonp(req.historicSync.info()); 52 | }; 53 | 54 | exports.peer = function(req, res) { 55 | if (req.peerSync) { 56 | var info = req.peerSync.info(); 57 | res.jsonp(info); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /app/controllers/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // server-side socket behaviour 4 | // io is a variable already taken in express 5 | var ios = null; 6 | var util = require('bitcore/util/util'); 7 | 8 | module.exports.init = function(app, io_ext) { 9 | ios = io_ext; 10 | ios.set('log level', 1); // reduce logging 11 | ios.sockets.on('connection', function(socket) { 12 | socket.on('subscribe', function(topic) { 13 | socket.join(topic); 14 | }); 15 | }); 16 | }; 17 | 18 | module.exports.broadcastTx = function(tx) { 19 | if (ios) { 20 | var t; 21 | if (typeof tx === 'string') { 22 | t = { 23 | txid: tx 24 | }; 25 | } 26 | 27 | else { 28 | t = { 29 | txid: tx.txid, 30 | size: tx.size, 31 | }; 32 | // Outputs 33 | var valueOut = 0; 34 | tx.vout.forEach(function(o) { 35 | valueOut += o.value * util.COIN; 36 | }); 37 | 38 | t.valueOut = parseInt(valueOut) / util.COIN; 39 | } 40 | ios.sockets. in ('inv').emit('tx', t); 41 | } 42 | }; 43 | 44 | module.exports.broadcastBlock = function(block) { 45 | if (ios) ios.sockets. in ('inv').emit('block', block); 46 | }; 47 | 48 | module.exports.broadcastAddressTx = function(address, tx) { 49 | if (ios) ios.sockets. in (address).emit(address, tx); 50 | }; 51 | 52 | module.exports.broadcastSyncInfo = function(historicSync) { 53 | 54 | if (ios) { 55 | ios.sockets. in ('sync').emit('status', historicSync); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var config = require('./config'); 7 | 8 | module.exports = function(app) { 9 | 10 | var apiPrefix = config.apiPrefix; 11 | 12 | //Block routes 13 | var blocks = require('../app/controllers/blocks'); 14 | app.get(apiPrefix + '/blocks', blocks.list); 15 | 16 | 17 | app.get(apiPrefix + '/block/:blockHash', blocks.show); 18 | app.param('blockHash', blocks.block); 19 | 20 | app.get(apiPrefix + '/block-index/:height', blocks.blockindex); 21 | app.param('height', blocks.blockindex); 22 | 23 | // Transaction routes 24 | var transactions = require('../app/controllers/transactions'); 25 | app.get(apiPrefix + '/tx/:txid', transactions.show); 26 | app.param('txid', transactions.transaction); 27 | app.get(apiPrefix + '/txs', transactions.list); 28 | app.post(apiPrefix + '/tx/send', transactions.send); 29 | 30 | // Address routes 31 | var addresses = require('../app/controllers/addresses'); 32 | app.get(apiPrefix + '/addr/:addr', addresses.show); 33 | app.get(apiPrefix + '/addr/:addr/utxo', addresses.utxo); 34 | 35 | // Status route 36 | var st = require('../app/controllers/status'); 37 | app.get(apiPrefix + '/status', st.show); 38 | 39 | app.get(apiPrefix + '/sync', st.sync); 40 | app.get(apiPrefix + '/peer', st.peer); 41 | 42 | // Currency 43 | var currency = require('../app/controllers/currency'); 44 | app.get(apiPrefix + '/currency', currency.index); 45 | 46 | //Home route 47 | var index = require('../app/controllers/index'); 48 | app.get(apiPrefix + '/version', index.version); 49 | app.get('*', index.render); 50 | }; 51 | -------------------------------------------------------------------------------- /app/controllers/currency.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = require('../../config/config'); 4 | 5 | // Set the initial vars 6 | var timestamp = +new Date(), 7 | delay = config.currencyRefresh * 60000, 8 | bitstampRate = 0; 9 | 10 | exports.index = function(req, res) { 11 | 12 | var _xhr = function() { 13 | if (typeof XMLHttpRequest !== 'undefined' && XMLHttpRequest !== null) { 14 | return new XMLHttpRequest(); 15 | } else if (typeof require !== 'undefined' && require !== null) { 16 | var XMLhttprequest = require('xmlhttprequest').XMLHttpRequest; 17 | return new XMLhttprequest(); 18 | } 19 | }; 20 | 21 | var _request = function(url, cb) { 22 | var request; 23 | request = _xhr(); 24 | request.open('GET', url, true); 25 | request.onreadystatechange = function() { 26 | if (request.readyState === 4) { 27 | if (request.status === 200) { 28 | return cb(false, request.responseText); 29 | } 30 | 31 | return cb(true, { 32 | status: request.status, 33 | message: 'Request error' 34 | }); 35 | } 36 | }; 37 | 38 | return request.send(null); 39 | }; 40 | 41 | // Init 42 | var currentTime = +new Date(); 43 | if (bitstampRate === 0 || currentTime >= (timestamp + delay)) { 44 | timestamp = currentTime; 45 | 46 | _request('https://www.bitstamp.net/api/ticker/', function(err, data) { 47 | if (!err) bitstampRate = parseFloat(JSON.parse(data).last); 48 | 49 | res.jsonp({ 50 | status: 200, 51 | data: { bitstamp: bitstampRate } 52 | }); 53 | }); 54 | } else { 55 | res.jsonp({ 56 | status: 200, 57 | data: { bitstamp: bitstampRate } 58 | }); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /test/integration/02-transactionouts.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | 7 | 8 | var 9 | assert = require('assert'), 10 | fs = require('fs'), 11 | util = require('util'), 12 | async = require('async'), 13 | config = require('../../config/config'), 14 | TransactionDb = require('../../lib/TransactionDb').class(); 15 | 16 | var spentValid = JSON.parse(fs.readFileSync('test/integration/spent.json')); 17 | 18 | var txDb; 19 | 20 | describe('TransactionDb Expenses', function(){ 21 | 22 | before(function(c) { 23 | txDb = new TransactionDb(); 24 | 25 | // lets spend! 26 | async.each(Object.keys(spentValid), 27 | function(txid,c_out) { 28 | async.each(spentValid[txid], 29 | function(i,c_in) { 30 | txDb.createFromArray([i.txid], null, function(err) { 31 | return c_in(); 32 | }); 33 | }, 34 | function(err) { 35 | return c_out(); 36 | } 37 | ); 38 | }, 39 | function(err) { 40 | return c(); 41 | } 42 | ); 43 | }); 44 | 45 | Object.keys(spentValid).forEach( function(txid) { 46 | it('test result of spending tx ' + txid, function(done) { 47 | var s = spentValid[txid]; 48 | var c=0; 49 | txDb.fromTxId( txid, function(err, readItems) { 50 | s.forEach( function(v) { 51 | assert.equal(readItems[c].spentTxId,v.txid); 52 | assert.equal(readItems[c].spentIndex,v.n); 53 | c++; 54 | }); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/integration/txitems.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "disabled": 1, 4 | "txid": "75c5ffe6dc2eb0f6bd011a08c041ef115380ccd637d859b379506a0dca4c26fc" 5 | }, 6 | { 7 | "txid": "21798ddc9664ac0ef618f52b151dda82dafaf2e26d2bbef6cdaf55a6957ca237", 8 | "toRm": [ 9 | "txs-86a03cac7d87f596008c6d5a8d3fd8b88842932ea6f0337673eda16f6b472f7f-0", 10 | "txs-bcd8da8ee847da377f8aaca92502c05e5f914c6a2452753146013b0e642a25a0-0" 11 | ], 12 | "items": [ 13 | { 14 | "addr": "mzjLe62faUqCSjkwQkwPAL5nYyR8K132fA", 15 | "value_sat": 134574000, 16 | "index": 0 17 | }, 18 | { 19 | "addr": "n28wb1cRGxPtfmsenYKFfsvnZ6kRapx3jF", 20 | "value_sat": 31600000, 21 | "index": 1 22 | } 23 | ] 24 | }, 25 | { 26 | "txid": "b633a6249d4a2bc123e7f8a151cae2d4afd17aa94840009f8697270c7818ceee", 27 | "toRm": [ 28 | "txs-01621403689cb4a95699a3dbae029d7031c5667678ef14e2054793954fb27917-0" 29 | ], 30 | "items": [ 31 | { 32 | "addr": "mhfQJUSissP6nLM5pz6DxHfctukrrLct2T", 33 | "value_sat": 19300000, 34 | "index": 0 35 | }, 36 | { 37 | "addr": "mzcDhbL877ES3MGftWnc3EuTSXs3WXDDML", 38 | "value_sat": 21440667, 39 | "index": 1 40 | } 41 | ] 42 | }, 43 | { 44 | "txid": "ca2f42e44455b8a84434de139efea1fe2c7d71414a8939e0a20f518849085c3b", 45 | "toRm": [ 46 | "txs-2d7b680fb06e4d7eeb65ca49ac7522276586e0090b7fe662fc708129429c5e6a-0" 47 | ], 48 | "items": [ 49 | { 50 | "addr": "mhqyL1nDQDo1WLH9qH8sjRjx2WwrnmAaXE", 51 | "value_sat": 1327746, 52 | "index": 0 53 | }, 54 | { 55 | "addr": "mkGrySSnxcqRbtPCisApj3zXCQVmUUWbf1", 56 | "value_sat": 1049948, 57 | "index": 1 58 | } 59 | ] 60 | } 61 | 62 | ] 63 | -------------------------------------------------------------------------------- /dev-util/stats: -------------------------------------------------------------------------------- 1 | 2 | 3 | first 5% 4 | 5 | => with data + mongo + w/RPC for blocks: 48.8s 6 | => with RPC + mongo: 2m26s 7 | => with files + mongo + wo/RPC for blocks: 36.7s 8 | => with files + mongo + wo/RPC for blocks + wo/mongoIndexes: 9 | 10 | 11 | first 10% 12 | 13 | => sin RPC, sin Tx, sin store block => 0.7s 14 | => sin RPC, sin grabar, procesando TX => 8.5s 15 | => sin RPC, sin TX processing, sin grabar => 12s28 16 | => con RPC, TX processing, sin Grabar Tx, grabando bloques => 29s 17 | => con RPC, sin TX processing, sin Grabar Tx, grabando bloques => 35s 18 | => con RPC, TX processing, sin Grabar Tx, grabando bloques => 43s 19 | 20 | => TX processing, sin RPC, sin saves TX, y blocks => 11.6s 21 | => TX processing, CON RPC, sin saves TX, y blocks => 35s 22 | => con RPC, TX processing, sin saves TX => 45s 23 | => con RPC, TX processing, Grabarndo todo => 78s 24 | => con RPC, TX processing, Grabarndo todo => 78s 25 | (18k blocks, 36k txouts) 26 | 27 | //LEVEL DB 28 | => sin RPC, TX processing, todo en level => 14s 29 | => con RPC, TX processing, todo en level => 39.7s 30 | => con RPC, TX processing, tx mongo, blocks en level => 64s 31 | 32 | 33 | => sin RPC, TX processing, todo en level, handling REORGs, more data => 28s 34 | => sin RPC, TX processing, todo en level, handling REORGs, more data, tx ts => 34t s 35 | 36 | 37 | //FROM blk00002.dat (more txs), 5% 38 | 39 | => now total : 1m13s 40 | => removing block writes => 1m8s 41 | => sacando los contenidos adentro de getblock from file de => 4.5s!! 42 | 43 | => con base58 cpp => 21s 44 | => toda la testnet => 17m 45 | 46 | 10% de blk2 47 | => 50s con base58cpp 48 | => 41s commentando todo addr 49 | => 5s commentando todo get HistoricSync.prototype.getBlockFromFile = function(cb) { 50 | => 15s commentando todo get HistoricSync.prototype.getBlockFromFile = function(cb) { 51 | 52 | 10% de blk 1 53 | => 59s 54 | => 15s comentando desde b.getStandardizedObject() 55 | => 39s comentando dps b.getStandardizedObject() 56 | 57 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. 3 | "browser": true, // Standard browser globals e.g. `window`, `document`. 4 | "esnext": true, // Allow ES.next specific features such as `const` and `let`. 5 | "bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.). 6 | "camelcase": false, // Permit only camelcase for `var` and `object indexes`. 7 | "curly": false, // Require {} for every new block or scope. 8 | "eqeqeq": true, // Require triple equals i.e. `===`. 9 | "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 10 | "latedef": true, // Prohibit variable use before definition. 11 | "newcap": true, // Require capitalization of all constructor functions e.g. `new F()`. 12 | "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. 13 | "quotmark": "single", // Define quotes to string values. 14 | "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. 15 | "undef": true, // Require all non-global variables be declared before they are used. 16 | "unused": true, // Warn unused variables. 17 | "strict": true, // Require `use strict` pragma in every file. 18 | "trailing": true, // Prohibit trailing whitespaces. 19 | "smarttabs": false, // Suppresses warnings about mixed tabs and spaces 20 | "globals": { // Globals variables. 21 | "angular": true 22 | }, 23 | "predef": [ // Extra globals. 24 | "define", 25 | "require", 26 | "exports", 27 | "module", 28 | "describe", 29 | "before", 30 | "beforeEach", 31 | "after", 32 | "afterEach", 33 | "it", 34 | "inject", 35 | "$", 36 | "io", 37 | "app", 38 | "moment" 39 | ], 40 | "indent": false, // Specify indentation spacing 41 | "devel": true, // Allow development statements e.g. `console.log();`. 42 | "noempty": true // Prohibit use of empty blocks. 43 | } 44 | 45 | -------------------------------------------------------------------------------- /config/express.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var express = require('express'), 7 | config = require('./config'), 8 | path = require('path'); 9 | 10 | module.exports = function(app, historicSync, peerSync) { 11 | 12 | 13 | //custom middleware 14 | var setHistoric = function(req, res, next) { 15 | req.historicSync = historicSync; 16 | next(); 17 | }; 18 | 19 | var setPeer = function(req, res, next) { 20 | req.peerSync = peerSync; 21 | next(); 22 | }; 23 | 24 | app.set('showStackError', true); 25 | 26 | // Compress JSON outputs 27 | app.set('json spaces', 0); 28 | 29 | //Enable jsonp 30 | app.enable('jsonp callback'); 31 | 32 | app.use(config.apiPrefix + '/sync', setHistoric); 33 | app.use(config.apiPrefix + '/peer', setPeer); 34 | app.use(express.logger('dev')); 35 | app.use(express.json()); 36 | app.use(express.urlencoded()); 37 | app.use(express.methodOverride()); 38 | app.use(express.compress()); 39 | 40 | if (config.publicPath) { 41 | var staticPath = path.normalize(config.rootPath + '/../' + config.publicPath); 42 | 43 | //IMPORTANT: for html5mode, this line must to be before app.router 44 | app.use(express.static(staticPath)); 45 | } 46 | 47 | // manual helpers 48 | app.use(function(req, res, next) { 49 | app.locals.config = config; 50 | next(); 51 | }); 52 | 53 | //routes should be at the last 54 | app.use(app.router); 55 | 56 | //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. 57 | app.use(function(err, req, res, next) { 58 | //Treat as 404 59 | if (~err.message.indexOf('not found')) return next(); 60 | 61 | //Log it 62 | console.error(err.stack); 63 | 64 | //Error page 65 | res.status(500).jsonp({ 66 | status: 500, 67 | error: err.stack 68 | }); 69 | }); 70 | 71 | //Assume 404 since no middleware responded 72 | app.use(function(req, res) { 73 | res.status(404).jsonp({ 74 | status: 404, 75 | url: req.originalUrl, 76 | error: 'Not found' 77 | }); 78 | }); 79 | }; 80 | 81 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'), 4 | rootPath = path.normalize(__dirname + '/..'), 5 | env, 6 | db, 7 | port, 8 | b_port, 9 | p2p_port; 10 | 11 | if (process.env.INSIGHT_NETWORK === 'livenet') { 12 | env = 'livenet'; 13 | db = rootPath + '/db'; 14 | port = '3000'; 15 | b_port = '8332'; 16 | p2p_port = '8333'; 17 | } 18 | else { 19 | env = 'testnet'; 20 | db = rootPath + '/db/testnet'; 21 | port = '3001'; 22 | b_port = '18332'; 23 | p2p_port = '18333'; 24 | } 25 | 26 | switch(process.env.NODE_ENV) { 27 | case 'production': 28 | env += ''; 29 | break; 30 | case 'test': 31 | env += ' - test environment'; 32 | break; 33 | default: 34 | env += ' - development'; 35 | break; 36 | } 37 | var network = process.env.INSIGHT_NETWORK || 'testnet'; 38 | 39 | var dataDir = process.env.BITCOIND_DATADIR; 40 | var isWin = /^win/.test(process.platform); 41 | var isMac = /^darwin/.test(process.platform); 42 | var isLinux = /^linux/.test(process.platform); 43 | if (!dataDir) { 44 | if (isWin) dataDir = '%APPDATA%\\Bitcoin\\'; 45 | if (isMac) dataDir = process.env.HOME + '/Library/Application Support/Bitcoin/'; 46 | if (isLinux) dataDir = process.env.HOME + '/.bitcoin/'; 47 | } 48 | dataDir += network === 'testnet' ? 'testnet3' : ''; 49 | 50 | module.exports = { 51 | root: rootPath, 52 | publicPath: process.env.INSIGHT_PUBLIC_PATH || false, 53 | appName: 'Insight ' + env, 54 | apiPrefix: '/api', 55 | port: port, 56 | leveldb: db, 57 | bitcoind: { 58 | protocol: process.env.BITCOIND_PROTO || 'http', 59 | user: process.env.BITCOIND_USER || 'user', 60 | pass: process.env.BITCOIND_PASS || 'pass', 61 | host: process.env.BITCOIND_HOST || '127.0.0.1', 62 | port: process.env.BITCOIND_PORT || b_port, 63 | p2pPort: process.env.BITCOIND_P2P_PORT || p2p_port, 64 | dataDir: dataDir, 65 | // DO NOT CHANGE THIS! 66 | disableAgent: true 67 | }, 68 | network: network, 69 | disableP2pSync: false, 70 | disableHistoricSync: false, 71 | poolMatchFile: rootPath + '/etc/minersPoolStrings.json', 72 | 73 | // Time to refresh the currency rate. In minutes 74 | currencyRefresh: 10, 75 | keys: { 76 | segmentio: process.env.INSIGHT_SEGMENTIO_KEY 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /test/integration/blockExtractor.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | 7 | 8 | var assert = require('assert'), 9 | config = require('../../config/config'), 10 | BlockExtractor = require('../../lib/BlockExtractor').class(), 11 | networks = require('bitcore/networks'), 12 | util = require('bitcore/util/util'); 13 | 14 | //var txItemsValid = JSON.parse(fs.readFileSync('test/model/txitems.json')); 15 | 16 | describe('BlockExtractor', function(){ 17 | 18 | var be = new BlockExtractor(config.bitcoind.dataDir, config.network); 19 | 20 | var network = config.network === 'testnet' ? networks.testnet: networks.livenet; 21 | 22 | it('should glob block files ', function(done) { 23 | assert(be.files.length>0); 24 | done(); 25 | }); 26 | 27 | var lastTs; 28 | 29 | it('should read genesis block ', function(done) { 30 | be.getNextBlock(function(err,b) { 31 | assert(!err); 32 | var genesisHashReversed = new Buffer(32); 33 | network.genesisBlock.hash.copy(genesisHashReversed); 34 | var genesis = util.formatHashFull(network.genesisBlock.hash); 35 | 36 | assert.equal(util.formatHashFull(b.hash),genesis); 37 | assert.equal(b.nounce,network.genesisBlock.nounce); 38 | assert.equal(b.timestamp,network.genesisBlock.timestamp); 39 | assert.equal(b.merkle_root.toString('hex'),network.genesisBlock.merkle_root.toString('hex')); 40 | 41 | lastTs = b.timestamp; 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should read next testnet block ', function(done) { 47 | be.getNextBlock(function(err,b) { 48 | assert(!err); 49 | assert(b.timestamp > lastTs, 'timestamp > genesis_ts'); 50 | done(); 51 | }); 52 | }); 53 | 54 | it.skip('should read 100000 blocks with no error ', function(done) { 55 | 56 | var i=0; 57 | while(i++<100000) { 58 | be.getNextBlock(function(err,b) { 59 | assert(!err,err); 60 | assert(lastTs < b.timestamp, 'genesisTS < b.timestamp: ' + lastTs + '<' + b.timestamp + ":" + i); 61 | if(i % 1000 === 1) process.stdout.write('.'); 62 | if(i === 100000) done(); 63 | }); 64 | } 65 | }); 66 | 67 | 68 | 69 | }); 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /insight.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | //Set the node enviornment variable if not set before 5 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | var express = require('express'), 11 | fs = require('fs'), 12 | PeerSync = require('./lib/PeerSync').class(), 13 | HistoricSync = require('./lib/HistoricSync').class(); 14 | 15 | //Initializing system variables 16 | var config = require('./config/config'); 17 | 18 | /** 19 | * express app 20 | */ 21 | var expressApp = express(); 22 | 23 | /** 24 | * Bootstrap models 25 | */ 26 | var models_path = __dirname + '/app/models'; 27 | var walk = function(path) { 28 | fs.readdirSync(path).forEach(function(file) { 29 | var newPath = path + '/' + file; 30 | var stat = fs.statSync(newPath); 31 | if (stat.isFile()) { 32 | if (/(.*)\.(js$)/.test(file)) { 33 | require(newPath); 34 | } 35 | } 36 | else if (stat.isDirectory()) { 37 | walk(newPath); 38 | } 39 | }); 40 | }; 41 | 42 | walk(models_path); 43 | 44 | /** 45 | * p2pSync process 46 | */ 47 | 48 | var peerSync = new PeerSync({shouldBroadcast: true}); 49 | 50 | if (!config.disableP2pSync) { 51 | peerSync.run(); 52 | } 53 | 54 | /** 55 | * historic_sync process 56 | */ 57 | var historicSync = new HistoricSync({ 58 | shouldBroadcastSync: true 59 | }); 60 | peerSync.historicSync = historicSync; 61 | 62 | if (!config.disableHistoricSync) { 63 | historicSync.start({}, function(err){ 64 | if (err) { 65 | var txt = 'ABORTED with error: ' + err.message; 66 | console.log('[historic_sync] ' + txt); 67 | } 68 | if (peerSync) peerSync.allowReorgs = true; 69 | }); 70 | } 71 | else 72 | if (peerSync) peerSync.allowReorgs = true; 73 | 74 | //express settings 75 | require('./config/express')(expressApp, historicSync, peerSync); 76 | 77 | //Bootstrap routes 78 | require('./config/routes')(expressApp); 79 | 80 | // socket.io 81 | var server = require('http').createServer(expressApp); 82 | var ios = require('socket.io').listen(server); 83 | require('./app/controllers/socket.js').init(expressApp, ios); 84 | 85 | //Start the app by listening on 86 | server.listen(config.port, function(){ 87 | console.log('insight server listening on port %d in %s mode', server.address().port, process.env.NODE_ENV); 88 | }); 89 | 90 | //expose app 91 | exports = module.exports = expressApp; 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "insight-bitcore-api", 3 | "description": "An open-source bitcoin blockchain API. The Insight API provides you with a convenient, powerful and simple way to query and broadcast data on the bitcoin network and build your own services with it.", 4 | "version": "0.1.3", 5 | "author": { 6 | "name": "Ryan X Charles", 7 | "email": "ryan@bitpay.com" 8 | }, 9 | "repository": "git://github.com/bitpay/insight-api.git", 10 | "contributors": [ 11 | { 12 | "name": "Matias Alejo Garcia", 13 | "email": "ematiu@gmail.com" 14 | }, 15 | { 16 | "name": "Manuel Araoz", 17 | "email": "manuelaraoz@gmail.com" 18 | }, 19 | { 20 | "name": "Mario Colque", 21 | "email": "colquemario@gmail.com" 22 | }, 23 | { 24 | "name": "Gustavo Cortez", 25 | "email": "cmgustavo83@gmail.com" 26 | }, 27 | { 28 | "name": "Juan Ignacio Sosa Lopez", 29 | "email": "bechilandia@gmail.com" 30 | } 31 | ], 32 | "bugs": { 33 | "url": "https://github.com/bitpay/insight-api/issues" 34 | }, 35 | "homepage": "https://github.com/bitpay/insight-api", 36 | "license": "MIT", 37 | "keywords": [ 38 | "insight", 39 | "secret", 40 | "enigma", 41 | "riddle", 42 | "mystification", 43 | "puzzle", 44 | "conundrum", 45 | "api", 46 | "bitcore" 47 | ], 48 | "engines": { 49 | "node": "*" 50 | }, 51 | "scripts": { 52 | "start": "node node_modules/grunt-cli/bin/grunt" 53 | }, 54 | "dependencies": { 55 | "base58-native": "0.1.2", 56 | "async": "*", 57 | "leveldown": "*", 58 | "levelup": "*", 59 | "glob": "*", 60 | "classtool": "*", 61 | "commander": "*", 62 | "bignum": "*", 63 | "express": "~3.4.7", 64 | "buffertools": "*", 65 | "should": "~2.1.1", 66 | "socket.io": "~0.9.16", 67 | "moment": "~2.5.0", 68 | "sinon": "~1.7.3", 69 | "chai": "~1.8.1", 70 | "bitcore": "git://github.com/bitpay/bitcore.git", 71 | "bufferput": "git://github.com/bitpay/node-bufferput.git", 72 | "xmlhttprequest": "~1.6.0" 73 | }, 74 | "devDependencies": { 75 | "grunt": "~0.4.2", 76 | "grunt-cli": "~0.1.11", 77 | "grunt-env": "~0.4.1", 78 | "grunt-contrib-jshint": "~0.8.0", 79 | "grunt-contrib-watch": "~0.5.3", 80 | "grunt-concurrent": "~0.4.2", 81 | "grunt-nodemon": "~0.2.0", 82 | "grunt-mocha-test": "~0.8.1", 83 | "should": "2.1.1", 84 | "grunt-markdown": "~0.5.0" 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | //Load NPM tasks 6 | grunt.loadNpmTasks('grunt-contrib-watch'); 7 | grunt.loadNpmTasks('grunt-contrib-jshint'); 8 | grunt.loadNpmTasks('grunt-mocha-test'); 9 | grunt.loadNpmTasks('grunt-nodemon'); 10 | grunt.loadNpmTasks('grunt-concurrent'); 11 | grunt.loadNpmTasks('grunt-env'); 12 | grunt.loadNpmTasks('grunt-markdown'); 13 | 14 | // Project Configuration 15 | grunt.initConfig({ 16 | pkg: grunt.file.readJSON('package.json'), 17 | watch: { 18 | readme: { 19 | files: ['README.md'], 20 | tasks: ['markdown'] 21 | }, 22 | js: { 23 | files: ['Gruntfile.js', 'insight.js', 'app/**/*.js'], 24 | tasks: ['jshint'], 25 | options: { 26 | livereload: true, 27 | }, 28 | }, 29 | // we monitor only app/models/* because we have test for models only now 30 | // test: { 31 | // files: ['test/**/*.js', 'test/*.js','app/models/*.js'], 32 | // tasks: ['test'], 33 | // } 34 | }, 35 | jshint: { 36 | all: { 37 | src: ['Gruntfile.js', 'insight.js', 'app/**/*.js', 'lib/*.js', 'config/*.js'], 38 | options: { 39 | jshintrc: true 40 | } 41 | } 42 | }, 43 | mochaTest: { 44 | options: { 45 | reporter: 'spec', 46 | }, 47 | src: ['test/**/*.js'], 48 | }, 49 | nodemon: { 50 | dev: { 51 | script: 'insight.js', 52 | options: { 53 | args: [], 54 | ignore: ['test/**/*', 'util/**/*', 'dev-util/**/*'], 55 | // nodeArgs: ['--debug'], 56 | delayTime: 1, 57 | env: { 58 | PORT: 3000 59 | }, 60 | cwd: __dirname 61 | } 62 | } 63 | }, 64 | concurrent: { 65 | tasks: ['nodemon', 'watch'], 66 | options: { 67 | logConcurrentOutput: true 68 | } 69 | }, 70 | env: { 71 | test: { 72 | NODE_ENV: 'test' 73 | } 74 | }, 75 | markdown: { 76 | all: { 77 | files: [ 78 | { 79 | expand: true, 80 | src: 'README.md', 81 | dest: '.', 82 | ext: '.html' 83 | } 84 | ] 85 | } 86 | } 87 | }); 88 | 89 | //Making grunt default to force in order not to break the project. 90 | grunt.option('force', true); 91 | 92 | //Default task(s). 93 | grunt.registerTask('default', ['jshint', 'concurrent']); 94 | 95 | //Test task. 96 | grunt.registerTask('test', ['env:test', 'mochaTest']); 97 | }; 98 | 99 | -------------------------------------------------------------------------------- /test/integration/addr.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 6 | 7 | var assert = require('assert'), 8 | fs = require('fs'), 9 | Address = require('../../app/models/Address').class(), 10 | TransactionDb = require('../../lib/TransactionDb').class(), 11 | addrValid = JSON.parse(fs.readFileSync('test/integration/addr.json')), 12 | utxoValid = JSON.parse(fs.readFileSync('test/integration/utxo.json')); 13 | 14 | var txDb; 15 | describe('Address balances', function() { 16 | 17 | before(function(c) { 18 | txDb = new TransactionDb(); 19 | return c(); 20 | }); 21 | 22 | addrValid.forEach(function(v) { 23 | if (v.disabled) { 24 | console.log(v.addr + ' => disabled in JSON'); 25 | } else { 26 | it('Address info for: ' + v.addr, function(done) { 27 | this.timeout(5000); 28 | 29 | var a = new Address(v.addr, txDb); 30 | 31 | a.update(function(err) { 32 | if (err) done(err); 33 | assert.equal(v.addr, a.addrStr); 34 | assert.equal(a.unconfirmedTxApperances ,0, 'unconfirmedTxApperances: 0'); 35 | assert.equal(a.unconfirmedBalanceSat ,0, 'unconfirmedBalanceSat: 0'); 36 | if (v.txApperances) 37 | assert.equal(v.txApperances, a.txApperances, 'txApperances: ' + a.txApperances); 38 | if (v.totalReceived) assert.equal(v.totalReceived, a.totalReceived, 'received: ' + a.totalReceived); 39 | if (v.totalSent) assert.equal(v.totalSent, a.totalSent, 'send: ' + a.totalSent); 40 | 41 | if (v.balance) assert.equal(v.balance, a.balance, 'balance: ' + a.balance); 42 | 43 | if (v.transactions) { 44 | 45 | v.transactions.forEach(function(tx) { 46 | assert(a.transactions.indexOf(tx) > -1, 'have tx ' + tx); 47 | }); 48 | } 49 | done(); 50 | }); 51 | }); 52 | } 53 | }); 54 | 55 | }); 56 | 57 | 58 | describe('Address utxo', function() { 59 | utxoValid.forEach(function(v) { 60 | if (v.disabled) { 61 | console.log(v.addr + ' => disabled in JSON'); 62 | } else { 63 | it('Address utxo for: ' + v.addr, function(done) { 64 | this.timeout(50000); 65 | 66 | var a = new Address(v.addr, txDb); 67 | a.getUtxo(function(err, utxo) { 68 | if (err) done(err); 69 | assert.equal(v.addr, a.addrStr); 70 | if (v.length) assert.equal(v.length, utxo.length, 'length: ' + utxo.length); 71 | if (v.tx0id) assert.equal(v.tx0id, utxo[0].txid, 'have tx: ' + utxo[0].txid); 72 | if (v.tx0scriptPubKey) 73 | assert.equal(v.tx0scriptPubKey, utxo[0].scriptPubKey, 'have tx: ' + utxo[0].scriptPubKey); 74 | if (v.tx0amount) 75 | assert.equal(v.tx0amount, utxo[0].amount, 'amount: ' + utxo[0].amount); 76 | done(); 77 | }); 78 | }); 79 | } 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /app/models/Status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('classtool'); 4 | 5 | function spec() { 6 | var async = require('async'); 7 | var RpcClient = require('bitcore/RpcClient').class(); 8 | var BlockDb = require('../../lib/BlockDb').class(); 9 | var config = require('../../config/config'); 10 | var rpc = new RpcClient(config.bitcoind); 11 | 12 | function Status() { 13 | this.bDb = new BlockDb(); 14 | } 15 | 16 | Status.prototype.getInfo = function(next) { 17 | var that = this; 18 | async.series([ 19 | function (cb) { 20 | rpc.getInfo(function(err, info){ 21 | if (err) return cb(err); 22 | 23 | that.info = info.result; 24 | return cb(); 25 | }); 26 | }, 27 | ], function (err) { 28 | return next(err); 29 | }); 30 | }; 31 | 32 | Status.prototype.getDifficulty = function(next) { 33 | var that = this; 34 | async.series([ 35 | function (cb) { 36 | rpc.getDifficulty(function(err, df){ 37 | if (err) return cb(err); 38 | 39 | that.difficulty = df.result; 40 | return cb(); 41 | }); 42 | } 43 | ], function (err) { 44 | return next(err); 45 | }); 46 | }; 47 | 48 | Status.prototype.getTxOutSetInfo = function(next) { 49 | var that = this; 50 | async.series([ 51 | function (cb) { 52 | rpc.getTxOutSetInfo(function(err, txout){ 53 | if (err) return cb(err); 54 | 55 | that.txoutsetinfo = txout.result; 56 | return cb(); 57 | }); 58 | } 59 | ], function (err) { 60 | return next(err); 61 | }); 62 | }; 63 | 64 | Status.prototype.getBestBlockHash = function(next) { 65 | var that = this; 66 | async.series([ 67 | function (cb) { 68 | rpc.getBestBlockHash(function(err, bbh){ 69 | if (err) return cb(err); 70 | 71 | that.bestblockhash = bbh.result; 72 | return cb(); 73 | }); 74 | }, 75 | 76 | ], function (err) { 77 | return next(err); 78 | }); 79 | }; 80 | 81 | Status.prototype.getLastBlockHash = function(next) { 82 | var that = this; 83 | that.bDb.getTip(function(err,tip) { 84 | that.syncTipHash = tip; 85 | async.waterfall( 86 | [ 87 | function(callback){ 88 | rpc.getBlockCount(function(err, bc){ 89 | if (err) return callback(err); 90 | callback(null, bc.result); 91 | }); 92 | }, 93 | function(bc, callback){ 94 | rpc.getBlockHash(bc, function(err, bh){ 95 | if (err) return callback(err); 96 | callback(null, bh.result); 97 | }); 98 | } 99 | ], 100 | function (err, result) { 101 | that.lastblockhash = result; 102 | return next(); 103 | } 104 | ); 105 | }); 106 | }; 107 | 108 | return Status; 109 | 110 | } 111 | module.defineClass(spec); 112 | 113 | -------------------------------------------------------------------------------- /lib/Rpc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('classtool'); 4 | 5 | 6 | function spec(b) { 7 | var RpcClient = require('bitcore/RpcClient').class(), 8 | BitcoreBlock = require('bitcore/Block').class(), 9 | bitcoreUtil = require('bitcore/util/util'), 10 | util = require('util'), 11 | config = require('../config/config'); 12 | 13 | var bitcoreRpc = b.bitcoreRpc || new RpcClient(config.bitcoind); 14 | 15 | function Rpc() { 16 | } 17 | 18 | Rpc._parseTxResult = function(info) { 19 | var b = new Buffer(info.hex,'hex'); 20 | 21 | // remove fields we dont need, to speed and adapt the information 22 | delete info.hex; 23 | 24 | // Inputs => add index + coinBase flag 25 | var n =0; 26 | info.vin.forEach(function(i) { 27 | i.n = n++; 28 | if (i.coinbase) info.isCoinBase = true; 29 | if (i.scriptSig) delete i.scriptSig.hex; 30 | }); 31 | 32 | // Outputs => add total 33 | var valueOutSat = 0; 34 | info.vout.forEach( function(o) { 35 | valueOutSat += o.value * bitcoreUtil.COIN; 36 | delete o.scriptPubKey.hex; 37 | }); 38 | info.valueOut = parseInt(valueOutSat) / bitcoreUtil.COIN; 39 | info.size = b.length; 40 | 41 | return info; 42 | }; 43 | 44 | 45 | Rpc.errMsg = function(err) { 46 | var e = err; 47 | e.message += util.format(' [Host: %s:%d User:%s Using password:%s]', 48 | bitcoreRpc.host, 49 | bitcoreRpc.port, 50 | bitcoreRpc.user, 51 | bitcoreRpc.pass?'yes':'no' 52 | ); 53 | return e; 54 | }; 55 | 56 | Rpc.getTxInfo = function(txid, doNotParse, cb) { 57 | var self = this; 58 | 59 | if (typeof doNotParse === 'function') { 60 | cb = doNotParse; 61 | doNotParse = false; 62 | } 63 | 64 | bitcoreRpc.getRawTransaction(txid, 1, function(err, txInfo) { 65 | // Not found? 66 | if (err && err.code === -5) return cb(); 67 | if (err) return cb(self.errMsg(err)); 68 | 69 | var info = doNotParse ? txInfo.result : self._parseTxResult(txInfo.result); 70 | return cb(null,info); 71 | }); 72 | }; 73 | 74 | 75 | Rpc.blockIndex = function(height, cb) { 76 | var self = this; 77 | 78 | bitcoreRpc.getBlockHash(height, function(err, bh){ 79 | if (err) return cb(self.errMsg(err)); 80 | cb(null, { blockHash: bh.result }); 81 | }); 82 | }; 83 | 84 | Rpc.getBlock = function(hash, cb) { 85 | var self = this; 86 | 87 | bitcoreRpc.getBlock(hash, function(err,info) { 88 | // Not found? 89 | if (err && err.code === -5) return cb(); 90 | if (err) return cb(self.errMsg(err)); 91 | 92 | 93 | if (info.result.height) 94 | info.result.reward = BitcoreBlock.getBlockValue(info.result.height) / bitcoreUtil.COIN ; 95 | 96 | return cb(err,info.result); 97 | }); 98 | }; 99 | 100 | Rpc.sendRawTransaction = function(rawtx, cb) { 101 | var self = this; 102 | bitcoreRpc.sendRawTransaction(rawtx, function(err, txid) { 103 | if (err && err.code === -5) return cb(err); // transaction already in block chain 104 | if (err) return cb(self.errMsg(err)); 105 | 106 | return cb(err, txid.result); 107 | }); 108 | }; 109 | 110 | return Rpc; 111 | } 112 | module.defineClass(spec); 113 | 114 | 115 | -------------------------------------------------------------------------------- /app/controllers/transactions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var Address = require('../models/Address'); 7 | var async = require('async'); 8 | var common = require('./common'); 9 | 10 | var TransactionDb = require('../../lib/TransactionDb').class(); 11 | var BlockDb = require('../../lib/BlockDb').class(); 12 | var Rpc = require('../../lib/Rpc').class(); 13 | 14 | var tDb = new TransactionDb(); 15 | var bdb = new BlockDb(); 16 | 17 | exports.send = function(req, res) { 18 | Rpc.sendRawTransaction(req.body.rawtx, function(err, txid) { 19 | if (err) return common.handleErrors(err, res); 20 | res.json({'txid' : txid}); 21 | }); 22 | }; 23 | 24 | 25 | /** 26 | * Find transaction by hash ... 27 | */ 28 | exports.transaction = function(req, res, next, txid) { 29 | 30 | tDb.fromIdWithInfo(txid, function(err, tx) { 31 | if (err || ! tx) 32 | return common.handleErrors(err, res); 33 | else { 34 | req.transaction = tx.info; 35 | return next(); 36 | } 37 | }); 38 | }; 39 | 40 | 41 | /** 42 | * Show transaction 43 | */ 44 | exports.show = function(req, res) { 45 | 46 | if (req.transaction) { 47 | res.jsonp(req.transaction); 48 | } 49 | }; 50 | 51 | 52 | var getTransaction = function(txid, cb) { 53 | 54 | tDb.fromIdWithInfo(txid, function(err, tx) { 55 | if (err) console.log(err); 56 | 57 | if (!tx || !tx.info) { 58 | console.log('[transactions.js.48]:: TXid %s not found in RPC. CHECK THIS.', txid); 59 | return ({ txid: txid }); 60 | } 61 | 62 | return cb(null, tx.info); 63 | }); 64 | }; 65 | 66 | 67 | /** 68 | * List of transaction 69 | */ 70 | exports.list = function(req, res, next) { 71 | var bId = req.query.block; 72 | var addrStr = req.query.address; 73 | var page = req.query.pageNum; 74 | var pageLength = 10; 75 | var pagesTotal = 1; 76 | var txLength; 77 | var txs; 78 | 79 | if (bId) { 80 | bdb.fromHashWithInfo(bId, function(err, block) { 81 | if (err) { 82 | console.log(err); 83 | return res.status(500).send('Internal Server Error'); 84 | } 85 | 86 | if (! block) { 87 | return res.status(404).send('Not found'); 88 | } 89 | 90 | txLength = block.info.tx.length; 91 | 92 | if (page) { 93 | var spliceInit = page * pageLength; 94 | txs = block.info.tx.splice(spliceInit, pageLength); 95 | pagesTotal = Math.ceil(txLength / pageLength); 96 | } 97 | else { 98 | txs = block.info.tx; 99 | } 100 | 101 | async.mapSeries(txs, getTransaction, function(err, results) { 102 | if (err) { 103 | console.log(err); 104 | res.status(404).send('TX not found'); 105 | } 106 | 107 | res.jsonp({ 108 | pagesTotal: pagesTotal, 109 | txs: results 110 | }); 111 | }); 112 | }); 113 | } 114 | else if (addrStr) { 115 | var a = Address.new(addrStr); 116 | 117 | a.update(function(err) { 118 | if (err && !a.totalReceivedSat) { 119 | console.log(err); 120 | res.status(404).send('Invalid address'); 121 | return next(); 122 | } 123 | 124 | txLength = a.transactions.length; 125 | 126 | if (page) { 127 | var spliceInit = page * pageLength; 128 | txs = a.transactions.splice(spliceInit, pageLength); 129 | pagesTotal = Math.ceil(txLength / pageLength); 130 | } 131 | else { 132 | txs = a.transactions; 133 | } 134 | 135 | async.mapSeries(txs, getTransaction, function(err, results) { 136 | if (err) { 137 | console.log(err); 138 | res.status(404).send('TX not found'); 139 | } 140 | 141 | res.jsonp({ 142 | pagesTotal: pagesTotal, 143 | txs: results 144 | }); 145 | }); 146 | }); 147 | } 148 | else { 149 | res.jsonp({ 150 | txs: [] 151 | }); 152 | } 153 | }; 154 | -------------------------------------------------------------------------------- /lib/PeerSync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('classtool'); 3 | 4 | function spec() { 5 | var fs = require('fs'); 6 | var bitcoreUtil = require('bitcore/util/util'); 7 | var Sync = require('./Sync').class(); 8 | var Peer = require('bitcore/Peer').class(); 9 | var config = require('../config/config'); 10 | var networks = require('bitcore/networks'); 11 | 12 | var peerdb_fn = 'peerdb.json'; 13 | 14 | function PeerSync(opts) { 15 | this.connected = false; 16 | this.peerdb = undefined; 17 | this.allowReorgs = false; 18 | this.PeerManager = require('bitcore/PeerManager').createClass({ 19 | network: (config.network === 'testnet' ? networks.testnet : networks.livenet) 20 | }); 21 | this.peerman = new this.PeerManager(); 22 | this.load_peers(); 23 | this.sync = new Sync(opts); 24 | } 25 | 26 | PeerSync.prototype.load_peers = function() { 27 | this.peerdb = [{ 28 | ipv4: config.bitcoind.host, 29 | port: config.bitcoind.p2pPort 30 | }]; 31 | 32 | fs.writeFileSync(peerdb_fn, JSON.stringify(this.peerdb)); 33 | }; 34 | 35 | PeerSync.prototype.info = function() { 36 | return { 37 | connected: this.connected, 38 | host: this.peerdb[0].ipv4, 39 | port: this.peerdb[0].port 40 | }; 41 | }; 42 | 43 | PeerSync.prototype.handleInv = function(info) { 44 | var invs = info.message.invs; 45 | info.conn.sendGetData(invs); 46 | }; 47 | 48 | PeerSync.prototype.handleTx = function(info) { 49 | var tx = info.message.tx.getStandardizedObject(); 50 | tx.outs = info.message.tx.outs; 51 | tx.ins = info.message.tx.ins; 52 | console.log('[p2p_sync] Handle tx: ' + tx.hash); 53 | tx.time = tx.time || Math.round(new Date().getTime() / 1000); 54 | 55 | this.sync.storeTxs([tx], function(err) { 56 | if (err) { 57 | console.log('[p2p_sync] Error in handle TX: ' + JSON.stringify(err)); 58 | } 59 | }); 60 | }; 61 | 62 | PeerSync.prototype.handleBlock = function(info) { 63 | var self = this; 64 | var block = info.message.block; 65 | var blockHash = bitcoreUtil.formatHashFull(block.calcHash()); 66 | 67 | console.log('[p2p_sync] Handle block: %s (allowReorgs: %s)', blockHash, self.allowReorgs); 68 | 69 | var tx_hashes = block.txs.map(function(tx) { 70 | return bitcoreUtil.formatHashFull(tx.hash); 71 | }); 72 | 73 | this.sync.storeTipBlock({ 74 | 'hash': blockHash, 75 | 'tx': tx_hashes, 76 | 'previousblockhash': bitcoreUtil.formatHashFull(block.prev_hash), 77 | }, self.allowReorgs, function(err) { 78 | if (err && err.message.match(/NEED_SYNC/) && self.historicSync) { 79 | console.log('[p2p_sync] Orphan block received. Triggering sync'); 80 | self.historicSync.start({}, function(){ 81 | console.log('[p2p_sync] Done resync.'); 82 | }); 83 | } 84 | else if (err) { 85 | console.log('[p2p_sync] Error in handle Block: ' + err); 86 | } 87 | }); 88 | }; 89 | 90 | PeerSync.prototype.handle_connected = function(data) { 91 | var peerman = data.pm; 92 | var peers_n = peerman.peers.length; 93 | console.log('[p2p_sync] Connected to ' + peers_n + ' peer' + (peers_n !== 1 ? 's' : '')); 94 | }; 95 | 96 | PeerSync.prototype.run = function() { 97 | var self = this; 98 | 99 | this.peerdb.forEach(function(datum) { 100 | var peer = new Peer(datum.ipv4, datum.port); 101 | self.peerman.addPeer(peer); 102 | }); 103 | 104 | this.peerman.on('connection', function(conn) { 105 | self.connected = true; 106 | conn.on('inv', self.handleInv.bind(self)); 107 | conn.on('block', self.handleBlock.bind(self)); 108 | conn.on('tx', self.handleTx.bind(self)); 109 | }); 110 | this.peerman.on('connect', self.handle_connected.bind(self)); 111 | 112 | this.peerman.on('netDisconnected', function() { 113 | self.connected = false; 114 | }); 115 | 116 | this.peerman.start(); 117 | }; 118 | 119 | PeerSync.prototype.close = function() { 120 | this.sync.close(); 121 | }; 122 | 123 | 124 | return PeerSync; 125 | 126 | } 127 | module.defineClass(spec); 128 | -------------------------------------------------------------------------------- /etc/minersPoolStrings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "poolName":"50BTC", 4 | "url":"https://50btc.com/", 5 | "searchStrings":[ 6 | "50BTC.com", 7 | "50btc.com" 8 | ] 9 | }, 10 | { 11 | "poolName":"175btc", 12 | "url":"http://www.175btc.com/", 13 | "searchStrings":[ 14 | "Mined By 175btc.com" 15 | ] 16 | }, 17 | { 18 | "poolName":"ASICminer", 19 | "url":"https://bitcointalk.org/index.php?topic=99497.0", 20 | "searchStrings":[ 21 | "Mined By ASICMiner" 22 | ] 23 | }, 24 | { 25 | "poolName":"BitMinter", 26 | "url":"https://bitminter.com/", 27 | "searchStrings":[ 28 | "BitMinter" 29 | ] 30 | }, 31 | { 32 | "poolName":"Bitparking", 33 | "url":"http://bitparking.com/", 34 | "searchStrings":[ 35 | "bitparking" 36 | ] 37 | }, 38 | { 39 | "poolName":"BTC Guild", 40 | "url":"https://www.btcguild.com/", 41 | "searchStrings":[ 42 | "Mined by BTC Guild", 43 | "BTC Guild" 44 | ] 45 | }, 46 | { 47 | "poolName":"Discus Fish", 48 | "url":"http://f2pool.com/", 49 | "searchStrings":[ 50 | "七彩神仙鱼", 51 | "Made in China", 52 | "Mined by user" 53 | ] 54 | }, 55 | { 56 | "poolName":"Discus Fish Solo", 57 | "url":"http://f2pool.com/", 58 | "searchStrings":[ 59 | "For Pierce and Paul" 60 | ] 61 | }, 62 | { 63 | "poolName":"Eligius", 64 | "url":"http://eligius.st/", 65 | "searchStrings":[ 66 | "Eligius" 67 | ] 68 | }, 69 | { 70 | "poolName":"EclipseMC", 71 | "url":"https://eclipsemc.com/", 72 | "searchStrings":[ 73 | "Josh Zerlan was here!", 74 | "EclipseMC", 75 | "Aluminum Falcon" 76 | ] 77 | }, 78 | { 79 | "poolName":"GIVE-ME-COINS", 80 | "url":"https://give-me-coins.com/", 81 | "searchStrings":[ 82 | "Mined at GIVE-ME-COINS.com" 83 | ] 84 | }, 85 | { 86 | "poolName":"ghash.io", 87 | "url":"https://ghash.io/", 88 | "searchStrings":[ 89 | "ghash.io", 90 | "GHash.IO" 91 | ] 92 | }, 93 | { 94 | "poolName":"HHTT", 95 | "url":"http://hhtt.1209k.com/", 96 | "searchStrings":[ 97 | "HHTT" 98 | ] 99 | }, 100 | { 101 | "poolName":"Megabigpower", 102 | "url":"http://megabigpower.com/", 103 | "searchStrings":[ 104 | "megabigpower.com" 105 | ] 106 | }, 107 | { 108 | "poolName":"Mt Red", 109 | "url":"https://mtred.com/‎", 110 | "searchStrings":[ 111 | "/mtred/" 112 | ] 113 | }, 114 | { 115 | "poolName":"MaxBTC", 116 | "url":"https://www.maxbtc.com", 117 | "searchStrings":[ 118 | "MaxBTC" 119 | ] 120 | }, 121 | { 122 | "poolName":"NMCbit", 123 | "url":"http://nmcbit.com/", 124 | "searchStrings":[ 125 | "nmcbit.com" 126 | ] 127 | }, 128 | { 129 | "poolName":"ozcoin", 130 | "url":"https://ozco.in/", 131 | "searchStrings":[ 132 | "ozco.in", 133 | "ozcoin" 134 | ] 135 | }, 136 | { 137 | "poolName":"Polmine.pl", 138 | "url":"https://polmine.pl/‎", 139 | "searchStrings":[ 140 | "by polmine.pl" 141 | ] 142 | }, 143 | { 144 | "poolName":"simplecoin", 145 | "url":"http://simplecoin.us/", 146 | "searchStrings":[ 147 | "simplecoin.us ftw" 148 | ] 149 | }, 150 | { 151 | "poolName":"Slush", 152 | "url":"https://mining.bitcoin.cz/", 153 | "searchStrings":[ 154 | "slush" 155 | ] 156 | }, 157 | { 158 | "poolName":"TripleMining", 159 | "url":"https://www.triplemining.com/", 160 | "searchStrings":[ 161 | "Triplemining.com" 162 | ] 163 | }, 164 | { 165 | "poolName":"wizkid057", 166 | "url":"http://wizkid057.com/btc", 167 | "searchStrings":[ 168 | "wizkid057" 169 | ] 170 | }, 171 | { 172 | "poolName":"Yourbtc.net", 173 | "url":"http://yourbtc.net/", 174 | "searchStrings":[ 175 | "yourbtc.net" 176 | ] 177 | } 178 | ] 179 | 180 | -------------------------------------------------------------------------------- /app/controllers/blocks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var common = require('./common'), 7 | async = require('async'), 8 | BlockDb = require('../../lib/BlockDb').class(); 9 | 10 | var bdb = new BlockDb(); 11 | 12 | /** 13 | * Find block by hash ... 14 | */ 15 | exports.block = function(req, res, next, hash) { 16 | bdb.fromHashWithInfo(hash, function(err, block) { 17 | if (err || !block) 18 | return common.handleErrors(err, res, next); 19 | else { 20 | bdb.getPoolInfo(block.info.tx[0], function(info) { 21 | block.info.poolInfo = info; 22 | req.block = block.info; 23 | return next(); 24 | }); 25 | } 26 | }); 27 | }; 28 | 29 | 30 | /** 31 | * Show block 32 | */ 33 | exports.show = function(req, res) { 34 | if (req.block) { 35 | res.jsonp(req.block); 36 | } 37 | }; 38 | 39 | /** 40 | * Show block by Height 41 | */ 42 | exports.blockindex = function(req, res, next, height) { 43 | bdb.blockIndex(height, function(err, hashStr) { 44 | if (err) { 45 | console.log(err); 46 | res.status(400).send('Bad Request'); // TODO 47 | } else { 48 | res.jsonp(hashStr); 49 | } 50 | }); 51 | }; 52 | 53 | var getBlock = function(blockhash, cb) { 54 | bdb.fromHashWithInfo(blockhash, function(err, block) { 55 | if (err) { 56 | console.log(err); 57 | return cb(err); 58 | } 59 | 60 | // TODO 61 | if (!block.info) { 62 | console.log('[blocks.js.60]: could not get %s from RPC. Orphan? Error?', blockhash); //TODO 63 | // Probably orphan 64 | block.info = { 65 | hash: blockhash, 66 | isOrphan: 1, 67 | }; 68 | } 69 | 70 | bdb.getPoolInfo(block.info.tx[0], function(info) { 71 | block.info.poolInfo = info; 72 | return cb(err, block.info); 73 | }); 74 | 75 | }); 76 | }; 77 | 78 | /** 79 | * List of blocks by date 80 | */ 81 | exports.list = function(req, res) { 82 | var isToday = false; 83 | 84 | //helper to convert timestamps to yyyy-mm-dd format 85 | var formatTimestamp = function(date) { 86 | var yyyy = date.getUTCFullYear().toString(); 87 | var mm = (date.getUTCMonth() + 1).toString(); // getMonth() is zero-based 88 | var dd = date.getUTCDate().toString(); 89 | 90 | return yyyy + '-' + (mm[1] ? mm : '0' + mm[0]) + '-' + (dd[1] ? dd : '0' + dd[0]); //padding 91 | }; 92 | 93 | var dateStr; 94 | var todayStr = formatTimestamp(new Date()); 95 | 96 | if (req.query.blockDate) { 97 | // TODO: Validate format yyyy-mm-dd 98 | dateStr = req.query.blockDate; 99 | isToday = dateStr === todayStr; 100 | } else { 101 | dateStr = todayStr; 102 | isToday = true; 103 | } 104 | 105 | var gte = Math.round((new Date(dateStr)).getTime() / 1000); 106 | 107 | //pagination 108 | var lte = gte + 86400; 109 | var prev = formatTimestamp(new Date((gte - 86400) * 1000)); 110 | var next = formatTimestamp(new Date(lte * 1000)); 111 | 112 | bdb.getBlocksByDate(gte, lte, function(err, blocks) { 113 | if (err) { 114 | res.status(500).send(err); 115 | } else { 116 | var blockList = []; 117 | var l = blocks.length; 118 | var limit = parseInt(req.query.limit || l); 119 | if (l < limit) limit = l; 120 | 121 | for (var i = 0; i < limit; i++) { 122 | blockList.push(blocks[i]); 123 | } 124 | 125 | async.mapSeries(blockList, 126 | function(b, cb) { 127 | getBlock(b.hash, function(err, info) { 128 | if (err) { 129 | console.log(err); 130 | return cb(err); 131 | } 132 | return cb(err, { 133 | height: info.height, 134 | size: info.size, 135 | hash: b.hash, 136 | time: b.ts || info.time, 137 | txlength: info.tx.length, 138 | poolInfo: info.poolInfo 139 | }); 140 | }); 141 | }, function(err, allblocks) { 142 | res.jsonp({ 143 | blocks: allblocks, 144 | length: allblocks.length, 145 | pagination: { 146 | next: next, 147 | prev: prev, 148 | currentTs: lte - 1, 149 | current: dateStr, 150 | isToday: isToday 151 | } 152 | }); 153 | }); 154 | } 155 | }); 156 | }; 157 | -------------------------------------------------------------------------------- /lib/BlockExtractor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('classtool'); 4 | 5 | function spec() { 6 | 7 | var Block = require('bitcore/Block').class(), 8 | networks = require('bitcore/networks'), 9 | Parser = require('bitcore/util/BinaryParser').class(), 10 | fs = require('fs'), 11 | Buffer = require('buffer').Buffer, 12 | glob = require('glob'), 13 | async = require('async'); 14 | 15 | function BlockExtractor(dataDir, network) { 16 | 17 | var self = this; 18 | var path = dataDir + '/blocks/blk*.dat'; 19 | 20 | self.dataDir = dataDir; 21 | self.files = glob.sync(path); 22 | self.nfiles = self.files.length; 23 | 24 | if (self.nfiles === 0) 25 | throw new Error('Could not find block files at: ' + path); 26 | 27 | self.currentFileIndex = 0; 28 | self.isCurrentRead = false; 29 | self.currentBuffer = null; 30 | self.currentParser = null; 31 | self.network = network === 'testnet' ? networks.testnet: networks.livenet; 32 | self.magic = self.network.magic.toString('hex'); 33 | } 34 | 35 | BlockExtractor.prototype.currentFile = function() { 36 | var self = this; 37 | 38 | return self.files[self.currentFileIndex]; 39 | }; 40 | 41 | 42 | BlockExtractor.prototype.nextFile = function() { 43 | var self = this; 44 | 45 | if (self.currentFileIndex < 0) return false; 46 | 47 | var ret = true; 48 | 49 | self.isCurrentRead = false; 50 | self.currentBuffer = null; 51 | self.currentParser = null; 52 | 53 | if (self.currentFileIndex < self.nfiles - 1) { 54 | self.currentFileIndex++; 55 | } 56 | else { 57 | self.currentFileIndex=-1; 58 | ret = false; 59 | } 60 | return ret; 61 | }; 62 | 63 | BlockExtractor.prototype.readCurrentFileSync = function() { 64 | var self = this; 65 | 66 | if (self.currentFileIndex < 0 || self.isCurrentRead) return; 67 | 68 | 69 | self.isCurrentRead = true; 70 | 71 | var fname = self.currentFile(); 72 | if (!fname) return; 73 | 74 | 75 | var stats = fs.statSync(fname); 76 | 77 | var size = stats.size; 78 | 79 | console.log('Reading Blockfile %s [%d MB]', 80 | fname, parseInt(size/1024/1024)); 81 | 82 | var fd = fs.openSync(fname, 'r'); 83 | 84 | var buffer = new Buffer(size); 85 | 86 | fs.readSync(fd, buffer, 0, size, 0); 87 | 88 | self.currentBuffer = buffer; 89 | self.currentParser = new Parser(buffer); 90 | }; 91 | 92 | 93 | 94 | BlockExtractor.prototype.getNextBlock = function(cb) { 95 | var self = this; 96 | 97 | var b; 98 | var magic; 99 | async.series([ 100 | function (a_cb) { 101 | 102 | async.whilst( 103 | function() { 104 | return (!magic); 105 | }, 106 | function(w_cb) { 107 | 108 | self.readCurrentFileSync(); 109 | 110 | if (self.currentFileIndex < 0) return cb(); 111 | 112 | 113 | magic = self.currentParser ? self.currentParser.buffer(4).toString('hex') 114 | : null ; 115 | 116 | if (!self.currentParser || self.currentParser.eof() || magic === '00000000') { 117 | magic = null; 118 | if (self.nextFile()) { 119 | console.log('Moving forward to file:' + self.currentFile() ); 120 | return w_cb(); 121 | } 122 | else { 123 | console.log('Finished all files'); 124 | magic = null; 125 | return w_cb(); 126 | } 127 | } 128 | else { 129 | return w_cb(); 130 | } 131 | }, a_cb); 132 | }, 133 | function (a_cb) { 134 | if (!magic) return a_cb(); 135 | 136 | if (magic !== self.magic) { 137 | var e = new Error('CRITICAL ERROR: Magic number mismatch: ' + 138 | magic + '!=' + self.magic); 139 | return a_cb(e); 140 | } 141 | 142 | // spacer? 143 | self.currentParser.word32le(); 144 | return a_cb(); 145 | }, 146 | function (a_cb) { 147 | if (!magic) return a_cb(); 148 | 149 | b = new Block(); 150 | b.parse(self.currentParser); 151 | b.getHash(); 152 | return a_cb(); 153 | }, 154 | ], function(err) { 155 | return cb(err,b); 156 | }); 157 | }; 158 | 159 | return BlockExtractor; 160 | } 161 | module.defineClass(spec); 162 | -------------------------------------------------------------------------------- /test/integration/01-transactionouts.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | 7 | 8 | var assert = require('assert'), 9 | fs = require('fs'), 10 | util = require('util'), 11 | TransactionDb = require('../../lib/TransactionDb').class(); 12 | 13 | var txItemsValid = JSON.parse(fs.readFileSync('test/integration/txitems.json')); 14 | var txDb; 15 | 16 | describe('TransactionDb fromIdWithInfo', function(){ 17 | 18 | before(function(c) { 19 | txDb = new TransactionDb(); 20 | return c(); 21 | }); 22 | 23 | 24 | var txid = '7e621eeb02874ab039a8566fd36f4591e65eca65313875221842c53de6907d6c'; 25 | it('tx info ' + txid, function(done) { 26 | txDb.fromIdWithInfo(txid, function(err, tx) { 27 | 28 | if (err) done(err); 29 | assert.equal(tx.txid, txid); 30 | assert(!tx.info.isCoinBase); 31 | 32 | for(var i=0; i<20; i++) 33 | assert(parseFloat(tx.info.vin[i].value) === parseFloat(50), 'input '+i); 34 | assert(tx.info.vin[0].addr === 'msGKGCy2i8wbKS5Fo1LbWUTJnf1GoFFG59', 'addr 0'); 35 | assert(tx.info.vin[1].addr === 'mfye7oHsdrHbydtj4coPXCasKad2eYSv5P', 'addr 1'); 36 | done(); 37 | }); 38 | }); 39 | 40 | it('tx info', function(done) { 41 | var txid = '21798ddc9664ac0ef618f52b151dda82dafaf2e26d2bbef6cdaf55a6957ca237'; 42 | txDb.fromIdWithInfo(txid, function(err, tx) { 43 | if (err) done(err); 44 | assert.equal(tx.txid, txid); 45 | assert(!tx.info.isCoinBase); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should pool tx\'s info from bitcoind', function(done) { 51 | var txid = '21798ddc9664ac0ef618f52b151dda82dafaf2e26d2bbef6cdaf55a6957ca237'; 52 | txDb.fromIdWithInfo(txid, function(err, tx) { 53 | if (err) done(err); 54 | assert.equal(tx.info.txid, txid); 55 | assert.equal(tx.info.blockhash, '000000000185678d3d7ecc9962c96418174431f93fe20bf216d5565272423f74'); 56 | assert.equal(tx.info.valueOut, 1.66174); 57 | assert.equal(tx.info.fees, 0.0005 ); 58 | assert.equal(tx.info.size, 226 ); 59 | assert(!tx.info.isCoinBase); 60 | done(); 61 | }); 62 | }); 63 | 64 | var txid1 = '2a104bab1782e9b6445583296d4a0ecc8af304e4769ceb64b890e8219c562399'; 65 | it('test a coinbase TX ' + txid1, function(done) { 66 | txDb.fromIdWithInfo(txid1, function(err, tx) { 67 | if (err) done(err); 68 | assert(tx.info.isCoinBase); 69 | assert.equal(tx.info.txid, txid1); 70 | assert(!tx.info.feeds); 71 | done(); 72 | }); 73 | }); 74 | var txid22 = '666'; 75 | it('test invalid TX ' + txid22, function(done) { 76 | txDb.fromIdWithInfo(txid22, function(err, tx) { 77 | if (err && err.message.match(/must.be.hexadecimal/)) { 78 | return done(); 79 | } 80 | else { 81 | return done(err); 82 | } 83 | }); 84 | }); 85 | 86 | var txid23 = '21798ddc9664ac0ef618f52b151dda82dafaf2e26d2bbef6cdaf55a6957ca227'; 87 | it('test unexisting TX ' + txid23, function(done) { 88 | 89 | txDb.fromIdWithInfo(txid23, function(err, tx) { 90 | assert(!err); 91 | assert(!tx); 92 | return done(); 93 | }); 94 | }); 95 | 96 | 97 | 98 | var txid2 = '64496d005faee77ac5a18866f50af6b8dd1f60107d6795df34c402747af98608'; 99 | it('create TX on the fly ' + txid2, function(done) { 100 | txDb.fromIdWithInfo(txid2, function(err, tx) { 101 | if (err) return done(err); 102 | assert.equal(tx.info.txid, txid2); 103 | done(); 104 | }); 105 | }); 106 | 107 | txid2 = '64496d005faee77ac5a18866f50af6b8dd1f60107d6795df34c402747af98608'; 108 | it('test a broken TX ' + txid2, function(done) { 109 | txDb.fromIdWithInfo(txid2, function(err, tx) { 110 | if (err) return done(err); 111 | assert.equal(tx.info.txid, txid2); 112 | assert.equal(tx.info.vin[0].addr, 'n1JagbRWBDi6VMvG7HfZmXX74dB9eiHJzU'); 113 | done(); 114 | }); 115 | }); 116 | }); 117 | 118 | describe('TransactionDb Outs', function(){ 119 | 120 | before(function(c) { 121 | txDb = new TransactionDb(); 122 | return c(); 123 | }); 124 | 125 | txItemsValid.forEach( function(v) { 126 | if (v.disabled) return; 127 | it('test a processing tx ' + v.txid, function(done) { 128 | this.timeout(60000); 129 | 130 | // Remove first 131 | txDb.removeFromTxId(v.txid, function() { 132 | 133 | txDb.fromTxId( v.txid, function(err, readItems) { 134 | assert.equal(readItems.length,0); 135 | 136 | var unmatch=[]; 137 | txDb.createFromArray([v.txid], null, function(err) { 138 | if (err) return done(err); 139 | 140 | txDb.fromTxId( v.txid, function(err, readItems) { 141 | 142 | v.items.forEach(function(validItem){ 143 | unmatch[validItem.addr] =1; 144 | }); 145 | assert.equal(readItems.length,v.items.length); 146 | 147 | v.items.forEach(function(validItem){ 148 | var readItem = readItems.shift(); 149 | 150 | assert.equal(readItem.addr,validItem.addr); 151 | assert.equal(readItem.value_sat,validItem.value_sat); 152 | assert.equal(readItem.index,validItem.index); 153 | assert.equal(readItem.spendIndex, null); 154 | assert.equal(readItem.spendTxIdBuf, null); 155 | delete unmatch[validItem.addr]; 156 | }); 157 | 158 | var valid = util.inspect(v.items, { depth: null }); 159 | assert(!Object.keys(unmatch).length,'\n\tUnmatchs:' + Object.keys(unmatch) + "\n\n" +valid + '\nvs.\n' + readItems); 160 | return done(); 161 | 162 | }); 163 | }); 164 | }); 165 | }); 166 | }); 167 | }); 168 | }); 169 | 170 | 171 | -------------------------------------------------------------------------------- /test/integration/addr.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "addr": "mp3Rzxx9s1A21SY3sjJ3CQoa2Xjph7e5eS", 4 | "balance": 0, 5 | "totalReceived": 50, 6 | "totalSent": 50.0, 7 | "txApperances": 2 8 | }, 9 | { 10 | "addr": "muyg1K5WsHkfMVCkUXU2y7Xp5ZD6RGzCeH", 11 | "balance": 0.38571339, 12 | "totalReceived": 0.38571339, 13 | "totalSent": 0, 14 | "txApperances": 1 15 | }, 16 | { 17 | "addr": "mhPEfAmeKVwT7arwMYbhwnL2TfwuWbP4r4", 18 | "totalReceived": 1069, 19 | "txApperances": 13, 20 | "balance": 1065, 21 | "totalSent": 4 22 | }, 23 | { 24 | "addr": "n47CfqnKWdNwqY1UWxTmNJAqYutFxdH3zY", 25 | "balance": 0, 26 | "totalReceived":26.4245, 27 | "totalSent": 26.4245, 28 | "txApperances": 4 29 | }, 30 | { 31 | "addr": "mzSyyXgofoBxpr6gYcU3cV345G8hJpixRd", 32 | "balance": 0, 33 | "totalReceived":3.9775, 34 | "totalSent": 3.9775, 35 | "txApperances": 2 36 | }, 37 | { 38 | "addr": "mgqvRGJMwR9JU5VhJ3x9uX9MTkzTsmmDgQ", 39 | "txApperances": 27, 40 | "balance": 0, 41 | "totalReceived": 54.81284116 42 | }, 43 | { 44 | "addr": "mzW2hdZN2um7WBvTDerdahKqRgj3md9C29", 45 | "balance": 1199.74393853, 46 | "totalReceived": 1199.74393853, 47 | "totalSent": 0, 48 | "txApperances": 6048 49 | }, 50 | { 51 | "addr": "mjRmkmYzvZN3cA3aBKJgYJ65epn3WCG84H", 52 | "txApperances": 7164, 53 | "balance": 46413.0, 54 | "totalReceived": 357130.17644359, 55 | "totalSent": 310717.17644359 56 | }, 57 | { 58 | "addr": "mgKY35SXqxFpcKK3Dq9mW9919N7wYXvcFM", 59 | "txApperances": 1, 60 | "balance": 0.01979459, 61 | "totalReceived": 0.01979459, 62 | "totalSent": 0, 63 | "transactions": [ "91800d80bb4c69b238c9bfd94eb5155ab821e6b25cae5c79903d12853bbb4ed5" ] 64 | }, 65 | { 66 | "addr": "mmvP3mTe53qxHdPqXEvdu8WdC7GfQ2vmx5", 67 | "balance": 10580.50027254, 68 | "totalReceived": 12157.65075053, 69 | "totalSent": 1577.15047799, 70 | "txApperances": 441, 71 | "transactions": [ 72 | "91800d80bb4c69b238c9bfd94eb5155ab821e6b25cae5c79903d12853bbb4ed5", 73 | "f6e80d4fd1a2377406856c67d0cee5ac7e5120993ff97e617ca9aac33b4c6b1e", 74 | "bc27f31caae86750b126d9b09e969362b85b7c15f41421387d682064544bf7e7", 75 | "2cd6a1cb26880276fbc9851396f1bd8081cb2b9107ff6921e8fd65ed2df3df79", 76 | "8bea41f573bccb7b648bc0b1bbfeba8a96da05b1d819ff4a33d39fbcd334ecfd", 77 | "cb0d55c37acc57f759255193673e13858b5ab3d8fdfa7ee8b25f9964bdaa11e3", 78 | "7b007aeace2299d27b6bb6c24d0a8040d6a87e4c2601216c34d226462b75f915", 79 | "a9f40fbaecd2b28a05405e28b95566d7b3bd8ac38a2853debd72517f2994c6fc", 80 | "4123255b7678e37c168b9e929927760bc5d9363b0c78ec61a7b4a78b2a07adab", 81 | "cb3760529c2684c32047a2fddf0e2534c9241e5d72011aac4a8982e0c7b46df3", 82 | "e8d00d8cc744381233dbc95e2d657345084dfb6df785b81285183f4c89b678d4", 83 | "7a748364255c5b64979d9d3da35ea0fbef0114e0d7f96fccd5bea76f6d19f06b", 84 | "d0b7e087040f67ef9bd9f21ccf53d1b5410400351d949cabf127caf28a6e7add", 85 | "209f97873265652b83922921148cad92d7e048c6822e4864e984753e04181470", 86 | "3a4af7755d3061ecced2f3707c2623534104f08aa73a52ca243d7ddecf5fe86d", 87 | "4a4b5c8d464a77814ed35a37e2a28e821d467a803761427c057f67823309b725", 88 | "d85f5265618fb694c3ea3ca6f73eba93df8a644bc1c7286cec2fbc2fbf7d895e", 89 | "0d2c778ed9976b52792c941cac126bda37d3b1453082022d5e36ac401be3b249", 90 | "daf03d666047ca0b5340b4a0027f8562b7c5bac87dca3727093b5393176a541a", 91 | "a0dc03a870e589ea51e3d3a8aed0d34f4f1ae6844acad26dae48fe523b26e764", 92 | "3df1a50e2e5d8525f04bd21a66bad824364a975449fa24fd5c2537d0f713919b", 93 | "7bc26c1f3b4ab5ca57677593d28d13bff468a658f4d5efc379c1612554cf668e", 94 | "ded4cbc9c52fd5599b6a93f89a79cde9aeb5a7f8f56732bb67ae9554325b3666", 95 | "91224a219196a3f6e6f40ad2137b13fe54109e57aaed7527ea34aa903e6b8313", 96 | "ee899a182bbb75e98ef14d83489e631dd66a8c5059dc8255692dd8ca9efba01f", 97 | "0a61590c7548bd4f6a0df1575b268057e5e3e295a44eaeeb1dfbd01332c585ed", 98 | "d56c22950ad2924f404b5b0baa6e49b0df1aaf09d1947842aed9d0178958eb9d", 99 | "c6b5368c5a256141894972fbd02377b3894aa0df7c35fab5e0eca90de064fdc1", 100 | "158e1f9c3f8ec44e88052cadef74e8eb99fbad5697d0b005ba48c933f7d96816", 101 | "7f6191c0f4e3040901ef0d5d6e76af4f16423061ca1347524c86205e35d904d9", 102 | "2c2e20f976b98a0ca76c57eca3653699b60c1bd9503cc9cc2fb755164a679a26", 103 | "59bc81733ff0eaf2b106a70a655e22d2cdeff80ada27b937693993bf0c22e9ea", 104 | "7da38b66fb5e8582c8be85abecfd744a6de89e738dd5f3aaa0270b218ec424eb", 105 | "393d51119cdfbf0a308c0bbde2d4c63546c0961022bad1503c4bbaed0638c837", 106 | "4518868741817ae6757fd98de27693b51fad100e89e5206b9bbf798aeebb804c", 107 | "c58bce14de1e3016504babd8bbe8175207d75074134a2548a71743fa3e56c58d", 108 | "6e69ec4a97515a8fd424f123a5fc1fdfd3c3adcd741292cbc09c09a2cc433bea", 109 | "0e15f2498362050e5ceb6157d0fbf820fdcaf936e447207d433ee7701d7b99c2", 110 | "a3789e113041db907a1217ddb5c3aaf0eff905cc3d913e68d977e1ab4d19acea", 111 | "80b460922faf0ad1e8b8a55533654c9a9f3039bfff0fff2bcf8536b8adf95939" 112 | ] 113 | }, 114 | { 115 | "addr": "mtA6woo1wjCeu1dLkWgpSD3tRnRfrHt3FL", 116 | "balance": 349.845, 117 | "totalReceived": 349.845, 118 | "totalSent": 0, 119 | "txApperances": 13, 120 | "transactions": [ 121 | "794eafc0ad68a3576034eb137d7d20d3bdf1777ecf27e0e20e96e1adcfc66659", 122 | "0130721f29f50b773858c3c9081678bdddebcd18078c5fa2436d979b54ed5cef", 123 | "fb1024947b48d90255aedd3f0f1df3673a7e98d06346bb2ac89b116aa19c5db4" 124 | ] 125 | } 126 | 127 | ] 128 | 129 | 130 | -------------------------------------------------------------------------------- /app/models/Address.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('classtool'); 4 | 5 | 6 | function spec() { 7 | var async = require('async'); 8 | var BitcoreAddress = require('bitcore/Address').class(); 9 | var BitcoreUtil = require('bitcore/util/util'); 10 | var TransactionDb = require('../../lib/TransactionDb').class(); 11 | var BitcoreTransaction = require('bitcore/Transaction').class(); 12 | var Parser = require('bitcore/util/BinaryParser').class(); 13 | var Buffer = require('buffer').Buffer; 14 | var CONCURRENCY = 5; 15 | 16 | function Address(addrStr) { 17 | this.balanceSat = 0; 18 | this.totalReceivedSat = 0; 19 | this.totalSentSat = 0; 20 | 21 | this.unconfirmedBalanceSat = 0; 22 | 23 | this.txApperances = 0; 24 | this.unconfirmedTxApperances= 0; 25 | 26 | // TODO store only txids? +index? +all? 27 | this.transactions = []; 28 | 29 | var a = new BitcoreAddress(addrStr); 30 | a.validate(); 31 | this.addrStr = addrStr; 32 | 33 | Object.defineProperty(this, 'totalSent', { 34 | get: function() { 35 | return parseFloat(this.totalSentSat) / parseFloat(BitcoreUtil.COIN); 36 | }, 37 | set: function(i) { 38 | this.totalSentSat = i * BitcoreUtil.COIN; 39 | }, 40 | enumerable: 1, 41 | }); 42 | 43 | Object.defineProperty(this, 'balance', { 44 | get: function() { 45 | return parseFloat(this.balanceSat) / parseFloat(BitcoreUtil.COIN); 46 | }, 47 | set: function(i) { 48 | this.balance = i * BitcoreUtil.COIN; 49 | }, 50 | enumerable: 1, 51 | }); 52 | 53 | Object.defineProperty(this, 'totalReceived', { 54 | get: function() { 55 | return parseFloat(this.totalReceivedSat) / parseFloat(BitcoreUtil.COIN); 56 | }, 57 | set: function(i) { 58 | this.totalReceived = i * BitcoreUtil.COIN; 59 | }, 60 | enumerable: 1, 61 | }); 62 | 63 | 64 | Object.defineProperty(this, 'unconfirmedBalance', { 65 | get: function() { 66 | return parseFloat(this.unconfirmedBalanceSat) / parseFloat(BitcoreUtil.COIN); 67 | }, 68 | set: function(i) { 69 | this.unconfirmedBalanceSat = i * BitcoreUtil.COIN; 70 | }, 71 | enumerable: 1, 72 | }); 73 | 74 | } 75 | 76 | Address.prototype._getScriptPubKey = function(hex,n) { 77 | // ScriptPubKey is not provided by bitcoind RPC, so we parse it from tx hex. 78 | 79 | var parser = new Parser(new Buffer(hex,'hex')); 80 | var tx = new BitcoreTransaction(); 81 | tx.parse(parser); 82 | return (tx.outs[n].s.toString('hex')); 83 | }; 84 | 85 | Address.prototype.getUtxo = function(next) { 86 | var self = this; 87 | if (!self.addrStr) return next(); 88 | 89 | var ret = []; 90 | var db = new TransactionDb(); 91 | 92 | db.fromAddr(self.addrStr, function(err,txOut){ 93 | if (err) return next(err); 94 | 95 | // Complete utxo info 96 | async.eachLimit(txOut,CONCURRENCY,function (txItem, a_c) { 97 | db.fromIdInfoSimple(txItem.txid, function(err, info) { 98 | 99 | // we are filtering out even unconfirmed spents! 100 | // add || !txItem.spentIsConfirmed 101 | 102 | if (!txItem.spentTxId && info && info.hex) { 103 | var scriptPubKey = self._getScriptPubKey(info.hex, txItem.index); 104 | ret.push({ 105 | address: self.addrStr, 106 | txid: txItem.txid, 107 | vout: txItem.index, 108 | ts: txItem.ts, 109 | scriptPubKey: scriptPubKey, 110 | amount: txItem.value_sat / BitcoreUtil.COIN, 111 | confirmations: txItem.isConfirmed ? info.confirmations : 0, 112 | }); 113 | } 114 | return a_c(err); 115 | }); 116 | }, function(err) { 117 | return next(err,ret); 118 | }); 119 | }); 120 | }; 121 | 122 | Address.prototype.update = function(next) { 123 | var self = this; 124 | if (!self.addrStr) return next(); 125 | 126 | var txs = []; 127 | var db = new TransactionDb(); 128 | async.series([ 129 | function (cb) { 130 | var seen={}; 131 | db.fromAddr(self.addrStr, function(err,txOut){ 132 | if (err) return cb(err); 133 | txOut.forEach(function(txItem){ 134 | var add=0, addSpend=0; 135 | var v = txItem.value_sat; 136 | 137 | if ( !seen[txItem.txid] ) { 138 | txs.push({txid: txItem.txid, ts: txItem.ts}); 139 | seen[txItem.txid]=1; 140 | add=1; 141 | } 142 | 143 | if (txItem.spentTxId && !seen[txItem.spentTxId] ) { 144 | txs.push({txid: txItem.spentTxId, ts: txItem.spentTs}); 145 | seen[txItem.spentTxId]=1; 146 | addSpend=1; 147 | } 148 | 149 | if (txItem.isConfirmed) { 150 | self.txApperances += add; 151 | self.totalReceivedSat += v; 152 | if (! txItem.spentTxId ) { 153 | //unspent 154 | self.balanceSat += v; 155 | } 156 | else if(!txItem.spentIsConfirmed) { 157 | // unspent 158 | self.balanceSat += v; 159 | self.unconfirmedBalanceSat -= v; 160 | self.unconfirmedTxApperances += addSpend; 161 | } 162 | else { 163 | // spent 164 | self.totalSentSat += v; 165 | self.txApperances += addSpend; 166 | } 167 | } 168 | else { 169 | self.unconfirmedBalanceSat += v; 170 | self.unconfirmedTxApperances += add; 171 | } 172 | }); 173 | return cb(); 174 | }); 175 | }, 176 | ], function (err) { 177 | 178 | // sort input and outputs togheter 179 | txs.sort( 180 | function compare(a,b) { 181 | if (a.ts < b.ts) return 1; 182 | if (a.ts > b.ts) return -1; 183 | return 0; 184 | }); 185 | 186 | self.transactions = txs.map(function(i) { return i.txid; } ); 187 | return next(err); 188 | }); 189 | }; 190 | 191 | return Address; 192 | } 193 | module.defineClass(spec); 194 | 195 | -------------------------------------------------------------------------------- /lib/BlockDb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('classtool'); 4 | 5 | 6 | function spec(b) { 7 | 8 | var superclass = b.superclass || require('events').EventEmitter; 9 | var TIMESTAMP_PREFIX = 'bts-'; // b-ts- => 10 | var PREV_PREFIX = 'bpr-'; // b-prev- => 11 | var NEXT_PREFIX = 'bne-'; // b-next- => 12 | var MAIN_PREFIX = 'bma-'; // b-main- => 1/0 13 | var TIP = 'bti-'; // last block on the chain 14 | var LAST_FILE_INDEX = 'file-'; // last processed file index 15 | 16 | var MAX_OPEN_FILES = 500; 17 | 18 | 19 | /** 20 | * Module dependencies. 21 | */ 22 | var levelup = require('levelup'), 23 | config = require('../config/config'); 24 | var db = b.db || levelup(config.leveldb + '/blocks',{maxOpenFiles: MAX_OPEN_FILES} ); 25 | var Rpc = b.rpc || require('./Rpc').class(); 26 | var PoolMatch = b.poolMatch || require('./PoolMatch').class(config); 27 | 28 | var TransactionDb = require('./TransactionDb.js').class(); 29 | 30 | var BlockDb = function() { 31 | BlockDb.super(this, arguments); 32 | this.poolMatch = new PoolMatch(); 33 | }; 34 | 35 | BlockDb.superclass = superclass; 36 | 37 | BlockDb.prototype.close = function(cb) { 38 | db.close(cb); 39 | }; 40 | 41 | BlockDb.prototype.drop = function(cb) { 42 | var path = config.leveldb + '/blocks'; 43 | db.close(function() { 44 | require('leveldown').destroy(path, function () { 45 | db = levelup(path,{maxOpenFiles: MAX_OPEN_FILES} ); 46 | return cb(); 47 | }); 48 | }); 49 | }; 50 | 51 | // adds a block. Does not update Next pointer in 52 | // the block prev to the new block, nor TIP pointer 53 | // 54 | BlockDb.prototype.add = function(b, cb) { 55 | var self = this; 56 | var time_key = TIMESTAMP_PREFIX + 57 | ( b.time || Math.round(new Date().getTime() / 1000) ); 58 | 59 | return db.batch() 60 | .put(time_key, b.hash) 61 | .put(MAIN_PREFIX + b.hash, 1) 62 | .put(PREV_PREFIX + b.hash, b.previousblockhash) 63 | .write(function(err){ 64 | if (!err) { 65 | self.emit('new_block', {blockid: b.hash}); 66 | } 67 | cb(err); 68 | }); 69 | }; 70 | 71 | BlockDb.prototype.getTip = function(cb) { 72 | db.get(TIP, function(err, val) { 73 | return cb(err,val); 74 | }); 75 | }; 76 | 77 | BlockDb.prototype.setTip = function(hash, cb) { 78 | db.put(TIP, hash, function(err) { 79 | return cb(err); 80 | }); 81 | }; 82 | 83 | //mainly for testing 84 | BlockDb.prototype.setPrev = function(hash, prevHash, cb) { 85 | db.put(PREV_PREFIX + hash, prevHash, function(err) { 86 | return cb(err); 87 | }); 88 | }; 89 | 90 | BlockDb.prototype.getPrev = function(hash, cb) { 91 | db.get(PREV_PREFIX + hash, function(err,val) { 92 | if (err && err.notFound) { err = null; val = null;} 93 | return cb(err,val); 94 | }); 95 | }; 96 | 97 | 98 | BlockDb.prototype.setLastFileIndex = function(idx, cb) { 99 | var self = this; 100 | if (this.lastFileIndexSaved === idx) return cb(); 101 | 102 | db.put(LAST_FILE_INDEX, idx, function(err) { 103 | self.lastFileIndexSaved = idx; 104 | return cb(err); 105 | }); 106 | }; 107 | 108 | BlockDb.prototype.getLastFileIndex = function(cb) { 109 | db.get(LAST_FILE_INDEX, function(err,val) { 110 | if (err && err.notFound) { err = null; val = null;} 111 | return cb(err,val); 112 | }); 113 | }; 114 | 115 | BlockDb.prototype.getNext = function(hash, cb) { 116 | db.get(NEXT_PREFIX + hash, function(err,val) { 117 | if (err && err.notFound) { err = null; val = null;} 118 | return cb(err,val); 119 | }); 120 | }; 121 | 122 | BlockDb.prototype.isMain = function(hash, cb) { 123 | db.get(MAIN_PREFIX + hash, function(err, val) { 124 | if (err && err.notFound) { err = null; val = 0;} 125 | return cb(err,parseInt(val)); 126 | }); 127 | }; 128 | 129 | BlockDb.prototype.setMain = function(hash, isMain, cb) { 130 | if (!isMain) console.log('\tNew orphan: %s',hash); 131 | db.put(MAIN_PREFIX + hash, isMain?1:0, function(err) { 132 | return cb(err); 133 | }); 134 | }; 135 | BlockDb.prototype.setNext = function(hash, nextHash, cb) { 136 | db.put(NEXT_PREFIX + hash, nextHash, function(err) { 137 | return cb(err); 138 | }); 139 | }; 140 | 141 | BlockDb.prototype.countConnected = function(cb) { 142 | var c = 0; 143 | console.log('Counting connected blocks. This could take some minutes'); 144 | db.createReadStream({start: MAIN_PREFIX, end: MAIN_PREFIX + '~' }) 145 | .on('data', function (data) { 146 | if (data.value !== 0) c++; 147 | }) 148 | .on('error', function (err) { 149 | return cb(err); 150 | }) 151 | .on('end', function () { 152 | return cb(null, c); 153 | }); 154 | }; 155 | 156 | // .has() return true orphans also 157 | BlockDb.prototype.has = function(hash, cb) { 158 | var k = PREV_PREFIX + hash; 159 | db.get(k, function (err) { 160 | var ret = true; 161 | if (err && err.notFound) { 162 | err = null; 163 | ret = false; 164 | } 165 | return cb(err, ret); 166 | }); 167 | }; 168 | 169 | BlockDb.prototype.getPoolInfo = function(tx, cb) { 170 | var tr = new TransactionDb(); 171 | var self = this; 172 | 173 | tr._getInfo(tx, function(e, a) { 174 | if (e) return cb(false); 175 | 176 | if (a.isCoinBase) { 177 | var coinbaseHexBuffer = new Buffer(a.vin[0].coinbase, 'hex'); 178 | var aa = self.poolMatch.match(coinbaseHexBuffer); 179 | 180 | return cb(aa); 181 | } 182 | }); 183 | }; 184 | 185 | BlockDb.prototype.fromHashWithInfo = function(hash, cb) { 186 | var self = this; 187 | 188 | Rpc.getBlock(hash, function(err, info) { 189 | if (err || !info) return cb(err); 190 | 191 | self.isMain(hash, function(err, val) { 192 | if (err) return cb(err); 193 | 194 | info.isMainChain = val ? true : false; 195 | 196 | return cb(null, { 197 | hash: hash, 198 | info: info, 199 | }); 200 | }); 201 | }); 202 | }; 203 | 204 | BlockDb.prototype.getBlocksByDate = function(start_ts, end_ts, cb) { 205 | var list = []; 206 | db.createReadStream({ 207 | start: TIMESTAMP_PREFIX + start_ts, 208 | end: TIMESTAMP_PREFIX + end_ts, 209 | fillCache: true 210 | }) 211 | .on('data', function (data) { 212 | var k = data.key.split('-'); 213 | list.push({ 214 | ts: k[1], 215 | hash: data.value, 216 | }); 217 | }) 218 | .on('error', function (err) { 219 | return cb(err); 220 | }) 221 | .on('end', function () { 222 | return cb(null, list.reverse()); 223 | }); 224 | }; 225 | 226 | BlockDb.prototype.blockIndex = function(height, cb) { 227 | return Rpc.blockIndex(height,cb); 228 | }; 229 | 230 | return BlockDb; 231 | } 232 | module.defineClass(spec); 233 | 234 | 235 | -------------------------------------------------------------------------------- /lib/Sync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('classtool'); 4 | 5 | 6 | function spec() { 7 | var sockets = require('../app/controllers/socket.js'); 8 | var BlockDb = require('./BlockDb').class(); 9 | 10 | var TransactionDb = require('./TransactionDb').class(); 11 | var config = require('../config/config'); 12 | var networks = require('bitcore/networks'); 13 | var async = require('async'); 14 | 15 | 16 | function Sync(opts) { 17 | this.opts = opts || {}; 18 | this.bDb = new BlockDb(opts); 19 | this.txDb = new TransactionDb(opts); 20 | this.txDb.on('tx_for_address', this.handleTxForAddress.bind(this)); 21 | this.txDb.on('new_tx', this.handleNewTx.bind(this)); 22 | this.bDb.on('new_block', this.handleNewBlock.bind(this)); 23 | this.network = config.network === 'testnet' ? networks.testnet : networks.livenet; 24 | } 25 | 26 | Sync.prototype.close = function(cb) { 27 | var self = this; 28 | self.txDb.close(function() { 29 | self.bDb.close(cb); 30 | }); 31 | }; 32 | 33 | 34 | Sync.prototype.destroy = function(next) { 35 | var self = this; 36 | async.series([ 37 | 38 | function(b) { 39 | self.bDb.drop(b); 40 | }, 41 | function(b) { 42 | self.txDb.drop(b); 43 | }, 44 | ], next); 45 | }; 46 | 47 | /* 48 | * Arrives a NEW block, which is the new TIP 49 | * 50 | * Case 0) Simple case 51 | * A-B-C-D-E(TIP)-NEW 52 | * 53 | * Case 1) 54 | * A-B-C-D-E(TIP) 55 | * \ 56 | * NEW 57 | * 58 | * 1) Declare D-E orphans (and possible invalidate TXs on them) 59 | * 60 | * Case 2) 61 | * A-B-C-D-E(TIP) 62 | * \ 63 | * F-G-NEW 64 | * 1) Set F-G as connected (mark TXs as valid) 65 | * 2) Declare D-E orphans (and possible invalidate TXs on them) 66 | * 67 | * 68 | * Case 3) 69 | * 70 | * A-B-C-D-E(TIP) ... NEW 71 | * 72 | * NEW is ignored (if allowReorgs is false) 73 | * 74 | * 75 | */ 76 | 77 | Sync.prototype.storeTipBlock = function(b, allowReorgs, cb) { 78 | 79 | if (typeof allowReorgs === 'function') { 80 | cb = allowReorgs; 81 | allowReorgs = true; 82 | } 83 | if (!b) return cb(); 84 | 85 | var self = this; 86 | var oldTip, oldNext, needReorg = false; 87 | var newPrev = b.previousblockhash; 88 | 89 | async.series([ 90 | 91 | function(c) { 92 | self.bDb.has(b.hash, function(err, val) { 93 | return c(err || 94 | (val ? new Error('WARN: Ignoring already existing block:' + b.hash) : null)); 95 | }); 96 | }, 97 | function(c) { 98 | if (!allowReorgs) return c(); 99 | self.bDb.has(newPrev, function(err, val) { 100 | if (!val && newPrev.match(/^0+$/)) return c(); 101 | 102 | return c(err || 103 | (!val ? new Error('NEED_SYNC Ignoring block with non existing prev:' + b.hash) : null)); 104 | }); 105 | }, 106 | function(c) { 107 | self.txDb.createFromBlock(b, function(err) { 108 | return c(err); 109 | }); 110 | }, 111 | function(c) { 112 | if (!allowReorgs) return c(); 113 | self.bDb.getTip(function(err, val) { 114 | oldTip = val; 115 | if (oldTip && newPrev !== oldTip) needReorg = true; 116 | return c(); 117 | }); 118 | }, 119 | function(c) { 120 | if (!needReorg) return c(); 121 | self.bDb.getNext(newPrev, function(err, val) { 122 | if (err) return c(err); 123 | oldNext = val; 124 | return c(); 125 | }); 126 | }, 127 | function(c) { 128 | self.bDb.add(b, c); 129 | }, 130 | function(c) { 131 | if (!needReorg) return c(); 132 | console.log('NEW TIP: %s NEED REORG (old tip: %s)', b.hash, oldTip); 133 | self.processReorg(oldTip, oldNext, newPrev, c); 134 | }, 135 | function(c) { 136 | if (!allowReorgs) return c(); 137 | self.bDb.setTip(b.hash, function(err) { 138 | return c(err); 139 | }); 140 | }, 141 | function(c) { 142 | self.bDb.setNext(newPrev, b.hash, function(err) { 143 | return c(err); 144 | }); 145 | } 146 | 147 | ], 148 | function(err) { 149 | if (err && err.toString().match(/WARN/)) { 150 | err = null; 151 | } 152 | return cb(err); 153 | }); 154 | }; 155 | 156 | 157 | 158 | Sync.prototype.processReorg = function(oldTip, oldNext, newPrev, cb) { 159 | var self = this; 160 | 161 | var orphanizeFrom; 162 | 163 | async.series([ 164 | 165 | function(c) { 166 | self.bDb.isMain(newPrev, function(err, val) { 167 | if (!val) return c(); 168 | 169 | console.log('# Reorg Case 1)'); 170 | // case 1 171 | orphanizeFrom = oldNext; 172 | return c(err); 173 | }); 174 | }, 175 | function(c) { 176 | if (orphanizeFrom) return c(); 177 | 178 | console.log('# Reorg Case 2)'); 179 | self.setBranchConnectedBackwards(newPrev, function(err, yHash, newYHashNext) { 180 | if (err) return c(err); 181 | self.bDb.getNext(yHash, function(err, yHashNext) { 182 | orphanizeFrom = yHashNext; 183 | self.bDb.setNext(yHash, newYHashNext, function(err) { 184 | return c(err); 185 | }); 186 | }); 187 | }); 188 | }, 189 | function(c) { 190 | if (!orphanizeFrom) return c(); 191 | self.setBranchOrphan(orphanizeFrom, function(err) { 192 | return c(err); 193 | }); 194 | }, 195 | ], 196 | function(err) { 197 | return cb(err); 198 | }); 199 | }; 200 | 201 | Sync.prototype.setBlockMain = function(hash, isMain, cb) { 202 | var self = this; 203 | self.bDb.setMain(hash, isMain, function(err) { 204 | if (err) return cb(err); 205 | return self.txDb.handleBlockChange(hash, isMain, cb); 206 | }); 207 | }; 208 | 209 | Sync.prototype.setBranchOrphan = function(fromHash, cb) { 210 | var self = this, 211 | hashInterator = fromHash; 212 | 213 | async.whilst( 214 | function() { 215 | return hashInterator; 216 | }, 217 | function(c) { 218 | self.setBlockMain(hashInterator, false, function(err) { 219 | if (err) return cb(err); 220 | self.bDb.getNext(hashInterator, function(err, val) { 221 | hashInterator = val; 222 | return c(err); 223 | }); 224 | }); 225 | }, cb); 226 | }; 227 | 228 | Sync.prototype.setBranchConnectedBackwards = function(fromHash, cb) { 229 | var self = this, 230 | hashInterator = fromHash, 231 | lastHash = fromHash, 232 | isMain; 233 | 234 | async.doWhilst( 235 | function(c) { 236 | self.setBlockMain(hashInterator, true, function(err) { 237 | if (err) return c(err); 238 | self.bDb.getPrev(hashInterator, function(err, val) { 239 | if (err) return c(err); 240 | lastHash = hashInterator; 241 | hashInterator = val; 242 | self.bDb.isMain(hashInterator, function(err, val) { 243 | isMain = val; 244 | return c(); 245 | }); 246 | }); 247 | }); 248 | }, 249 | function() { 250 | return hashInterator && !isMain; 251 | }, 252 | function(err) { 253 | console.log('\tFound yBlock:', hashInterator); 254 | return cb(err, hashInterator, lastHash); 255 | } 256 | ); 257 | }; 258 | 259 | 260 | Sync.prototype.handleTxForAddress = function(data) { 261 | if (this.opts.shouldBroadcast) { 262 | sockets.broadcastAddressTx(data.address, data.txid); 263 | } 264 | }; 265 | 266 | Sync.prototype.handleNewTx = function(data) { 267 | if (this.opts.shouldBroadcast) { 268 | sockets.broadcastTx(data.tx); 269 | } 270 | }; 271 | 272 | Sync.prototype.handleNewBlock = function(data) { 273 | if (this.opts.shouldBroadcast) { 274 | sockets.broadcastBlock(data.blockid); 275 | } 276 | }; 277 | 278 | Sync.prototype.storeTxs = function(txs, cb) { 279 | var self = this; 280 | self.txDb.createFromArray(txs, null, function(err) { 281 | if (err) return cb(err); 282 | return cb(err); 283 | }); 284 | }; 285 | 286 | 287 | return Sync; 288 | } 289 | module.defineClass(spec); 290 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # *insight API* 2 | 3 | *insight API* is an open-source bitcoin blockchain REST 4 | and websocket API. Insight API runs in NodeJS and use LevelDB for storage. 5 | 6 | *Insight API* allows to develop bitcoin related applications such as wallets that 7 | require certain information from the blockchain that bitcoind does not provide. 8 | 9 | A blockchain explorer front-end have been developed to top of *Insight API*, it can 10 | be downloaded at [Github Insight Repository](https://github.com/bitpay/insight). 11 | 12 | 13 | ## Prerequisites 14 | 15 | * **bitcoind** - Download and Install [Bitcoin](http://bitcoin.org/en/download) 16 | 17 | *insight* needs a *trusted* bitcoind node to run. *insight* will connect to the node 18 | thru the RPC API, Peer-to-peer protocol and will even read its raw .dat files for syncing. 19 | 20 | Configure bitcoind to listen to RPC calls and set `txindex` to true. 21 | The easiest way to do this is by copying `./etc/bitcoind/bitcoin.conf` to your 22 | bitcoin data directory (usually `"~/.bitcoin"` on Linux, `"%appdata%\Bitcoin\"` on Windows, 23 | or `"~/Library/Application Support/Bitcoin"` on Mac OS X). 24 | 25 | bitcoind must be running and must have finished downloading the blockchain **before** running *insight*. 26 | 27 | 28 | * **Node.js v0.10.x** - Download and Install [Node.js](http://www.nodejs.org/download/). 29 | 30 | * **NPM** - Node.js package manager, should be automatically installed when you get node.js. 31 | 32 | ## Quick Install 33 | Check the Prerequisites section above before installing. 34 | 35 | To install Insight API, clone the main repository: 36 | 37 | $ git clone git@github.com:bitpay/insight-api.git && cd insight-api 38 | 39 | Install dependencies: 40 | 41 | $ npm install 42 | 43 | Run the main application: 44 | 45 | $ node insight.js 46 | 47 | Then open a browser and go to: 48 | 49 | http://localhost:3001 50 | 51 | Please note that the app will need to sync its internal database 52 | with the blockchain state, which may take some time. You can check 53 | sync progress from within the web interface. 54 | 55 | 56 | ## Configuration 57 | 58 | All configuration is specified in the [config](config/) folder, particularly the [config.js](config/config.js) file. There you can specify your application name and database name. Certain configuration values are pulled from environment variables if they are defined: 59 | 60 | ### bitcoind connexion 61 | ``` 62 | BITCOIND_HOST # RPC bitcoind host 63 | BITCOIND_PORT # RPC bitcoind Port 64 | BITCOIND_P2P_PORT # P2P bitcoind Port 65 | BITCOIND_USER # RPC username 66 | BITCOIND_PASS # RPC password 67 | BITCOIND_DATADIR # bitcoind datadir for livenet, or datadir/testnet3 for testnet 68 | INSIGHT_NETWORK [= 'livenet' | 'testnet'] 69 | ``` 70 | 71 | Make sure that bitcoind is configured to [accept incoming connections using 'rpcallowip'](https://en.bitcoin.it/wiki/Running_Bitcoin). 72 | 73 | In case the network is changed (testnet to livenet or vice versa) levelDB database needs to be deleted. This can be performed running: 74 | ```util/sync.js -D``` and waiting for *insight* to synchronize again. Once the database is deleted, the sync.js process can be safely interrupted (CTRL+C) and continued from the synchronization process embedded in main app. 75 | 76 | ## Synchronization 77 | 78 | The initial synchronization process scans the blockchain from the paired bitcoind server to update addresses and balances. *insight* needs one (and only one) trusted bitcoind node to run. This node must have finished downloading the blockchain befure running *insight*. 79 | 80 | While *insight* is synchronizing the website can be accessed (the sync process is embedded in the webserver), but there may be missing data or incorrect balances for addresses. The 'sync' status is shown on the top-right of all pages. 81 | 82 | The blockchain can be read from bitcoind's raw `.dat` files or RPC interface. Reading the information from the `.dat` files is much faster so it's the recommended (and default) alternative. `.dat` files are scanned in the default location for each platform. In case a non-standard location is used, it needs to be defined (see the Configuration section). The synchronization type being used can be seen at the [Status page](http://localhost:3001/status). As of February 2014, using `.dat` files the sync process takes 7 hrs. for livenet and 20 mins. for testnet. 83 | 84 | While synchronizing the blockchain, *insight* listens for new blocks and transactions relayed by the bitcoind node. Those are also stored on *insight*'s database. In case *insight* is shutdown for a period of time, restarting it will trigger a partial (historic) synchronization of the blockchain. Depending on the size of that synchronization task, a reverse RPC or forward `.dat` syncing strategy will be used. 85 | 86 | If bitcoind is shutdown, *insight* needs to be stopped and restarted once bitcoind is restarted. 87 | 88 | ### Syncing old blockchain data manualy 89 | 90 | Old blockchain data can be manually synced issuing: 91 | 92 | $ util/sync.js 93 | 94 | Check util/sync.js --help for options, particulary -D to erase the current DB. 95 | 96 | *NOTE* that there is no need to run this manually since the historic synchronization is embedded on the web application, so by running you will trigger the historic sync automatically. 97 | 98 | 99 | ### DB storage requirement 100 | 101 | To store the blockchain and address related information, *insight* uses LevelDB. Two DBs are created: txs and blocks. By default these are stored on 102 | ```/db``` 103 | 104 | this can be changed on config/config.js. As of February 2014, storing the livenet blockchain takes ~30GB of disk space (2GB for the testnet). 105 | 106 | ## Development 107 | 108 | To run insight locally for development with grunt: 109 | 110 | ```$ NODE_ENV=development grunt``` 111 | 112 | To run the tests 113 | 114 | ```$ grunt test``` 115 | 116 | 117 | Contributions and suggestions are welcomed at [insight github repository](https://github.com/bitpay/insight). 118 | 119 | 120 | ## API 121 | 122 | By default, insight provides a REST API at /api, but this prefix is configurable from the var `apiPrefix` in the `config.js` file. 123 | 124 | The end-points are: 125 | 126 | 127 | ### Block 128 | ``` 129 | /api/block/[:hash] 130 | /api/block/00000000a967199a2fad0877433c93df785a8d8ce062e5f9b451cd1397bdbf62 131 | ``` 132 | ### Transaction 133 | ``` 134 | /api/tx/[:txid] 135 | /api/tx/525de308971eabd941b139f46c7198b5af9479325c2395db7f2fb5ae8562556c 136 | ``` 137 | ### Address 138 | ``` 139 | /api/addr/[:addr] 140 | /api/addr/mmvP3mTe53qxHdPqXEvdu8WdC7GfQ2vmx5 141 | ``` 142 | ### Transactions by Block 143 | ``` 144 | /api/txs/?block=HASH 145 | /api/txs/?block=00000000fa6cf7367e50ad14eb0ca4737131f256fc4c5841fd3c3f140140e6b6 146 | ``` 147 | ### Transactions by Address 148 | ``` 149 | /api/txs/?address=ADDR 150 | /api/txs/?address=mmhmMNfBiZZ37g1tgg2t8DDbNoEdqKVxAL 151 | ``` 152 | 153 | ### Historic blockchain data sync status 154 | ``` 155 | /api/sync 156 | ``` 157 | 158 | ### Live network p2p data sync status 159 | ``` 160 | /api/peer 161 | ``` 162 | 163 | ## Web Socket API 164 | The web socket API is served using [socket.io](http://socket.io) at: 165 | ``` 166 | /socket.io/1/ 167 | ``` 168 | 169 | Bitcoin network events published are: 170 | 'tx': new transaction received from network. Data will be a app/models/Transaction object. 171 | Sample output: 172 | ``` 173 | { 174 | "txid":"00c1b1acb310b87085c7deaaeba478cef5dc9519fab87a4d943ecbb39bd5b053", 175 | "processed":false 176 | ... 177 | } 178 | ``` 179 | 180 | 181 | 'block': new block received from network. Data will be a app/models/Block object. 182 | Sample output: 183 | ``` 184 | { 185 | "hash":"000000004a3d187c430cd6a5e988aca3b19e1f1d1727a50dead6c8ac26899b96", 186 | "time":1389789343, 187 | ... 188 | } 189 | ``` 190 | 191 | 'sync': every 1% increment on the sync task, this event will be triggered. 192 | 193 | Sample output: 194 | ``` 195 | { 196 | blocksToSync: 164141, 197 | syncedBlocks: 475, 198 | upToExisting: true, 199 | scanningBackward: true, 200 | isEndGenesis: true, 201 | end: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", 202 | isStartGenesis: false, 203 | start: "000000009f929800556a8f3cfdbe57c187f2f679e351b12f7011bfc276c41b6d" 204 | } 205 | ``` 206 | ## License 207 | (The MIT License) 208 | 209 | Permission is hereby granted, free of charge, to any person obtaining 210 | a copy of this software and associated documentation files (the 211 | 'Software'), to deal in the Software without restriction, including 212 | without limitation the rights to use, copy, modify, merge, publish, 213 | distribute, sublicense, and/or sell copies of the Software, and to 214 | permit persons to whom the Software is furnished to do so, subject to 215 | the following conditions: 216 | 217 | The above copyright notice and this permission notice shall be 218 | included in all copies or substantial portions of the Software. 219 | 220 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 221 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 222 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 223 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 224 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 225 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 226 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 227 | -------------------------------------------------------------------------------- /lib/HistoricSync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('classtool'); 4 | 5 | 6 | 7 | function spec() { 8 | var util = require('util'); 9 | var assert = require('assert'); 10 | var RpcClient = require('bitcore/RpcClient').class(); 11 | var Script = require('bitcore/Script').class(); 12 | var networks = require('bitcore/networks'); 13 | var async = require('async'); 14 | var config = require('../config/config'); 15 | var Sync = require('./Sync').class(); 16 | var sockets = require('../app/controllers/socket.js'); 17 | var BlockExtractor = require('./BlockExtractor.js').class(); 18 | var buffertools = require('buffertools'); 19 | 20 | // var bitcoreUtil = require('bitcore/util/util'); 21 | // var Deserialize = require('bitcore/Deserialize'); 22 | 23 | 24 | var BAD_GEN_ERROR = 'Bad genesis block. Network mismatch between Insight and bitcoind? Insight is configured for:'; 25 | 26 | var BAD_GEN_ERROR_DB = 'Bad genesis block. Network mismatch between Insight and levelDB? Insight is configured for:'; 27 | function HistoricSync(opts) { 28 | opts = opts || {}; 29 | 30 | this.network = config.network === 'testnet' ? networks.testnet: networks.livenet; 31 | 32 | var genesisHashReversed = new Buffer(32); 33 | this.network.genesisBlock.hash.copy(genesisHashReversed); 34 | buffertools.reverse(genesisHashReversed); 35 | this.genesis = genesisHashReversed.toString('hex'); 36 | 37 | this.rpc = new RpcClient(config.bitcoind); 38 | this.shouldBroadcast = opts.shouldBroadcastSync; 39 | this.sync = new Sync(opts); 40 | } 41 | 42 | function p() { 43 | var args = []; 44 | Array.prototype.push.apply(args, arguments); 45 | 46 | args.unshift('[historic_sync]'); 47 | /*jshint validthis:true */ 48 | console.log.apply(this, args); 49 | } 50 | 51 | HistoricSync.prototype.showProgress = function() { 52 | var self = this; 53 | 54 | if ( self.status ==='syncing' && 55 | ( self.syncedBlocks ) % self.step !== 1) return; 56 | 57 | if (self.error) { 58 | p('ERROR: ' + self.error); 59 | } 60 | else { 61 | self.updatePercentage(); 62 | p(util.format('status: [%d%%]', self.syncPercentage)); 63 | } 64 | if (self.shouldBroadcast) { 65 | sockets.broadcastSyncInfo(self.info()); 66 | } 67 | 68 | // if (self.syncPercentage > 10) { 69 | // process.exit(-1); 70 | // } 71 | }; 72 | 73 | 74 | HistoricSync.prototype.setError = function(err) { 75 | var self = this; 76 | self.error = err.message?err.message:err.toString(); 77 | self.status='error'; 78 | self.showProgress(); 79 | return err; 80 | }; 81 | 82 | 83 | 84 | HistoricSync.prototype.close = function() { 85 | this.sync.close(); 86 | }; 87 | 88 | 89 | HistoricSync.prototype.info = function() { 90 | this.updatePercentage(); 91 | return { 92 | status: this.status, 93 | blockChainHeight: this.blockChainHeight, 94 | syncPercentage: this.syncPercentage, 95 | syncedBlocks: this.syncedBlocks, 96 | syncTipHash: this.sync.tip, 97 | error: this.error, 98 | type: this.type, 99 | startTs: this.startTs, 100 | endTs: this.endTs, 101 | }; 102 | }; 103 | 104 | HistoricSync.prototype.updatePercentage = function() { 105 | var r = this.syncedBlocks / this.blockChainHeight; 106 | this.syncPercentage = parseFloat(100 * r).toFixed(3); 107 | if (this.syncPercentage > 100) this.syncPercentage = 100; 108 | }; 109 | 110 | HistoricSync.prototype.getBlockFromRPC = function(cb) { 111 | var self = this; 112 | 113 | if (!self.currentRpcHash) return cb(); 114 | 115 | var blockInfo; 116 | self.rpc.getBlock(self.currentRpcHash, function(err, ret) { 117 | if (err) return cb(err); 118 | if (ret) { 119 | blockInfo = ret.result; 120 | // this is to match block retreived from file 121 | if (blockInfo.hash === self.genesis) 122 | blockInfo.previousblockhash = 123 | self.network.genesisBlock.prev_hash.toString('hex'); 124 | 125 | self.currentRpcHash = blockInfo.nextblockhash; 126 | } 127 | else { 128 | blockInfo = null; 129 | } 130 | return cb(null, blockInfo); 131 | }); 132 | }; 133 | 134 | HistoricSync.prototype.getBlockFromFile = function(cb) { 135 | var self = this; 136 | 137 | var blockInfo; 138 | 139 | //get Info 140 | self.blockExtractor.getNextBlock(function(err, b) { 141 | if (err || ! b) return cb(err); 142 | blockInfo = b.getStandardizedObject(b.txs, self.network); 143 | blockInfo.previousblockhash = blockInfo.prev_block; 144 | 145 | var ti=0; 146 | // Get TX Address 147 | b.txs.forEach(function(t) { 148 | 149 | 150 | var objTx = blockInfo.tx[ti++]; 151 | 152 | //add time from block 153 | objTx.time = blockInfo.time; 154 | 155 | var to=0; 156 | t.outs.forEach( function(o) { 157 | 158 | 159 | var s = new Script(o.s); 160 | var addrs = self.sync.txDb.getAddrStr(s); 161 | 162 | // support only for p2pubkey p2pubkeyhash and p2sh 163 | if (addrs.length === 1) { 164 | objTx.out[to].addrStr = addrs[0]; 165 | } 166 | to++; 167 | }); 168 | }); 169 | self.sync.bDb.setLastFileIndex(self.blockExtractor.currentFileIndex, function(err) { 170 | return cb(err,blockInfo); 171 | }); 172 | }); 173 | }; 174 | 175 | HistoricSync.prototype.updateConnectedCountDB = function(cb) { 176 | var self = this; 177 | self.sync.bDb.countConnected(function(err, count) { 178 | self.connectedCountDB = count || 0; 179 | self.syncedBlocks = count || 0; 180 | return cb(err); 181 | }); 182 | }; 183 | 184 | 185 | HistoricSync.prototype.updateBlockChainHeight = function(cb) { 186 | var self = this; 187 | 188 | self.rpc.getBlockCount(function(err, res) { 189 | self.blockChainHeight = res.result; 190 | return cb(err); 191 | }); 192 | }; 193 | 194 | 195 | HistoricSync.prototype.checkNetworkSettings = function(next) { 196 | var self = this; 197 | 198 | self.hasGenesis = false; 199 | 200 | // check network config 201 | self.rpc.getBlockHash(0, function(err, res){ 202 | if (!err && ( res && res.result !== self.genesis)) { 203 | err = new Error(BAD_GEN_ERROR + config.network); 204 | } 205 | if (err) return next(err); 206 | self.sync.bDb.has(self.genesis, function(err, b) { 207 | if (!err && ( res && res.result !== self.genesis)) { 208 | err = new Error(BAD_GEN_ERROR_DB + config.network); 209 | } 210 | self.hasGenesis = b?true:false; 211 | return next(err); 212 | }); 213 | }); 214 | }; 215 | 216 | HistoricSync.prototype.updateStartBlock = function(next) { 217 | var self = this; 218 | 219 | self.startBlock = self.genesis; 220 | 221 | self.sync.bDb.getTip(function(err,tip) { 222 | if (!tip) return next(); 223 | 224 | var blockInfo; 225 | var oldtip; 226 | 227 | //check that the tip is still on the mainchain 228 | async.doWhilst( 229 | function(cb) { 230 | self.sync.bDb.fromHashWithInfo(tip, function(err, bi) { 231 | blockInfo = bi ? bi.info : {}; 232 | if (oldtip) 233 | self.sync.setBlockMain(oldtip, false, cb); 234 | else 235 | return cb(); 236 | }); 237 | }, 238 | function(err) { 239 | if (err) return next(err); 240 | var ret = false; 241 | if ( self.blockChainHeight === blockInfo.height || 242 | blockInfo.confirmations > 0) { 243 | ret = false; 244 | } 245 | else { 246 | oldtip = tip; 247 | tip = blockInfo.previousblockhash; 248 | assert(tip); 249 | p('Previous TIP is now orphan. Back to:' + tip); 250 | ret = true; 251 | } 252 | return ret; 253 | }, 254 | function(err) { 255 | self.startBlock = tip; 256 | p('Resuming sync from block:'+tip); 257 | return next(err); 258 | } 259 | ); 260 | }); 261 | }; 262 | 263 | HistoricSync.prototype.prepareFileSync = function(opts, next) { 264 | var self = this; 265 | 266 | if ( opts.forceRPC || !config.bitcoind.dataDir || 267 | self.connectedCountDB > self.blockChainHeight * 0.9) return next(); 268 | 269 | 270 | try { 271 | self.blockExtractor = new BlockExtractor(config.bitcoind.dataDir, config.network); 272 | } catch (e) { 273 | p(e.message + '. Disabling file sync.'); 274 | return next(); 275 | } 276 | 277 | self.getFn = self.getBlockFromFile; 278 | self.allowReorgs = true; 279 | self.sync.bDb.getLastFileIndex(function(err, idx) { 280 | if (opts.forceStartFile) 281 | self.blockExtractor.currentFileIndex = opts.forceStartFile; 282 | else if (idx) self.blockExtractor.currentFileIndex = idx; 283 | 284 | var h = self.genesis; 285 | 286 | p('Seeking file to:' + self.startBlock); 287 | //forward till startBlock 288 | async.whilst( 289 | function() { 290 | return h !== self.startBlock; 291 | }, 292 | function (w_cb) { 293 | self.getBlockFromFile(function(err,b) { 294 | if (!b) return w_cb('Could not find block ' + self.startBlock); 295 | h=b.hash; 296 | setImmediate(function(){ 297 | return w_cb(err); 298 | }); 299 | }); 300 | }, next); 301 | }); 302 | }; 303 | 304 | //NOP 305 | HistoricSync.prototype.prepareRpcSync = function(opts, next) { 306 | var self = this; 307 | 308 | if (self.blockExtractor) return next(); 309 | self.getFn = self.getBlockFromRPC; 310 | self.currentRpcHash = self.startBlock; 311 | self.allowReorgs = false; 312 | return next(); 313 | }; 314 | 315 | HistoricSync.prototype.showSyncStartMessage = function() { 316 | var self = this; 317 | 318 | p('Got ' + self.connectedCountDB + 319 | ' blocks in current DB, out of ' + self.blockChainHeight + ' block at bitcoind'); 320 | 321 | if (self.blockExtractor) { 322 | p('bitcoind dataDir configured...importing blocks from .dat files'); 323 | p('First file index: ' + self.blockExtractor.currentFileIndex); 324 | } 325 | else { 326 | p('syncing from RPC (slow)'); 327 | } 328 | 329 | p('Starting from: ', self.startBlock); 330 | self.showProgress(); 331 | }; 332 | 333 | 334 | HistoricSync.prototype.setupSyncStatus = function() { 335 | var self = this; 336 | 337 | var step = parseInt( (self.blockChainHeight - self.syncedBlocks) / 1000); 338 | if (step < 10) step = 10; 339 | 340 | self.step = step; 341 | self.type = self.blockExtractor?'from .dat Files':'from RPC calls'; 342 | self.status = 'syncing'; 343 | self.startTs = Date.now(); 344 | self.endTs = null; 345 | this.error = null; 346 | this.syncPercentage = 0; 347 | }; 348 | 349 | HistoricSync.prototype.prepareToSync = function(opts, next) { 350 | var self = this; 351 | 352 | self.status = 'starting'; 353 | async.series([ 354 | function(s_c) { 355 | self.checkNetworkSettings(s_c); 356 | }, 357 | function(s_c) { 358 | self.updateConnectedCountDB(s_c); 359 | }, 360 | function(s_c) { 361 | self.updateBlockChainHeight(s_c); 362 | }, 363 | function(s_c) { 364 | self.updateStartBlock(s_c); 365 | }, 366 | function(s_c) { 367 | self.prepareFileSync(opts, s_c); 368 | }, 369 | function(s_c) { 370 | self.prepareRpcSync(opts, s_c); 371 | }, 372 | ], 373 | function(err) { 374 | if (err) return(self.setError(err)); 375 | 376 | self.showSyncStartMessage(); 377 | self.setupSyncStatus(); 378 | return next(); 379 | }); 380 | }; 381 | 382 | 383 | HistoricSync.prototype.start = function(opts, next) { 384 | var self = this; 385 | 386 | if (self.status==='starting' || self.status==='syncing') { 387 | p('## Wont start to sync while status is %s', self.status); 388 | return next(); 389 | } 390 | 391 | self.prepareToSync(opts, function(err) { 392 | if (err) return next(self.setError(err)); 393 | 394 | async.whilst( 395 | function() { 396 | self.showProgress(); 397 | return self.status === 'syncing'; 398 | }, 399 | function (w_cb) { 400 | self.getFn(function(err,blockInfo) { 401 | if (err) return w_cb(self.setError(err)); 402 | if (blockInfo && blockInfo.hash) { 403 | self.syncedBlocks++; 404 | self.sync.storeTipBlock(blockInfo, self.allowReorgs, function(err) { 405 | if (err) return w_cb(self.setError(err)); 406 | 407 | self.sync.bDb.setTip(blockInfo.hash, function(err) { 408 | if (err) return w_cb(self.setError(err)); 409 | 410 | setImmediate(function(){ 411 | return w_cb(err); 412 | }); 413 | }); 414 | }); 415 | } 416 | else { 417 | self.endTs = Date.now(); 418 | self.status = 'finished'; 419 | console.log('Done Syncing', self.info()); 420 | return w_cb(err); 421 | } 422 | }); 423 | }, next); 424 | }); 425 | }; 426 | return HistoricSync; 427 | } 428 | module.defineClass(spec); 429 | 430 | -------------------------------------------------------------------------------- /test/integration/99-sync.js.descructive-test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 4 | 5 | 6 | var 7 | assert = require('assert'), 8 | async = require('async'), 9 | HistoricSync = require('../../lib/HistoricSync').class(); 10 | 11 | 12 | var s; 13 | var b = [ 14 | '00000000c4cbd75af741f3a2b2ff72d9ed4d83a048462c1efe331be31ccf006b', //0 B#16 15 | '00000000fe198cce4c8abf9dca0fee1182cb130df966cc428ad2a230df8da743', //1 16 | '000000008d55c3e978639f70af1d2bf1fe6f09cb3143e104405a599215c89a48', //2 17 | '000000009b3bca4909f38313f2746120129cce4a699a1f552390955da470c5a9', //3 18 | '00000000ede57f31cc598dc241d129ccb4d8168ef112afbdc870dc60a85f5dd3', //4 B#20 19 | ]; 20 | var t = [ 21 | 'd08582d3711f75d085c618874fb0d049ae09d5ec95ec6f5abd289f4b54712c54', // TX from B#16 22 | '1729001087e0cebea8d14de1653d5cf59628d9746bc1ae65f776f1cbaff7ebad', //1 23 | 'cf53d7ccd83a099acfbc319ee10c1e3b10e3d42ba675b569fdd6b69cb8d2db4e', //2 24 | '73a4988adf462b6540cfa59097804174b298cfa439f73c1a072c2c6fbdbe57c7', //3 25 | 'd45f9da73619799e9d7bd03cc290e70875ea4cbad56b8bffa15135fbbb3df9ea', //4 Tx from B20 26 | ]; 27 | 28 | var test = function(cb) { 29 | async.each([2,3,4], function(i,c) { 30 | s.sync.bDb.getPrev(b[i], function(err, p) { 31 | assert.equal(p,b[i-1]); 32 | return c(); 33 | }); 34 | }, function() { 35 | async.each([0,1,2,3,4], function(i,c) { 36 | s.sync.bDb.has(b[i], function(err, p) { 37 | assert(p); 38 | return c(); 39 | }); 40 | }, function() { 41 | async.each([0,1,2,3], function(i,c) { 42 | s.sync.bDb.getNext(b[i], function(err, p) { 43 | assert.equal(p,b[i+1]); 44 | return c(); 45 | }); 46 | }, cb); 47 | }); 48 | }); 49 | }; 50 | 51 | /* 52 | * TEST CASES 53 | * 54 | * Blocks: 0-1-2-3-4 55 | * case 1) 56 | * 0-1-2-3-4 57 | * \ 58 | * C1* 59 | * 60 | * case 2) 61 | * 0-1-2---3-4 62 | * \ \ 63 | * C1 C2* 64 | * 65 | * case 2b) 66 | * 0-1-2---3-4 67 | * \ \ 68 | * C1 C2-C2b(TX=C1.TX)* 69 | * case 2c) 70 | * 0-1-2---3-4 71 | * \ \ 72 | * C1 C2-C2b(TX=C1.TX) 73 | * \ 74 | * C2c(TX=C2.TX)* 75 | * 76 | */ 77 | 78 | describe('Sync Reorgs', function(){ 79 | 80 | before(function(done) { 81 | s = new HistoricSync(); 82 | s.init({}, function(err) { 83 | if (err) return done(err); 84 | s.sync.destroy(done); 85 | }); 86 | }); 87 | 88 | it('simple RPC forward syncing', function(done) { 89 | s.getPrevNextBlock(s.genesis,b[4], { 90 | next: true, 91 | }, function(err) { 92 | if (err) return done(err); 93 | test(done); 94 | }); 95 | }); 96 | 97 | var case1 = { 98 | hash: '0000000000000000000000000000000000000000000000000000000000000001', 99 | tx: [ 'f0596531810160d090813673b4a397f4617aab44eb26c7f06c8a766eac984b91' ], 100 | time: 1296690099, 101 | previousblockhash: b[2], 102 | }; 103 | 104 | 105 | it('reorg, case 1', function(done) { 106 | async.series([ 107 | function (c) { 108 | s.sync.txDb.isConfirmed(t[0], function(err,is) { 109 | assert(!err); 110 | assert(is); 111 | return c(); 112 | }); 113 | }, 114 | function (c) { 115 | s.sync.txDb.isConfirmed(t[3], function(err,is) { 116 | assert(!err); 117 | assert(is); 118 | return c(); 119 | }); 120 | }, 121 | function (c) { 122 | s.sync.txDb.isConfirmed(t[4], function(err,is) { 123 | assert(!err); 124 | assert(is); 125 | return c(); 126 | }); 127 | }, 128 | function (c) { 129 | s.sync.storeTipBlock(case1, function(err) { 130 | assert(!err, 'shouldnt return error' + err); 131 | return c(); 132 | }); 133 | }, 134 | function (c) { 135 | s.sync.bDb.isMain(b[2], function(err,is) { 136 | assert(!err); 137 | assert(is); 138 | return c(); 139 | }); 140 | }, 141 | function (c) { 142 | s.sync.bDb.isMain(b[3], function(err,is) { 143 | assert(!err); 144 | assert(!is, b[3] + 'should not be on main chain'); 145 | return c(); 146 | }); 147 | }, 148 | function (c) { 149 | s.sync.bDb.isMain(b[4], function(err,is) { 150 | assert(!err); 151 | assert(!is); 152 | return c(); 153 | }); 154 | }, 155 | function (c) { 156 | s.sync.txDb.isConfirmed(t[0], function(err,is) { 157 | assert(!err); 158 | assert(is); 159 | return c(); 160 | }); 161 | }, 162 | function (c) { 163 | s.sync.txDb.isConfirmed(t[3], function(err,is) { 164 | assert(!err); 165 | assert(!is); 166 | return c(); 167 | }); 168 | }, 169 | function (c) { 170 | s.sync.txDb.isConfirmed(t[4], function(err,is) { 171 | assert(!err); 172 | assert(!is); 173 | return c(); 174 | }); 175 | }, 176 | function (c) { 177 | s.sync.txDb.isConfirmed(case1.tx[0], function(err,is) { 178 | assert(!err); 179 | assert(is); 180 | return c(); 181 | }); 182 | }, 183 | ], done ); 184 | }); 185 | 186 | it('reorg, case 1 (repeat)', function(done) { 187 | s.sync.storeTipBlock(case1, function(err) { 188 | assert(!err, 'shouldnt return error' + err); 189 | return done(); 190 | }); 191 | }); 192 | 193 | var case2 = { 194 | hash: '0000000000000000000000000000000000000000000000000000000000000002', 195 | tx: [ '99bb359a4b12a588fcb9e59e5e8d92d593ce7a56d2ba42085fe86d9a0b4fde15' ], 196 | time: 1296690099, 197 | previousblockhash: b[3], 198 | }; 199 | 200 | 201 | it('reorg, case 2', function(done) { 202 | async.series([ 203 | function (c) { 204 | s.sync.txDb.isConfirmed(t[0], function(err,is) { 205 | assert(!err); 206 | assert(is); 207 | return c(); 208 | }); 209 | }, 210 | function (c) { 211 | s.sync.txDb.isConfirmed(case1.tx[0], function(err,is) { 212 | assert(!err); 213 | assert(is); 214 | return c(); 215 | }); 216 | }, 217 | function (c) { 218 | s.sync.txDb.isConfirmed(t[4], function(err,is) { 219 | assert(!err); 220 | assert(!is); 221 | return c(); 222 | }); 223 | }, 224 | function (c) { 225 | s.sync.storeTipBlock(case2, function(err) { 226 | assert(!err, 'shouldnt return error' + err); 227 | return c(); 228 | }); 229 | }, 230 | function (c) { 231 | s.sync.bDb.isMain(b[3], function(err,is) { 232 | assert(!err); 233 | assert(is); 234 | return c(); 235 | }); 236 | }, 237 | function (c) { 238 | s.sync.bDb.isMain(b[4], function(err,is) { 239 | assert(!err); 240 | assert(!is, b[3] + 'should not be on main chain'); 241 | return c(); 242 | }); 243 | }, 244 | function (c) { 245 | s.sync.bDb.isMain(case1.hash, function(err,is) { 246 | assert(!err); 247 | assert(!is); 248 | return c(); 249 | }); 250 | }, 251 | function (c) { 252 | s.sync.bDb.isMain(case2.hash, function(err,is) { 253 | assert(!err); 254 | assert(is); 255 | return c(); 256 | }); 257 | }, 258 | function (c) { 259 | s.sync.txDb.isConfirmed(t[3], function(err,is) { 260 | assert(!err); 261 | assert(is, 'transaction t[3] should be valid:' + t[3]); 262 | return c(); 263 | }); 264 | }, 265 | function (c) { 266 | s.sync.txDb.isConfirmed(case1.tx[0], function(err,is) { 267 | assert(!err); 268 | assert(!is); 269 | return c(); 270 | }); 271 | }, 272 | function (c) { 273 | s.sync.txDb.isConfirmed(case2.tx[0], function(err,is) { 274 | assert(!err); 275 | assert(is); 276 | return c(); 277 | }); 278 | }, 279 | function (c) { 280 | s.sync.txDb.isConfirmed(t[4], function(err,is) { 281 | assert(!err); 282 | assert(!is); 283 | return c(); 284 | }); 285 | }, 286 | function (c) { 287 | s.sync.bDb.getNext(b[2], function(err, val) { 288 | assert(!err); 289 | assert.equal(val,b[3]); 290 | return c(); 291 | }); 292 | }, 293 | 294 | 295 | 296 | ], done ); 297 | }); 298 | 299 | 300 | var case2b = { 301 | hash: '0000000000000000000000000000000000000000000000000000000000000003', 302 | tx: case1.tx, 303 | time: 1296690099, 304 | previousblockhash: case2.hash, 305 | }; 306 | 307 | it('reorg, case 2b', function(done) { 308 | async.series([ 309 | function (c) { 310 | s.sync.txDb.isConfirmed(case2b.tx[0], function(err,is) { 311 | assert(!err); 312 | assert(!is); 313 | return c(); 314 | }); 315 | }, 316 | function (c) { 317 | s.sync.storeTipBlock(case2b, function(err) { 318 | assert(!err, 'shouldnt return error' + err); 319 | return c(); 320 | }); 321 | }, 322 | function (c) { 323 | s.sync.txDb.isConfirmed(t[3], function(err,is) { 324 | assert(!err); 325 | assert(is, 'transaction t[3] should be valid:' + t[3]); 326 | return c(); 327 | }); 328 | }, 329 | function (c) { 330 | s.sync.txDb.isConfirmed(case2b.tx[0], function(err,is) { 331 | assert(!err); 332 | assert(is); 333 | return c(); 334 | }); 335 | }, 336 | ], done ); 337 | }); 338 | 339 | 340 | 341 | var case2c = { 342 | hash: '0000000000000000000000000000000000000000000000000000000000000004', 343 | tx: case2.tx, 344 | time: 1296690099, 345 | previousblockhash: case1.hash, 346 | }; 347 | 348 | it('reorg, case 2c', function(done) { 349 | async.series([ 350 | function (c) { 351 | s.sync.txDb.isConfirmed(case1.tx[0], function(err,is) { 352 | assert(!err); 353 | assert(is); 354 | return c(); 355 | }); 356 | }, 357 | function (c) { 358 | s.sync.bDb.isMain(case1.hash, function(err,is) { 359 | assert(!err); 360 | assert(!is, 'case1 block shouldnt be main:' + case1.hash); 361 | return c(); 362 | }); 363 | }, 364 | function (c) { 365 | s.sync.txDb.isConfirmed(case2c.tx[0], function(err,is) { 366 | assert(!err); 367 | assert(is); //It was there before (from case2) 368 | return c(); 369 | }); 370 | }, 371 | function (c) { 372 | s.sync.storeTipBlock(case2c, function(err) { 373 | assert(!err, 'shouldnt return error' + err); 374 | return c(); 375 | }); 376 | }, 377 | function (c) { 378 | s.sync.txDb.isConfirmed(case1.tx[0], function(err,is) { 379 | assert(!err); 380 | assert(is); 381 | return c(); 382 | }); 383 | }, 384 | function (c) { 385 | s.sync.bDb.has(case1.hash, function(err,is) { 386 | assert(!err); 387 | assert(is); 388 | return c(); 389 | }); 390 | }, 391 | function (c) { 392 | s.sync.bDb.has(case2c.hash, function(err,is) { 393 | assert(!err); 394 | assert(is); 395 | return c(); 396 | }); 397 | }, 398 | function (c) { 399 | s.sync.txDb.isConfirmed(case2c.tx[0], function(err,is) { 400 | assert(!err); 401 | assert(is); 402 | return c(); 403 | }); 404 | }, 405 | function (c) { 406 | s.sync.txDb.isConfirmed(t[3], function(err,is) { 407 | assert(!err); 408 | assert(!is, 'TX t[3]: shouldnt be confirmed:' + t[3] +':'+ is); 409 | return c(); 410 | }); 411 | }, 412 | function (c) { 413 | s.sync.txDb.isConfirmed(t[4], function(err,is) { 414 | assert(!err); 415 | assert(!is); 416 | return c(); 417 | }); 418 | }, 419 | function (c) { 420 | s.sync.txDb.isConfirmed(case2.tx[0], function(err,is) { 421 | assert(!err); 422 | assert(is); 423 | return c(); 424 | }); 425 | }, 426 | 427 | ], done ); 428 | }); 429 | 430 | var case3 = { 431 | hash: '0000000000000000000000000000000000000000000000000000000000000005', 432 | tx: case2.tx, 433 | time: 1296690099, 434 | previousblockhash: '666', 435 | }; 436 | 437 | it('reorg, case 3)', function(done) { 438 | async.series([ 439 | function (c) { 440 | s.sync.storeTipBlock(case3, function(err) { 441 | assert(!err, 'shouldnt return error' + err); 442 | return c(); 443 | }); 444 | }, 445 | 446 | //shoudnt change anything 447 | function (c) { 448 | s.sync.txDb.isConfirmed(case1.tx[0], function(err,is) { 449 | assert(!err); 450 | assert(is); 451 | return c(); 452 | }); 453 | }, 454 | function (c) { 455 | s.sync.bDb.has(case1.hash, function(err,is) { 456 | assert(!err); 457 | assert(is); 458 | return c(); 459 | }); 460 | }, 461 | function (c) { 462 | s.sync.bDb.has(case2c.hash, function(err,is) { 463 | assert(!err); 464 | assert(is); 465 | return c(); 466 | }); 467 | }, 468 | function (c) { 469 | s.sync.txDb.isConfirmed(case2c.tx[0], function(err,is) { 470 | assert(!err); 471 | assert(is); 472 | return c(); 473 | }); 474 | }, 475 | function (c) { 476 | s.sync.txDb.isConfirmed(t[3], function(err,is) { 477 | assert(!err); 478 | assert(!is, 'TX t[3]: shouldnt be confirmed:' + t[3] +':'+ is); 479 | return c(); 480 | }); 481 | }, 482 | function (c) { 483 | s.sync.txDb.isConfirmed(t[4], function(err,is) { 484 | assert(!err); 485 | assert(!is); 486 | return c(); 487 | }); 488 | }, 489 | function (c) { 490 | s.sync.txDb.isConfirmed(case2.tx[0], function(err,is) { 491 | assert(!err); 492 | assert(is); 493 | return c(); 494 | }); 495 | }, 496 | 497 | ], done ); 498 | }); 499 | 500 | var p2p = { 501 | hash: '0000000000000000000000000000000000000000000000000000000000000006', 502 | tx: ['f6c2901f39fd07f2f2e503183d76f73ecc1aee9ac9216fde58e867bc29ce674e'], 503 | time: 1296690099, 504 | previousblockhash: '111', 505 | }; 506 | 507 | it('p2p, no reorg allowed', function(done) { 508 | async.series([ 509 | function (c) { 510 | s.sync.storeTipBlock(p2p, false, function(err) { 511 | assert(!err, 'shouldnt return error' + err); 512 | return c(); 513 | }); 514 | }, 515 | function (c) { 516 | s.sync.bDb.has(p2p.hash, function(err,is) { 517 | assert(!err); 518 | assert(is); 519 | return c(); 520 | }); 521 | }, 522 | function (c) { 523 | s.sync.txDb.isConfirmed(p2p.tx[0], function(err,is) { 524 | assert(!err); 525 | assert(is); 526 | return c(); 527 | }); 528 | }, 529 | function (c) { 530 | s.sync.bDb.getNext(p2p.hash, function(err,v) { 531 | assert(!err); 532 | assert.equal(v,p2p.nextblockhash); 533 | return c(); 534 | }); 535 | }, 536 | function (c) { 537 | s.sync.bDb.getNext(p2p.previousblockhash, function(err,v) { 538 | assert(!err); 539 | assert.equal(v,p2p.hash); 540 | return c(); 541 | }); 542 | }, 543 | 544 | ], done ); 545 | }); 546 | }); 547 | 548 | 549 | -------------------------------------------------------------------------------- /lib/TransactionDb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('classtool'); 4 | 5 | 6 | function spec(b) { 7 | 8 | var superclass = b.superclass || require('events').EventEmitter; 9 | // blockHash -> txid mapping 10 | var IN_BLK_PREFIX = 'txb-'; //txb-- => 1/0 (connected or not) 11 | 12 | // Only for orphan blocks 13 | var FROM_BLK_PREFIX = 'tx-'; //tx-- => 1 14 | 15 | // to show tx outs 16 | var OUTS_PREFIX = 'txo-'; //txo-- => [addr, btc_sat] 17 | var SPENT_PREFIX = 'txs-'; //txs---- = ts 18 | 19 | // to sum up addr balance (only outs, spents are gotten later) 20 | var ADDR_PREFIX = 'txa-'; //txa--- => + btc_sat:ts 21 | 22 | // TODO: use bitcore networks module 23 | var genesisTXID = '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b'; 24 | var CONCURRENCY = 10; 25 | 26 | var MAX_OPEN_FILES = 500; 27 | // var CONFIRMATION_NR_TO_NOT_CHECK = 10; //Spend 28 | /** 29 | * Module dependencies. 30 | */ 31 | var Rpc = b.rpc || require('./Rpc').class(), 32 | util = require('bitcore/util/util'), 33 | levelup = require('levelup'), 34 | async = require('async'), 35 | config = require('../config/config'), 36 | assert = require('assert'); 37 | var db = b.db || levelup(config.leveldb + '/txs',{maxOpenFiles: MAX_OPEN_FILES} ); 38 | var Script = require('bitcore/Script').class(); 39 | // This is 0.1.2 => c++ version of base57-native 40 | var base58 = require('base58-native').base58Check; 41 | var encodedData = require('bitcore/util/EncodedData').class({ 42 | base58: base58 43 | }); 44 | var versionedData = require('bitcore/util/VersionedData').class({ 45 | superclass: encodedData 46 | }); 47 | var Address = require('bitcore/Address').class({ 48 | superclass: versionedData 49 | }); 50 | var bitutil = require('bitcore/util/util'); 51 | var networks = require('bitcore/networks'); 52 | 53 | var TransactionDb = function() { 54 | TransactionDb.super(this, arguments); 55 | this.network = config.network === 'testnet' ? networks.testnet : networks.livenet; 56 | }; 57 | TransactionDb.superclass = superclass; 58 | 59 | TransactionDb.prototype.close = function(cb) { 60 | db.close(cb); 61 | }; 62 | 63 | TransactionDb.prototype.drop = function(cb) { 64 | var path = config.leveldb + '/txs'; 65 | db.close(function() { 66 | require('leveldown').destroy(path, function() { 67 | db = levelup(path, {maxOpenFiles: 500}); 68 | return cb(); 69 | }); 70 | }); 71 | }; 72 | 73 | 74 | TransactionDb.prototype.has = function(txid, cb) { 75 | 76 | var k = OUTS_PREFIX + txid; 77 | db.get(k, function(err, val) { 78 | 79 | var ret; 80 | 81 | if (err && err.notFound) { 82 | err = null; 83 | ret = false; 84 | } 85 | if (typeof val !== undefined) { 86 | ret = true; 87 | } 88 | return cb(err, ret); 89 | }); 90 | }; 91 | 92 | TransactionDb.prototype._addSpentInfo = function(r, txid, index, ts) { 93 | if (r.spentTxId) { 94 | if (!r.multipleSpentAttempts) { 95 | r.multipleSpentAttempts = [{ 96 | txid: r.spentTxId, 97 | index: r.index, 98 | }]; 99 | } 100 | r.multipleSpentAttempts.push({ 101 | txid: txid, 102 | index: parseInt(index), 103 | }); 104 | } else { 105 | r.spentTxId = txid; 106 | r.spentIndex = parseInt(index); 107 | r.spentTs = parseInt(ts); 108 | } 109 | }; 110 | 111 | 112 | // This is not used now 113 | TransactionDb.prototype.fromTxId = function(txid, cb) { 114 | var self = this; 115 | var k = OUTS_PREFIX + txid; 116 | var ret = []; 117 | var idx = {}; 118 | var i = 0; 119 | 120 | // outs. 121 | db.createReadStream({ 122 | start: k, 123 | end: k + '~' 124 | }) 125 | .on('data', function(data) { 126 | var k = data.key.split('-'); 127 | var v = data.value.split(':'); 128 | ret.push({ 129 | addr: v[0], 130 | value_sat: parseInt(v[1]), 131 | index: parseInt(k[2]), 132 | }); 133 | idx[parseInt(k[2])] = i++; 134 | }) 135 | .on('error', function(err) { 136 | return cb(err); 137 | }) 138 | .on('end', function() { 139 | 140 | var k = SPENT_PREFIX + txid + '-'; 141 | db.createReadStream({ 142 | start: k, 143 | end: k + '~' 144 | }) 145 | .on('data', function(data) { 146 | var k = data.key.split('-'); 147 | var j = idx[parseInt(k[2])]; 148 | 149 | assert(typeof j !== 'undefined', 'Spent could not be stored: tx ' + txid + 150 | 'spent in TX:' + k[1] + ',' + k[2] + ' j:' + j); 151 | 152 | self._addSpentInfo(ret[j], k[3], k[4], data.value); 153 | }) 154 | .on('error', function(err) { 155 | return cb(err); 156 | }) 157 | .on('end', function(err) { 158 | return cb(err, ret); 159 | }); 160 | }); 161 | }; 162 | 163 | 164 | TransactionDb.prototype._fillSpent = function(info, cb) { 165 | var self = this; 166 | 167 | if (!info) return cb(); 168 | 169 | var k = SPENT_PREFIX + info.txid + '-'; 170 | db.createReadStream({ 171 | start: k, 172 | end: k + '~' 173 | }) 174 | .on('data', function(data) { 175 | var k = data.key.split('-'); 176 | self._addSpentInfo(info.vout[k[2]], k[3], k[4], data.value); 177 | }) 178 | .on('error', function(err) { 179 | return cb(err); 180 | }) 181 | .on('end', function(err) { 182 | return cb(err); 183 | }); 184 | }; 185 | 186 | 187 | TransactionDb.prototype._fillOutpoints = function(info, cb) { 188 | var self = this; 189 | 190 | if (!info || info.isCoinBase) return cb(); 191 | 192 | var valueIn = 0; 193 | var incompleteInputs = 0; 194 | 195 | async.eachLimit(info.vin, CONCURRENCY, function(i, c_in) { 196 | self.fromTxIdN(i.txid, i.vout, info.confirmations, function(err, ret) { 197 | //console.log('[TransactionDb.js.154:ret:]',ret); //TODO 198 | if (!ret || !ret.addr || !ret.valueSat) { 199 | console.log('Could not get TXouts in %s,%d from %s ', i.txid, i.vout, info.txid); 200 | if (ret) i.unconfirmedInput = ret.unconfirmedInput; 201 | incompleteInputs = 1; 202 | return c_in(); // error not scalated 203 | } 204 | 205 | info.firstSeenTs = ret.spentTs; 206 | i.unconfirmedInput = i.unconfirmedInput; 207 | i.addr = ret.addr; 208 | i.valueSat = ret.valueSat; 209 | i.value = ret.valueSat / util.COIN; 210 | valueIn += i.valueSat; 211 | 212 | /* 213 | * If confirmed by bitcoind, we could not check for double spents 214 | * but we prefer to keep the flag of double spent attempt 215 | * 216 | if (info.confirmations 217 | && info.confirmations >= CONFIRMATION_NR_TO_NOT_CHECK) 218 | return c_in(); 219 | isspent 220 | */ 221 | // Double spent? 222 | if (ret.multipleSpentAttempt || !ret.spentTxId || 223 | (ret.spentTxId && ret.spentTxId !== info.txid) 224 | ) { 225 | if (ret.multipleSpentAttempts) { 226 | ret.multipleSpentAttempts.each(function(mul) { 227 | if (mul.spentTxId !== info.txid) { 228 | i.doubleSpentTxID = ret.spentTxId; 229 | i.doubleSpentIndex = ret.spentIndex; 230 | } 231 | }); 232 | } else if (!ret.spentTxId) { 233 | i.dbError = 'Input spent not registered'; 234 | } else { 235 | i.doubleSpentTxID = ret.spentTxId; 236 | i.doubleSpentIndex = ret.spentIndex; 237 | } 238 | } else { 239 | i.doubleSpentTxID = null; 240 | } 241 | return c_in(); 242 | }); 243 | }, 244 | function() { 245 | if (!incompleteInputs) { 246 | info.valueIn = valueIn / util.COIN; 247 | info.fees = (valueIn - parseInt(info.valueOut * util.COIN)) / util.COIN; 248 | } else { 249 | info.incompleteInputs = 1; 250 | } 251 | return cb(); 252 | }); 253 | }; 254 | 255 | TransactionDb.prototype._getInfo = function(txid, next) { 256 | var self = this; 257 | 258 | Rpc.getTxInfo(txid, function(err, info) { 259 | if (err) return next(err); 260 | 261 | self._fillOutpoints(info, function() { 262 | self._fillSpent(info, function() { 263 | return next(null, info); 264 | }); 265 | }); 266 | }); 267 | }; 268 | 269 | 270 | // Simplified / faster Info version: No spent / outpoints info. 271 | TransactionDb.prototype.fromIdInfoSimple = function(txid, cb) { 272 | Rpc.getTxInfo(txid, true, function(err, info) { 273 | if (err) return cb(err); 274 | if (!info) return cb(); 275 | return cb(err, info); 276 | }); 277 | }; 278 | 279 | TransactionDb.prototype.fromIdWithInfo = function(txid, cb) { 280 | var self = this; 281 | 282 | self._getInfo(txid, function(err, info) { 283 | if (err) return cb(err); 284 | if (!info) return cb(); 285 | return cb(err, { 286 | txid: txid, 287 | info: info 288 | }); 289 | }); 290 | }; 291 | 292 | TransactionDb.prototype.fromTxIdN = function(txid, n, confirmations, cb) { 293 | var self = this; 294 | var k = OUTS_PREFIX + txid + '-' + n; 295 | 296 | db.get(k, function(err, val) { 297 | if (!val || (err && err.notFound)) { 298 | return cb(null, { 299 | unconfirmedInput: 1 300 | }); 301 | } 302 | 303 | var a = val.split(':'); 304 | var ret = { 305 | addr: a[0], 306 | valueSat: parseInt(a[1]), 307 | }; 308 | 309 | /* 310 | * If this TxID comes from an RPC request 311 | * the .confirmations value from bitcoind is available 312 | * so we could avoid checking if the input were double spented 313 | * 314 | * This speed up address calculations by ~30% 315 | * 316 | if (confirmations >= CONFIRMATION_NR_TO_NOT_CHECK) { 317 | return cb(null, ret); 318 | } 319 | */ 320 | 321 | // spent? 322 | var k = SPENT_PREFIX + txid + '-' + n + '-'; 323 | db.createReadStream({ 324 | start: k, 325 | end: k + '~' 326 | }) 327 | .on('data', function(data) { 328 | var k = data.key.split('-'); 329 | self._addSpentInfo(ret, k[3], k[4], data.value); 330 | }) 331 | .on('error', function(error) { 332 | return cb(error); 333 | }) 334 | .on('end', function() { 335 | return cb(null, ret); 336 | }); 337 | }); 338 | }; 339 | 340 | TransactionDb.prototype.fillConfirmations = function(o, cb) { 341 | var self = this; 342 | 343 | self.isConfirmed(o.txid, function(err, is) { 344 | if (err) return cb(err); 345 | 346 | o.isConfirmed = is; 347 | if (!o.spentTxId) return cb(); 348 | 349 | if (o.multipleSpentAttempts) { 350 | 351 | async.eachLimit(o.multipleSpentAttempts, CONCURRENCY, 352 | function(oi, e_c) { 353 | self.isConfirmed(oi.spentTxId, function(err, is) { 354 | if (err) return; 355 | if (is) { 356 | o.spentTxId = oi.spentTxId; 357 | o.index = oi.index; 358 | o.spentIsConfirmed = 1; 359 | } 360 | return e_c(); 361 | }); 362 | }, cb); 363 | } else { 364 | self.isConfirmed(o.spentTxId, function(err, is) { 365 | if (err) return cb(err); 366 | o.spentIsConfirmed = is; 367 | return cb(); 368 | }); 369 | } 370 | }); 371 | }; 372 | 373 | TransactionDb.prototype.fromAddr = function(addr, cb) { 374 | var self = this; 375 | 376 | var k = ADDR_PREFIX + addr + '-'; 377 | var ret = []; 378 | 379 | db.createReadStream({ 380 | start: k, 381 | end: k + '~' 382 | }) 383 | .on('data', function(data) { 384 | var k = data.key.split('-'); 385 | var v = data.value.split(':'); 386 | ret.push({ 387 | txid: k[2], 388 | index: parseInt(k[3]), 389 | value_sat: parseInt(v[0]), 390 | ts: parseInt(v[1]), 391 | }); 392 | }) 393 | .on('error', function(err) { 394 | return cb(err); 395 | }) 396 | .on('end', function() { 397 | 398 | async.eachLimit(ret, CONCURRENCY, function(o, e_c) { 399 | var k = SPENT_PREFIX + o.txid + '-' + o.index + '-'; 400 | db.createReadStream({ 401 | start: k, 402 | end: k + '~' 403 | }) 404 | .on('data', function(data) { 405 | var k = data.key.split('-'); 406 | self._addSpentInfo(o, k[3], k[4], data.value); 407 | }) 408 | .on('error', function(err) { 409 | return e_c(err); 410 | }) 411 | .on('end', function(err) { 412 | return e_c(err); 413 | }); 414 | }, 415 | function() { 416 | async.eachLimit(ret, CONCURRENCY, function(o, e_c) { 417 | self.fillConfirmations(o, e_c); 418 | }, function(err) { 419 | return cb(err, ret); 420 | }); 421 | }); 422 | }); 423 | }; 424 | 425 | 426 | TransactionDb.prototype.removeFromTxId = function(txid, cb) { 427 | 428 | async.series([ 429 | 430 | function(c) { 431 | db.createReadStream({ 432 | start: OUTS_PREFIX + txid + '-', 433 | end: OUTS_PREFIX + txid + '~', 434 | }).pipe( 435 | db.createWriteStream({ 436 | type: 'del' 437 | }) 438 | ).on('close', c); 439 | }, 440 | function(c) { 441 | db.createReadStream({ 442 | start: SPENT_PREFIX + txid + '-', 443 | end: SPENT_PREFIX + txid + '~' 444 | }) 445 | .pipe( 446 | db.createWriteStream({ 447 | type: 'del' 448 | }) 449 | ).on('close', c); 450 | } 451 | ], 452 | function(err) { 453 | cb(err); 454 | }); 455 | 456 | }; 457 | 458 | 459 | // TODO. replace with 460 | // Script.prototype.getAddrStrs if that one get merged in bitcore 461 | TransactionDb.prototype.getAddrStr = function(s) { 462 | var self = this; 463 | 464 | var addrStrs = []; 465 | var type = s.classify(); 466 | var addr; 467 | 468 | switch (type) { 469 | case Script.TX_PUBKEY: 470 | var chunk = s.captureOne(); 471 | addr = new Address(self.network.addressPubkey, bitutil.sha256ripe160(chunk)); 472 | addrStrs.push(addr.toString()); 473 | break; 474 | case Script.TX_PUBKEYHASH: 475 | addr = new Address(self.network.addressPubkey, s.captureOne()); 476 | addrStrs.push(addr.toString()); 477 | break; 478 | case Script.TX_SCRIPTHASH: 479 | addr = new Address(self.network.addressScript, s.captureOne()); 480 | addrStrs.push(addr.toString()); 481 | break; 482 | case Script.TX_MULTISIG: 483 | var chunks = s.capture(); 484 | chunks.forEach(function(chunk) { 485 | var a = new Address(self.network.addressPubkey, bitutil.sha256ripe160(chunk)); 486 | addrStrs.push(a.toString()); 487 | }); 488 | break; 489 | case Script.TX_UNKNOWN: 490 | break; 491 | } 492 | 493 | return addrStrs; 494 | }; 495 | 496 | TransactionDb.prototype.adaptTxObject = function(txInfo) { 497 | var self = this; 498 | // adapt bitcore TX object to bitcoind JSON response 499 | txInfo.txid = txInfo.hash; 500 | 501 | 502 | var to = 0; 503 | var tx = txInfo; 504 | if (tx.outs) { 505 | tx.outs.forEach(function(o) { 506 | var s = new Script(o.s); 507 | var addrs = self.getAddrStr(s); 508 | 509 | // support only for p2pubkey p2pubkeyhash and p2sh 510 | if (addrs.length === 1) { 511 | tx.out[to].addrStr = addrs[0]; 512 | tx.out[to].n = to; 513 | } 514 | to++; 515 | }); 516 | } 517 | 518 | var count = 0; 519 | txInfo.vin = txInfo. in .map(function(txin) { 520 | var i = {}; 521 | 522 | if (txin.coinbase) { 523 | txInfo.isCoinBase = true; 524 | } else { 525 | i.txid = txin.prev_out.hash; 526 | i.vout = txin.prev_out.n; 527 | } 528 | i.n = count++; 529 | return i; 530 | }); 531 | 532 | 533 | count = 0; 534 | txInfo.vout = txInfo.out.map(function(txout) { 535 | var o = {}; 536 | 537 | o.value = txout.value; 538 | o.n = count++; 539 | 540 | if (txout.addrStr) { 541 | o.scriptPubKey = {}; 542 | o.scriptPubKey.addresses = [txout.addrStr]; 543 | } 544 | return o; 545 | }); 546 | }; 547 | 548 | 549 | 550 | TransactionDb.prototype.add = function(tx, blockhash, cb) { 551 | var self = this; 552 | var addrs = []; 553 | 554 | if (tx.hash) self.adaptTxObject(tx); 555 | 556 | var ts = tx.time; 557 | 558 | async.series([ 559 | // Input Outpoints (mark them as spent) 560 | function(p_c) { 561 | if (tx.isCoinBase) return p_c(); 562 | async.forEachLimit(tx.vin, CONCURRENCY, 563 | function(i, next_out) { 564 | db.batch() 565 | .put(SPENT_PREFIX + i.txid + '-' + i.vout + '-' + tx.txid + '-' + i.n, 566 | ts || 0) 567 | .write(next_out); 568 | }, 569 | function(err) { 570 | return p_c(err); 571 | }); 572 | }, 573 | // Parse Outputs 574 | function(p_c) { 575 | async.forEachLimit(tx.vout, CONCURRENCY, 576 | function(o, next_out) { 577 | if (o.value && o.scriptPubKey && 578 | o.scriptPubKey.addresses && 579 | o.scriptPubKey.addresses[0] && !o.scriptPubKey.addresses[1] // TODO : not supported 580 | ) { 581 | var addr = o.scriptPubKey.addresses[0]; 582 | var sat = Math.round(o.value * util.COIN); 583 | 584 | if (addrs.indexOf(addr) === -1) { 585 | addrs.push(addr); 586 | } 587 | 588 | // existed? 589 | var k = OUTS_PREFIX + tx.txid + '-' + o.n; 590 | db.get(k, function(err) { 591 | if (err && err.notFound) { 592 | db.batch() 593 | .put(k, addr + ':' + sat) 594 | .put(ADDR_PREFIX + addr + '-' + tx.txid + '-' + o.n, sat + ':' + ts) 595 | .write(next_out); 596 | } else { 597 | return next_out(); 598 | } 599 | }); 600 | } else { 601 | return next_out(); 602 | } 603 | }, 604 | function(err) { 605 | if (err) { 606 | console.log('ERR at TX %s: %s', tx.txid, err); 607 | return cb(err); 608 | } 609 | return p_c(); 610 | }); 611 | }, 612 | function(p_c) { 613 | if (!blockhash) { 614 | return p_c(); 615 | } 616 | return self.setConfirmation(tx.txid, blockhash, true, p_c); 617 | }, 618 | ], function(err) { 619 | if (addrs.length > 0 && !blockhash) { 620 | // only emit if we are processing a single tx (not from a block) 621 | addrs.forEach(function(addr) { 622 | self.emit('tx_for_address', { 623 | address: addr, 624 | txid: tx.txid 625 | }); 626 | }); 627 | } 628 | self.emit('new_tx', { 629 | tx: tx 630 | }); 631 | 632 | return cb(err); 633 | }); 634 | }; 635 | 636 | 637 | 638 | TransactionDb.prototype.setConfirmation = function(txId, blockHash, confirmed, c) { 639 | if (!blockHash) return c(); 640 | 641 | confirmed = confirmed ? 1 : 0; 642 | 643 | db.batch() 644 | .put(IN_BLK_PREFIX + txId + '-' + blockHash, confirmed) 645 | .put(FROM_BLK_PREFIX + blockHash + '-' + txId, 1) 646 | .write(c); 647 | }; 648 | 649 | 650 | // This slowdown addr balance calculation by 100% 651 | TransactionDb.prototype.isConfirmed = function(txId, c) { 652 | var k = IN_BLK_PREFIX + txId; 653 | var ret = false; 654 | 655 | db.createReadStream({ 656 | start: k, 657 | end: k + '~' 658 | }) 659 | .on('data', function(data) { 660 | if (data.value === '1') ret = true; 661 | }) 662 | .on('error', function(err) { 663 | return c(err); 664 | }) 665 | .on('end', function(err) { 666 | return c(err, ret); 667 | }); 668 | }; 669 | 670 | TransactionDb.prototype.handleBlockChange = function(hash, isMain, cb) { 671 | var toChange = []; 672 | console.log('\tSearching Txs from block:' + hash); 673 | 674 | var k = FROM_BLK_PREFIX + hash; 675 | var k2 = IN_BLK_PREFIX; 676 | // This is slow, but prevent us to create a new block->tx index. 677 | db.createReadStream({ 678 | start: k, 679 | end: k + '~' 680 | }) 681 | .on('data', function(data) { 682 | var ks = data.key.split('-'); 683 | toChange.push({ 684 | key: k2 + ks[2] + '-' + ks[1], 685 | type: 'put', 686 | value: isMain ? 1 : 0, 687 | }); 688 | }) 689 | .on('error', function(err) { 690 | return cb(err); 691 | }) 692 | .on('end', function(err) { 693 | if (err) return cb(err); 694 | console.log('\t%s %d Txs', isMain ? 'Confirming' : 'Invalidating', toChange.length); 695 | db.batch(toChange, cb); 696 | }); 697 | }; 698 | 699 | // txs can be a [hashes] or [txObjects] 700 | TransactionDb.prototype.createFromArray = function(txs, blockHash, next) { 701 | var self = this; 702 | if (!txs) return next(); 703 | 704 | async.forEachLimit(txs, CONCURRENCY, function(t, each_cb) { 705 | if (typeof t === 'string') { 706 | // TODO: parse it from networks.genesisTX? 707 | if (t === genesisTXID) return each_cb(); 708 | 709 | Rpc.getTxInfo(t, function(err, inInfo) { 710 | if (!inInfo) return each_cb(err); 711 | 712 | return self.add(inInfo, blockHash, each_cb); 713 | }); 714 | } else { 715 | return self.add(t, blockHash, each_cb); 716 | } 717 | }, 718 | function(err) { 719 | return next(err); 720 | }); 721 | }; 722 | 723 | 724 | TransactionDb.prototype.createFromBlock = function(b, next) { 725 | var self = this; 726 | if (!b || !b.tx) return next(); 727 | 728 | return self.createFromArray(b.tx, b.hash, next); 729 | }; 730 | 731 | return TransactionDb; 732 | } 733 | module.defineClass(spec); 734 | --------------------------------------------------------------------------------