Merge pull request #207 from isocolsky/feat/email_notifications
Email notifications
This commit is contained in:
commit
ecbcf7f3e4
16
config.js
16
config.js
|
@ -18,10 +18,10 @@ var config = {
|
||||||
},
|
},
|
||||||
lockOpts: {
|
lockOpts: {
|
||||||
// To use locker-server, uncomment this:
|
// To use locker-server, uncomment this:
|
||||||
// lockerServer: {
|
lockerServer: {
|
||||||
// host: 'localhost',
|
host: 'localhost',
|
||||||
// port: 3231,
|
port: 3231,
|
||||||
// },
|
},
|
||||||
},
|
},
|
||||||
messageBrokerOpts: {
|
messageBrokerOpts: {
|
||||||
// To use message broker server, uncomment this:
|
// To use message broker server, uncomment this:
|
||||||
|
@ -39,5 +39,13 @@ var config = {
|
||||||
url: 'https://test-insight.bitpay.com:443',
|
url: 'https://test-insight.bitpay.com:443',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// To use email notifications uncomment this:
|
||||||
|
emailOpts: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 25,
|
||||||
|
ignoreTLS: true,
|
||||||
|
subjectPrefix: '[Wallet Service]',
|
||||||
|
from: 'wallet-service@bitcore.io',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|
|
@ -9,6 +9,8 @@ log.debug = log.verbose;
|
||||||
var BlockchainExplorer = require('./blockchainexplorer');
|
var BlockchainExplorer = require('./blockchainexplorer');
|
||||||
var Storage = require('./storage');
|
var Storage = require('./storage');
|
||||||
var MessageBroker = require('./messagebroker');
|
var MessageBroker = require('./messagebroker');
|
||||||
|
var Lock = require('./lock');
|
||||||
|
var EmailService = require('./emailservice');
|
||||||
|
|
||||||
var Notification = require('./model/notification');
|
var Notification = require('./model/notification');
|
||||||
|
|
||||||
|
@ -18,6 +20,8 @@ BlockchainMonitor.prototype.start = function(opts, cb) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
$.checkArgument(opts.blockchainExplorerOpts);
|
$.checkArgument(opts.blockchainExplorerOpts);
|
||||||
$.checkArgument(opts.storageOpts);
|
$.checkArgument(opts.storageOpts);
|
||||||
|
$.checkArgument(opts.lockOpts);
|
||||||
|
$.checkArgument(opts.emailOpts);
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
@ -38,8 +42,20 @@ BlockchainMonitor.prototype.start = function(opts, cb) {
|
||||||
self.messageBroker = new MessageBroker(opts.messageBrokerOpts);
|
self.messageBroker = new MessageBroker(opts.messageBrokerOpts);
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
], cb);
|
function(done) {
|
||||||
|
self.lock = new Lock(opts.lockOpts);
|
||||||
|
},
|
||||||
|
], function(err) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
self.emailService = new EmailService({
|
||||||
|
lock: self.lock,
|
||||||
|
storage: self.storage,
|
||||||
|
mailer: opts.mailer,
|
||||||
|
emailOpts: opts.emailOpts,
|
||||||
|
});
|
||||||
|
return cb();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockchainMonitor.prototype._initExplorer = function(provider, network, url) {
|
BlockchainMonitor.prototype._initExplorer = function(provider, network, url) {
|
||||||
|
@ -105,7 +121,7 @@ BlockchainMonitor.prototype._handleIncommingTx = function(data) {
|
||||||
BlockchainMonitor.prototype._createNotification = function(walletId, txid, address, amount, cb) {
|
BlockchainMonitor.prototype._createNotification = function(walletId, txid, address, amount, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var n = Notification.create({
|
var notification = Notification.create({
|
||||||
type: 'NewIncomingTx',
|
type: 'NewIncomingTx',
|
||||||
data: {
|
data: {
|
||||||
txid: txid,
|
txid: txid,
|
||||||
|
@ -114,9 +130,13 @@ BlockchainMonitor.prototype._createNotification = function(walletId, txid, addre
|
||||||
},
|
},
|
||||||
walletId: walletId,
|
walletId: walletId,
|
||||||
});
|
});
|
||||||
self.storage.storeNotification(walletId, n, function() {
|
self.storage.storeNotification(walletId, notification, function() {
|
||||||
self.messageBroker.send(n)
|
self.messageBroker.send(notification)
|
||||||
return cb();
|
if (self.emailService) {
|
||||||
|
self.emailService.sendEmail(notification, function() {
|
||||||
|
if (cb) return cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var _ = require('lodash');
|
||||||
|
var $ = require('preconditions').singleton();
|
||||||
|
var async = require('async');
|
||||||
|
var log = require('npmlog');
|
||||||
|
log.debug = log.verbose;
|
||||||
|
var fs = require('fs');
|
||||||
|
var nodemailer = require('nodemailer');
|
||||||
|
|
||||||
|
var Model = require('./model');
|
||||||
|
|
||||||
|
var EMAIL_TYPES = {
|
||||||
|
'NewCopayer': {
|
||||||
|
filename: 'new_copayer',
|
||||||
|
notifyDoer: false,
|
||||||
|
},
|
||||||
|
'NewTxProposal': {
|
||||||
|
filename: 'new_tx_proposal',
|
||||||
|
notifyDoer: false,
|
||||||
|
},
|
||||||
|
'NewOutgoingTx': {
|
||||||
|
filename: 'new_outgoing_tx',
|
||||||
|
notifyDoer: true,
|
||||||
|
},
|
||||||
|
'NewIncomingTx': {
|
||||||
|
filename: 'new_incoming_tx',
|
||||||
|
notifyDoer: true,
|
||||||
|
},
|
||||||
|
'TxProposalFinallyRejected': {
|
||||||
|
filename: 'txp_finally_rejected',
|
||||||
|
notifyDoer: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function EmailService(opts) {
|
||||||
|
$.checkArgument(opts);
|
||||||
|
|
||||||
|
opts.emailOpts = opts.emailOpts || {};
|
||||||
|
|
||||||
|
this.storage = opts.storage;
|
||||||
|
this.lock = opts.lock;
|
||||||
|
this.mailer = opts.mailer || nodemailer.createTransport(opts.emailOpts);
|
||||||
|
this.subjectPrefix = opts.emailOpts.subjectPrefix || '[Wallet service]';
|
||||||
|
this.from = opts.emailOpts.from;
|
||||||
|
|
||||||
|
$.checkState(this.mailer);
|
||||||
|
$.checkState(this.from);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: cache for X minutes
|
||||||
|
EmailService.prototype._readTemplate = function(filename, cb) {
|
||||||
|
fs.readFile(__dirname + '/templates/' + filename + '.plain', 'utf8', function(err, template) {
|
||||||
|
if (err) {
|
||||||
|
log.error('Could not read template file ' + filename, err);
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
var lines = template.split('\n');
|
||||||
|
return cb(null, {
|
||||||
|
subject: _.template(lines[0]),
|
||||||
|
body: _.template(_.rest(lines).join('\n')),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
EmailService.prototype._applyTemplate = function(template, data, cb) {
|
||||||
|
var result = _.mapValues(template, function(t) {
|
||||||
|
try {
|
||||||
|
return t(data);
|
||||||
|
} catch (e) {
|
||||||
|
log.error('Could not apply data to template', e);
|
||||||
|
return cb(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return cb(null, result);
|
||||||
|
};
|
||||||
|
|
||||||
|
EmailService.prototype._getRecipientsList = function(notification, emailType, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.storage.fetchPreferences(notification.walletId, null, function(err, preferences) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
if (_.isEmpty(preferences)) return cb(null, []);
|
||||||
|
|
||||||
|
var recipients = _.compact(_.map(preferences, function(p) {
|
||||||
|
if (!p.email) return;
|
||||||
|
if (notification.creatorId == p.copayerId && !emailType.notifyDoer) return;
|
||||||
|
return {
|
||||||
|
copayerId: p.copayerId,
|
||||||
|
emailAddress: p.email
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
return cb(null, recipients);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
EmailService.prototype._getDataForTemplate = function(notification, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var data = _.cloneDeep(notification.data);
|
||||||
|
data.subjectPrefix = _.trim(self.subjectPrefix) + ' ';
|
||||||
|
self.storage.fetchWallet(notification.walletId, function(err, wallet) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
data.walletId = wallet.id;
|
||||||
|
data.walletName = wallet.name;
|
||||||
|
data.walletM = wallet.m;
|
||||||
|
data.walletN = wallet.n;
|
||||||
|
var copayer = _.find(wallet.copayers, {
|
||||||
|
id: notification.creatorId
|
||||||
|
});
|
||||||
|
if (copayer) {
|
||||||
|
data.creatorId = copayer.id;
|
||||||
|
data.creatorName = copayer.name;
|
||||||
|
}
|
||||||
|
return cb(null, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
EmailService.prototype._send = function(email, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var mailOptions = {
|
||||||
|
from: email.from,
|
||||||
|
to: email.to,
|
||||||
|
subject: email.subject,
|
||||||
|
text: email.body,
|
||||||
|
};
|
||||||
|
self.mailer.sendMail(mailOptions, function(err, result) {
|
||||||
|
if (err) {
|
||||||
|
log.error('An error occurred when trying to send email to ' + email.to, err);
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
log.debug('Message sent: ', result || '');
|
||||||
|
return cb(err, result);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
EmailService.prototype.sendEmail = function(notification, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var emailType = EMAIL_TYPES[notification.type];
|
||||||
|
if (!emailType) return cb();
|
||||||
|
|
||||||
|
self._getRecipientsList(notification, emailType, function(err, recipientsList) {
|
||||||
|
if (_.isEmpty(recipientsList)) return cb();
|
||||||
|
|
||||||
|
async.waterfall([
|
||||||
|
|
||||||
|
function(next) {
|
||||||
|
async.parallel([
|
||||||
|
|
||||||
|
function(next) {
|
||||||
|
self._readTemplate(emailType.filename, next);
|
||||||
|
},
|
||||||
|
function(next) {
|
||||||
|
self._getDataForTemplate(notification, next);
|
||||||
|
},
|
||||||
|
], function(err, res) {
|
||||||
|
next(err, res[0], res[1]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(template, data, next) {
|
||||||
|
self._applyTemplate(template, data, next);
|
||||||
|
},
|
||||||
|
function(content, next) {
|
||||||
|
async.map(recipientsList, function(recipient, next) {
|
||||||
|
var email = Model.Email.create({
|
||||||
|
walletId: notification.walletId,
|
||||||
|
copayerId: recipient.copayerId,
|
||||||
|
from: self.from,
|
||||||
|
to: recipient.emailAddress,
|
||||||
|
subject: content.subject,
|
||||||
|
body: content.body,
|
||||||
|
});
|
||||||
|
self.storage.storeEmail(email, function(err) {
|
||||||
|
return next(err, email);
|
||||||
|
});
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
function(emails, next) {
|
||||||
|
async.each(emails, function(email, next) {
|
||||||
|
self._send(email, function(err) {
|
||||||
|
if (err) {
|
||||||
|
email.setFail();
|
||||||
|
} else {
|
||||||
|
email.setSent();
|
||||||
|
}
|
||||||
|
self.storage.storeEmail(email, next);
|
||||||
|
});
|
||||||
|
}, function(err) {
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
], cb);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = EmailService;
|
|
@ -178,6 +178,13 @@ ExpressApp.prototype.start = function(opts, cb) {
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
function(next) {
|
||||||
|
server.getPreferences({}, function(err, preferences) {
|
||||||
|
if (err) return next(err);
|
||||||
|
result.preferences = preferences;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
], function(err) {
|
], function(err) {
|
||||||
if (err) return returnError(err, res, req);
|
if (err) return returnError(err, res, req);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
|
|
|
@ -12,7 +12,7 @@ function Lock(opts) {
|
||||||
if (opts.lockerServer) {
|
if (opts.lockerServer) {
|
||||||
this.lock = new RemoteLock(opts.lockerServer.port, opts.lockerServer.host);
|
this.lock = new RemoteLock(opts.lockerServer.port, opts.lockerServer.host);
|
||||||
|
|
||||||
log.info('Using locker server:' + opts.lockerServer.host + ':' + opts.lockerServer.port);
|
log.info('Using locker server:' + opts.lockerServer.host + ':' + opts.lockerServer.port);
|
||||||
|
|
||||||
this.lock.on('reset', function() {
|
this.lock.on('reset', function() {
|
||||||
log.debug('Locker server reset');
|
log.debug('Locker server reset');
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var _ = require('lodash');
|
||||||
|
var Uuid = require('uuid');
|
||||||
|
|
||||||
|
function Email() {
|
||||||
|
this.version = '1.0.0';
|
||||||
|
};
|
||||||
|
|
||||||
|
Email.create = function(opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
|
||||||
|
var x = new Email();
|
||||||
|
|
||||||
|
var now = Date.now();
|
||||||
|
x.createdOn = Math.floor(now / 1000);
|
||||||
|
x.id = _.padLeft(now, 14, '0') + Uuid.v4();
|
||||||
|
x.walletId = opts.walletId;
|
||||||
|
x.copayerId = opts.copayerId;
|
||||||
|
x.from = opts.from;
|
||||||
|
x.to = opts.to;
|
||||||
|
x.subject = opts.subject;
|
||||||
|
x.body = opts.body;
|
||||||
|
x.status = 'pending';
|
||||||
|
x.attempts = 0;
|
||||||
|
x.lastAttemptOn = null;
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
Email.fromObj = function(obj) {
|
||||||
|
var x = new Email();
|
||||||
|
|
||||||
|
x.createdOn = obj.createdOn;
|
||||||
|
x.id = obj.id;
|
||||||
|
x.walletId = obj.walletId;
|
||||||
|
x.copayerId = obj.copayerId;
|
||||||
|
x.from = obj.from;
|
||||||
|
x.to = obj.to;
|
||||||
|
x.subject = obj.subject;
|
||||||
|
x.body = obj.body;
|
||||||
|
x.status = obj.status;
|
||||||
|
x.attempts = obj.attempts;
|
||||||
|
x.lastAttemptOn = obj.lastAttemptOn;
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
Email.prototype._logAttempt = function(result) {
|
||||||
|
this.attempts++;
|
||||||
|
this.lastAttemptOn = Math.floor(Date.now() / 1000);
|
||||||
|
this.status = result;
|
||||||
|
};
|
||||||
|
|
||||||
|
Email.prototype.setSent = function() {
|
||||||
|
this._logAttempt('sent');
|
||||||
|
};
|
||||||
|
|
||||||
|
Email.prototype.setFail = function() {
|
||||||
|
this._logAttempt('fail');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = Email;
|
|
@ -5,5 +5,7 @@ Model.Copayer = require('./copayer');
|
||||||
Model.TxProposal = require('./txproposal');
|
Model.TxProposal = require('./txproposal');
|
||||||
Model.Address = require('./address');
|
Model.Address = require('./address');
|
||||||
Model.Notification = require('./notification');
|
Model.Notification = require('./notification');
|
||||||
|
Model.Preferences = require('./preferences');
|
||||||
|
Model.Email = require('./email');
|
||||||
|
|
||||||
module.exports = Model;
|
module.exports = Model;
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function Preferences() {
|
||||||
|
this.version = '1.0.0';
|
||||||
|
};
|
||||||
|
|
||||||
|
Preferences.create = function(opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
|
||||||
|
var x = new Preferences();
|
||||||
|
|
||||||
|
x.createdOn = Math.floor(Date.now() / 1000);
|
||||||
|
x.walletId = opts.walletId;
|
||||||
|
x.copayerId = opts.copayerId;
|
||||||
|
x.email = opts.email;
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
Preferences.fromObj = function(obj) {
|
||||||
|
var x = new Preferences();
|
||||||
|
|
||||||
|
x.createdOn = obj.createdOn;
|
||||||
|
x.walletId = obj.walletId;
|
||||||
|
x.copayerId = obj.copayerId;
|
||||||
|
x.email = obj.email;
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = Preferences;
|
|
@ -0,0 +1,25 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var log = require('npmlog');
|
||||||
|
log.debug = log.verbose;
|
||||||
|
var inherits = require('inherits');
|
||||||
|
var events = require('events');
|
||||||
|
var nodeutil = require('util');
|
||||||
|
|
||||||
|
function NotificationBroadcaster() {};
|
||||||
|
|
||||||
|
nodeutil.inherits(NotificationBroadcaster, events.EventEmitter);
|
||||||
|
|
||||||
|
NotificationBroadcaster.prototype.broadcast = function(eventName, notification, walletService) {
|
||||||
|
this.emit(eventName, notification, walletService);
|
||||||
|
};
|
||||||
|
|
||||||
|
var _instance;
|
||||||
|
NotificationBroadcaster.singleton = function() {
|
||||||
|
if (!_instance) {
|
||||||
|
_instance = new NotificationBroadcaster();
|
||||||
|
}
|
||||||
|
return _instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = NotificationBroadcaster.singleton();
|
108
lib/server.js
108
lib/server.js
|
@ -5,6 +5,7 @@ var async = require('async');
|
||||||
var log = require('npmlog');
|
var log = require('npmlog');
|
||||||
log.debug = log.verbose;
|
log.debug = log.verbose;
|
||||||
log.disableColor();
|
log.disableColor();
|
||||||
|
var EmailValidator = require('email-validator');
|
||||||
|
|
||||||
var WalletUtils = require('bitcore-wallet-utils');
|
var WalletUtils = require('bitcore-wallet-utils');
|
||||||
var Bitcore = WalletUtils.Bitcore;
|
var Bitcore = WalletUtils.Bitcore;
|
||||||
|
@ -18,16 +19,19 @@ var Lock = require('./lock');
|
||||||
var Storage = require('./storage');
|
var Storage = require('./storage');
|
||||||
var MessageBroker = require('./messagebroker');
|
var MessageBroker = require('./messagebroker');
|
||||||
var BlockchainExplorer = require('./blockchainexplorer');
|
var BlockchainExplorer = require('./blockchainexplorer');
|
||||||
|
var EmailService = require('./emailservice');
|
||||||
|
|
||||||
var Wallet = require('./model/wallet');
|
var Model = require('./model');
|
||||||
var Copayer = require('./model/copayer');
|
var Wallet = Model.Wallet;
|
||||||
var Address = require('./model/address');
|
|
||||||
var TxProposal = require('./model/txproposal');
|
|
||||||
var Notification = require('./model/notification');
|
|
||||||
|
|
||||||
var initialized = false;
|
var initialized = false;
|
||||||
var lock, storage, blockchainExplorer, blockchainExplorerOpts;
|
|
||||||
|
var lock;
|
||||||
|
var storage;
|
||||||
|
var blockchainExplorer;
|
||||||
|
var blockchainExplorerOpts;
|
||||||
var messageBroker;
|
var messageBroker;
|
||||||
|
var emailService;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,6 +48,7 @@ function WalletService() {
|
||||||
this.blockchainExplorerOpts = blockchainExplorerOpts;
|
this.blockchainExplorerOpts = blockchainExplorerOpts;
|
||||||
this.messageBroker = messageBroker;
|
this.messageBroker = messageBroker;
|
||||||
this.notifyTicker = 0;
|
this.notifyTicker = 0;
|
||||||
|
this.emailService = emailService;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,20 +66,31 @@ WalletService.initialize = function(opts, cb) {
|
||||||
blockchainExplorer = opts.blockchainExplorer;
|
blockchainExplorer = opts.blockchainExplorer;
|
||||||
blockchainExplorerOpts = opts.blockchainExplorerOpts;
|
blockchainExplorerOpts = opts.blockchainExplorerOpts;
|
||||||
|
|
||||||
if (initialized)
|
if (initialized) return cb();
|
||||||
return cb();
|
|
||||||
|
|
||||||
function initStorage(cb) {
|
function initStorage(cb) {
|
||||||
if (opts.storage) {
|
if (opts.storage) {
|
||||||
storage = opts.storage;
|
storage = opts.storage;
|
||||||
return cb();
|
return cb();
|
||||||
|
} else {
|
||||||
|
var newStorage = new Storage();
|
||||||
|
newStorage.connect(opts.storageOpts, function(err) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
storage = newStorage;
|
||||||
|
return cb();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
var newStorage = new Storage();
|
};
|
||||||
newStorage.connect(opts.storageOpts, function(err) {
|
|
||||||
if (err) return cb(err);
|
function initEmailService(cb) {
|
||||||
storage = newStorage;
|
if (!opts.mailer && !opts.emailOpts) return cb();
|
||||||
return cb();
|
emailService = new EmailService({
|
||||||
|
lock: lock,
|
||||||
|
storage: storage,
|
||||||
|
mailer: opts.mailer,
|
||||||
|
emailOpts: opts.emailOpts,
|
||||||
});
|
});
|
||||||
|
return cb();
|
||||||
};
|
};
|
||||||
|
|
||||||
function initMessageBroker(cb) {
|
function initMessageBroker(cb) {
|
||||||
|
@ -94,6 +110,9 @@ WalletService.initialize = function(opts, cb) {
|
||||||
function(next) {
|
function(next) {
|
||||||
initMessageBroker(next);
|
initMessageBroker(next);
|
||||||
},
|
},
|
||||||
|
function(next) {
|
||||||
|
initEmailService(next);
|
||||||
|
},
|
||||||
], function(err) {
|
], function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Could not initialize', err);
|
log.error('Could not initialize', err);
|
||||||
|
@ -322,24 +341,32 @@ WalletService.prototype._notify = function(type, data, opts, cb) {
|
||||||
|
|
||||||
log.debug('Notification', type, data);
|
log.debug('Notification', type, data);
|
||||||
|
|
||||||
|
cb = cb || function() {};
|
||||||
|
|
||||||
var walletId = self.walletId || data.walletId;
|
var walletId = self.walletId || data.walletId;
|
||||||
var copayerId = self.copayerId || data.copayerId;
|
var copayerId = self.copayerId || data.copayerId;
|
||||||
|
|
||||||
$.checkState(walletId);
|
$.checkState(walletId);
|
||||||
|
|
||||||
var n = Notification.create({
|
var notification = Model.Notification.create({
|
||||||
type: type,
|
type: type,
|
||||||
data: data,
|
data: data,
|
||||||
ticker: this.notifyTicker++,
|
ticker: this.notifyTicker++,
|
||||||
creatorId: opts.isGlobal ? null : copayerId,
|
creatorId: opts.isGlobal ? null : copayerId,
|
||||||
walletId: walletId,
|
walletId: walletId,
|
||||||
});
|
});
|
||||||
this.storage.storeNotification(walletId, n, function() {
|
|
||||||
self.messageBroker.send(n);
|
this.storage.storeNotification(walletId, notification, function() {
|
||||||
if (cb) return cb();
|
self.messageBroker.send(notification);
|
||||||
|
if (self.emailService) {
|
||||||
|
self.emailService.sendEmail(notification, function() {
|
||||||
|
if (cb) return cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Joins a wallet in creation.
|
* Joins a wallet in creation.
|
||||||
* @param {Object} opts
|
* @param {Object} opts
|
||||||
|
@ -378,7 +405,7 @@ WalletService.prototype.joinWallet = function(opts, cb) {
|
||||||
if (wallet.copayers.length == wallet.n)
|
if (wallet.copayers.length == wallet.n)
|
||||||
return cb(new ClientError('WFULL', 'Wallet full'));
|
return cb(new ClientError('WFULL', 'Wallet full'));
|
||||||
|
|
||||||
var copayer = Copayer.create({
|
var copayer = Model.Copayer.create({
|
||||||
name: opts.name,
|
name: opts.name,
|
||||||
copayerIndex: wallet.copayers.length,
|
copayerIndex: wallet.copayers.length,
|
||||||
xPubKey: opts.xPubKey,
|
xPubKey: opts.xPubKey,
|
||||||
|
@ -412,6 +439,49 @@ WalletService.prototype.joinWallet = function(opts, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save copayer preferences for the current wallet/copayer pair.
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {string} opts.email - Email address for notifications.
|
||||||
|
*/
|
||||||
|
WalletService.prototype.savePreferences = function(opts, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
opts = opts || {};
|
||||||
|
|
||||||
|
if (opts.email) {
|
||||||
|
if (!EmailValidator.validate(opts.email)) {
|
||||||
|
return cb(new ClientError('Invalid email address'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self._runLocked(cb, function(cb) {
|
||||||
|
var preferences = Model.Preferences.create({
|
||||||
|
walletId: self.walletId,
|
||||||
|
copayerId: self.copayerId,
|
||||||
|
email: opts.email,
|
||||||
|
});
|
||||||
|
self.storage.storePreferences(preferences, function(err) {
|
||||||
|
return cb(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a preferences for the current wallet/copayer pair.
|
||||||
|
* @param {Object} opts
|
||||||
|
* @returns {Object} preferences
|
||||||
|
*/
|
||||||
|
WalletService.prototype.getPreferences = function(opts, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.storage.fetchPreferences(self.walletId, self.copayerId, function(err, preferences) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
return cb(null, preferences || {});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new address.
|
* Creates a new address.
|
||||||
* @param {Object} opts
|
* @param {Object} opts
|
||||||
|
@ -707,7 +777,7 @@ WalletService.prototype.createTx = function(opts, cb) {
|
||||||
|
|
||||||
var changeAddress = wallet.createAddress(true);
|
var changeAddress = wallet.createAddress(true);
|
||||||
|
|
||||||
var txp = TxProposal.create({
|
var txp = Model.TxProposal.create({
|
||||||
walletId: self.walletId,
|
walletId: self.walletId,
|
||||||
creatorId: self.copayerId,
|
creatorId: self.copayerId,
|
||||||
toAddress: opts.toAddress,
|
toAddress: opts.toAddress,
|
||||||
|
|
|
@ -18,6 +18,8 @@ var collections = {
|
||||||
ADDRESSES: 'addresses',
|
ADDRESSES: 'addresses',
|
||||||
NOTIFICATIONS: 'notifications',
|
NOTIFICATIONS: 'notifications',
|
||||||
COPAYERS_LOOKUP: 'copayers_lookup',
|
COPAYERS_LOOKUP: 'copayers_lookup',
|
||||||
|
PREFERENCES: 'preferences',
|
||||||
|
EMAIL_QUEUE: 'email_queue',
|
||||||
};
|
};
|
||||||
|
|
||||||
var Storage = function(opts) {
|
var Storage = function(opts) {
|
||||||
|
@ -353,6 +355,58 @@ Storage.prototype.fetchAddress = function(address, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Storage.prototype.fetchPreferences = function(walletId, copayerId, cb) {
|
||||||
|
this.db.collection(collections.PREFERENCES).find({
|
||||||
|
walletId: walletId,
|
||||||
|
}).toArray(function(err, result) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
if (copayerId) {
|
||||||
|
result = _.find(result, {
|
||||||
|
copayerId: copayerId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!result) return cb();
|
||||||
|
|
||||||
|
var preferences = _.map([].concat(result), function(r) {
|
||||||
|
return Model.Preferences.fromObj(r);
|
||||||
|
});
|
||||||
|
if (copayerId) {
|
||||||
|
preferences = preferences[0];
|
||||||
|
}
|
||||||
|
return cb(null, preferences);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Storage.prototype.storePreferences = function(preferences, cb) {
|
||||||
|
this.db.collection(collections.PREFERENCES).update({
|
||||||
|
walletId: preferences.walletId,
|
||||||
|
copayerId: preferences.copayerId,
|
||||||
|
}, preferences, {
|
||||||
|
w: 1,
|
||||||
|
upsert: true,
|
||||||
|
}, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
Storage.prototype.storeEmail = function(email, cb) {
|
||||||
|
this.db.collection(collections.EMAIL_QUEUE).update({
|
||||||
|
id: email.id,
|
||||||
|
}, email, {
|
||||||
|
w: 1,
|
||||||
|
upsert: true,
|
||||||
|
}, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
Storage.prototype.fetchUnsentEmails = function(cb) {
|
||||||
|
this.db.collection(collections.EMAIL_QUEUE).find({
|
||||||
|
status: 'pending',
|
||||||
|
}).toArray(function(err, result) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
if (!result || _.isEmpty(result)) return cb(null, []);
|
||||||
|
return cb(null, Model.Email.fromObj(result));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
Storage.prototype._dump = function(cb, fn) {
|
Storage.prototype._dump = function(cb, fn) {
|
||||||
fn = fn || console.log;
|
fn = fn || console.log;
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
<%= subjectPrefix %>New copayer
|
||||||
|
A new copayer just joined your wallet <%= walletName %>.
|
|
@ -0,0 +1,2 @@
|
||||||
|
<%= subjectPrefix %>New payment received
|
||||||
|
A Payment has been received into your wallet <%= walletName %>.
|
|
@ -0,0 +1,2 @@
|
||||||
|
<%= subjectPrefix %>Payment sent
|
||||||
|
A Payment has been sent from your wallet <%= walletName %>.
|
|
@ -0,0 +1,2 @@
|
||||||
|
<%= subjectPrefix %>New spend proposal
|
||||||
|
A new spend proposal has been created in your wallet <%= walletName %> by <%= creatorName %>.
|
|
@ -0,0 +1,2 @@
|
||||||
|
<%= subjectPrefix %>Spend proposal rejected
|
||||||
|
A spend proposal in your wallet <%= walletName %> has been rejected by <%= creatorName %>.
|
|
@ -24,6 +24,7 @@
|
||||||
"bitcore-wallet-utils": "0.0.12",
|
"bitcore-wallet-utils": "0.0.12",
|
||||||
"body-parser": "^1.11.0",
|
"body-parser": "^1.11.0",
|
||||||
"coveralls": "^2.11.2",
|
"coveralls": "^2.11.2",
|
||||||
|
"email-validator": "^1.0.1",
|
||||||
"express": "^4.10.0",
|
"express": "^4.10.0",
|
||||||
"inherits": "^2.0.1",
|
"inherits": "^2.0.1",
|
||||||
"locker": "^0.1.0",
|
"locker": "^0.1.0",
|
||||||
|
@ -32,6 +33,7 @@
|
||||||
"mocha-lcov-reporter": "0.0.1",
|
"mocha-lcov-reporter": "0.0.1",
|
||||||
"mongodb": "^2.0.27",
|
"mongodb": "^2.0.27",
|
||||||
"morgan": "*",
|
"morgan": "*",
|
||||||
|
"nodemailer": "^1.3.4",
|
||||||
"npmlog": "^0.1.1",
|
"npmlog": "^0.1.1",
|
||||||
"preconditions": "^1.0.7",
|
"preconditions": "^1.0.7",
|
||||||
"read": "^1.0.5",
|
"read": "^1.0.5",
|
||||||
|
|
|
@ -21,25 +21,21 @@ var Bitcore = WalletUtils.Bitcore;
|
||||||
var Storage = require('../../lib/storage');
|
var Storage = require('../../lib/storage');
|
||||||
|
|
||||||
var Model = require('../../lib/model');
|
var Model = require('../../lib/model');
|
||||||
var Wallet = Model.Wallet;
|
|
||||||
var TxProposal = Model.TxProposal;
|
|
||||||
var Address = Model.Address;
|
|
||||||
var Copayer = Model.Copayer;
|
|
||||||
|
|
||||||
var WalletService = require('../../lib/server');
|
var WalletService = require('../../lib/server');
|
||||||
var TestData = require('../testdata');
|
var TestData = require('../testdata');
|
||||||
|
|
||||||
var helpers = {};
|
var helpers = {};
|
||||||
helpers.getAuthServer = function(copayerId, cb) {
|
helpers.getAuthServer = function(copayerId, cb) {
|
||||||
var signatureStub = sinon.stub(WalletService.prototype, '_verifySignature');
|
var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature');
|
||||||
signatureStub.returns(true);
|
verifyStub.returns(true);
|
||||||
WalletService.getInstanceWithAuth({
|
WalletService.getInstanceWithAuth({
|
||||||
copayerId: copayerId,
|
copayerId: copayerId,
|
||||||
message: 'dummy',
|
message: 'dummy',
|
||||||
signature: 'dummy',
|
signature: 'dummy',
|
||||||
}, function(err, server) {
|
}, function(err, server) {
|
||||||
|
verifyStub.restore();
|
||||||
if (err || !server) throw new Error('Could not login as copayerId ' + copayerId);
|
if (err || !server) throw new Error('Could not login as copayerId ' + copayerId);
|
||||||
signatureStub.restore();
|
|
||||||
return cb(server);
|
return cb(server);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -136,7 +132,6 @@ helpers.toSatoshi = function(btc) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Amounts in satoshis
|
|
||||||
helpers.stubUtxos = function(server, wallet, amounts, cb) {
|
helpers.stubUtxos = function(server, wallet, amounts, cb) {
|
||||||
var amounts = [].concat(amounts);
|
var amounts = [].concat(amounts);
|
||||||
|
|
||||||
|
@ -215,16 +210,34 @@ helpers.createAddresses = function(server, wallet, main, change, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var db, storage, blockchainExplorer;
|
var storage, blockchainExplorer, mailer;
|
||||||
|
|
||||||
function openDb(cb) {
|
var useMongo = false;
|
||||||
db = new tingodb.Db('./db/test', {});
|
|
||||||
return cb();
|
function initStorage(cb) {
|
||||||
|
function getDb(cb) {
|
||||||
|
if (useMongo) {
|
||||||
|
var mongodb = require('mongodb');
|
||||||
|
mongodb.MongoClient.connect('mongodb://localhost:27017/bws_test', function(err, db) {
|
||||||
|
if (err) throw err;
|
||||||
|
return cb(db);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var db = new tingodb.Db('./db/test', {});
|
||||||
|
return cb(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getDb(function(db) {
|
||||||
|
storage = new Storage({
|
||||||
|
db: db
|
||||||
|
});
|
||||||
|
return cb();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetDb(cb) {
|
function resetStorage(cb) {
|
||||||
if (!db) return cb();
|
if (!storage.db) return cb();
|
||||||
db.dropDatabase(function(err) {
|
storage.db.dropDatabase(function(err) {
|
||||||
return cb();
|
return cb();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -232,19 +245,19 @@ function resetDb(cb) {
|
||||||
|
|
||||||
describe('Wallet service', function() {
|
describe('Wallet service', function() {
|
||||||
before(function(done) {
|
before(function(done) {
|
||||||
openDb(function() {
|
initStorage(done);
|
||||||
storage = new Storage({
|
|
||||||
db: db
|
|
||||||
});
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
resetDb(function() {
|
resetStorage(function() {
|
||||||
blockchainExplorer = sinon.stub();
|
blockchainExplorer = sinon.stub();
|
||||||
|
mailer = sinon.stub();
|
||||||
WalletService.initialize({
|
WalletService.initialize({
|
||||||
storage: storage,
|
storage: storage,
|
||||||
blockchainExplorer: blockchainExplorer,
|
blockchainExplorer: blockchainExplorer,
|
||||||
|
mailer: mailer,
|
||||||
|
emailOpts: {
|
||||||
|
from: 'bws@dummy.net',
|
||||||
|
}
|
||||||
}, done);
|
}, done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -673,7 +686,7 @@ describe('Wallet service', function() {
|
||||||
it('should create address', function(done) {
|
it('should create address', function(done) {
|
||||||
server.createAddress({}, function(err, address) {
|
server.createAddress({}, function(err, address) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
address.should.exist;
|
should.exist(address);
|
||||||
address.walletId.should.equal(wallet.id);
|
address.walletId.should.equal(wallet.id);
|
||||||
address.network.should.equal('livenet');
|
address.network.should.equal('livenet');
|
||||||
address.address.should.equal('3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg');
|
address.address.should.equal('3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg');
|
||||||
|
@ -718,7 +731,7 @@ describe('Wallet service', function() {
|
||||||
server.storage.storeAddressAndWallet.restore();
|
server.storage.storeAddressAndWallet.restore();
|
||||||
server.createAddress({}, function(err, address) {
|
server.createAddress({}, function(err, address) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
address.should.exist;
|
should.exist(address);
|
||||||
address.address.should.equal('3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg');
|
address.address.should.equal('3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg');
|
||||||
address.path.should.equal('m/2147483647/0/0');
|
address.path.should.equal('m/2147483647/0/0');
|
||||||
done();
|
done();
|
||||||
|
@ -728,6 +741,61 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Preferences', function() {
|
||||||
|
var server, wallet;
|
||||||
|
beforeEach(function(done) {
|
||||||
|
helpers.createAndJoinWallet(2, 2, function(s, w) {
|
||||||
|
server = s;
|
||||||
|
wallet = w;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save & retrieve preferences', function(done) {
|
||||||
|
server.savePreferences({
|
||||||
|
email: 'dummy@dummy.com'
|
||||||
|
}, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
server.getPreferences({}, function(err, preferences) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(preferences);
|
||||||
|
preferences.email.should.equal('dummy@dummy.com');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should save preferences only for requesting copayer', function(done) {
|
||||||
|
server.savePreferences({
|
||||||
|
email: 'dummy@dummy.com'
|
||||||
|
}, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
helpers.getAuthServer(wallet.copayers[1].id, function(server2) {
|
||||||
|
server2.getPreferences({}, function(err, preferences) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.not.exist(preferences.email);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it.skip('should save preferences only for requesting wallet', function(done) {});
|
||||||
|
it('should validate email address', function(done) {
|
||||||
|
server.savePreferences({
|
||||||
|
email: ' '
|
||||||
|
}, function(err) {
|
||||||
|
should.exist(err);
|
||||||
|
err.message.should.contain('email');
|
||||||
|
server.savePreferences({
|
||||||
|
email: 'dummy@' + _.repeat('domain', 50),
|
||||||
|
}, function(err) {
|
||||||
|
should.exist(err);
|
||||||
|
err.message.should.contain('email');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#getBalance', function() {
|
describe('#getBalance', function() {
|
||||||
var server, wallet;
|
var server, wallet;
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
|
@ -1514,7 +1582,7 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should brodcast a tx', function(done) {
|
it('should broadcast a tx', function(done) {
|
||||||
var clock = sinon.useFakeTimers(1234000);
|
var clock = sinon.useFakeTimers(1234000);
|
||||||
helpers.stubBroadcast('999');
|
helpers.stubBroadcast('999');
|
||||||
server.broadcastTx({
|
server.broadcastTx({
|
||||||
|
@ -3030,4 +3098,50 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('Email notifications', function() {
|
||||||
|
var server, wallet, sendMailStub;
|
||||||
|
beforeEach(function(done) {
|
||||||
|
helpers.createAndJoinWallet(2, 3, function(s, w) {
|
||||||
|
server = s;
|
||||||
|
wallet = w;
|
||||||
|
sendMailStub = sinon.stub();
|
||||||
|
sendMailStub.yields();
|
||||||
|
server.emailService.mailer.sendMail = sendMailStub;
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
async.eachSeries(w.copayers, function(copayer, next) {
|
||||||
|
helpers.getAuthServer(copayer.id, function(server) {
|
||||||
|
server.savePreferences({
|
||||||
|
email: 'copayer' + (i++) + '@domain.com',
|
||||||
|
}, next);
|
||||||
|
});
|
||||||
|
}, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should notify copayers a new tx proposal has been created', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
||||||
|
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, 'some message', TestData.copayers[0].privKey_1H_0);
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
var calls = sendMailStub.getCalls();
|
||||||
|
calls.length.should.equal(2);
|
||||||
|
var emails = _.map(calls, function(c) {
|
||||||
|
return c.args[0];
|
||||||
|
});
|
||||||
|
_.difference(['copayer1@domain.com', 'copayer2@domain.com'], _.pluck(emails, 'to')).should.be.empty;
|
||||||
|
var one = emails[0];
|
||||||
|
one.from.should.equal('bws@dummy.net');
|
||||||
|
one.subject.should.contain('New spend proposal');
|
||||||
|
one.text.should.contain(wallet.name);
|
||||||
|
one.text.should.contain(wallet.copayers[0].name);
|
||||||
|
server.storage.fetchUnsentEmails(function(err, unsent) {
|
||||||
|
should.not.exist(err);
|
||||||
|
unsent.should.be.empty;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,9 +16,16 @@ var db, storage;
|
||||||
|
|
||||||
function openDb(cb) {
|
function openDb(cb) {
|
||||||
db = new tingodb.Db('./db/test', {});
|
db = new tingodb.Db('./db/test', {});
|
||||||
|
// HACK: There appears to be a bug in TingoDB's close function where the callback is not being executed
|
||||||
|
db.__close = db.close;
|
||||||
|
db.close = function(force, cb) {
|
||||||
|
this.__close(force, cb);
|
||||||
|
return cb();
|
||||||
|
};
|
||||||
return cb();
|
return cb();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function resetDb(cb) {
|
function resetDb(cb) {
|
||||||
if (!db) return cb();
|
if (!db) return cb();
|
||||||
db.dropDatabase(function(err) {
|
db.dropDatabase(function(err) {
|
||||||
|
|
Loading…
Reference in New Issue