Skip to content

Commit

Permalink
Align RPC errors with EIP-1193 / EIP-1474 spec
Browse files Browse the repository at this point in the history
  • Loading branch information
jribbink committed Feb 6, 2025
1 parent 15aa223 commit dbbb6da
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 128 deletions.
2 changes: 1 addition & 1 deletion packages/fcl-ethereum-provider/src/create-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function createProvider(config: {
)

const networkManager = new NetworkManager(config.config)
const accountManager = new AccountManager(config.user)
const accountManager = new AccountManager(config.user, networkManager)
const gateway = new Gateway({
...defaultRpcUrls,
...(config.rpcUrls || {}),
Expand Down
21 changes: 5 additions & 16 deletions packages/fcl-ethereum-provider/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "./types/provider"
import {RpcProcessor} from "./rpc/rpc-processor"
import {EventDispatcher} from "./events/event-dispatcher"
import {RpcError, RpcErrorCode} from "./util/errors"
import {ProviderError, ProviderErrorCode} from "./util/errors"
import {AccountManager} from "./accounts/account-manager"

export class FclEthereumProvider implements Eip1193Provider {
Expand All @@ -22,22 +22,11 @@ export class FclEthereumProvider implements Eip1193Provider {
method,
params,
}: ProviderRequest): Promise<ProviderResponse<T>> {
try {
if (!method) {
throw new Error("Method is required")
}
const result = await this.rpcProcessor.handleRequest({method, params})
return result
} catch (error) {
if (error instanceof RpcError) {
throw error
} else {
throw new RpcError({
code: RpcErrorCode.InternalError,
cause: error,
})
}
if (!method) {
throw new Error("Method is required")
}
const result = await this.rpcProcessor.handleRequest({method, params})
return result
}

disconnect(): void {
Expand Down
57 changes: 57 additions & 0 deletions packages/fcl-ethereum-provider/src/rpc/rpc-processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,61 @@ describe("rpc processor", () => {
chainId: 646,
})
})

test("caught RpcError should be rethrown", async () => {
const gateway: jest.Mocked<Gateway> = new (Gateway as any)()
const accountManager: jest.Mocked<AccountManager> =
new (AccountManager as any)()
const networkManager: jest.Mocked<NetworkManager> =
new (NetworkManager as any)()
const rpcProcessor = new RpcProcessor(
gateway,
accountManager,
networkManager
)

const error = new Error("test error")
;(error as any).code = -32000
jest.mocked(gateway).request.mockRejectedValue(error)
networkManager.getChainId.mockResolvedValue(747)

await expect(
rpcProcessor.handleRequest({
method: "eth_blockNumber",
params: [],
})
).rejects.toMatchObject({
code: -32000,
message: "test error",
})
})

test("caught generic error should be rethrown as an internal error", async () => {
const gateway: jest.Mocked<Gateway> = new (Gateway as any)()
const accountManager: jest.Mocked<AccountManager> =
new (AccountManager as any)()
const networkManager: jest.Mocked<NetworkManager> =
new (NetworkManager as any)()
const rpcProcessor = new RpcProcessor(
gateway,
accountManager,
networkManager
)

jest.mocked(gateway).request.mockRejectedValue(new Error("test error"))
networkManager.getChainId.mockResolvedValue(747)

const promise = rpcProcessor.handleRequest({
method: "eth_blockNumber",
params: [],
})

await expect(promise).rejects.toMatchObject({
code: -32603,
message: "Internal error",
data: {
cause: new Error("test error"),
},
})
})
})
136 changes: 74 additions & 62 deletions packages/fcl-ethereum-provider/src/rpc/rpc-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "../types/eth"
import {signTypedData} from "./handlers/eth-signtypeddata"
import {ethChainId} from "./handlers/eth-chain-id"
import {ProviderError, ProviderErrorCode} from "../util/errors"

export class RpcProcessor {
constructor(
Expand All @@ -23,76 +24,87 @@ export class RpcProcessor {
) {}

async handleRequest({method, params}: ProviderRequest): Promise<any> {
const chainId = await this.networkManager.getChainId()
if (!chainId) {
throw new Error("No active chain")
}
try {
const chainId = await this.networkManager.getChainId()
if (!chainId) {
throw new Error("No active chain")
}

switch (method) {
case "eth_accounts":
return ethAccounts(this.accountManager)
case "eth_requestAccounts":
return ethRequestAccounts(this.accountManager)
case "eth_sendTransaction":
return await ethSendTransaction(this.accountManager, params)
case "eth_signTypedData":
case "eth_signTypedData_v3":
case "eth_signTypedData_v4": {
if (!params || typeof params !== "object") {
throw new Error(`${method} requires valid parameters.`)
}
switch (method) {
case "eth_accounts":
return ethAccounts(this.accountManager)
case "eth_requestAccounts":
return ethRequestAccounts(this.accountManager)
case "eth_sendTransaction":
return await ethSendTransaction(this.accountManager, params)
case "eth_signTypedData":
case "eth_signTypedData_v3":
case "eth_signTypedData_v4": {
if (!params || typeof params !== "object") {
throw new Error(`${method} requires valid parameters.`)
}

const {address, data} = params as {address?: unknown; data?: unknown}
const {address, data} = params as {address?: unknown; data?: unknown}

if (
typeof address !== "string" ||
typeof data !== "object" ||
data === null
) {
throw new Error(
`${method} requires 'address' (string) and a valid 'data' object.`
)
}
if (
typeof address !== "string" ||
typeof data !== "object" ||
data === null
) {
throw new Error(
`${method} requires 'address' (string) and a valid 'data' object.`
)
}

const validParams: SignTypedDataParams = {
address,
data: data as TypedData,
}
const validParams: SignTypedDataParams = {
address,
data: data as TypedData,
}

return await signTypedData(this.accountManager, validParams, method)
}
case "personal_sign":
return await personalSign(
this.accountManager,
params as PersonalSignParams
)
case "wallet_addEthereumChain":
// Expect params to be an array with one chain configuration object.
if (!params || !Array.isArray(params) || !params[0]) {
throw new Error(
"wallet_addEthereumChain requires an array with a chain configuration object."
)
return await signTypedData(this.accountManager, validParams, method)
}
const chainConfig = params[0] as AddEthereumChainParams

return await this.networkManager.addChain(chainConfig)
case "wallet_switchEthereumChain":
// Expect params to be an array with one object.
if (!params || !Array.isArray(params) || !params[0]) {
throw new Error(
"wallet_switchEthereumChain requires an array with a chain configuration object."
case "personal_sign":
return await personalSign(
this.accountManager,
params as PersonalSignParams
)
}
const switchParams = params[0] as SwitchEthereumChainParams
return await this.networkManager.switchChain(switchParams)
case "eth_chainId":
return ethChainId(this.networkManager)
default:
return await this.gateway.request({
chainId,
method,
params,
case "wallet_addEthereumChain":
// Expect params to be an array with one chain configuration object.
if (!params || !Array.isArray(params) || !params[0]) {
throw new Error(
"wallet_addEthereumChain requires an array with a chain configuration object."
)
}
const chainConfig = params[0] as AddEthereumChainParams

return await this.networkManager.addChain(chainConfig)
case "wallet_switchEthereumChain":
// Expect params to be an array with one object.
if (!params || !Array.isArray(params) || !params[0]) {
throw new Error(
"wallet_switchEthereumChain requires an array with a chain configuration object."
)
}
const switchParams = params[0] as SwitchEthereumChainParams
return await this.networkManager.switchChain(switchParams)
case "eth_chainId":
return ethChainId(this.networkManager)
default:
return await this.gateway.request({
chainId,
method,
params,
})
}
} catch (error: any) {
if (error?.code !== undefined) {
throw error
} else {
throw new ProviderError({
code: ProviderErrorCode.InternalError,
cause: error,
})
}
}
}
}
70 changes: 21 additions & 49 deletions packages/fcl-ethereum-provider/src/util/errors.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,41 @@
// EIP-1474 error codes
export enum RpcErrorCode {
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
}

// EIP-1193 error codes
export enum ProviderErrorCode {
// EIP-1193 error codes
UserRejectedRequest = 4001,
Unauthorized = 4100,
UnsupportedMethod = 4200,
Disconnected = 4900,
}

// EI-1474 error messages used as default
export const RpcErrorMessage: Record<RpcErrorCode, string> = {
[-32700]: "Parse error",
[-32600]: "Invalid request",
[-32601]: "Method not found",
[-32602]: "Invalid params",
[-32603]: "Internal error",
// EIP-1474 / JSON-RPC error codes
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
}

// EIP-1193 error messages used as default
export const ProviderErrorMessage: Record<ProviderErrorCode, string> = {
// EIP-1193 error messages
[4001]: "User rejected request",
[4100]: "Unauthorized",
[4200]: "Unsupported method",
[4900]: "Disconnected",
}

export class RpcError extends Error {
public code: RpcErrorCode
public data?: unknown

constructor({
code,
message,
data,
cause,
}: {
code: RpcErrorCode
message?: string
data?: unknown
cause?: any
}) {
if (!RpcErrorMessage[code]) {
throw new Error(`Invalid RPC error code: ${code}`)
}
super(message || RpcErrorMessage[code])
this.code = code
this.data = data
if (cause) {
this.stack = `${this.stack}\nCaused by: ${cause.stack}`
}
}
// EIP-1474 / JSON-RPC error messages
[-32700]: "Parse error",
[-32600]: "Invalid request",
[-32601]: "Method not found",
[-32602]: "Invalid params",
[-32603]: "Internal error",
}

export class ProviderError extends Error {
public code: ProviderErrorCode
public data?: any

constructor(code: ProviderErrorCode, message?: string) {
super(message || ProviderErrorMessage[code])
constructor({code, cause}: {code: ProviderErrorCode; cause?: any}) {
super(ProviderErrorMessage[code])
this.code = code
if (cause) {
this.stack = `${this.stack}\nCaused by: ${cause?.stack || cause}`
}
this.data = {cause}
}
}

0 comments on commit dbbb6da

Please sign in to comment.