diff --git a/packages/fcl-ethereum-provider/src/wc.ts b/packages/fcl-ethereum-provider/src/wc-provider.ts similarity index 93% rename from packages/fcl-ethereum-provider/src/wc.ts rename to packages/fcl-ethereum-provider/src/wc-provider.ts index b6796654e..4873c0ae2 100644 --- a/packages/fcl-ethereum-provider/src/wc.ts +++ b/packages/fcl-ethereum-provider/src/wc-provider.ts @@ -16,7 +16,7 @@ import {formatChainId} from "./util/eth" import {getAccountsFromNamespaces} from "@walletconnect/utils" import {FLOW_METHODS} from "@onflow/fcl-wc" import * as fcl from "@onflow/fcl" -import {CurrentUser, Service} from "@onflow/typedefs" +import {Service} from "@onflow/typedefs" const BASE_WC_SERVICE = ( externalProvider: InstanceType @@ -43,11 +43,11 @@ const BASE_WC_SERVICE = ( }, }) as unknown as Service -export class ExtendedEthereumProvider extends EthereumProvider { +export class WalletConnectEthereumProvider extends EthereumProvider { static async init( opts: EthereumProviderOptions - ): Promise { - const provider = new ExtendedEthereumProvider() + ): Promise { + const provider = new WalletConnectEthereumProvider() await provider.initialize(opts) // Refresh the FCL user to align with the WalletConnect session @@ -63,7 +63,10 @@ export class ExtendedEthereumProvider extends EthereumProvider { // If there’s no auth service or the auth service if (authnService && authnService.uid !== wcService.uid) { - // TODO: need to handle... maybe wait for condition to reauthenticate + // Another FCL user is already authenticated, we need to unauthenticate it + if (provider.signer.session) { + await fclUser.authenticate({service: wcService}) + } } else { // Determine the external provider's topic from the auth service params const externalProvider = authnService?.params?.externalProvider as @@ -108,10 +111,7 @@ export class ExtendedEthereumProvider extends EthereumProvider { return provider } - // TODO: remove - protected async initialize(opts: EthereumProviderOptions): Promise { - await super.initialize(opts) - } + async connect( opts?: Parameters["connect"]>[0] ) { diff --git a/packages/fcl-wagmi-adapter/src/fcl-connector.ts b/packages/fcl-wagmi-adapter/src/fcl-connector.ts new file mode 100644 index 000000000..5fffa43bb --- /dev/null +++ b/packages/fcl-wagmi-adapter/src/fcl-connector.ts @@ -0,0 +1,215 @@ +import { + ChainNotConfiguredError, + type Connector, + createConnector, +} from "@wagmi/core" +import { + type Address, + type ProviderConnectInfo, + ProviderDisconnectedError, + SwitchChainError, + getAddress, + numberToHex, +} from "viem" +import {createProvider} from "@onflow/fcl-ethereum-provider" + +type FclWagmiAdapterParams = Parameters[0] + +export function fclWagmiAdapter(params: FclWagmiAdapterParams) { + type Provider = ReturnType + type Properties = { + onConnect(connectInfo: ProviderConnectInfo): void + onDisplayUri(uri: string): void + } + let provider: Provider | undefined + + let accountsChanged: Connector["onAccountsChanged"] | undefined + let chainChanged: Connector["onChainChanged"] | undefined + let connect: Connector["onConnect"] | undefined + let disconnect: ((error: Error) => void) | undefined + + // Parse and validate service parameters + const id = params.service?.uid || "fcl" + const name = params.service?.provider?.name || "Cadence Wallet" + + // TODO: we need to surface this through FCL service configuration + const rdns = (params.service?.provider as any)?.rdns + + return createConnector(config => ({ + id: id, + name: name, + type: "fcl-wagmi-adapter", + rdns: rdns, + async setup() { + const provider = await this.getProvider() + + 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) + } + }, + async connect({isReconnecting}: any = {}) { + const provider = await this.getProvider() + + 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 = 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 = (error: Error) => { + throw new ProviderDisconnectedError(error) + } + provider.on("disconnect", disconnect) + + return {accounts, chainId: await this.getChainId()} + }, + async disconnect() { + const provider = await this.getProvider() + + // Manage EIP-1193 event listeners + if (chainChanged) provider.removeListener("chainChanged", chainChanged) + chainChanged = undefined + + if (disconnect) provider.removeListener("disconnect", disconnect) + disconnect = undefined + + if (connect) provider.removeListener("connect", connect) + connect = this.onConnect.bind(this) + provider.on("connect", connect) + + await provider.disconnect() + }, + async getAccounts() { + const provider = await this.getProvider() + const accounts = (await provider.request({ + method: "eth_accounts", + })) as string[] + return accounts.map(x => getAddress(x)) + }, + async getChainId() { + const provider = await this.getProvider() + const chainId = await provider.request({method: "eth_chainId"}) + console.log("CHAIN ID", chainId) + return Number(chainId) + }, + async getProvider() { + return provider ?? (provider = createProvider(params)) + }, + async isAuthorized() { + // 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() + 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) + if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()) + + try { + await provider.request({ + method: "wallet_switchEthereumChain", + params: [{chainId: numberToHex(chainId)}], + }) + + return chain + } catch (err) { + // TODO: Error handling + throw new SwitchChainError(err as Error) + } + }, + onAccountsChanged(accounts) { + if (accounts.length === 0) this.onDisconnect() + else + config.emitter.emit("change", { + accounts: accounts.map((x: any) => getAddress(x)), + }) + }, + onChainChanged(chain) { + const chainId = Number(chain) + config.emitter.emit("change", {chainId}) + }, + async onConnect(connectInfo) { + const accounts = await this.getAccounts() + + // TODO: What to do if accounts is empty? not sure this is accurate + if (accounts.length === 0) return + + const chainId = Number(connectInfo.chainId) + config.emitter.emit("connect", {accounts, chainId}) + + const provider = await this.getProvider() + + 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 = (error: Error) => { + throw new ProviderDisconnectedError(error) + } + provider.on("disconnect", disconnect) + }, + // TODO: waht to do with error? + async onDisconnect(error) { + const provider = await this.getProvider() + + config.emitter.emit("disconnect") + + // Manage EIP-1193 event listeners + if (chainChanged) { + provider.removeListener("chainChanged", chainChanged) + chainChanged = undefined + } + if (disconnect) { + provider.removeListener("disconnect", disconnect) + disconnect = undefined + } + if (!connect) { + connect = this.onConnect.bind(this) + provider.on("connect", connect) + } + }, + onDisplayUri(uri: string) { + config.emitter.emit("message", {type: "display_uri", data: uri}) + }, + })) +} diff --git a/packages/fcl-wagmi-adapter/src/index.ts b/packages/fcl-wagmi-adapter/src/index.ts index ad4b4a2b7..6cea6ebfa 100644 --- a/packages/fcl-wagmi-adapter/src/index.ts +++ b/packages/fcl-wagmi-adapter/src/index.ts @@ -1,217 +1,2 @@ -import { - ChainNotConfiguredError, - type Connector, - createConnector, -} from "@wagmi/core" -import { - type Address, - type ProviderConnectInfo, - ProviderDisconnectedError, - SwitchChainError, - getAddress, - numberToHex, -} from "viem" -import {createProvider} from "@onflow/fcl-ethereum-provider" - -type FclWagmiAdapterParams = Parameters[0] - -export function fclWagmiAdapter(params: FclWagmiAdapterParams) { - type Provider = ReturnType - type Properties = { - onConnect(connectInfo: ProviderConnectInfo): void - onDisplayUri(uri: string): void - } - let provider: Provider | undefined - - let accountsChanged: Connector["onAccountsChanged"] | undefined - let chainChanged: Connector["onChainChanged"] | undefined - let connect: Connector["onConnect"] | undefined - let disconnect: ((error: Error) => void) | undefined - - // Parse and validate service parameters - const id = params.service?.uid || "fcl" - const name = params.service?.provider?.name || "Cadence Wallet" - - // TODO: we need to surface this through FCL service configuration - const rdns = (params.service?.provider as any)?.rdns - - return createConnector(config => ({ - id: id, - name: name, - type: "fcl-wagmi-adapter", - rdns: rdns, - async setup() { - const provider = await this.getProvider() - - 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) - } - }, - async connect({isReconnecting}: any = {}) { - const provider = await this.getProvider() - - 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 = 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 = (error: Error) => { - throw new ProviderDisconnectedError(error) - } - provider.on("disconnect", disconnect) - - return {accounts, chainId: await this.getChainId()} - }, - async disconnect() { - const provider = await this.getProvider() - - // Manage EIP-1193 event listeners - if (chainChanged) provider.removeListener("chainChanged", chainChanged) - chainChanged = undefined - - if (disconnect) provider.removeListener("disconnect", disconnect) - disconnect = undefined - - if (connect) provider.removeListener("connect", connect) - connect = this.onConnect.bind(this) - provider.on("connect", connect) - - await provider.disconnect() - }, - async getAccounts() { - const provider = await this.getProvider() - const accounts = (await provider.request({ - method: "eth_accounts", - })) as string[] - return accounts.map(x => getAddress(x)) - }, - async getChainId() { - const provider = await this.getProvider() - const chainId = await provider.request({method: "eth_chainId"}) - console.log("CHAIN ID", chainId) - return Number(chainId) - }, - async getProvider() { - return provider ?? (provider = createProvider(params)) - }, - async isAuthorized() { - // 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() - 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) - if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()) - - try { - await provider.request({ - method: "wallet_switchEthereumChain", - params: [{chainId: numberToHex(chainId)}], - }) - - return chain - } catch (err) { - // TODO: Error handling - throw new SwitchChainError(err as Error) - } - }, - onAccountsChanged(accounts) { - if (accounts.length === 0) this.onDisconnect() - else - config.emitter.emit("change", { - accounts: accounts.map((x: any) => getAddress(x)), - }) - }, - onChainChanged(chain) { - const chainId = Number(chain) - config.emitter.emit("change", {chainId}) - }, - async onConnect(connectInfo) { - const accounts = await this.getAccounts() - - // TODO: What to do if accounts is empty? not sure this is accurate - if (accounts.length === 0) return - - const chainId = Number(connectInfo.chainId) - config.emitter.emit("connect", {accounts, chainId}) - - const provider = await this.getProvider() - - 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 = (error: Error) => { - throw new ProviderDisconnectedError(error) - } - provider.on("disconnect", disconnect) - }, - // TODO: waht to do with error? - async onDisconnect(error) { - const provider = await this.getProvider() - - config.emitter.emit("disconnect") - - // Manage EIP-1193 event listeners - if (chainChanged) { - provider.removeListener("chainChanged", chainChanged) - chainChanged = undefined - } - if (disconnect) { - provider.removeListener("disconnect", disconnect) - disconnect = undefined - } - if (!connect) { - connect = this.onConnect.bind(this) - provider.on("connect", connect) - } - }, - onDisplayUri(uri: string) { - config.emitter.emit("message", {type: "display_uri", data: uri}) - }, - })) -} - -export * from "./wc" +export * from "./wc-connector" +export * from "./fcl-connector" diff --git a/packages/fcl-wagmi-adapter/src/wc.ts b/packages/fcl-wagmi-adapter/src/wc-connector.ts similarity index 91% rename from packages/fcl-wagmi-adapter/src/wc.ts rename to packages/fcl-wagmi-adapter/src/wc-connector.ts index afdbfec54..5ca617f69 100644 --- a/packages/fcl-wagmi-adapter/src/wc.ts +++ b/packages/fcl-wagmi-adapter/src/wc-connector.ts @@ -1,3 +1,35 @@ +/** + * This file is a modified version of the original WalletConnect connector from wagmi. + * The purpose is to substitute the original WalletConnect EthereumProvider with an extended + * version that is able to be used in a cross-VM context (authenticating multiple VMs). + * + * See: https://github.com/wevm/wagmi/blob/2ca5742840f0c3be99cd61095650400aee514913/packages/connectors/src/walletConnect.ts + */ + +/*! + * MIT License + * + * Copyright (c) 2022-present weth, LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + import { ChainNotConfiguredError, type Connector, @@ -127,7 +159,6 @@ export function walletConnect(parameters: WalletConnectParameters) { } }, async connect({chainId, ...rest} = {}) { - console.log("HELLO") try { const provider = await this.getProvider() if (!provider) throw new ProviderNotFoundError()