From 51e8f9a87abe42d6b19390400f34de48f2356368 Mon Sep 17 00:00:00 2001 From: Ivan Tivonenko Date: Mon, 14 Sep 2015 22:41:50 +0300 Subject: [PATCH] make Remote.getLedgerSequence asynchronous --- src/api/common/utils.js | 3 +- src/api/ledger/balances.js | 21 ++++++++-- src/api/ledger/orders.js | 17 ++++++-- src/api/ledger/transaction.js | 19 +++++++-- src/api/ledger/trustlines.js | 17 ++++++-- src/api/ledger/utils.js | 2 +- src/api/server/server.js | 32 ++++++++++---- src/api/transaction/utils.js | 78 +++++++++++++++++++++++------------ src/core/remote.js | 58 +++++++++++++++++++++++++- src/core/rippleerror.js | 19 ++++++--- test/api-test.js | 25 +++++++---- 11 files changed, 225 insertions(+), 66 deletions(-) diff --git a/src/api/common/utils.js b/src/api/common/utils.js index bb231ef6..30f49874 100644 --- a/src/api/common/utils.js +++ b/src/api/common/utils.js @@ -71,7 +71,8 @@ function composeAsync(wrapper: Wrapper, callback: Callback): Callback { function convertErrors(callback: Callback): () => void { return function(error, data) { if (error && !(error instanceof errors.RippleError)) { - const error_ = new errors.RippleError(error); + const message = _.get(error, ['remote', 'error_message'], error.message); + const error_ = new errors.RippleError(message); error_.data = data; callback(error_, data); } else if (error) { diff --git a/src/api/ledger/balances.js b/src/api/ledger/balances.js index d05704da..fba119a5 100644 --- a/src/api/ledger/balances.js +++ b/src/api/ledger/balances.js @@ -7,6 +7,9 @@ const getTrustlines = require('./trustlines'); const validate = utils.common.validate; const composeAsync = utils.common.composeAsync; const convertErrors = utils.common.convertErrors; +import type {Remote} from '../../core/remote'; +import type {GetLedgerSequenceCallback} from '../../core/remote'; + function getTrustlineBalanceAmount(trustline) { return { @@ -31,14 +34,26 @@ function getTrustlinesAsync(account, options, callback) { .catch(callback); } +function getLedgerVersion(remote: Remote, optionValue?: number, + callback: GetLedgerSequenceCallback +) { + if (typeof optionValue === 'number') { + callback(null, optionValue); + } else { + remote.getLedgerSequence(callback); + } +} + + function getBalancesAsync(account, options, callback) { validate.address(account); validate.getBalancesOptions(options); - const ledgerVersion = options.ledgerVersion - || this.remote.getLedgerSequence(); async.parallel({ - xrp: _.partial(utils.getXRPBalance, this.remote, account, ledgerVersion), + xrp: async.compose( + _.partial(utils.getXRPBalance, this.remote, account), + _.partial(getLedgerVersion, this.remote, options.ledgerVersion) + ), trustlines: _.partial(getTrustlinesAsync.bind(this), account, options) }, composeAsync(formatBalances, convertErrors(callback))); } diff --git a/src/api/ledger/orders.js b/src/api/ledger/orders.js index 9a765237..cfb507ec 100644 --- a/src/api/ledger/orders.js +++ b/src/api/ledger/orders.js @@ -26,10 +26,21 @@ function getOrdersAsync(account, options, callback) { validate.address(account); validate.getOrdersOptions(options); - const ledgerVersion = options.ledgerVersion - || this.remote.getLedgerSequence(); + if (!options.ledgerVersion) { + const self = this; + this.remote.getLedgerSequence((err, seq) => { + if (err) { + convertErrors(callback)(err); + return; + } + const newOptions = _.extend(options, {ledgerVersion: seq}); + getOrdersAsync.call(self, account, newOptions, callback); + }); + return; + } + const getter = _.partial(requestAccountOffers, this.remote, account, - ledgerVersion); + options.ledgerVersion); utils.getRecursive(getter, options.limit, composeAsync((orders) => _.sortBy(orders, (order) => order.properties.sequence), callback)); diff --git a/src/api/ledger/transaction.js b/src/api/ledger/transaction.js index 126dd353..e25a65a7 100644 --- a/src/api/ledger/transaction.js +++ b/src/api/ledger/transaction.js @@ -52,10 +52,10 @@ function getTransactionAsync(identifier: string, options: TransactionOptions, validate.getTransactionOptions(options); const remote = this.remote; - const maxLedgerVersion = - options.maxLedgerVersion || remote.getLedgerSequence(); - function callbackWrapper(error_?: Error, tx?: Object) { + function callbackWrapper(error_?: Error, tx?: Object, + maxLedgerVersion?: number + ) { let error = error_; if (!error && tx && tx.validated !== true) { @@ -89,11 +89,22 @@ function getTransactionAsync(identifier: string, options: TransactionOptions, } } + + /* eslint-disable no-unused-vars, handle-callback-err */ + function callbackWrapper2(error_?: Error, tx?: Object) { + remote.getLedgerSequence(function(err?, seq: number) { + const maxLedgerVersion = Math.min(options.maxLedgerVersion || Infinity, + seq); + callbackWrapper(error_, tx, maxLedgerVersion); + }); + } + /* eslint-enable no-unused-vars, handle-callback-err */ + async.waterfall([ _.partial(remote.requestTx.bind(remote), {hash: identifier, binary: false}), _.partial(attachTransactionDate, remote) - ], callbackWrapper); + ], callbackWrapper2); } function getTransaction(identifier: string, diff --git a/src/api/ledger/trustlines.js b/src/api/ledger/trustlines.js index eefd1fd8..74093c1a 100644 --- a/src/api/ledger/trustlines.js +++ b/src/api/ledger/trustlines.js @@ -42,10 +42,21 @@ function getTrustlinesAsync(account: string, options: {currency: string, validate.address(account); validate.getTrustlinesOptions(options); - const ledgerVersion = options.ledgerVersion - || this.remote.getLedgerSequence(); + if (!options.ledgerVersion) { + const self = this; + this.remote.getLedgerSequence(convertErrors(function(err?, seq: number) { + if (err) { + callback(err); + } else { + const newOptions = _.extend(options, {ledgerVersion: seq}); + getTrustlinesAsync.call(self, account, newOptions, callback); + } + })); + return; + } + const getter = _.partial(getAccountLines, this.remote, account, - ledgerVersion, options); + options.ledgerVersion, options); utils.getRecursive(getter, options.limit, callback); } diff --git a/src/api/ledger/utils.js b/src/api/ledger/utils.js index 75a966d2..dcb88ebd 100644 --- a/src/api/ledger/utils.js +++ b/src/api/ledger/utils.js @@ -103,7 +103,7 @@ function hasCompleteLedgerRange(remote: Remote, minLedgerVersion?: number, const firstLedgerVersion = 32570; // earlier versions have been lost return remote.getServer().hasLedgerRange( minLedgerVersion || firstLedgerVersion, - maxLedgerVersion || remote.getLedgerSequence()); + maxLedgerVersion || remote.getLedgerSequenceSync()); } function isPendingLedgerVersion(remote: Remote, maxLedgerVersion: ?number diff --git a/src/api/server/server.js b/src/api/server/server.js index 51417e27..9b2998ec 100644 --- a/src/api/server/server.js +++ b/src/api/server/server.js @@ -53,8 +53,7 @@ function getServerInfoAsync( ): void { this.remote.requestServerInfo((error, response) => { if (error) { - const message = - _.get(error, ['remote', 'error_message'], error.message); + const message = _.get(error, ['remote', 'error_message'], error.message); callback(new common.errors.RippledNetworkError(message)); } else { callback(null, @@ -63,28 +62,43 @@ function getServerInfoAsync( }); } -function getFee(): number { - return common.dropsToXrp(this.remote.createTransaction()._computeFee()); +function getFee(): ?number { + if (!this.remote._servers.length) { + throw new common.errors.RippledNetworkError('No servers available.'); + } + const fee = this.remote.createTransaction()._computeFee(); + if (typeof fee !== 'string') { + return undefined; + } + return common.dropsToXrp(fee); } -function getLedgerVersion(): number { - return this.remote.getLedgerSequence(); +function getLedgerVersion(): Promise { + return common.promisify(this.remote.getLedgerSequence).call(this.remote); } function connect(): Promise { return common.promisify(callback => { - this.remote.connect(() => callback(null)); + try { + this.remote.connect(() => callback(null)); + } catch(error) { + callback(new common.errors.RippledNetworkError(error.message)); + } })(); } function disconnect(): Promise { return common.promisify(callback => { - this.remote.disconnect(() => callback(null)); + try { + this.remote.disconnect(() => callback(null)); + } catch(error) { + callback(new common.errors.RippledNetworkError(error.message)); + } })(); } function getServerInfo(): Promise { - return common.promisify(getServerInfoAsync.bind(this))(); + return common.promisify(getServerInfoAsync).call(this); } function rippleTimeToISO8601(rippleTime: string): string { diff --git a/src/api/transaction/utils.js b/src/api/transaction/utils.js index a72b72fb..0512360d 100644 --- a/src/api/transaction/utils.js +++ b/src/api/transaction/utils.js @@ -1,8 +1,10 @@ /* @flow */ 'use strict'; const _ = require('lodash'); +const async = require('async'); const BigNumber = require('bignumber.js'); const common = require('../common'); +const composeAsync = common.composeAsync; function setTransactionBitFlags(transaction: any, values: any, flags: any ): void { @@ -19,9 +21,11 @@ function setTransactionBitFlags(transaction: any, values: any, flags: any } } -function getFeeDrops(remote) { +function getFeeDrops(remote, callback) { const feeUnits = 10; // all transactions currently have a fee of 10 fee units - return remote.feeTx(feeUnits).to_text(); + remote.feeTxAsync(feeUnits, (err, data) => { + callback(err, data ? data.to_text() : undefined); + }); } function formatPrepareResponse(txJSON) { @@ -39,42 +43,64 @@ function formatPrepareResponse(txJSON) { type Callback = (err: ?(typeof Error), data: {txJSON: string, instructions: any}) => void; function prepareTransaction(transaction: any, remote: any, instructions: any, - callback: Callback): void { + callback: Callback +): void { common.validate.instructions(instructions); transaction.complete(); const account = transaction.getAccount(); const txJSON = transaction.tx_json; - if (instructions.maxLedgerVersion !== undefined) { - txJSON.LastLedgerSequence = parseInt(instructions.maxLedgerVersion, 10); - } else { - const offset = instructions.maxLedgerVersionOffset !== undefined ? - parseInt(instructions.maxLedgerVersionOffset, 10) : 3; - txJSON.LastLedgerSequence = remote.getLedgerSequence() + offset; - } - if (instructions.fee !== undefined) { - txJSON.Fee = common.xrpToDrops(instructions.fee); - } else { - const serverFeeDrops = getFeeDrops(remote); - if (instructions.maxFee !== undefined) { - const maxFeeDrops = common.xrpToDrops(instructions.maxFee); - txJSON.Fee = BigNumber.min(serverFeeDrops, maxFeeDrops).toString(); + function prepare1(callback_) { + if (instructions.maxLedgerVersion !== undefined) { + txJSON.LastLedgerSequence = parseInt(instructions.maxLedgerVersion, 10); + callback_(); } else { - txJSON.Fee = serverFeeDrops; + const offset = instructions.maxLedgerVersionOffset !== undefined ? + parseInt(instructions.maxLedgerVersionOffset, 10) : 3; + remote.getLedgerSequence((error, seq) => { + txJSON.LastLedgerSequence = seq + offset; + callback_(error); + }); } } - if (instructions.sequence !== undefined) { - txJSON.Sequence = parseInt(instructions.sequence, 10); - callback(null, formatPrepareResponse(txJSON)); - } else { - remote.findAccount(account).getNextSequence(function(error, sequence) { - txJSON.Sequence = sequence; - callback(error, formatPrepareResponse(txJSON)); - }); + function prepare2(callback_) { + if (instructions.fee !== undefined) { + txJSON.Fee = common.xrpToDrops(instructions.fee); + callback_(); + } else { + getFeeDrops(remote, composeAsync((serverFeeDrops) => { + if (instructions.maxFee !== undefined) { + const maxFeeDrops = common.xrpToDrops(instructions.maxFee); + txJSON.Fee = BigNumber.min(serverFeeDrops, maxFeeDrops).toString(); + } else { + txJSON.Fee = serverFeeDrops; + } + }, callback_)); + } } + + function prepare3(callback_) { + if (instructions.sequence !== undefined) { + txJSON.Sequence = parseInt(instructions.sequence, 10); + callback_(null, formatPrepareResponse(txJSON)); + } else { + remote.findAccount(account).getNextSequence(function(error, sequence) { + txJSON.Sequence = sequence; + callback_(error, formatPrepareResponse(txJSON)); + }); + } + } + + async.series([ + prepare1, + prepare2, + prepare3 + ], common.convertErrors(function(error, results) { + callback(error, results && results[2]); + })); } module.exports = { diff --git a/src/core/remote.js b/src/core/remote.js index 638def41..5804c479 100644 --- a/src/core/remote.js +++ b/src/core/remote.js @@ -36,6 +36,8 @@ const utils = require('./utils'); const hashprefixes = require('./hashprefixes'); const log = require('./log').internal.sub('remote'); +export type GetLedgerSequenceCallback = (err?: ?Error, index?: number) => void; + /** * Interface to manage connections to rippled servers * @@ -518,7 +520,35 @@ Remote.prototype._handleMessage = function(message, server) { } }; -Remote.prototype.getLedgerSequence = function() { +/** + * + * @param {Function} [callback] + * @api public + */ + +Remote.prototype.getLedgerSequence = function(callback = function() {}) { + if (!this._servers.length) { + callback(new Error('No servers available.')); + return; + } + + if (_.isFinite(this._ledger_current_index)) { + // the "current" ledger is the one after the most recently closed ledger + callback(null, this._ledger_current_index - 1); + } else { + const self = this; + this.once('ledger_closed', function() { + callback(null, self._ledger_current_index - 1); + }); + } +}; + +/** + * + * @api private + */ + +Remote.prototype.getLedgerSequenceSync = function(): number { if (!this._ledger_current_index) { throw new Error('Ledger sequence has not yet been initialized'); } @@ -2307,6 +2337,32 @@ Remote.prototype.feeTx = function(units) { return server._feeTx(units); }; +/** + * Same as feeTx, but will wait to connect to server if currently + * disconnected. + * + * @param {Number} fee units + * @param {Function} callback + */ + +Remote.prototype.feeTxAsync = function(units, callback) { + if (!this._servers.length) { + callback(new Error('No servers available.')); + return; + } + + let server = this.getServer(); + + if (!server) { + this.once('connected', () => { + server = this.getServer(); + callback(null, server._feeTx(units)); + }); + } else { + callback(null, server._feeTx(units)); + } +}; + /** * Get the current recommended transaction fee unit. * diff --git a/src/core/rippleerror.js b/src/core/rippleerror.js index 08d647f1..45096d17 100644 --- a/src/core/rippleerror.js +++ b/src/core/rippleerror.js @@ -1,3 +1,5 @@ +'use strict'; + const util = require('util'); const _ = require('lodash'); @@ -18,18 +20,23 @@ function RippleError(code?: any, message?: string) { } } - this.engine_result = this.result = (this.result || this.engine_result || this.error || 'Error'); - this.engine_result_message = this.result_message = (this.result_message || this.engine_result_message || this.error_message || 'Error'); + this.engine_result = this.result = (this.result || this.engine_result || + this.error || 'Error'); + this.engine_result_message = this.result_message = (this.result_message || + this.engine_result_message || this.error_message || 'Error'); this.message = this.result_message; let stack; - if (!!Error.captureStackTrace) { + if (Boolean(Error.captureStackTrace)) { Error.captureStackTrace(this, code || this); - } else if ((stack = new Error().stack)) { - this.stack = stack; + } else { + stack = new Error().stack; + if (Boolean(stack)) { + this.stack = stack; + } } -}; +} util.inherits(RippleError, Error); diff --git a/test/api-test.js b/test/api-test.js index 9e2f4225..a050c4d3 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -52,13 +52,15 @@ describe('RippleAPI', function() { }); it('preparePayment with all options specified', function() { - const localInstructions = { - maxLedgerVersion: this.api.getLedgerVersion() + 100, - fee: '0.000012' - }; - return this.api.preparePayment( - address, requests.preparePaymentAllOptions, localInstructions).then( - _.partial(checkResult, responses.preparePaymentAllOptions, 'prepare')); + return this.api.getLedgerVersion().then((ver) => { + const localInstructions = { + maxLedgerVersion: ver + 100, + fee: '0.000012' + }; + return this.api.preparePayment( + address, requests.preparePaymentAllOptions, localInstructions).then( + _.partial(checkResult, responses.preparePaymentAllOptions, 'prepare')); + }); }); it('preparePayment without counterparty set', function() { @@ -637,8 +639,13 @@ describe('RippleAPI', function() { }); }); - it('getLedgerVersion', function() { - assert.strictEqual(this.api.getLedgerVersion(), 8819951); + it('getLedgerVersion', function(done) { + this.api.getLedgerVersion().then((ver) => { + assert.strictEqual(ver, 8819951); + done(); + }, (err) => { + done(err); + }); }); it('getLedger', function() {