Skip to content

Commit

Permalink
Add fallback gateway (#2083)
Browse files Browse the repository at this point in the history
  • Loading branch information
jribbink authored Jan 28, 2025
1 parent 1452dc0 commit 64487ca
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 15 deletions.
19 changes: 17 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/fcl-ethereum-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
},
"dependencies": {
"@babel/runtime": "^7.25.7",
"@onflow/fcl": "1.13.4"
"@onflow/fcl": "1.13.4",
"@walletconnect/jsonrpc-http-connection": "^1.0.8",
"@walletconnect/jsonrpc-provider": "^1.0.14"
}
}
15 changes: 15 additions & 0 deletions packages/fcl-ethereum-provider/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export enum FlowNetwork {
MAINNET = "mainnet",
TESTNET = "testnet",
}

export const FLOW_CHAINS = {
[FlowNetwork.MAINNET]: {
eip155ChainId: 747,
publicRpcUrl: "https://access.mainnet.nodes.onflow.org",
},
[FlowNetwork.TESTNET]: {
eip155ChainId: 646,
publicRpcUrl: "https://access.testnet.nodes.onflow.org",
},
}
18 changes: 16 additions & 2 deletions packages/fcl-ethereum-provider/src/create-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {RpcProcessor} from "./rpc/rpc-processor"
import {Service} from "@onflow/typedefs"
import {EventDispatcher} from "./events/event-dispatcher"
import {AccountManager} from "./accounts/account-manager"
import {FLOW_CHAINS} from "./constants"
import {Gateway} from "./gateway/gateway"

/**
* Create a new FCL Ethereum provider
Expand All @@ -28,10 +30,22 @@ import {AccountManager} from "./accounts/account-manager"
export function createProvider(config: {
user: typeof fcl.currentUser
service?: Service
gateway?: string
rpcUrls?: {[chainId: string]: number}
}): Eip1193Provider {
const defaultRpcUrls = Object.values(FLOW_CHAINS).reduce(
(acc, chain) => {
acc[chain.eip155ChainId] = chain.publicRpcUrl
return acc
},
{} as {[chainId: number]: string}
)

const accountManager = new AccountManager(config.user)
const rpcProcessor = new RpcProcessor(accountManager)
const gateway = new Gateway({
...defaultRpcUrls,
...(config.rpcUrls || {}),
})
const rpcProcessor = new RpcProcessor(gateway, accountManager)
const eventProcessor = new EventDispatcher()
const provider = new FclEthereumProvider(rpcProcessor, eventProcessor)
return provider
Expand Down
201 changes: 201 additions & 0 deletions packages/fcl-ethereum-provider/src/gateway/gateway.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import HttpConnection from "@walletconnect/jsonrpc-http-connection"
import {Gateway} from "./gateway"
import * as fcl from "@onflow/fcl"
import {JsonRpcProvider} from "@walletconnect/jsonrpc-provider"

jest.mock("@walletconnect/jsonrpc-http-connection")
jest.mock("@walletconnect/jsonrpc-provider")
jest.mock("@onflow/fcl", () => ({
getChainId: jest.fn(),
}))

describe("gateway", () => {
beforeEach(() => {
jest.clearAllMocks()
})

test("request should work for mainnet", async () => {
const gateway = new Gateway({
747: "https://example.com",
646: "https://example.com/testnet",
})

jest.mocked(fcl.getChainId).mockResolvedValue("mainnet")
jest.mocked(JsonRpcProvider).mockImplementation(
jest.fn(
() =>
({
request: jest.fn().mockResolvedValue(["0x123"]),
}) as any as JsonRpcProvider
)
)

const returnValue = await gateway.request({
method: "eth_accounts",
params: [],
})

// Check that the arguments are correct
expect(
jest.mocked(JsonRpcProvider).mock.results[0].value.request
).toHaveBeenCalledWith({method: "eth_accounts", params: []})

// Check that the return value propogates correctly
expect(returnValue).toEqual(["0x123"])

// Verify that the mainnet provider was used
expect(JsonRpcProvider).toHaveBeenCalled()
expect(JsonRpcProvider).toHaveBeenCalledTimes(1)
expect(JsonRpcProvider).toHaveBeenCalledWith(
jest.mocked(HttpConnection).mock.instances[0]
)
expect(HttpConnection).toHaveBeenCalledWith("https://example.com")
})

test("request should work for testnet", async () => {
const gateway = new Gateway({
747: "https://example.com",
646: "https://example.com/testnet",
})

jest.mocked(fcl.getChainId).mockResolvedValue("testnet")
jest.mocked(JsonRpcProvider).mockImplementation(
jest.fn(
() =>
({
request: jest.fn().mockResolvedValue(["0x123"]),
}) as any as JsonRpcProvider
)
)

const returnValue = await gateway.request({
method: "eth_accounts",
params: [],
})

// Check that the arguments are correct
expect(
jest.mocked(JsonRpcProvider).mock.results[0].value.request
).toHaveBeenCalledWith({method: "eth_accounts", params: []})

// Check that the return value propogates correctly
expect(returnValue).toEqual(["0x123"])

// Verify that the testnet provider was used
expect(JsonRpcProvider).toHaveBeenCalled()
expect(JsonRpcProvider).toHaveBeenCalledTimes(1)
expect(JsonRpcProvider).toHaveBeenCalledWith(
jest.mocked(HttpConnection).mock.instances[0]
)
expect(HttpConnection).toHaveBeenCalledWith("https://example.com/testnet")
})

test("subsequent requests should use the same provider", async () => {
const gateway = new Gateway({
747: "https://example.com",
646: "https://example.com/testnet",
})

jest.mocked(fcl.getChainId).mockResolvedValue("testnet")
jest.mocked(JsonRpcProvider).mockImplementation(
jest.fn(
() =>
({
request: jest.fn().mockResolvedValue(["0x123"]),
}) as any as JsonRpcProvider
)
)

await gateway.request({
method: "eth_accounts",
params: [],
})

await gateway.request({
method: "eth_accounts",
params: [],
})

// Verify that the testnet provider was used
expect(JsonRpcProvider).toHaveBeenCalled()
expect(JsonRpcProvider).toHaveBeenCalledTimes(1)
expect(JsonRpcProvider).toHaveBeenCalledWith(
jest.mocked(HttpConnection).mock.instances[0]
)
expect(HttpConnection).toHaveBeenCalledWith("https://example.com/testnet")
})

test("request should throw if chainId is not found", async () => {
const gateway = new Gateway({
747: "https://example.com",
646: "https://example.com/testnet",
})

jest.mocked(fcl.getChainId).mockResolvedValue("unknown")

await expect(
gateway.request({
method: "eth_accounts",
params: [],
})
).rejects.toThrow("Unsupported chainId unknown")
})

test("should default to public gateway mainnet", async () => {
const gateway = new Gateway({})

jest.mocked(fcl.getChainId).mockResolvedValue("mainnet")
jest.mocked(JsonRpcProvider).mockImplementation(
jest.fn(
() =>
({
request: jest.fn().mockResolvedValue(["0x123"]),
}) as any as JsonRpcProvider
)
)

await gateway.request({
method: "eth_accounts",
params: [],
})

// Verify that the mainnet provider was used
expect(JsonRpcProvider).toHaveBeenCalled()
expect(JsonRpcProvider).toHaveBeenCalledTimes(1)
expect(JsonRpcProvider).toHaveBeenCalledWith(
jest.mocked(HttpConnection).mock.instances[0]
)
expect(HttpConnection).toHaveBeenCalledWith(
"https://access.mainnet.nodes.onflow.org"
)
})

test("should default to public gateway testnet", async () => {
const gateway = new Gateway({})

jest.mocked(fcl.getChainId).mockResolvedValue("testnet")
jest.mocked(JsonRpcProvider).mockImplementation(
jest.fn(
() =>
({
request: jest.fn().mockResolvedValue(["0x123"]),
}) as any as JsonRpcProvider
)
)

await gateway.request({
method: "eth_accounts",
params: [],
})

// Verify that the testnet provider was used
expect(JsonRpcProvider).toHaveBeenCalled()
expect(JsonRpcProvider).toHaveBeenCalledTimes(1)
expect(JsonRpcProvider).toHaveBeenCalledWith(
jest.mocked(HttpConnection).mock.instances[0]
)
expect(HttpConnection).toHaveBeenCalledWith(
"https://access.testnet.nodes.onflow.org"
)
})
})
35 changes: 35 additions & 0 deletions packages/fcl-ethereum-provider/src/gateway/gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import HTTPConnection from "@walletconnect/jsonrpc-http-connection"
import * as fcl from "@onflow/fcl"
import {JsonRpcProvider} from "@walletconnect/jsonrpc-provider"
import {FLOW_CHAINS, FlowNetwork} from "../constants"

export class Gateway {
private providers: {[chainId: number]: JsonRpcProvider} = {}

constructor(private rpcUrls: {[chainId: number]: string}) {}

async request({method, params}: {method: string; params: any}) {
return this.getProvider().then(provider =>
provider.request({method, params})
)
}

private async getProvider(): Promise<JsonRpcProvider> {
const flowChainId = await fcl.getChainId()
if (!(flowChainId in FLOW_CHAINS)) {
throw new Error(`Unsupported chainId ${flowChainId}`)
}

const {eip155ChainId} = FLOW_CHAINS[flowChainId as FlowNetwork]
if (this.providers[eip155ChainId]) {
return this.providers[eip155ChainId]
}

const rpcUrl =
this.rpcUrls[eip155ChainId] ||
FLOW_CHAINS[flowChainId as FlowNetwork].publicRpcUrl
const provider = new JsonRpcProvider(new HTTPConnection(rpcUrl))
this.providers[eip155ChainId] = provider
return provider
}
}
31 changes: 28 additions & 3 deletions packages/fcl-ethereum-provider/src/rpc/rpc-processor.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
describe("rpc", () => {
test("should be tested", () => {
expect(true).toBe(true)
import {AccountManager} from "../accounts/account-manager"
import {Gateway} from "../gateway/gateway"
import {RpcProcessor} from "./rpc-processor"

jest.mock("../gateway/gateway")
jest.mock("../accounts/account-manager")

describe("rpc processor", () => {
test("fallback to gateway", async () => {
const gateway: jest.Mocked<Gateway> = new (Gateway as any)()
const accountManager: jest.Mocked<AccountManager> =
new (AccountManager as any)()
const rpcProcessor = new RpcProcessor(gateway, accountManager)

jest.mocked(gateway).request.mockResolvedValue("0x0")

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

expect(response).toEqual("0x0")
expect(gateway.request).toHaveBeenCalled()
expect(gateway.request).toHaveBeenCalledTimes(1)
expect(gateway.request).toHaveBeenCalledWith({
method: "eth_blockNumber",
params: [],
})
})
})
9 changes: 7 additions & 2 deletions packages/fcl-ethereum-provider/src/rpc/rpc-processor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import {ProviderRequest} from "../types/provider"
import {ethAccounts} from "./handlers/eth-accounts"
import {JsonRpcProvider} from "@walletconnect/jsonrpc-provider"
import {Gateway} from "../gateway/gateway"
import {AccountManager} from "../accounts/account-manager"

export class RpcProcessor {
constructor(private accountManager: AccountManager) {}
constructor(
private gateway: Gateway,
private accountManager: AccountManager
) {}

async handleRequest({method, params}: ProviderRequest): Promise<any> {
switch (method) {
Expand All @@ -12,7 +17,7 @@ export class RpcProcessor {
case "eth_requestAccounts":
throw new Error("Not implemented")
default:
throw new Error(`Method ${method} not supported`)
return await this.gateway.request({method, params})
}
}
}
Loading

0 comments on commit 64487ca

Please sign in to comment.