ox
Version:
Ethereum Standard Library
256 lines (209 loc) • 7.82 kB
text/typescript
import { setTimeout } from 'node:timers/promises'
import { RpcTransport } from 'ox'
import { describe, expect, test } from 'vp/test'
import { createHttpServer } from '../../../test/http.js'
import { anvilMainnet } from '../../../test/prool.js'
describe('fromHttp', () => {
test('default', async () => {
const transport = RpcTransport.fromHttp(anvilMainnet.rpcUrl)
const accounts = await transport.request({ method: 'eth_accounts' })
expect(accounts).toMatchInlineSnapshot(`
[
"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
"0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc",
"0x90f79bf6eb2c4f870365e785982e1f101e93b906",
"0x15d34aaf54267db7d7c367839aaf71a00a2c6a65",
"0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc",
"0x976ea74026e726554db657fa54763abd0c3a0aa9",
"0x14dc79964da2c08b23698b3d3cc7ca32193d9955",
"0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f",
"0xa0ee7a142d267c1f36714e4a8f75612f20a79720",
]
`)
})
test('options: raw', async () => {
const transport = RpcTransport.fromHttp(anvilMainnet.rpcUrl, { raw: true })
const accounts = await transport.request({ method: 'eth_accounts' })
expect(accounts).toMatchInlineSnapshot(`
{
"id": 0,
"jsonrpc": "2.0",
"result": [
"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
"0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc",
"0x90f79bf6eb2c4f870365e785982e1f101e93b906",
"0x15d34aaf54267db7d7c367839aaf71a00a2c6a65",
"0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc",
"0x976ea74026e726554db657fa54763abd0c3a0aa9",
"0x14dc79964da2c08b23698b3d3cc7ca32193d9955",
"0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f",
"0xa0ee7a142d267c1f36714e4a8f75612f20a79720",
],
}
`)
})
test('options: fetchOptions', async () => {
const server = await createHttpServer((req, res) => {
const header = req.headers['x-custom-header']
res.end(JSON.stringify({ id: 0, jsonrpc: '2.0', result: header }))
})
const transport = RpcTransport.fromHttp(server.url, {
fetchOptions: {
headers: {
'x-custom-header': 'wagmi',
},
},
})
const blockNumber = await transport.request({ method: 'eth_accounts' })
expect(blockNumber).toMatchInlineSnapshot(`"wagmi"`)
})
test('options: fetchOptions (fn)', async () => {
const server = await createHttpServer((req, res) => {
const header = req.headers['x-custom-header']
res.end(JSON.stringify({ id: 0, jsonrpc: '2.0', result: header }))
})
const transport = RpcTransport.fromHttp(server.url, {
fetchOptions: ({ method }) => ({
headers: {
'x-custom-header': method,
},
}),
})
const blockNumber = await transport.request({ method: 'eth_accounts' })
expect(blockNumber).toMatchInlineSnapshot(`"eth_accounts"`)
})
test('options: timeout', async () => {
const server = await createHttpServer(async (_, res) => {
await setTimeout(100)
res.end(JSON.stringify({ result: 'wagmi' }))
})
const transport = RpcTransport.fromHttp(server.url, {
timeout: 50,
})
await expect(() =>
transport.request({ method: 'eth_accounts' }),
).rejects.toThrowErrorMatchingInlineSnapshot(
'[Promise.TimeoutError: Operation timed out.]',
)
})
test('options: signal', async () => {
const server = await createHttpServer(async (_, res) => {
await setTimeout(100)
res.end(JSON.stringify({ result: 'wagmi' }))
})
const controller = new AbortController()
const transport = RpcTransport.fromHttp(server.url, {
fetchOptions: {
signal: controller.signal,
},
})
controller.abort()
await expect(() =>
transport.request({ method: 'eth_accounts' }),
).rejects.toThrowErrorMatchingInlineSnapshot(
'[Promise.TimeoutError: Operation timed out.]',
)
})
test('behavior: no timeout', async () => {
const transport = RpcTransport.fromHttp(anvilMainnet.rpcUrl, { timeout: 0 })
const blockNumber = await transport.request({ method: 'eth_accounts' })
expect(blockNumber).toMatchInlineSnapshot(`
[
"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
"0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc",
"0x90f79bf6eb2c4f870365e785982e1f101e93b906",
"0x15d34aaf54267db7d7c367839aaf71a00a2c6a65",
"0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc",
"0x976ea74026e726554db657fa54763abd0c3a0aa9",
"0x14dc79964da2c08b23698b3d3cc7ca32193d9955",
"0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f",
"0xa0ee7a142d267c1f36714e4a8f75612f20a79720",
]
`)
})
test('behavior: no res', async () => {
const server = await createHttpServer((_, res) => {
res.end()
})
const transport = RpcTransport.fromHttp(server.url)
await expect(() => transport.request({ method: 'eth_accounts' })).rejects
.toThrowErrorMatchingInlineSnapshot(`
[RpcTransport.MalformedResponseError: HTTP Response could not be parsed as JSON.
Response: ]
`)
})
test('behavior: http error', async () => {
const server = await createHttpServer((_, res) => {
res.statusCode = 400
res.end()
})
const transport = RpcTransport.fromHttp(server.url)
await expect(() => transport.request({ method: 'eth_accounts' })).rejects
.toThrowErrorMatchingInlineSnapshot(`
[RpcTransport.HttpError: HTTP request failed.
Status: 400
URL: https://oxlib.sh/rpc
Body: "{\\"id\\":0,\\"method\\":\\"eth_accounts\\",\\"jsonrpc\\":\\"2.0\\"}"
Details: Bad Request]
`)
})
test('behavior: http error + response', async () => {
const server = await createHttpServer((_, res) => {
res.statusCode = 400
res.end('bad')
})
const transport = RpcTransport.fromHttp(server.url)
await expect(() => transport.request({ method: 'eth_accounts' })).rejects
.toThrowErrorMatchingInlineSnapshot(`
[RpcTransport.HttpError: HTTP request failed.
Status: 400
URL: https://oxlib.sh/rpc
Body: "{\\"id\\":0,\\"method\\":\\"eth_accounts\\",\\"jsonrpc\\":\\"2.0\\"}"
Details: bad]
`)
})
test('behavior: malformed text response', async () => {
const server = await createHttpServer((_, res) => {
res.end('bad')
})
const transport = RpcTransport.fromHttp(server.url)
await expect(() => transport.request({ method: 'eth_accounts' })).rejects
.toThrowErrorMatchingInlineSnapshot(`
[RpcTransport.MalformedResponseError: HTTP Response could not be parsed as JSON.
Response: bad]
`)
})
test('behavior: timeout still fires when caller supplies signal', async () => {
// Regression: previously fetchOptions.signal would replace the timeout
// signal, so the timeout could never abort the request.
const server = await createHttpServer(async (_, res) => {
await setTimeout(200)
res.end(JSON.stringify({ id: 0, jsonrpc: '2.0', result: 'wagmi' }))
})
const controller = new AbortController()
const transport = RpcTransport.fromHttp(server.url, {
timeout: 50,
fetchOptions: {
signal: controller.signal,
},
})
await expect(() =>
transport.request({ method: 'eth_accounts' }),
).rejects.toThrowErrorMatchingInlineSnapshot(
'[Promise.TimeoutError: Operation timed out.]',
)
await server.close()
})
})
test('exports', () => {
expect(Object.keys(RpcTransport)).toMatchInlineSnapshot(`
[
"fromHttp",
"HttpError",
"MalformedResponseError",
]
`)
})