diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index b5b37ddbf1..825f69b185 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -11,6 +11,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr ### Fixed * Add missing fields (`Sequence`, `DomainID`) to `MPTokenIssuance` ledger type, add missing fields (`VaultID` and `LoanBrokerID`) to `AccountRoot` ledger type and missing fields (`AssetScale`, `MaximumAmount`, `TransferFee`, `MPTokenMetadata`, `LockedAmount`) to `vault_info` response `shares` object. Fix incorrect optionality of `Flags`, `ShareMPTID`, `WithdrawalPolicy`, and `OwnerNode` in `VaultInfoResponse`. +* Reverted [#3331](https://github.com/XRPLF/xrpl.js/pull/3331), which made `Client.getServerInfo()` and `Client.connect()` throw when the `server_info` request failed or the response omitted `network_id`. The SDK must not enforce `network_id` rules more strictly than a rippled node does for custom XRPL networks, so `getServerInfo()` once again logs such failures via `console.error` and leaves `client.networkID` undefined rather than throwing. ## 5.0.0 (2026-06-05) diff --git a/packages/xrpl/src/client/index.ts b/packages/xrpl/src/client/index.ts index 420d60058f..dec5fe98d6 100644 --- a/packages/xrpl/src/client/index.ts +++ b/packages/xrpl/src/client/index.ts @@ -523,19 +523,9 @@ class Client extends EventEmitter { } /** - * Get networkID and buildVersion from server_info. - * - * Throws if the underlying `server_info` request fails (e.g. `RippledError` - * for `noNetwork` / `notSynced`, `DisconnectedError`, `TimeoutError`), or if - * the response succeeds but does not include `network_id`. Callers must - * handle these — letting them propagate is intentional, since signing a - * transaction without a known network ID can produce a signature that is - * valid on the wrong network (cross-network replay). + * Get networkID and buildVersion from server_info * * @returns void - * @throws {XrplError} If the `server_info` response does not include a `network_id`. - * @throws {RippledError} If rippled returns an error (e.g. server not synced to network). - * @throws {DisconnectedError | TimeoutError | NotConnectedError} If the request cannot reach the server. * @example * ```ts * const { Client } = require('xrpl') @@ -546,15 +536,15 @@ class Client extends EventEmitter { * ``` */ public async getServerInfo(): Promise { - const response = await this.request({ - command: 'server_info', - }) - this.networkID = response.result.info.network_id ?? undefined - this.buildVersion = response.result.info.build_version - if (this.networkID === undefined) { - throw new XrplError( - 'server_info response is missing network_id; cannot safely sign transactions without a known network ID', - ) + try { + const response = await this.request({ + command: 'server_info', + }) + this.networkID = response.result.info.network_id ?? undefined + this.buildVersion = response.result.info.build_version + } catch (error) { + // eslint-disable-next-line no-console -- Print the error to console but allows client to be connected. + console.error(error) } } diff --git a/packages/xrpl/test/client/getServerInfo.test.ts b/packages/xrpl/test/client/getServerInfo.test.ts deleted file mode 100644 index adfc885906..0000000000 --- a/packages/xrpl/test/client/getServerInfo.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { assert } from 'chai' - -import { Client } from '../../src' -import createMockRippled, { - type MockedWebSocketServer, -} from '../createMockRippled' -import rippled from '../fixtures/rippled' -import { destroyServer, getFreePort } from '../testUtils' - -const TIMEOUT = 20000 - -describe('client.getServerInfo', function () { - let mockRippled: MockedWebSocketServer - let client: Client - let port: number - - beforeEach(async () => { - port = await getFreePort() - mockRippled = createMockRippled(port) - client = new Client(`ws://localhost:${port}`) - client.on('error', () => { - // Required to avoid unhandled error events from reconnect attempts - }) - }) - - afterEach(async () => { - if (client.isConnected()) { - await client.disconnect() - } - await new Promise((resolve) => { - mockRippled.close(() => resolve()) - }) - await destroyServer(port) - }) - - it( - 'connect() rejects when server_info request fails', - async () => { - mockRippled.addResponse('server_info', { - id: 0, - status: 'error', - type: 'response', - error: 'noNetwork', - error_code: 17, - error_message: 'Not synced to the network.', - request: { command: 'server_info', id: 0 }, - }) - - let connectError: Error | undefined - try { - await client.connect() - } catch (err) { - connectError = err as Error - } - - assert.isDefined( - connectError, - 'connect() should reject when server_info fails so that signed transactions cannot accidentally omit NetworkID', - ) - assert.strictEqual(client.networkID, undefined) - assert.strictEqual(client.buildVersion, undefined) - }, - TIMEOUT, - ) - - it( - 'getServerInfo() throws when server_info request fails', - async () => { - mockRippled.addResponse('server_info', rippled.server_info.withNetworkId) - await client.connect() - - mockRippled.addResponse('server_info', { - id: 0, - status: 'error', - type: 'response', - error: 'noNetwork', - error_code: 17, - error_message: 'Not synced to the network.', - request: { command: 'server_info', id: 0 }, - }) - - let getServerInfoError: Error | undefined - try { - await client.getServerInfo() - } catch (err) { - getServerInfoError = err as Error - } - - assert.isDefined( - getServerInfoError, - 'getServerInfo() should propagate the underlying request error rather than swallow it', - ) - }, - TIMEOUT, - ) - - it( - 'connect() populates networkID and buildVersion on success', - async () => { - mockRippled.addResponse('server_info', rippled.server_info.withNetworkId) - - await client.connect() - - assert.strictEqual( - client.networkID, - rippled.server_info.withNetworkId.result.info.network_id, - ) - assert.strictEqual( - client.buildVersion, - rippled.server_info.withNetworkId.result.info.build_version, - ) - }, - TIMEOUT, - ) - - it( - 'connect() rejects when server_info succeeds without network_id', - async () => { - const responseWithoutNetworkId = JSON.parse( - JSON.stringify(rippled.server_info.withNetworkId), - ) - delete responseWithoutNetworkId.result.info.network_id - mockRippled.addResponse('server_info', responseWithoutNetworkId) - - let connectError: Error | undefined - try { - await client.connect() - } catch (err) { - connectError = err as Error - } - - assert.isDefined( - connectError, - 'connect() should reject when server_info returns no network_id, since signing without a known network ID can produce cross-network-replayable transactions', - ) - assert.strictEqual(client.networkID, undefined) - }, - TIMEOUT, - ) - - it( - 'getServerInfo() throws when server_info succeeds without network_id', - async () => { - mockRippled.addResponse('server_info', rippled.server_info.withNetworkId) - await client.connect() - - const responseWithoutNetworkId = JSON.parse( - JSON.stringify(rippled.server_info.withNetworkId), - ) - delete responseWithoutNetworkId.result.info.network_id - mockRippled.addResponse('server_info', responseWithoutNetworkId) - - let getServerInfoError: Error | undefined - try { - await client.getServerInfo() - } catch (err) { - getServerInfoError = err as Error - } - - assert.isDefined( - getServerInfoError, - 'getServerInfo() should throw when server_info returns no network_id, since signing without a known network ID can produce cross-network-replayable transactions', - ) - }, - TIMEOUT, - ) -})