diff --git a/.ctags b/.ctags new file mode 100644 index 0000000..fa51e9e --- /dev/null +++ b/.ctags @@ -0,0 +1,11 @@ +--extra=+f +--exclude=*jquery* +--exclude=node_modules/a* +--exclude=node_modules/[c-z]* +--exclude=*grunt* +--exclude=*bower* +--exclude=.swp +--exclude=public +--links=yes +--totals=yes + diff --git a/.jshintrc b/.jshintrc index 3959625..9e1a154 100644 --- a/.jshintrc +++ b/.jshintrc @@ -32,7 +32,9 @@ "afterEach", "it", "inject", - "expect" + "expect", + "$" + ], "indent": false, // Specify indentation spacing "devel": true, // Allow development statements e.g. `console.log();`. diff --git a/Gruntfile.js b/Gruntfile.js index 32cf055..715c647 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -40,11 +40,11 @@ module.exports = function(grunt) { livereload: true } }, - test: { // we monitor only app/models/* because we have test for models only now - files: ['test/**/*.js', 'test/*.js','app/models/*.js'], - tasks: ['test'], - } +// test: { +// files: ['test/**/*.js', 'test/*.js','app/models/*.js'], +// tasks: ['test'], +// } }, jshint: { all: { @@ -66,7 +66,7 @@ module.exports = function(grunt) { options: { file: 'server.js', args: [], - ignoredFiles: ['public/**', 'test/**','util/**'], + ignoredFiles: ['public/**', 'test/**','util/**','lib/**'], watchedExtensions: ['js'], // nodeArgs: ['--debug'], delayTime: 1, diff --git a/HeaderDB.js b/HeaderDB.js deleted file mode 100644 index 530951b..0000000 --- a/HeaderDB.js +++ /dev/null @@ -1,213 +0,0 @@ -require('classtool'); - -function ClassSpec(b) { - var assert = require('assert'); - var fs = require('fs'); - var Block = require('bitcore/Block').class(); - var Deserialize = require('bitcore/Deserialize'); - var Parser = require('bitcore/util/BinaryParser').class(); - var coinUtil = require('bitcore/util/util'); - - function HeaderDB(b) { - this.network = b.network; - this.fd = null; - this.blocks = {}; - this.byHeight = []; - this.bestBlock = null; - this.cached_size = 0; - } - - HeaderDB.prototype.size = function() { - this.cached_size = Object.keys(this.blocks).length; - return this.cached_size; - }; - - HeaderDB.prototype.locator = function(block) { - if (!block) - block = this.bestBlock; - - var step = 1; - var start = 0; - var loc = []; - // see https://en.bitcoin.it/wiki/Protocol_specification#getblocks - for (var i = block.height; i > 0; i -= step, ++start) { - if (start >= 10) - step *= 2; - loc.push(this.byHeight[i]); - } - assert.equal(this.byHeight[0].toString(), - this.network.genesisBlock.hash.toString()); - loc.push(this.byHeight[0]); - //console.log('Requesting more headers. Current height: ' + block.height ); - return loc; - }; - - HeaderDB.prototype.add = function(block) { - var hash = block.calcHash(); - block.hash = hash; - var curWork = Deserialize.intFromCompact(block.bits); - - if (hash in this.blocks) { - var old = this.blocks[hash]; - throw new Error('duplicate block (was at height ' + old.height + ')'); - } - - var bestChain = false; - - var reorg = { - oldBest: null, - conn: 0, - disconn: 0, - }; - - if (this.cached_size == 0) { - if (this.network.genesisBlock.hash.toString() != - hash.toString()) - throw new Error('Invalid genesis block'); - - block.height = 0; - block.work = curWork; - bestChain = true; - this.cached_size++; - } else { - var prevBlock = this.blocks[block.prev_hash]; - if (!prevBlock) - throw new Error('orphan block; prev not found'); - - block.height = prevBlock.height + 1; - block.work = prevBlock.work + curWork; - this.cached_size++; - - if (block.work > this.bestBlock.work) - bestChain = true; - } - - - // add to by-hash index - this.blocks[hash] = block; - - if (bestChain) { - var oldBest = this.bestBlock; - var newBest = block; - - reorg.oldBest = oldBest; - - // likely case: new best chain has greater height - if (!oldBest) { - while (newBest) { - newBest = this.blocks[newBest.prev_hash]; - reorg.conn++; - } - } else { - while (newBest && - (newBest.height > oldBest.height)) { - newBest = this.blocks[newBest.prev_hash]; - reorg.conn++; - } - } - - // unlikely: old best chain has greater height - while (oldBest && newBest && - (oldBest.height > newBest.height)) { - oldBest = this.blocks[oldBest.prev_hash]; - reorg.disconn++; - } - - // height matches, but still walking parallel - while (oldBest && newBest && (oldBest != newBest)) { - newBest = this.blocks[newBest.prev_hash]; - reorg.conn++; - - oldBest = this.blocks[oldBest.prev_hash]; - reorg.disconn++; - } - - var shuf = (reorg.conn > reorg.disconn) ? - reorg.conn : reorg.disconn; - - // reorg analyzed, updated best-chain pointer - this.bestBlock = block; - - // update by-height index - var ptr = block; - var updated = []; - for (var idx = block.height; - idx > (block.height - shuf); idx--) { - if (idx < 0) - break; - var update = [ idx, ptr ]; - updated.push(update); - ptr = this.blocks[ptr.prev_hash]; - } - - updated.reverse(); - - for (var i = 0; i < updated.length; i++) { - var update = updated[i]; - var idx = update[0]; - var ptr = update[1]; - - if (idx < this.byHeight.length) - this.byHeight[idx] = ptr.hash; - else - this.byHeight.push(ptr.hash); - } - } - return reorg; - }; - - HeaderDB.prototype.addBuf = function(buf) { - var block = new Block(); - var parser = new Parser(buf); - block.parse(parser, true); - this.add(block); - - }; - - - HeaderDB.prototype.readFile = function(filename) { - var fd = fs.openSync(filename, 'r'); - var stats = fs.fstatSync(fd); - if (stats.size % 80 != 0) - throw new Error('Corrupted header db'); - - while (1) { - var buf = new Buffer(80); - var bread = fs.readSync(fd, buf, 0, 80, null); - if (bread < 80) - break; - - this.addBuf(buf); - - if ( ! ( this.cached_size % 1000 )) { - console.log('\tblock...' + this.cached_size ) ; - } - } - - fs.closeSync(fd); - }; - - HeaderDB.prototype.writeFile = function(filename) { - var block = this.bestBlock; - var data = []; - while (block) { - var s = block.getHeader(); - data.push(s); - block = this.blocks[block.prev_hash]; - } - - data.reverse(); - - var fd = fs.openSync(filename, 'w'); - - data.forEach(function(datum) { - fs.writeSync(fd, datum, 0, 80, null); - }); - - fs.closeSync(fd); - }; - - return HeaderDB; -}; -module.defineClass(ClassSpec); - diff --git a/README.md b/README.md index 50af0ee..49f2ca6 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ $ npm install -g bower $ ln -s /node-bufferput bufferput Get bitcore from github repository: + + Get bitcore from github repository: $ git clone https://github.com/bitpay/bitcore.git @@ -95,6 +97,12 @@ $ npm install -g bower Check utils/sync.js --help for options. + +## API + +A REST API is provided at /api. The entry points are: + + ### Blocks ``` /api/block/[:hash] @@ -120,13 +128,13 @@ All configuration is specified in the [config](config/) folder, particularly the There is a bitcoind configuration sample at: ``` - etc/mystery/bitcoin.conf + etc/bitcoind/bitcoin.conf ``` If you want to use a external bitcoind server set BITCOIND_HOST / BITCOIND_PORT enviroment variables. Make sure that bitcoind is configured to accept incomming connections using 'rpcallowip' decribed in https://en.bitcoin.it/wiki/Running_Bitcoin. -### Environmental Settings +### Environment Variables Settings There are three environments provided by default, __development__, __test__, and __production__. Each of these environments has the following configuration options: * __db__ - This is the name of the MongoDB database to use, and is set by default to __mystery-dev__ for the development environment. @@ -140,6 +148,13 @@ If you are using node instead of grunt, it is very similar: $ NODE_ENV=test node server + +### Development enviroment +To run mystery locally for development: + + $ NODE_ENV=development grunt + + ## Github [Mystery](https://github.com/bitpay/mystery) diff --git a/app/controllers/addresses.js b/app/controllers/addresses.js new file mode 100644 index 0000000..e13000c --- /dev/null +++ b/app/controllers/addresses.js @@ -0,0 +1,37 @@ +'use strict'; + +/** + * Module dependencies. + */ + +var Address = require('../models/Address'); + + +/** + * Find block by hash ... + */ +exports.address = function(req, res, next, addr) { + var a = Address.new(addr); + + a.update(function(err) { + if (err && !a.totalReceivedSat) { + console.log(err); + res.status(404).send('Invalid address'); + return next(); + } + + req.address = a; + return next(); + }); +}; + + +/** + * Show block + */ +exports.show = function(req, res) { + if (req.address) { + res.jsonp(req.address); + } +}; + diff --git a/app/controllers/blocks.js b/app/controllers/blocks.js index 5381ec0..21f82d0 100644 --- a/app/controllers/blocks.js +++ b/app/controllers/blocks.js @@ -14,10 +14,14 @@ var mongoose = require('mongoose'), */ exports.block = function(req, res, next, hash) { Block.fromHashWithInfo(hash, function(err, block) { - if (err) return next(err); - if (!block) return next(new Error('Failed to load block ' + hash)); + if (err && !block) { + console.log(err); + res.status(404).send('Not found'); + return next(); + } + req.block = block.info; - next(); + return next(); }); }; @@ -26,7 +30,9 @@ exports.block = function(req, res, next, hash) { * Show block */ exports.show = function(req, res) { - res.jsonp(req.block); + if (req.block) { + res.jsonp(req.block); + } }; /** diff --git a/app/models/Address.js b/app/models/Address.js new file mode 100644 index 0000000..0541488 --- /dev/null +++ b/app/models/Address.js @@ -0,0 +1,127 @@ +'use strict'; + +require('classtool'); + + +function spec() { + var async = require('async'); + var TransactionItem = require('./TransactionItem'); + var BitcoreAddress = require('bitcore/Address').class(); + var BitcoreUtil = require('bitcore/util/util'); + + function Address(addrStr) { + this.balanceSat = 0; + this.totalReceivedSat = 0; + this.totalSentSat = 0; + this.txApperances = 0; + + // TODO store only txids? +index? +all? + this.transactions = []; + + var a = new BitcoreAddress(addrStr); + try { + a.validate(); + this.addrStr = addrStr; + } catch(e){ + } + } + + + Address.prototype.__defineGetter__('balance', function(){ + +console.log('#################### '+this.balanceSat); + + + return this.balanceSat / BitcoreUtil.COIN; + }); + + Address.prototype.update = function(next) { + + if (! this.addrStr) { + return next(new Error('Invalid or undefined address string')); + } + + var that = this; + async.series([ + // TODO TXout! + //T + function (cb) { + TransactionItem.find({addr:that.addrStr}, function(err,txItems){ + if (err) return cb(err); + + txItems.forEach(function(txItem){ + + // console.log(txItem.txid + ' : ' + txItem.value_sat); + that.txApperances +=1; + that.balanceSat += txItem.value_sat; + + that.transactions.push(txItem.txid); + + if (txItem.value_sat > 0) + that.totalSentSat += txItem.value_sat; + else + that.totalReceivedSat += Math.abs(txItem.value_sat); + }); + return cb(); + }); + } + ], function (err) { + return next(err); + }); + }; + + return Address; +} +module.defineClass(spec); + + +/** + * Addr Schema Idea for moogose. Not used now. + * +var AddressSchema = new Schema({ + + // For now we keep this as short as possible + // More fields will be propably added as we move + // forward with the UX + addr: { + type: String, + index: true, + unique: true, + }, + inputs: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'TransactionItem' //Edit: I'd put the schema. Silly me. + }], + output: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'TransactionItem' //Edit: I'd put the schema. Silly me. + }], +}); + + +AddressSchema.statics.load = function(id, cb) { + this.findOne({ + _id: id + }).exec(cb); +}; + + +AddressSchema.statics.fromAddr = function(hash, cb) { + this.findOne({ + hash: hash, + }).exec(cb); +}; + + +AddressSchema.statics.fromAddrWithInfo = function(hash, cb) { + this.fromHash(hash, function(err, addr) { + if (err) return cb(err); + if (!addr) { return cb(new Error('Addr not found')); } +// TODO +// addr.getInfo(function(err) { return cb(err,addr); } ); + }); +}; + +module.exports = mongoose.model('Address', AddressSchema); +*/ + diff --git a/app/models/Transaction.js b/app/models/Transaction.js index 61c0605..29b0499 100644 --- a/app/models/Transaction.js +++ b/app/models/Transaction.js @@ -13,7 +13,8 @@ var mongoose = require('mongoose'), networks = require('bitcore/networks'), util = require('bitcore/util/util'), bignum = require('bignum'), - config = require('../../config/config'); + config = require('../../config/config'), + TransactionItem = require('./TransactionItem'); /** @@ -27,6 +28,15 @@ var TransactionSchema = new Schema({ index: true, unique: true, }, + processed: { + type: Boolean, + default: false, + index: true, + }, + orphaned: { + type: Boolean, + default: false, + }, }); /** @@ -46,19 +56,37 @@ TransactionSchema.statics.fromId = function(txid, cb) { }).exec(cb); }; -TransactionSchema.statics.fromIdWithInfo = function(txid, cb) { - // TODO Should we go to mongoDB first? Now, no extra information is stored at mongo. +TransactionSchema.statics.fromIdWithInfo = function(txid, cb) { + var That = this; this.fromId(txid, function(err, tx) { if (err) return cb(err); - if (!tx) { return cb(new Error('TX not found')); } + if (!tx) { + // No in mongo...but maybe in bitcoind... lets query it + tx = new That(); - tx.queryInfo(function(err) { return cb(err,tx); } ); + tx.txid = txid; + tx.queryInfo(function(err, txInfo) { + + if (!txInfo) + return cb(new Error('TX not found1')); + + tx.save(function(err) { + return cb(err,tx); + }); + }); + } + else { + tx.queryInfo(function(err) { + return cb(err,tx); + }); + } }); }; + TransactionSchema.statics.createFromArray = function(txs, next) { var that = this; if (!txs) return next(); @@ -79,11 +107,72 @@ TransactionSchema.statics.createFromArray = function(txs, next) { }; +TransactionSchema.statics.explodeTransactionItems = function(txid, cb) { + + this.fromIdWithInfo(txid, function(err, t) { + if (err || !t) return cb(err); + + var index = 0; + t.info.vin.forEach( function(i){ + i.n = index++; + }); + + async.each(t.info.vin, function(i, next_in) { + if (i.addr && i.value) { + +//console.log("Creating IN %s %d", i.addr, i.valueSat); + TransactionItem.create({ + txid : t.txid, + value_sat : -1 * i.valueSat, + addr : i.addr, + index : i.n, + ts : t.info.time, + }, next_in); + } + else { + if ( !i.coinbase ) { + console.log ('TX: %s,%d could not parse INPUT', t.txid, i.n); + } + return next_in(); + } + }, + function (err) { + if (err) console.log (err); + async.each(t.info.vout, function(o, next_out) { + + /* + * TODO Support multisigs + */ + if (o.value && o.scriptPubKey && o.scriptPubKey.addresses && o.scriptPubKey.addresses[0]) { +//console.log("Creating OUT %s %d", o.scriptPubKey.addresses[0], o.valueSat); + TransactionItem.create({ + txid : t.txid, + value_sat : o.valueSat, + addr : o.scriptPubKey.addresses[0], + index : o.n, + ts : t.info.time, + }, next_out); + } + else { + console.log ('TX: %s,%d could not parse OUTPUT', t.txid, o.n); + return next_out(); + } + }, + function (err) { + return cb(err); + }); + }); + }); +}; + + + TransactionSchema.methods.fillInputValues = function (tx, next) { if (tx.isCoinBase()) return next(); if (! this.rpc) this.rpc = new RpcClient(config.bitcoind); + var network = ( config.network === 'testnet') ? networks.testnet : networks.livenet ; var that = this; async.each(tx.ins, function(i, cb) { @@ -95,21 +184,31 @@ TransactionSchema.methods.fillInputValues = function (tx, next) { var c=0; that.rpc.getRawTransaction(outHashBase64, function(err, txdata) { var txin = new Transaction(); - if (err || ! txdata.result) return cb( new Error('Input TX '+outHashBase64+' not found')); var b = new Buffer(txdata.result,'hex'); txin.parse(b); - - if ( txin.isCoinBase() ) { - return cb(); - } + /* + *We have to parse it anyways. It will have outputs even it is a coinbase tx + if ( txin.isCoinBase() ) { + return cb(); + } + */ txin.outs.forEach( function(j) { // console.log( c + ': ' + util.formatValue(j.v) ); if (c === outIndex) { i.value = j.v; + + // This is used for pay-to-pubkey transaction in which + // the pubkey is not provided on the input + var scriptPubKey = j.getScript(); + var hash = scriptPubKey.simpleOutHash(); + if (hash) { + var addr = new Address(network.addressPubkey, hash); + i.addrFromOutput = addr.toString(); + } } c++; }); @@ -154,29 +253,41 @@ TransactionSchema.methods.queryInfo = function (next) { } else { tx.ins.forEach(function(i) { + if (i.value) { + that.info.vin[c].value = util.formatValue(i.value); + var n = util.valueToBigInt(i.value).toNumber(); + that.info.vin[c].valueSat = n; + valueIn = valueIn.add( n ); - that.info.vin[c].value = util.formatValue(i.value); - var n = util.valueToBigInt(i.value).toNumber(); - valueIn = valueIn.add( n ); + var scriptSig = i.getScript(); + var pubKey = scriptSig.simpleInPubKey(); - var scriptSig = i.getScript(); - var pubKey = scriptSig.simpleInPubKey(); - - // We check for pubKey in case a broken / strange TX. - if (pubKey) { - var pubKeyHash = util.sha256ripe160(pubKey); - var addr = new Address(network.addressPubkey, pubKeyHash); - var addrStr = addr.toString(); - that.info.vin[c].addr = addrStr; + // We check for pubKey in case a broken / strange TX. + if (pubKey) { + var pubKeyHash = util.sha256ripe160(pubKey); + var addr = new Address(network.addressPubkey, pubKeyHash); + var addrStr = addr.toString(); + that.info.vin[c].addr = addrStr; + } + else { + if (i.addrFromOutput) + that.info.vin[c].addr = i.addrFromOutput; + } + } + else { + console.log('TX could not be parsed: %s,%d' ,txInfo.result.txid, c); } - c++; }); } + c=0; tx.outs.forEach( function(i) { var n = util.valueToBigInt(i.v).toNumber(); valueOut = valueOut.add(n); + + that.info.vout[c].valueSat = n; + c++; }); that.info.valueOut = valueOut / util.COIN; diff --git a/app/models/TransactionItem.js b/app/models/TransactionItem.js new file mode 100644 index 0000000..5a04f84 --- /dev/null +++ b/app/models/TransactionItem.js @@ -0,0 +1,54 @@ +'use strict'; + +/** + * Module dependencies. + */ +var mongoose = require('mongoose'), + Schema = mongoose.Schema; + +var TransactionItemSchema = new Schema({ + txid: String, + index: Number, + addr: { + type: String, + index: true, + }, + // OJO: mongoose doesnt accept camelcase for field names + // <0 is Input >0 is Output + value_sat: Number, + ts: Number, +}); + + +// Compound index +TransactionItemSchema.index({txid: 1, index: 1, value_sat: 1}, {unique: true, dropDups: true}); + + +TransactionItemSchema.statics.load = function(id, cb) { + this.findOne({ + _id: id + }).exec(cb); +}; + + +TransactionItemSchema.statics.fromTxId = function(txid, cb) { + this.find({ + txid: txid, + }).exec(function (err,items) { + + // sort by 1) value sign 2) index + return cb(err,items.sort(function(a,b){ + var sa= a.value_sat < 0 ? -1 : 1; + var sb= b.value_sat < 0 ? -1 : 1; + + if (sa !== sb) { + return sa-sb; + } + else { + return a.index - b.index; + } + })); + }); +}; + +module.exports = mongoose.model('TransactionItem', TransactionItemSchema); diff --git a/app/views/includes/foot.jade b/app/views/includes/foot.jade index 2b10e08..ab3e493 100755 --- a/app/views/includes/foot.jade +++ b/app/views/includes/foot.jade @@ -25,6 +25,7 @@ script(type='text/javascript', src='/js/directives.js') script(type='text/javascript', src='/js/filters.js') //Application Services +script(type='text/javascript', src='/js/services/address.js') script(type='text/javascript', src='/js/services/transactions.js') script(type='text/javascript', src='/js/services/blocks.js') script(type='text/javascript', src='/js/services/global.js') diff --git a/app/views/includes/head.jade b/app/views/includes/head.jade index 090c3e2..cdec0ae 100755 --- a/app/views/includes/head.jade +++ b/app/views/includes/head.jade @@ -3,13 +3,16 @@ head meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1') meta(name='viewport', content='width=device-width,initial-scale=1.0') - title Mystery + title= appName+' - '+title meta(http-equiv='Content-type', content='text/html;charset=UTF-8') meta(name="keywords", content="node.js, express, mongoose, mongodb, angularjs") meta(name="description", content="Mystery") link(href='/img/icons/favicon.ico', rel='shortcut icon', type='image/x-icon') - + link(rel='stylesheet', href='/lib/bootstrap/dist/css/bootstrap.min.css') link(rel='stylesheet', href='/css/common.css') - + + script(src='/socket.io/socket.io.js') + script(src='/lib/jquery/jquery.js') + diff --git a/app/views/index.jade b/app/views/index.jade index a74f173..310005d 100755 --- a/app/views/index.jade +++ b/app/views/index.jade @@ -2,3 +2,4 @@ extends layouts/default block content section.container(data-ng-view) + diff --git a/app/views/sockets/main.js b/app/views/sockets/main.js new file mode 100644 index 0000000..b6d9db4 --- /dev/null +++ b/app/views/sockets/main.js @@ -0,0 +1,13 @@ +'use strict'; + +var Transaction = require('../../models/Transaction'); + +module.exports = function(app, io) { + io.set('log level', 1); // reduce logging + io.sockets.on('connection', function(socket) { + Transaction.findOne(function(err, tx) { + socket.emit('tx', tx); + }); + }); +}; + diff --git a/config/env/development.js b/config/env/development.js index f01f9fb..3439a60 100755 --- a/config/env/development.js +++ b/config/env/development.js @@ -6,9 +6,9 @@ module.exports = { name: "Mystery - Development" }, bitcoind: { - user: 'mystery', - pass: 'real_mystery', - protocol: 'http', + protocol: process.env.BITCOIND_PROTO || 'http', + user: process.env.BITCOIND_USER || 'mystery', + pass: process.env.BITCOIND_PASS || 'real_mystery', host: process.env.BITCOIND_HOST || '127.0.0.1', port: process.env.BITCOIND_PORT || '18332', }, diff --git a/config/env/test.js b/config/env/test.js index 902efe9..dd7a70e 100755 --- a/config/env/test.js +++ b/config/env/test.js @@ -1,16 +1,17 @@ 'use strict'; module.exports = { - db: "mongodb://localhost/mystery-dev", + db: "mongodb://localhost/mystery-test", app: { name: "Mystery - Test" }, + port: '3301', bitcoind: { - user: 'mystery', - pass: 'real_mystery', - protocol: 'http', + protocol: process.env.BITCOIND_PROTO || 'http', + user: process.env.BITCOIND_USER || 'mystery', + pass: process.env.BITCOIND_PASS || 'real_mystery', host: process.env.BITCOIND_HOST || '127.0.0.1', - port: process.env.BITCOIND_PORT || '8332', + port: process.env.BITCOIND_PORT || '18332', }, network: 'testnet', } diff --git a/config/express.js b/config/express.js index 1abe190..a25db3b 100644 --- a/config/express.js +++ b/config/express.js @@ -4,7 +4,8 @@ * Module dependencies. */ var express = require('express'), - config = require('./config'); + helpers = require('view-helpers'), + config = require('./config'); module.exports = function(app, passport, db) { app.set('showStackError', true); @@ -36,6 +37,9 @@ module.exports = function(app, passport, db) { app.use(express.json()); app.use(express.methodOverride()); + //dynamic helpers + app.use(helpers(config.app.name)); + //routes should be at the last app.use(app.router); @@ -66,4 +70,4 @@ module.exports = function(app, passport, db) { }); }); -}; \ No newline at end of file +}; diff --git a/config/routes.js b/config/routes.js index 83c7eb3..45bc4ad 100644 --- a/config/routes.js +++ b/config/routes.js @@ -19,4 +19,8 @@ module.exports = function(app) { app.param('txid', transactions.transaction); + var addresses = require('../app/controllers/addresses'); + app.get('/api/addr/:addr', addresses.show); + app.param('addr', addresses.address); + }; diff --git a/lib/PeerSync.js b/lib/PeerSync.js new file mode 100644 index 0000000..573c402 --- /dev/null +++ b/lib/PeerSync.js @@ -0,0 +1,121 @@ +'use strict'; +require('classtool'); + +function spec() { + var fs = require('fs'); + var CoinConst = require('bitcore/const'); + var coinUtil = require('bitcore/util/util'); + var Sync = require('./Sync').class(); + var Peer = require('bitcore/Peer').class(); + + var peerdb_fn = 'peerdb.json'; + + function PeerSync() {} + PeerSync.prototype.init = function(config) { + + var network = config && (config.network || "testnet"); + + this.peerdb = undefined; + this.sync = new Sync({ + networkName: network + }); + this.sync.init(config); + + this.PeerManager = require('bitcore/PeerManager').createClass({ + config: { + network: network + } + }); + this.load_peers(); + + }; + + PeerSync.prototype.load_peers = function() { + try { + this.peerdb = JSON.parse(fs.readFileSync(peerdb_fn)); + } catch(d) { + console.warn('Unable to read peer db', peerdb_fn, 'creating new one.'); + this.peerdb = [{ + ipv4: '127.0.0.1', + port: 18333 + }, + ]; + + fs.writeFileSync(peerdb_fn, JSON.stringify(this.peerdb)); + } + }; + + PeerSync.prototype.handle_inv = function(info) { + // TODO: should limit the invs to objects we haven't seen yet + var invs = info.message.invs; + invs.forEach(function(inv) { + console.log('[p2p_sync] Handle inv for a ' + CoinConst.MSG.to_str(inv.type)); + }); + // this is not needed right now, but it's left in case + // we need to store more info in the future + info.conn.sendGetData(invs); + }; + + PeerSync.prototype.handle_tx = function(info) { + var tx = info.message.tx.getStandardizedObject(); + console.log('[p2p_sync] Handle tx: ' + tx.hash); + this.sync.storeTxs([tx.hash], function(err) { + if (err) { + console.log('[p2p_sync] Error in handle TX: ' + err); + } + }); + }; + + PeerSync.prototype.handle_block = function(info) { + var self = this; + var block = info.message.block; + var now = Math.round(new Date().getTime() / 1000); + var blockHash = coinUtil.formatHashFull(block.calcHash()); + console.log('[p2p_sync] Handle block: ' + blockHash); + this.sync.storeBlock({ + 'hash': blockHash, + 'time': now + }, + function(err) { + if (err) { + console.log('[p2p_sync] Error in handle Block: ' + err); + } else { + // if no errors importing block, import the transactions + var hashes = block.txs.map(function(tx) { + return coinUtil.formatHashFull(tx.hash); + }); + self.sync.storeTxs(hashes, function() {}); + } + }); + + }; + + PeerSync.prototype.handle_connected = function(data) { + var peerman = data.pm; + var peers_n = peerman.peers.length; + console.log('[p2p_sync] Connected to ' + peers_n + ' peer' + (peers_n !== 1 ? 's': '')); + }; + + PeerSync.prototype.run = function() { + var self = this; + var peerman = new this.PeerManager(); + + this.peerdb.forEach(function(datum) { + var peer = new Peer(datum.ipv4, datum.port); + peerman.addPeer(peer); + }); + + peerman.on('connection', function(conn) { + conn.on('inv', self.handle_inv.bind(self)); + conn.on('block', self.handle_block.bind(self)); + conn.on('tx', self.handle_tx.bind(self)); + }); + peerman.on('connect', self.handle_connected.bind(self)); + + peerman.start(); + }; + return PeerSync; + +} +module.defineClass(spec); + diff --git a/lib/Sync.js b/lib/Sync.js index af05493..b3dfc4a 100644 --- a/lib/Sync.js +++ b/lib/Sync.js @@ -2,23 +2,20 @@ require('classtool'); -/* We dont sync any contents from TXs, only their IDs are stored */ - -var isSyncTxEnabled = 0; function spec() { - var mongoose = require('mongoose'); - var util = require('util'); - - var RpcClient = require('bitcore/RpcClient').class(); - var networks = require('bitcore/networks'); - var async = require('async'); - - var config = require('../config/config'); - var Block = require('../app/models/Block'); - var Transaction = require('../app/models/Transaction'); + var mongoose = require('mongoose'); + var util = require('util'); + var RpcClient = require('bitcore/RpcClient').class(); + var networks = require('bitcore/networks'); + var async = require('async'); + var config = require('../config/config'); + var Block = require('../app/models/Block'); + var Transaction = require('../app/models/Transaction'); + var TransactionItem = require('../app/models/TransactionItem'); function Sync(config) { + this.tx_count =0; this.network = config.networkName === 'testnet' ? networks.testnet: networks.livenet; } @@ -36,7 +33,7 @@ function spec() { if (blockInfo.result.height % 1000 === 0) { var h = blockInfo.result.height, d = blockInfo.result.confirmations; - progress_bar('height', h, h + d); + progress_bar(util.format('Height [txs:%d]',that.tx_count), h, h + d); } that.storeBlock(blockInfo.result, function(err) { @@ -62,8 +59,23 @@ function spec() { }); }; - Sync.prototype.storeTxs = function(txs, cb) { - Transaction.createFromArray(txs, cb); + Sync.prototype.storeTxs = function(txids, cb) { + var that=this; + Transaction.createFromArray(txids, function(err) { + if (err) return cb(err); + + async.each(txids, function(txid, next) { + + // This will trigger an RPC call + Transaction.explodeTransactionItems( txid, function(err) { + that.tx_count++; + next(err); + }); + }, + function(err) { + return cb(); + }); + }); }; Sync.prototype.syncBlocks = function(reindex, cb) { @@ -151,9 +163,50 @@ function spec() { }); }; - Sync.prototype.init = function(opts) { - mongoose.connect(config.db); + // Not used + Sync.prototype.processTXs = function(reindex, cb) { + + var that = this; + + console.log('Syncing TXs...'); + + var filter = reindex ? {} : { processed: false } ; + + Transaction.find(filter, function(err, txs) { + if (err) return cb(err); + + var read = 0, + pull = 0, + proc = 0, + total = txs.length; + + console.log('\tneed to pull %d txs', total); + + if (!total) return cb(); + + async.each(txs, function(tx, next) { + if (read++ % 1000 === 0) progress_bar('read', read, total); + + if (!tx.txid) { + console.log('NO TXID skipping...', tx); + return next(); + } + + // This will trigger an RPC call + Transaction.explodeTransactionItems( tx.txid, function(err) { + if (proc++ % 1000 === 0) progress_bar('\tproc', pull, total); + next(err); + }); + }, + cb); + }); + }; + + Sync.prototype.init = function(opts) { + if (!(opts && opts.skip_db_connection)) { + mongoose.connect(config.db); + } this.db = mongoose.connection; this.rpc = new RpcClient(config.bitcoind); @@ -182,6 +235,15 @@ function spec() { cb(); } }, + function(cb) { + if (opts.destroy) { + console.log('Deleting TXItems...'); + that.db.collections.transactionitems.drop(cb); + } else { + cb(); + } + }, + function(cb) { if (!opts.skip_blocks) { that.syncBlocks(opts.reindex, cb); @@ -189,14 +251,27 @@ function spec() { cb(); } }, +/* Exploding happens on block insertion function(cb) { - if (isSyncTxEnabled && ! opts.skip_txs) { + if (! opts.skip_txs) { + that.processTXs(opts.reindex, cb); + } + else { + return cb(); + } + } +*/ +/* We dont sync any contents from TXs, only their IDs are stored + function(cb) { + if (! opts.skip_txs) { that.syncTXs(opts.reindex, cb); } else { return cb(); } - }], function(err) { + } +*/ + ], function(err) { return next(err); }); }); diff --git a/p2p.js b/p2p.js index e6ee4a0..aa0a08d 100755 --- a/p2p.js +++ b/p2p.js @@ -1,4 +1,8 @@ -#! /usr/bin/env node +#!/usr/bin/env node +<<<<<<< HEAD +======= + +>>>>>>> 71e1c718ac8f5eb89acedb4f91f2207ec463808b 'use strict'; process.env.NODE_ENV = process.env.NODE_ENV || 'development'; diff --git a/package.json b/package.json index c9338fc..968afbb 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,10 @@ "grunt-concurrent": "~0.4.2", "grunt-nodemon": "~0.1.2", "grunt-mocha-test": "~0.8.1", - "should": "~2.1.1" + "should": "~2.1.1", + "view-helpers": "latest", + "socket.io": "~0.9.16" + }, "devDependencies": { "grunt-contrib-watch": "latest", diff --git a/public/js/config.js b/public/js/config.js index c176160..1281385 100755 --- a/public/js/config.js +++ b/public/js/config.js @@ -19,7 +19,7 @@ angular.module('mystery').config(['$routeProvider', when('/blocks-date/:blockDate', { templateUrl: 'views/blocks/list.html' }). - when('/address/:address', { + when('/address/:addrStr', { templateUrl: 'views/address.html' }). otherwise({ diff --git a/public/js/controllers/address.js b/public/js/controllers/address.js index e921d98..7c859db 100644 --- a/public/js/controllers/address.js +++ b/public/js/controllers/address.js @@ -1,8 +1,7 @@ 'use strict'; -angular.module('mystery.address').controller('AddressController', ['$scope', function ($scope) { - - //example data +angular.module('mystery.address').controller('AddressController', ['$scope', '$routeParams', '$location', 'Global', 'Address', function ($scope, $routeParams, $location, Global, Address) { +/* $scope.address = '1JmTTDcksW7A6GN7JnxuXkMAXsVN9zmgm1'; $scope.hash160 = '77ad7d08aaa9cf489ea4e468eaeb892b85f71e27'; $scope.transactions = [ @@ -17,4 +16,15 @@ angular.module('mystery.address').controller('AddressController', ['$scope', fun amount: 0.1 } ]; +*/ + $scope.global = Global; + + $scope.findOne = function() { + Address.get({ + addrStr: $routeParams.addrStr + }, function(address) { + $scope.address = address; + }); + }; + }]); diff --git a/public/js/controllers/blocks.js b/public/js/controllers/blocks.js index 7bb5c80..d9dd958 100644 --- a/public/js/controllers/blocks.js +++ b/public/js/controllers/blocks.js @@ -20,6 +20,4 @@ angular.module('mystery.blocks').controller('BlocksController', ['$scope', '$rou }); }; - // for avoid warning. please remove when you use Blocks - $scope.blocks = Blocks; }]); diff --git a/public/js/controllers/index.js b/public/js/controllers/index.js index 6043526..3681101 100755 --- a/public/js/controllers/index.js +++ b/public/js/controllers/index.js @@ -1,6 +1,16 @@ 'use strict'; -angular.module('mystery.system').controller('IndexController', ['$scope', 'Global', 'Index', function ($scope, Global, Index) { +angular.module('mystery.system').controller('IndexController', ['$scope', 'Global', 'Index', function($scope, Global, Index) { $scope.global = Global; $scope.index = Index; }]); + +$(document).ready(function() { + var socket = io.connect('http://localhost'); + socket.on('tx', function(data) { + var tx = data; + console.log('Transaction received! '+tx.txid); + }); + +}); + diff --git a/public/js/services/address.js b/public/js/services/address.js new file mode 100644 index 0000000..820f537 --- /dev/null +++ b/public/js/services/address.js @@ -0,0 +1,8 @@ +'use strict'; + +angular.module('mystery.address').factory('Address', ['$resource', function($resource) { + return $resource('/api/addr/:addrStr', { + addrStr: '@addStr' + }); +}]); + diff --git a/public/views/address.html b/public/views/address.html index 45a0149..4237598 100644 --- a/public/views/address.html +++ b/public/views/address.html @@ -1,8 +1,8 @@ -
+
@@ -10,29 +10,30 @@ Address - {{address}} + {{address.addrStr}} - Hash160 - {{hash160}} + Total Received + {{address.totalReceivedSat / 100000000}} BTC - Total Output - 1 BTC + Total Sent + {{address.totalSentSat / 100000000}} BTC - Total Input - 0.2 BTC + Final Balance + {{address.balanceSat / 100000000}} BTC - Current balance - 10.2 BTC + No. Transactions + {{address.txApperances}} +
- +

@@ -49,10 +50,10 @@ - - {{transaction.hash}} - {{transaction.time | date:'medium'}} - {{transaction.amount}} BTC + + {{transaction}} + -- + -- diff --git a/public/views/transaction.html b/public/views/transaction.html index 85336c0..cadce85 100644 --- a/public/views/transaction.html +++ b/public/views/transaction.html @@ -52,7 +52,12 @@ - + + diff --git a/server.js b/server.js index de2a349..ba879b4 100644 --- a/server.js +++ b/server.js @@ -1,23 +1,25 @@ 'use strict'; -/** - * Module dependencies. - */ -var express = require('express'), - fs = require('fs'); - -/** - * Main application entry file. - */ - //Load configurations //Set the node enviornment variable if not set before process.env.NODE_ENV = process.env.NODE_ENV || 'development'; -//Initializing system variables -var config = require('./config/config'), +/** + * Module dependencies. + */ +var express = require('express'), + fs = require('fs'), + PeerSync = require('./lib/PeerSync').class(), mongoose = require('mongoose'); +/** + * Main application entry file. + */ + + +//Initializing system variables +var config = require('./config/config'); + //Bootstrap db connection var db = mongoose.connect(config.db); @@ -38,6 +40,14 @@ var walk = function(path) { }; walk(models_path); +// p2p_sync process +var ps = new PeerSync(); +ps.init({ + skip_db_connection: true +}); +ps.run(); + +// express app var app = express(); //express settings @@ -46,9 +56,14 @@ require('./config/express')(app, db); //Bootstrap routes require('./config/routes')(app); +// socket.io +var server = require('http').createServer(app); +var io = require('socket.io').listen(server); +require('./app/views/sockets/main.js')(app,io); + //Start the app by listening on var port = process.env.PORT || config.port; -app.listen(port); +server.listen(port); console.log('Express app started on port ' + port); //expose app diff --git a/test/model/addr.js b/test/model/addr.js new file mode 100644 index 0000000..2a59f61 --- /dev/null +++ b/test/model/addr.js @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + +var + assert = require('assert'), + fs = require('fs'), + config = require('../../config/config'), + Address = require('../../app/models/Address').class(); + mongoose= require('mongoose'), + addrValid = JSON.parse(fs.readFileSync('test/model/addr.json')); + +describe('Address update', function(){ + + before(function(done) { + mongoose.connect(config.db); + done(); + }); + + after(function(done) { + mongoose.connection.close(); + done(); + }); + + addrValid.forEach( function(v) { + if (v.disabled) { + console.log(v.addr + " => disabled in JSON"); + } + else { + it('should retrieve the correct info for:' + v.addr, function(done) { + this.timeout(5000); + + var a = new Address(v.addr); + + a.update(function(err) { + if (err) done(err); + + assert.equal(v.addr, a.addrStr); + if (v.balance) assert.equal(v.balance, a.balance); + if (v.totalReceived) assert.equal(v.totalReceived, a.totalReceived); + if (v.totalSent) assert.equal(v.totalSent, a.totalSent); + if (v.transactions) { + v.transactions.forEach( function(tx) { + assert(tx in a.inTransactions); + }); + } + done(); + }); + }); + } + }); + +}); + diff --git a/test/model/addr.json b/test/model/addr.json new file mode 100644 index 0000000..22f4255 --- /dev/null +++ b/test/model/addr.json @@ -0,0 +1,93 @@ +[ + { + "addr": "mjRmkmYzvZN3cA3aBKJgYJ65epn3WCG84H" + }, + { + "addr": "mp3Rzxx9s1A21SY3sjJ3CQoa2Xjph7e5eS", + "balance": 0, + "totalReceived": 50, + "totalSent": 50.0 + } + , + { + "addr": "mgqvRGJMwR9JU5VhJ3x9uX9MTkzTsmmDgQ", + "balance": 43.1 + }, + { +"disabled":1, + "addr": "mzW2hdZN2um7WBvTDerdahKqRgj3md9C29", + "balance": 910.39522682, + "totalReceived": 910.39522682, + "totalSent": 0 + } + , + + + + { +"disabled":1, + "addr": "mjRmkmYzvZN3cA3aBKJgYJ65epn3WCG84H", + "balance": 46413.0, + "totalReceived": 357130.17644359, + "totalSent": 310717.17644359 + }, + { +"disabled":1, + "addr": "mgKY35SXqxFpcKK3Dq9mW9919N7wYXvcFM", + "balance": 0.01979459, + "totalReceived": 0.01979459, + "totalSent": 0, + "transactions": [ "91800d80bb4c69b238c9bfd94eb5155ab821e6b25cae5c79903d12853bbb4ed5" ] + }, + { +"disabled":1, + "addr": "mmvP3mTe53qxHdPqXEvdu8WdC7GfQ2vmx5", + "balance": 10580.50027254, + "totalReceived": 12157.65075053, + "totalSent": 1577.15047799, + "transactions": [ + "91800d80bb4c69b238c9bfd94eb5155ab821e6b25cae5c79903d12853bbb4ed5", + "f6e80d4fd1a2377406856c67d0cee5ac7e5120993ff97e617ca9aac33b4c6b1e", + "bc27f31caae86750b126d9b09e969362b85b7c15f41421387d682064544bf7e7", + "2cd6a1cb26880276fbc9851396f1bd8081cb2b9107ff6921e8fd65ed2df3df79", + "8bea41f573bccb7b648bc0b1bbfeba8a96da05b1d819ff4a33d39fbcd334ecfd", + "cb0d55c37acc57f759255193673e13858b5ab3d8fdfa7ee8b25f9964bdaa11e3", + "7b007aeace2299d27b6bb6c24d0a8040d6a87e4c2601216c34d226462b75f915", + "a9f40fbaecd2b28a05405e28b95566d7b3bd8ac38a2853debd72517f2994c6fc", + "4123255b7678e37c168b9e929927760bc5d9363b0c78ec61a7b4a78b2a07adab", + "cb3760529c2684c32047a2fddf0e2534c9241e5d72011aac4a8982e0c7b46df3", + "e8d00d8cc744381233dbc95e2d657345084dfb6df785b81285183f4c89b678d4", + "7a748364255c5b64979d9d3da35ea0fbef0114e0d7f96fccd5bea76f6d19f06b", + "d0b7e087040f67ef9bd9f21ccf53d1b5410400351d949cabf127caf28a6e7add", + "209f97873265652b83922921148cad92d7e048c6822e4864e984753e04181470", + "3a4af7755d3061ecced2f3707c2623534104f08aa73a52ca243d7ddecf5fe86d", + "4a4b5c8d464a77814ed35a37e2a28e821d467a803761427c057f67823309b725", + "d85f5265618fb694c3ea3ca6f73eba93df8a644bc1c7286cec2fbc2fbf7d895e", + "0d2c778ed9976b52792c941cac126bda37d3b1453082022d5e36ac401be3b249", + "daf03d666047ca0b5340b4a0027f8562b7c5bac87dca3727093b5393176a541a", + "a0dc03a870e589ea51e3d3a8aed0d34f4f1ae6844acad26dae48fe523b26e764", + "3df1a50e2e5d8525f04bd21a66bad824364a975449fa24fd5c2537d0f713919b", + "7bc26c1f3b4ab5ca57677593d28d13bff468a658f4d5efc379c1612554cf668e", + "ded4cbc9c52fd5599b6a93f89a79cde9aeb5a7f8f56732bb67ae9554325b3666", + "91224a219196a3f6e6f40ad2137b13fe54109e57aaed7527ea34aa903e6b8313", + "ee899a182bbb75e98ef14d83489e631dd66a8c5059dc8255692dd8ca9efba01f", + "0a61590c7548bd4f6a0df1575b268057e5e3e295a44eaeeb1dfbd01332c585ed", + "d56c22950ad2924f404b5b0baa6e49b0df1aaf09d1947842aed9d0178958eb9d", + "c6b5368c5a256141894972fbd02377b3894aa0df7c35fab5e0eca90de064fdc1", + "158e1f9c3f8ec44e88052cadef74e8eb99fbad5697d0b005ba48c933f7d96816", + "7f6191c0f4e3040901ef0d5d6e76af4f16423061ca1347524c86205e35d904d9", + "2c2e20f976b98a0ca76c57eca3653699b60c1bd9503cc9cc2fb755164a679a26", + "59bc81733ff0eaf2b106a70a655e22d2cdeff80ada27b937693993bf0c22e9ea", + "7da38b66fb5e8582c8be85abecfd744a6de89e738dd5f3aaa0270b218ec424eb", + "393d51119cdfbf0a308c0bbde2d4c63546c0961022bad1503c4bbaed0638c837", + "4518868741817ae6757fd98de27693b51fad100e89e5206b9bbf798aeebb804c", + "c58bce14de1e3016504babd8bbe8175207d75074134a2548a71743fa3e56c58d", + "6e69ec4a97515a8fd424f123a5fc1fdfd3c3adcd741292cbc09c09a2cc433bea", + "0e15f2498362050e5ceb6157d0fbf820fdcaf936e447207d433ee7701d7b99c2", + "a3789e113041db907a1217ddb5c3aaf0eff905cc3d913e68d977e1ab4d19acea", + "80b460922faf0ad1e8b8a55533654c9a9f3039bfff0fff2bcf8536b8adf95939" + ] + } +] + + diff --git a/test/model/transaction.js b/test/model/transaction.js index 0b3e18d..d42c172 100644 --- a/test/model/transaction.js +++ b/test/model/transaction.js @@ -8,12 +8,16 @@ var mongoose= require('mongoose'), assert = require('assert'), config = require('../../config/config'), - Transaction = require('../../app/models/Transaction'); + Transaction = require('../../app/models/Transaction'), + TransactionItem = require('../../app/models/TransactionItem'), + fs = require('fs'), + util = require('util'); +var txItemsValid = JSON.parse(fs.readFileSync('test/model/txitems.json')); mongoose.connection.on('error', function(err) { console.log(err); }); -describe('Transaction fromIdWithInfo', function(){ +describe('Transaction', function(){ before(function(done) { mongoose.connect(config.db); @@ -24,22 +28,36 @@ describe('Transaction fromIdWithInfo', function(){ mongoose.connection.close(); done(); }); + it('should pool tx\'s object from mongoose', function(done) { + var txid = '7e621eeb02874ab039a8566fd36f4591e65eca65313875221842c53de6907d6c'; + Transaction.fromIdWithInfo(txid, function(err, tx) { + if (err) done(err); + assert.equal(tx.txid, txid); + assert(!tx.info.isCoinBase); + + for(var i=0; i<20; i++) + assert(parseFloat(tx.info.vin[i].value) === parseFloat(50)); + assert(tx.info.vin[0].addr === 'msGKGCy2i8wbKS5Fo1LbWUTJnf1GoFFG59'); + assert(tx.info.vin[1].addr === 'mfye7oHsdrHbydtj4coPXCasKad2eYSv5P'); + done(); + }); + }); it('should pool tx\'s object from mongoose', function(done) { - var test_txid = '21798ddc9664ac0ef618f52b151dda82dafaf2e26d2bbef6cdaf55a6957ca237'; - Transaction.fromIdWithInfo(test_txid, function(err, tx) { + var txid = '21798ddc9664ac0ef618f52b151dda82dafaf2e26d2bbef6cdaf55a6957ca237'; + Transaction.fromIdWithInfo(txid, function(err, tx) { if (err) done(err); - assert.equal(tx.txid, test_txid); + assert.equal(tx.txid, txid); assert(!tx.info.isCoinBase); done(); }); }); it('should pool tx\'s info from bitcoind', function(done) { - var test_txid = '21798ddc9664ac0ef618f52b151dda82dafaf2e26d2bbef6cdaf55a6957ca237'; - Transaction.fromIdWithInfo(test_txid, function(err, tx) { + var txid = '21798ddc9664ac0ef618f52b151dda82dafaf2e26d2bbef6cdaf55a6957ca237'; + Transaction.fromIdWithInfo(txid, function(err, tx) { if (err) done(err); - assert.equal(tx.info.txid, test_txid); + assert.equal(tx.info.txid, txid); assert.equal(tx.info.blockhash, '000000000185678d3d7ecc9962c96418174431f93fe20bf216d5565272423f74'); assert.equal(tx.info.valueOut, 1.66174); assert.equal(tx.info.feeds, 0.0005 ); @@ -49,28 +67,85 @@ describe('Transaction fromIdWithInfo', function(){ }); }); - it('test a coinbase TX 2a104bab1782e9b6445583296d4a0ecc8af304e4769ceb64b890e8219c562399', function(done) { - var test_txid2 = '2a104bab1782e9b6445583296d4a0ecc8af304e4769ceb64b890e8219c562399'; - Transaction.fromIdWithInfo(test_txid2, function(err, tx) { + var txid1 = '2a104bab1782e9b6445583296d4a0ecc8af304e4769ceb64b890e8219c562399'; + it('test a coinbase TX ' + txid1, function(done) { + Transaction.fromIdWithInfo(txid1, function(err, tx) { if (err) done(err); assert(tx.info.isCoinBase); - assert.equal(tx.info.txid, test_txid2); + assert.equal(tx.info.txid, txid1); assert(!tx.info.feeds); done(); }); }); + var txid22 = '666'; + it('test unexisting TX ' + txid22, function(done) { + Transaction.fromIdWithInfo(txid22, function(err, tx) { + if (err && err.toString().match(/TX.not.found/)) { + return done(); + } + else { + return done(err); + } + }); + }); + var txid2 = '64496d005faee77ac5a18866f50af6b8dd1f60107d6795df34c402747af98608'; + it('create TX on the fly ' + txid2, function(done) { + TransactionItem.remove({txid: txid2}, function(err) { + Transaction.fromIdWithInfo(txid2, function(err, tx) { + if (err) return done(err); + assert.equal(tx.info.txid, txid2); + done(); + }); + }); + }); - it('test a broken TX 64496d005faee77ac5a18866f50af6b8dd1f60107d6795df34c402747af98608', function(done) { - var test_txid2 = '64496d005faee77ac5a18866f50af6b8dd1f60107d6795df34c402747af98608'; - Transaction.fromIdWithInfo(test_txid2, function(err, tx) { - if (err) done(err); - assert.equal(tx.info.txid, test_txid2); - assert.equal(tx.info.vin[0].addr, null); + var txid2 = '64496d005faee77ac5a18866f50af6b8dd1f60107d6795df34c402747af98608'; + it('test a broken TX ' + txid2, function(done) { + Transaction.fromIdWithInfo(txid2, function(err, tx) { + if (err) return done(err); + assert.equal(tx.info.txid, txid2); + assert.equal(tx.info.vin[0].addr, 'n1JagbRWBDi6VMvG7HfZmXX74dB9eiHJzU'); done(); }); }); - - + + + txItemsValid.forEach( function(v) { + if (v.disabled) return; + it('test a exploding TX ' + v.txid, function(done) { + + // Remove first + TransactionItem.remove({txid: v.txid}, function(err) { + + Transaction.explodeTransactionItems(v.txid, function(err, tx) { + if (err) done(err); + + TransactionItem + .fromTxId( v.txid, function(err, readItems) { + + var unmatch={}; + + v.items.forEach(function(validItem){ + unmatch[validItem.addr] =1; + }); + v.items.forEach(function(validItem){ + var readItem = readItems.shift(); + assert.equal(readItem.addr,validItem.addr); + assert.equal(readItem.value_sat,validItem.value_sat); + assert.equal(readItem.index,validItem.index); + delete unmatch[validItem.addr]; + }); + + var valid = util.inspect(v.items, { depth: null }); + assert(!Object.keys(unmatch).length, + '\n\tUnmatchs:' + Object.keys(unmatch) + "\n\n" +valid + '\nvs.\n' + readItems); + done(); + }); + }); + }); + }); + }); + }); diff --git a/test/model/txitems.json b/test/model/txitems.json new file mode 100644 index 0000000..9e92a17 --- /dev/null +++ b/test/model/txitems.json @@ -0,0 +1,72 @@ +[ + { + "disabled": 1, + "txid": "75c5ffe6dc2eb0f6bd011a08c041ef115380ccd637d859b379506a0dca4c26fc" + }, + { + "txid": "21798ddc9664ac0ef618f52b151dda82dafaf2e26d2bbef6cdaf55a6957ca237", + "items": [ + { + "addr": "mwcFwXv2Yquy4vJA4nnNLAbHVjrPdC8Q1Z", + "value_sat": -166224000, + "index": 0 + }, + { + "addr": "mzjLe62faUqCSjkwQkwPAL5nYyR8K132fA", + "value_sat": 134574000, + "index": 0 + }, + { + "addr": "n28wb1cRGxPtfmsenYKFfsvnZ6kRapx3jF", + "value_sat": 31600000, + "index": 1 + } + ] + }, + { + "txid": "b633a6249d4a2bc123e7f8a151cae2d4afd17aa94840009f8697270c7818ceee", + "items": [ + { + "addr": "mzjLe62faUqCSjkwQkwPAL5nYyR8K132fA", + "value_sat": -40790667, + "index": 0 + }, + { + "addr": "mhfQJUSissP6nLM5pz6DxHfctukrrLct2T", + "value_sat": 19300000, + "index": 0 + }, + { + "addr": "mzcDhbL877ES3MGftWnc3EuTSXs3WXDDML", + "value_sat": 21440667, + "index": 1 + } + ] + }, + { + "txid": "ca2f42e44455b8a84434de139efea1fe2c7d71414a8939e0a20f518849085c3b", + "items": [ + { + "addr": "mzeiUi4opeheWYveXqp8ebqHyVwYGA2s3x", + "value_sat": -1225871, + "index": 0 + }, + { + "addr": "mtMLijHAbG8CsgBbQGajsqav9p9wKUYad5", + "value_sat": -1201823, + "index": 1 + }, + { + "addr": "mhqyL1nDQDo1WLH9qH8sjRjx2WwrnmAaXE", + "value_sat": 1327746, + "index": 0 + }, + { + "addr": "mkGrySSnxcqRbtPCisApj3zXCQVmUUWbf1", + "value_sat": 1049948, + "index": 1 + } + ] + } + +] diff --git a/util/find_ref.sh b/util/find_ref.sh new file mode 100755 index 0000000..2656eb2 --- /dev/null +++ b/util/find_ref.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +FIND='find'; + +##if [[ "$OSTYPE" =~ "darwin" ]] +##then +## FIND='gfind' +##fi + + +if [ -z "$1" ] +then + echo "$0 : find functions references " + echo "Usage $0 function_name " + exit; +fi + +EXTRA='' + + +CMD="grep -rnH" + +if [ "$2" != '--nocolor' ] +then + CMD="$CMD --color=always" +fi + + +$FIND -L . -name \*.json -not -wholename \*node_modules\* -not -wholename \*public/lib\* -exec $CMD "$1" {} + \ + -o -name \*.html -not -wholename \*node_modules\* -not -wholename \*public/lib\* -exec $CMD "$1" {} + \ + -o -name \*.jade -not -wholename \*node_modules\* -not -wholename \*public/lib\* -exec $CMD "$1" {} + \ + -o -name \*.js -not -wholename \*node_modules\* -not -wholename \*public/lib\* -exec $CMD "$1" {} + + diff --git a/util/p2p.js b/util/p2p.js new file mode 100755 index 0000000..91ead1b --- /dev/null +++ b/util/p2p.js @@ -0,0 +1,20 @@ +#! /usr/bin/env node +'use strict'; + +process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + +var PeerSync = require('../lib/PeerSync').class(); + +var PROGRAM_VERSION = '0.1'; +var program = require('commander'); + +program + .version(PROGRAM_VERSION) + .option('-N --network [testnet]', 'Set bitcoin network [testnet]', 'testnet') + .parse(process.argv); + +var ps = new PeerSync(); +ps.init(program); +ps.run(); + +