diff --git a/docs/index.md b/docs/index.md index 7d64b9b3..e653167f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,6 +33,11 @@ - [Payment Channel Create](#payment-channel-create) - [Payment Channel Fund](#payment-channel-fund) - [Payment Channel Claim](#payment-channel-claim) +- [rippled APIs](#rippled-apis) + - [Listening to streams](#listening-to-streams) + - [request](#request) + - [hasNextPage](#hasnextpage) + - [requestNextPage](#requestnextpage) - [API Methods](#api-methods) - [connect](#connect) - [disconnect](#disconnect) @@ -84,7 +89,7 @@ # Introduction -RippleAPI is the official client library to the XRP Ledger. Currently, RippleAPI is only available in JavaScript. +RippleAPI (ripple-lib) is the official client library to the XRP Ledger. Currently, RippleAPI is only available in JavaScript. Using RippleAPI, you can: * [Query transactions from the XRP Ledger history](#gettransaction) @@ -93,8 +98,6 @@ Using RippleAPI, you can: * [Generate a new XRP Ledger Address](#generateaddress) * ... and [much more](#api-methods). -RippleAPI only provides access to *validated*, *immutable* transaction data. - ## Boilerplate Use the following [boilerplate code](https://en.wikipedia.org/wiki/Boilerplate_code) to wrap your custom code using RippleAPI. @@ -750,6 +753,185 @@ signature | string | *Optional* Signed claim authorizing withdrawal of XRP from ``` +# rippled APIs + +ripple-lib relies on [rippled APIs](https://ripple.com/build/rippled-apis/) for all online functionality. With ripple-lib version 1.0.0 and higher, you can easily access rippled APIs through ripple-lib. Use the `request()`, `hasNextPage()`, and `requestNextPage()` methods: +* Use `request()` to issue any `rippled` command, including `account_currencies`, `subscribe`, and `unsubscribe`. [Full list of API Methods](https://ripple.com/build/rippled-apis/#api-methods). +* Use `hasNextPage()` to determine whether a response has more pages. This is true when the response includes a [`marker` field](https://ripple.com/build/rippled-apis/#markers-and-pagination). +* Use `requestNextPage()` to request the next page of data. + +When using rippled APIs, [specify XRP amounts in drops](https://ripple.com/build/rippled-apis/#specifying-currency-amounts). 1 XRP = 1000000 drops. + +## Listening to streams + +The `rippled` server can push updates to your client when various events happen. Refer to [Subscriptions in the `rippled` API docs](https://ripple.com/build/rippled-apis/#subscriptions) for details. + +Note that the `streams` parameter for generic streams takes an array. For example, to subscribe to the `validations` stream, use `{ streams: [ 'validations' ] }`. + +The string names of some generic streams to subscribe to are in the table below. (Refer to `rippled` for an up-to-date list of streams.) + +Type | Description +---- | ----------- +`server` | Sends a message whenever the status of the `rippled` server (for example, network connectivity) changes. +`ledger` | Sends a message whenever the consensus process declares a new validated ledger. +`transactions` | Sends a message whenever a transaction is included in a closed ledger. +`transactions_proposed` | Sends a message whenever a transaction is included in a closed ledger, as well as some transactions that have not yet been included in a validated ledger and may never be. Not all proposed transactions appear before validation. Even some transactions that don't succeed are included in validated ledgers because they take the anti-spam transaction fee. +`validations` | Sends a message whenever the server receives a validation message, also called a validation vote, regardless of whether the server trusts the validator. +`manifests` | Sends a message whenever the server receives a manifest. +`peer_status` | (Admin-only) Information about connected peer `rippled` servers, especially with regards to the consensus process. + +When you subscribe to a stream, you must also listen to the relevant message type(s). Some of the available message types are in the table below. (Refer to `rippled` for an up-to-date list of message types.) + +Type | Description +---- | ----------- +`ledgerClosed` | Sent by the `ledger` stream when the consensus process declares a new fully validated ledger. The message identifies the ledger and provides some information about its contents. +`validationReceived` | Sent by the `validations` stream when the server receives a validation message, also called a validation vote, regardless of whether the server trusts the validator. +`manifestReceived` | Sent by the `manifests` stream when the server receives a manifest. +`transaction` | Sent by many subscriptions including `transactions`, `transactions_proposed`, `accounts`, `accounts_proposed`, and `book` (Order Book). See [Transaction Streams](https://ripple.com/build/rippled-apis/#transaction-streams) for details. +`peerStatusChange` | (Admin-only) Reports a large amount of information on the activities of other `rippled` servers to which the server is connected. + +To register your listener function, use `connection.on(type, handler)`. + +Here is an example of listening for transactions on given account(s): +``` +const account = 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn' // Replace with the account you want notifications for +api.connect().then(() => { // Omit this if you are already connected + + // 'transaction' can be replaced with the relevant `type` from the table above + api.connection.on('transaction', (event) => { + + // Do something useful with `event` + console.log(JSON.stringify(event, null, 2)) + }) + + api.request('subscribe', { + accounts: [ account ] + }).then(response => { + if (response.status === 'success') { + console.log('Successfully subscribed') + } + }).catch(error => { + // Handle `error` + }) +}) +``` + +The subscription ends when you unsubscribe or the WebSocket connection is closed. + +For full details, see [rippled Subscriptions](https://ripple.com/build/rippled-apis/#subscriptions). + +## request + +`request(command: string, options: object): Promise` + +Returns the response from invoking the specified command, with the specified options, on the connected rippled server. + +Refer to [rippled APIs](https://ripple.com/build/rippled-apis/) for commands and options. All XRP amounts must be specified in drops. One drop is equal to 0.000001 XRP. See [Specifying Currency Amounts](https://ripple.com/build/rippled-apis/#specifying-currency-amounts). + +Most commands return data for the `current` (in-progress, open) ledger by default. Do not rely on this. Always specify a ledger version in your request. In the example below, the 'validated' ledger is requested, which is the most recent ledger that has been validated by the whole network. See [Specifying Ledgers](https://ripple.com/build/rippled-apis/#specifying-ledgers). + +### Return Value + +This method returns a promise that resolves with the response from rippled. + +### Example + +```javascript +// Replace 'ledger' with your desired rippled command +return api.request('ledger', { + ledger_index: 'validated' +}).then(response => { + /* Do something useful with response */ + console.log(JSON.stringify(response, null, 2)) +}).catch(console.error); +``` + + +```json +{ + "ledger": { + "accepted": true, + "account_hash": "F9E9653EA76EA0AEA58AC98A8E19EDCEC8299C2940519A190674FFAED3639A1F", + "close_flags": 0, + "close_time": 577999430, + "close_time_human": "2018-Apr-25 19:23:50", + "close_time_resolution": 10, + "closed": true, + "hash": "450E5CB0A39495839DA9CD9A0FED74BD71CBB929423A907ADC00F14FC7E7F920", + "ledger_hash": "450E5CB0A39495839DA9CD9A0FED74BD71CBB929423A907ADC00F14FC7E7F920", + "ledger_index": "38217406", + "parent_close_time": 577999422, + "parent_hash": "B8B364C63EB9E13FDB89CB729FEF833089B8438CBEB8FC41744CB667209221B3", + "seqNum": "38217406", + "totalCoins": "99992286058637091", + "total_coins": "99992286058637091", + "transaction_hash": "5BDD3D2780C28FB2C91C3404BD8ED04786B764B1E18CF319888EDE2C09834726" + }, + "ledger_hash": "450E5CB0A39495839DA9CD9A0FED74BD71CBB929423A907ADC00F14FC7E7F920", + "ledger_index": 38217406, + "validated": true +} +``` + + +## hasNextPage + +`hasNextPage(currentResponse): boolean` + +Returns `true` when there are more pages available. + +When there are more results than contained in the response, the response includes a `marker` field. You can use this convenience method, or check for `marker` yourself. + +See [Markers and Pagination](https://ripple.com/build/rippled-apis/#markers-and-pagination). + +### Return Value + +This method returns `true` if `currentResponse` includes a `marker`. + +### Example + +```javascript +return api.request('ledger_data', { + ledger_index: 'validated' +}).then(response => { + /* Do something useful with response */ + + if (api.hasNextPage(response)) { + /* There are more pages available */ + } +}).catch(console.error); +``` + +## requestNextPage + +`requestNextPage(command: string, params: object = {}, currentResponse: object): Promise` + +Requests the next page of data. + +You can use this convenience method, or include `currentResponse.marker` in `params` yourself, when using `request`. + +See [Markers and Pagination](https://ripple.com/build/rippled-apis/#markers-and-pagination). + +### Return Value + +This method returns a promise that resolves with the next page of data from rippled. + +If the response does not have a next page, the promise will reject with `new errors.NotFoundError('response does not have a next page')`. + +### Example + +```javascript +const command = 'ledger_data' +const params = { + ledger_index: 'validated' +} +return api.request(command, params).then(response => { + return api.requestNextPage(command, params, response) +}).then(response_page_2 => { + /* Do something useful with second page of response */ +}).catch(console.error); +``` + # API Methods ## connect diff --git a/docs/samples/README b/docs/samples/README.md similarity index 100% rename from docs/samples/README rename to docs/samples/README.md diff --git a/docs/src/hasNextPage.md.ejs b/docs/src/hasNextPage.md.ejs new file mode 100644 index 00000000..e052a32d --- /dev/null +++ b/docs/src/hasNextPage.md.ejs @@ -0,0 +1,27 @@ +## hasNextPage + +`hasNextPage(currentResponse): boolean` + +Returns `true` when there are more pages available. + +When there are more results than contained in the response, the response includes a `marker` field. You can use this convenience method, or check for `marker` yourself. + +See [Markers and Pagination](https://ripple.com/build/rippled-apis/#markers-and-pagination). + +### Return Value + +This method returns `true` if `currentResponse` includes a `marker`. + +### Example + +```javascript +return api.request('ledger_data', { + ledger_index: 'validated' +}).then(response => { + /* Do something useful with response */ + + if (api.hasNextPage(response)) { + /* There are more pages available */ + } +}).catch(console.error); +``` diff --git a/docs/src/index.md.ejs b/docs/src/index.md.ejs index a9626457..8f23f152 100644 --- a/docs/src/index.md.ejs +++ b/docs/src/index.md.ejs @@ -4,6 +4,10 @@ <% include basictypes.md.ejs %> <% include transactions.md.ejs %> <% include specifications.md.ejs %> +<% include rippledAPIs.md.ejs %> +<% include request.md.ejs %> +<% include hasNextPage.md.ejs %> +<% include requestNextPage.md.ejs %> <% include methods.md.ejs %> <% include connect.md.ejs %> <% include disconnect.md.ejs %> diff --git a/docs/src/introduction.md.ejs b/docs/src/introduction.md.ejs index 88d453e5..d59ddf4d 100644 --- a/docs/src/introduction.md.ejs +++ b/docs/src/introduction.md.ejs @@ -1,6 +1,6 @@ # Introduction -RippleAPI is the official client library to the XRP Ledger. Currently, RippleAPI is only available in JavaScript. +RippleAPI (ripple-lib) is the official client library to the XRP Ledger. Currently, RippleAPI is only available in JavaScript. Using RippleAPI, you can: * [Query transactions from the XRP Ledger history](#gettransaction) @@ -8,5 +8,3 @@ Using RippleAPI, you can: * [Submit](#submit) transactions to the XRP Ledger, including [Payments](#payment), [Orders](#order), [Settings changes](#settings), and [other types](#transaction-types) * [Generate a new XRP Ledger Address](#generateaddress) * ... and [much more](#api-methods). - -RippleAPI only provides access to *validated*, *immutable* transaction data. diff --git a/docs/src/request.md.ejs b/docs/src/request.md.ejs new file mode 100644 index 00000000..cb21421d --- /dev/null +++ b/docs/src/request.md.ejs @@ -0,0 +1,27 @@ +## request + +`request(command: string, options: object): Promise` + +Returns the response from invoking the specified command, with the specified options, on the connected rippled server. + +Refer to [rippled APIs](https://ripple.com/build/rippled-apis/) for commands and options. All XRP amounts must be specified in drops. One drop is equal to 0.000001 XRP. See [Specifying Currency Amounts](https://ripple.com/build/rippled-apis/#specifying-currency-amounts). + +Most commands return data for the `current` (in-progress, open) ledger by default. Do not rely on this. Always specify a ledger version in your request. In the example below, the 'validated' ledger is requested, which is the most recent ledger that has been validated by the whole network. See [Specifying Ledgers](https://ripple.com/build/rippled-apis/#specifying-ledgers). + +### Return Value + +This method returns a promise that resolves with the response from rippled. + +### Example + +```javascript +// Replace 'ledger' with your desired rippled command +return api.request('ledger', { + ledger_index: 'validated' +}).then(response => { + /* Do something useful with response */ + console.log(JSON.stringify(response, null, 2)) +}).catch(console.error); +``` + +<%- renderFixture('responses/ledger.json') %> diff --git a/docs/src/requestNextPage.md.ejs b/docs/src/requestNextPage.md.ejs new file mode 100644 index 00000000..5a607500 --- /dev/null +++ b/docs/src/requestNextPage.md.ejs @@ -0,0 +1,29 @@ +## requestNextPage + +`requestNextPage(command: string, params: object = {}, currentResponse: object): Promise` + +Requests the next page of data. + +You can use this convenience method, or include `currentResponse.marker` in `params` yourself, when using `request`. + +See [Markers and Pagination](https://ripple.com/build/rippled-apis/#markers-and-pagination). + +### Return Value + +This method returns a promise that resolves with the next page of data from rippled. + +If the response does not have a next page, the promise will reject with `new errors.NotFoundError('response does not have a next page')`. + +### Example + +```javascript +const command = 'ledger_data' +const params = { + ledger_index: 'validated' +} +return api.request(command, params).then(response => { + return api.requestNextPage(command, params, response) +}).then(response_page_2 => { + /* Do something useful with second page of response */ +}).catch(console.error); +``` diff --git a/docs/src/rippledAPIs.md.ejs b/docs/src/rippledAPIs.md.ejs new file mode 100644 index 00000000..dde92ef4 --- /dev/null +++ b/docs/src/rippledAPIs.md.ejs @@ -0,0 +1,66 @@ +# rippled APIs + +ripple-lib relies on [rippled APIs](https://ripple.com/build/rippled-apis/) for all online functionality. With ripple-lib version 1.0.0 and higher, you can easily access rippled APIs through ripple-lib. Use the `request()`, `hasNextPage()`, and `requestNextPage()` methods: +* Use `request()` to issue any `rippled` command, including `account_currencies`, `subscribe`, and `unsubscribe`. [Full list of API Methods](https://ripple.com/build/rippled-apis/#api-methods). +* Use `hasNextPage()` to determine whether a response has more pages. This is true when the response includes a [`marker` field](https://ripple.com/build/rippled-apis/#markers-and-pagination). +* Use `requestNextPage()` to request the next page of data. + +When using rippled APIs, [specify XRP amounts in drops](https://ripple.com/build/rippled-apis/#specifying-currency-amounts). 1 XRP = 1000000 drops. + +## Listening to streams + +The `rippled` server can push updates to your client when various events happen. Refer to [Subscriptions in the `rippled` API docs](https://ripple.com/build/rippled-apis/#subscriptions) for details. + +Note that the `streams` parameter for generic streams takes an array. For example, to subscribe to the `validations` stream, use `{ streams: [ 'validations' ] }`. + +The string names of some generic streams to subscribe to are in the table below. (Refer to `rippled` for an up-to-date list of streams.) + +Type | Description +---- | ----------- +`server` | Sends a message whenever the status of the `rippled` server (for example, network connectivity) changes. +`ledger` | Sends a message whenever the consensus process declares a new validated ledger. +`transactions` | Sends a message whenever a transaction is included in a closed ledger. +`transactions_proposed` | Sends a message whenever a transaction is included in a closed ledger, as well as some transactions that have not yet been included in a validated ledger and may never be. Not all proposed transactions appear before validation. Even some transactions that don't succeed are included in validated ledgers because they take the anti-spam transaction fee. +`validations` | Sends a message whenever the server receives a validation message, also called a validation vote, regardless of whether the server trusts the validator. +`manifests` | Sends a message whenever the server receives a manifest. +`peer_status` | (Admin-only) Information about connected peer `rippled` servers, especially with regards to the consensus process. + +When you subscribe to a stream, you must also listen to the relevant message type(s). Some of the available message types are in the table below. (Refer to `rippled` for an up-to-date list of message types.) + +Type | Description +---- | ----------- +`ledgerClosed` | Sent by the `ledger` stream when the consensus process declares a new fully validated ledger. The message identifies the ledger and provides some information about its contents. +`validationReceived` | Sent by the `validations` stream when the server receives a validation message, also called a validation vote, regardless of whether the server trusts the validator. +`manifestReceived` | Sent by the `manifests` stream when the server receives a manifest. +`transaction` | Sent by many subscriptions including `transactions`, `transactions_proposed`, `accounts`, `accounts_proposed`, and `book` (Order Book). See [Transaction Streams](https://ripple.com/build/rippled-apis/#transaction-streams) for details. +`peerStatusChange` | (Admin-only) Reports a large amount of information on the activities of other `rippled` servers to which the server is connected. + +To register your listener function, use `connection.on(type, handler)`. + +Here is an example of listening for transactions on given account(s): +``` +const account = 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn' // Replace with the account you want notifications for +api.connect().then(() => { // Omit this if you are already connected + + // 'transaction' can be replaced with the relevant `type` from the table above + api.connection.on('transaction', (event) => { + + // Do something useful with `event` + console.log(JSON.stringify(event, null, 2)) + }) + + api.request('subscribe', { + accounts: [ account ] + }).then(response => { + if (response.status === 'success') { + console.log('Successfully subscribed') + } + }).catch(error => { + // Handle `error` + }) +}) +``` + +The subscription ends when you unsubscribe or the WebSocket connection is closed. + +For full details, see [rippled Subscriptions](https://ripple.com/build/rippled-apis/#subscriptions). diff --git a/src/api.ts b/src/api.ts index c1bac551..9c78903e 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,4 +1,3 @@ -import * as _ from 'lodash' import {EventEmitter} from 'events' import {Connection, errors, validate} from './common' import { @@ -87,25 +86,12 @@ function getCollectKeyFromCommand(command: string): string|undefined { } } -// prevent access to non-validated ledger versions -export class RestrictedConnection extends Connection { - request(request: any, timeout?: number) { - const ledger_index = request.ledger_index - if (ledger_index !== undefined && ledger_index !== 'validated') { - if (!_.isNumber(ledger_index) || ledger_index > this._ledgerVersion) { - return Promise.reject(new errors.LedgerVersionError( - `ledgerVersion ${ledger_index} is greater than server\'s ` + - `most recent validated ledger: ${this._ledgerVersion}`)) - } - } - return super.request(request, timeout) - } -} - class RippleAPI extends EventEmitter { - _feeCushion: number - connection: RestrictedConnection + + // New in > 0.21.0 + // non-validated ledger versions are allowed, and passed to rippled as-is. + connection: Connection // these are exposed only for use by unit tests; they are not part of the API. static _PRIVATE = { @@ -121,7 +107,7 @@ class RippleAPI extends EventEmitter { this._feeCushion = options.feeCushion || 1.2 const serverURL = options.server if (serverURL !== undefined) { - this.connection = new RestrictedConnection(serverURL, options) + this.connection = new Connection(serverURL, options) this.connection.on('ledgerClosed', message => { this.emit('ledger', formatLedgerClose(message)) }) @@ -137,14 +123,14 @@ class RippleAPI extends EventEmitter { } else { // use null object pattern to provide better error message if user // tries to call a method that requires a connection - this.connection = new RestrictedConnection(null, options) + this.connection = new Connection(null, options) } } - async _request(command: 'account_info', params: AccountInfoRequest): + async request(command: 'account_info', params: AccountInfoRequest): Promise - async _request(command: 'account_lines', params: AccountLinesRequest): + async request(command: 'account_lines', params: AccountLinesRequest): Promise /** @@ -152,33 +138,62 @@ class RippleAPI extends EventEmitter { * For an account's trust lines and balances, * see `getTrustlines` and `getBalances`. */ - async _request(command: 'account_objects', params: AccountObjectsRequest): + async request(command: 'account_objects', params: AccountObjectsRequest): Promise - async _request(command: 'account_offers', params: AccountOffersRequest): + async request(command: 'account_offers', params: AccountOffersRequest): Promise - async _request(command: 'book_offers', params: BookOffersRequest): + async request(command: 'book_offers', params: BookOffersRequest): Promise - async _request(command: 'gateway_balances', params: GatewayBalancesRequest): + async request(command: 'gateway_balances', params: GatewayBalancesRequest): Promise - async _request(command: 'ledger', params: LedgerRequest): + async request(command: 'ledger', params: LedgerRequest): Promise - async _request(command: 'ledger_entry', params: LedgerEntryRequest): + async request(command: 'ledger_entry', params: LedgerEntryRequest): Promise + async request(command: string, params: object): + Promise + /** * Makes a request to the API with the given command and * additional request body parameters. - * - * NOTE: This command is under development. */ - async _request(command: string, params: object = {}) { + async request(command: string, params: object = {}): Promise { return this.connection.request({ ...params, command }) } + /** + * Returns true if there are more pages of data. + * + * When there are more results than contained in the response, the response + * includes a `marker` field. + * + * See https://ripple.com/build/rippled-apis/#markers-and-pagination + */ + hasNextPage(currentResponse: T): boolean { + return !!currentResponse.marker + } + + async requestNextPage( + command: string, + params: object = {}, + currentResponse: T + ): Promise { + if (!currentResponse.marker) { + return Promise.reject( + new errors.NotFoundError('response does not have a next page') + ) + } + const nextPageParams = Object.assign({}, params, { + marker: currentResponse.marker + }) + return this.request(command, nextPageParams) + } + /** * Makes multiple paged requests to the API to return a given number of * resources. _requestAll() will make multiple requests until the `limit` @@ -188,8 +203,9 @@ class RippleAPI extends EventEmitter { * If the command is unknown, an additional `collect` property is required to * know which response key contains the array of resources. * - * NOTE: This command is under development and should not yet be relied - * on by external consumers. + * NOTE: This command is used by existing methods and is not recommended for + * general use. Instead, use rippled's built-in pagination and make multiple + * requests as needed. */ async _requestAll(command: 'account_offers', params: AccountOffersRequest): Promise @@ -222,12 +238,9 @@ class RippleAPI extends EventEmitter { limit: countRemaining, marker } - // NOTE: We have to generalize the `this._request()` function signature - // here until we add support for unknown commands (since command is some - // unknown string). - const singleResult = await (this._request)(command, repeatProps) + const singleResult = await this.request(command, repeatProps) const collectedData = singleResult[collectKey] - marker = singleResult.marker + marker = singleResult['marker'] results.push(singleResult) // Make sure we handle when no data (not even an empty array) is returned. const isExpectedFormat = Array.isArray(collectedData) diff --git a/src/common/connection.ts b/src/common/connection.ts index 6c1ba647..f9a6daf0 100644 --- a/src/common/connection.ts +++ b/src/common/connection.ts @@ -7,12 +7,6 @@ import {RippledError, DisconnectedError, NotConnectedError, TimeoutError, ResponseFormatError, ConnectionError, RippledNotInitializedError} from './errors' -function isStreamMessageType(type) { - return type === 'ledgerClosed' || - type === 'transaction' || - type === 'path_find' -} - export interface ConnectionOptions { trace?: boolean, proxy?: string @@ -93,16 +87,17 @@ class Connection extends EventEmitter { throw new ResponseFormatError('valid id not found in response') } return [data.id.toString(), data] - } else if (isStreamMessageType(data.type)) { - if (data.type === 'ledgerClosed') { - this._updateLedgerVersions(data) - this._updateFees(data) - } - return [data.type, data] } else if (data.type === undefined && data.error) { return ['error', data.error, data.error_message, data] // e.g. slowDown } - throw new ResponseFormatError('unrecognized message type: ' + data.type) + + // Possible `data.type` values include 'ledgerClosed', + // 'transaction', 'path_find', and many others. + if (data.type === 'ledgerClosed') { + this._updateLedgerVersions(data) + this._updateFees(data) + } + return [data.type, data] } _onMessage(message) { @@ -427,7 +422,7 @@ class Connection extends EventEmitter { function onDisconnect() { clearTimeout(timer) self.removeAllListeners(eventName) - reject(new DisconnectedError()) + reject(new DisconnectedError('websocket was closed')) } function cleanup() { diff --git a/src/ledger/accountinfo.ts b/src/ledger/accountinfo.ts index 778de005..48f50dee 100644 --- a/src/ledger/accountinfo.ts +++ b/src/ledger/accountinfo.ts @@ -35,7 +35,7 @@ export default async function getAccountInfo( // 1. Validate validate.getAccountInfo({address, options}) // 2. Make Request - const response = await this._request('account_info', { + const response = await this.request('account_info', { account: address, ledger_index: options.ledgerVersion || 'validated' }) diff --git a/src/ledger/accountobjects.ts b/src/ledger/accountobjects.ts index d580ac4a..05944906 100644 --- a/src/ledger/accountobjects.ts +++ b/src/ledger/accountobjects.ts @@ -14,7 +14,7 @@ export default async function getAccountObjects( // through to rippled. rippled validates requests. // Make Request - const response = await this._request('account_objects', removeUndefined({ + const response = await this.request('account_objects', removeUndefined({ account: address, type: options.type, ledger_hash: options.ledgerHash, diff --git a/src/ledger/balance-sheet.ts b/src/ledger/balance-sheet.ts index 9f53f9d8..4ce01bc1 100644 --- a/src/ledger/balance-sheet.ts +++ b/src/ledger/balance-sheet.ts @@ -54,7 +54,7 @@ async function getBalanceSheet( validate.getBalanceSheet({address, options}) options = await ensureLedgerVersion.call(this, options) // 2. Make Request - const response = await this._request('gateway_balances', { + const response = await this.request('gateway_balances', { account: address, strict: true, hotwallet: options.excludeAddresses, diff --git a/src/ledger/ledger.ts b/src/ledger/ledger.ts index b7847c5f..c4479145 100644 --- a/src/ledger/ledger.ts +++ b/src/ledger/ledger.ts @@ -15,7 +15,7 @@ async function getLedger( // 1. Validate validate.getLedger({options}) // 2. Make Request - const response = await this._request('ledger', { + const response = await this.request('ledger', { ledger_index: options.ledgerVersion || 'validated', expand: options.includeAllData, transactions: options.includeTransactions, diff --git a/src/ledger/payment-channel.ts b/src/ledger/payment-channel.ts index 17859537..7903d6ac 100644 --- a/src/ledger/payment-channel.ts +++ b/src/ledger/payment-channel.ts @@ -23,7 +23,7 @@ async function getPaymentChannel( // 1. Validate validate.getPaymentChannel({id}) // 2. Make Request - const response = await this._request('ledger_entry', { + const response = await this.request('ledger_entry', { index: id, binary: false, ledger_index: 'validated' diff --git a/src/ledger/settings.ts b/src/ledger/settings.ts index 4494cc62..be1d8695 100644 --- a/src/ledger/settings.ts +++ b/src/ledger/settings.ts @@ -33,7 +33,7 @@ async function getSettings( // 1. Validate validate.getSettings({address, options}) // 2. Make Request - const response = await this._request('account_info', { + const response = await this.request('account_info', { account: address, ledger_index: options.ledgerVersion || 'validated', signer_lists: true diff --git a/test/api-test.js b/test/api-test.js index 57de29a3..d8d53531 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -51,6 +51,55 @@ describe('RippleAPI', function () { assert.strictEqual(error.inspect(), '[RippleError(mess, { data: 1 })]'); }); + describe('pagination', function () { + + describe('hasNextPage', function () { + + it('returns true when there is another page', function () { + return this.api.request('ledger_data').then(response => { + assert(this.api.hasNextPage(response)); + } + ); + }); + + it('returns false when there are no more pages', function () { + return this.api.request('ledger_data').then(response => { + return this.api.requestNextPage('ledger_data', {}, response); + }).then(response => { + assert(!this.api.hasNextPage(response)); + }); + }); + + }); + + describe('requestNextPage', function () { + + it('requests the next page', function () { + return this.api.request('ledger_data').then(response => { + return this.api.requestNextPage('ledger_data', {}, response); + }).then(response => { + assert.equal(response.state[0].index, '000B714B790C3C79FEE00D17C4DEB436B375466F29679447BA64F265FD63D731') + }); + }); + + it('rejects when there are no more pages', function () { + return this.api.request('ledger_data').then(response => { + return this.api.requestNextPage('ledger_data', {}, response); + }).then(response => { + assert(!this.api.hasNextPage(response)) + return this.api.requestNextPage('ledger_data', {}, response); + }).then(() => { + assert(false, 'Should reject'); + }).catch(error => { + assert(error instanceof Error); + assert.equal(error.message, 'response does not have a next page') + }); + }); + + }); + + }); + describe('preparePayment', function () { it('normal', function () { @@ -1213,7 +1262,7 @@ describe('RippleAPI', function () { }); it('request account_objects', function () { - return this.api._request('account_objects', { + return this.api.request('account_objects', { account: address }).then(response => checkResult(responses.getAccountObjects, 'AccountObjectsResponse', response)); @@ -1221,7 +1270,7 @@ describe('RippleAPI', function () { it('request account_objects - invalid options', function () { // Intentionally no local validation of these options - return this.api._request('account_objects', { + return this.api.request('account_objects', { account: address, invalid: 'options' }).then(response => @@ -1528,12 +1577,12 @@ describe('RippleAPI', function () { _.partial(checkResult, responses.getLedger.header, 'getLedger')); }); + // New in > 0.21.0 + // future ledger versions are allowed, and passed to rippled as-is. it('getLedger - future ledger version', function () { - return this.api.getLedger({ ledgerVersion: 14661789 }).then(() => { - assert(false, 'Should throw LedgerVersionError'); - }).catch(error => { - assert(error instanceof this.api.errors.LedgerVersionError); - }); + return this.api.getLedger({ ledgerVersion: 14661789 }).then(response => { + assert(response) + }) }); it('getLedger - with state as hashes', function () { diff --git a/test/connection-test.js b/test/connection-test.js index 8facd42b..a90678b0 100644 --- a/test/connection-test.js +++ b/test/connection-test.js @@ -394,10 +394,10 @@ describe('Connection', function() { }); it('unrecognized message type', function(done) { - this.api.on('error', (errorCode, errorMessage, message) => { - assert.strictEqual(errorCode, 'badMessage'); - assert.strictEqual(errorMessage, 'unrecognized message type: unknown'); - assert.strictEqual(message, '{"type":"unknown"}'); + // This enables us to automatically support any + // new messages added by rippled in the future. + this.api.connection.on('unknown', (event) => { + assert.deepEqual(event, {type: 'unknown'}) done(); }); diff --git a/test/fixtures/responses/ledger.json b/test/fixtures/responses/ledger.json new file mode 100644 index 00000000..7ed8ab54 --- /dev/null +++ b/test/fixtures/responses/ledger.json @@ -0,0 +1,23 @@ +{ + "ledger": { + "accepted": true, + "account_hash": "F9E9653EA76EA0AEA58AC98A8E19EDCEC8299C2940519A190674FFAED3639A1F", + "close_flags": 0, + "close_time": 577999430, + "close_time_human": "2018-Apr-25 19:23:50", + "close_time_resolution": 10, + "closed": true, + "hash": "450E5CB0A39495839DA9CD9A0FED74BD71CBB929423A907ADC00F14FC7E7F920", + "ledger_hash": "450E5CB0A39495839DA9CD9A0FED74BD71CBB929423A907ADC00F14FC7E7F920", + "ledger_index": "38217406", + "parent_close_time": 577999422, + "parent_hash": "B8B364C63EB9E13FDB89CB729FEF833089B8438CBEB8FC41744CB667209221B3", + "seqNum": "38217406", + "totalCoins": "99992286058637091", + "total_coins": "99992286058637091", + "transaction_hash": "5BDD3D2780C28FB2C91C3404BD8ED04786B764B1E18CF319888EDE2C09834726" + }, + "ledger_hash": "450E5CB0A39495839DA9CD9A0FED74BD71CBB929423A907ADC00F14FC7E7F920", + "ledger_index": 38217406, + "validated": true +} diff --git a/test/fixtures/rippled/index.js b/test/fixtures/rippled/index.js index 6700cf7b..014bceb0 100644 --- a/test/fixtures/rippled/index.js +++ b/test/fixtures/rippled/index.js @@ -37,6 +37,10 @@ module.exports = { usd_xrp: require('./book-offers-usd-xrp'), xrp_usd: require('./book-offers-xrp-usd') }, + ledger_data: { + first_page: require('./ledger-data-first-page'), + last_page: require('./ledger-data-last-page') + }, ledger_entry: { error: require('./ledger-entry-error') }, diff --git a/test/fixtures/rippled/ledger-data-first-page.json b/test/fixtures/rippled/ledger-data-first-page.json new file mode 100644 index 00000000..b646c45c --- /dev/null +++ b/test/fixtures/rippled/ledger-data-first-page.json @@ -0,0 +1,40 @@ +{ + "id": 0, + "status": "success", + "type": "response", + "result": { + "ledger_hash": + "102A6E70FFB18C18E97BB56E3047B0E45EA1BCC90BFCCB8CBB0D07BF0E2AB449", + "ledger_index": 38202000, + "marker": + "000B714B790C3C79FEE00D17C4DEB436B375466F29679447BA64F265FD63D730", + "state": [ + { + "Flags": 0, + "Indexes": [ + "B32769DB3BE790E959A96CF37A62414479E3EB20A5AEC7156B2BF8FD816DBFF8" + ], + "LedgerEntryType": "DirectoryNode", + "Owner": "rwt5iiE1mRbBgNhH6spU4nKgHcE7xK9joN", + "RootIndex": + "0005C961C890079D3C4CC8317F9735D388C3CE3D9BCDC152D3C9A7C08F508D1B", + "index": + "0005C961C890079D3C4CC8317F9735D388C3CE3D9BCDC152D3C9A7C08F508D1B" + }, + { + "Account": "rpzpyUjdWKmz7yyMvirk3abcaNvSPmDpJn", + "Balance": "91508000", + "Flags": 0, + "LedgerEntryType": "AccountRoot", + "OwnerCount": 0, + "PreviousTxnID": + "F62A5A5EC92DE4E52663B9C7B44A2B76DAB1371737C83A5A81127CBDA84DFE9E", + "PreviousTxnLgrSeq": 35672898, + "Sequence": 1, + "index": + "000B6A1287DB6174F61B1BF987E630CF41DA2A2131CFEB6C5C8143A8F539E9D1" + } + ], + "validated": true + } +} diff --git a/test/fixtures/rippled/ledger-data-last-page.json b/test/fixtures/rippled/ledger-data-last-page.json new file mode 100644 index 00000000..e240ec73 --- /dev/null +++ b/test/fixtures/rippled/ledger-data-last-page.json @@ -0,0 +1,47 @@ +{ + "id": 0, + "status": "success", + "type": "response", + "result": { + "ledger_hash": + "102A6E70FFB18C18E97BB56E3047B0E45EA1BCC90BFCCB8CBB0D07BF0E2AB449", + "ledger_index": 38202000, + "state": [ + { + "Account": "rN3rdDNhQidDuzTFU1ArXWr89B4JG9xZ99", + "Balance": "249222644", + "Flags": 0, + "LedgerEntryType": "AccountRoot", + "OwnerCount": 0, + "PreviousTxnID": + "9A6EEBB6055E2C768BCA3B89B458A5D14A931449443053D9A1A9256F79D590DC", + "PreviousTxnLgrSeq": 35891744, + "Sequence": 1, + "index": + "000B714B790C3C79FEE00D17C4DEB436B375466F29679447BA64F265FD63D731" + }, + { + "Account": "rLNNqGs2jJKQcg2CuoACuwkJ1ssga9LTYT", + "BookDirectory": + "6FA9AF02AF19345DC187747EF07CDABECA37CB6DCFFB045E5A08D0CF885B163B", + "BookNode": "0000000000000000", + "Flags": 0, + "LedgerEntryType": "Offer", + "OwnerNode": "0000000000000000", + "PreviousTxnID": + "5D3E557E7C08FA90EF9EE144165855B3823BD24319F28BDD81E23C3573398C44", + "PreviousTxnLgrSeq": 38040457, + "Sequence": 9, + "TakerGets": { + "currency": "CNY", + "issuer": "rPT74sUcTBTQhkHVD54WGncoqXEAMYbmH7", + "value": "322.4" + }, + "TakerPays": "80000000", + "index": + "0011C33FA959278D478E7A3811D7DBB9E43E1768E12538CD54B028E5E7DA96E5" + } + ], + "validated": true + } +} diff --git a/test/mock-rippled.js b/test/mock-rippled.js index bcc1bfcd..1b0a0bbd 100644 --- a/test/mock-rippled.js +++ b/test/mock-rippled.js @@ -237,6 +237,15 @@ module.exports = function createMockRippled(port) { } }); + mock.on('request_ledger_data', function (request, conn) { + assert.strictEqual(request.command, 'ledger_data'); + if (request.marker) { + conn.send(createResponse(request, fixtures.ledger_data.last_page)); + } else { + conn.send(createResponse(request, fixtures.ledger_data.first_page)); + } + }); + mock.on('request_ledger_entry', function (request, conn) { assert.strictEqual(request.command, 'ledger_entry'); if (request.index ===