Compare commits

..

2 Commits

Author SHA1 Message Date
Elliot Lee
1d1132b7fa Release 1.0.0-beta.2 2018-06-08 08:11:00 -07:00
Elliot Lee
e07fa11923 Maximum fee values (#902)
* Add maxFeeXRP (default 2 XRP) as an optional RippleAPI constructor parameter
  - No calculated or specified fee can exceed this value
  - If the fee exceeds 2 XRP, throw a ValidationError
* sign() - throw ValidationError when Fee exceeds maxFeeXRP
* Document getFee parameters
* Explain new fee limits in HISTORY.md
* Deprecate `maxFee`
2018-06-07 23:29:24 -07:00
14 changed files with 300 additions and 27 deletions

View File

@@ -1,5 +1,36 @@
# ripple-lib Release History
## 1.0.0-beta.2 (2018-06-08)
### Breaking Changes
+ During transaction preparation, there is now a maximum fee. Also, when a transaction is signed, its fee is checked and an error is thrown if the fee exceeds the maximum. The default `maxFeeXRP` is `'2'` (2 XRP). Override this value in the RippleAPI constructor.
+ Attempting to prepare a transaction with an exact `fee` higher than `maxFeeXRP` causes a `ValidationError` to be thrown.
+ Attempting to sign a transaction with a fee higher than `maxFeeXRP` causes a `ValidationError` to be thrown.
+ The value returned by `getFee()` is capped at `maxFeeXRP`.
### Other Changes
+ In Transaction Instructions, the `maxFee` parameter is deprecated. Use the `maxFeeXRP` parameter in the RippleAPI constructor.
#### Overview of new fee limit
Most users of ripple-lib do not need to make any code changes to accommodate the new soft limit on fees. The limit is designed to protect against the most severe cases where an unintentionally high fee may be used.
+ When having ripple-lib provide the fee with a `prepare*` method, a maximum fee of `maxFeeXRP` (default 2 XRP) applies. You can prepare more economical transactions by setting a lower `maxFeeXRP`, or support high-priority transactions by setting a higher `maxFeeXRP` in the RippleAPI constructor.
+ When using `sign` with a Fee higher than `maxFeeXRP`, a `ValidationError` is thrown.
If you have any questions or concerns, please open an issue on GitHub.
The SHA-256 checksums for the browser version of this release can be found
below.
```
% shasum -a 256 *
ef348a2805098e61395b689b410cbf4bfd35e4d72e38c89f4ab74ec5e19793f5 ripple-1.0.0-beta.2-debug.js
ea33fd53df8c7176d5fbf52dae0b64aade7180860f26449062cdbefaf8bd4d9b ripple-1.0.0-beta.2-min.js
fe5cc6e97c9b8a1470dacb34f16a64255cd639a25381abe9db1ba79e102456f2 ripple-1.0.0-beta.2.js
```
## 1.0.0-beta.1 (2018-05-24)
### Breaking Changes

View File

@@ -152,6 +152,7 @@ authorization | string | *Optional* Username and password for HTTP basic authent
certificate | string | *Optional* A string containing the certificate key of the client in PEM format. (Can be an array of certificates).
feeCushion | number | *Optional* Factor to multiply estimated fee by to provide a cushion in case the required fee rises during submission of a transaction. Defaults to `1.2`.
key | string | *Optional* A string containing the private key of the client in PEM format. (Can be an array of keys).
maxFeeXRP | string | *Optional* Maximum fee to use with transactions, in XRP. Must be a string-encoded number. Defaults to `'2'`.
passphrase | string | *Optional* The passphrase for the private key of the client.
proxy | uri string | *Optional* URI for HTTP/HTTPS proxy to use to connect to the rippled server.
proxyAuthorization | string | *Optional* Username and password for HTTP basic authentication to the proxy in the format **username:password**.
@@ -316,7 +317,7 @@ Transaction instructions indicate how to execute a transaction, complementary wi
Name | Type | Description
---- | ---- | -----------
fee | [value](#value) | *Optional* An exact fee to pay for the transaction. See [Transaction Fees](#transaction-fees) for more information.
maxFee | [value](#value) | *Optional* The maximum fee to pay for the transaction. See [Transaction Fees](#transaction-fees) for more information.
maxFee | [value](#value) | *Optional* Deprecated: Use `maxFeeXRP` in the RippleAPI constructor instead. The maximum fee to pay for this transaction. If this exceeds `maxFeeXRP`, `maxFeeXRP` will be used instead. See [Transaction Fees](#transaction-fees) for more information.
maxLedgerVersion | integer,null | *Optional* The highest ledger version that the transaction can be included in. If this option and `maxLedgerVersionOffset` are both omitted, the `maxLedgerVersion` option will default to 3 greater than the current validated ledger version (equivalent to `maxLedgerVersionOffset=3`). Use `null` to not set a maximum ledger version.
maxLedgerVersion | string,null | *Optional* The highest ledger version that the transaction can be included in. If this option and `maxLedgerVersionOffset` are both omitted, the `maxLedgerVersion` option will default to 3 greater than the current validated ledger version (equivalent to `maxLedgerVersionOffset=3`). Use `null` to not set a maximum ledger version.
maxLedgerVersionOffset | integer | *Optional* Offset from current validated ledger version to highest ledger version that the transaction can be included in.
@@ -1076,7 +1077,9 @@ Returns the estimated transaction fee for the rippled server the RippleAPI insta
### Parameters
This method has no parameters.
Name | Type | Description
---- | ---- | -----------
cushion | number | *Optional* The fee is the product of the base fee, the `load_factor`, and this cushion. Default is provided by the `RippleAPI` constructor's `feeCushion`.
### Return Value

View File

@@ -6,7 +6,7 @@ Returns the estimated transaction fee for the rippled server the RippleAPI insta
### Parameters
This method has no parameters.
<%- renderSchema('input/get-fee.json') %>
### Return Value

View File

@@ -1,6 +1,6 @@
{
"name": "ripple-lib",
"version": "1.0.0-beta.1",
"version": "1.0.0-beta.2",
"license": "ISC",
"description": "A JavaScript API for interacting with Ripple in Node.js and the browser",
"files": [

View File

@@ -64,6 +64,7 @@ import {clamp} from './ledger/utils'
export type APIOptions = {
server?: string,
feeCushion?: number,
maxFeeXRP?: string,
trace?: boolean,
proxy?: string,
timeout?: number
@@ -88,6 +89,7 @@ function getCollectKeyFromCommand(command: string): string|undefined {
class RippleAPI extends EventEmitter {
_feeCushion: number
_maxFeeXRP: string
// New in > 0.21.0
// non-validated ledger versions are allowed, and passed to rippled as-is.
@@ -105,6 +107,7 @@ class RippleAPI extends EventEmitter {
super()
validate.apiOptions(options)
this._feeCushion = options.feeCushion || 1.2
this._maxFeeXRP = options.maxFeeXRP || '2'
const serverURL = options.server
if (serverURL !== undefined) {
this.connection = new Connection(serverURL, options)

View File

@@ -12,6 +12,10 @@
"minimum": 1,
"description": "Factor to multiply estimated fee by to provide a cushion in case the required fee rises during submission of a transaction. Defaults to `1.2`."
},
"maxFeeXRP": {
"type": "string",
"description": "Maximum fee to use with transactions, in XRP. Must be a string-encoded number. Defaults to `'2'`."
},
"server": {
"type": "string",
"description": "URI for rippled websocket port to connect to. Must start with `wss://` or `ws://`.",

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "getFeeParameters",
"description": "Parameters for getFee",
"type": "object",
"properties": {
"cushion": {
"type": "number",
"description": "The fee is the product of the base fee, the `load_factor`, and this cushion. Default is provided by the `RippleAPI` constructor's `feeCushion`."
}
},
"additionalProperties": false
}

View File

@@ -14,7 +14,7 @@
"$ref": "value"
},
"maxFee": {
"description": "The maximum fee to pay for the transaction. See [Transaction Fees](#transaction-fees) for more information.",
"description": "Deprecated: Use `maxFeeXRP` in the RippleAPI constructor instead. The maximum fee to pay for this transaction. If this exceeds `maxFeeXRP`, `maxFeeXRP` will be used instead. See [Transaction Fees](#transaction-fees) for more information.",
"$ref": "value"
},
"maxLedgerVersion": {

View File

@@ -2,7 +2,6 @@ import * as _ from 'lodash'
import {convertKeysFromSnakeCaseToCamelCase} from './utils'
import BigNumber from 'bignumber.js'
import {RippleAPI} from '../index'
import {ServerInfoResponse} from './types/commands'
export type GetServerInfoResponse = {
buildVersion: string,
@@ -40,14 +39,6 @@ function renameKeys(object, mapping) {
})
}
function computeFeeFromServerInfo(
cushion: number, serverInfo: ServerInfoResponse
): string {
return (new BigNumber(serverInfo.info.validated_ledger.base_fee_xrp)).
times(serverInfo.info.load_factor).
times(cushion).toString()
}
function getServerInfo(this: RippleAPI): Promise<GetServerInfoResponse> {
return this.request('server_info').then(response => {
const info = convertKeysFromSnakeCaseToCamelCase(response.info)
@@ -70,15 +61,23 @@ function getServerInfo(this: RippleAPI): Promise<GetServerInfoResponse> {
})
}
async function getFee(this: RippleAPI, cushion?: number): Promise<string> {
async function getFee(
this: RippleAPI,
cushion?: number
): Promise<string> {
if (cushion === undefined) {
cushion = this._feeCushion
}
if (cushion === undefined) {
cushion = 1.2
}
const response = await this.request('server_info')
return computeFeeFromServerInfo(cushion, response)
const serverInfo = (await this.request('server_info')).info
const baseFeeXrp = new BigNumber(serverInfo.validated_ledger.base_fee_xrp)
const fee = baseFeeXrp.times(serverInfo.load_factor).times(cushion)
// Cap fee to `this._maxFeeXRP`
return BigNumber.min(fee, this._maxFeeXRP).toString(10)
}
export {

View File

@@ -3,6 +3,9 @@ import keypairs = require('ripple-keypairs')
import binary = require('ripple-binary-codec')
import {computeBinaryTransactionHash} from 'ripple-hashes'
import {SignOptions, KeyPair} from './types'
import {BigNumber} from 'bignumber.js'
import {xrpToDrops} from '../common'
import {RippleAPI} from '../api'
const validate = utils.common.validate
function computeSignature(tx: Object, privateKey: string, signAs?: string) {
@@ -13,6 +16,7 @@ function computeSignature(tx: Object, privateKey: string, signAs?: string) {
}
function signWithKeypair(
api: RippleAPI,
txJSON: string,
keypair: KeyPair,
options: SignOptions = {
@@ -28,6 +32,15 @@ function signWithKeypair(
)
}
const fee = new BigNumber(tx.Fee)
const maxFeeDrops = xrpToDrops(api._maxFeeXRP)
if (fee.greaterThan(maxFeeDrops)) {
throw new utils.common.errors.ValidationError(
`"Fee" should not exceed "${maxFeeDrops}". ` +
'To use a higher fee, set `maxFeeXRP` in the RippleAPI constructor.'
)
}
tx.SigningPubKey = options.signAs ? '' : keypair.publicKey
if (options.signAs) {
@@ -49,6 +62,7 @@ function signWithKeypair(
}
function sign(
this: RippleAPI,
txJSON: string,
secret?: any,
options?: SignOptions,
@@ -58,9 +72,18 @@ function sign(
// we can't validate that the secret matches the account because
// the secret could correspond to the regular key
validate.sign({txJSON, secret})
return signWithKeypair(txJSON, keypairs.deriveKeypair(secret), options)
return signWithKeypair(
this,
txJSON,
keypairs.deriveKeypair(secret),
options
)
} else {
return signWithKeypair(txJSON, keypair ? keypair : secret, options)
return signWithKeypair(
this,
txJSON,
keypair ? keypair : secret,
options)
}
}

View File

@@ -12,6 +12,7 @@ import {ApiMemo} from './utils'
export type Instructions = {
sequence?: number,
fee?: string,
// @deprecated
maxFee?: string,
maxLedgerVersion?: number,
maxLedgerVersionOffset?: number,

View File

@@ -4,6 +4,7 @@ import {Memo} from '../common/types/objects'
const txFlags = common.txFlags
import {Instructions, Prepare} from './types'
import {RippleAPI} from '../api'
import {ValidationError} from '../common/errors'
export type ApiMemo = {
MemoData?: string,
@@ -63,6 +64,13 @@ function prepareTransaction(txJSON: any, api: RippleAPI,
const multiplier = instructions.signersCount === undefined ? 1 :
instructions.signersCount + 1
if (instructions.fee !== undefined) {
const fee = new BigNumber(instructions.fee)
if (fee.greaterThan(api._maxFeeXRP)) {
const errorMessage = `Fee of ${fee.toString(10)} XRP exceeds ` +
`max of ${api._maxFeeXRP} XRP. To use this fee, increase ` +
'`maxFeeXRP` in the RippleAPI constructor.'
throw new ValidationError(errorMessage)
}
txJSON.Fee = scaleValue(common.xrpToDrops(instructions.fee), multiplier)
return Promise.resolve(txJSON)
}
@@ -75,13 +83,12 @@ function prepareTransaction(txJSON: any, api: RippleAPI,
(cushion * feeRef * (32 + Math.floor(
new Buffer(txJSON.Fulfillment, 'hex').length / 16)))
const feeDrops = common.xrpToDrops(fee)
if (instructions.maxFee !== undefined) {
const maxFeeDrops = common.xrpToDrops(instructions.maxFee)
const normalFee = scaleValue(feeDrops, multiplier, extraFee)
txJSON.Fee = BigNumber.min(normalFee, maxFeeDrops).toString()
} else {
txJSON.Fee = scaleValue(feeDrops, multiplier, extraFee)
}
const maxFeeXRP = instructions.maxFee ?
BigNumber.min(api._maxFeeXRP, instructions.maxFee) : api._maxFeeXRP
const maxFeeDrops = common.xrpToDrops(maxFeeXRP)
const normalFee = scaleValue(feeDrops, multiplier, extraFee)
txJSON.Fee = BigNumber.min(normalFee, maxFeeDrops).toString(10)
return txJSON
})
})

View File

@@ -392,6 +392,37 @@ describe('RippleAPI', function () {
instructions).then(_.partial(checkResult,
responses.preparePayment.minAmount, 'prepare'));
});
it('preparePayment - throws when fee exceeds 2 XRP', function () {
const localInstructions = _.defaults({
fee: '2.1'
}, instructions);
assert.throws(() => {
this.api.preparePayment(
address, requests.preparePayment.normal, localInstructions)
}, /Fee of 2\.1 XRP exceeds max of 2 XRP\. To use this fee, increase `maxFeeXRP` in the RippleAPI constructor\./)
});
it('preparePayment - allows fee exceeding 2 XRP when maxFeeXRP is higher', function () {
this.api._maxFeeXRP = '2.2'
const localInstructions = _.defaults({
fee: '2.1'
}, instructions);
const expectedResponse = {
"txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"2100000\",\"Sequence\":23}",
"instructions": {
"fee": "2.1",
"sequence": 23,
"maxLedgerVersion": 8820051
}
}
return this.api.preparePayment(
address, requests.preparePayment.normal, localInstructions).then(
_.partial(checkResult, expectedResponse, 'prepare'));
});
});
it('prepareOrder - buy order', function () {
@@ -823,6 +854,62 @@ describe('RippleAPI', function () {
assert.deepEqual(signature, responses.sign.signAs);
});
it('sign - throws when Fee exceeds maxFeeXRP (in drops)', function () {
const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV';
const request = {
"txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"AccountSet\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Domain\":\"726970706C652E636F6D\",\"LastLedgerSequence\":8820051,\"Fee\":\"2010000\",\"Sequence\":23,\"SigningPubKey\":\"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8\"}",
"instructions": {
"fee": "2.01",
"sequence": 23,
"maxLedgerVersion": 8820051
}
}
assert.throws(() => {
this.api.sign(request.txJSON, secret)
}, /Fee" should not exceed "2000000"\. To use a higher fee, set `maxFeeXRP` in the RippleAPI constructor\./)
});
it('sign - throws when Fee exceeds maxFeeXRP (in drops) - custom maxFeeXRP', function () {
this.api._maxFeeXRP = '1.9'
const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV';
const request = {
"txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"AccountSet\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Domain\":\"726970706C652E636F6D\",\"LastLedgerSequence\":8820051,\"Fee\":\"2010000\",\"Sequence\":23,\"SigningPubKey\":\"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8\"}",
"instructions": {
"fee": "2.01",
"sequence": 23,
"maxLedgerVersion": 8820051
}
}
assert.throws(() => {
this.api.sign(request.txJSON, secret)
}, /Fee" should not exceed "1900000"\. To use a higher fee, set `maxFeeXRP` in the RippleAPI constructor\./)
});
it('sign - permits fee exceeding 2000000 drops when maxFeeXRP is higher than 2 XRP', function () {
this.api._maxFeeXRP = '2.1'
const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV';
const request = {
"txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"AccountSet\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Domain\":\"726970706C652E636F6D\",\"LastLedgerSequence\":8820051,\"Fee\":\"2010000\",\"Sequence\":23,\"SigningPubKey\":\"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8\"}",
"instructions": {
"fee": "2.01",
"sequence": 23,
"maxLedgerVersion": 8820051
}
}
const result = this.api.sign(request.txJSON, secret)
const expectedResponse = {
signedTransaction: "12000322800000002400000017201B008695536840000000001EAB90732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8744630440220384FBB48EEE7B0E58BD89294A609F9407C51FBE8FA08A4B305B22E9A7489D66602200152315EFE752DA381E74493419871550D206AC6503841DA5F8C30E35D9E3892770A726970706C652E636F6D81145E7B112523F68D2F5E879DB4EAC51C6698A69304",
id: "A1586D6AF7B0821E7075E12A0132D9EB50BC1874A0749441201497F7561795FB"
}
assert.deepEqual(result, expectedResponse)
schemaValidator.schemaValidate('sign', result)
});
it('submit', function () {
return this.api.submit(responses.sign.normal.signedTransaction).then(
_.partial(checkResult, responses.submit, 'submit'));
@@ -1663,6 +1750,76 @@ describe('RippleAPI', function () {
});
});
it('getFee - high load_factor', function () {
this.api.connection._send(JSON.stringify({
command: 'config',
data: { highLoadFactor: true }
}));
return this.api.getFee().then(fee => {
assert.strictEqual(fee, '2');
});
});
it('fee - default maxFee of 2 XRP', function () {
this.api._feeCushion = 1000000;
const expectedResponse = {
"txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"2000000\",\"Sequence\":23}",
"instructions": {
"fee": "2",
"sequence": 23,
"maxLedgerVersion": 8820051
}
}
return this.api.preparePayment(
address, requests.preparePayment.normal, instructions).then(
_.partial(checkResult, expectedResponse, 'prepare'));
});
it('fee - capped to maxFeeXRP when maxFee exceeds maxFeeXRP', function () {
this.api._feeCushion = 1000000
this.api._maxFeeXRP = '3'
const localInstructions = _.defaults({
maxFee: '4'
}, instructions);
const expectedResponse = {
"txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"3000000\",\"Sequence\":23}",
"instructions": {
"fee": "3",
"sequence": 23,
"maxLedgerVersion": 8820051
}
}
return this.api.preparePayment(
address, requests.preparePayment.normal, localInstructions).then(
_.partial(checkResult, expectedResponse, 'prepare'));
});
it('fee - capped to maxFee', function () {
this.api._feeCushion = 1000000
this.api._maxFeeXRP = '5'
const localInstructions = _.defaults({
maxFee: '4'
}, instructions);
const expectedResponse = {
"txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"4000000\",\"Sequence\":23}",
"instructions": {
"fee": "4",
"sequence": 23,
"maxLedgerVersion": 8820051
}
}
return this.api.preparePayment(
address, requests.preparePayment.normal, localInstructions).then(
_.partial(checkResult, expectedResponse, 'prepare'));
});
it('disconnect & isConnected', function () {
assert.strictEqual(this.api.isConnected(), true);
return this.api.disconnect().then(() => {

View File

@@ -152,7 +152,39 @@ module.exports = function createMockRippled(port) {
mock.on('request_server_info', function (request, conn) {
assert.strictEqual(request.command, 'server_info');
if (conn.config.returnErrorOnServerInfo) {
if (conn.config.highLoadFactor) {
const response = {
"id": 0,
"status": "success",
"type": "response",
"result": {
"info": {
"build_version": "0.24.0-rc1",
"complete_ledgers": "32570-6595042",
"hostid": "ARTS",
"io_latency_ms": 1,
"last_close": {
"converge_time_s": 2.007,
"proposers": 4
},
"load_factor": 4294967296,
"peers": 53,
"pubkey_node": "n94wWvFUmaKGYrKUGgpv1DyYgDeXRGdACkNQaSe7zJiy5Znio7UC",
"server_state": "full",
"validated_ledger": {
"age": 5,
"base_fee_xrp": 0.00001,
"hash": "4482DEE5362332F54A4036ED57EE1767C9F33CF7CE5A6670355C16CECE381D46",
"reserve_base_xrp": 20,
"reserve_inc_xrp": 5,
"seq": 6595042
},
"validation_quorum": 3
}
}
}
conn.send(createResponse(request, response));
} else if (conn.config.returnErrorOnServerInfo) {
conn.send(createResponse(request, fixtures.server_info.error));
} else if (conn.config.disconnectOnServerInfo) {
conn.close();