From f175e86c180c7a5719cd1bc93cba6c54a8c6d4ae Mon Sep 17 00:00:00 2001 From: Justin Reynolds Date: Wed, 3 Apr 2024 00:41:59 -0500 Subject: [PATCH] example of types --- packages/xrpl/src/client/RequestManager.ts | 20 +-- packages/xrpl/src/client/connection.ts | 31 +++- packages/xrpl/src/client/index.ts | 13 +- .../xrpl/src/models/methods/accountInfo.ts | 40 ++++- .../xrpl/src/models/methods/baseMethod.ts | 10 +- packages/xrpl/src/models/methods/index.ts | 170 +++++++++++++++++- packages/xrpl/src/utils/index.ts | 4 +- packages/xrpl/test/connection.test.ts | 6 +- 8 files changed, 248 insertions(+), 46 deletions(-) diff --git a/packages/xrpl/src/client/RequestManager.ts b/packages/xrpl/src/client/RequestManager.ts index f9cff40da4..2655f63d56 100644 --- a/packages/xrpl/src/client/RequestManager.ts +++ b/packages/xrpl/src/client/RequestManager.ts @@ -171,11 +171,8 @@ export default class RequestManager { * @param response - The response to handle. * @throws ResponseFormatError if the response format is invalid, RippledError if rippled returns an error. */ - public handleResponse(response: Partial): void { - if ( - response.id == null || - !(typeof response.id === 'string' || typeof response.id === 'number') - ) { + public handleResponse(response: Response | ErrorResponse): void { + if (!(typeof response.id === 'string' || typeof response.id === 'number')) { throw new ResponseFormatError('valid id not found in response', response) } if (!this.promisesAwaitingResponse.has(response.id)) { @@ -185,12 +182,11 @@ export default class RequestManager { const error = new ResponseFormatError('Response has no status') this.reject(response.id, error) } - if (response.status === 'error') { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We know this must be true - const errorResponse = response as Partial + // Error response type + if (!('result' in response)) { const error = new RippledError( - errorResponse.error_message ?? errorResponse.error, - errorResponse, + response.error_message ?? response.error, + response, ) this.reject(response.id, error) return @@ -205,8 +201,8 @@ export default class RequestManager { } // status no longer needed because error is thrown if status is not "success" delete response.status - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Must be a valid Response here - this.resolve(response.id, response as unknown as Response) + + this.resolve(response.id, response) } /** diff --git a/packages/xrpl/src/client/connection.ts b/packages/xrpl/src/client/connection.ts index a8796e9218..13d0c95b75 100644 --- a/packages/xrpl/src/client/connection.ts +++ b/packages/xrpl/src/client/connection.ts @@ -11,7 +11,8 @@ import { ConnectionError, XrplError, } from '../errors' -import type { RequestResponseMap } from '../models' +import { ErrorResponse } from '../models' +import type { RequestResponseMap, Response } from '../models' import { BaseRequest } from '../models/methods/baseMethod' import ConnectionManager from './ConnectionManager' @@ -323,26 +324,40 @@ export class Connection extends EventEmitter { * * @param message - The message received from the server. */ - private onMessage(message): void { + // eslint-disable-next-line complexity -- It is fine to have a high complexity here. + private onMessage(message: string): void { this.trace('receive', message) - let data: Record + let data: Response | ErrorResponse | null = null + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Typeguard function + function isResponseOrError(obj: any): obj is Response | ErrorResponse { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Made it safe with ? + return 'result' in obj || obj?.status === 'error' + } try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Must be a JSON dictionary - data = JSON.parse(message) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Only away to do a typeguard + const parsed = JSON.parse(message) + + if (isResponseOrError(parsed)) { + data = parsed + } } catch (error) { if (error instanceof Error) { this.emit('error', 'badMessage', error.message, message) } return } - if (data.type == null && data.error) { + if (data === null) { + this.emit('error', 'badMessage', 'Invalid JSON', message) + return + } + if (!('result' in data) && data.type == null && data.error) { // e.g. slowDown this.emit('error', data.error, data.error_message, data) return } if (data.type) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Should be true - this.emit(data.type as string, data) + this.emit(data.type, data) } if (data.type === 'response') { try { diff --git a/packages/xrpl/src/client/index.ts b/packages/xrpl/src/client/index.ts index 5d2c881592..287d56f771 100644 --- a/packages/xrpl/src/client/index.ts +++ b/packages/xrpl/src/client/index.ts @@ -32,7 +32,6 @@ import { import type { RequestResponseMap, RequestAllResponseMap, - MarkerRequest, MarkerResponse, SubmitResponse, } from '../models/methods' @@ -437,10 +436,11 @@ class Client extends EventEmitter { * const allResponses = await client.requestAll({ command: 'transaction_data' }); * console.log(allResponses); */ - public async requestAll< - T extends MarkerRequest, - U = RequestAllResponseMap, - >(request: T, collect?: string): Promise { + + public async requestAll>( + request: T, + collect?: string, + ): Promise { /* * The data under collection is keyed based on the command. Fail if command * not recognized and collection key not provided. @@ -453,7 +453,8 @@ class Client extends EventEmitter { * If limit is not provided, fetches all data over multiple requests. * NOTE: This may return much more than needed. Set limit when possible. */ - const countTo: number = request.limit == null ? Infinity : request.limit + const countTo: number = + typeof request.limit === 'number' ? request.limit : Infinity let count = 0 let marker: unknown = request.marker const results: U[] = [] diff --git a/packages/xrpl/src/models/methods/accountInfo.ts b/packages/xrpl/src/models/methods/accountInfo.ts index d872b4a100..94dccdcb28 100644 --- a/packages/xrpl/src/models/methods/accountInfo.ts +++ b/packages/xrpl/src/models/methods/accountInfo.ts @@ -138,7 +138,7 @@ export interface AccountInfoAccountFlags { * * @category Responses */ -export interface AccountInfoResponse extends BaseResponse { +export interface BaseAccountInfoResponse extends BaseResponse { result: { /** * The AccountRoot ledger object with this account's information, as stored @@ -148,7 +148,7 @@ export interface AccountInfoResponse extends BaseResponse { * at most one SignerList, this array must have exactly one member if it is * present. */ - account_data: AccountRoot & { signer_lists?: SignerList[] } + account_data: AccountRoot /** * A map of account flags parsed out. This will only be available for rippled nodes 1.11.0 and higher. @@ -180,3 +180,39 @@ export interface AccountInfoResponse extends BaseResponse { validated?: boolean } } + +export interface AccountInfoV1Response extends BaseAccountInfoResponse { + result: BaseAccountInfoResponse['result'] & { + /** + * The AccountRoot ledger object with this account's information, as stored + * in the ledger. + * If requested, also includes Array of SignerList ledger objects + * associated with this account for Multi-Signing. Since an account can own + * at most one SignerList, this array must have exactly one member if it is + * present. + */ + account_data: BaseAccountInfoResponse['result']['account_data'] & { + /** + * Array of SignerList ledger objects associated with this account for Multi-Signing. + * Since an account can own at most one SignerList, this array must have exactly one + * member if it is present. + * Quirk: In API version 1, this field is nested under account_data. For this method, + * Clio implements the API version 2 behavior where is field is not nested under account_data. + */ + signer_lists?: SignerList[] + } + } +} + +export interface AccountInfoV2Response extends BaseAccountInfoResponse { + result: BaseAccountInfoResponse['result'] & { + /** + * Array of SignerList ledger objects associated with this account for Multi-Signing. + * Since an account can own at most one SignerList, this array must have exactly one + * member if it is present. + * Quirk: In API version 1, this field is nested under account_data. For this method, + * Clio implements the API version 2 behavior where is field is not nested under account_data. + */ + signer_lists?: SignerList[] + } +} diff --git a/packages/xrpl/src/models/methods/baseMethod.ts b/packages/xrpl/src/models/methods/baseMethod.ts index 85dcf90efb..c43ae86a83 100644 --- a/packages/xrpl/src/models/methods/baseMethod.ts +++ b/packages/xrpl/src/models/methods/baseMethod.ts @@ -30,9 +30,9 @@ export interface ResponseWarning { } export interface BaseResponse { - id: number | string - status?: 'success' | string - type: 'response' | string + id: number | string | null + status?: 'success' | string | null + type: 'response' | string | null result: unknown warning?: 'load' warnings?: ResponseWarning[] @@ -47,9 +47,9 @@ export interface BaseResponse { * @category Responses */ export interface ErrorResponse { - id: number | string + id: number | string | null status: 'error' - type: 'response' | string + type: 'response' | string | null error: string error_code?: string error_message?: string diff --git a/packages/xrpl/src/models/methods/index.ts b/packages/xrpl/src/models/methods/index.ts index f6929f8cc9..32a6eca6c0 100644 --- a/packages/xrpl/src/models/methods/index.ts +++ b/packages/xrpl/src/models/methods/index.ts @@ -10,9 +10,10 @@ import { AccountCurrenciesResponse, } from './accountCurrencies' import { + AccountInfoV1Response, + AccountInfoV2Response, AccountInfoAccountFlags, AccountInfoRequest, - AccountInfoResponse, AccountQueueData, AccountQueueTransaction, } from './accountInfo' @@ -210,6 +211,7 @@ type Request = | NFTHistoryRequest // AMM methods | AMMInfoRequest + | BaseRequest /** * @category Responses @@ -218,7 +220,8 @@ type Response = // account methods | AccountChannelsResponse | AccountCurrenciesResponse - | AccountInfoResponse + | AccountInfoV1Response + | AccountInfoV2Response | AccountLinesResponse | AccountNFTsResponse | AccountObjectsResponse @@ -264,13 +267,153 @@ type Response = | NFTHistoryResponse // AMM methods | AMMInfoResponse + | BaseResponse -export type RequestResponseMap = T extends AccountChannelsRequest +// export type RequestResponseMap = T extends AccountChannelsRequest +// ? AccountChannelsResponse +// : T extends AccountCurrenciesRequest +// ? AccountCurrenciesResponse +// : T extends AccountInfoRequest +// ? AccountInfoResponse +// : T extends AccountLinesRequest +// ? AccountLinesResponse +// : T extends AccountNFTsRequest +// ? AccountNFTsResponse +// : T extends AccountObjectsRequest +// ? AccountObjectsResponse +// : T extends AccountOffersRequest +// ? AccountOffersResponse +// : T extends AccountTxRequest +// ? AccountTxResponse +// : T extends AMMInfoRequest +// ? AMMInfoResponse +// : T extends GatewayBalancesRequest +// ? GatewayBalancesResponse +// : T extends NoRippleCheckRequest +// ? NoRippleCheckResponse +// : // NOTE: The order of these LedgerRequest types is important +// // to get the proper type matching overrides based on parameters set +// // in the request. For example LedgerRequestExpandedTransactionsBinary +// // should match LedgerRequestExpandedTransactionsOnly, but not +// // LedgerRequestExpandedAccountsOnly. This is because the +// // LedgerRequestExpandedTransactionsBinary type is a superset of +// // LedgerRequestExpandedTransactionsOnly, but not of the other. +// // This is why LedgerRequestExpandedTransactionsBinary is listed +// // first in the type list. +// // +// // Here is an example using real data: +// // LedgerRequestExpandedTransactionsBinary = { +// // command: 'ledger', +// // ledger_index: 'validated', +// // expand: true, +// // transactions: true, +// // binary: true, +// // } +// // LedgerRequestExpandedTransactionsOnly = { +// // command: 'ledger', +// // ledger_index: 'validated', +// // expand: true, +// // transactions: true, +// // } +// // LedgerRequestExpandedAccountsOnly = { +// // command: 'ledger', +// // ledger_index: 'validated', +// // accounts: true, +// // expand: true, +// // } +// // LedgerRequest = { +// // command: 'ledger', +// // ledger_index: 'validated', +// // } +// // +// // The type with the most parameters set should be listed first. In this +// // case LedgerRequestExpandedTransactionsBinary has the most parameters (`expand`, `transactions`, and `binary`) +// // set, so it is listed first. When TypeScript tries to match the type of +// // a request to a response, it will try to match the request type to the +// // response type in the order they are listed. So, if we have a request +// // with the following parameters: +// // { +// // command: 'ledger', +// // ledger_index: 'validated', +// // expand: true, +// // transactions: true, +// // binary: true, +// // } +// // TypeScript will first try to match the request type to +// // LedgerRequestExpandedTransactionsBinary, which will succeed. It will +// // then try to match the response type to LedgerResponseExpanded, which +// // will also succeed. If we had listed LedgerRequestExpandedTransactionsOnly +// // first, TypeScript would have tried to match the request type to +// // LedgerRequestExpandedTransactionsOnly, which would have succeeded, but +// // then we'd get the wrong response type, LedgerResponse, instead of +// // LedgerResponseExpanded. +// T extends LedgerRequestExpandedTransactionsBinary +// ? LedgerResponse +// : T extends LedgerRequestExpandedAccountsAndTransactions +// ? LedgerResponseExpanded +// : T extends LedgerRequestExpandedTransactionsOnly +// ? LedgerResponseExpanded +// : T extends LedgerRequestExpandedAccountsOnly +// ? LedgerResponseExpanded +// : T extends LedgerRequest +// ? LedgerResponse +// : T extends LedgerClosedRequest +// ? LedgerClosedResponse +// : T extends LedgerCurrentRequest +// ? LedgerCurrentResponse +// : T extends LedgerDataRequest +// ? LedgerDataResponse +// : T extends LedgerEntryRequest +// ? LedgerEntryResponse +// : T extends SubmitRequest +// ? SubmitResponse +// : T extends SubmitMultisignedRequest +// ? SubmitMultisignedResponse +// : T extends TransactionEntryRequest +// ? TransactionEntryResponse +// : T extends TxRequest +// ? TxResponse +// : T extends BookOffersRequest +// ? BookOffersResponse +// : T extends DepositAuthorizedRequest +// ? DepositAuthorizedResponse +// : T extends PathFindRequest +// ? PathFindResponse +// : T extends RipplePathFindRequest +// ? RipplePathFindResponse +// : T extends ChannelVerifyRequest +// ? ChannelVerifyResponse +// : T extends SubscribeRequest +// ? SubscribeResponse +// : T extends UnsubscribeRequest +// ? UnsubscribeResponse +// : T extends FeeRequest +// ? FeeResponse +// : T extends ManifestRequest +// ? ManifestResponse +// : T extends ServerInfoRequest +// ? ServerInfoResponse +// : T extends ServerStateRequest +// ? ServerStateResponse +// : T extends ServerDefinitionsRequest +// ? ServerDefinitionsResponse +// : T extends PingRequest +// ? PingResponse +// : T extends RandomRequest +// ? RandomResponse +// : T extends NFTBuyOffersRequest +// ? NFTBuyOffersResponse +// : T extends NFTSellOffersRequest +// ? NFTSellOffersResponse +// : T extends NFTInfoRequest +// ? NFTInfoResponse +// : T extends NFTHistoryRequest +// ? NFTHistoryResponse +// : Response +export type RequestResponseMapBase = T extends AccountChannelsRequest ? AccountChannelsResponse : T extends AccountCurrenciesRequest ? AccountCurrenciesResponse - : T extends AccountInfoRequest - ? AccountInfoResponse : T extends AccountLinesRequest ? AccountLinesResponse : T extends AccountNFTsRequest @@ -405,7 +548,19 @@ export type RequestResponseMap = T extends AccountChannelsRequest ? NFTInfoResponse : T extends NFTHistoryRequest ? NFTHistoryResponse - : Response + : BaseResponse + +export type RequestResponseMapV1 = T extends AccountInfoRequest + ? AccountInfoV1Response + : RequestResponseMapBase + +export type RequestResponseMapV2 = T extends AccountInfoRequest + ? AccountInfoV1Response + : RequestResponseMapBase + +export type RequestResponseMap = T extends { api_version: 2 } + ? RequestResponseMapV2 + : RequestResponseMapV1 export type MarkerRequest = Request & { limit?: number @@ -451,7 +606,8 @@ export { AccountCurrenciesResponse, AccountInfoAccountFlags, AccountInfoRequest, - AccountInfoResponse, + AccountInfoV1Response, + AccountInfoV2Response, AccountQueueData, AccountQueueTransaction, AccountLinesRequest, diff --git a/packages/xrpl/src/utils/index.ts b/packages/xrpl/src/utils/index.ts index c96afd4151..867d7adf33 100644 --- a/packages/xrpl/src/utils/index.ts +++ b/packages/xrpl/src/utils/index.ts @@ -24,7 +24,7 @@ import { import { verify as verifyKeypairSignature } from 'ripple-keypairs' import { LedgerEntry } from '../models/ledger' -import { Response } from '../models/methods' +import { MarkerResponse } from '../models/methods' import { PaymentChannelClaim } from '../models/transactions/paymentChannelClaim' import { Transaction } from '../models/transactions/transaction' @@ -157,7 +157,7 @@ function isValidAddress(address: string): boolean { * @returns Whether the response has more pages of data. * @category Utilities */ -function hasNextPage(response: Response): boolean { +function hasNextPage(response: MarkerResponse): boolean { // eslint-disable-next-line @typescript-eslint/dot-notation -- only checking if it exists return Boolean(response.result['marker']) } diff --git a/packages/xrpl/test/connection.test.ts b/packages/xrpl/test/connection.test.ts index ef300f6088..cb2f3261a4 100644 --- a/packages/xrpl/test/connection.test.ts +++ b/packages/xrpl/test/connection.test.ts @@ -331,7 +331,6 @@ describe('Connection', function () { 'DisconnectedError', async () => { await clientContext.client - // @ts-expect-error -- Intentionally invalid command .request({ command: 'test_command', data: { closeServer: true } }) .then(() => { assert.fail('Should throw DisconnectedError') @@ -422,8 +421,8 @@ describe('Connection', function () { try { await clientContext.client.connect() - } catch (error) { - // @ts-expect-error -- Error has a message + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is correct for an error + } catch (error: any) { expect(error.message).toEqual( "Error: connect() timed out after 5000 ms. If your internet connection is working, the rippled server may be blocked or inaccessible. You can also try setting the 'connectionTimeout' option in the Client constructor.", ) @@ -442,7 +441,6 @@ describe('Connection', function () { async () => { await clientContext.client .request({ - // @ts-expect-error -- Intentionally invalid command command: 'test_command', data: { unrecognizedResponse: true }, })