Merge pull request #658 from isocolsky/feat/confirmation-notif
Notify of tx confirmations
This commit is contained in:
commit
954b88aceb
32
README.md
32
README.md
|
@ -101,6 +101,18 @@ Returns:
|
||||||
* availableConfirmedAmount: Same as availableAmount for confirmed UTXOs only.
|
* availableConfirmedAmount: Same as availableAmount for confirmed UTXOs only.
|
||||||
* byAddress array ['address', 'path', 'amount']: A list of addresses holding funds.
|
* byAddress array ['address', 'path', 'amount']: A list of addresses holding funds.
|
||||||
* totalKbToSendMax: An estimation of the number of KiB required to include all available UTXOs in a tx (including unconfirmed).
|
* totalKbToSendMax: An estimation of the number of KiB required to include all available UTXOs in a tx (including unconfirmed).
|
||||||
|
|
||||||
|
`/v1/txnotes/:txid`: Get user notes associated to the specified transaction.
|
||||||
|
Returns:
|
||||||
|
* The note associated to the `txid` as a string.
|
||||||
|
|
||||||
|
`/v1/fiatrates/:code`: Get the fiat rate for the specified ISO 4217 code.
|
||||||
|
Optional Arguments:
|
||||||
|
* provider: An identifier representing the source of the rates.
|
||||||
|
* ts: The timestamp for the fiat rate (defaults to now).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* The fiat exchange rate.
|
||||||
|
|
||||||
## POST Endpoints
|
## POST Endpoints
|
||||||
`/v1/wallets/`: Create a new Wallet
|
`/v1/wallets/`: Create a new Wallet
|
||||||
|
@ -172,20 +184,24 @@ Returns:
|
||||||
Optional Arguments:
|
Optional Arguments:
|
||||||
* includeCopayerBranches: Scan all copayer branches following BIP45 recommendation (defaults to false).
|
* includeCopayerBranches: Scan all copayer branches following BIP45 recommendation (defaults to false).
|
||||||
|
|
||||||
|
`/v1/txconfirmations/`: Subscribe to receive push notifications when the specified transaction gets confirmed.
|
||||||
|
Required Arguments:
|
||||||
|
* txid: The transaction to subscribe to.
|
||||||
|
|
||||||
|
## PUT Endpoints
|
||||||
|
`/v1/txnotes/:txid/`: Modify a note for a tx.
|
||||||
|
|
||||||
|
|
||||||
## DELETE Endpoints
|
## DELETE Endpoints
|
||||||
`/v1/txproposals/:id/`: Deletes a transaction proposal. Only the creator can delete a TX Proposal, and only if it has no other signatures or rejections
|
`/v1/txproposals/:id/`: Deletes a transaction proposal. Only the creator can delete a TX Proposal, and only if it has no other signatures or rejections
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
* TX Proposal object. (see [fields on the source code](https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js)). `.id` is probably needed in this case.
|
* TX Proposal object. (see [fields on the source code](https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js)). `.id` is probably needed in this case.
|
||||||
|
|
||||||
|
`/v1/txconfirmations/:txid`: Unsubscribe from transaction `txid` and no longer listen to its confirmation.
|
||||||
|
|
||||||
|
|
||||||
# Push Notifications
|
# Push Notifications
|
||||||
## Installation
|
|
||||||
|
|
||||||
In order to use push notifications service, you need install:
|
|
||||||
|
|
||||||
* [node-pushserver](https://www.npmjs.com/package/node-pushserver)
|
|
||||||
|
|
||||||
Recomended to complete config.js file:
|
Recomended to complete config.js file:
|
||||||
|
|
||||||
* [GCM documentation to get your API key](https://developers.google.com/cloud-messaging/gcm)
|
* [GCM documentation to get your API key](https://developers.google.com/cloud-messaging/gcm)
|
||||||
|
@ -197,7 +213,7 @@ Returns:
|
||||||
|
|
||||||
|
|
||||||
## DELETE Endopints
|
## DELETE Endopints
|
||||||
`/v1/pushnotifications/subscriptions/`: Remove subscriptions for push notifications service from database.
|
`/v2/pushnotifications/subscriptions/`: Remove subscriptions for push notifications service from database.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ Insight.prototype.getTransactions = function(addresses, from, to, cb) {
|
||||||
json: {
|
json: {
|
||||||
addrs: [].concat(addresses).join(',')
|
addrs: [].concat(addresses).join(',')
|
||||||
},
|
},
|
||||||
timeout: 120000,
|
timeout: 120000,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._doRequest(args, function(err, res, txs) {
|
this._doRequest(args, function(err, res, txs) {
|
||||||
|
@ -122,7 +122,7 @@ Insight.prototype.getTransactions = function(addresses, from, to, cb) {
|
||||||
if (txs.totalItems)
|
if (txs.totalItems)
|
||||||
total = txs.totalItems;
|
total = txs.totalItems;
|
||||||
|
|
||||||
if (txs.items)
|
if (txs.items)
|
||||||
txs = txs.items;
|
txs = txs.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,6 +183,21 @@ Insight.prototype.getBlockchainHeight = function(cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Insight.prototype.getTxidsInBlock = function(blockHash, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var args = {
|
||||||
|
method: 'GET',
|
||||||
|
path: this.apiPrefix + '/block/' + blockHash,
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._doRequest(args, function(err, res, body) {
|
||||||
|
if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
|
||||||
|
return cb(null, body.tx);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
Insight.prototype.initSocket = function() {
|
Insight.prototype.initSocket = function() {
|
||||||
|
|
||||||
// sockets always use the first server on the pull
|
// sockets always use the first server on the pull
|
||||||
|
|
|
@ -25,7 +25,8 @@ BlockchainMonitor.prototype.start = function(opts, cb) {
|
||||||
async.parallel([
|
async.parallel([
|
||||||
|
|
||||||
function(done) {
|
function(done) {
|
||||||
self.explorers = _.map(['livenet', 'testnet'], function(network) {
|
self.explorers = {};
|
||||||
|
_.map(['livenet', 'testnet'], function(network) {
|
||||||
var explorer;
|
var explorer;
|
||||||
if (opts.blockchainExplorers) {
|
if (opts.blockchainExplorers) {
|
||||||
explorer = opts.blockchainExplorers[network];
|
explorer = opts.blockchainExplorers[network];
|
||||||
|
@ -42,8 +43,8 @@ BlockchainMonitor.prototype.start = function(opts, cb) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
$.checkState(explorer);
|
$.checkState(explorer);
|
||||||
self._initExplorer(explorer);
|
self._initExplorer(network, explorer);
|
||||||
return explorer;
|
self.explorers[network] = explorer;
|
||||||
});
|
});
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
|
@ -72,7 +73,7 @@ BlockchainMonitor.prototype.start = function(opts, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockchainMonitor.prototype._initExplorer = function(explorer) {
|
BlockchainMonitor.prototype._initExplorer = function(network, explorer) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var socket = explorer.initSocket();
|
var socket = explorer.initSocket();
|
||||||
|
@ -85,10 +86,10 @@ BlockchainMonitor.prototype._initExplorer = function(explorer) {
|
||||||
log.error('Error connecting to ' + explorer.getConnectionInfo());
|
log.error('Error connecting to ' + explorer.getConnectionInfo());
|
||||||
});
|
});
|
||||||
socket.on('tx', _.bind(self._handleIncomingTx, self));
|
socket.on('tx', _.bind(self._handleIncomingTx, self));
|
||||||
socket.on('block', _.bind(self._handleNewBlock, self, explorer.network));
|
socket.on('block', _.bind(self._handleNewBlock, self, network));
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockchainMonitor.prototype._handleTxId = function(data, processIt) {
|
BlockchainMonitor.prototype._handleThirdPartyBroadcasts = function(data, processIt) {
|
||||||
var self = this;
|
var self = this;
|
||||||
if (!data || !data.txid) return;
|
if (!data || !data.txid) return;
|
||||||
|
|
||||||
|
@ -103,7 +104,7 @@ BlockchainMonitor.prototype._handleTxId = function(data, processIt) {
|
||||||
|
|
||||||
if (!processIt) {
|
if (!processIt) {
|
||||||
log.info('Detected broadcast ' + data.txid + ' of an accepted txp [' + txp.id + '] for wallet ' + walletId + ' [' + txp.amount + 'sat ]');
|
log.info('Detected broadcast ' + data.txid + ' of an accepted txp [' + txp.id + '] for wallet ' + walletId + ' [' + txp.amount + 'sat ]');
|
||||||
return setTimeout(self._handleTxId.bind(self, data, true), 20 * 1000);
|
return setTimeout(self._handleThirdPartyBroadcasts.bind(self, data, true), 20 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('Processing accepted txp [' + txp.id + '] for wallet ' + walletId + ' [' + txp.amount + 'sat ]');
|
log.info('Processing accepted txp [' + txp.id + '] for wallet ' + walletId + ' [' + txp.amount + 'sat ]');
|
||||||
|
@ -132,9 +133,7 @@ BlockchainMonitor.prototype._handleTxId = function(data, processIt) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BlockchainMonitor.prototype._handleIncomingPayments = function(data) {
|
||||||
|
|
||||||
BlockchainMonitor.prototype._handleTxOuts = function(data) {
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (!data || !data.vout) return;
|
if (!data || !data.vout) return;
|
||||||
|
@ -204,14 +203,14 @@ BlockchainMonitor.prototype._updateActiveAddresses = function(address, cb) {
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockchainMonitor.prototype._handleIncomingTx = function(data) {
|
BlockchainMonitor.prototype._handleIncomingTx = function(data) {
|
||||||
this._handleTxId(data);
|
this._handleThirdPartyBroadcasts(data);
|
||||||
this._handleTxOuts(data);
|
this._handleIncomingPayments(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockchainMonitor.prototype._handleNewBlock = function(network, hash) {
|
BlockchainMonitor.prototype._notifyNewBlock = function(network, hash) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
log.info('New ' + network + ' block: ', hash);
|
log.info('New ' + network + ' block: ' + hash);
|
||||||
var notification = Notification.create({
|
var notification = Notification.create({
|
||||||
type: 'NewBlock',
|
type: 'NewBlock',
|
||||||
walletId: network, // use network name as wallet id for global notifications
|
walletId: network, // use network name as wallet id for global notifications
|
||||||
|
@ -228,6 +227,63 @@ BlockchainMonitor.prototype._handleNewBlock = function(network, hash) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BlockchainMonitor.prototype._handleTxConfirmations = function(network, hash) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
function processTriggeredSubs(subs, cb) {
|
||||||
|
async.each(subs, function(sub) {
|
||||||
|
log.info('New tx confirmation ' + sub.txid);
|
||||||
|
sub.isActive = false;
|
||||||
|
self.storage.storeTxConfirmationSub(sub, function(err) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
var notification = Notification.create({
|
||||||
|
type: 'TxConfirmation',
|
||||||
|
walletId: sub.walletId,
|
||||||
|
creatorId: sub.copayerId,
|
||||||
|
data: {
|
||||||
|
txid: sub.txid,
|
||||||
|
network: network,
|
||||||
|
// TODO: amount
|
||||||
|
},
|
||||||
|
});
|
||||||
|
self._storeAndBroadcastNotification(notification, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var explorer = self.explorers[network];
|
||||||
|
if (!explorer) return;
|
||||||
|
|
||||||
|
explorer.getTxidsInBlock(hash, function(err, txids) {
|
||||||
|
if (err) {
|
||||||
|
log.error('Could not fetch txids from block ' + hash, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.storage.fetchActiveTxConfirmationSubs(null, function(err, subs) {
|
||||||
|
if (err) return;
|
||||||
|
if (_.isEmpty(subs)) return;
|
||||||
|
var indexedSubs = _.indexBy(subs, 'txid');
|
||||||
|
var triggered = [];
|
||||||
|
_.each(txids, function(txid) {
|
||||||
|
if (indexedSubs[txid]) triggered.push(indexedSubs[txid]);
|
||||||
|
});
|
||||||
|
processTriggeredSubs(triggered, function(err) {
|
||||||
|
if (err) {
|
||||||
|
log.error('Could not process tx confirmations', err);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockchainMonitor.prototype._handleNewBlock = function(network, hash) {
|
||||||
|
this._notifyNewBlock(network, hash);
|
||||||
|
this._handleTxConfirmations(network, hash);
|
||||||
|
};
|
||||||
|
|
||||||
BlockchainMonitor.prototype._storeAndBroadcastNotification = function(notification, cb) {
|
BlockchainMonitor.prototype._storeAndBroadcastNotification = function(notification, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
|
|
@ -21,26 +21,37 @@ var EMAIL_TYPES = {
|
||||||
'NewCopayer': {
|
'NewCopayer': {
|
||||||
filename: 'new_copayer',
|
filename: 'new_copayer',
|
||||||
notifyDoer: false,
|
notifyDoer: false,
|
||||||
|
notifyOthers: true,
|
||||||
},
|
},
|
||||||
'WalletComplete': {
|
'WalletComplete': {
|
||||||
filename: 'wallet_complete',
|
filename: 'wallet_complete',
|
||||||
notifyDoer: true,
|
notifyDoer: true,
|
||||||
|
notifyOthers: true,
|
||||||
},
|
},
|
||||||
'NewTxProposal': {
|
'NewTxProposal': {
|
||||||
filename: 'new_tx_proposal',
|
filename: 'new_tx_proposal',
|
||||||
notifyDoer: false,
|
notifyDoer: false,
|
||||||
|
notifyOthers: true,
|
||||||
},
|
},
|
||||||
'NewOutgoingTx': {
|
'NewOutgoingTx': {
|
||||||
filename: 'new_outgoing_tx',
|
filename: 'new_outgoing_tx',
|
||||||
notifyDoer: true,
|
notifyDoer: true,
|
||||||
|
notifyOthers: true,
|
||||||
},
|
},
|
||||||
'NewIncomingTx': {
|
'NewIncomingTx': {
|
||||||
filename: 'new_incoming_tx',
|
filename: 'new_incoming_tx',
|
||||||
notifyDoer: true,
|
notifyDoer: true,
|
||||||
|
notifyOthers: true,
|
||||||
},
|
},
|
||||||
'TxProposalFinallyRejected': {
|
'TxProposalFinallyRejected': {
|
||||||
filename: 'txp_finally_rejected',
|
filename: 'txp_finally_rejected',
|
||||||
notifyDoer: false,
|
notifyDoer: false,
|
||||||
|
notifyOthers: true,
|
||||||
|
},
|
||||||
|
'TxConfirmation': {
|
||||||
|
filename: 'tx_confirmation',
|
||||||
|
notifyDoer: true,
|
||||||
|
notifyOthers: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -172,6 +183,7 @@ EmailService.prototype._getRecipientsList = function(notification, emailType, cb
|
||||||
|
|
||||||
usedEmails[p.email] = true;
|
usedEmails[p.email] = true;
|
||||||
if (notification.creatorId == p.copayerId && !emailType.notifyDoer) return;
|
if (notification.creatorId == p.copayerId && !emailType.notifyDoer) return;
|
||||||
|
if (notification.creatorId != p.copayerId && !emailType.notifyOthers) return;
|
||||||
if (!_.contains(self.availableLanguages, p.language)) {
|
if (!_.contains(self.availableLanguages, p.language)) {
|
||||||
if (p.language) {
|
if (p.language) {
|
||||||
log.warn('Language for email "' + p.language + '" not available.');
|
log.warn('Language for email "' + p.language + '" not available.');
|
||||||
|
|
|
@ -57,7 +57,7 @@ ExpressApp.prototype.start = function(opts, cb) {
|
||||||
|
|
||||||
|
|
||||||
// handle `abort` https://nodejs.org/api/http.html#http_event_abort
|
// handle `abort` https://nodejs.org/api/http.html#http_event_abort
|
||||||
this.app.use(function (req, res, next) {
|
this.app.use(function(req, res, next) {
|
||||||
req.on('abort', function() {
|
req.on('abort', function() {
|
||||||
log.warn('Request aborted by the client');
|
log.warn('Request aborted by the client');
|
||||||
});
|
});
|
||||||
|
@ -711,6 +711,28 @@ ExpressApp.prototype.start = function(opts, cb) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/v1/txconfirmations/', function(req, res) {
|
||||||
|
getServerWithAuth(req, res, function(server) {
|
||||||
|
server.txConfirmationSubscribe(req.body, function(err, response) {
|
||||||
|
if (err) return returnError(err, res, req);
|
||||||
|
res.json(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/v1/txconfirmations/:txid', function(req, res) {
|
||||||
|
var opts = {
|
||||||
|
txid: req.params['txid'],
|
||||||
|
};
|
||||||
|
getServerWithAuth(req, res, function(server) {
|
||||||
|
server.txConfirmationUnsubscribe(opts, function(err, response) {
|
||||||
|
if (err) return returnError(err, res, req);
|
||||||
|
res.json(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.app.use(opts.basePath || '/bws/api', router);
|
this.app.use(opts.basePath || '/bws/api', router);
|
||||||
|
|
||||||
WalletService.initialize(opts, cb);
|
WalletService.initialize(opts, cb);
|
||||||
|
|
|
@ -10,5 +10,6 @@ Model.Email = require('./email');
|
||||||
Model.TxNote = require('./txnote');
|
Model.TxNote = require('./txnote');
|
||||||
Model.Session = require('./session');
|
Model.Session = require('./session');
|
||||||
Model.PushNotificationSub = require('./pushnotificationsub');
|
Model.PushNotificationSub = require('./pushnotificationsub');
|
||||||
|
Model.TxConfirmationSub = require('./txconfirmationsub');
|
||||||
|
|
||||||
module.exports = Model;
|
module.exports = Model;
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function TxConfirmationSub() {};
|
||||||
|
|
||||||
|
TxConfirmationSub.create = function(opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
|
||||||
|
var x = new TxConfirmationSub();
|
||||||
|
|
||||||
|
x.version = 1;
|
||||||
|
x.createdOn = Math.floor(Date.now() / 1000);
|
||||||
|
x.walletId = opts.walletId;
|
||||||
|
x.copayerId = opts.copayerId;
|
||||||
|
x.txid = opts.txid;
|
||||||
|
x.isActive = true;
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
TxConfirmationSub.fromObj = function(obj) {
|
||||||
|
var x = new TxConfirmationSub();
|
||||||
|
|
||||||
|
x.version = obj.version;
|
||||||
|
x.createdOn = obj.createdOn;
|
||||||
|
x.walletId = obj.walletId;
|
||||||
|
x.copayerId = obj.copayerId;
|
||||||
|
x.txid = obj.txid;
|
||||||
|
x.isActive = obj.isActive;
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = TxConfirmationSub;
|
|
@ -33,6 +33,10 @@ var PUSHNOTIFICATIONS_TYPES = {
|
||||||
'TxProposalFinallyRejected': {
|
'TxProposalFinallyRejected': {
|
||||||
filename: 'txp_finally_rejected',
|
filename: 'txp_finally_rejected',
|
||||||
},
|
},
|
||||||
|
'TxConfirmation': {
|
||||||
|
filename: 'tx_confirmation',
|
||||||
|
notifyCreatorOnly: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function PushNotificationsService() {};
|
function PushNotificationsService() {};
|
||||||
|
@ -111,7 +115,7 @@ PushNotificationsService.prototype._sendPushNotifications = function(notificatio
|
||||||
log.debug('Should send notification: ', should);
|
log.debug('Should send notification: ', should);
|
||||||
if (!should) return cb();
|
if (!should) return cb();
|
||||||
|
|
||||||
self._getRecipientsList(notification, function(err, recipientsList) {
|
self._getRecipientsList(notification, notifType, function(err, recipientsList) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
|
@ -188,7 +192,7 @@ PushNotificationsService.prototype._checkShouldSendNotif = function(notification
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
PushNotificationsService.prototype._getRecipientsList = function(notification, cb) {
|
PushNotificationsService.prototype._getRecipientsList = function(notification, notificationType, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.storage.fetchWallet(notification.walletId, function(err, wallet) {
|
self.storage.fetchWallet(notification.walletId, function(err, wallet) {
|
||||||
|
@ -216,16 +220,17 @@ PushNotificationsService.prototype._getRecipientsList = function(notification, c
|
||||||
|
|
||||||
recipientPreferences = _.indexBy(recipientPreferences, 'copayerId');
|
recipientPreferences = _.indexBy(recipientPreferences, 'copayerId');
|
||||||
|
|
||||||
var recipientsList = _.reject(_.map(wallet.copayers, function(copayer) {
|
var recipientsList = _.compact(_.map(wallet.copayers, function(copayer) {
|
||||||
var p = recipientPreferences[copayer.id] || {};
|
if ((copayer.id == notification.creatorId && notificationType.notifyCreatorOnly) ||
|
||||||
return {
|
(copayer.id != notification.creatorId && !notificationType.notifyCreatorOnly)) {
|
||||||
copayerId: copayer.id,
|
var p = recipientPreferences[copayer.id] || {};
|
||||||
language: p.language || self.defaultLanguage,
|
return {
|
||||||
unit: p.unit || self.defaultUnit,
|
copayerId: copayer.id,
|
||||||
|
language: p.language || self.defaultLanguage,
|
||||||
|
unit: p.unit || self.defaultUnit,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}), {
|
}));
|
||||||
copayerId: notification.creatorId
|
|
||||||
});
|
|
||||||
|
|
||||||
return cb(null, recipientsList);
|
return cb(null, recipientsList);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3136,5 +3136,37 @@ WalletService.prototype.pushNotificationsUnsubscribe = function(opts, cb) {
|
||||||
self.storage.removePushNotificationSub(self.copayerId, opts.token, cb);
|
self.storage.removePushNotificationSub(self.copayerId, opts.token, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe this copayer to the specified tx to get a notification when the tx confirms.
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {string} opts.txid - The txid of the tx to be notified of.
|
||||||
|
*/
|
||||||
|
WalletService.prototype.txConfirmationSubscribe = function(opts, cb) {
|
||||||
|
if (!checkRequired(opts, ['txid'], cb)) return;
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var sub = Model.TxConfirmationSub.create({
|
||||||
|
copayerId: self.copayerId,
|
||||||
|
walletId: self.walletId,
|
||||||
|
txid: opts.txid,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.storage.storeTxConfirmationSub(sub, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe this copayer to the Push Notifications service using the specified token.
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {string} opts.txid - The txid of the tx to be notified of.
|
||||||
|
*/
|
||||||
|
WalletService.prototype.txConfirmationUnsubscribe = function(opts, cb) {
|
||||||
|
if (!checkRequired(opts, ['txid'], cb)) return;
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.storage.removeTxConfirmationSub(self.copayerId, opts.txid, cb);
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = WalletService;
|
module.exports = WalletService;
|
||||||
module.exports.ClientError = ClientError;
|
module.exports.ClientError = ClientError;
|
||||||
|
|
|
@ -25,6 +25,7 @@ var collections = {
|
||||||
TX_NOTES: 'tx_notes',
|
TX_NOTES: 'tx_notes',
|
||||||
SESSIONS: 'sessions',
|
SESSIONS: 'sessions',
|
||||||
PUSH_NOTIFICATION_SUBS: 'push_notification_subs',
|
PUSH_NOTIFICATION_SUBS: 'push_notification_subs',
|
||||||
|
TX_CONFIRMATION_SUBS: 'tx_confirmation_subs',
|
||||||
};
|
};
|
||||||
|
|
||||||
var Storage = function(opts) {
|
var Storage = function(opts) {
|
||||||
|
@ -78,6 +79,10 @@ Storage.prototype._createIndexes = function() {
|
||||||
this.db.collection(collections.PUSH_NOTIFICATION_SUBS).createIndex({
|
this.db.collection(collections.PUSH_NOTIFICATION_SUBS).createIndex({
|
||||||
copayerId: 1,
|
copayerId: 1,
|
||||||
});
|
});
|
||||||
|
this.db.collection(collections.TX_CONFIRMATION_SUBS).createIndex({
|
||||||
|
copayerId: 1,
|
||||||
|
txid: 1,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Storage.prototype.connect = function(opts, cb) {
|
Storage.prototype.connect = function(opts, cb) {
|
||||||
|
@ -935,6 +940,45 @@ Storage.prototype.removePushNotificationSub = function(copayerId, token, cb) {
|
||||||
}, cb);
|
}, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Storage.prototype.fetchActiveTxConfirmationSubs = function(copayerId, cb) {
|
||||||
|
var filter = {
|
||||||
|
isActive: true
|
||||||
|
};
|
||||||
|
if (copayerId) filter.copayerId = copayerId;
|
||||||
|
|
||||||
|
this.db.collection(collections.TX_CONFIRMATION_SUBS).find(filter)
|
||||||
|
.toArray(function(err, result) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
if (!result) return cb();
|
||||||
|
|
||||||
|
var subs = _.map([].concat(result), function(r) {
|
||||||
|
return Model.TxConfirmationSub.fromObj(r);
|
||||||
|
});
|
||||||
|
return cb(null, subs);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Storage.prototype.storeTxConfirmationSub = function(txConfirmationSub, cb) {
|
||||||
|
this.db.collection(collections.TX_CONFIRMATION_SUBS).update({
|
||||||
|
copayerId: txConfirmationSub.copayerId,
|
||||||
|
txid: txConfirmationSub.txid,
|
||||||
|
}, txConfirmationSub, {
|
||||||
|
w: 1,
|
||||||
|
upsert: true,
|
||||||
|
}, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
Storage.prototype.removeTxConfirmationSub = function(copayerId, txid, cb) {
|
||||||
|
this.db.collection(collections.TX_CONFIRMATION_SUBS).remove({
|
||||||
|
copayerId: copayerId,
|
||||||
|
txid: txid,
|
||||||
|
}, {
|
||||||
|
w: 1
|
||||||
|
}, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
Storage.prototype._dump = function(cb, fn) {
|
Storage.prototype._dump = function(cb, fn) {
|
||||||
fn = fn || console.log;
|
fn = fn || console.log;
|
||||||
cb = cb || function() {};
|
cb = cb || function() {};
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
{{subjectPrefix}} Transaction confirmed
|
||||||
|
The transaction you were waiting for has been confirmed.
|
|
@ -0,0 +1,2 @@
|
||||||
|
{{subjectPrefix}} Transacción confirmada
|
||||||
|
La transacción que estabas esperando se ha confirmado.
|
|
@ -114,4 +114,45 @@ describe('Blockchain monitor', function() {
|
||||||
}, 50);
|
}, 50);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should notify copayers of tx confirmation', function(done) {
|
||||||
|
server.createAddress({}, function(err, address) {
|
||||||
|
should.not.exist(err);
|
||||||
|
|
||||||
|
var incoming = {
|
||||||
|
txid: '123',
|
||||||
|
vout: [{}],
|
||||||
|
};
|
||||||
|
incoming.vout[0][address.address] = 1500;
|
||||||
|
|
||||||
|
server.txConfirmationSubscribe({
|
||||||
|
txid: '123'
|
||||||
|
}, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
|
||||||
|
blockchainExplorer.getTxidsInBlock = sinon.stub().callsArgWith(1, null, ['123', '456']);
|
||||||
|
socket.handlers['block']('block1');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
blockchainExplorer.getTxidsInBlock = sinon.stub().callsArgWith(1, null, ['123', '456']);
|
||||||
|
socket.handlers['block']('block2');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
server.getNotifications({}, function(err, notifications) {
|
||||||
|
should.not.exist(err);
|
||||||
|
var notifications = _.filter(notifications, {
|
||||||
|
type: 'TxConfirmation'
|
||||||
|
});
|
||||||
|
notifications.length.should.equal(1);
|
||||||
|
var n = notifications[0];
|
||||||
|
n.walletId.should.equal(wallet.id);
|
||||||
|
n.creatorId.should.equal(server.copayerId);
|
||||||
|
n.data.txid.should.equal('123');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -295,6 +295,38 @@ describe('Email notifications', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should notify copayers when tx is confirmed if they are subscribed', function(done) {
|
||||||
|
server.createAddress({}, function(err, address) {
|
||||||
|
should.not.exist(err);
|
||||||
|
|
||||||
|
server.txConfirmationSubscribe({
|
||||||
|
txid: '123'
|
||||||
|
}, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
|
||||||
|
// Simulate tx confirmation notification
|
||||||
|
server._notify('TxConfirmation', {
|
||||||
|
txid: '123',
|
||||||
|
}, function(err) {
|
||||||
|
setTimeout(function() {
|
||||||
|
var calls = mailerStub.sendMail.getCalls();
|
||||||
|
calls.length.should.equal(1);
|
||||||
|
var email = calls[0].args[0];
|
||||||
|
email.to.should.equal('copayer1@domain.com');
|
||||||
|
email.from.should.equal('bws@dummy.net');
|
||||||
|
email.subject.should.contain('Transaction confirmed');
|
||||||
|
server.storage.fetchUnsentEmails(function(err, unsent) {
|
||||||
|
should.not.exist(err);
|
||||||
|
unsent.should.be.empty;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should notify each email address only once', function(done) {
|
it('should notify each email address only once', function(done) {
|
||||||
// Set same email address for copayer1 and copayer2
|
// Set same email address for copayer1 and copayer2
|
||||||
server.savePreferences({
|
server.savePreferences({
|
||||||
|
|
|
@ -158,6 +158,29 @@ describe('Push notifications', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should notify copayers when tx is confirmed if they are subscribed', function(done) {
|
||||||
|
server.createAddress({}, function(err, address) {
|
||||||
|
should.not.exist(err);
|
||||||
|
|
||||||
|
server.txConfirmationSubscribe({
|
||||||
|
txid: '123'
|
||||||
|
}, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
|
||||||
|
// Simulate tx confirmation notification
|
||||||
|
server._notify('TxConfirmation', {
|
||||||
|
txid: '123',
|
||||||
|
}, function(err) {
|
||||||
|
setTimeout(function() {
|
||||||
|
var calls = requestStub.getCalls();
|
||||||
|
calls.length.should.equal(1);
|
||||||
|
done();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Shared wallet', function() {
|
describe('Shared wallet', function() {
|
||||||
|
|
|
@ -1311,7 +1311,7 @@ describe('Wallet service', function() {
|
||||||
var MAX_MAIN_ADDRESS_GAP_old = Defaults.MAX_MAIN_ADDRESS_GAP;
|
var MAX_MAIN_ADDRESS_GAP_old = Defaults.MAX_MAIN_ADDRESS_GAP;
|
||||||
Defaults.MAX_MAIN_ADDRESS_GAP = 2;
|
Defaults.MAX_MAIN_ADDRESS_GAP = 2;
|
||||||
helpers.stubAddressActivity([]);
|
helpers.stubAddressActivity([]);
|
||||||
async.map(_.range(2), function(i, next) {
|
async.mapSeries(_.range(2), function(i, next) {
|
||||||
server.createAddress({}, next);
|
server.createAddress({}, next);
|
||||||
}, function(err, addresses) {
|
}, function(err, addresses) {
|
||||||
addresses.length.should.equal(2);
|
addresses.length.should.equal(2);
|
||||||
|
@ -7341,4 +7341,109 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Tx confirmation notifications', function() {
|
||||||
|
var server, wallet;
|
||||||
|
beforeEach(function(done) {
|
||||||
|
helpers.createAndJoinWallet(2, 3, function(s, w) {
|
||||||
|
server = s;
|
||||||
|
wallet = w;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should subscribe copayer to a tx confirmation', function(done) {
|
||||||
|
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
|
||||||
|
should.exist(server);
|
||||||
|
server.txConfirmationSubscribe({
|
||||||
|
txid: '123',
|
||||||
|
}, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
server.storage.fetchActiveTxConfirmationSubs(wallet.copayers[0].id, function(err, subs) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(subs);
|
||||||
|
subs.length.should.equal(1);
|
||||||
|
var s = subs[0];
|
||||||
|
s.txid.should.equal('123');
|
||||||
|
s.isActive.should.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should overwrite last subscription', function(done) {
|
||||||
|
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
|
||||||
|
should.exist(server);
|
||||||
|
server.txConfirmationSubscribe({
|
||||||
|
txid: '123',
|
||||||
|
}, function(err) {
|
||||||
|
server.txConfirmationSubscribe({
|
||||||
|
txid: '123',
|
||||||
|
}, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
server.storage.fetchActiveTxConfirmationSubs(wallet.copayers[0].id, function(err, subs) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(subs);
|
||||||
|
subs.length.should.equal(1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should unsubscribe copayer to the specified tx', function(done) {
|
||||||
|
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
|
||||||
|
should.exist(server);
|
||||||
|
async.series([
|
||||||
|
|
||||||
|
function(next) {
|
||||||
|
server.txConfirmationSubscribe({
|
||||||
|
txid: '123',
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
function(next) {
|
||||||
|
server.txConfirmationSubscribe({
|
||||||
|
txid: '456',
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
function(next) {
|
||||||
|
server.txConfirmationUnsubscribe({
|
||||||
|
txid: '123',
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
function(next) {
|
||||||
|
server.storage.fetchActiveTxConfirmationSubs(wallet.copayers[0].id, function(err, subs) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(subs);
|
||||||
|
subs.length.should.equal(1);
|
||||||
|
var s = subs[0];
|
||||||
|
s.txid.should.equal('456');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(next) {
|
||||||
|
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
|
||||||
|
server.txConfirmationUnsubscribe({
|
||||||
|
txid: '456'
|
||||||
|
}, next);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(next) {
|
||||||
|
server.storage.fetchActiveTxConfirmationSubs(wallet.copayers[0].id, function(err, subs) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(subs);
|
||||||
|
subs.length.should.equal(1);
|
||||||
|
var s = subs[0];
|
||||||
|
s.txid.should.equal('456');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
], function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue