divvy-rest
Version:
A RESTful API for submitting payments and monitoring accounts on the Divvy network.
273 lines (237 loc) • 9.46 kB
JavaScript
/* eslint-disable valid-jsdoc */
;
var _ = require('lodash');
var async = require('async');
var transactions = require('./transactions');
var serverLib = require('./lib/server-lib');
var NotificationParser = require('./lib/notification_parser.js');
var errors = require('./lib/errors.js');
var utils = require('./lib/utils.js');
var validate = require('./lib/validate.js');
/**
* Find the previous and next transaction hashes or
* client_resource_ids using both the divvyd and
* local database. Report errors to the client using res.json
* or pass the notificationDetails with the added fields
* back to the callback.
*
* @param {Remote} $.remote
* @param {/lib/db-interface} $.dbinterface
* @param {Express.js Response} res
* @param {DivvyAddress} notificationDetails.account
* @param {Divvy Transaction in JSON Format} notificationDetails.transaction
* @param {Hex-encoded String|ResourceId} notificationDetails.identifier
* @param {Function} callback
*
* @callback
* @param {Error} error
* @param {Object with fields "account", "transaction",
* "next_transaction_identifier", "next_hash",
* "previous_transaction_identifier", "previous_hash"} notificationDetails
*/
function attachPreviousAndNextTransactionIdentifiers(api,
notificationDetails, topCallback) {
// Get all of the transactions affecting the specified
// account in the given ledger. This is done so that
// we can query for one more than that number on either
// side to ensure that we'll find the next and previous
// transactions, no matter how many transactions the
// given account had in the same ledger
function getAccountTransactionsInBaseTransactionLedger(callback) {
var params = {
account: notificationDetails.account,
ledger_index_min: notificationDetails.transaction.ledger_index,
ledger_index_max: notificationDetails.transaction.ledger_index,
exclude_failed: false,
max: 99999999,
limit: 200 // arbitrary, just checking number of transactions in ledger
};
transactions.getAccountTransactions(api, params, callback);
}
// All we care about is the count of the transactions
function countAccountTransactionsInBaseTransactionledger(txns, callback) {
callback(null, txns.length);
}
// Query for one more than the numTransactionsInLedger
// going forward and backwards to get a range of transactions
// that will definitely include the next and previous transactions
function getNextAndPreviousTransactions(numTransactionsInLedger, callback) {
async.concat([false, true], function(earliestFirst, concat_callback) {
var params = {
account: notificationDetails.account,
max: numTransactionsInLedger + 1,
min: numTransactionsInLedger + 1,
limit: numTransactionsInLedger + 1,
earliestFirst: earliestFirst
};
// In divvyd -1 corresponds to the first or last ledger
// in its database, depending on whether it is the min or max value
if (params.earliestFirst) {
params.ledger_index_max = -1;
params.ledger_index_min = notificationDetails.transaction.ledger_index;
} else {
params.ledger_index_max = notificationDetails.transaction.ledger_index;
params.ledger_index_min = -1;
}
transactions.getAccountTransactions(api, params, concat_callback);
}, callback);
}
// Sort the transactions returned by ledger_index and remove duplicates
function sortTransactions(allTransactions, callback) {
allTransactions.push(notificationDetails.transaction);
var txns = _.uniq(allTransactions, function(tx) {
return tx.hash;
});
txns.sort(utils.compareTransactions);
callback(null, txns);
}
// Find the baseTransaction amongst the results. Because the
// transactions have been sorted, the next and previous transactions
// will be the ones on either side of the base transaction
function findPreviousAndNextTransactions(txns, callback) {
// Find the index in the array of the baseTransaction
var baseTransactionIndex = _.findIndex(txns, function(possibility) {
if (possibility.hash === notificationDetails.transaction.hash) {
return true;
} else if (possibility.client_resource_id &&
(possibility.client_resource_id ===
notificationDetails.transaction.client_resource_id ||
possibility.client_resource_id === notificationDetails.identifier)) {
return true;
}
return false;
});
// The previous transaction is the one with an index in
// the array of baseTransactionIndex - 1
if (baseTransactionIndex > 0) {
var previous_transaction = txns[baseTransactionIndex - 1];
notificationDetails.previous_transaction_identifier =
(previous_transaction.from_local_db ?
previous_transaction.client_resource_id : previous_transaction.hash);
notificationDetails.previous_hash = previous_transaction.hash;
}
// The next transaction is the one with an index in
// the array of baseTransactionIndex + 1
if (baseTransactionIndex + 1 < txns.length) {
var next_transaction = txns[baseTransactionIndex + 1];
notificationDetails.next_transaction_identifier =
(next_transaction.from_local_db ?
next_transaction.client_resource_id : next_transaction.hash);
notificationDetails.next_hash = next_transaction.hash;
}
callback(null, notificationDetails);
}
var steps = [
getAccountTransactionsInBaseTransactionLedger,
countAccountTransactionsInBaseTransactionledger,
getNextAndPreviousTransactions,
sortTransactions,
findPreviousAndNextTransactions
];
async.waterfall(steps, topCallback);
}
/**
* Get a notification corresponding to the specified
* account and transaction identifier. Send errors back
* to the client using the res.json method or pass
* the notification json to the callback function.
*
* @param {Remote} $.remote
* @param {/lib/db-interface} $.dbinterface
* @param {DivvyAddress} req.params.account
* @param {Hex-encoded String|ResourceId} req.params.identifier
* @param {Express.js Response} res
* @param {Function} callback
*
* @callback
* @param {Error} error
* @param {Notification} notification
*/
function getNotificationHelper(api, account, identifier, topCallback) {
validate.address(account);
function getTransaction(callback) {
transactions.getTransaction(api, account, identifier, {}, callback);
}
function checkLedger(baseTransaction, callback) {
serverLib.remoteHasLedger(api.remote, baseTransaction.ledger_index,
function(error, remoteHasLedger) {
if (error) {
return callback(error);
}
if (remoteHasLedger) {
callback(null, baseTransaction);
} else {
callback(new errors.NotFoundError('Cannot Get Notification. ' +
'This transaction is not in the divvy\'s complete ledger set. ' +
'Because there is a gap in the divvyd\'s historical database it ' +
'is not possible to determine the transactions that precede this one')
);
}
});
}
function prepareNotificationDetails(baseTransaction, callback) {
var notificationDetails = {
account: account,
identifier: identifier,
transaction: baseTransaction
};
// Move client_resource_id to notificationDetails from transaction
if (baseTransaction.client_resource_id) {
notificationDetails.client_resource_id =
baseTransaction.client_resource_id;
}
attachPreviousAndNextTransactionIdentifiers(api, notificationDetails,
callback);
}
// Parse the Notification object from the notificationDetails
function parseNotificationDetails(notificationDetails, callback) {
callback(null, NotificationParser.parse(notificationDetails));
}
var steps = [
getTransaction,
checkLedger,
prepareNotificationDetails,
parseNotificationDetails
];
async.waterfall(steps, topCallback);
}
/**
* Get a notification corresponding to the specified
* account and transaction identifier. Uses the res.json
* method to send errors or a notification back to the client.
*
* @param {Remote} $.remote
* @param {/lib/db-interface} $.dbinterface
* @param {/lib/config-loader} $.config
* @param {DivvyAddress} req.params.account
* @param {Hex-encoded String|ResourceId} req.params.identifier
*/
function getNotification(account, identifier, urlBase, callback) {
getNotificationHelper(this, account, identifier,
function(error, notification) {
if (error) {
return callback(error);
}
var responseBody = {
notification: notification
};
// Add urlBase to each url in notification
Object.keys(responseBody.notification).forEach(function(key) {
if (/url/.test(key) && responseBody.notification[key]) {
responseBody.notification[key] = urlBase +
responseBody.notification[key];
}
});
// Move client_resource_id to response body instead of inside
// the Notification
var client_resource_id = responseBody.notification.client_resource_id;
delete responseBody.notification.client_resource_id;
if (client_resource_id) {
responseBody.client_resource_id = client_resource_id;
}
callback(null, responseBody);
});
}
module.exports = {
getNotification: getNotification
};