api/orders.js
/* globals Promise: true */
/* eslint-disable valid-jsdoc */
'use strict';
var _ = require('lodash');
var asyncify = require('simple-asyncify');
var Promise = require('bluebird');
var ripple = require('ripple-lib');
var utils = require('./lib/utils');
var errors = require('./lib/errors.js');
var TxToRestConverter = require('./lib/tx-to-rest-converter.js');
var validator = require('./lib/schema-validator.js');
var bignum = require('bignumber.js');
var validate = require('./lib/validate');
var createOrderTransaction = require('./transaction').createOrderTransaction;
var createOrderCancellationTransaction =
require('./transaction').createOrderCancellationTransaction;
var transact = require('./transact');
var InvalidRequestError = errors.InvalidRequestError;
var DefaultPageLimit = 200;
/**
* Get orders from the ripple network
*
* @query
* @param {String} [request.query.limit]
* - Set a limit to the number of results returned
* @param {String} [request.query.marker]
* - Used to paginate results
* @param {String} [request.query.ledger]
* - The ledger index to query against
* - (required if request.query.marker is present)
*
* @url
* @param {RippleAddress} request.params.account
* - The ripple address to query orders
*
*/
function getOrders(account, options, callback) {
var self = this;
validate.address(account);
validate.options(options);
function getAccountOrders(prevResult) {
var isAggregate = options.limit === 'all';
if (prevResult && (!isAggregate || !prevResult.marker)) {
return Promise.resolve(prevResult);
}
var promise = new Promise(function(resolve, reject) {
var accountOrdersRequest;
var marker;
var ledger;
var limit;
if (prevResult) {
marker = prevResult.marker;
limit = prevResult.limit;
ledger = prevResult.ledger_index;
} else {
marker = options.marker;
limit = validator.isValid(options.limit, 'UINT32') ?
Number(options.limit) : DefaultPageLimit;
ledger = utils.parseLedger(options.ledger);
}
accountOrdersRequest = self.remote.requestAccountOffers({
account: account,
marker: marker,
limit: limit,
ledger: ledger
});
accountOrdersRequest.once('error', reject);
accountOrdersRequest.once('success', function(nextResult) {
nextResult.offers = prevResult ?
nextResult.offers.concat(prevResult.offers) : nextResult.offers;
resolve(nextResult);
});
accountOrdersRequest.request();
});
return promise.then(getAccountOrders);
}
function getParsedOrders(offers) {
return _.reduce(offers, function(orders, off) {
var sequence = off.seq;
var type = off.flags & ripple.Remote.flags.offer.Sell ? 'sell' : 'buy';
var passive = (off.flags & ripple.Remote.flags.offer.Passive) !== 0;
var taker_gets = utils.parseCurrencyAmount(off.taker_gets);
var taker_pays = utils.parseCurrencyAmount(off.taker_pays);
orders.push({
type: type,
taker_gets: taker_gets,
taker_pays: taker_pays,
sequence: sequence,
passive: passive
});
return orders;
}, []);
}
function respondWithOrders(result) {
var promise = new Promise(function(resolve) {
var orders = {};
if (result.marker) {
orders.marker = result.marker;
}
orders.limit = result.limit;
orders.ledger = result.ledger_index;
orders.validated = result.validated;
orders.orders = getParsedOrders(result.offers);
resolve(callback(null, orders));
});
return promise;
}
getAccountOrders()
.then(respondWithOrders)
.catch(callback);
}
/**
* Submit an order to the ripple network
*
* More information about order flags can be found at
* https://ripple.com/build/transactions/#offercreate-flags
*
* @body
* @param {Order} request.body.order
* - Object that holds information about the order
* @param {String "buy"|"sell"} request.body.order.type
* - Choose whether to submit a buy or sell order
* @param {Boolean} [request.body.order.passive]
* - Set whether order is passive
* @param {Boolean} [request.body.order.immediate_or_cancel]
* - Set whether order is immediate or cancel
* @param {Boolean} [request.body.order.fill_or_kill]
* - Set whether order is fill or kill
* @param {String} request.body.order.taker_gets
* - Amount of a currency the taker receives for consuming this order
* @param {String} request.body.order.taker_pays
* - Amount of a currency the taker must pay for consuming this order
* @param {String} request.body.secret
* - YOUR secret key. Do NOT submit to an unknown ripple-rest server
*
* @query
* @param {String "true"|"false"} request.query.validated
* - used to force request to wait until rippled has finished
* - validating the submitted transaction
*/
function placeOrder(account, order, secret, options, callback) {
var transaction = createOrderTransaction(account, order);
var converter = TxToRestConverter.parseSubmitOrderFromTx;
transact(transaction, this, secret, options, converter, callback);
}
/**
* Cancel an order in the ripple network
*
* @url
* @param {Number String} request.params.sequence
* - sequence number of order to cancel
*
* @query
* @param {String "true"|"false"} request.query.validated
* - used to force request to wait until rippled has finished
* validating the submitted transaction
*/
function cancelOrder(account, sequence, secret, options, callback) {
var transaction = createOrderCancellationTransaction(account, sequence);
var converter = TxToRestConverter.parseCancelOrderFromTx;
transact(transaction, this, secret, options, converter, callback);
}
/**
* Get the most recent spapshot of the order book for a currency pair
*
* @url
* @param {RippleAddress} request.params.account
* - The ripple address to use as point-of-view
* (returns unfunded orders for this account)
* @param {String ISO 4217 Currency Code + RippleAddress} request.params.base
* - Base currency as currency+issuer
* @param {String ISO 4217 Currency Code + RippleAddress}
* request.params.counter - Counter currency as currency+issuer
*
* @query
* @param {String} [request.query.limit]
* - Set a limit to the number of results returned
*
* @param {Express.js Request} request
*/
function getOrderBook(account, base, counter, options, callback) {
var self = this;
var params = _.merge(options, {
validated: true,
order_book: base + '/' + counter,
base: utils.parseCurrencyQuery(base),
counter: utils.parseCurrencyQuery(counter)
});
validate.address(account);
validate.orderbook(params);
validate.options(options);
function getLastValidatedLedger(parameters) {
var promise = new Promise(function(resolve, reject) {
var ledgerRequest = self.remote.requestLedger('validated');
ledgerRequest.once('success', function(res) {
parameters.ledger = res.ledger.ledger_index;
resolve(parameters);
});
ledgerRequest.once('error', reject);
ledgerRequest.request();
});
return promise;
}
function getBookOffers(taker_gets, taker_pays, parameters) {
var promise = new Promise(function(resolve, reject) {
var bookOffersRequest = self.remote.requestBookOffers({
taker_gets: {currency: taker_gets.currency,
issuer: taker_gets.counterparty},
taker_pays: {currency: taker_pays.currency,
issuer: taker_pays.counterparty},
ledger: parameters.ledger,
limit: parameters.limit,
taker: account
});
bookOffersRequest.once('success', resolve);
bookOffersRequest.once('error', reject);
bookOffersRequest.request();
});
return promise;
}
function getBids(parameters) {
var taker_gets = parameters.counter;
var taker_pays = parameters.base;
return getBookOffers(taker_gets, taker_pays, parameters);
}
function getAsks(parameters) {
var taker_gets = parameters.base;
var taker_pays = parameters.counter;
return getBookOffers(taker_gets, taker_pays, parameters);
}
function getBidsAndAsks(parameters) {
return Promise.join(
getBids(parameters),
getAsks(parameters),
function(bids, asks) {
return [bids, asks, parameters];
}
);
}
function getParsedBookOffers(offers, isAsk) {
return offers.reduce(function(orderBook, off) {
var price;
var order_maker = off.Account;
var sequence = off.Sequence;
// Transaction Flags
var passive = (off.Flags & ripple.Remote.flags.offer.Passive) !== 0;
var sell = (off.Flags & ripple.Remote.flags.offer.Sell) !== 0;
var taker_gets_total = utils.parseCurrencyAmount(off.TakerGets);
var taker_gets_funded = off.taker_gets_funded ?
utils.parseCurrencyAmount(off.taker_gets_funded) : taker_gets_total;
var taker_pays_total = utils.parseCurrencyAmount(off.TakerPays);
var taker_pays_funded = off.taker_pays_funded ?
utils.parseCurrencyAmount(off.taker_pays_funded) : taker_pays_total;
if (isAsk) {
price = {
currency: taker_pays_total.currency,
counterparty: taker_pays_total.counterparty,
value: bignum(taker_pays_total.value).div(
bignum(taker_gets_total.value))
};
} else {
price = {
currency: taker_gets_total.currency,
counterparty: taker_gets_total.counterparty,
value: bignum(taker_gets_total.value).div(
bignum(taker_pays_total.value))
};
}
price.value = price.value.toString();
orderBook.push({
price: price,
taker_gets_funded: taker_gets_funded,
taker_gets_total: taker_gets_total,
taker_pays_funded: taker_pays_funded,
taker_pays_total: taker_pays_total,
order_maker: order_maker,
sequence: sequence,
passive: passive,
sell: sell
});
return orderBook;
}, []);
}
function respondWithOrderBook(bids, asks, parameters) {
var promise = new Promise(function(resolve) {
var orderBook = {
order_book: parameters.order_book,
ledger: parameters.ledger,
validated: parameters.validated,
bids: getParsedBookOffers(bids.offers),
asks: getParsedBookOffers(asks.offers, true)
};
resolve(callback(null, orderBook));
});
return promise;
}
getLastValidatedLedger(params)
.then(getBidsAndAsks)
.spread(respondWithOrderBook)
.catch(callback);
}
/**
* Get an Order transaction (`OfferCreate` or `OfferCancel`)
*
* @url
* @param {RippleAddress} request.params.account
* @param {String} request.params.identifier
*
* @param {Express.js Request} request
*/
function getOrder(account, identifier, callback) {
validate.address(account);
validate.identifier(identifier);
var txRequest = this.remote.requestTx({
hash: identifier
});
txRequest.once('error', callback);
txRequest.once('transaction', function(response) {
if (response.TransactionType !== 'OfferCreate'
&& response.TransactionType !== 'OfferCancel') {
callback(new InvalidRequestError('Invalid parameter: identifier. '
+ 'The transaction corresponding to the given identifier '
+ 'is not an order'));
} else {
var options = {
account: account,
identifier: identifier
};
asyncify(TxToRestConverter.parseOrderFromTx)(response, options, callback);
}
});
txRequest.request();
}
module.exports = {
getOrders: getOrders,
placeOrder: utils.wrapCatch(placeOrder),
cancelOrder: utils.wrapCatch(cancelOrder),
getOrderBook: getOrderBook,
getOrder: getOrder
};