From 2d651a36297e692b46d3467ae5315d1c7f9ca851 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink <17958158+jribbink@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:31:34 -0800 Subject: [PATCH 1/3] Handle `eth_chainId` Locally in FCL Ethereum Provider (#2116) --- .../src/events/event-dispatcher.ts | 5 +-- .../src/rpc/handlers/eth-chain-id.test.ts | 33 +++++++++++++++++++ .../src/rpc/handlers/eth-chain-id.ts | 12 +++++++ .../src/rpc/rpc-processor.ts | 3 ++ .../fcl-ethereum-provider/src/util/eth.ts | 5 +++ 5 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 packages/fcl-ethereum-provider/src/rpc/handlers/eth-chain-id.test.ts create mode 100644 packages/fcl-ethereum-provider/src/rpc/handlers/eth-chain-id.ts create mode 100644 packages/fcl-ethereum-provider/src/util/eth.ts diff --git a/packages/fcl-ethereum-provider/src/events/event-dispatcher.ts b/packages/fcl-ethereum-provider/src/events/event-dispatcher.ts index e94cc7a47..081b36c67 100644 --- a/packages/fcl-ethereum-provider/src/events/event-dispatcher.ts +++ b/packages/fcl-ethereum-provider/src/events/event-dispatcher.ts @@ -9,6 +9,7 @@ import { Subscription, takeFirst, } from "../util/observable" +import {formatChainId} from "../util/eth" export class EventDispatcher { private $emitters: { @@ -33,7 +34,7 @@ export class EventDispatcher { chainChanged: networkManager.$chainId.pipe( filter(({isLoading, error}) => !isLoading && !error), map(({chainId}) => { - return `0x${chainId!.toString(16)}` + return formatChainId(chainId!) }), skip(1) ) as Observable, @@ -41,7 +42,7 @@ export class EventDispatcher { connect: networkManager.$chainId.pipe( filter(({isLoading, error}) => !isLoading && !error), map(({chainId}) => { - return {chainId: `0x${chainId!.toString(16)}`} + return {chainId: formatChainId(chainId!)} }), takeFirst() ), diff --git a/packages/fcl-ethereum-provider/src/rpc/handlers/eth-chain-id.test.ts b/packages/fcl-ethereum-provider/src/rpc/handlers/eth-chain-id.test.ts new file mode 100644 index 000000000..94d6e578f --- /dev/null +++ b/packages/fcl-ethereum-provider/src/rpc/handlers/eth-chain-id.test.ts @@ -0,0 +1,33 @@ +import {formatChainId} from "../../util/eth" +import {ethChainId} from "./eth-chain-id" + +jest.mock("../../util/eth", () => ({ + ...jest.requireActual("../../util/eth"), + formatChainId: jest.fn().mockReturnValue("0x747"), +})) + +describe("eth_chainId handler", () => { + test("should return the formatted chain id", async () => { + const networkManagerMock = { + getChainId: jest.fn(), + } + networkManagerMock.getChainId.mockResolvedValue(747) + + const chainId = await ethChainId(networkManagerMock as any)() + + expect(chainId).toEqual("0x747") + expect(networkManagerMock.getChainId).toHaveBeenCalled() + }) + + test("should throw an error if no chain id is available", async () => { + const networkManagerMock = { + getChainId: jest.fn(), + } + networkManagerMock.getChainId.mockResolvedValue(null) + + await expect(ethChainId(networkManagerMock as any)()).rejects.toThrow( + "No active chain" + ) + expect(networkManagerMock.getChainId).toHaveBeenCalled() + }) +}) diff --git a/packages/fcl-ethereum-provider/src/rpc/handlers/eth-chain-id.ts b/packages/fcl-ethereum-provider/src/rpc/handlers/eth-chain-id.ts new file mode 100644 index 000000000..9246bcfe3 --- /dev/null +++ b/packages/fcl-ethereum-provider/src/rpc/handlers/eth-chain-id.ts @@ -0,0 +1,12 @@ +import {NetworkManager} from "../../network/network-manager" +import {formatChainId} from "../../util/eth" + +export function ethChainId(networkManager: NetworkManager) { + return async function () { + const chainId = await networkManager.getChainId() + if (!chainId) { + throw new Error("No active chain") + } + return formatChainId(chainId) + } +} diff --git a/packages/fcl-ethereum-provider/src/rpc/rpc-processor.ts b/packages/fcl-ethereum-provider/src/rpc/rpc-processor.ts index 17b72959a..31516a576 100644 --- a/packages/fcl-ethereum-provider/src/rpc/rpc-processor.ts +++ b/packages/fcl-ethereum-provider/src/rpc/rpc-processor.ts @@ -13,6 +13,7 @@ import { TypedData, } from "../types/eth" import {signTypedData} from "./handlers/eth-signtypeddata" +import {ethChainId} from "./handlers/eth-chain-id" export class RpcProcessor { constructor( @@ -84,6 +85,8 @@ export class RpcProcessor { } 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, diff --git a/packages/fcl-ethereum-provider/src/util/eth.ts b/packages/fcl-ethereum-provider/src/util/eth.ts new file mode 100644 index 000000000..8cc15491e --- /dev/null +++ b/packages/fcl-ethereum-provider/src/util/eth.ts @@ -0,0 +1,5 @@ +export function formatChainId(chainId: string | number): `0x${string}` { + const numericChainId = + typeof chainId === "string" ? parseInt(chainId) : chainId + return `0x${numericChainId.toString(16)}` +} From e3dae26b3677f55d793bf09508aa866b5a353cf9 Mon Sep 17 00:00:00 2001 From: Chase Fleming Date: Wed, 5 Feb 2025 14:23:47 -0800 Subject: [PATCH 2/3] Setup disconnect (#2119) * Move to account manager * Setup disconnect * Run prettier * Fix test * Remove import * Push fix * Push fix --------- Co-authored-by: Chase Fleming <1666730+chasefleming@users.noreply.github.com> --- .../src/accounts/account-manager.ts | 8 +++++++ .../src/create-provider.ts | 6 ++++- .../fcl-ethereum-provider/src/provider.ts | 6 +++++ .../src/rpc/handlers/eth-accounts.test.ts | 22 +++---------------- .../src/rpc/handlers/eth-accounts.ts | 3 +-- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/fcl-ethereum-provider/src/accounts/account-manager.ts b/packages/fcl-ethereum-provider/src/accounts/account-manager.ts index 7d470e105..4ff515efb 100644 --- a/packages/fcl-ethereum-provider/src/accounts/account-manager.ts +++ b/packages/fcl-ethereum-provider/src/accounts/account-manager.ts @@ -82,6 +82,14 @@ export class AccountManager { .subscribe(this.$addressStore) } + public async authenticate(): Promise { + return await this.user.authenticate() + } + + public async unauthenticate(): Promise { + await this.user.unauthenticate() + } + private async fetchCOAFromFlowAddress(flowAddr: string): Promise { const cadenceScript = ` import EVM diff --git a/packages/fcl-ethereum-provider/src/create-provider.ts b/packages/fcl-ethereum-provider/src/create-provider.ts index 73a7f9e34..da1f783df 100644 --- a/packages/fcl-ethereum-provider/src/create-provider.ts +++ b/packages/fcl-ethereum-provider/src/create-provider.ts @@ -50,7 +50,11 @@ export function createProvider(config: { }) const rpcProcessor = new RpcProcessor(gateway, accountManager, networkManager) const eventProcessor = new EventDispatcher(accountManager, networkManager) - const provider = new FclEthereumProvider(rpcProcessor, eventProcessor) + const provider = new FclEthereumProvider( + accountManager, + rpcProcessor, + eventProcessor + ) return provider } diff --git a/packages/fcl-ethereum-provider/src/provider.ts b/packages/fcl-ethereum-provider/src/provider.ts index 01ab36dc7..ee6d9ed34 100644 --- a/packages/fcl-ethereum-provider/src/provider.ts +++ b/packages/fcl-ethereum-provider/src/provider.ts @@ -7,9 +7,11 @@ import { } from "./types/provider" import {RpcProcessor} from "./rpc/rpc-processor" import {EventDispatcher} from "./events/event-dispatcher" +import {AccountManager} from "./accounts/account-manager" export class FclEthereumProvider implements Eip1193Provider { constructor( + private acountManager: AccountManager, private rpcProcessor: RpcProcessor, private eventDispatcher: EventDispatcher ) {} @@ -30,6 +32,10 @@ export class FclEthereumProvider implements Eip1193Provider { } } + disconnect(): void { + this.acountManager.unauthenticate() + } + // Listen to events on( event: E, diff --git a/packages/fcl-ethereum-provider/src/rpc/handlers/eth-accounts.test.ts b/packages/fcl-ethereum-provider/src/rpc/handlers/eth-accounts.test.ts index 181916eef..c849a9cd2 100644 --- a/packages/fcl-ethereum-provider/src/rpc/handlers/eth-accounts.test.ts +++ b/packages/fcl-ethereum-provider/src/rpc/handlers/eth-accounts.test.ts @@ -1,17 +1,5 @@ -// eth-accounts.spec.ts import {ethAccounts, ethRequestAccounts} from "./eth-accounts" import {AccountManager} from "../../accounts/account-manager" -import * as fcl from "@onflow/fcl" -import {CurrentUser} from "@onflow/typedefs" -import {mockUser} from "../../__mocks__/fcl" - -// Mock FCL at the top-level -jest.mock("@onflow/fcl", () => ({ - currentUser: jest.fn().mockReturnValue({ - authenticate: jest.fn(), - snapshot: jest.fn(), - }), -})) describe("ethAccounts handler", () => { let accountManagerMock: jest.Mocked @@ -45,14 +33,10 @@ describe("ethAccounts handler", () => { describe("ethRequestAccounts handler", () => { let accountManagerMock: jest.Mocked - let userMock: jest.Mocked beforeEach(() => { - userMock = fcl.currentUser() as jest.Mocked - - userMock.snapshot.mockResolvedValue({addr: null} as unknown as CurrentUser) - accountManagerMock = { + authenticate: jest.fn(), getAccounts: jest.fn(), updateCOAAddress: jest.fn(), subscribe: jest.fn(), @@ -64,7 +48,7 @@ describe("ethRequestAccounts handler", () => { const accounts = await ethRequestAccounts(accountManagerMock) - expect(userMock.authenticate).toHaveBeenCalled() + expect(accountManagerMock.authenticate).toHaveBeenCalled() expect(accountManagerMock.getAccounts).toHaveBeenCalled() expect(accounts).toEqual(["0x1234..."]) }) @@ -74,7 +58,7 @@ describe("ethRequestAccounts handler", () => { const accounts = await ethRequestAccounts(accountManagerMock) - expect(userMock.authenticate).toHaveBeenCalled() + expect(accountManagerMock.authenticate).toHaveBeenCalled() expect(accountManagerMock.getAccounts).toHaveBeenCalled() expect(accounts).toEqual([]) }) diff --git a/packages/fcl-ethereum-provider/src/rpc/handlers/eth-accounts.ts b/packages/fcl-ethereum-provider/src/rpc/handlers/eth-accounts.ts index 9ed4b2a9f..54508fdaf 100644 --- a/packages/fcl-ethereum-provider/src/rpc/handlers/eth-accounts.ts +++ b/packages/fcl-ethereum-provider/src/rpc/handlers/eth-accounts.ts @@ -1,4 +1,3 @@ -import * as fcl from "@onflow/fcl" import {AccountManager} from "../../accounts/account-manager" export async function ethAccounts( @@ -8,7 +7,7 @@ export async function ethAccounts( } export async function ethRequestAccounts(accountManager: AccountManager) { - await fcl.currentUser().authenticate() + await accountManager.authenticate() return await accountManager.getAccounts() } From d11dd4ecd82b3f6ead56e38d576a3e849029df3e Mon Sep 17 00:00:00 2001 From: Jordan Ribbink <17958158+jribbink@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:50:25 -0800 Subject: [PATCH 3/3] Add EVM contract address to scripts (#2121) --- .../src/accounts/account-manager.test.ts | 110 ++++++++++-------- .../src/accounts/account-manager.ts | 29 +++-- .../fcl-ethereum-provider/src/util/eth.ts | 27 +++++ 3 files changed, 111 insertions(+), 55 deletions(-) diff --git a/packages/fcl-ethereum-provider/src/accounts/account-manager.test.ts b/packages/fcl-ethereum-provider/src/accounts/account-manager.test.ts index c0492a14c..af911ef9c 100644 --- a/packages/fcl-ethereum-provider/src/accounts/account-manager.test.ts +++ b/packages/fcl-ethereum-provider/src/accounts/account-manager.test.ts @@ -3,6 +3,8 @@ import {mockUser} from "../__mocks__/fcl" import * as fcl from "@onflow/fcl" import * as rlp from "@onflow/rlp" import {CurrentUser} from "@onflow/typedefs" +import {ChainIdStore, NetworkManager} from "../network/network-manager" +import {BehaviorSubject, Subject} from "../util/observable" jest.mock("@onflow/fcl", () => { const fcl = jest.requireActual("@onflow/fcl") @@ -24,35 +26,41 @@ const mockFcl = jest.mocked(fcl) const mockQuery = jest.mocked(fcl.query) describe("AccountManager", () => { + let networkManager: jest.Mocked + let userMock: ReturnType + beforeEach(() => { jest.clearAllMocks() + + const chainId$ = new BehaviorSubject(747) + networkManager = { + $chainId: chainId$, + getChainId: () => chainId$.getValue(), + } as any as jest.Mocked + userMock = mockUser() }) it("should initialize with null COA address", async () => { - const user = mockUser() - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(userMock.mock, networkManager) expect(await accountManager.getCOAAddress()).toBeNull() expect(await accountManager.getAccounts()).toEqual([]) }) it("should reset state when the user is not logged in", async () => { - const user = mockUser() - - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(userMock.mock, networkManager) expect(await accountManager.getCOAAddress()).toBeNull() expect(await accountManager.getAccounts()).toEqual([]) }) it("should fetch and update COA address when user logs in", async () => { - const user = mockUser() mockQuery.mockResolvedValue("0x123") - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(userMock.mock, networkManager) expect(await accountManager.getCOAAddress()).toBe(null) - user.set!({addr: "0x1"} as CurrentUser) + userMock.set!({addr: "0x1"} as CurrentUser) expect(await accountManager.getCOAAddress()).toBe("0x123") expect(await accountManager.getAccounts()).toEqual(["0x123"]) @@ -63,26 +71,24 @@ describe("AccountManager", () => { }) it("should not update COA address if user has not changed", async () => { - const user = mockUser() mockQuery.mockResolvedValue("0x123") - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(userMock.mock, networkManager) - user.set!({addr: "0x1"} as CurrentUser) + userMock.set!({addr: "0x1"} as CurrentUser) await new Promise(setImmediate) expect(await accountManager.getCOAAddress()).toBe("0x123") expect(fcl.query).toHaveBeenCalledTimes(1) - user.set!({addr: "0x1"} as CurrentUser) + userMock.set!({addr: "0x1"} as CurrentUser) expect(await accountManager.getCOAAddress()).toBe("0x123") expect(fcl.query).toHaveBeenCalledTimes(1) // Should not have fetched again }) it("should not update COA address if fetch is outdated when user changes", async () => { - const user = mockUser() mockQuery.mockResolvedValue("0x123") mockQuery @@ -93,39 +99,36 @@ describe("AccountManager", () => { // 2nd fetch: immediate .mockResolvedValueOnce("0x456") - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(userMock.mock, networkManager) - await user.set!({addr: "0x1"} as CurrentUser) - await user.set!({addr: "0x2"} as CurrentUser) + await userMock.set!({addr: "0x1"} as CurrentUser) + await userMock.set!({addr: "0x2"} as CurrentUser) // The second fetch (for address 0x2) is the latest, so "0x456" expect(await accountManager.getCOAAddress()).toBe("0x456") }) it("should throw if COA address fetch fails", async () => { - const user = mockUser() mockQuery.mockRejectedValueOnce(new Error("Fetch failed")) - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(userMock.mock, networkManager) - await user.set!({addr: "0x1"} as CurrentUser) + await userMock.set!({addr: "0x1"} as CurrentUser) await expect(accountManager.getCOAAddress()).rejects.toThrow("Fetch failed") }) it("should handle user changes correctly", async () => { - const user = mockUser() - mockQuery .mockResolvedValueOnce("0x123") // for user 0x1 .mockResolvedValueOnce("0x456") // for user 0x2 - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(userMock.mock, networkManager) - await user.set({addr: "0x1"} as CurrentUser) + await userMock.set({addr: "0x1"} as CurrentUser) expect(await accountManager.getCOAAddress()).toBe("0x123") - await user.set({addr: "0x2"} as CurrentUser) + await userMock.set({addr: "0x2"} as CurrentUser) await new Promise(setImmediate) expect(await accountManager.getCOAAddress()).toBe("0x456") @@ -134,14 +137,12 @@ describe("AccountManager", () => { it("should call the callback with updated accounts in subscribe", async () => { mockQuery.mockResolvedValue("0x123") - const user = mockUser() - - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(userMock.mock, networkManager) const callback = jest.fn() accountManager.subscribe(callback) - user.set({addr: "0x1"} as CurrentUser) + userMock.set({addr: "0x1"} as CurrentUser) await new Promise(setImmediate) @@ -150,11 +151,10 @@ describe("AccountManager", () => { it("should reset accounts in subscribe if user is not authenticated", async () => { mockQuery.mockResolvedValue("0x123") - const user = mockUser() const callback = jest.fn() - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(userMock.mock, networkManager) accountManager.subscribe(callback) @@ -166,11 +166,11 @@ describe("AccountManager", () => { it("should call the callback when COA address is updated", async () => { const callback = jest.fn() - const user = mockUser({addr: "0x1"} as CurrentUser) - mockQuery.mockResolvedValueOnce("0x123") - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(userMock.mock, networkManager) + + userMock.set({addr: "0x1"} as CurrentUser) accountManager.subscribe(callback) @@ -180,23 +180,31 @@ describe("AccountManager", () => { }) it("should return an empty array when COA address is null", async () => { - const {mock: user} = mockUser() - const accountManager = new AccountManager(user) + const accountManager = new AccountManager(userMock.mock, networkManager) expect(await accountManager.getAccounts()).toEqual([]) }) it("should return COA address array when available", async () => { mockQuery.mockResolvedValueOnce("0x123") - const {mock: user} = mockUser({addr: "0x1"} as CurrentUser) + userMock.set({addr: "0x1"} as CurrentUser) - const accountManager = new AccountManager(user) + const accountManager = new AccountManager(userMock.mock, networkManager) expect(await accountManager.getAccounts()).toEqual(["0x123"]) }) }) describe("send transaction", () => { + let networkManager: jest.Mocked + let $mockChainId: BehaviorSubject + beforeEach(() => { + $mockChainId = new BehaviorSubject(747) + networkManager = { + $chainId: $mockChainId, + getChainId: () => $mockChainId.getValue(), + } as any as jest.Mocked + jest.clearAllMocks() }) @@ -219,7 +227,7 @@ describe("send transaction", () => { jest.mocked(fcl.query).mockResolvedValue("0x1234") const user = mockUser({addr: "0x4444"} as CurrentUser).mock - const accountManager = new AccountManager(user) + const accountManager = new AccountManager(user, networkManager) const tx = { to: "0x1234", @@ -248,6 +256,9 @@ describe("send transaction", () => { }) test("send transaction testnet", async () => { + // Set chainId to testnet + $mockChainId.next(646) + const mockTxResult = { onceExecuted: jest.fn().mockResolvedValue({ events: [ @@ -266,7 +277,7 @@ describe("send transaction", () => { jest.mocked(fcl.query).mockResolvedValue("0x1234") const user = mockUser({addr: "0x4444"} as CurrentUser) - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(user.mock, networkManager) const tx = { to: "0x1234", @@ -306,7 +317,7 @@ describe("send transaction", () => { jest.mocked(fcl.query).mockResolvedValue("0x1234") const user = mockUser({addr: "0x4444"} as CurrentUser) - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(user.mock, networkManager) const tx = { to: "0x1234", @@ -315,7 +326,7 @@ describe("send transaction", () => { data: "0x1234", nonce: "0", gas: "0", - chainId: "646", + chainId: "747", } await expect(accountManager.sendTransaction(tx)).rejects.toThrow( @@ -326,7 +337,7 @@ describe("send transaction", () => { test("throws error if from address does not match user address", async () => { jest.mocked(fcl.query).mockResolvedValue("0x1234") const user = mockUser({addr: "0x4444"} as CurrentUser) - const accountManager = new AccountManager(user.mock) + const accountManager = new AccountManager(user.mock, networkManager) const tx = { to: "0x1234", @@ -339,7 +350,7 @@ describe("send transaction", () => { } await expect(accountManager.sendTransaction(tx)).rejects.toThrow( - `From address does not match authenticated user address.\nUser: 0x1234\nFrom: 0x4567` + `Chain ID does not match the current network. Expected: 747, Received: 646` ) expect(fcl.mutate).not.toHaveBeenCalled() @@ -347,6 +358,7 @@ describe("send transaction", () => { }) describe("signMessage", () => { + let networkManager: jest.Mocked let accountManager: AccountManager let user: ReturnType["mock"] let updateUser: ReturnType["set"] @@ -355,7 +367,12 @@ describe("signMessage", () => { jest.clearAllMocks() ;({mock: user, set: updateUser} = mockUser({addr: "0x123"} as CurrentUser)) jest.mocked(fcl.query).mockResolvedValue("0xCOA1") - accountManager = new AccountManager(user) + const $mockChainId = new BehaviorSubject(747) + networkManager = { + $chainId: $mockChainId, + getChainId: () => $mockChainId.getValue(), + } as any as jest.Mocked + accountManager = new AccountManager(user, networkManager) }) it("should throw an error if the COA address is not available", async () => { @@ -400,10 +417,7 @@ describe("signMessage", () => { }) it("should throw an error if signUserMessage returns an empty array", async () => { - accountManager["coaAddress"] = "0xCOA1" - - user.signUserMessage = jest.fn().mockResolvedValue([]) - + user.signUserMessage.mockResolvedValue([]) await expect( accountManager.signMessage("Test message", "0xCOA1") ).rejects.toThrow("Failed to sign message") diff --git a/packages/fcl-ethereum-provider/src/accounts/account-manager.ts b/packages/fcl-ethereum-provider/src/accounts/account-manager.ts index 4ff515efb..804d00ca7 100644 --- a/packages/fcl-ethereum-provider/src/accounts/account-manager.ts +++ b/packages/fcl-ethereum-provider/src/accounts/account-manager.ts @@ -24,6 +24,8 @@ import { switchMap, } from "../util/observable" import {EthSignatureResponse} from "../types/eth" +import {NetworkManager} from "../network/network-manager" +import {formatChainId, getContractAddress} from "../util/eth" export class AccountManager { private $addressStore = new BehaviorSubject<{ @@ -36,7 +38,10 @@ export class AccountManager { error: null, }) - constructor(private user: typeof fcl.currentUser) { + constructor( + private user: typeof fcl.currentUser, + private networkManager: NetworkManager + ) { // Create an observable from the user const $user = new Observable(subscriber => { return this.user.subscribe((currentUser: CurrentUser, error?: Error) => { @@ -91,8 +96,13 @@ export class AccountManager { } private async fetchCOAFromFlowAddress(flowAddr: string): Promise { + const chainId = await this.networkManager.getChainId() + if (!chainId) { + throw new Error("No active chain") + } + const cadenceScript = ` - import EVM + import EVM from ${getContractAddress(ContractType.EVM, chainId)} access(all) fun main(address: Address): String? { @@ -156,17 +166,22 @@ export class AccountManager { chainId: string }) { // Find the Flow network based on the chain ID + const parsedChainId = parseInt(chainId) const flowNetwork = Object.entries(FLOW_CHAINS).find( - ([, chain]) => chain.eip155ChainId === parseInt(chainId) + ([, chain]) => chain.eip155ChainId === parsedChainId )?.[0] as FlowNetwork | undefined if (!flowNetwork) { throw new Error("Flow network not found for chain ID") } - const evmContractAddress = fcl.withPrefix( - FLOW_CONTRACTS[ContractType.EVM][flowNetwork] - ) + // Validate the chain ID + const currentChainId = await this.networkManager.getChainId() + if (parsedChainId !== currentChainId) { + throw new Error( + `Chain ID does not match the current network. Expected: ${currentChainId}, Received: ${parsedChainId}` + ) + } // Check if the from address matches the authenticated COA address const expectedCOAAddress = await this.getCOAAddress() @@ -177,7 +192,7 @@ export class AccountManager { } const txId = await fcl.mutate({ - cadence: `import EVM from ${evmContractAddress} + cadence: `import EVM from ${getContractAddress(ContractType.EVM, parsedChainId)} /// Executes the calldata from the signer's COA /// diff --git a/packages/fcl-ethereum-provider/src/util/eth.ts b/packages/fcl-ethereum-provider/src/util/eth.ts index 8cc15491e..98ad6499b 100644 --- a/packages/fcl-ethereum-provider/src/util/eth.ts +++ b/packages/fcl-ethereum-provider/src/util/eth.ts @@ -1,5 +1,32 @@ +import { + ContractType, + FLOW_CHAINS, + FLOW_CONTRACTS, + FlowNetwork, +} from "../constants" +import * as fcl from "@onflow/fcl" + export function formatChainId(chainId: string | number): `0x${string}` { const numericChainId = typeof chainId === "string" ? parseInt(chainId) : chainId return `0x${numericChainId.toString(16)}` } + +export function getContractAddress( + contractType: ContractType, + chainId: number +) { + // Find the Flow network based on the chain ID + const flowNetwork = Object.entries(FLOW_CHAINS).find( + ([, chain]) => chain.eip155ChainId === chainId + )?.[0] as FlowNetwork | undefined + + if (!flowNetwork) { + throw new Error("Flow network not found for chain ID") + } + + const evmContractAddress = fcl.withPrefix( + FLOW_CONTRACTS[contractType][flowNetwork] + ) + return evmContractAddress +}