diff --git a/src/client/connection.ts b/src/client/connection.ts index 69ad4f86..6a40842a 100644 --- a/src/client/connection.ts +++ b/src/client/connection.ts @@ -11,7 +11,7 @@ import { DisconnectedError, NotConnectedError, ConnectionError, - RippleError, + XrplError, } from '../common/errors' import { BaseRequest } from '../models/methods/baseMethod' @@ -217,7 +217,7 @@ export class Connection extends EventEmitter { } if (this.ws != null) { return Promise.reject( - new RippleError('Websocket connection never cleaned up.', { + new XrplError('Websocket connection never cleaned up.', { state: this.state, }), ) diff --git a/src/client/index.ts b/src/client/index.ts index dc06ee30..9481453b 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -21,8 +21,8 @@ import { } from 'ripple-address-codec' import { constants, errors, txFlags, ensureClassicAddress } from '../common' -import { RippledError, ValidationError } from '../common/errors' -import { getFee } from '../common/fee' +import { ValidationError, XrplError } from '../common/errors' +import getFee from '../common/fee' import getBalances from '../ledger/balances' import { getOrderbook, formatBidsAndAsks } from '../ledger/orderbook' import getPaths from '../ledger/pathfind' @@ -429,7 +429,7 @@ class Client extends EventEmitter { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Should be true const singleResult = (singleResponse as U).result if (!(collectKey in singleResult)) { - throw new RippledError(`${collectKey} not in result`) + throw new XrplError(`${collectKey} not in result`) } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Should be true const collectedData = singleResult[collectKey] diff --git a/src/common/ecdsa.ts b/src/common/ecdsa.ts index d166c2d1..93bd96e8 100644 --- a/src/common/ecdsa.ts +++ b/src/common/ecdsa.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line no-shadow -- No shadow here enum ECDSA { ed25519 = 'ed25519', secp256k1 = 'ecdsa-secp256k1', diff --git a/src/common/errors.ts b/src/common/errors.ts index d582da70..349ed241 100644 --- a/src/common/errors.ts +++ b/src/common/errors.ts @@ -1,22 +1,34 @@ +/* eslint-disable max-classes-per-file -- Errors can be defined in the same file */ import { inspect } from 'util' -class RippleError extends Error { - name: string - message: string - data?: any +// TODO: replace all `new Error`s with `new XrplError`s - constructor(message = '', data?: any) { +class XrplError extends Error { + public readonly name: string + public readonly message: string + public readonly data?: unknown + + /** + * Construct an XrplError. + * + * @param message - The error message. + * @param data - The data that caused the error. + */ + public constructor(message = '', data?: unknown) { super(message) this.name = this.constructor.name this.message = message this.data = data - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor) - } + Error.captureStackTrace(this, this.constructor) } - toString() { + /** + * Converts the Error to a human-readable String form. + * + * @returns The String output of the Error. + */ + public toString(): string { let result = `[${this.name}(${this.message}` if (this.data) { result += `, ${inspect(this.data)}` @@ -25,21 +37,25 @@ class RippleError extends Error { return result } - // console.log in node uses util.inspect on object, and util.inspect allows - // us to customize its output: - // https://nodejs.org/api/util.html#util_custom_inspect_function_on_objects - inspect() { + /** + * Console.log in node uses util.inspect on object, and util.inspect allows + * us to customize its output: + * https://nodejs.org/api/util.html#util_custom_inspect_function_on_objects. + * + * @returns The String output of the Error. + */ + public inspect(): string { return this.toString() } } -class RippledError extends RippleError {} +class RippledError extends XrplError {} -class UnexpectedError extends RippleError {} +class UnexpectedError extends XrplError {} -class LedgerVersionError extends RippleError {} +class LedgerVersionError extends XrplError {} -class ConnectionError extends RippleError {} +class ConnectionError extends XrplError {} class NotConnectedError extends ConnectionError {} @@ -51,34 +67,23 @@ class TimeoutError extends ConnectionError {} class ResponseFormatError extends ConnectionError {} -class ValidationError extends RippleError {} +class ValidationError extends XrplError {} -class XRPLFaucetError extends RippleError {} +class XRPLFaucetError extends XrplError {} -class NotFoundError extends RippleError { - constructor(message = 'Not found') { +class NotFoundError extends XrplError { + /** + * Construct an XrplError. + * + * @param message - The error message. Defaults to "Not found". + */ + public constructor(message = 'Not found') { super(message) } } -class MissingLedgerHistoryError extends RippleError { - constructor(message?: string) { - super(message || 'Server is missing ledger history in the specified range') - } -} - -class PendingLedgerVersionError extends RippleError { - constructor(message?: string) { - super( - message || - "maxLedgerVersion is greater than server's most recent" + - ' validated ledger', - ) - } -} - export { - RippleError, + XrplError, UnexpectedError, ConnectionError, RippledError, @@ -89,8 +94,6 @@ export { ResponseFormatError, ValidationError, NotFoundError, - PendingLedgerVersionError, - MissingLedgerHistoryError, LedgerVersionError, XRPLFaucetError, } diff --git a/src/common/fee.ts b/src/common/fee.ts index d830ebe7..9873d476 100644 --- a/src/common/fee.ts +++ b/src/common/fee.ts @@ -1,17 +1,24 @@ import BigNumber from 'bignumber.js' -import _ from 'lodash' import type { Client } from '..' -// This is a public API that can be called directly. -// This is not used by the `prepare*` methods. See `src/transaction/utils.ts` -async function getFee(this: Client, cushion?: number): Promise { - if (cushion == null) { - cushion = this.feeCushion - } - if (cushion == null) { - cushion = 1.2 - } +const NUM_DECIMAL_PLACES = 6 +const BASE_10 = 10 + +/** + * Calculates the current transaction fee for the ledger. + * Note: This is a public API that can be called directly. + * This is not used by the `prepare*` methods. See `src/transaction/utils.ts`. + * + * @param this - The Client used to connect to the ledger. + * @param cushion - The fee cushion to use. + * @returns The transaction fee. + */ +export default async function getFee( + this: Client, + cushion: number | null, +): Promise { + const feeCushion = cushion ?? this.feeCushion const serverInfo = (await this.request({ command: 'server_info' })).result .info @@ -27,12 +34,10 @@ async function getFee(this: Client, cushion?: number): Promise { // https://github.com/ripple/rippled/issues/3812#issuecomment-816871100 serverInfo.load_factor = 1 } - let fee = baseFeeXrp.times(serverInfo.load_factor).times(cushion) + let fee = baseFeeXrp.times(serverInfo.load_factor).times(feeCushion) // Cap fee to `this.maxFeeXRP` fee = BigNumber.min(fee, this.maxFeeXRP) // Round fee to 6 decimal places - return new BigNumber(fee.toFixed(6)).toString(10) + return new BigNumber(fee.toFixed(NUM_DECIMAL_PLACES)).toString(BASE_10) } - -export { getFee } diff --git a/src/common/index.ts b/src/common/index.ts index cb57b4d2..40d51dd3 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -3,6 +3,13 @@ import { xAddressToClassicAddress, isValidXAddress } from 'ripple-address-codec' import * as constants from './constants' import * as errors from './errors' +/** + * If an address is an X-Address, converts it to a classic address. + * + * @param account - A classic address or X-address. + * @returns The account's classic address. + * @throws Error if the X-Address has an associated tag. + */ export function ensureClassicAddress(account: string): string { if (isValidXAddress(account)) { const { classicAddress, tag } = xAddressToClassicAddress(account) diff --git a/src/models/methods/serverInfo.ts b/src/models/methods/serverInfo.ts index 27127808..94a423d1 100644 --- a/src/models/methods/serverInfo.ts +++ b/src/models/methods/serverInfo.ts @@ -51,7 +51,7 @@ export interface ServerInfoResponse extends BaseResponse { job_types: JobType[] threads: number } - load_factor: number + load_factor?: number load_factor_local?: number load_factor_net?: number load_factor_cluster?: number diff --git a/src/transaction/sign.ts b/src/transaction/sign.ts index 1ce284cb..94dee513 100644 --- a/src/transaction/sign.ts +++ b/src/transaction/sign.ts @@ -4,12 +4,12 @@ import binaryCodec from 'ripple-binary-codec' import keypairs from 'ripple-keypairs' import type { Client, Wallet } from '..' +import { ValidationError } from '../common/errors' import { SignedTransaction } from '../common/types/objects' import { xrpToDrops } from '../utils' import { computeBinaryTransactionHash } from '../utils/hashes' import { SignOptions, KeyPair, TransactionJSON } from './types' -import * as utils from './utils' function computeSignature(tx: object, privateKey: string, signAs?: string) { const signingData = signAs @@ -28,7 +28,7 @@ function signWithKeypair( ): SignedTransaction { const tx = JSON.parse(txJSON) if (tx.TxnSignature || tx.Signers) { - throw new utils.common.errors.ValidationError( + throw new ValidationError( 'txJSON must not contain "TxnSignature" or "Signers" properties', ) } @@ -148,7 +148,7 @@ function checkTxSerialization(serialized: string, tx: TransactionJSON): void { // ...And ensure it is equal to the original tx, except: // - It must have a TxnSignature or Signers (multisign). if (!decoded.TxnSignature && !decoded.Signers) { - throw new utils.common.errors.ValidationError( + throw new ValidationError( 'Serialized transaction must have a TxnSignature or Signers property', ) } @@ -182,14 +182,15 @@ function checkTxSerialization(serialized: string, tx: TransactionJSON): void { }) if (!_.isEqual(decoded, tx)) { - const error = new utils.common.errors.ValidationError( - 'Serialized transaction does not match original txJSON. See `error.data`', - ) - error.data = { + const data = { decoded, tx, diff: objectDiff(tx, decoded), } + const error = new ValidationError( + 'Serialized transaction does not match original txJSON. See `error.data`', + data, + ) throw error } } @@ -208,7 +209,7 @@ function checkFee(client: Client, txFee: string): void { const fee = new BigNumber(txFee) const maxFeeDrops = xrpToDrops(client.maxFeeXRP) if (fee.isGreaterThan(maxFeeDrops)) { - throw new utils.common.errors.ValidationError( + throw new ValidationError( `"Fee" should not exceed "${maxFeeDrops}". ` + 'To use a higher fee, set `maxFeeXRP` in the Client constructor.', ) @@ -234,9 +235,7 @@ function sign( } if (!keypair && !secret) { // Clearer message than 'ValidationError: instance is not exactly one from [subschema 0],[subschema 1]' - throw new utils.common.errors.ValidationError( - 'sign: Missing secret or keypair.', - ) + throw new ValidationError('sign: Missing secret or keypair.') } return signWithKeypair(this, txJSON, keypair || secret, options) } diff --git a/test/client/errors.ts b/test/client/errors.ts index 889114b4..ab81ff2b 100644 --- a/test/client/errors.ts +++ b/test/client/errors.ts @@ -6,9 +6,9 @@ describe('client errors', function () { beforeEach(setupClient.setup) afterEach(setupClient.teardown) - it('RippleError with data', async function () { - const error = new this.client.errors.RippleError('_message_', '_data_') - assert.strictEqual(error.toString(), "[RippleError(_message_, '_data_')]") + it('XrplError with data', async function () { + const error = new this.client.errors.XrplError('_message_', '_data_') + assert.strictEqual(error.toString(), "[XrplError(_message_, '_data_')]") }) it('NotFoundError default message', async function () { diff --git a/test/client/getFee.ts b/test/client/getFee.ts index 836cc7f2..9748d339 100644 --- a/test/client/getFee.ts +++ b/test/client/getFee.ts @@ -2,69 +2,57 @@ import { assert } from 'chai' import rippled from '../fixtures/rippled' import setupClient from '../setupClient' -import { addressTests } from '../testUtils' describe('client.getFee', function () { beforeEach(setupClient.setup) afterEach(setupClient.teardown) - addressTests.forEach(function (test) { - describe(test.type, function () { - it('getFee', async function () { - this.mockRippled.addResponse('server_info', rippled.server_info.normal) - const fee = await this.client.getFee() - assert.strictEqual(fee, '0.000012') - }) + it('getFee', async function () { + this.mockRippled.addResponse('server_info', rippled.server_info.normal) + const fee = await this.client.getFee() + assert.strictEqual(fee, '0.000012') + }) - it('getFee default', async function () { - this.mockRippled.addResponse('server_info', rippled.server_info.normal) - this.client.feeCushion = undefined as unknown as number - const fee = await this.client.getFee() - assert.strictEqual(fee, '0.000012') - }) + it('getFee - high load_factor', async function () { + this.mockRippled.addResponse( + 'server_info', + rippled.server_info.highLoadFactor, + ) + const fee = await this.client.getFee() + assert.strictEqual(fee, '2') + }) - it('getFee - high load_factor', async function () { - this.mockRippled.addResponse( - 'server_info', - rippled.server_info.highLoadFactor, - ) - const fee = await this.client.getFee() - assert.strictEqual(fee, '2') - }) + it('getFee - high load_factor with custom maxFeeXRP', async function () { + this.mockRippled.addResponse( + 'server_info', + rippled.server_info.highLoadFactor, + ) + // Ensure that overriding with high maxFeeXRP of '51540' causes no errors. + // (fee will actually be 51539.607552) + this.client.maxFeeXRP = '51540' + const fee = await this.client.getFee() + assert.strictEqual(fee, '51539.607552') + }) - it('getFee - high load_factor with custom maxFeeXRP', async function () { - this.mockRippled.addResponse( - 'server_info', - rippled.server_info.highLoadFactor, - ) - // Ensure that overriding with high maxFeeXRP of '51540' causes no errors. - // (fee will actually be 51539.607552) - this.client.maxFeeXRP = '51540' - const fee = await this.client.getFee() - assert.strictEqual(fee, '51539.607552') - }) + it('getFee custom cushion', async function () { + this.mockRippled.addResponse('server_info', rippled.server_info.normal) + this.client.feeCushion = 1.4 + const fee = await this.client.getFee() + assert.strictEqual(fee, '0.000014') + }) - it('getFee custom cushion', async function () { - this.mockRippled.addResponse('server_info', rippled.server_info.normal) - this.client.feeCushion = 1.4 - const fee = await this.client.getFee() - assert.strictEqual(fee, '0.000014') - }) + // This is not recommended since it may result in attempting to pay + // less than the base fee. However, this test verifies the existing behavior. + it('getFee cushion less than 1.0', async function () { + this.mockRippled.addResponse('server_info', rippled.server_info.normal) + this.client.feeCushion = 0.9 + const fee = await this.client.getFee() + assert.strictEqual(fee, '0.000009') + }) - // This is not recommended since it may result in attempting to pay - // less than the base fee. However, this test verifies the existing behavior. - it('getFee cushion less than 1.0', async function () { - this.mockRippled.addResponse('server_info', rippled.server_info.normal) - this.client.feeCushion = 0.9 - const fee = await this.client.getFee() - assert.strictEqual(fee, '0.000009') - }) - - it('getFee reporting', async function () { - this.mockRippled.addResponse('server_info', rippled.server_info.normal) - const fee = await this.client.getFee() - assert.strictEqual(fee, '0.000012') - }) - }) + it('getFee reporting', async function () { + this.mockRippled.addResponse('server_info', rippled.server_info.normal) + const fee = await this.client.getFee() + assert.strictEqual(fee, '0.000012') }) }) diff --git a/test/client/getPaths.ts b/test/client/getPaths.ts index 17d5899a..8d634d2e 100644 --- a/test/client/getPaths.ts +++ b/test/client/getPaths.ts @@ -97,7 +97,7 @@ describe('client.getPaths', function () { ...REQUEST_FIXTURES.normal, source: { address: addresses.NOTFOUND }, }), - this.client.errors.RippleError, + this.client.errors.XrplError, ) }) // 'send all', function () { diff --git a/test/connection.ts b/test/connection.ts index b2512dc1..bfa97cbc 100644 --- a/test/connection.ts +++ b/test/connection.ts @@ -11,7 +11,7 @@ import { DisconnectedError, NotConnectedError, ResponseFormatError, - RippleError, + XrplError, TimeoutError, } from '../src/common/errors' @@ -207,11 +207,11 @@ describe('Connection', function () { }) it('DisconnectedError on initial onOpen send', async function () { - // _onOpen previously could throw PromiseRejectionHandledWarning: Promise rejection was handled asynchronously + // onOpen previously could throw PromiseRejectionHandledWarning: Promise rejection was handled asynchronously // do not rely on the client.setup hook to test this as it bypasses the case, disconnect client connection first await this.client.disconnect() - // stub _onOpen to only run logic relevant to test case + // stub onOpen to only run logic relevant to test case this.client.connection.onOpen = () => { // overload websocket send on open when _ws exists this.client.connection.ws.send = function (_0, _1, _2) { @@ -420,7 +420,7 @@ describe('Connection', function () { new Client({ servers: ['wss://server1.com', 'wss://server2.com'], } as any) - }, RippleError) + }, XrplError) }) it('connect throws error', function (done) { diff --git a/test/integration/integration.ts b/test/integration/integration.ts index 451433ac..66611205 100644 --- a/test/integration/integration.ts +++ b/test/integration/integration.ts @@ -4,7 +4,6 @@ import _ from 'lodash' import { isValidXAddress } from 'ripple-address-codec' import { Client } from 'xrpl-local' -import { errors } from 'xrpl-local/common' import { isValidSecret } from 'xrpl-local/utils' import { generateXAddress } from '../../src/utils/generateAddress' @@ -46,24 +45,7 @@ function verifyTransaction(testcase, hash, type, options, txData, account) { } return { txJSON: JSON.stringify(txData), id: hash, tx: data } }) - .catch(async (error) => { - if (error instanceof errors.PendingLedgerVersionError) { - console.log('NOT VALIDATED YET...') - return new Promise((resolve, reject) => { - setTimeout( - () => - verifyTransaction( - testcase, - hash, - type, - options, - txData, - account, - ).then(resolve, reject), - INTERVAL, - ) - }) - } + .catch((error) => { console.log(error.stack) assert(false, `Transaction not successful: ${error.message}`) })