Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions src/actions/public/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import type { ErrorType } from '../../errors/utils.js'
import type { BlockTag } from '../../types/block.js'
import type { Chain } from '../../types/chain.js'
import type { EIP1193RequestOptions } from '../../types/eip1193.js'
import type { Hex } from '../../types/misc.js'
import type { RpcTransactionRequest } from '../../types/rpc.js'
import type { StateOverride } from '../../types/stateOverride.js'
Expand Down Expand Up @@ -92,6 +93,8 @@ export type CallParameters<
factory?: Address | undefined
/** Calldata to execute on the factory to deploy the contract. */
factoryData?: Hex | undefined
/** Request options. */
requestOptions?: EIP1193RequestOptions | undefined
/** State overrides for the call. */
stateOverride?: StateOverride | undefined
} & (
Expand Down Expand Up @@ -174,6 +177,7 @@ export async function call<chain extends Chain | undefined>(
maxFeePerGas,
maxPriorityFeePerGas,
nonce,
requestOptions,
to,
value,
stateOverride,
Expand Down Expand Up @@ -276,10 +280,13 @@ export async function call<chain extends Chain | undefined>(
return base
})()

const response = await client.request({
method: 'eth_call',
params,
})
const response = await client.request(
{
method: 'eth_call',
params,
},
requestOptions,
)
if (response === '0x') return { data: undefined }
return { data: response }
} catch (err) {
Expand Down Expand Up @@ -428,10 +435,7 @@ type ToDeploylessCallViaBytecodeDataErrorType =
| EncodeDeployDataErrorType
| ErrorType

function toDeploylessCallViaBytecodeData(parameters: {
code: Hex
data: Hex
}) {
function toDeploylessCallViaBytecodeData(parameters: { code: Hex; data: Hex }) {
const { code, data } = parameters
return encodeDeployData({
abi: parseAbi(['constructor(bytes, bytes)']),
Expand Down
8 changes: 6 additions & 2 deletions src/actions/public/getBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import type { ErrorType } from '../../errors/utils.js'
import type { BlockTag } from '../../types/block.js'
import type { Chain } from '../../types/chain.js'
import type { EIP1193RequestOptions } from '../../types/eip1193.js'
import type { Hash } from '../../types/misc.js'
import type { RpcBlock } from '../../types/rpc.js'
import type { Prettify } from '../../types/utils.js'
Expand All @@ -27,6 +28,8 @@ export type GetBlockParameters<
> = {
/** Whether or not to include transaction data in the response. */
includeTransactions?: includeTransactions | undefined
/** Request options. */
requestOptions?: EIP1193RequestOptions | undefined
} & (
| {
/** Hash of the block. */
Expand Down Expand Up @@ -99,6 +102,7 @@ export async function getBlock<
blockNumber,
blockTag = client.experimental_blockTag ?? 'latest',
includeTransactions: includeTransactions_,
requestOptions,
}: GetBlockParameters<includeTransactions, blockTag> = {},
): Promise<GetBlockReturnType<chain, includeTransactions, blockTag>> {
const includeTransactions = includeTransactions_ ?? false
Expand All @@ -113,15 +117,15 @@ export async function getBlock<
method: 'eth_getBlockByHash',
params: [blockHash, includeTransactions],
},
{ dedupe: true },
{ dedupe: true, ...requestOptions },
)
} else {
block = await client.request(
{
method: 'eth_getBlockByNumber',
params: [blockNumberHex || blockTag, includeTransactions],
},
{ dedupe: Boolean(blockNumberHex) },
{ dedupe: Boolean(blockNumberHex), ...requestOptions },
)
}

Expand Down
78 changes: 78 additions & 0 deletions src/clients/transports/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,3 +612,81 @@ test('no url', () => {
`,
)
})

describe('request cancellation', () => {
test('cancels request with AbortSignal', async () => {
const server = await createHttpServer((_, res) => {
// Delay response to allow time for cancellation
setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ jsonrpc: '2.0', result: '0x1', id: 0 }))
}, 100)
})

const controller = new AbortController()
const transport = http(server.url)({})

// Cancel after 50ms (before server responds at 100ms)
setTimeout(() => controller.abort(), 50)

await expect(
transport.request(
{ method: 'eth_blockNumber' },
{ signal: controller.signal },
),
).rejects.toThrow()

await server.close()
})

test('successful request with signal', async () => {
const server = await createHttpServer((_, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ jsonrpc: '2.0', result: '0x1', id: 0 }))
})

const controller = new AbortController()
const transport = http(server.url)({})

const result = await transport.request(
{ method: 'eth_blockNumber' },
{ signal: controller.signal },
)

expect(result).toBe('0x1')
await server.close()
})

test('multiple requests with same controller', async () => {
const server = await createHttpServer((_, res) => {
setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ jsonrpc: '2.0', result: '0x1', id: 0 }))
}, 100)
})

const controller = new AbortController()
const transport = http(server.url)({})

// Start multiple requests
const promise1 = transport.request(
{ method: 'eth_blockNumber' },
{ signal: controller.signal },
)
const promise2 = transport.request(
{
method: 'eth_getBalance',
params: ['0x0000000000000000000000000000000000000000'],
},
{ signal: controller.signal },
)

// Cancel after 50ms
setTimeout(() => controller.abort(), 50)

await expect(promise1).rejects.toThrow()
await expect(promise2).rejects.toThrow()

await server.close()
})
})
8 changes: 7 additions & 1 deletion src/clients/transports/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function http<
key,
methods,
name,
async request({ method, params }) {
async request({ method, params }, options) {
const body = { method, params }

const { schedule } = createBatchScheduler({
Expand All @@ -134,6 +134,9 @@ export function http<
fn: (body: RpcRequest[]) =>
rpcClient.request({
body,
fetchOptions: options?.signal
? { signal: options.signal }
: undefined,
}),
sort: (a, b) => a.id - b.id,
})
Expand All @@ -144,6 +147,9 @@ export function http<
: [
await rpcClient.request({
body,
fetchOptions: options?.signal
? { signal: options.signal }
: undefined,
}),
]

Expand Down
2 changes: 2 additions & 0 deletions src/types/eip1193.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2031,6 +2031,8 @@ export type EIP1193RequestOptions = {
retryDelay?: number | undefined
/** The max number of times to retry. */
retryCount?: number | undefined
/** AbortSignal to cancel the request. */
signal?: AbortSignal | undefined
/** Unique identifier for the request. */
uid?: string | undefined
}
Expand Down
76 changes: 75 additions & 1 deletion src/utils/buildRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ import { getHttpRpcClient } from './rpc/http.js'

function request(url: string) {
const httpClient = getHttpRpcClient(url)
return async ({ method, params }: any) => {
return async ({ method, params }: any, options?: any) => {
const { error, result } = await httpClient.request({
body: {
method,
params,
},
fetchOptions: options?.signal ? { signal: options.signal } : undefined,
})
if (error)
throw new RpcRequestError({
Expand Down Expand Up @@ -254,6 +255,79 @@ describe('args', () => {
})
})

describe('options', () => {
test('passes signal to underlying request', async () => {
let receivedSignal: AbortSignal | undefined
const mockRequest = async (_args: any, options?: any) => {
receivedSignal = options?.signal
return 'success'
}

const controller = new AbortController()
const request_ = buildRequest(mockRequest)

await request_({ method: 'eth_blockNumber' }, { signal: controller.signal })

expect(receivedSignal).toBe(controller.signal)
})

test('passes other options alongside signal', async () => {
let receivedOptions: any
const mockRequest = async (_args: any, options?: any) => {
receivedOptions = options
return 'success'
}

const controller = new AbortController()
const request_ = buildRequest(mockRequest)

await request_(
{ method: 'eth_blockNumber' },
{
signal: controller.signal,
retryCount: 1,
dedupe: true,
},
)

expect(receivedOptions).toEqual({ signal: controller.signal })
})

test('works without signal', async () => {
let receivedOptions: any
const mockRequest = async (_args: any, options?: any) => {
receivedOptions = options
return 'success'
}

const request_ = buildRequest(mockRequest)

await request_({ method: 'eth_blockNumber' }, { dedupe: true })

expect(receivedOptions).toBeUndefined()
})

test('prioritizes override options over initial options', async () => {
let receivedSignal: AbortSignal | undefined
const mockRequest = async (_args: any, options?: any) => {
receivedSignal = options?.signal
return 'success'
}

const controller1 = new AbortController()
const controller2 = new AbortController()

const request_ = buildRequest(mockRequest, { signal: controller1.signal })

await request_(
{ method: 'eth_blockNumber' },
{ signal: controller2.signal },
)

expect(receivedSignal).toBe(controller2.signal)
})
})

describe('behavior', () => {
describe('error types', () => {
test('BaseError', async () => {
Expand Down
12 changes: 6 additions & 6 deletions src/utils/buildRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,16 @@ export type RequestErrorType =
| WithRetryErrorType
| ErrorType

export function buildRequest<request extends (args: any) => Promise<any>>(
request: request,
options: EIP1193RequestOptions = {},
): EIP1193RequestFn {
return async (args, overrideOptions = {}) => {
export function buildRequest<
request extends (args: any, options?: EIP1193RequestOptions) => Promise<any>,
>(request: request, options: EIP1193RequestOptions = {}): EIP1193RequestFn {
return async (args, overrideOptions: EIP1193RequestOptions = {}) => {
const {
dedupe = false,
methods,
retryDelay = 150,
retryCount = 3,
signal,
uid,
} = {
...options,
Expand All @@ -147,7 +147,7 @@ export function buildRequest<request extends (args: any) => Promise<any>>(
withRetry(
async () => {
try {
return await request(args)
return await request(args, signal ? { signal } : undefined)
} catch (err_) {
const err = err_ as unknown as RpcError<
RpcErrorCode | ProviderRpcErrorCode
Expand Down
Loading