diff --git a/package-lock.json b/package-lock.json index 858b20412..c4ae5c935 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31347,7 +31347,7 @@ "jest": "^29.7.0" }, "peerDependencies": { - "@onflow/fcl": "1.13.4" + "@onflow/fcl": "^1.13.4" } }, "packages/fcl-rainbowkit-adapter": { @@ -31378,7 +31378,7 @@ "jest": "^29.7.0" }, "peerDependencies": { - "@onflow/fcl": "1.13.4", + "@onflow/fcl": "^1.13.4", "@rainbow-me/rainbowkit": "^2.2.3" } }, @@ -31669,7 +31669,7 @@ "jest": "^29.7.0" }, "peerDependencies": { - "@onflow/fcl": "1.13.4", + "@onflow/fcl": "^1.13.4", "@wagmi/core": "^2.16.3" } }, 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 index 94d6e578f..212f4cbfc 100644 --- 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 @@ -1,4 +1,4 @@ -import {formatChainId} from "../../util/eth" +import {NetworkManager} from "../../network/network-manager" import {ethChainId} from "./eth-chain-id" jest.mock("../../util/eth", () => ({ @@ -13,7 +13,7 @@ describe("eth_chainId handler", () => { } networkManagerMock.getChainId.mockResolvedValue(747) - const chainId = await ethChainId(networkManagerMock as any)() + const chainId = await ethChainId(networkManagerMock as any) expect(chainId).toEqual("0x747") expect(networkManagerMock.getChainId).toHaveBeenCalled() @@ -22,10 +22,10 @@ describe("eth_chainId handler", () => { test("should throw an error if no chain id is available", async () => { const networkManagerMock = { getChainId: jest.fn(), - } + } as unknown as jest.Mocked networkManagerMock.getChainId.mockResolvedValue(null) - await expect(ethChainId(networkManagerMock as any)()).rejects.toThrow( + await expect(ethChainId(networkManagerMock)).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 index 9246bcfe3..4614af03e 100644 --- a/packages/fcl-ethereum-provider/src/rpc/handlers/eth-chain-id.ts +++ b/packages/fcl-ethereum-provider/src/rpc/handlers/eth-chain-id.ts @@ -1,12 +1,10 @@ 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) +export async function ethChainId(networkManager: NetworkManager) { + 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 ba50102b7..c153c8c43 100644 --- a/packages/fcl-ethereum-provider/src/rpc/rpc-processor.ts +++ b/packages/fcl-ethereum-provider/src/rpc/rpc-processor.ts @@ -88,7 +88,7 @@ export class RpcProcessor { const switchParams = params[0] as SwitchEthereumChainParams return await this.networkManager.switchChain(switchParams) case "eth_chainId": - return ethChainId(this.networkManager) + return await ethChainId(this.networkManager) default: return await this.gateway.request({ chainId, diff --git a/packages/fcl-wagmi-adapter/src/index.ts b/packages/fcl-wagmi-adapter/src/index.ts index 73c28af31..5429598e5 100644 --- a/packages/fcl-wagmi-adapter/src/index.ts +++ b/packages/fcl-wagmi-adapter/src/index.ts @@ -4,19 +4,12 @@ import { createConnector, } from "@wagmi/core" import { - type AddEthereumChainParameter, type Address, - type Hex, type ProviderConnectInfo, ProviderDisconnectedError, - type ProviderRpcError, - type RpcError, SwitchChainError, - UserRejectedRequestError, getAddress, - hexToNumber, numberToHex, - withRetry, } from "viem" import {createProvider} from "@onflow/fcl-ethereum-provider" @@ -47,21 +40,18 @@ export function fclWagmiAdapter(params: FclWagmiAdapterParams) { async setup() { const provider = await this.getProvider() - if (provider?.on) { - if (!connect) { - connect = this.onConnect.bind(this) - provider.on("connect", connect) - } + if (connect) provider.removeListener("connect", connect) + connect = this.onConnect.bind(this) + provider.on("connect", connect) - // We shouldn't need to listen for `'accountsChanged'` here since the `'connect'` event should suffice (and wallet shouldn't be connected yet). - // Some wallets, like MetaMask, do not implement the `'connect'` event and overload `'accountsChanged'` instead. - if (!accountsChanged) { - accountsChanged = this.onAccountsChanged.bind(this) - provider.on("accountsChanged", accountsChanged) - } + // We shouldn't need to listen for `'accountsChanged'` here since the `'connect'` event should suffice (and wallet shouldn't be connected yet). + // Some wallets, like MetaMask, do not implement the `'connect'` event and overload `'accountsChanged'` instead. + if (!accountsChanged) { + accountsChanged = this.onAccountsChanged.bind(this) + provider.on("accountsChanged", accountsChanged) } }, - async connect({chainId, isReconnecting}: any = {}) { + async connect({isReconnecting}: any = {}) { const provider = await this.getProvider() /* TODO: This will be solved as part of the WalletConnect integration. @@ -73,34 +63,41 @@ export function fclWagmiAdapter(params: FclWagmiAdapterParams) { */ - let accounts: readonly Address[] = await provider.request({ - method: "eth_requestAccounts", - }) - - if (isReconnecting) accounts = await this.getAccounts() + let accounts: readonly Address[] + if (isReconnecting) { + accounts = await this.getAccounts() + } else { + accounts = ( + (await provider.request({ + method: "eth_requestAccounts", + })) as string[] + ).map(x => getAddress(x)) + } // Manage EIP-1193 event listeners // https://eips.ethereum.org/EIPS/eip-1193#events - if (connect) { - provider.removeListener("connect", connect) - connect = undefined - } - if (!accountsChanged) { - accountsChanged = this.onAccountsChanged.bind(this) - provider.on("accountsChanged", accountsChanged) - } - if (!chainChanged) { - chainChanged = this.onChainChanged.bind(this) - provider.on("chainChanged", chainChanged) - } - if (!disconnect) { - disconnect = (({reason}) => { - throw new ProviderDisconnectedError(new Error(reason)) - }) as ({reason}: {reason: string}) => void - provider.on("disconnect", disconnect) - } + if (connect) provider.removeListener("connect", connect) + connect = this.onConnect.bind(this) + provider.on("connect", connect) + + if (accountsChanged) + provider.removeListener("accountsChanged", accountsChanged) + accountsChanged = this.onAccountsChanged.bind(this) + provider.on("accountsChanged", accountsChanged) + + if (chainChanged) provider.removeListener("chainChanged", chainChanged) + chainChanged = this.onChainChanged.bind(this) + provider.on("chainChanged", chainChanged) + + if (disconnect) provider.removeListener("disconnect", disconnect) + disconnect = (({reason}) => { + throw new ProviderDisconnectedError(new Error(reason)) + }) as ({reason}: {reason: string}) => void + provider.on("disconnect", disconnect) - return {accounts, chainId} + console.log("EH CURRENT FUUU", await this.getChainId()) + + return {accounts, chainId: await this.getChainId()} }, async disconnect() { const provider = await this.getProvider() @@ -130,7 +127,8 @@ export function fclWagmiAdapter(params: FclWagmiAdapterParams) { }, async getChainId() { const provider = await this.getProvider() - const chainId = await provider?.request({method: "eth_chainId"}) + const chainId = await provider.request({method: "eth_chainId"}) + console.log("CHAIN ID", chainId) return Number(chainId) }, async getProvider() { @@ -140,9 +138,11 @@ export function fclWagmiAdapter(params: FclWagmiAdapterParams) { // TODO: There may be an issue here if a user without a COA refreshes the page // We should instead be checking whether FCL itself is authorized const accounts = await this.getAccounts() + console.log("RETURNING", accounts.length > 0) return accounts.length > 0 }, async switchChain({addEthereumChainParameter, chainId}: any) { + console.log("HEY") const provider = await this.getProvider() const chain = config.chains.find(x => x.id === chainId) @@ -154,152 +154,58 @@ export function fclWagmiAdapter(params: FclWagmiAdapterParams) { params: [{chainId: numberToHex(chainId)}], }) - // During `'wallet_switchEthereumChain'`, MetaMask makes a `'net_version'` RPC call to the target chain. - // If this request fails, MetaMask does not emit the `'chainChanged'` event, but will still switch the chain. - // To counter this behavior, we request and emit the current chain ID to confirm the chain switch either via - // this callback or an externally emitted `'chainChanged'` event. - // https://github.com/MetaMask/metamask-extension/issues/24247 - await waitForChainIdToSync() - await sendAndWaitForChangeEvent(chainId) - return chain } catch (err) { - const error = err as RpcError - - if (error.code === UserRejectedRequestError.code) - throw new UserRejectedRequestError(error) - - // Indicates chain is not added to provider - if ( - error.code === 4902 || - // Unwrapping for MetaMask Mobile - // https://github.com/MetaMask/metamask-mobile/issues/2944#issuecomment-976988719 - (error as ProviderRpcError<{originalError?: {code: number}}>)?.data - ?.originalError?.code === 4902 - ) { - try { - await provider.request({ - method: "wallet_addEthereumChain", - params: [ - { - blockExplorerUrls: (() => { - const {default: blockExplorer, ...blockExplorers} = - chain.blockExplorers ?? {} - if (addEthereumChainParameter?.blockExplorerUrls) - return addEthereumChainParameter.blockExplorerUrls - if (blockExplorer) - return [ - blockExplorer.url, - ...Object.values(blockExplorers).map(x => x.url), - ] - return - })(), - chainId: numberToHex(chainId), - chainName: addEthereumChainParameter?.chainName ?? chain.name, - iconUrls: addEthereumChainParameter?.iconUrls, - nativeCurrency: - addEthereumChainParameter?.nativeCurrency ?? - chain.nativeCurrency, - rpcUrls: (() => { - if (addEthereumChainParameter?.rpcUrls?.length) - return addEthereumChainParameter.rpcUrls - return [chain.rpcUrls.default?.http[0] ?? ""] - })(), - } satisfies AddEthereumChainParameter, - ], - }) - - await waitForChainIdToSync() - await sendAndWaitForChangeEvent(chainId) - - return chain - } catch (err) { - const error = err as RpcError - if (error.code === UserRejectedRequestError.code) - throw new UserRejectedRequestError(error) - throw new SwitchChainError(error) - } - } - - throw new SwitchChainError(error) - } - - async function waitForChainIdToSync() { - // On mobile, there is a race condition between the result of `'wallet_addEthereumChain'` and `'eth_chainId'`. - // To avoid this, we wait for `'eth_chainId'` to return the expected chain ID with a retry loop. - await withRetry( - async () => { - const value = hexToNumber( - // `'eth_chainId'` is cached by the MetaMask SDK side to avoid unnecessary deeplinks - (await provider.request({method: "eth_chainId"})) as Hex - ) - // `value` doesn't match expected `chainId`, throw to trigger retry - if (value !== chainId) - throw new Error("User rejected switch after adding network.") - return value - }, - { - delay: 50, - retryCount: 20, // android device encryption is slower - } - ) - } - - async function sendAndWaitForChangeEvent(chainId: number) { - await new Promise(resolve => { - const listener = (data => { - if ("chainId" in data && data.chainId === chainId) { - config.emitter.off("change", listener) - resolve() - } - }) satisfies Parameters[1] - config.emitter.on("change", listener) - config.emitter.emit("change", {chainId}) - }) + // TODO: Error handling + throw new SwitchChainError(err as Error) } }, - onAccountsChanged(accounts: any) { + onAccountsChanged(accounts) { if (accounts.length === 0) this.onDisconnect() else config.emitter.emit("change", { accounts: accounts.map((x: any) => getAddress(x)), }) }, - onChainChanged(chain: any) { + onChainChanged(chain) { const chainId = Number(chain) config.emitter.emit("change", {chainId}) }, - async onConnect(connectInfo: any) { + async onConnect(connectInfo) { + console.log("HEY") const accounts = await this.getAccounts() // TODO: What to do if accounts is empty? not sure this is accurate if (accounts.length === 0) return + console.log({accounts, chainId: Number(connectInfo.chainId)}) + const chainId = Number(connectInfo.chainId) + console.log("onConnect", {accounts, chainId}) config.emitter.emit("connect", {accounts, chainId}) const provider = await this.getProvider() - if (connect) { - provider.removeListener("connect", connect) - connect = undefined - } - if (!accountsChanged) { - accountsChanged = this.onAccountsChanged.bind(this) - provider.on("accountsChanged", accountsChanged) - } - if (!chainChanged) { - chainChanged = this.onChainChanged.bind(this) - provider.on("chainChanged", chainChanged) - } - if (!disconnect) { - disconnect = (({reason}) => { - throw new ProviderDisconnectedError(new Error(reason)) - }) as ({reason}: {reason: string}) => void - provider.on("disconnect", disconnect) - } + + if (connect) provider.removeListener("connect", connect) + connect = undefined + + if (accountsChanged) + provider.removeListener("accountsChanged", accountsChanged) + accountsChanged = this.onAccountsChanged.bind(this) + provider.on("accountsChanged", accountsChanged) + + if (chainChanged) provider.removeListener("chainChanged", chainChanged) + chainChanged = this.onChainChanged.bind(this) + provider.on("chainChanged", chainChanged) + + if (disconnect) provider.removeListener("disconnect", disconnect) + disconnect = (({reason}) => { + throw new ProviderDisconnectedError(new Error(reason)) + }) as ({reason}: {reason: string}) => void + provider.on("disconnect", disconnect) }, // TODO: waht to do with error? - async onDisconnect(error: any) { + async onDisconnect(error) { const provider = await this.getProvider() config.emitter.emit("disconnect")