diff --git a/src/api/common/schemas/options.json b/src/api/common/schemas/options.json index 42632ad6..1633d063 100644 --- a/src/api/common/schemas/options.json +++ b/src/api/common/schemas/options.json @@ -10,13 +10,11 @@ "type": "integer", "minimum": 1 }, - "ledgerVersion": { - "anyOf": [ - {"enum": ["current", "closed", "validated"]}, - {"$ref": "ledgerVersion"}, - {"type": "string", "format": "ledgerHash"} - ] + "ledgerHash": { + "type": "string", + "format": "ledgerHash" }, + "ledgerVersion": {"$ref": "ledgerVersion"}, "minLedgerVersion": {"$ref": "ledgerVersion"}, "maxLedgerVersion": {"$ref": "ledgerVersion"}, "marker": { diff --git a/src/api/ledger/parse/account-trustline.js b/src/api/ledger/parse/account-trustline.js new file mode 100644 index 00000000..2f69caf5 --- /dev/null +++ b/src/api/ledger/parse/account-trustline.js @@ -0,0 +1,30 @@ +'use strict'; +const utils = require('./utils'); + +// rippled 'account_lines' returns a different format for +// trustlines than 'tx' +function parseAccountTrustline(trustline) { + const specification = utils.removeUndefined({ + limit: trustline.limit, + currency: trustline.currency, + counterparty: trustline.account, + qualityIn: trustline.quality_in || undefined, + qualityOut: trustline.quality_out || undefined, + disableRippling: trustline.no_ripple, + frozen: trustline.freeze, + authorized: trustline.authorized + }); + // rippled doesn't provide the counterparty's qualities + const counterparty = utils.removeUndefined({ + limit: trustline.limit_peer, + disableRippling: trustline.no_ripple_peer, + frozen: trustline.freeze_peer, + authorized: trustline.peer_authorized + }); + const state = { + balance: trustline.balance + }; + return {specification, counterparty, state}; +} + +module.exports = parseAccountTrustline; diff --git a/src/api/ledger/parse/trustline.js b/src/api/ledger/parse/trustline.js index 70ca8c8b..b1298a0b 100644 --- a/src/api/ledger/parse/trustline.js +++ b/src/api/ledger/parse/trustline.js @@ -13,7 +13,7 @@ function parseTrustline(tx: Object): Object { counterparty: tx.LimitAmount.issuer, qualityIn: tx.QualityIn, qualityOut: tx.QualityOut, - allowRippling: (tx.Flags & flags.NoRipple) === 0, + disableRippling: (tx.Flags & flags.NoRipple) !== 0, frozen: (tx.Flags & flags.SetFreeze) !== 0, authorized: (tx.Flags & flags.SetAuth) !== 0 }; diff --git a/src/api/ledger/transactions.js b/src/api/ledger/transactions.js index 233f6a59..c5d0921f 100644 --- a/src/api/ledger/transactions.js +++ b/src/api/ledger/transactions.js @@ -81,25 +81,6 @@ function parseAccountTxTransaction(tx) { return parseTransaction(tx.tx); } -function getAccountTx(remote, address, limit, marker, options, callback) { - const params = { - account: address, - ledger_index_min: options.ledgerVersion || options.minLedgerVersion || -1, - ledger_index_max: options.ledgerVersion || options.maxLedgerVersion || -1, - forward: options.earliestFirst, - binary: options.binary, - limit: Math.min(limit || DEFAULT_LIMIT, 10), - marker: marker - }; - - remote.requestAccountTx(params, (error, data) => { - return error ? callback(error) : callback(null, { - transactions: data.transactions.filter((tx) => tx.validated) - .map(parseAccountTxTransaction), - marker: data.marker - }); - }); -} function transactionFilter(address, filters, tx) { if (filters.excludeFailures && tx.outcome.result !== 'tesSUCCESS') { @@ -117,27 +98,25 @@ function transactionFilter(address, filters, tx) { return true; } -function getAccountTransactionsRecursive( - remote, address, limit, marker, options, callback) { - getAccountTx(remote, address, limit, marker, options, (error, data) => { - if (error) { - callback(error); - return; - } - const filter = _.partial(transactionFilter, address, options); - const unfilteredTransactions = data.transactions; - const filteredTransactions = unfilteredTransactions.filter(filter); - const isExhausted = unfilteredTransactions.length === 0; - if (!isExhausted && filteredTransactions.length < limit) { - const remaining = limit - filteredTransactions.length; - getAccountTransactionsRecursive( - remote, address, remaining, data.marker, options, (_err, txs) => { - return error ? callback(_err) : - callback(null, filteredTransactions.concat(txs)); - }); - } else { - callback(null, filteredTransactions.slice(0, limit)); - } +function getAccountTx(remote, address, options, marker, limit, callback) { + const params = { + account: address, + ledger_index_min: options.ledgerVersion || options.minLedgerVersion || -1, + ledger_index_max: options.ledgerVersion || options.maxLedgerVersion || -1, + forward: options.earliestFirst, + binary: options.binary, + limit: Math.min(limit || DEFAULT_LIMIT, 10), + marker: marker + }; + + remote.requestAccountTx(params, (error, data) => { + return error ? callback(error) : callback(null, { + marker: data.marker, + results: data.transactions + .filter((tx) => tx.validated) + .map(parseAccountTxTransaction) + .filter(_.partial(transactionFilter, address, options)) + }); }); } @@ -147,9 +126,9 @@ function getAccountTransactions(address, options, callback) { const limit = options.limit || DEFAULT_LIMIT; const compare = options.earliestFirst ? utils.compareTransactions : _.rearg(utils.compareTransactions, 1, 0); - getAccountTransactionsRecursive( - this.remote, address, limit, null, options, (error, transactions) => { - return error ? callback(error) : callback(null, transactions.sort(compare)); + const getter = _.partial(getAccountTx, this.remote, address, options); + utils.getRecursive(getter, limit, (error, data) => { + return error ? callback(error) : callback(null, data.sort(compare)); }); } diff --git a/src/api/ledger/trustlines.js b/src/api/ledger/trustlines.js index 35484902..9e68d025 100644 --- a/src/api/ledger/trustlines.js +++ b/src/api/ledger/trustlines.js @@ -1,127 +1,48 @@ -/* globals Promise: true */ -/* eslint-disable valid-jsdoc */ 'use strict'; +const _ = require('lodash'); const utils = require('./utils'); const validate = utils.common.validate; +const parseAccountTrustline = require('./parse/account-trustline'); -const DefaultPageLimit = 200; +function currencyFilter(currency, trustline) { + return currency === null || trustline.specification.currency === currency; +} -/** - * Retrieves all trustlines for a given account - * - * Notes: - * In order to use paging, you must provide at least ledger as a query parameter - * Additionally, any limit lower than 10 will be bumped up to 10. - * - * @url - * @param {String} request.params.account - account to retrieve trustlines for - * - * @query - * @param {String ISO 4217 Currency Code} [request.query.currency] - * - only request trustlines with given currency - * @param {RippleAddress} [request.query.counterparty] - * - only request trustlines with given counterparty - * @param {String} [request.query.marker] - start position in response paging - * @param {Number String} [request.query.limit] - max results per response - * @param {Number String} [request.query.ledger] - identifier - * - */ -function getTrustLines(account, options, callback) { +function getAccountLines(remote, address, ledgerVersion, options, marker, limit, + callback) { + const requestOptions = { + account: address, + ledger: ledgerVersion, + marker: marker, + limit: Math.max(limit, 10), + peer: options.counterparty + }; + + const currency = options.currency ? options.currency.toUpperCase() : null; + remote.requestAccountLines(requestOptions, (error, data) => { + return error ? callback(error) : + callback(null, { + marker: data.marker, + results: data.lines.map(parseAccountTrustline) + .filter(_.partial(currencyFilter, currency)) + }); + }); +} + +/*:: type Options = {currency: string, counterparty: string, + limit: number, ledgerVersion: number} */ +function getTrustlines(account: string, options: Options, + callback: () => void): void { validate.address(account); validate.options(options); - const self = this; - - const currencyRE = new RegExp(options.currency ? - ('^' + options.currency.toUpperCase() + '$') : /./); - - function getAccountLines(prevResult) { - const isAggregate = options.limit === undefined; - if (prevResult && (!isAggregate || !prevResult.marker)) { - return Promise.resolve(prevResult); - } - - const promise = new Promise(function(resolve, reject) { - let accountLinesRequest; - let marker; - let ledger; - let limit; - - if (prevResult) { - marker = prevResult.marker; - limit = prevResult.limit; - ledger = prevResult.ledger_index; - } else { - marker = options.marker; - limit = options.limit || DefaultPageLimit; - ledger = utils.parseLedger(options.ledger); - } - - accountLinesRequest = self.remote.requestAccountLines({ - account: account, - marker: marker, - limit: limit, - ledger: ledger - }); - - if (options.counterparty) { - accountLinesRequest.message.peer = options.counterparty; - } - - accountLinesRequest.once('error', reject); - accountLinesRequest.once('success', function(nextResult) { - - const lines = [ ]; - nextResult.lines.forEach(function(line) { - if (!currencyRE.test(line.currency)) { - return; - } - lines.push({ - account: account, - counterparty: line.account, - currency: line.currency, - limit: line.limit, - reciprocated_limit: line.limit_peer, - account_allows_rippling: line.no_ripple ? !line.no_ripple : true, - counterparty_allows_rippling: line.no_ripple_peer - ? !line.no_ripple_peer : true, - account_trustline_frozen: line.freeze ? line.freeze : false, - counterparty_trustline_frozen: line.freeze_peer - ? line.freeze_peer : false - }); - }); - - nextResult.lines = prevResult ? prevResult.lines.concat(lines) : lines; - resolve([nextResult]); - }); - accountLinesRequest.request(); - }); - - return promise.spread(getAccountLines); - } - - function respondWithTrustlines(result) { - const promise = new Promise(function(resolve) { - const trustlines = {}; - - if (result.marker) { - trustlines.marker = result.marker; - } - - trustlines.limit = result.limit; - trustlines.ledger = result.ledger_index; - trustlines.validated = result.validated; - trustlines.trustlines = result.lines; - - resolve(callback(null, trustlines)); - }); - - return promise; - } - - getAccountLines() - .then(respondWithTrustlines) - .catch(callback); + const defaultLimit = 100; + const limit = options.limit || defaultLimit; + const ledgerVersion = options.ledgerVersion + || this.remote.getLedgerSequence(); + const getter = _.partial(getAccountLines, this.remote, account, + ledgerVersion, options); + utils.getRecursive(getter, limit, callback); } -module.exports.getTrustLines = getTrustLines; +module.exports.getTrustlines = getTrustlines; diff --git a/src/api/ledger/utils.js b/src/api/ledger/utils.js index 4704be0c..b6048853 100644 --- a/src/api/ledger/utils.js +++ b/src/api/ledger/utils.js @@ -6,6 +6,29 @@ const asyncify = require('simple-asyncify'); const common = require('../common'); const ripple = common.core; +// If the marker is omitted from a response, you have reached the end +// getter(marker, limit, callback), callback(error, {marker, results}) +function getRecursiveRecur(getter, marker, limit, callback) { + getter(marker, limit, (error, data) => { + if (error) { + return callback(error); + } + const remaining = limit - data.results.length; + if (remaining > 0 && data.marker !== undefined) { + getRecursiveRecur(getter, data.marker, remaining, (_error, results) => { + return _error ? callback(_error) : + callback(null, data.results.concat(results)); + }); + } else { + return callback(null, data.results.slice(0, limit)); + } + }); +} + +function getRecursive(getter, limit, callback) { + getRecursiveRecur(getter, undefined, limit, callback); +} + function renameCounterpartyToIssuer(amount) { if (amount === undefined) { return undefined; @@ -99,6 +122,7 @@ module.exports = { renameCounterpartyToIssuer: renameCounterpartyToIssuer, renameCounterpartyToIssuerInOrder: renameCounterpartyToIssuerInOrder, attachDate: attachDate, + getRecursive: getRecursive, wrapCatch: common.wrapCatch, common: common }; diff --git a/src/api/transaction/trustline.js b/src/api/transaction/trustline.js index c5d58e7e..e6aedcc6 100644 --- a/src/api/transaction/trustline.js +++ b/src/api/transaction/trustline.js @@ -10,7 +10,7 @@ const TrustSetFlags = { frozed: {set: 'SetFreeze', unset: 'ClearFreeze'} }; -function createTrustLineTransaction(account, trustline) { +function createTrustlineTransaction(account, trustline) { validate.address(account); validate.trustline(trustline); @@ -27,9 +27,9 @@ function createTrustLineTransaction(account, trustline) { return transaction; } -function prepareTrustLine(account, trustline, instructions, callback) { - const transaction = createTrustLineTransaction(account, trustline); +function prepareTrustline(account, trustline, instructions, callback) { + const transaction = createTrustlineTransaction(account, trustline); utils.createTxJSON(transaction, this.remote, instructions, callback); } -module.exports = utils.wrapCatch(prepareTrustLine); +module.exports = utils.wrapCatch(prepareTrustline); diff --git a/test/api-test.js b/test/api-test.js index 9b7424b7..ee5a8888 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -24,6 +24,7 @@ const submitResponse = require('./fixtures/submit-response'); const transactionResponse = require('./fixtures/transaction-response'); const accountTransactionsResponse = require('./fixtures/account-transactions-response'); +const trustlinesResponse = require('./fixtures/trustlines-response'); function checkResult(expected, done, error, response) { if (error) { @@ -99,4 +100,10 @@ describe('RippleAPI', function() { this.api.getAccountTransactions(address, options, _.partial(checkResult, accountTransactionsResponse, done)); }); + + it('getTrustlines', function(done) { + const options = {currency: 'USD'}; + this.api.getTrustlines(address, options, + _.partial(checkResult, trustlinesResponse, done)); + }); }); diff --git a/test/fixtures/acct-tx-response.js b/test/fixtures/acct-tx-response.js index 6ea7555a..dba4b9ea 100644 --- a/test/fixtures/acct-tx-response.js +++ b/test/fixtures/acct-tx-response.js @@ -196,6 +196,7 @@ module.exports = function(request, options={}) { status: 'success', type: 'response', result: { + marker: request.marker === undefined ? 'ABC' : undefined, transactions: [ { ledger_index: 348860, diff --git a/test/fixtures/trustlines-response.json b/test/fixtures/trustlines-response.json new file mode 100644 index 00000000..59902bfa --- /dev/null +++ b/test/fixtures/trustlines-response.json @@ -0,0 +1,99 @@ +[ + { + "specification": { + "limit": "5", + "currency": "USD", + "counterparty": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "disableRippling": true, + "frozen": true + }, + "counterparty": { + "limit": "0" + }, + "state": { + "balance": "2.497605752725159" + } + }, + { + "specification": { + "limit": "5000", + "currency": "USD", + "counterparty": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" + }, + "counterparty": { + "limit": "0" + }, + "state": { + "balance": "0" + } + }, + { + "specification": { + "limit": "1", + "currency": "USD", + "counterparty": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun" + }, + "counterparty": { + "limit": "0" + }, + "state": { + "balance": "1" + } + }, + { + "specification": { + "limit": "1", + "currency": "USD", + "counterparty": "r9vbV3EHvXWjSkeQ6CAcYVPGeq7TuiXY2X", + "disableRippling": true + }, + "counterparty": { + "limit": "0" + }, + "state": { + "balance": "0" + } + }, + { + "specification": { + "limit": "500", + "currency": "USD", + "counterparty": "rfF3PNkwkq1DygW2wum2HK3RGfgkJjdPVD", + "disableRippling": true + }, + "counterparty": { + "limit": "0" + }, + "state": { + "balance": "35" + } + }, + { + "specification": { + "limit": "0", + "currency": "USD", + "counterparty": "rE6R3DWF9fBD7CyiQciePF9SqK58Ubp8o2" + }, + "counterparty": { + "limit": "100", + "disableRippling": true + }, + "state": { + "balance": "0" + } + }, + { + "specification": { + "limit": "0", + "currency": "USD", + "counterparty": "rEhDDUUNxpXgEHVJtC2cjXAgyx5VCFxdMF", + "frozen": true + }, + "counterparty": { + "limit": "1" + }, + "state": { + "balance": "0" + } + } +]