Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adhere to EIP-1193 / EIP-1474 Error Format #2117

Merged
merged 5 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions packages/fcl-ethereum-provider/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "./types/provider"
import {RpcProcessor} from "./rpc/rpc-processor"
import {EventDispatcher} from "./events/event-dispatcher"
import {ProviderError, ProviderErrorCode} from "./util/errors"
import {AccountManager} from "./accounts/account-manager"

export class FclEthereumProvider implements Eip1193Provider {
Expand All @@ -21,15 +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) {
throw new Error(`Request failed: ${(error as Error).message}`)
if (!method) {
throw new Error("Method is required")
}
const result = await this.rpcProcessor.handleRequest({method, params})
return result
}

disconnect(): void {
Expand Down
55 changes: 55 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,59 @@ 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",
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, chainId)
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, chainId)
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,
})
}
}
}
}
38 changes: 38 additions & 0 deletions packages/fcl-ethereum-provider/src/util/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export enum ProviderErrorCode {
// EIP-1193 error codes
UserRejectedRequest = 4001,
Unauthorized = 4100,
UnsupportedMethod = 4200,
Disconnected = 4900,

// EIP-1474 / JSON-RPC error codes
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
}
export const ProviderErrorMessage: Record<ProviderErrorCode, string> = {
// EIP-1193 error messages
[4001]: "User rejected request",
[4100]: "Unauthorized",
[4200]: "Unsupported method",
[4900]: "Disconnected",
// 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 cause?: any

constructor({code, cause}: {code: ProviderErrorCode; cause?: any}) {
super(ProviderErrorMessage[code])
this.code = code
this.cause = cause
}
}