From fc39f7bb1d27ca21aa7c65d991a2571efa518014 Mon Sep 17 00:00:00 2001 From: "moxey.eth" Date: Tue, 11 Apr 2023 15:53:12 +1000 Subject: [PATCH] fix: scope filters to their respective transport --- .changeset/ten-roses-whisper.md | 5 ++ src/actions/public/createBlockFilter.test.ts | 41 +++++++++- src/actions/public/createBlockFilter.ts | 6 +- .../public/createContractEventFilter.test.ts | 46 +++++++++++ .../public/createContractEventFilter.ts | 6 ++ src/actions/public/createEventFilter.test.ts | 51 ++++++++++++ src/actions/public/createEventFilter.ts | 7 ++ .../createPendingTransactionFilter.test.ts | 41 +++++++++- .../public/createPendingTransactionFilter.ts | 6 +- src/actions/public/getFilterChanges.ts | 4 +- src/actions/public/getFilterLogs.ts | 4 +- src/actions/public/uninstallFilter.test.ts | 5 +- src/actions/public/uninstallFilter.ts | 4 +- src/clients/transports/fallback.test.ts | 77 ++++++++++++++++++- src/clients/transports/fallback.ts | 48 +++++++++++- src/types/filter.ts | 3 + src/utils/buildRequest.test.ts | 21 +++++ src/utils/buildRequest.ts | 9 ++- .../filters/createFilterRequestScope.test.ts | 44 +++++++++++ src/utils/filters/createFilterRequestScope.ts | 42 ++++++++++ 20 files changed, 455 insertions(+), 15 deletions(-) create mode 100644 .changeset/ten-roses-whisper.md create mode 100644 src/utils/filters/createFilterRequestScope.test.ts create mode 100644 src/utils/filters/createFilterRequestScope.ts diff --git a/.changeset/ten-roses-whisper.md b/.changeset/ten-roses-whisper.md new file mode 100644 index 0000000000..b42a3a2d05 --- /dev/null +++ b/.changeset/ten-roses-whisper.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Scoped filters to their respective transport. diff --git a/src/actions/public/createBlockFilter.test.ts b/src/actions/public/createBlockFilter.test.ts index ea967db0f9..15c56864f9 100644 --- a/src/actions/public/createBlockFilter.test.ts +++ b/src/actions/public/createBlockFilter.test.ts @@ -1,9 +1,48 @@ import { expect, test } from 'vitest' -import { publicClient } from '../../_test/index.js' +import { createHttpServer, publicClient } from '../../_test/index.js' import { createBlockFilter } from './createBlockFilter.js' +import { createPublicClient, fallback, http } from '../../clients/index.js' test('default', async () => { expect(await createBlockFilter(publicClient)).toBeDefined() }) + +test('fallback client: scopes request', async () => { + let count1 = 0 + const server1 = await createHttpServer((_req, res) => { + count1++ + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end( + JSON.stringify({ + error: { code: -32004, message: 'method not supported' }, + }), + ) + }) + + let count2 = 0 + const server2 = await createHttpServer((_req, res) => { + count2++ + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end(JSON.stringify({ result: '0x1' })) + }) + + const fallbackClient = createPublicClient({ + transport: fallback([http(server1.url), http(server2.url)], { + rank: false, + }), + }) + const filter = await createBlockFilter(fallbackClient) + expect(filter).toBeDefined() + expect(count1).toBe(1) + expect(count2).toBe(1) + + await filter.request({ method: 'eth_getFilterChanges', params: [filter.id] }) + expect(count1).toBe(1) + expect(count2).toBe(2) +}) diff --git a/src/actions/public/createBlockFilter.ts b/src/actions/public/createBlockFilter.ts index a5d706eaf7..517a17308a 100644 --- a/src/actions/public/createBlockFilter.ts +++ b/src/actions/public/createBlockFilter.ts @@ -1,13 +1,17 @@ import type { PublicClient, Transport } from '../../clients/index.js' import type { Chain, Filter } from '../../types/index.js' +import { createFilterRequestScope } from '../../utils/filters/createFilterRequestScope.js' export type CreateBlockFilterReturnType = Filter<'block'> export async function createBlockFilter( client: PublicClient, ): Promise { + const getRequest = createFilterRequestScope(client, { + method: 'eth_newBlockFilter', + }) const id = await client.request({ method: 'eth_newBlockFilter', }) - return { id, type: 'block' } + return { id, request: getRequest(id), type: 'block' } } diff --git a/src/actions/public/createContractEventFilter.test.ts b/src/actions/public/createContractEventFilter.test.ts index 47c7b672a9..a2903e652b 100644 --- a/src/actions/public/createContractEventFilter.test.ts +++ b/src/actions/public/createContractEventFilter.test.ts @@ -2,13 +2,18 @@ import { assertType, expect, test } from 'vitest' import { accounts, + createHttpServer, initialBlockNumber, publicClient, usdcContractConfig, } from '../../_test/index.js' +import { createPublicClient, fallback, http } from '../../clients/index.js' +import type { Requests } from '../../types/eip1193.js' import { createContractEventFilter } from './createContractEventFilter.js' +const request = (() => {}) as unknown as Requests['request'] + test('default', async () => { const filter = await createContractEventFilter(publicClient, { abi: usdcContractConfig.abi, @@ -19,6 +24,7 @@ test('default', async () => { type: 'event', args: undefined, eventName: undefined, + request, }) expect(filter.id).toBeDefined() expect(filter.type).toBe('event') @@ -125,3 +131,43 @@ test('args: toBlock', async () => { ).id, ).toBeDefined() }) + +test('fallback client: scopes request', async () => { + let count1 = 0 + const server1 = await createHttpServer((_req, res) => { + count1++ + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end( + JSON.stringify({ + error: { code: -32004, message: 'method not supported' }, + }), + ) + }) + + let count2 = 0 + const server2 = await createHttpServer((_req, res) => { + count2++ + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end(JSON.stringify({ result: '0x1' })) + }) + + const fallbackClient = createPublicClient({ + transport: fallback([http(server1.url), http(server2.url)], { + rank: false, + }), + }) + const filter = await createContractEventFilter(fallbackClient, { + abi: usdcContractConfig.abi, + }) + expect(filter).toBeDefined() + expect(count1).toBe(1) + expect(count2).toBe(1) + + await filter.request({ method: 'eth_getFilterChanges', params: [filter.id] }) + expect(count1).toBe(1) + expect(count2).toBe(2) +}) diff --git a/src/actions/public/createContractEventFilter.ts b/src/actions/public/createContractEventFilter.ts index 43ce230aa0..ddfeee174a 100644 --- a/src/actions/public/createContractEventFilter.ts +++ b/src/actions/public/createContractEventFilter.ts @@ -12,6 +12,7 @@ import type { } from '../../types/index.js' import { encodeEventTopics, numberToHex } from '../../utils/index.js' import type { EncodeEventTopicsParameters } from '../../utils/index.js' +import { createFilterRequestScope } from '../../utils/filters/createFilterRequestScope.js' export type CreateContractEventFilterParameters< TAbi extends Abi | readonly unknown[] = Abi, @@ -64,6 +65,10 @@ export async function createContractEventFilter< toBlock, }: CreateContractEventFilterParameters, ): Promise> { + const getRequest = createFilterRequestScope(client, { + method: 'eth_newFilter', + }) + const topics = eventName ? encodeEventTopics({ abi, @@ -88,6 +93,7 @@ export async function createContractEventFilter< args, eventName, id, + request: getRequest(id), type: 'event', } as unknown as CreateContractEventFilterReturnType } diff --git a/src/actions/public/createEventFilter.test.ts b/src/actions/public/createEventFilter.test.ts index 1e92cec125..cfa59d344e 100644 --- a/src/actions/public/createEventFilter.test.ts +++ b/src/actions/public/createEventFilter.test.ts @@ -2,10 +2,13 @@ import { assertType, describe, expect, test } from 'vitest' import { accounts, + createHttpServer, initialBlockNumber, publicClient, } from '../../_test/index.js' import { createEventFilter } from './createEventFilter.js' +import type { Requests } from '../../types/eip1193.js' +import { createPublicClient, fallback, http } from '../../clients/index.js' const event = { default: { @@ -49,11 +52,14 @@ const event = { }, } as const +const request = (() => {}) as unknown as Requests['request'] + describe('default', () => { test('no args', async () => { const filter = await createEventFilter(publicClient) assertType({ id: '0x', + request, type: 'event', }) expect(filter.id).toBeDefined() @@ -77,6 +83,7 @@ describe('default', () => { abi: [event.default], eventName: 'Transfer', id: '0x', + request, type: 'event', }) expect(filter.args).toBeUndefined() @@ -100,6 +107,7 @@ describe('default', () => { }, eventName: 'Transfer', id: '0x', + request, type: 'event', }) expect(filter.args).toEqual({ @@ -122,6 +130,7 @@ describe('default', () => { }, eventName: 'Transfer', id: '0x', + request, type: 'event', }) expect(filter2.args).toEqual({ @@ -143,6 +152,7 @@ describe('default', () => { }, eventName: 'Transfer', id: '0x', + request, type: 'event', }) expect(filter3.args).toEqual({ @@ -162,6 +172,7 @@ describe('default', () => { args: [accounts[0].address, accounts[1].address], eventName: 'Transfer', id: '0x', + request, type: 'event', }) expect(filter1.args).toEqual([accounts[0].address, accounts[1].address]) @@ -177,6 +188,7 @@ describe('default', () => { args: [[accounts[0].address, accounts[1].address]], eventName: 'Transfer', id: '0x', + request, type: 'event', }) expect(filter2.args).toEqual([[accounts[0].address, accounts[1].address]]) @@ -192,6 +204,7 @@ describe('default', () => { args: [null, accounts[0].address], eventName: 'Transfer', id: '0x', + request, type: 'event', }) expect(filter3.args).toEqual([null, accounts[0].address]) @@ -221,3 +234,41 @@ describe('default', () => { }) }) }) + +test('fallback client: scopes request', async () => { + let count1 = 0 + const server1 = await createHttpServer((_req, res) => { + count1++ + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end( + JSON.stringify({ + error: { code: -32004, message: 'method not supported' }, + }), + ) + }) + + let count2 = 0 + const server2 = await createHttpServer((_req, res) => { + count2++ + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end(JSON.stringify({ result: '0x1' })) + }) + + const fallbackClient = createPublicClient({ + transport: fallback([http(server1.url), http(server2.url)], { + rank: false, + }), + }) + const filter = await createEventFilter(fallbackClient) + expect(filter).toBeDefined() + expect(count1).toBe(1) + expect(count2).toBe(1) + + await filter.request({ method: 'eth_getFilterChanges', params: [filter.id] }) + expect(count1).toBe(1) + expect(count2).toBe(2) +}) diff --git a/src/actions/public/createEventFilter.ts b/src/actions/public/createEventFilter.ts index 5b0a7ea717..23b9f609fa 100644 --- a/src/actions/public/createEventFilter.ts +++ b/src/actions/public/createEventFilter.ts @@ -14,6 +14,7 @@ import type { } from '../../types/index.js' import { encodeEventTopics, numberToHex } from '../../utils/index.js' import type { EncodeEventTopicsParameters } from '../../utils/index.js' +import { createFilterRequestScope } from '../../utils/filters/createFilterRequestScope.js' export type CreateEventFilterParameters< TAbiEvent extends AbiEvent | undefined = undefined, @@ -82,6 +83,10 @@ export async function createEventFilter< _Args > = {} as any, ): Promise> { + const getRequest = createFilterRequestScope(client, { + method: 'eth_newFilter', + }) + let topics: LogTopic[] = [] if (event) topics = encodeEventTopics({ @@ -102,11 +107,13 @@ export async function createEventFilter< }, ], }) + return { abi: event ? [event] : undefined, args, eventName: event ? (event as AbiEvent).name : undefined, id, + request: getRequest(id), type: 'event', } as unknown as CreateEventFilterReturnType< TAbiEvent, diff --git a/src/actions/public/createPendingTransactionFilter.test.ts b/src/actions/public/createPendingTransactionFilter.test.ts index 2a123e2c13..9d547cab8f 100644 --- a/src/actions/public/createPendingTransactionFilter.test.ts +++ b/src/actions/public/createPendingTransactionFilter.test.ts @@ -1,9 +1,48 @@ import { expect, test } from 'vitest' -import { publicClient } from '../../_test/index.js' +import { createHttpServer, publicClient } from '../../_test/index.js' import { createPendingTransactionFilter } from './createPendingTransactionFilter.js' +import { createPublicClient, fallback, http } from '../../clients/index.js' test('default', async () => { expect(await createPendingTransactionFilter(publicClient)).toBeDefined() }) + +test('fallback client: scopes request', async () => { + let count1 = 0 + const server1 = await createHttpServer((_req, res) => { + count1++ + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end( + JSON.stringify({ + error: { code: -32004, message: 'method not supported' }, + }), + ) + }) + + let count2 = 0 + const server2 = await createHttpServer((_req, res) => { + count2++ + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end(JSON.stringify({ result: '0x1' })) + }) + + const fallbackClient = createPublicClient({ + transport: fallback([http(server1.url), http(server2.url)], { + rank: false, + }), + }) + const filter = await createPendingTransactionFilter(fallbackClient) + expect(filter).toBeDefined() + expect(count1).toBe(1) + expect(count2).toBe(1) + + await filter.request({ method: 'eth_getFilterChanges', params: [filter.id] }) + expect(count1).toBe(1) + expect(count2).toBe(2) +}) diff --git a/src/actions/public/createPendingTransactionFilter.ts b/src/actions/public/createPendingTransactionFilter.ts index 2d3b88662b..ca954498ab 100644 --- a/src/actions/public/createPendingTransactionFilter.ts +++ b/src/actions/public/createPendingTransactionFilter.ts @@ -1,5 +1,6 @@ import type { PublicClient, Transport } from '../../clients/index.js' import type { Chain, Filter } from '../../types/index.js' +import { createFilterRequestScope } from '../../utils/filters/createFilterRequestScope.js' export type CreatePendingTransactionFilterReturnType = Filter<'transaction'> @@ -9,8 +10,11 @@ export async function createPendingTransactionFilter< >( client: PublicClient, ): Promise { + const getRequest = createFilterRequestScope(client, { + method: 'eth_newPendingTransactionFilter', + }) const id = await client.request({ method: 'eth_newPendingTransactionFilter', }) - return { id, type: 'transaction' } + return { id, request: getRequest(id), type: 'transaction' } } diff --git a/src/actions/public/getFilterChanges.ts b/src/actions/public/getFilterChanges.ts index f937b8ea40..178074cbc0 100644 --- a/src/actions/public/getFilterChanges.ts +++ b/src/actions/public/getFilterChanges.ts @@ -38,12 +38,12 @@ export async function getFilterChanges< TAbi extends Abi | readonly unknown[], TEventName extends string | undefined, >( - client: PublicClient, + _client: PublicClient, { filter, }: GetFilterChangesParameters, ) { - const logs = await client.request({ + const logs = await filter.request({ method: 'eth_getFilterChanges', params: [filter.id], }) diff --git a/src/actions/public/getFilterLogs.ts b/src/actions/public/getFilterLogs.ts index 6402c7f204..06854f0680 100644 --- a/src/actions/public/getFilterLogs.ts +++ b/src/actions/public/getFilterLogs.ts @@ -29,10 +29,10 @@ export async function getFilterLogs< TAbi extends Abi | readonly unknown[], TEventName extends string | undefined, >( - client: PublicClient, + _client: PublicClient, { filter }: GetFilterLogsParameters, ): Promise> { - const logs = await client.request({ + const logs = await filter.request({ method: 'eth_getFilterLogs', params: [filter.id], }) diff --git a/src/actions/public/uninstallFilter.test.ts b/src/actions/public/uninstallFilter.test.ts index 6b34540bb0..405c7ee0ea 100644 --- a/src/actions/public/uninstallFilter.test.ts +++ b/src/actions/public/uninstallFilter.test.ts @@ -14,6 +14,9 @@ import { mine } from '../test/index.js' import { sendTransaction } from '../wallet/index.js' import { parseEther } from '../../utils/index.js' import type { Hash } from '../../types/index.js' +import type { Requests } from '../../types/eip1193.js' + +const request = (() => {}) as unknown as Requests['request'] test('default', async () => { const filter = await createPendingTransactionFilter(publicClient) @@ -58,7 +61,7 @@ test('pending txns', async () => { test('filter does not exist', async () => { expect( await uninstallFilter(publicClient, { - filter: { id: '0x1', type: 'default' }, + filter: { id: '0x1', request, type: 'default' }, }), ).toBeFalsy() }) diff --git a/src/actions/public/uninstallFilter.ts b/src/actions/public/uninstallFilter.ts index e0db98311a..d5d2b7e0be 100644 --- a/src/actions/public/uninstallFilter.ts +++ b/src/actions/public/uninstallFilter.ts @@ -10,10 +10,10 @@ export async function uninstallFilter< TTransport extends Transport, TChain extends Chain | undefined, >( - client: PublicClient, + _client: PublicClient, { filter }: UninstallFilterParameters, ): Promise { - return client.request({ + return filter.request({ method: 'eth_uninstallFilter', params: [filter.id], }) diff --git a/src/clients/transports/fallback.test.ts b/src/clients/transports/fallback.test.ts index 51ef3cfd48..3a7d6f8c0b 100644 --- a/src/clients/transports/fallback.test.ts +++ b/src/clients/transports/fallback.test.ts @@ -7,7 +7,7 @@ import { createPublicClient } from '../createPublicClient.js' import { getBlockNumber } from '../../actions/index.js' import { wait } from '../../utils/wait.js' import type { Transport } from './createTransport.js' -import type { FallbackTransport } from './fallback.js' +import type { FallbackTransport, OnResponseFn } from './fallback.js' import { fallback, rankTransports } from './fallback.js' import { http } from './http.js' @@ -32,6 +32,7 @@ test('default', () => { }, "request": [Function], "value": { + "onResponse": [Function], "transports": [ { "config": { @@ -142,6 +143,79 @@ describe('request', () => { expect(count).toBe(8) }) + test('onResponse', async () => { + let count = 0 + const server1 = await createHttpServer((_req, res) => { + count++ + res.writeHead(500) + res.end() + }) + const server2 = await createHttpServer((_req, res) => { + count++ + res.writeHead(500) + res.end() + }) + const server3 = await createHttpServer((_req, res) => { + count++ + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end(JSON.stringify({ result: '0x1' })) + }) + + const transport = fallback( + [http(server1.url), http(server2.url), http(server3.url)], + { + rank: false, + }, + )({ + chain: localhost, + }) + + const args: Parameters[0][] = [] + transport.value?.onResponse((args_) => args.push(args_)) + + expect(await transport.request({ method: 'eth_blockNumber' })).toBe('0x1') + expect( + args.map(({ transport: _transport, ...rest }) => rest), + ).toMatchInlineSnapshot(` + [ + { + "error": [HttpRequestError: HTTP request failed. + + Status: 500 + URL: http://localhost + Request body: {"method":"eth_blockNumber"} + + Details: Internal Server Error + Version: viem@1.0.2], + "method": "eth_blockNumber", + "params": undefined, + "status": "error", + }, + { + "error": [HttpRequestError: HTTP request failed. + + Status: 500 + URL: http://localhost + Request body: {"method":"eth_blockNumber"} + + Details: Internal Server Error + Version: viem@1.0.2], + "method": "eth_blockNumber", + "params": undefined, + "status": "error", + }, + { + "method": "eth_blockNumber", + "params": undefined, + "response": "0x1", + "status": "success", + }, + ] + `) + }) + test('error (rpc)', async () => { let count = 0 const server1 = await createHttpServer((_req, res) => { @@ -362,6 +436,7 @@ describe('client', () => { "transport": { "key": "fallback", "name": "Fallback", + "onResponse": [Function], "request": [Function], "retryCount": 3, "retryDelay": 150, diff --git a/src/clients/transports/fallback.ts b/src/clients/transports/fallback.ts index deaa10b3a2..b5b8808a50 100644 --- a/src/clients/transports/fallback.ts +++ b/src/clients/transports/fallback.ts @@ -4,6 +4,26 @@ import { wait } from '../../utils/wait.js' import type { Transport, TransportConfig } from './createTransport.js' import { createTransport } from './createTransport.js' +// TODO: Narrow `method` & `params` types. +export type OnResponseFn = ( + args: { + method: string + params: unknown[] + transport: ReturnType + } & ( + | { + error?: never + response: unknown + status: 'success' + } + | { + error: Error + response?: never + status: 'error' + } + ), +) => void + type RankOptions = { /** * The polling interval (in ms) at which the ranker should ping the RPC URL. @@ -52,7 +72,10 @@ export type FallbackTransportConfig = { export type FallbackTransport = Transport< 'fallback', - { transports: ReturnType[] } + { + onResponse: (fn: OnResponseFn) => void + transports: ReturnType[] + } > export function fallback( @@ -69,6 +92,8 @@ export function fallback( return ({ chain, pollingInterval = 4_000, timeout }) => { let transports = transports_ + let onResponse: OnResponseFn = () => {} + const transport = createTransport( { key, @@ -77,11 +102,29 @@ export function fallback( const fetch = async (i: number = 0): Promise => { const transport = transports[i]({ chain, retryCount: 0, timeout }) try { - return await transport.request({ + const response = await transport.request({ method, params, } as any) + + onResponse({ + method, + params: params as unknown[], + response, + transport, + status: 'success', + }) + + return response } catch (err) { + onResponse({ + error: err as Error, + method, + params: params as unknown[], + transport, + status: 'error', + }) + // If the error is deterministic, we don't need to fall back. // So throw the error. if (isDeterministicError(err as Error)) throw err @@ -100,6 +143,7 @@ export function fallback( type: 'fallback', }, { + onResponse: (fn: OnResponseFn) => (onResponse = fn), transports: transports.map((fn) => fn({ chain, retryCount: 0 })), }, ) diff --git a/src/types/filter.ts b/src/types/filter.ts index 486ba0c0fb..2948c5d75f 100644 --- a/src/types/filter.ts +++ b/src/types/filter.ts @@ -1,6 +1,7 @@ import type { Abi } from 'abitype' import type { MaybeExtractEventArgsFromAbi } from './contract.js' import type { Hex } from './misc.js' +import type { Requests } from './eip1193.js' export type FilterType = 'transaction' | 'block' | 'event' @@ -13,6 +14,8 @@ export type Filter< | undefined = MaybeExtractEventArgsFromAbi, > = { id: Hex + // TODO: Narrow `request` to filter-based methods (ie. `eth_getFilterLogs`, etc). + request: Requests['request'] type: TFilterType } & (TFilterType extends 'event' ? TAbi extends Abi diff --git a/src/utils/buildRequest.test.ts b/src/utils/buildRequest.test.ts index bc659f5e30..5638ba9130 100644 --- a/src/utils/buildRequest.test.ts +++ b/src/utils/buildRequest.test.ts @@ -415,6 +415,27 @@ describe('behavior', () => { `) }) + test('MethodNotSupportedRpcError', async () => { + const server = await createHttpServer((_req, res) => { + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end(JSON.stringify({ error: { code: -32042, message: 'message' } })) + }) + + await expect(() => + buildRequest(request(server.url))({ method: 'eth_blockNumber' }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Method is not implemented. + + URL: http://localhost + Request body: {\\"method\\":\\"eth_blockNumber\\"} + + Details: message + Version: viem@1.0.2" + `) + }) + test('UnknownRpcError', async () => { await expect(() => buildRequest(() => Promise.reject(new Error('wat')))(), diff --git a/src/utils/buildRequest.ts b/src/utils/buildRequest.ts index 20a7c876f9..6cc82b20da 100644 --- a/src/utils/buildRequest.ts +++ b/src/utils/buildRequest.ts @@ -21,7 +21,13 @@ import { import { withRetry } from './promise/index.js' export const isDeterministicError = (error: Error) => { - if ('code' in error) return error.code !== -32603 && error.code !== -32005 + if ('code' in error) + return ( + error.code !== -32603 && + error.code !== -32004 && + error.code !== -32042 && + error.code !== -32005 + ) if (error instanceof HttpRequestError && error.status) return ( error.status !== 408 && @@ -66,6 +72,7 @@ export function buildRequest Promise>( if (err.code === -32004) throw new MethodNotSupportedRpcError(err) if (err.code === -32005) throw new LimitExceededRpcError(err) if (err.code === -32006) throw new JsonRpcVersionUnsupportedError(err) + if (err.code === -32042) throw new MethodNotSupportedRpcError(err) if (err.code === 4001) throw new UserRejectedRequestError(err) if (err.code === 4902) throw new SwitchChainError(err) if (err_ instanceof BaseError) throw err_ diff --git a/src/utils/filters/createFilterRequestScope.test.ts b/src/utils/filters/createFilterRequestScope.test.ts new file mode 100644 index 0000000000..70cca4b1e2 --- /dev/null +++ b/src/utils/filters/createFilterRequestScope.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "vitest"; +import { createFilterRequestScope } from "./createFilterRequestScope.js"; +import { createHttpServer, publicClient } from "../../_test/utils.js"; +import { createBlockFilter } from "../../public.js"; +import { createPublicClient, fallback, http } from "../../clients/index.js"; + +test('default', async () => { + const getRequest = createFilterRequestScope(publicClient, { method: 'eth_newBlockFilter' }) + const { id } = await createBlockFilter(publicClient) + expect(getRequest(id)).toEqual(publicClient.request) +}) + +test('fallback transport', async () => { + const server1 = await createHttpServer((_req, res) => { + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end( + JSON.stringify({ + error: { code: -32004, message: 'method not supported' }, + }), + ) + }) + + let count = 0 + const server2 = await createHttpServer((_req, res) => { + count++ + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end(JSON.stringify({ result: '0x1' })) + }) + + const fallbackClient = createPublicClient({ + transport: fallback([http(server1.url), http(server2.url)]) + }) + const getRequest = createFilterRequestScope(fallbackClient, { method: 'eth_newBlockFilter' }) + const { id } = await createBlockFilter(fallbackClient) + + const request = getRequest(id) + count = 0 + await request({ method: 'eth_getFilterChanges', params: [id] }) + expect(count).toBe(1) +}) \ No newline at end of file diff --git a/src/utils/filters/createFilterRequestScope.ts b/src/utils/filters/createFilterRequestScope.ts new file mode 100644 index 0000000000..0b59bd5fd7 --- /dev/null +++ b/src/utils/filters/createFilterRequestScope.ts @@ -0,0 +1,42 @@ +import type { Chain } from '../../chains.js' +import type { PublicClient, Transport } from '../../clients/index.js' +import type { OnResponseFn } from '../../clients/transports/fallback.js' +import type { Requests } from '../../types/eip1193.js' +import type { Hex } from '../../types/index.js' + +type CreateFilterRequestScopeParameters = { + method: + | 'eth_newFilter' + | 'eth_newPendingTransactionFilter' + | 'eth_newBlockFilter' +} +// TODO: Narrow `request` to filter-based methods (ie. `eth_getFilterLogs`, etc). +type CreateFilterRequestScopeReturnType = (id: Hex) => Requests['request'] + +/** + * Scopes `request` to the filter ID. If the client is a fallback, it will + * listen for responses and scope the child transport `request` function + * to the successful filter ID. + */ +export function createFilterRequestScope( + client: PublicClient, + { method }: CreateFilterRequestScopeParameters, +): CreateFilterRequestScopeReturnType { + const requestMap: Record['request']> = {} + + if (client.transport.type === 'fallback') + client.transport.onResponse?.( + ({ + method: method_, + response: id, + status, + transport, + }: Parameters[0]) => { + if (status === 'success' && method === method_) + requestMap[id as Hex] = transport.request + }, + ) + + return ((id) => + requestMap[id] || client.request) as CreateFilterRequestScopeReturnType +}