Add getTrustlines and unit test

This commit is contained in:
Chris Clark
2015-06-18 17:30:11 -07:00
parent 1b3be55711
commit d92fbfb7aa
10 changed files with 230 additions and 171 deletions

View File

@@ -10,13 +10,11 @@
"type": "integer", "type": "integer",
"minimum": 1 "minimum": 1
}, },
"ledgerVersion": { "ledgerHash": {
"anyOf": [ "type": "string",
{"enum": ["current", "closed", "validated"]}, "format": "ledgerHash"
{"$ref": "ledgerVersion"},
{"type": "string", "format": "ledgerHash"}
]
}, },
"ledgerVersion": {"$ref": "ledgerVersion"},
"minLedgerVersion": {"$ref": "ledgerVersion"}, "minLedgerVersion": {"$ref": "ledgerVersion"},
"maxLedgerVersion": {"$ref": "ledgerVersion"}, "maxLedgerVersion": {"$ref": "ledgerVersion"},
"marker": { "marker": {

View File

@@ -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;

View File

@@ -13,7 +13,7 @@ function parseTrustline(tx: Object): Object {
counterparty: tx.LimitAmount.issuer, counterparty: tx.LimitAmount.issuer,
qualityIn: tx.QualityIn, qualityIn: tx.QualityIn,
qualityOut: tx.QualityOut, qualityOut: tx.QualityOut,
allowRippling: (tx.Flags & flags.NoRipple) === 0, disableRippling: (tx.Flags & flags.NoRipple) !== 0,
frozen: (tx.Flags & flags.SetFreeze) !== 0, frozen: (tx.Flags & flags.SetFreeze) !== 0,
authorized: (tx.Flags & flags.SetAuth) !== 0 authorized: (tx.Flags & flags.SetAuth) !== 0
}; };

View File

@@ -81,25 +81,6 @@ function parseAccountTxTransaction(tx) {
return parseTransaction(tx.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) { function transactionFilter(address, filters, tx) {
if (filters.excludeFailures && tx.outcome.result !== 'tesSUCCESS') { if (filters.excludeFailures && tx.outcome.result !== 'tesSUCCESS') {
@@ -117,27 +98,25 @@ function transactionFilter(address, filters, tx) {
return true; return true;
} }
function getAccountTransactionsRecursive( function getAccountTx(remote, address, options, marker, limit, callback) {
remote, address, limit, marker, options, callback) { const params = {
getAccountTx(remote, address, limit, marker, options, (error, data) => { account: address,
if (error) { ledger_index_min: options.ledgerVersion || options.minLedgerVersion || -1,
callback(error); ledger_index_max: options.ledgerVersion || options.maxLedgerVersion || -1,
return; forward: options.earliestFirst,
} binary: options.binary,
const filter = _.partial(transactionFilter, address, options); limit: Math.min(limit || DEFAULT_LIMIT, 10),
const unfilteredTransactions = data.transactions; marker: marker
const filteredTransactions = unfilteredTransactions.filter(filter); };
const isExhausted = unfilteredTransactions.length === 0;
if (!isExhausted && filteredTransactions.length < limit) { remote.requestAccountTx(params, (error, data) => {
const remaining = limit - filteredTransactions.length; return error ? callback(error) : callback(null, {
getAccountTransactionsRecursive( marker: data.marker,
remote, address, remaining, data.marker, options, (_err, txs) => { results: data.transactions
return error ? callback(_err) : .filter((tx) => tx.validated)
callback(null, filteredTransactions.concat(txs)); .map(parseAccountTxTransaction)
.filter(_.partial(transactionFilter, address, options))
}); });
} else {
callback(null, filteredTransactions.slice(0, limit));
}
}); });
} }
@@ -147,9 +126,9 @@ function getAccountTransactions(address, options, callback) {
const limit = options.limit || DEFAULT_LIMIT; const limit = options.limit || DEFAULT_LIMIT;
const compare = options.earliestFirst ? utils.compareTransactions : const compare = options.earliestFirst ? utils.compareTransactions :
_.rearg(utils.compareTransactions, 1, 0); _.rearg(utils.compareTransactions, 1, 0);
getAccountTransactionsRecursive( const getter = _.partial(getAccountTx, this.remote, address, options);
this.remote, address, limit, null, options, (error, transactions) => { utils.getRecursive(getter, limit, (error, data) => {
return error ? callback(error) : callback(null, transactions.sort(compare)); return error ? callback(error) : callback(null, data.sort(compare));
}); });
} }

View File

@@ -1,127 +1,48 @@
/* globals Promise: true */
/* eslint-disable valid-jsdoc */
'use strict'; 'use strict';
const _ = require('lodash');
const utils = require('./utils'); const utils = require('./utils');
const validate = utils.common.validate; 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;
}
/** function getAccountLines(remote, address, ledgerVersion, options, marker, limit,
* Retrieves all trustlines for a given account callback) {
* const requestOptions = {
* Notes: account: address,
* In order to use paging, you must provide at least ledger as a query parameter ledger: ledgerVersion,
* Additionally, any limit lower than 10 will be bumped up to 10. marker: marker,
* limit: Math.max(limit, 10),
* @url peer: options.counterparty
* @param {String} request.params.account - account to retrieve trustlines for };
*
* @query const currency = options.currency ? options.currency.toUpperCase() : null;
* @param {String ISO 4217 Currency Code} [request.query.currency] remote.requestAccountLines(requestOptions, (error, data) => {
* - only request trustlines with given currency return error ? callback(error) :
* @param {RippleAddress} [request.query.counterparty] callback(null, {
* - only request trustlines with given counterparty marker: data.marker,
* @param {String} [request.query.marker] - start position in response paging results: data.lines.map(parseAccountTrustline)
* @param {Number String} [request.query.limit] - max results per response .filter(_.partial(currencyFilter, currency))
* @param {Number String} [request.query.ledger] - identifier });
* });
*/ }
function getTrustLines(account, options, callback) {
/*:: type Options = {currency: string, counterparty: string,
limit: number, ledgerVersion: number} */
function getTrustlines(account: string, options: Options,
callback: () => void): void {
validate.address(account); validate.address(account);
validate.options(options); validate.options(options);
const self = this; const defaultLimit = 100;
const limit = options.limit || defaultLimit;
const currencyRE = new RegExp(options.currency ? const ledgerVersion = options.ledgerVersion
('^' + options.currency.toUpperCase() + '$') : /./); || this.remote.getLedgerSequence();
const getter = _.partial(getAccountLines, this.remote, account,
function getAccountLines(prevResult) { ledgerVersion, options);
const isAggregate = options.limit === undefined; utils.getRecursive(getter, limit, callback);
if (prevResult && (!isAggregate || !prevResult.marker)) {
return Promise.resolve(prevResult);
} }
const promise = new Promise(function(resolve, reject) { module.exports.getTrustlines = getTrustlines;
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);
}
module.exports.getTrustLines = getTrustLines;

View File

@@ -6,6 +6,29 @@ const asyncify = require('simple-asyncify');
const common = require('../common'); const common = require('../common');
const ripple = common.core; 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) { function renameCounterpartyToIssuer(amount) {
if (amount === undefined) { if (amount === undefined) {
return undefined; return undefined;
@@ -99,6 +122,7 @@ module.exports = {
renameCounterpartyToIssuer: renameCounterpartyToIssuer, renameCounterpartyToIssuer: renameCounterpartyToIssuer,
renameCounterpartyToIssuerInOrder: renameCounterpartyToIssuerInOrder, renameCounterpartyToIssuerInOrder: renameCounterpartyToIssuerInOrder,
attachDate: attachDate, attachDate: attachDate,
getRecursive: getRecursive,
wrapCatch: common.wrapCatch, wrapCatch: common.wrapCatch,
common: common common: common
}; };

View File

@@ -10,7 +10,7 @@ const TrustSetFlags = {
frozed: {set: 'SetFreeze', unset: 'ClearFreeze'} frozed: {set: 'SetFreeze', unset: 'ClearFreeze'}
}; };
function createTrustLineTransaction(account, trustline) { function createTrustlineTransaction(account, trustline) {
validate.address(account); validate.address(account);
validate.trustline(trustline); validate.trustline(trustline);
@@ -27,9 +27,9 @@ function createTrustLineTransaction(account, trustline) {
return transaction; return transaction;
} }
function prepareTrustLine(account, trustline, instructions, callback) { function prepareTrustline(account, trustline, instructions, callback) {
const transaction = createTrustLineTransaction(account, trustline); const transaction = createTrustlineTransaction(account, trustline);
utils.createTxJSON(transaction, this.remote, instructions, callback); utils.createTxJSON(transaction, this.remote, instructions, callback);
} }
module.exports = utils.wrapCatch(prepareTrustLine); module.exports = utils.wrapCatch(prepareTrustline);

View File

@@ -24,6 +24,7 @@ const submitResponse = require('./fixtures/submit-response');
const transactionResponse = require('./fixtures/transaction-response'); const transactionResponse = require('./fixtures/transaction-response');
const accountTransactionsResponse = const accountTransactionsResponse =
require('./fixtures/account-transactions-response'); require('./fixtures/account-transactions-response');
const trustlinesResponse = require('./fixtures/trustlines-response');
function checkResult(expected, done, error, response) { function checkResult(expected, done, error, response) {
if (error) { if (error) {
@@ -99,4 +100,10 @@ describe('RippleAPI', function() {
this.api.getAccountTransactions(address, options, this.api.getAccountTransactions(address, options,
_.partial(checkResult, accountTransactionsResponse, done)); _.partial(checkResult, accountTransactionsResponse, done));
}); });
it('getTrustlines', function(done) {
const options = {currency: 'USD'};
this.api.getTrustlines(address, options,
_.partial(checkResult, trustlinesResponse, done));
});
}); });

View File

@@ -196,6 +196,7 @@ module.exports = function(request, options={}) {
status: 'success', status: 'success',
type: 'response', type: 'response',
result: { result: {
marker: request.marker === undefined ? 'ABC' : undefined,
transactions: [ transactions: [
{ {
ledger_index: 348860, ledger_index: 348860,

99
test/fixtures/trustlines-response.json vendored Normal file
View File

@@ -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"
}
}
]