Merge pull request #29 from matiu/feature/01addr

Feature/01addr
This commit is contained in:
Gustavo Maximiliano Cortez 2014-01-13 13:15:12 -08:00
commit d589abb847
17 changed files with 932 additions and 81 deletions

11
.ctags Normal file
View File

@ -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

View File

@ -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,

View File

@ -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.

View File

@ -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);
}
};

View File

@ -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);
}
};
/**

127
app/models/Address.js Normal file
View File

@ -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);
*/

View File

@ -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;

View File

@ -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);

View File

@ -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',
},

6
config/env/test.js vendored
View File

@ -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',
},

View File

@ -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);
};

View File

@ -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);
});
});

131
p2p.js Executable file
View File

@ -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();

54
test/model/addr.js Normal file
View File

@ -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();
});
});
}
});
});

93
test/model/addr.json Normal file
View File

@ -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"
]
}
]

View File

@ -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();
});
});
});
});
});
});

72
test/model/txitems.json Normal file
View File

@ -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
}
]
}
]