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/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/README.md b/README.md index a85ed9d..9843a23 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,6 @@ $ npm install -g bower http://localhost:3000 -## API - -A REST API is provided at /api. The entry points are: - ### Prerequisites Get bitcore from github repository: @@ -73,6 +69,12 @@ A REST API is provided at /api. The entry points are: Check utils/sync.js --help for options. + +## API + +A REST API is provided at /api. The entry points are: + + ### Blocks ``` /api/block/[:hash] @@ -104,7 +106,7 @@ There is a bitcoind configuration sample at: 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. 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/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 f668acf..dd7a70e 100755 --- a/config/env/test.js +++ b/config/env/test.js @@ -7,9 +7,9 @@ module.exports = { }, 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 || '18332', }, 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/Sync.js b/lib/Sync.js index 0fcdc8f..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,6 +163,46 @@ function spec() { }); }; + + // 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); @@ -183,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); @@ -190,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 new file mode 100755 index 0000000..aa0a08d --- /dev/null +++ b/p2p.js @@ -0,0 +1,131 @@ +#!/usr/bin/env node +<<<<<<< HEAD +======= + +>>>>>>> 71e1c718ac8f5eb89acedb4f91f2207ec463808b +'use strict'; + +process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + +var fs = require('fs'); +var HeaderDB = require('./HeaderDB').class(); +var Block = require('bitcore/Block').class(); +var CoinConst = require('bitcore/const'); +var coinUtil = require('bitcore/util/util'); +var networks = require('bitcore/networks'); +var Parser = require('bitcore/util/BinaryParser').class(); +var Sync = require('./lib/Sync').class(); +var Peer = require('bitcore/Peer').class(); + +var peerdb_fn = 'peerdb.json'; +var peerdb = undefined; + +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 sync = new Sync({ + networkName: program.network +}); +sync.init(); + +var PeerManager = require('bitcore/PeerManager').createClass({ + config: { + network: program.network + } +}); + +function peerdb_load() { + try { + peerdb = JSON.parse(fs.readFileSync(peerdb_fn)); + } catch(d) { + console.warn('Unable to read peer db', peerdb_fn, 'creating new one.'); + peerdb = [{ + ipv4: '127.0.0.1', + port: 18333 + }, + ]; + + fs.writeFileSync(peerdb_fn, JSON.stringify(peerdb)); + } +} + +function handle_inv(info) { + // TODO: should limit the invs to objects we haven't seen yet + var invs = info.message.invs; + invs.forEach(function(inv) { + console.log('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); +} + +function handle_tx(info) { + var tx = info.message.tx.getStandardizedObject(); + console.log('Handle tx: ' + tx.hash); + sync.storeTxs([tx.hash], function(err) { + if (err) { + console.log('Error in handle TX: ' + err); + } + }); +} + +function handle_block(info) { + var block = info.message.block; + var now = Math.round(new Date().getTime() / 1000); + var blockHash = coinUtil.formatHashFull(block.calcHash()); + console.log('Handle block: ' + blockHash); + sync.storeBlock({ + 'hash': blockHash, + 'time': now + }, + function(err) { + if (err) { + console.log('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); + }); + sync.storeTxs(hashes, function() {}); + } + }); + +} + +function handle_connected(data) { + var peerman = data.pm; + var peers_n = peerman.peers.length; + console.log('p2psync: Connected to ' + peers_n + ' peer' + (peers_n !== 1 ? 's': '')); +} + +function p2psync() { + var peerman = new PeerManager(); + + peerdb.forEach(function(datum) { + var peer = new Peer(datum.ipv4, datum.port); + peerman.addPeer(peer); + }); + + peerman.on('connection', function(conn) { + conn.on('inv', handle_inv); + conn.on('block', handle_block); + conn.on('tx', handle_tx); + }); + peerman.on('connect', handle_connected); + + peerman.start(); +} + +function main() { + peerdb_load(); + p2psync(); +} + +main(); + 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 + } + ] + } + +]