ripple/ripple-rest

View on GitHub
api/notifications.js

Summary

Maintainability
D
2 days
Test Coverage
/* eslint-disable valid-jsdoc */
'use strict';
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 rippled 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 {RippleAddress} notificationDetails.account
 *  @param {Ripple Transaction in JSON Format} notificationDetails.transaction
 *  @param {Hex-encoded String|ResourceId} notificationDetails.identifier
 *  @param {Function} callback
 *
 *  @callback
 *  @param {Error} error
 *  @param {Object} 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 rippled -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 {RippleAddress} 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, urlBase, topCallback) {

  function getTransaction(callback) {
    try {
      transactions.getTransaction(api, account, identifier, {}, callback);
    } catch(err) {
      callback(err);
    }
  }

  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 ripple\'s complete ledger set. ' +
          'Because there is a gap in the rippled\'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, urlBase));
  }

  function formatNotificationResponse(notificationDetails, callback) {
    var responseBody = {
      notification: notificationDetails
    };

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


  var steps = [
    getTransaction,
    checkLedger,
    prepareNotificationDetails,
    parseNotificationDetails,
    formatNotificationResponse
  ];

  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 {RippleAddress} req.params.account
 *  @param {Hex-encoded String|ResourceId} req.params.identifier
 */
function getNotification(account, identifier, urlBase, callback) {
  validate.address(account);
  validate.paymentIdentifier(identifier);

  return getNotificationHelper(this, account, identifier, urlBase, callback);
}

/**
 *  Get a notifications corresponding to the specified
 *  account.
 *
 *  This function calls transactions.getAccountTransactions
 *  recursively to retrieve results_per_page number of transactions
 *  and filters the results using client-specified parameters.
 *
 *  @param {RippleAddress} account
 *  @param {string} urlBase - The url to use for the transaction status URL
 *
 *  @param {string} options.source_account
 *  @param {Number} options.ledger_min
 *  @param {Number} options.ledger_max
 *  @param {string} [false] options.earliest_first
 *  @param {string[]} options.types - @see transactions.getAccountTransactions
 *
 */
// TODO: If given ledger range, check for ledger gaps
function getNotifications(account, urlBase, options, callback) {
  validate.address(account);

  var self = this;

  function getTransactions(_callback) {

    var resultsPerPage = options.results_per_page ||
      transactions.DEFAULT_RESULTS_PER_PAGE;
    var offset = resultsPerPage * ((options.page || 1) - 1);

    var args = {
      account: account,
      direction: options.direction,
      min: resultsPerPage,
      max: resultsPerPage,
      ledger_index_min: options.ledger_min,
      ledger_index_max: options.ledger_max,
      offset: offset,
      earliestFirst: options.earliest_first
    };

    transactions.getAccountTransactions(self, args, _callback);
  }

  function parseNotifications(baseTransactions, _callback) {
    var numTransactions = baseTransactions.length;

    function parseNotification(transaction, __callback) {
      var args = {
        account: account,
        identifier: transaction.hash,
        transaction: transaction
      };

      // Attaching previous and next identifiers
      var idx = baseTransactions.indexOf(transaction);
      var previous = baseTransactions[idx - 1];
      var next = baseTransactions[idx + 1];

      if (!options.earliest_first) {
        args.previous_hash = previous ? previous.hash : undefined;
        args.next_hash = next ? next.hash : undefined;
      } else {
        args.previous_hash = next ? next.hash : undefined;
        args.next_hash = previous ? previous.hash : undefined;
      }

      args.previous_transaction_identifier = args.previous_hash;
      args.next_transaction_identifier = args.next_hash;

      var firstAndPaging = options.page &&
        (options.earliest_first ?
         args.previous_hash === undefined : args.next_hash === undefined);

      var last = idx === numTransactions - 1;

      if (firstAndPaging || last) {
        attachPreviousAndNextTransactionIdentifiers(self, args,
          function(err, _args) {
            return __callback(err, NotificationParser.parse(_args, urlBase));
          }
        );
      } else {
        return __callback(null, NotificationParser.parse(args, urlBase));
      }
    }

    return async.map(baseTransactions, parseNotification, _callback);
  }

  function formatResponse(notifications, _callback) {
    _callback(null, {notifications: notifications});
  }

  var steps = [
    getTransactions,
    _.partial(utils.attachDate, self),
    parseNotifications,
    formatResponse
  ];

  return async.waterfall(steps, callback);
}

module.exports = {
  getNotification: getNotification,
  getNotifications: getNotifications
};