ox
Version:
Ethereum Standard Library
915 lines (816 loc) • 24.5 kB
text/typescript
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",
]
`)
})