ripple/ripple-rest

View on GitHub
api/lib/tx-to-rest-converter.js

Summary

Maintainability
D
2 days
Test Coverage
/* eslint-disable valid-jsdoc */
'use strict';
var ripple = require('ripple-lib');
var utils = require('./utils');
var _ = require('lodash');
var constants = require('./constants');
var parseBalanceChanges = require('ripple-lib-transactionparser')
                          .parseBalanceChanges;
var parseOrderBookChanges = require('ripple-lib-transactionparser')
                            .parseOrderBookChanges;

// This is just to support the legacy naming of "counterparty", this
// function should be removed when "issuer" is eliminated
function renameCounterpartyToIssuerInOrderChanges(orderChanges) {
  return _.mapValues(orderChanges, function(changes) {
    return _.map(changes, function(change) {
      return utils.renameCounterpartyToIssuerInOrder(change);
    });
  });
}

function renameCounterpartyToIssuerInBalanceChanges(balanceChanges) {
  return _.mapValues(balanceChanges, function(changes) {
    return _.map(changes, function(change) {
      return utils.renameCounterpartyToIssuer(change);
    });
  });
}

/**
 *  Helper that parses bit flags from ripple response
 *
 *  @param {Number} responseFlags - Integer flag on the ripple response
 *  @param {Object} flags - Object with parameter name and bit flag value pairs
 *
 *  @returns {Object} parsedFlags - Object with parameter name and boolean
 *                                  flags depending on response flag
 */
function parseFlagsFromResponse(responseFlags, flags) {
  var parsedFlags = {};

  for (var flagName in flags) {
    var flag = flags[flagName];
    parsedFlags[flag.name] = Boolean(responseFlags & flag.value);
  }

  return parsedFlags;
}

/**
 *  Convert a transaction in rippled tx format
 *  to a ripple-rest payment
 *
 *  @param {transaction} tx
 *  @param {Function} callback
 *  @param {Object} options
 *
 *  @callback
 *  @param {Error} error
 *  @param {Object} payment
 */

function isPartialPayment(tx) {
  return (tx.Flags & ripple.Transaction.flags.Payment.PartialPayment) !== 0;
}

function isNoDirectRipple(tx) {
  return (tx.Flags & ripple.Transaction.flags.Payment.NoRippleDirect) !== 0;
}

function convertAmount(amount) {
  if (typeof amount === 'string') {
    return {
      value: utils.dropsToXrp(amount),
      currency: 'XRP',
      issuer: ''
    };
  }
  return amount;
}

function parsePaymentMeta(account, tx, meta) {
  if (_.isUndefined(meta) || _.isEmpty(meta)) {
    return {};
  }
  if (meta.TransactionResult === 'tejSecretInvalid') {
    throw new Error('Invalid secret provided.');
  }

  var balanceChanges = renameCounterpartyToIssuerInBalanceChanges(
    parseBalanceChanges(meta));

  var order_changes = renameCounterpartyToIssuerInOrderChanges(
    parseOrderBookChanges(meta))[account];

  var partialPayment = (isPartialPayment(tx) && meta.DeliveredAmount) ? {
      destination_amount_submitted: convertAmount(tx.Amount),
      source_amount_submitted: convertAmount(tx.SendMax || tx.Amount)
    } : {};

  return _.assign({
    result: meta.TransactionResult,
    balance_changes: balanceChanges[account] || [],
    source_balance_changes: balanceChanges[tx.Account] || [],
    destination_balance_changes: balanceChanges[tx.Destination] || [],
    order_changes: order_changes || []
  }, partialPayment);
}

function parsePaymentFromTx(account, message, meta) {
  if (!account) {
    throw new Error('Internal Error. must supply options.account');
  }

  var tx = message.tx_json;
  if (tx.TransactionType !== 'Payment') {
    throw new Error('Not a payment. The transaction corresponding to '
                    + 'the given identifier is not a payment.');
  }

  var amount;
  // if there is a DeliveredAmount we should use it over Amount there should
  // always be a DeliveredAmount if the partial payment flag is set. also
  // there shouldn't be a DeliveredAmount if there's no partial payment flag
  if (isPartialPayment(tx) && meta && meta.DeliveredAmount) {
    amount = meta.DeliveredAmount;
  } else {
    amount = tx.Amount;
  }

  var source_amount = utils.parseCurrencyAmount(tx.SendMax || amount, true);
  var destination_amount = utils.parseCurrencyAmount(amount, true);

  var payment = {
    // User supplied
    source_account: tx.Account,
    source_tag: (tx.SourceTag ? '' + tx.SourceTag : ''),
    source_amount: source_amount,
    source_slippage: '0',     // TODO: why is this hard-coded?
    destination_account: tx.Destination,
    destination_tag: (tx.DestinationTag ? '' + tx.DestinationTag : ''),
    destination_amount: destination_amount,
    // Advanced options
    invoice_id: tx.InvoiceID || '',
    paths: JSON.stringify(tx.Paths || []),
    no_direct_ripple: isNoDirectRipple(tx),
    partial_payment: isPartialPayment(tx),
    // TODO: Update to use `unaffected` when perspective account in URI
    // is not affected
    direction: (account === tx.Account ? 'outgoing' :
        (account === tx.Destination ? 'incoming' : 'passthrough')),
    timestamp: (tx.date
      ? new Date(ripple.utils.toTimestamp(tx.date)).toISOString() : ''),
    fee: utils.dropsToXrp(tx.Fee) || ''
  };

  if (Array.isArray(tx.Memos) && tx.Memos.length > 0) {
    payment.memos = [];
    for (var m = 0; m < tx.Memos.length; m++) {
      payment.memos.push(tx.Memos[m].Memo);
    }
  }

  var fullPayment = _.assign(payment, parsePaymentMeta(account, tx, meta));
  return _.assign({payment: fullPayment}, meta);
}

/**
 *  Convert an OfferCreate or OfferCancel transaction in rippled tx format
 *  to a ripple-rest order_change
 *
 *  @param {Object} tx
 *  @param {Object} options
 *  @param {String} options.account - The account to use as perspective when
 *                                    parsing the transaction.
 *
 *  @returns {Promise.<Object,Error>} - resolves to a parsed OrderChange
 *                                      transaction or an Error
 */

function parseOrderFromTx(tx, options) {
  if (!options.account) {
    throw new Error('Internal Error. must supply options.account');
  }
  if (tx.TransactionType !== 'OfferCreate'
      && tx.TransactionType !== 'OfferCancel') {
    throw new Error('Invalid parameter: identifier. The transaction '
      + 'corresponding to the given identifier is not an order');
  }
  if (tx.meta !== undefined && tx.meta.TransactionResult !== undefined) {
    if (tx.meta.TransactionResult === 'tejSecretInvalid') {
      throw new Error('Invalid secret provided.');
    }
  }

  var order;
  var flags = parseFlagsFromResponse(tx.flags, constants.OfferCreateFlags);
  var action = tx.TransactionType === 'OfferCreate'
    ? 'order_create' : 'order_cancel';
  var balance_changes = tx.meta
    ? parseBalanceChanges(tx.meta)[options.account] || [] : [];
  var timestamp = tx.date
    ? new Date(ripple.utils.toTimestamp(tx.date)).toISOString() : '';
  var order_changes = tx.meta ?
    parseOrderBookChanges(tx.meta)[options.account] : [];

  var direction;
  if (options.account === tx.Account) {
    direction = 'outgoing';
  } else if (balance_changes.length && order_changes.length) {
    direction = 'incoming';
  } else {
    direction = 'passthrough';
  }

  if (action === 'order_create') {
    order = {
      account: tx.Account,
      taker_pays: utils.parseCurrencyAmount(tx.TakerPays),
      taker_gets: utils.parseCurrencyAmount(tx.TakerGets),
      passive: flags.passive,
      immediate_or_cancel: flags.immediate_or_cancel,
      fill_or_kill: flags.fill_or_kill,
      type: flags.sell ? 'sell' : 'buy',
      sequence: tx.Sequence
    };
  } else {
    order = {
      account: tx.Account,
      type: 'cancel',
      sequence: tx.Sequence,
      cancel_sequence: tx.OfferSequence
    };
  }

  return {
    hash: tx.hash,
    ledger: tx.ledger_index,
    validated: tx.validated,
    timestamp: timestamp,
    fee: utils.dropsToXrp(tx.Fee),
    action: action,
    direction: direction,
    order: order,
    balance_changes: balance_changes,
    order_changes: order_changes || []
  };
}

/**
 *  Convert the pathfind results returned from rippled into an
 *  array of payments in the ripple-rest format. The client should be
 *  able to submit any of the payments in the array back to ripple-rest.
 *
 *  @param {rippled Pathfind results} pathfindResults
 *  @param {Amount} options.destination_amount Since this is not returned by
 *                  rippled in the pathfind results it can either be added
 *                  to the results or included in the options here
 *  @param {RippleAddress} options.source_account Since this is not returned
 *                  by rippled in the pathfind results it can either be added
 *                  to the results or included in the options here
 *
 *  @returns {Array of Payments} payments
 */
function parsePaymentsFromPathFind(pathfindResults) {
  return pathfindResults.alternatives.map(function(alternative) {
    return {
      source_account: pathfindResults.source_account,
      source_tag: '',
      source_amount: (typeof alternative.source_amount === 'string' ?
      {
        value: utils.dropsToXrp(alternative.source_amount),
        currency: 'XRP',
        issuer: ''
      } :
      {
        value: alternative.source_amount.value,
        currency: alternative.source_amount.currency,
        issuer: (typeof alternative.source_amount.issuer !== 'string'
          || alternative.source_amount.issuer === pathfindResults.source_account
          ? '' : alternative.source_amount.issuer)
      }),
      source_slippage: '0',
      destination_account: pathfindResults.destination_account,
      destination_tag: '',
      destination_amount: (
        typeof pathfindResults.destination_amount === 'string' ?
      {
        value: utils.dropsToXrp(pathfindResults.destination_amount),
        currency: 'XRP',
        issuer: ''
      } :
      {
        value: pathfindResults.destination_amount.value,
        currency: pathfindResults.destination_amount.currency,
        issuer: pathfindResults.destination_amount.issuer
      }),
      invoice_id: '',
      paths: JSON.stringify(alternative.paths_computed),
      partial_payment: false,
      no_direct_ripple: false
    };
  });
}

function parseOrderCancellationResponse(message, meta) {
  var order = {
    account: message.tx_json.Account,
    fee: utils.dropsToXrp(message.tx_json.Fee),
    offer_sequence: message.tx_json.OfferSequence,
    sequence: message.tx_json.Sequence
  };
  return _.assign({order: order}, meta);
}

function parseOrderResponse(message, meta) {
  var order = {
    account: message.tx_json.Account,
    taker_gets: utils.parseCurrencyAmount(message.tx_json.TakerGets),
    taker_pays: utils.parseCurrencyAmount(message.tx_json.TakerPays),
    fee: utils.dropsToXrp(message.tx_json.Fee),
    type: (message.tx_json.Flags & ripple.Transaction.flags.OfferCreate.Sell)
      > 0 ? 'sell' : 'buy',
    sequence: message.tx_json.Sequence
  };
  return _.assign({order: order}, meta);
}

function parseTrustLineResponse(message, meta) {
  var limit = message.tx_json.LimitAmount;
  var parsedFlags = parseFlagsFromResponse(message.tx_json.Flags,
    constants.TrustSetResponseFlags);
  var trustline = {
    account: message.tx_json.Account,
    limit: limit.value,
    currency: limit.currency,
    counterparty: limit.issuer,
    account_allows_rippling: !parsedFlags.prevent_rippling,
    account_trustline_frozen: parsedFlags.account_trustline_frozen,
    authorized: parsedFlags.authorized ? parsedFlags.authorized : undefined
  };
  return _.assign({trustline: trustline}, meta);
}

function parseSettingsResponse(settings, message, meta) {
  var _settings = {};
  for (var flagName in constants.AccountSetIntFlags) {
    var flag = constants.AccountSetIntFlags[flagName];
    _settings[flag.name] = settings[flag.name];
  }

  for (var fieldName in constants.AccountRootFields) {
    var field = constants.AccountRootFields[fieldName];
    _settings[field.name] = settings[field.name];
  }

  _.assign(_settings, parseFlagsFromResponse(message.tx_json.Flags,
    constants.AccountSetResponseFlags));
  return _.assign({settings: _settings}, meta);
}

module.exports = {
  parsePaymentFromTx: parsePaymentFromTx,
  parsePaymentsFromPathFind: parsePaymentsFromPathFind,
  parseOrderFromTx: parseOrderFromTx,
  parseCancelOrderFromTx: parseOrderCancellationResponse,
  parseSubmitOrderFromTx: parseOrderResponse,
  parseTrustResponseFromTx: parseTrustLineResponse,
  parseSettingsResponseFromTx: parseSettingsResponse,
  parseFlagsFromResponse: parseFlagsFromResponse
};