Skip to content

Commit

Permalink
Convert getPathFind and add unit test
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Clark committed Jul 2, 2015
1 parent 077a534 commit b7d10ad
Show file tree
Hide file tree
Showing 12 changed files with 622 additions and 144 deletions.
9 changes: 8 additions & 1 deletion src/api/common/utils.js
Expand Up @@ -38,7 +38,14 @@ function composeAsync(wrapper, callback) {
callback(error);
return;
}
callback(null, wrapper(data));
let result;
try {
result = wrapper(data);
} catch (exception) {
callback(exception);
return;
}
callback(null, result);
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/api/index.js
Expand Up @@ -10,7 +10,7 @@ const getTransaction = require('./ledger/transaction');
const getAccountTransactions = require('./ledger/transactions');
const getTrustlines = require('./ledger/trustlines');
const getBalances = require('./ledger/balances');
// const getPathFind = require('./ledger/pathfind');
const getPathFind = require('./ledger/pathfind');
const getOrders = require('./ledger/orders');
const getOrderBook = require('./ledger/orderbook');
const getSettings = require('./ledger/settings');
Expand Down Expand Up @@ -39,7 +39,7 @@ RippleAPI.prototype = {
getAccountTransactions,
getTrustlines,
getBalances,
// getPathFind,
getPathFind,
getOrders,
getOrderBook,
getSettings,
Expand Down
19 changes: 7 additions & 12 deletions src/api/ledger/balances.js
Expand Up @@ -5,12 +5,6 @@ const utils = require('./utils');
const getTrustlines = require('./trustlines');
const validate = utils.common.validate;
const composeAsync = utils.common.composeAsync;
const dropsToXrp = utils.common.dropsToXrp;

function getXRPBalance(remote, address, ledgerVersion, callback) {
remote.requestAccountInfo({account: address, ledger: ledgerVersion},
composeAsync((data) => dropsToXrp(data.account_data.Balance), callback));
}

function getTrustlineBalanceAmount(trustline) {
return {
Expand All @@ -23,9 +17,10 @@ function getTrustlineBalanceAmount(trustline) {
function formatBalances(balances) {
const xrpBalance = {
currency: 'XRP',
value: balances[0]
value: balances.xrp
};
return [xrpBalance].concat(balances[1].map(getTrustlineBalanceAmount));
return [xrpBalance].concat(
balances.trustlines.map(getTrustlineBalanceAmount));
}

function getBalances(account, options, callback) {
Expand All @@ -34,10 +29,10 @@ function getBalances(account, options, callback) {

const ledgerVersion = options.ledgerVersion
|| this.remote.getLedgerSequence();
async.parallel([
_.partial(getXRPBalance, this.remote, account, ledgerVersion),
_.partial(getTrustlines.bind(this), account, options)
], composeAsync(formatBalances, callback));
async.parallel({
xrp: _.partial(utils.getXRPBalance, this.remote, account, ledgerVersion),
trustlines: _.partial(getTrustlines.bind(this), account, options)
}, composeAsync(formatBalances, callback));
}

module.exports = utils.wrapCatch(getBalances);
8 changes: 7 additions & 1 deletion src/api/ledger/parse/pathfind.js
@@ -1,7 +1,13 @@
/* @flow */
'use strict';
const _ = require('lodash');
const parseAmount = require('./amount');

function parsePaths(paths) {
return paths.map(steps => steps.map(step =>
_.omit(step, ['type', 'type_hex'])));
}

function parsePathfind(sourceAddress: string,
destinationAmount: Object, pathfindResult: Object): Object {
return pathfindResult.alternatives.map(function(alternative) {
Expand All @@ -14,7 +20,7 @@ function parsePathfind(sourceAddress: string,
address: pathfindResult.destination_account,
amount: destinationAmount
},
paths: JSON.stringify(alternative.paths_computed),
paths: JSON.stringify(parsePaths(alternative.paths_computed)),
allowPartialPayment: false,
noDirectRipple: false
};
Expand Down
207 changes: 83 additions & 124 deletions src/api/ledger/pathfind.js
@@ -1,147 +1,106 @@
/* eslint-disable valid-jsdoc */
'use strict';
const _ = require('lodash');
const async = require('async');
const asyncify = require('simple-asyncify');
const bignum = require('bignumber.js');
const BigNumber = require('bignumber.js');
const utils = require('./utils');
const validate = utils.common.validate;
const parsePathfind = require('./parse/pathfind');
const NotFoundError = utils.common.errors.NotFoundError;
const TimeOutError = utils.common.errors.TimeOutError;
const composeAsync = utils.common.composeAsync;

/**
* Get a ripple path find, a.k.a. payment options,
* for a given set of parameters and respond to the
* client with an array of fully-formed Payments.
*
* @param {Remote} remote
* @param {RippleAddress} req.params.source_account
* @param {Amount Array ["USD r...,XRP,..."]} req.query.source_currencies
* - Note that Express.js middleware replaces "+" signs with spaces.
* Clients should use "+" signs but the values here will end up
* as spaces
* @param {RippleAddress} req.params.destination_account
* @param {Amount "1+USD+r..."} req.params.destination_amount_string
*/
function getPathFind(pathfind, callback) {
const self = this;
validate.pathfind(pathfind);
function addParams(params, result) {
return _.assign({}, result, {
source_account: params.src_account,
source_currencies: params.src_currencies,
destination_amount: params.dst_amount
});
}

function prepareOptions() {
const pathfindParams = {
src_account: pathfind.source.address,
dst_account: pathfind.destination.address,
dst_amount: utils.common.toRippledAmount(pathfind.destination.amount)
};
if (typeof pathfindParams.dst_amount === 'object'
&& !pathfindParams.dst_amount.issuer) {
// Convert blank issuer to sender's address
// (Ripple convention for 'any issuer')
// https://ripple.com/build/transactions/
// #special-issuer-values-for-sendmax-and-amount
// https://ripple.com/build/ripple-rest/#counterparties-in-payments
pathfindParams.dst_amount.issuer = pathfindParams.dst_account;
}
if (pathfind.source.amounts && pathfind.source.amounts.length > 0) {
pathfindParams.src_currencies = pathfind.source.amounts;
}
return pathfindParams;
function requestPathFind(remote, pathfind, callback) {
const params = {
src_account: pathfind.source.address,
dst_account: pathfind.destination.address,
dst_amount: utils.common.toRippledAmount(pathfind.destination.amount)
};
if (typeof params.dst_amount === 'object' && !params.dst_amount.issuer) {
// Convert blank issuer to sender's address
// (Ripple convention for 'any issuer')
// https://ripple.com/build/transactions/
// #special-issuer-values-for-sendmax-and-amount
// https://ripple.com/build/ripple-rest/#counterparties-in-payments
params.dst_amount.issuer = params.dst_account;
}
if (pathfind.source.amounts && pathfind.source.amounts.length > 0) {
params.src_currencies = pathfind.source.amounts.map(amount =>
_.omit(amount, 'value'));
}

function findPath(pathfindParams, _callback) {
const request = self.remote.requestRipplePathFind(pathfindParams);
request.once('error', _callback);
request.once('success', function(pathfindResults) {
pathfindResults.source_account = pathfindParams.src_account;
pathfindResults.source_currencies = pathfindParams.src_currencies;
pathfindResults.destination_amount = pathfindParams.dst_amount;
_callback(null, pathfindResults);
});
remote.requestRipplePathFind(params,
composeAsync(_.partial(addParams, params), callback));
}

function reconnectRippled() {
self.remote.disconnect(function() {
self.remote.connect();
});
}
request.timeout(utils.common.server.CONNECTION_TIMEOUT, function() {
request.removeAllListeners();
reconnectRippled();
_callback(new TimeOutError('Path request timeout'));
function addDirectXrpPath(paths, xrpBalance) {
// Add XRP "path" only if the source acct has enough XRP to make the payment
const destinationAmount = paths.destination_amount;
if ((new BigNumber(xrpBalance)).greaterThanOrEqualTo(destinationAmount)) {
paths.alternatives.unshift({
paths_canonical: [],
paths_computed: [],
source_amount: paths.destination_amount
});
request.request();
}
return paths;
}

function addDirectXrpPath(pathfindResults, _callback) {
// Check if destination_amount is XRP and if destination_account accepts XRP
if (typeof pathfindResults.destination_amount.currency === 'string'
|| pathfindResults.destination_currencies.indexOf('XRP') === -1) {
return _callback(null, pathfindResults);
}
// Check source_account balance
self.remote.requestAccountInfo({account: pathfindResults.source_account},
function(error, result) {
if (error) {
return _callback(new Error(
'Cannot get account info for source_account. ' + error));
}
if (!result || !result.account_data || !result.account_data.Balance) {
return _callback(new Error('Internal Error. Malformed account info : '
+ JSON.stringify(result)));
}
// Add XRP "path" only if the source_account has enough money
// to execute the payment
if (bignum(result.account_data.Balance).greaterThan(
pathfindResults.destination_amount)) {
pathfindResults.alternatives.unshift({
paths_canonical: [],
paths_computed: [],
source_amount: pathfindResults.destination_amount
});
}
_callback(null, pathfindResults);
});
}
function isRippledIOUAmount(amount) {
// rippled XRP amounts are specified as decimal strings
return (typeof amount === 'object') &&
amount.currency && (amount.currency !== 'XRP');
}

function formatPath(pathfindResults) {
const alternatives = pathfindResults.alternatives;
if (alternatives && alternatives.length > 0) {
return parsePathfind(pathfindResults);
}
if (pathfindResults.destination_currencies.indexOf(
pathfind.destination.amount.currency) === -1) {
throw new NotFoundError('No paths found. ' +
'The destination_account does not accept ' +
pathfind.destination.amount.currency +
', they only accept: ' +
pathfindResults.destination_currencies.join(', '));
} else if (pathfindResults.source_currencies
&& pathfindResults.source_currencies.length > 0) {
throw new NotFoundError('No paths found. Please ensure' +
' that the source_account has sufficient funds to execute' +
' the payment in one of the specified source_currencies. If it does' +
' there may be insufficient liquidity in the network to execute' +
' this payment right now');
} else {
throw new NotFoundError('No paths found.' +
' Please ensure that the source_account has sufficient funds to' +
' execute the payment. If it does there may be insufficient liquidity' +
' in the network to execute this payment right now');
}
function conditionallyAddDirectXRPPath(remote, address, paths, callback) {
if (isRippledIOUAmount(paths.destination_amount)
|| !_.includes(paths.destination_currencies, 'XRP')) {
callback(null, paths);
} else {
utils.getXRPBalance(remote, address, undefined,
composeAsync(_.partial(addDirectXrpPath, paths), callback));
}
}

function formatResponse(payments) {
return {payments: payments};
function formatResponse(pathfind, paths) {
if (paths.alternatives && paths.alternatives.length > 0) {
const address = pathfind.source.address;
return parsePathfind(address, pathfind.destination.amount, paths);
}
if (!_.includes(paths.destination_currencies,
pathfind.destination.amount.currency)) {
throw new NotFoundError('No paths found. ' +
'The destination_account does not accept ' +
pathfind.destination.amount.currency + ', they only accept: ' +
paths.destination_currencies.join(', '));
} else if (paths.source_currencies && paths.source_currencies.length > 0) {
throw new NotFoundError('No paths found. Please ensure' +
' that the source_account has sufficient funds to execute' +
' the payment in one of the specified source_currencies. If it does' +
' there may be insufficient liquidity in the network to execute' +
' this payment right now');
} else {
throw new NotFoundError('No paths found.' +
' Please ensure that the source_account has sufficient funds to' +
' execute the payment. If it does there may be insufficient liquidity' +
' in the network to execute this payment right now');
}
}

const steps = [
asyncify(prepareOptions),
findPath,
addDirectXrpPath,
asyncify(formatPath),
asyncify(formatResponse)
];
function getPathFind(pathfind, callback) {
validate.pathfind(pathfind);

async.waterfall(steps, callback);
const address = pathfind.source.address;
async.waterfall([
_.partial(requestPathFind, this.remote, pathfind),
_.partial(conditionallyAddDirectXRPPath, this.remote, address)
], composeAsync(_.partial(formatResponse, pathfind), callback));
}

module.exports = utils.wrapCatch(getPathFind);
9 changes: 9 additions & 0 deletions src/api/ledger/utils.js
@@ -1,6 +1,13 @@
'use strict';
const _ = require('lodash');
const common = require('../common');
const dropsToXrp = common.dropsToXrp;
const composeAsync = common.composeAsync;

function getXRPBalance(remote, address, ledgerVersion, callback) {
remote.requestAccountInfo({account: address, ledger: ledgerVersion},
composeAsync((data) => dropsToXrp(data.account_data.Balance), callback));
}

// If the marker is omitted from a response, you have reached the end
// getter(marker, limit, callback), callback(error, {marker, results})
Expand Down Expand Up @@ -56,6 +63,7 @@ function signum(num) {
* @param {Object} second
* @returns {Number} [-1, 0, 1]
*/

function compareTransactions(first, second) {
if (first.ledgerVersion === second.ledgerVersion) {
return signum(Number(first.indexInLedger) - Number(second.indexInLedger));
Expand All @@ -64,6 +72,7 @@ function compareTransactions(first, second) {
}

module.exports = {
getXRPBalance: getXRPBalance,
compareTransactions: compareTransactions,
renameCounterpartyToIssuer: renameCounterpartyToIssuer,
renameCounterpartyToIssuerInOrder: renameCounterpartyToIssuerInOrder,
Expand Down
1 change: 1 addition & 0 deletions src/api/transaction/settings.js
Expand Up @@ -78,6 +78,7 @@ function setTransactionFields(transaction, input) {
* @returns {Number|String} numbers will be converted while strings
* are returned
*/

function convertTransferRate(transferRate) {
if (_.isNumber(transferRate)) {
return transferRate * Math.pow(10, 9);
Expand Down
5 changes: 2 additions & 3 deletions src/api/transaction/utils.js
Expand Up @@ -23,9 +23,8 @@ function getFeeDrops(remote) {
return remote.feeTx(feeUnits).to_text();
}

/*:: type Callback = (err: ?(typeof Error), data: {tx_json: any}) => void */
function createTxJSON(transaction: any, remote: any,
instructions: any, callback: Callback): void {
function createTxJSON(transaction: any, remote: any, instructions: any,
callback: (err: ?(typeof Error), data: {tx_json: any}) => void): void {
common.validate.instructions(instructions);

transaction.complete();
Expand Down

0 comments on commit b7d10ad

Please sign in to comment.