- Consolidated duplicate UndoManagers to single instance - Fixed connection promise to only resolve on 'connected' status - Fixed WebSocketProvider import (WebsocketProvider) - Added proper doc.destroy() cleanup - Renamed isPresenceInitialized property to avoid conflict Co-Authored-By: Paperclip <noreply@paperclip.ing>
598 lines
18 KiB
TypeScript
598 lines
18 KiB
TypeScript
import type { Errors, RpcRequest } from '../index.js'
|
||
import type {
|
||
Compute,
|
||
IsNarrowable,
|
||
IsNever,
|
||
OneOf,
|
||
UnionPartialBy,
|
||
} from './internal/types.js'
|
||
|
||
/** A JSON-RPC response object as per the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification#request_object). */
|
||
export type RpcResponse<
|
||
result = unknown,
|
||
error extends ErrorObject = ErrorObject,
|
||
> = Compute<
|
||
{
|
||
id: number
|
||
jsonrpc: '2.0'
|
||
} & OneOf<{ result: result } | { error: error }>
|
||
>
|
||
|
||
/** JSON-RPC error object as per the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification#error_object). */
|
||
export type ErrorObject = {
|
||
code: number
|
||
message: string
|
||
data?: unknown | undefined
|
||
}
|
||
|
||
/**
|
||
* A type-safe interface to instantiate a JSON-RPC response object as per the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification#response_object).
|
||
*
|
||
* @example
|
||
* ### Instantiating a Response Object
|
||
*
|
||
* ```ts twoslash
|
||
* import { RpcResponse } from 'ox'
|
||
*
|
||
* const response = RpcResponse.from({
|
||
* id: 0,
|
||
* jsonrpc: '2.0',
|
||
* result: '0x69420',
|
||
* })
|
||
* ```
|
||
*
|
||
* @example
|
||
* ### Type-safe Instantiation
|
||
*
|
||
* If you have a JSON-RPC request object, you can use it to strongly-type the response. If a `request` is provided,
|
||
* then the `id` and `jsonrpc` properties will be overridden with the values from the request.
|
||
*
|
||
* ```ts twoslash
|
||
* import { RpcRequest, RpcResponse } from 'ox'
|
||
*
|
||
* const request = RpcRequest.from({ id: 0, method: 'eth_blockNumber' })
|
||
*
|
||
* const response = RpcResponse.from(
|
||
* { result: '0x69420' },
|
||
* { request },
|
||
* )
|
||
* ```
|
||
*
|
||
* @param response - Opaque JSON-RPC response object.
|
||
* @param options - Parsing options.
|
||
* @returns Typed JSON-RPC result, or response object (if `raw` is `true`).
|
||
*/
|
||
export function from<
|
||
request extends RpcRequest.RpcRequest | undefined = undefined,
|
||
const response =
|
||
| (request extends RpcRequest.RpcRequest
|
||
? request['_returnType']
|
||
: RpcResponse)
|
||
| unknown,
|
||
>(
|
||
response: from.Response<request, response>,
|
||
options?: from.Options<request>,
|
||
): Compute<from.ReturnType<response>>
|
||
// eslint-disable-next-line jsdoc/require-jsdoc
|
||
export function from(response: RpcResponse, options: any = {}): RpcResponse {
|
||
const { request } = options
|
||
return {
|
||
...response,
|
||
id: response.id ?? request?.id,
|
||
jsonrpc: response.jsonrpc ?? request.jsonrpc,
|
||
}
|
||
}
|
||
|
||
export declare namespace from {
|
||
type Response<
|
||
request extends RpcRequest.RpcRequest | undefined = undefined,
|
||
response = unknown,
|
||
> = response &
|
||
(request extends RpcRequest.RpcRequest
|
||
? UnionPartialBy<RpcResponse<request['_returnType']>, 'id' | 'jsonrpc'>
|
||
: RpcResponse)
|
||
|
||
type Options<
|
||
request extends RpcRequest.RpcRequest | undefined =
|
||
| RpcRequest.RpcRequest
|
||
| undefined,
|
||
> = {
|
||
request?: request | RpcRequest.RpcRequest | undefined
|
||
}
|
||
|
||
type ReturnType<response> = IsNarrowable<response, RpcResponse> extends true
|
||
? RpcResponse
|
||
: response & Readonly<{ id: number; jsonrpc: '2.0' }>
|
||
}
|
||
|
||
/**
|
||
* A type-safe interface to parse a JSON-RPC response object as per the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification#response_object), and extract the result.
|
||
*
|
||
* @example
|
||
* ```ts twoslash
|
||
* import { RpcRequest, RpcResponse } from 'ox'
|
||
*
|
||
* // 1. Create a request store.
|
||
* const store = RpcRequest.createStore()
|
||
*
|
||
* // 2. Get a request object.
|
||
* const request = store.prepare({
|
||
* method: 'eth_getBlockByNumber',
|
||
* params: ['0x1', false],
|
||
* })
|
||
*
|
||
* // 3. Send the JSON-RPC request via HTTP.
|
||
* const block = await fetch('https://1.rpc.thirdweb.com', {
|
||
* body: JSON.stringify(request),
|
||
* headers: {
|
||
* 'Content-Type': 'application/json',
|
||
* },
|
||
* method: 'POST',
|
||
* })
|
||
* .then((response) => response.json())
|
||
* // 4. Parse the JSON-RPC response into a type-safe result. // [!code focus]
|
||
* .then((response) => RpcResponse.parse(response, { request })) // [!code focus]
|
||
*
|
||
* block // [!code focus]
|
||
* // ^?
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*
|
||
* ```
|
||
*
|
||
* :::tip
|
||
*
|
||
* If you don't need the return type, you can omit the options entirely.
|
||
*
|
||
* ```ts twoslash
|
||
* // @noErrors
|
||
* import { RpcResponse } from 'ox'
|
||
*
|
||
* const block = await fetch('https://1.rpc.thirdweb.com', {})
|
||
* .then((response) => response.json())
|
||
* .then((response) => RpcResponse.parse(response, { request })) // [!code --]
|
||
* .then(RpcResponse.parse) // [!code ++]
|
||
* ```
|
||
* :::
|
||
*
|
||
* @example
|
||
* ### Raw Mode
|
||
*
|
||
* If `raw` is `true`, the response will be returned as an object with `result` and `error` properties instead of returning the `result` directly and throwing errors.
|
||
*
|
||
* ```ts twoslash
|
||
* import { RpcRequest, RpcResponse } from 'ox'
|
||
*
|
||
* const store = RpcRequest.createStore()
|
||
*
|
||
* const request = store.prepare({
|
||
* method: 'eth_blockNumber',
|
||
* })
|
||
*
|
||
* const response = RpcResponse.parse({}, {
|
||
* request,
|
||
* raw: true, // [!code hl]
|
||
* })
|
||
*
|
||
* response.result
|
||
* // ^?
|
||
*
|
||
*
|
||
* response.error
|
||
* // ^?
|
||
*
|
||
*
|
||
* ```
|
||
*
|
||
* @param response - Opaque JSON-RPC response object.
|
||
* @param options - Parsing options.
|
||
* @returns Typed JSON-RPC result, or response object (if `raw` is `true`).
|
||
*/
|
||
export function parse<
|
||
const response extends RpcResponse | unknown,
|
||
returnType,
|
||
raw extends boolean = false,
|
||
>(
|
||
response: response,
|
||
options: parse.Options<returnType, raw> = {},
|
||
): parse.ReturnType<
|
||
unknown extends response
|
||
? returnType
|
||
: response extends RpcResponse
|
||
? response extends { result: infer result }
|
||
? result
|
||
: never
|
||
: returnType,
|
||
raw
|
||
> {
|
||
const { raw = false } = options
|
||
const response_ = response as RpcResponse
|
||
if (raw) return response as never
|
||
if (response_.error) throw parseError(response_.error)
|
||
return response_.result as never
|
||
}
|
||
|
||
export declare namespace parse {
|
||
type Options<returnType, raw extends boolean = false> = {
|
||
/**
|
||
* JSON-RPC Method that was used to make the request. Used for typing the response.
|
||
*/
|
||
request?:
|
||
| {
|
||
_returnType: returnType
|
||
}
|
||
| RpcRequest.RpcRequest
|
||
| undefined
|
||
/**
|
||
* Enables raw mode – responses will return an object with `result` and `error` properties instead of returning the `result` directly and throwing errors.
|
||
*
|
||
* - `true`: a JSON-RPC response object will be returned with `result` and `error` properties.
|
||
* - `false`: the JSON-RPC response object's `result` property will be returned directly, and JSON-RPC Errors will be thrown.
|
||
*
|
||
* @default false
|
||
*/
|
||
raw?: raw | boolean | undefined
|
||
}
|
||
|
||
type ReturnType<returnType, raw extends boolean = false> = Compute<
|
||
raw extends true ? RpcResponse<returnType> : returnType
|
||
>
|
||
|
||
type ErrorType =
|
||
| ParseError
|
||
| InvalidInputError
|
||
| ResourceNotFoundError
|
||
| ResourceUnavailableError
|
||
| TransactionRejectedError
|
||
| MethodNotSupportedError
|
||
| LimitExceededError
|
||
| VersionNotSupportedError
|
||
| InvalidRequestError
|
||
| MethodNotFoundError
|
||
| InvalidParamsError
|
||
| InternalError
|
||
| BaseErrorType
|
||
| Errors.GlobalErrorType
|
||
}
|
||
|
||
/**
|
||
* Parses a JSON-RPC error object into an error instance.
|
||
*
|
||
* @example
|
||
* ```ts twoslash
|
||
* import { RpcResponse } from 'ox'
|
||
*
|
||
* const error = RpcResponse.parseError({ code: -32000, message: 'unsupported method' })
|
||
*
|
||
* error
|
||
* // ^?
|
||
*
|
||
* ```
|
||
*
|
||
* @param errorObject - JSON-RPC error object.
|
||
* @returns Error instance.
|
||
*/
|
||
export function parseError<const errorObject extends ErrorObject | unknown>(
|
||
errorObject: errorObject | ErrorObject,
|
||
): parseError.ReturnType<errorObject> {
|
||
const errorObject_ = errorObject as ErrorObject
|
||
const { code } = errorObject_
|
||
if (code === InternalError.code)
|
||
return new InternalError(errorObject_) as never
|
||
if (code === InvalidInputError.code)
|
||
return new InvalidInputError(errorObject_) as never
|
||
if (code === InvalidParamsError.code)
|
||
return new InvalidParamsError(errorObject_) as never
|
||
if (code === InvalidRequestError.code)
|
||
return new InvalidRequestError(errorObject_) as never
|
||
if (code === LimitExceededError.code)
|
||
return new LimitExceededError(errorObject_) as never
|
||
if (code === MethodNotFoundError.code)
|
||
return new MethodNotFoundError(errorObject_) as never
|
||
if (code === MethodNotSupportedError.code)
|
||
return new MethodNotSupportedError(errorObject_) as never
|
||
if (code === ParseError.code) return new ParseError(errorObject_) as never
|
||
if (code === ResourceNotFoundError.code)
|
||
return new ResourceNotFoundError(errorObject_) as never
|
||
if (code === ResourceUnavailableError.code)
|
||
return new ResourceUnavailableError(errorObject_) as never
|
||
if (code === TransactionRejectedError.code)
|
||
return new TransactionRejectedError(errorObject_) as never
|
||
if (code === VersionNotSupportedError.code)
|
||
return new VersionNotSupportedError(errorObject_) as never
|
||
return new InternalError({
|
||
data: errorObject_,
|
||
message: errorObject_.message,
|
||
}) as never
|
||
}
|
||
|
||
export declare namespace parseError {
|
||
type ReturnType<
|
||
errorObject extends ErrorObject | unknown,
|
||
//
|
||
error = errorObject extends ErrorObject
|
||
?
|
||
| (errorObject['code'] extends InternalError['code']
|
||
? InternalError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? InternalError
|
||
: never)
|
||
| (errorObject['code'] extends InvalidInputError['code']
|
||
? InvalidInputError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? InvalidInputError
|
||
: never)
|
||
| (errorObject['code'] extends ResourceNotFoundError['code']
|
||
? ResourceNotFoundError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? ResourceNotFoundError
|
||
: never)
|
||
| (errorObject['code'] extends ResourceUnavailableError['code']
|
||
? ResourceUnavailableError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? ResourceUnavailableError
|
||
: never)
|
||
| (errorObject['code'] extends TransactionRejectedError['code']
|
||
? TransactionRejectedError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? TransactionRejectedError
|
||
: never)
|
||
| (errorObject['code'] extends ParseError['code']
|
||
? ParseError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? ParseError
|
||
: never)
|
||
| (errorObject['code'] extends MethodNotSupportedError['code']
|
||
? MethodNotSupportedError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? MethodNotSupportedError
|
||
: never)
|
||
| (errorObject['code'] extends LimitExceededError['code']
|
||
? LimitExceededError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? LimitExceededError
|
||
: never)
|
||
| (errorObject['code'] extends VersionNotSupportedError['code']
|
||
? VersionNotSupportedError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? VersionNotSupportedError
|
||
: never)
|
||
| (errorObject['code'] extends InvalidRequestError['code']
|
||
? InvalidRequestError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? InvalidRequestError
|
||
: never)
|
||
| (errorObject['code'] extends MethodNotFoundError['code']
|
||
? MethodNotFoundError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? MethodNotFoundError
|
||
: never)
|
||
| (errorObject['code'] extends InvalidParamsError['code']
|
||
? InvalidParamsError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? InvalidParamsError
|
||
: never)
|
||
| (IsNarrowable<errorObject['code'], number> extends false
|
||
? BaseError
|
||
: never)
|
||
: parseError.ReturnType<ErrorObject>,
|
||
> = IsNever<error> extends true ? BaseError : error
|
||
}
|
||
|
||
export type BaseErrorType = BaseError & { name: 'BaseError' }
|
||
|
||
/** Thrown when a JSON-RPC error has occurred. */
|
||
export class BaseError extends Error {
|
||
override name = 'RpcResponse.BaseError'
|
||
|
||
readonly code: number
|
||
readonly data?: unknown | undefined
|
||
|
||
constructor(errorObject: ErrorObject) {
|
||
const { code, message, data } = errorObject
|
||
super(message)
|
||
this.code = code
|
||
this.data = data
|
||
}
|
||
}
|
||
|
||
/** Thrown when the input to a JSON-RPC method is invalid. */
|
||
export class InvalidInputError extends BaseError {
|
||
static readonly code = -32000
|
||
override readonly code = -32000
|
||
override readonly name = 'RpcResponse.InvalidInputError'
|
||
|
||
constructor(parameters: Partial<Omit<ErrorObject, 'code'>> = {}) {
|
||
super({
|
||
code: InvalidInputError.code,
|
||
data: parameters.data,
|
||
message: parameters.message ?? 'Missing or invalid parameters.',
|
||
})
|
||
}
|
||
}
|
||
|
||
/** Thrown when a JSON-RPC resource is not found. */
|
||
export class ResourceNotFoundError extends BaseError {
|
||
static readonly code = -32001
|
||
override readonly code = -32001
|
||
override readonly name = 'RpcResponse.ResourceNotFoundError'
|
||
|
||
constructor(parameters: Partial<Omit<ErrorObject, 'code'>> = {}) {
|
||
super({
|
||
code: ResourceNotFoundError.code,
|
||
data: parameters.data,
|
||
message: parameters.message ?? 'Requested resource not found.',
|
||
})
|
||
}
|
||
}
|
||
|
||
/** Thrown when a JSON-RPC resource is unavailable. */
|
||
export class ResourceUnavailableError extends BaseError {
|
||
static readonly code = -32002
|
||
override readonly code = -32002
|
||
override readonly name = 'RpcResponse.ResourceUnavailableError'
|
||
|
||
constructor(parameters: Partial<Omit<ErrorObject, 'code'>> = {}) {
|
||
super({
|
||
code: ResourceUnavailableError.code,
|
||
data: parameters.data,
|
||
message: parameters.message ?? 'Requested resource not available.',
|
||
})
|
||
}
|
||
}
|
||
|
||
/** Thrown when a JSON-RPC transaction is rejected. */
|
||
export class TransactionRejectedError extends BaseError {
|
||
static readonly code = -32003
|
||
override readonly code = -32003
|
||
override readonly name = 'RpcResponse.TransactionRejectedError'
|
||
|
||
constructor(parameters: Partial<Omit<ErrorObject, 'code'>> = {}) {
|
||
super({
|
||
code: TransactionRejectedError.code,
|
||
data: parameters.data,
|
||
message: parameters.message ?? 'Transaction creation failed.',
|
||
})
|
||
}
|
||
}
|
||
|
||
/** Thrown when a JSON-RPC method is not supported. */
|
||
export class MethodNotSupportedError extends BaseError {
|
||
static readonly code = -32004
|
||
override readonly code = -32004
|
||
override readonly name = 'RpcResponse.MethodNotSupportedError'
|
||
|
||
constructor(parameters: Partial<Omit<ErrorObject, 'code'>> = {}) {
|
||
super({
|
||
code: MethodNotSupportedError.code,
|
||
data: parameters.data,
|
||
message: parameters.message ?? 'Method is not implemented.',
|
||
})
|
||
}
|
||
}
|
||
|
||
/** Thrown when a rate-limit is exceeded. */
|
||
export class LimitExceededError extends BaseError {
|
||
static readonly code = -32005
|
||
override readonly code = -32005
|
||
override readonly name = 'RpcResponse.LimitExceededError'
|
||
|
||
constructor(parameters: Partial<Omit<ErrorObject, 'code'>> = {}) {
|
||
super({
|
||
code: LimitExceededError.code,
|
||
data: parameters.data,
|
||
message: parameters.message ?? 'Rate limit exceeded.',
|
||
})
|
||
}
|
||
}
|
||
|
||
/** Thrown when a JSON-RPC version is not supported. */
|
||
export class VersionNotSupportedError extends BaseError {
|
||
static readonly code = -32006
|
||
override readonly code = -32006
|
||
override readonly name = 'RpcResponse.VersionNotSupportedError'
|
||
|
||
constructor(parameters: Partial<Omit<ErrorObject, 'code'>> = {}) {
|
||
super({
|
||
code: VersionNotSupportedError.code,
|
||
data: parameters.data,
|
||
message: parameters.message ?? 'JSON-RPC version not supported.',
|
||
})
|
||
}
|
||
}
|
||
|
||
/** Thrown when a JSON-RPC request is invalid. */
|
||
export class InvalidRequestError extends BaseError {
|
||
static readonly code = -32600
|
||
override readonly code = -32600
|
||
override readonly name = 'RpcResponse.InvalidRequestError'
|
||
|
||
constructor(parameters: Partial<Omit<ErrorObject, 'code'>> = {}) {
|
||
super({
|
||
code: InvalidRequestError.code,
|
||
data: parameters.data,
|
||
message: parameters.message ?? 'Input is not a valid JSON-RPC request.',
|
||
})
|
||
}
|
||
}
|
||
|
||
/** Thrown when a JSON-RPC method is not found. */
|
||
export class MethodNotFoundError extends BaseError {
|
||
static readonly code = -32601
|
||
override readonly code = -32601
|
||
override readonly name = 'RpcResponse.MethodNotFoundError'
|
||
|
||
constructor(parameters: Partial<Omit<ErrorObject, 'code'>> = {}) {
|
||
super({
|
||
code: MethodNotFoundError.code,
|
||
data: parameters.data,
|
||
message: parameters.message ?? 'Method does not exist.',
|
||
})
|
||
}
|
||
}
|
||
|
||
/** Thrown when the parameters to a JSON-RPC method are invalid. */
|
||
export class InvalidParamsError extends BaseError {
|
||
static readonly code = -32602
|
||
override readonly code = -32602
|
||
override readonly name = 'RpcResponse.InvalidParamsError'
|
||
|
||
constructor(parameters: Partial<Omit<ErrorObject, 'code'>> = {}) {
|
||
super({
|
||
code: InvalidParamsError.code,
|
||
data: parameters.data,
|
||
message: parameters.message ?? 'Invalid method parameters.',
|
||
})
|
||
}
|
||
}
|
||
|
||
/** Thrown when an internal JSON-RPC error has occurred. */
|
||
export class InternalError extends BaseError {
|
||
static readonly code = -32603
|
||
override readonly code = -32603
|
||
override readonly name = 'RpcResponse.InternalError'
|
||
|
||
constructor(parameters: Partial<Omit<ErrorObject, 'code'>> = {}) {
|
||
super({
|
||
code: InternalError.code,
|
||
data: parameters.data,
|
||
message: parameters.message ?? 'Internal JSON-RPC error.',
|
||
})
|
||
}
|
||
}
|
||
|
||
/** Thrown when a JSON-RPC response is invalid. */
|
||
export class ParseError extends BaseError {
|
||
static readonly code = -32700
|
||
override readonly code = -32700
|
||
override readonly name = 'RpcResponse.ParseError'
|
||
|
||
constructor(parameters: Partial<Omit<ErrorObject, 'code'>> = {}) {
|
||
super({
|
||
code: ParseError.code,
|
||
data: parameters.data,
|
||
message: parameters.message ?? 'Failed to parse JSON-RPC response.',
|
||
})
|
||
}
|
||
}
|