Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Convert getPathFind and add unit test
- Loading branch information
Chris Clark
committed
Jul 2, 2015
1 parent
077a534
commit b7d10ad
Showing
12 changed files
with
622 additions
and
144 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.