From 5c6b5aeae235a88f27d13189a1012807b12a070f Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 29 May 2015 10:30:59 -0300 Subject: [PATCH 1/8] send email from a separate service --- emailservice/emailservice.js | 17 ++++++++++++ lib/blockchainmonitor.js | 19 +++---------- lib/emailservice.js | 52 +++++++++++++++++++++++++++++------- lib/server.js | 23 +--------------- test/integration/server.js | 47 -------------------------------- 5 files changed, 63 insertions(+), 95 deletions(-) create mode 100644 emailservice/emailservice.js diff --git a/emailservice/emailservice.js b/emailservice/emailservice.js new file mode 100644 index 0000000..fbb7e38 --- /dev/null +++ b/emailservice/emailservice.js @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +'use strict'; + +var _ = require('lodash'); +var log = require('npmlog'); +log.debug = log.verbose; + +var config = require('../config'); +var EmailService = require('../lib/emailservice'); + +var emailService = new EmailService(); +emailService.start(config, function(err) { + if (err) throw err; + + console.log('Email service started'); +}); diff --git a/lib/blockchainmonitor.js b/lib/blockchainmonitor.js index 8c4af51..812f386 100644 --- a/lib/blockchainmonitor.js +++ b/lib/blockchainmonitor.js @@ -10,7 +10,6 @@ var BlockchainExplorer = require('./blockchainexplorer'); var Storage = require('./storage'); var MessageBroker = require('./messagebroker'); var Lock = require('./lock'); -var EmailService = require('./emailservice'); var Notification = require('./model/notification'); @@ -20,8 +19,8 @@ BlockchainMonitor.prototype.start = function(opts, cb) { opts = opts || {}; $.checkArgument(opts.blockchainExplorerOpts); $.checkArgument(opts.storageOpts); + $.checkArgument(opts.messageBrokerOpts); $.checkArgument(opts.lockOpts); - $.checkArgument(opts.emailOpts); var self = this; @@ -49,16 +48,8 @@ BlockchainMonitor.prototype.start = function(opts, cb) { ], function(err) { if (err) { log.error(err); - return cb(err); } - - self.emailService = new EmailService({ - lock: self.lock, - storage: self.storage, - mailer: opts.mailer, - emailOpts: opts.emailOpts, - }); - return cb(); + return cb(err); }); }; @@ -136,11 +127,7 @@ BlockchainMonitor.prototype._createNotification = function(walletId, txid, addre }); self.storage.storeNotification(walletId, notification, function() { self.messageBroker.send(notification) - if (self.emailService) { - self.emailService.sendEmail(notification, cb); - } else { - return cb(); - } + return cb(); }); }; diff --git a/lib/emailservice.js b/lib/emailservice.js index e5f8d43..deda15e 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -8,6 +8,10 @@ log.debug = log.verbose; var fs = require('fs'); var nodemailer = require('nodemailer'); +var Storage = require('./storage'); +var MessageBroker = require('./messagebroker'); +var Lock = require('./lock'); + var Model = require('./model'); var EMAIL_TYPES = { @@ -38,21 +42,47 @@ var EMAIL_TYPES = { }; -function EmailService(opts) { - $.checkArgument(opts); +function EmailService() {}; - opts.emailOpts = opts.emailOpts || {}; +EmailService.prototype.start = function(opts, cb) { + opts = opts || {}; + $.checkArgument(opts.storageOpts); + $.checkArgument(opts.messageBrokerOpts); + $.checkArgument(opts.lockOpts); + $.checkArgument(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; + var self = this; - $.checkState(this.mailer); - $.checkState(this.from); + async.parallel([ + + function(done) { + self.storage = new Storage(); + self.storage.connect(opts.storageOpts, done); + }, + function(done) { + self.messageBroker = new MessageBroker(opts.messageBrokerOpts); + self.messageBroker.onMessage(_.bind(self.sendEmail, self)); + done(); + }, + function(done) { + self.lock = new Lock(opts.lockOpts); + done(); + }, + function(done) { + self.mailer = opts.mailer || nodemailer.createTransport(opts.emailOpts); + self.subjectPrefix = opts.emailOpts.subjectPrefix || '[Wallet service]'; + self.from = opts.emailOpts.from; + done(); + }, + ], function(err) { + if (err) { + log.error(err); + } + return cb(err); + }); }; + // TODO: cache for X minutes EmailService.prototype._readTemplate = function(filename, cb) { fs.readFile(__dirname + '/templates/' + filename + '.plain', 'utf8', function(err, template) { @@ -144,6 +174,8 @@ EmailService.prototype._send = function(email, cb) { EmailService.prototype.sendEmail = function(notification, cb) { var self = this; + cb = cb || function() {}; + var emailType = EMAIL_TYPES[notification.type]; if (!emailType) return cb(); diff --git a/lib/server.js b/lib/server.js index 1951b36..9f0c4f5 100644 --- a/lib/server.js +++ b/lib/server.js @@ -19,7 +19,6 @@ var Lock = require('./lock'); var Storage = require('./storage'); var MessageBroker = require('./messagebroker'); var BlockchainExplorer = require('./blockchainexplorer'); -var EmailService = require('./emailservice'); var Model = require('./model'); var Wallet = Model.Wallet; @@ -31,7 +30,6 @@ var storage; var blockchainExplorer; var blockchainExplorerOpts; var messageBroker; -var emailService; /** @@ -48,7 +46,6 @@ function WalletService() { this.blockchainExplorerOpts = blockchainExplorerOpts; this.messageBroker = messageBroker; this.notifyTicker = 0; - this.emailService = emailService; }; /** @@ -80,17 +77,6 @@ WalletService.initialize = function(opts, cb) { } }; - function initEmailService(cb) { - if (!opts.mailer && !opts.emailOpts) return cb(); - emailService = new EmailService({ - lock: lock, - storage: storage, - mailer: opts.mailer, - emailOpts: opts.emailOpts, - }); - return cb(); - }; - function initMessageBroker(cb) { if (opts.messageBroker) { messageBroker = opts.messageBroker; @@ -108,9 +94,6 @@ WalletService.initialize = function(opts, cb) { function(next) { initMessageBroker(next); }, - function(next) { - initEmailService(next); - }, ], function(err) { if (err) { log.error('Could not initialize', err); @@ -356,11 +339,7 @@ WalletService.prototype._notify = function(type, data, opts, cb) { this.storage.storeNotification(walletId, notification, function() { self.messageBroker.send(notification); - if (self.emailService) { - self.emailService.sendEmail(notification, cb); - } else { - return cb(); - } + return cb(); }); }; diff --git a/test/integration/server.js b/test/integration/server.js index 0675559..e02923c 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3229,51 +3229,4 @@ 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 payment 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(); - }); - }); - }); - }); - }); }); From 1ea9447223eb1dc9ee24c37850720a27d5cc0386 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 29 May 2015 12:30:42 -0300 Subject: [PATCH 2/8] remove unused opts on wallet service init --- test/integration/server.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/integration/server.js b/test/integration/server.js index e02923c..e4f68b4 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -210,7 +210,7 @@ helpers.createAddresses = function(server, wallet, main, change, cb) { }); }; -var storage, blockchainExplorer, mailer; +var storage, blockchainExplorer; var useMongo = false; @@ -250,14 +250,9 @@ describe('Wallet service', function() { beforeEach(function(done) { resetStorage(function() { blockchainExplorer = sinon.stub(); - mailer = sinon.stub(); WalletService.initialize({ storage: storage, blockchainExplorer: blockchainExplorer, - mailer: mailer, - emailOpts: { - from: 'bws@dummy.net', - } }, done); }); }); From 46b4652c8a883152b2a0f8b5911e41bbece690ab Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 29 May 2015 16:16:42 -0300 Subject: [PATCH 3/8] test email for tx creation --- lib/emailservice.js | 28 ++++++++++----- test/integration/server.js | 72 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/lib/emailservice.js b/lib/emailservice.js index deda15e..4c7753f 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -46,26 +46,31 @@ function EmailService() {}; EmailService.prototype.start = function(opts, cb) { opts = opts || {}; - $.checkArgument(opts.storageOpts); - $.checkArgument(opts.messageBrokerOpts); - $.checkArgument(opts.lockOpts); - $.checkArgument(opts.emailOpts); var self = this; async.parallel([ function(done) { - self.storage = new Storage(); - self.storage.connect(opts.storageOpts, done); + if (opts.storage) { + self.storage = opts.storage; + done(); + } else { + self.storage = new Storage(); + self.storage.connect(opts.storageOpts, done); + } }, function(done) { - self.messageBroker = new MessageBroker(opts.messageBrokerOpts); + if (opts.messageBroker) { + self.messageBroker = opts.messageBroker; + } else { + self.messageBroker = new MessageBroker(opts.messageBrokerOpts); + } self.messageBroker.onMessage(_.bind(self.sendEmail, self)); done(); }, function(done) { - self.lock = new Lock(opts.lockOpts); + self.lock = opts.lock || new Lock(opts.lockOpts); done(); }, function(done) { @@ -229,7 +234,12 @@ EmailService.prototype.sendEmail = function(notification, cb) { return next(); }); }, - ], cb); + ], function(err) { + if (err) { + log.error('An error ocurred generating email notification', err); + } + return cb(err); + }); }); }; diff --git a/test/integration/server.js b/test/integration/server.js index e4f68b4..5e1f014 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -23,6 +23,8 @@ var Storage = require('../../lib/storage'); var Model = require('../../lib/model'); var WalletService = require('../../lib/server'); +var EmailService = require('../../lib/emailservice'); + var TestData = require('../testdata'); var helpers = {}; @@ -260,6 +262,76 @@ describe('Wallet service', function() { WalletService.shutDown(done); }); + describe('Email notifications', function() { + var server, wallet, mailerStub, emailService; + + beforeEach(function(done) { + helpers.createAndJoinWallet(2, 3, function(s, w) { + server = s; + wallet = w; + + var i = 0; + async.eachSeries(w.copayers, function(copayer, next) { + helpers.getAuthServer(copayer.id, function(server) { + server.savePreferences({ + email: 'copayer' + (i++) + '@domain.com', + }, next); + }); + }, function(err) { + should.not.exist(err); + + mailerStub = sinon.stub(); + mailerStub.sendMail = sinon.stub(); + mailerStub.sendMail.yields(); + + emailService = new EmailService(); + emailService.start({ + lockOpts: {}, + messageBroker: server.messageBroker, + storage: storage, + mailer: mailerStub, + emailOpts: { + from: 'bws@dummy.net', + subjectPrefix: '[test wallet]', + }, + }, function(err) { + should.not.exist(err); + 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); + setTimeout(function() { + var calls = mailerStub.sendMail.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 payment 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(); + }); + }, 100); + }); + }); + }); + }); + + + describe('#getInstanceWithAuth', function() { it('should get server instance for existing copayer', function(done) { From 2f98041bfa58c3e8a11df7f47eb9f8d7066a9dca Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 29 May 2015 16:27:12 -0300 Subject: [PATCH 4/8] test incoming tx --- test/integration/server.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/integration/server.js b/test/integration/server.js index 5e1f014..a719d30 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -328,8 +328,38 @@ describe('Wallet service', function() { }); }); }); - }); + it('should notify copayers of incoming txs', function(done) { + server.createAddress({}, function(err, address) { + should.not.exist(err); + + // Simulate incoming tx notification + server._notify('NewIncomingTx', { + txid: '999', + address: address, + amount: 123, + }, function(err) { + setTimeout(function() { + var calls = mailerStub.sendMail.getCalls(); + calls.length.should.equal(3); + var emails = _.map(calls, function(c) { + return c.args[0]; + }); + _.difference(['copayer0@domain.com', '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 payment received'); + one.text.should.contain(wallet.name); + server.storage.fetchUnsentEmails(function(err, unsent) { + should.not.exist(err); + unsent.should.be.empty; + done(); + }); + }, 100); + }); + }); + }); + }); describe('#getInstanceWithAuth', function() { From 3316b643323c055c404f54c10dd43139fcc46dea Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 1 Jun 2015 11:23:37 -0300 Subject: [PATCH 5/8] bitcore-wallet-utils v0.0.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b17a8bd..d3abf13 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "async": "^0.9.0", "bitcore": "^0.11.6", "bitcore-explorers": "^0.10.3", - "bitcore-wallet-utils": "0.0.12", + "bitcore-wallet-utils": "0.0.13", "body-parser": "^1.11.0", "coveralls": "^2.11.2", "email-validator": "^1.0.1", From 26cb34e2b7f7e0718efb2d822a77e7c626049248 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 1 Jun 2015 11:28:31 -0300 Subject: [PATCH 6/8] include amount in incomingTx email --- lib/emailservice.js | 4 ++++ lib/templates/new_incoming_tx.plain | 2 +- test/integration/server.js | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/emailservice.js b/lib/emailservice.js index 4c7753f..ac4ef26 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -8,6 +8,7 @@ log.debug = log.verbose; var fs = require('fs'); var nodemailer = require('nodemailer'); +var WalletUtils = require('bitcore-wallet-utils'); var Storage = require('./storage'); var MessageBroker = require('./messagebroker'); var Lock = require('./lock'); @@ -140,6 +141,9 @@ EmailService.prototype._getDataForTemplate = function(notification, cb) { var data = _.cloneDeep(notification.data); data.subjectPrefix = _.trim(self.subjectPrefix) + ' '; + if (data.amount) { + data.amount = WalletUtils.formatAmount(+data.amount, 'bit') + ' bits'; + } self.storage.fetchWallet(notification.walletId, function(err, wallet) { if (err) return cb(err); data.walletId = wallet.id; diff --git a/lib/templates/new_incoming_tx.plain b/lib/templates/new_incoming_tx.plain index c980ef5..498b75b 100644 --- a/lib/templates/new_incoming_tx.plain +++ b/lib/templates/new_incoming_tx.plain @@ -1,2 +1,2 @@ <%= subjectPrefix %>New payment received -A Payment has been received into your wallet <%= walletName %>. +A payment of <%= amount %> has been received into your wallet <%= walletName %>. diff --git a/test/integration/server.js b/test/integration/server.js index a719d30..d1fafe9 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -329,7 +329,7 @@ describe('Wallet service', function() { }); }); - it('should notify copayers of incoming txs', function(done) { + it.only('should notify copayers of incoming txs', function(done) { server.createAddress({}, function(err, address) { should.not.exist(err); @@ -337,7 +337,7 @@ describe('Wallet service', function() { server._notify('NewIncomingTx', { txid: '999', address: address, - amount: 123, + amount: 12300000, }, function(err) { setTimeout(function() { var calls = mailerStub.sendMail.getCalls(); @@ -350,6 +350,7 @@ describe('Wallet service', function() { one.from.should.equal('bws@dummy.net'); one.subject.should.contain('New payment received'); one.text.should.contain(wallet.name); + one.text.should.contain('123,000'); server.storage.fetchUnsentEmails(function(err, unsent) { should.not.exist(err); unsent.should.be.empty; From 71d97f34f92503254667b630fd99422ec43571e9 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 1 Jun 2015 12:16:34 -0300 Subject: [PATCH 7/8] add amount to outgoingTx email --- lib/server.js | 3 +- lib/templates/new_outgoing_tx.plain | 2 +- test/integration/server.js | 55 ++++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/lib/server.js b/lib/server.js index 9f0c4f5..bfa38a2 100644 --- a/lib/server.js +++ b/lib/server.js @@ -991,7 +991,8 @@ WalletService.prototype.broadcastTx = function(opts, cb) { self._notify('NewOutgoingTx', { txProposalId: opts.txProposalId, - txid: txid + txid: txid, + amount: txp.amount, }, function() { return cb(null, txp); }); diff --git a/lib/templates/new_outgoing_tx.plain b/lib/templates/new_outgoing_tx.plain index a5cba78..38ece4f 100644 --- a/lib/templates/new_outgoing_tx.plain +++ b/lib/templates/new_outgoing_tx.plain @@ -1,2 +1,2 @@ <%= subjectPrefix %>Payment sent -A Payment has been sent from your wallet <%= walletName %>. +A Payment of <%= amount %> has been sent from your wallet <%= walletName %>. diff --git a/test/integration/server.js b/test/integration/server.js index d1fafe9..d8924cc 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -329,7 +329,60 @@ describe('Wallet service', function() { }); }); - it.only('should notify copayers of incoming txs', function(done) { + it('should notify copayers a new outgoing tx 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); + + var txpId; + async.waterfall([ + + function(next) { + server.createTx(txOpts, next); + }, + function(txp, next) { + txpId = txp.id; + async.eachSeries(_.range(2), function(i, next) { + var copayer = TestData.copayers[i]; + helpers.getAuthServer(copayer.id, function(server) { + var signatures = helpers.clientSign(txp, copayer.xPrivKey); + server.signTx({ + txProposalId: txp.id, + signatures: signatures, + }, next); + }); + }, next); + }, + function(next) { + helpers.stubBroadcast('999'); + server.broadcastTx({ + txProposalId: txpId, + }, next); + }, + ], function(err) { + should.not.exist(err); + + setTimeout(function() { + var calls = mailerStub.sendMail.getCalls(); + var emails = _.map(_.takeRight(calls, 3), function(c) { + return c.args[0]; + }); + _.difference(['copayer0@domain.com', '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('Payment sent'); + one.text.should.contain(wallet.name); + one.text.should.contain('800,000'); + server.storage.fetchUnsentEmails(function(err, unsent) { + should.not.exist(err); + unsent.should.be.empty; + done(); + }); + }, 100); + }); + }); + }); + + it('should notify copayers of incoming txs', function(done) { server.createAddress({}, function(err, address) { should.not.exist(err); From 6f76a359c239177d7f1664bbc0a7d088c86420e5 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 1 Jun 2015 12:16:56 -0300 Subject: [PATCH 8/8] v0.0.33 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d3abf13..7c09fc0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bitcore-wallet-service", "description": "A service for Mutisig HD Bitcoin Wallets", "author": "BitPay Inc", - "version": "0.0.32", + "version": "0.0.33", "keywords": [ "bitcoin", "copay",