UNPKG

ox

Version:

Ethereum Standard Library

915 lines (816 loc) 24.5 kB
import { Provider, RpcRequest, RpcResponse } from 'ox' import { describe, expect, test } from 'vp/test' import { address } from '../../../test/constants/addresses.js' import { anvilMainnet } from '../../../test/prool.js' describe('Provider.createEmitter', () => { test('default', () => { const emitter = Provider.createEmitter() expect(emitter).toMatchInlineSnapshot(` { "addListener": [Function], "emit": [Function], "eventNames": [Function], "listenerCount": [Function], "listeners": [Function], "off": [Function], "on": [Function], "once": [Function], "removeAllListeners": [Function], "removeListener": [Function], } `) }) }) describe('Provider.from', () => { test('default', async () => { const store = RpcRequest.createStore() const provider = Provider.from({ async request(args) { return await fetch(anvilMainnet.rpcUrl, { body: JSON.stringify(store.prepare(args as never)), method: 'POST', headers: { 'Content-Type': 'application/json', }, }) .then((res) => res.json()) .then(RpcResponse.parse) }, }) const blockNumber = await provider.request({ method: 'eth_blockNumber', }) expect(blockNumber).toMatchInlineSnapshot(`"0x12f2974"`) }) test('behavior: pre-parsed', async () => { const store = RpcRequest.createStore() const provider = Provider.from({ async request(args) { return await fetch(anvilMainnet.rpcUrl, { body: JSON.stringify(store.prepare(args as never)), method: 'POST', headers: { 'Content-Type': 'application/json', }, }) .then((res) => res.json()) .then(RpcResponse.parse) }, }) const blockNumber = await provider.request({ method: 'eth_blockNumber' }) expect(blockNumber).toMatchInlineSnapshot(`"0x12f2974"`) }) test('behavior: events', async () => { const emitter = Provider.createEmitter() const store = RpcRequest.createStore() const provider = Provider.from({ ...emitter, async request(args) { return await fetch(anvilMainnet.rpcUrl, { body: JSON.stringify(store.prepare(args as never)), method: 'POST', headers: { 'Content-Type': 'application/json', }, }) .then((res) => res.json()) .then(RpcResponse.parse) }, }) const calls: any = [] provider.on('accountsChanged', (accounts) => calls.push(accounts)) emitter.emit('accountsChanged', [address.vitalik]) emitter.emit('accountsChanged', [address.usdcHolder]) expect(calls).toMatchInlineSnapshot(` [ [ "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", ], [ "0x5414d89a8bf7e99d732bc52f3e6a3ef461c0c078", ], ] `) }) test('behavior: methods work without explicit binding', () => { const emitter = Provider.createEmitter() const provider = Provider.from({ ...emitter, async request(args) { return args }, }) // Test that destructured methods still work correctly const { on, emit, off, once } = provider const calls: any = [] // Methods should work when called independently on('accountsChanged', (accounts) => calls.push(['on', accounts])) once('chainChanged', (chainId) => calls.push(['once', chainId])) emit('accountsChanged', [address.vitalik]) emit('accountsChanged', [address.usdcHolder]) emit('chainChanged', '0x1') emit('chainChanged', '0x89') // Should not be captured by 'once' expect(calls).toMatchInlineSnapshot(` [ [ "on", [ "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", ], ], [ "on", [ "0x5414d89a8bf7e99d732bc52f3e6a3ef461c0c078", ], ], [ "once", "0x1", ], ] `) // Clean up off('accountsChanged') }) test('behavior: preserves prototype methods and accessors', async () => { class CustomProvider { _calls: string[] = [] get isCustom() { return true } async request(args: any) { this._calls.push(args.method) return '0x1' } } const original = new CustomProvider() const provider = Provider.from(original) as Provider.Provider & { isCustom: boolean _calls: string[] } expect(provider.isCustom).toBe(true) expect(await provider.request({ method: 'eth_blockNumber' })).toBe('0x1') expect(original._calls).toEqual(['eth_blockNumber']) }) test('behavior: does not parse JSON-RPC envelope success payloads', async () => { // EIP-1193 `request` resolves with the method result, not a JSON-RPC // envelope. If a provider returns an envelope-shaped object as the // method result, Provider.from should pass it through verbatim. const envelopeShapedResult = { jsonrpc: '2.0', id: 0, result: '0x1', } const provider = Provider.from({ async request(_) { return envelopeShapedResult }, }) const result = await provider.request({ method: 'eth_blockNumber' }) expect(result).toEqual(envelopeShapedResult) }) test('behavior: UnauthorizedError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.UnauthorizedError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.UnauthorizedError: The requested method and/or account has not been authorized by the user.]', ) }) test('behavior: UnauthorizedError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.UnauthorizedError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.UnauthorizedError: foo]', ) }) test('behavior: UserRejectedRequestError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.UserRejectedRequestError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.UserRejectedRequestError: The user rejected the request.]', ) }) test('behavior: UserRejectedRequestError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.UserRejectedRequestError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.UserRejectedRequestError: foo]', ) }) test('behavior: UnsupportedMethodError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.UnsupportedMethodError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.UnsupportedMethodError: The provider does not support the requested method.]', ) }) test('behavior: UnsupportedMethodError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.UnsupportedMethodError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.UnsupportedMethodError: foo]', ) }) test('behavior: DisconnectedError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.DisconnectedError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.DisconnectedError: The provider is disconnected from all chains.]', ) }) test('behavior: DisconnectedError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.DisconnectedError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.DisconnectedError: foo]', ) }) test('behavior: ChainDisconnectedError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.ChainDisconnectedError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.ChainDisconnectedError: The provider is not connected to the requested chain.]', ) }) test('behavior: ChainDisconnectedError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.ChainDisconnectedError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.ChainDisconnectedError: foo]', ) }) test('behavior: SwitchChainError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.SwitchChainError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.SwitchChainError: An error occurred when attempting to switch chain.]', ) }) test('behavior: SwitchChainError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.SwitchChainError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.SwitchChainError: foo]', ) }) test('behavior: UnsupportedNonOptionalCapabilityError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.UnsupportedNonOptionalCapabilityError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.UnsupportedNonOptionalCapabilityError: This Wallet does not support a capability that was not marked as optional.]', ) }) test('behavior: UnsupportedNonOptionalCapabilityError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.UnsupportedNonOptionalCapabilityError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.UnsupportedNonOptionalCapabilityError: foo]', ) }) test('behavior: UnsupportedChainIdError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.UnsupportedChainIdError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.UnsupportedChainIdError: This Wallet does not support the requested chain ID.]', ) }) test('behavior: UnsupportedChainIdError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.UnsupportedChainIdError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.UnsupportedChainIdError: foo]', ) }) test('behavior: DuplicateIdError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.DuplicateIdError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.DuplicateIdError: There is already a bundle submitted with this ID.]', ) }) test('behavior: DuplicateIdError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.DuplicateIdError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.DuplicateIdError: foo]', ) }) test('behavior: UnknownBundleIdError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.UnknownBundleIdError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.UnknownBundleIdError: This bundle id is unknown / has not been submitted.]', ) }) test('behavior: UnknownBundleIdError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.UnknownBundleIdError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.UnknownBundleIdError: foo]', ) }) test('behavior: BundleTooLargeError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.BundleTooLargeError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.BundleTooLargeError: The call bundle is too large for the Wallet to process.]', ) }) test('behavior: BundleTooLargeError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.BundleTooLargeError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.BundleTooLargeError: foo]', ) }) test('behavior: AtomicReadyWalletRejectedUpgradeError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.AtomicReadyWalletRejectedUpgradeError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.AtomicReadyWalletRejectedUpgradeError: The Wallet can support atomicity after an upgrade, but the user rejected the upgrade.]', ) }) test('behavior: AtomicReadyWalletRejectedUpgradeError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.AtomicReadyWalletRejectedUpgradeError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.AtomicReadyWalletRejectedUpgradeError: foo]', ) }) test('behavior: AtomicityNotSupportedError', async () => { const provider = Provider.from({ async request(_) { throw new Provider.AtomicityNotSupportedError() }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.AtomicityNotSupportedError: The wallet does not support atomic execution but the request requires it.]', ) }) test('behavior: AtomicityNotSupportedError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: Provider.AtomicityNotSupportedError.code, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Provider.AtomicityNotSupportedError: foo]', ) }) test('behavior: BaseError', async () => { const provider = Provider.from({ async request(_) { throw new Error('foo') }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[RpcResponse.InternalError: foo]', ) }) test('behavior: InternalError', async () => { const provider = Provider.from({ async request(_) { throw new RpcResponse.InternalError({ message: 'foo', }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[RpcResponse.InternalError: foo]', ) }) test('behavior: InternalError (cause)', async () => { const provider = Provider.from({ async request(_) { throw new Error('lol') }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[RpcResponse.InternalError: lol]', ) }) test('behavior: BaseError (raw)', async () => { const provider = Provider.from({ async request(_) { return RpcResponse.parse({ jsonrpc: '2.0', id: 0, error: { code: 1000, message: 'foo', }, }) }, }) await expect(() => provider.request({ method: 'eth_blockNumber', }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[RpcResponse.InternalError: foo]', ) }) test('behavior: network rpc error', async () => { const store = RpcRequest.createStore() const provider = Provider.from({ async request(args) { return await fetch(anvilMainnet.rpcUrl, { body: JSON.stringify(store.prepare(args as never)), method: 'POST', headers: { 'Content-Type': 'application/json', }, }) .then((res) => res.json()) .then(RpcResponse.parse) }, }) await expect(() => provider.request({ method: 'eth_sendTransaction', params: [ { from: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', to: '0x0000000000000000000000000000000000000000', }, ], }), ).rejects.toThrowErrorMatchingInlineSnapshot( `[RpcResponse.InvalidParamsError: No Signer available]`, ) }) test('error: undefined', () => { expect(() => Provider.from(undefined)).toThrowErrorMatchingInlineSnapshot( '[Provider.IsUndefinedError: `provider` is undefined.]', ) }) }) test('Provider.InvalidMessageFieldError', () => { expect(new Provider.UserRejectedRequestError()).toMatchInlineSnapshot( '[Provider.UserRejectedRequestError: The user rejected the request.]', ) expect( new Provider.UserRejectedRequestError({ message: 'foo' }), ).toMatchInlineSnapshot('[Provider.UserRejectedRequestError: foo]') expect( JSON.stringify(new Provider.UserRejectedRequestError()), ).toMatchInlineSnapshot( `"{"name":"Provider.UserRejectedRequestError","code":4001,"details":"The user rejected the request."}"`, ) }) test('Provider.UnauthorizedError', () => { expect(new Provider.UnauthorizedError()).toMatchInlineSnapshot( '[Provider.UnauthorizedError: The requested method and/or account has not been authorized by the user.]', ) expect( new Provider.UnauthorizedError({ message: 'foo' }), ).toMatchInlineSnapshot('[Provider.UnauthorizedError: foo]') expect( JSON.stringify(new Provider.UnauthorizedError()), ).toMatchInlineSnapshot( `"{"name":"Provider.UnauthorizedError","code":4100,"details":"The requested method and/or account has not been authorized by the user."}"`, ) }) test('Provider.UnsupportedMethodError', () => { expect(new Provider.UnsupportedMethodError()).toMatchInlineSnapshot( '[Provider.UnsupportedMethodError: The provider does not support the requested method.]', ) expect( new Provider.UnsupportedMethodError({ message: 'foo' }), ).toMatchInlineSnapshot('[Provider.UnsupportedMethodError: foo]') expect( JSON.stringify(new Provider.UnsupportedMethodError()), ).toMatchInlineSnapshot( `"{"name":"Provider.UnsupportedMethodError","code":4200,"details":"The provider does not support the requested method."}"`, ) }) test('Provider.DisconnectedError', () => { expect(new Provider.DisconnectedError()).toMatchInlineSnapshot( '[Provider.DisconnectedError: The provider is disconnected from all chains.]', ) expect( new Provider.DisconnectedError({ message: 'foo' }), ).toMatchInlineSnapshot('[Provider.DisconnectedError: foo]') expect( JSON.stringify(new Provider.DisconnectedError()), ).toMatchInlineSnapshot( `"{"name":"Provider.DisconnectedError","code":4900,"details":"The provider is disconnected from all chains."}"`, ) }) test('Provider.ChainDisconnectedError', () => { expect(new Provider.ChainDisconnectedError()).toMatchInlineSnapshot( '[Provider.ChainDisconnectedError: The provider is not connected to the requested chain.]', ) expect( new Provider.ChainDisconnectedError({ message: 'foo' }), ).toMatchInlineSnapshot('[Provider.ChainDisconnectedError: foo]') expect( JSON.stringify(new Provider.ChainDisconnectedError()), ).toMatchInlineSnapshot( `"{"name":"Provider.ChainDisconnectedError","code":4901,"details":"The provider is not connected to the requested chain."}"`, ) }) test('exports', () => { expect(Object.keys(Provider)).toMatchInlineSnapshot(` [ "ProviderRpcError", "UserRejectedRequestError", "UnauthorizedError", "UnsupportedMethodError", "DisconnectedError", "ChainDisconnectedError", "SwitchChainError", "UnsupportedNonOptionalCapabilityError", "UnsupportedChainIdError", "DuplicateIdError", "UnknownBundleIdError", "BundleTooLargeError", "AtomicReadyWalletRejectedUpgradeError", "AtomicityNotSupportedError", "createEmitter", "from", "parseError", "IsUndefinedError", ] `) })