From 1bd3dc70850257dd8db523499e8a38e3a0f2ac4a Mon Sep 17 00:00:00 2001 From: Sven <38101365+svenvoskamp@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:06:42 +0100 Subject: [PATCH] :feat refactor adapter clients (#3076) Co-authored-by: Felipe Mendes --- .changeset/pink-rockets-explain.md | 24 + .../components/Ethers/Ethers5ModalInfo.tsx | 2 +- .../src/components/Ethers/EthersModalInfo.tsx | 2 +- .../MultiChainTestsEthersSolana.tsx | 8 +- apps/laboratory/src/pages/library/wagmi.tsx | 2 +- ...multichain-wagmi-solana-email-siwe.spec.ts | 2 + packages/adapters/ethers/src/client.ts | 1369 ++++------------- packages/adapters/ethers/src/index.ts | 1 - .../adapters/ethers/src/tests/client.test.ts | 1100 +++---------- .../ethers/src/utils/EthersMethods.ts | 12 +- packages/adapters/ethers5/src/client.ts | 1364 ++++------------ packages/adapters/ethers5/src/index.ts | 1 - .../adapters/ethers5/src/tests/client.test.ts | 1112 +++---------- .../ethers5/src/utils/Ethers5Methods.ts | 10 +- packages/adapters/solana/src/client.ts | 918 ++++------- .../adapters/solana/src/tests/client.test.ts | 309 ++-- .../utils/handleMobileWalletRedirection.ts | 17 + packages/adapters/wagmi/src/client.ts | 1253 ++++----------- .../wagmi/src/connectors/AuthConnector.ts | 3 +- .../src/connectors/AuthConnectorExport.ts | 13 +- .../src/connectors/UniversalConnector.ts | 4 +- packages/adapters/wagmi/src/index.ts | 3 - .../adapters/wagmi/src/tests/client.test.ts | 763 +++------ .../wagmi/src/tests/mocks/adapter.mock.ts | 70 - packages/appkit-utils/src/ConstantsUtil.ts | 11 +- .../src/ethers/EthersStoreUtil.ts | 2 +- packages/appkit/exports/adapters.ts | 1 + packages/appkit/exports/constants.ts | 2 +- packages/appkit/package.json | 8 + .../src/adapters/ChainAdapterBlueprint.ts | 484 ++++++ .../src/adapters/ChainAdapterConnector.ts | 6 + packages/appkit/src/adapters/index.ts | 1 + packages/appkit/src/client.ts | 1150 +++++++++++++- packages/appkit/src/store/ProviderUtil.ts | 11 +- packages/appkit/src/store/index.ts | 2 +- packages/appkit/src/tests/appkit.test.ts | 47 +- .../src/tests/universal-adapter.test.ts | 286 ++-- .../appkit/src/universal-adapter/client.ts | 798 ++-------- .../appkit/src/universal-adapter/index.ts | 2 +- packages/appkit/src/utils/HelpersUtil.ts | 8 + packages/common/src/utils/SafeLocalStorage.ts | 6 +- .../core/src/controllers/AccountController.ts | 9 +- .../core/src/controllers/ChainController.ts | 161 +- .../src/controllers/ConnectionController.ts | 67 +- .../src/controllers/ConnectorController.ts | 8 +- .../core/src/controllers/EnsController.ts | 2 +- .../core/src/controllers/RouterController.ts | 2 +- .../core/src/controllers/SendController.ts | 9 +- .../core/src/controllers/SwapController.ts | 29 +- packages/core/src/utils/StorageUtil.ts | 10 +- packages/core/src/utils/SwapApiUtil.ts | 4 +- packages/core/src/utils/TypeUtil.ts | 26 +- .../controllers/AccountController.test.ts | 2 +- .../tests/controllers/ApiController.test.ts | 2 +- .../tests/controllers/ChainController.test.ts | 2 +- .../controllers/ConnectionController.test.ts | 20 +- .../controllers/ConnectorController.test.ts | 21 +- .../tests/controllers/EnsController.test.ts | 4 +- .../tests/controllers/SwapController.test.ts | 2 +- packages/core/tests/utils/StorageUtil.test.ts | 6 +- .../controllers/SmartSessionsController.ts | 19 +- .../src/smart-session/grantPermissions.ts | 6 +- .../src/modal/w3m-account-button/index.ts | 4 +- .../scaffold-ui/src/modal/w3m-modal/index.ts | 3 + .../partials/w3m-account-auth-button/index.ts | 2 +- .../w3m-account-default-widget/index.ts | 4 +- .../w3m-connect-external-widget/index.ts | 7 +- .../w3m-connect-injected-widget/index.ts | 2 +- .../src/partials/w3m-connector-list/index.ts | 8 +- .../src/partials/w3m-header/index.ts | 4 + .../views/w3m-account-settings-view/index.ts | 7 +- .../src/views/w3m-account-view/index.ts | 2 +- .../views/w3m-network-switch-view/index.ts | 2 +- .../src/views/w3m-networks-view/index.ts | 5 +- packages/siwe/src/client.ts | 4 +- .../src/composites/wui-list-account/index.ts | 4 +- packages/wallet/src/W3mFrameProvider.ts | 1 + 77 files changed, 4303 insertions(+), 7354 deletions(-) create mode 100644 .changeset/pink-rockets-explain.md create mode 100644 packages/adapters/solana/src/utils/handleMobileWalletRedirection.ts create mode 100644 packages/appkit/exports/adapters.ts create mode 100644 packages/appkit/src/adapters/ChainAdapterBlueprint.ts create mode 100644 packages/appkit/src/adapters/ChainAdapterConnector.ts create mode 100644 packages/appkit/src/adapters/index.ts diff --git a/.changeset/pink-rockets-explain.md b/.changeset/pink-rockets-explain.md new file mode 100644 index 0000000000..8c88dce755 --- /dev/null +++ b/.changeset/pink-rockets-explain.md @@ -0,0 +1,24 @@ +--- +'@reown/appkit-adapter-polkadot': minor +'@reown/appkit-adapter-ethers5': minor +'@reown/appkit-adapter-ethers': minor +'@reown/appkit-adapter-solana': minor +'@reown/appkit-adapter-wagmi': minor +'@reown/appkit-utils': minor +'@reown/appkit-experimental': minor +'@reown/appkit-scaffold-ui': minor +'@reown/appkit-polyfills': minor +'@apps/laboratory': minor +'@reown/appkit': minor +'@reown/appkit-common': minor +'@reown/appkit-wallet': minor +'@reown/appkit-core': minor +'@reown/appkit-siwe': minor +'@reown/appkit-siwx': minor +'@apps/gallery': minor +'@reown/appkit-cdn': minor +'@reown/appkit-ui': minor +'@apps/demo': minor +--- + +Implementing new architecture design for better handling and scalibity of the various adapters diff --git a/apps/laboratory/src/components/Ethers/Ethers5ModalInfo.tsx b/apps/laboratory/src/components/Ethers/Ethers5ModalInfo.tsx index b513d96137..2d4c9de200 100644 --- a/apps/laboratory/src/components/Ethers/Ethers5ModalInfo.tsx +++ b/apps/laboratory/src/components/Ethers/Ethers5ModalInfo.tsx @@ -13,7 +13,7 @@ export function Ethers5ModalInfo() { const { walletProvider, walletProviderType } = useAppKitProvider('eip155') async function getClientId() { - if (walletProviderType === 'walletConnect') { + if (walletProviderType === 'WALLET_CONNECT') { return await walletProvider?.client?.core?.crypto?.getClientId() } diff --git a/apps/laboratory/src/components/Ethers/EthersModalInfo.tsx b/apps/laboratory/src/components/Ethers/EthersModalInfo.tsx index a962ddd3e6..1345e9aee6 100644 --- a/apps/laboratory/src/components/Ethers/EthersModalInfo.tsx +++ b/apps/laboratory/src/components/Ethers/EthersModalInfo.tsx @@ -12,7 +12,7 @@ export function EthersModalInfo() { const { walletProvider, walletProviderType } = useAppKitProvider('eip155') async function getClientId() { - if (walletProviderType === 'walletConnect') { + if (walletProviderType === 'WALLET_CONNECT') { return await walletProvider?.client?.core?.crypto?.getClientId() } diff --git a/apps/laboratory/src/components/MultiChainTestsEthersSolana.tsx b/apps/laboratory/src/components/MultiChainTestsEthersSolana.tsx index 51118f61fc..12f2e60e6f 100644 --- a/apps/laboratory/src/components/MultiChainTestsEthersSolana.tsx +++ b/apps/laboratory/src/components/MultiChainTestsEthersSolana.tsx @@ -4,6 +4,7 @@ import { useAppKitState } from '@reown/appkit/react' import { SolanaTests } from './Solana/SolanaTests' import { EthersTests } from './Ethers/EthersTests' import { EthersModalInfo } from './Ethers/EthersModalInfo' +import { SolanaModalInfo } from './Solana/SolanaModalInfo' export function MultiChainTestsEthersSolana() { const { activeChain } = useAppKitState() @@ -16,7 +17,12 @@ export function MultiChainTestsEthersSolana() { ) : null} - {activeChain === 'solana' ? : null} + {activeChain === 'solana' ? ( + + + + + ) : null} ) } diff --git a/apps/laboratory/src/pages/library/wagmi.tsx b/apps/laboratory/src/pages/library/wagmi.tsx index a0d91237d0..1972e7f61d 100644 --- a/apps/laboratory/src/pages/library/wagmi.tsx +++ b/apps/laboratory/src/pages/library/wagmi.tsx @@ -18,7 +18,7 @@ const wagmiAdapter = new WagmiAdapter({ const modal = createAppKit({ adapters: [wagmiAdapter], - networks: wagmiAdapter.caipNetworks, + networks: ConstantsUtil.EvmNetworks, projectId: ConstantsUtil.ProjectId, features: { analytics: true diff --git a/apps/laboratory/tests/multichain/multichain-wagmi-solana-email-siwe.spec.ts b/apps/laboratory/tests/multichain/multichain-wagmi-solana-email-siwe.spec.ts index 64ba005d34..38b19f4c21 100644 --- a/apps/laboratory/tests/multichain/multichain-wagmi-solana-email-siwe.spec.ts +++ b/apps/laboratory/tests/multichain/multichain-wagmi-solana-email-siwe.spec.ts @@ -59,6 +59,8 @@ test('it should switch to different namespace', async () => { const chainName = 'Solana' await page.switchNetwork(chainName) + await validator.expectUnauthenticated() + await page.closeModal() await page.openAccount() await page.openNetworks() await validator.expectSwitchedNetwork(chainName) diff --git a/packages/adapters/ethers/src/client.ts b/packages/adapters/ethers/src/client.ts index df30e9eb8c..683e1d3728 100644 --- a/packages/adapters/ethers/src/client.ts +++ b/packages/adapters/ethers/src/client.ts @@ -1,114 +1,34 @@ -import type { AppKitOptions, AppKitOptionsWithCaipNetworks } from '@reown/appkit' -import { - SafeLocalStorage, - SafeLocalStorageKeys, - type AdapterType, - type CaipAddress, - type CaipNetwork, - type CaipNetworkId, - type ChainNamespace -} from '@reown/appkit-common' +import { AdapterBlueprint } from '@reown/appkit/adapters' +import type { CaipNetwork } from '@reown/appkit-common' +import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' import { - AccountController, - ChainController, - CoreHelperUtil, - AlertController, type CombinedProvider, - type Connector + type Connector, + type ConnectorType, + type Provider } from '@reown/appkit-core' -import { - EthersHelpersUtil, - type Provider, - type ProviderType, - type Address -} from '@reown/appkit-utils/ethers' -import type { AppKit } from '@reown/appkit' -import { - W3mFrameHelpers, - W3mFrameProvider, - W3mFrameRpcConstants, - type W3mFrameTypes -} from '@reown/appkit-wallet' -import { ConstantsUtil as CoreConstantsUtil } from '@reown/appkit-core' -import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' -import { - CaipNetworksUtil, - ConstantsUtil, - ErrorUtil, - HelpersUtil, - PresetsUtil -} from '@reown/appkit-utils' +import { ConstantsUtil, PresetsUtil } from '@reown/appkit-utils' +import { EthersHelpersUtil, type ProviderType } from '@reown/appkit-utils/ethers' +import { WcConstantsUtil, WcHelpersUtil, type AppKitOptions } from '@reown/appkit' import UniversalProvider from '@walletconnect/universal-provider' -import type { ConnectionControllerClient, NetworkControllerClient } from '@reown/appkit-core' -import { WcConstantsUtil } from '@reown/appkit' -import { EthersMethods } from './utils/EthersMethods.js' import { formatEther, InfuraProvider, JsonRpcProvider } from 'ethers' -import type { PublicStateControllerState } from '@reown/appkit-core' -import { ProviderUtil, type ProviderIdType } from '@reown/appkit/store' import { CoinbaseWalletSDK, type ProviderInterface } from '@coinbase/wallet-sdk' -import { W3mFrameProviderSingleton } from '@reown/appkit/auth-provider' - -// -- Types --------------------------------------------------------------------- -export interface AdapterOptions { - ethersConfig: ProviderType - defaultCaipNetwork?: CaipNetwork -} - -type CoinbaseProviderError = { - code: number - message: string - data: string | undefined -} - -interface ExternalProvider extends Provider { - accounts: string[] -} - -declare global { - interface Window { - ethereum?: Record - } -} - -interface Info { - uuid: string - name: string - icon: string - rdns: string -} +import type { W3mFrameProvider } from '@reown/appkit-wallet' +import { EthersMethods } from './utils/EthersMethods.js' export interface EIP6963ProviderDetail { - info: Info + info: Connector['info'] provider: Provider } -export class EthersAdapter { - private appKit: AppKit | undefined = undefined - - private EIP6963Providers: EIP6963ProviderDetail[] = [] - - private ethersConfig?: AdapterOptions['ethersConfig'] - - private authProvider?: W3mFrameProvider - - // -- Public variables -------------------------------------------------------- - public options: AppKitOptions | undefined = undefined +export class EthersAdapter extends AdapterBlueprint { + private ethersConfig?: ProviderType + public adapterType = 'ethers' - public caipNetworks: CaipNetwork[] = [] - - public chainNamespace: ChainNamespace = CommonConstantsUtil.CHAIN.EVM - - public networkControllerClient?: NetworkControllerClient - - public connectionControllerClient?: ConnectionControllerClient - - public siweControllerClient = this.options?.siweConfig - - public tokens = HelpersUtil.getCaipTokens(this.options?.tokens) - - public defaultCaipNetwork: CaipNetwork | undefined = undefined - - public adapterType: AdapterType = 'ethers' + constructor() { + super({}) + this.namespace = CommonConstantsUtil.CHAIN.EVM + } private createEthersConfig(options: AppKitOptions) { if (!options.metadata) { @@ -175,609 +95,376 @@ export class EthersAdapter { return providers } - // -- Public ------------------------------------------------------------------- - // eslint-disable-next-line @typescript-eslint/no-useless-constructor, @typescript-eslint/no-empty-function - public constructor() { - ChainController.subscribeKey('activeCaipNetwork', val => { - const caipAddress = this.appKit?.getCaipAddress(this.chainNamespace) - const isEVMAddress = caipAddress?.startsWith('eip155:') - const isEVMNetwork = val?.chainNamespace === this.chainNamespace - - if (isEVMAddress && isEVMNetwork && caipAddress) { - this.syncBalance(CoreHelperUtil.getPlainAddress(caipAddress) as Address, val) - this.syncAccount({ - address: CoreHelperUtil.getPlainAddress(caipAddress) as Address | undefined, - caipNetwork: val - }) - } - }) - ChainController.subscribeKey('activeCaipAddress', val => { - const isEVMAddress = val?.startsWith('eip155:') - const caipNetwork = ChainController.state.activeCaipNetwork - const isEVMNetwork = caipNetwork?.chainNamespace === this.chainNamespace - - if (isEVMAddress) { - if (isEVMNetwork) { - this.syncBalance(CoreHelperUtil.getPlainAddress(val) as Address, caipNetwork) - } - this.syncAccount({ address: CoreHelperUtil.getPlainAddress(val) as Address }) - } - }) - AccountController.subscribeKey( - 'shouldUpdateToAddress', - newAddress => { - const isEVMAddress = newAddress?.startsWith('0x') - - if (isEVMAddress) { - this.syncAccount({ address: newAddress as Address }) - } - }, - this.chainNamespace - ) - } - - public construct(appKit: AppKit, options: AppKitOptionsWithCaipNetworks) { - this.appKit = appKit - this.options = options - this.caipNetworks = options.networks - this.defaultCaipNetwork = options.defaultNetwork - ? CaipNetworksUtil.extendCaipNetwork(options.defaultNetwork, { - customNetworkImageUrls: options.chainImages, - projectId: options.projectId - }) - : this.caipNetworks[0] - this.tokens = HelpersUtil.getCaipTokens(options.tokens) - this.ethersConfig = this.createEthersConfig(options) - - this.networkControllerClient = { - switchCaipNetwork: async caipNetwork => { - if (caipNetwork?.id) { - try { - await this.switchNetwork(caipNetwork) - } catch (error) { - throw new Error('networkControllerClient:switchCaipNetwork - unable to switch chain') - } - } - }, + public async signMessage( + params: AdapterBlueprint.SignMessageParams + ): Promise { + const { message, address, provider } = params - // eslint-disable-next-line @typescript-eslint/require-await - getApprovedCaipNetworksData: async () => this.getApprovedCaipNetworksData() + if (!provider) { + throw new Error('Provider is undefined') } + try { + const signature = await EthersMethods.signMessage(message, provider as Provider, address) - this.connectionControllerClient = { - connectWalletConnect: async onUri => { - await this.appKit?.universalAdapter?.connectionControllerClient?.connectWalletConnect?.( - onUri - ) - }, - - // @ts-expect-error TODO expected types in arguments are incomplete - connectExternal: async ({ - id, - info, - provider - }: { - id: string - info?: Info - provider: Provider - }) => { - this.appKit?.setClientId(null) - - const connectorConfig = { - [ConstantsUtil.INJECTED_CONNECTOR_ID]: { - getProvider: () => this.ethersConfig?.injected, - providerType: 'injected' as const - }, - [ConstantsUtil.EIP6963_CONNECTOR_ID]: { - getProvider: () => provider, - providerType: 'eip6963' as const - }, - [ConstantsUtil.COINBASE_SDK_CONNECTOR_ID]: { - getProvider: () => this.ethersConfig?.coinbase, - providerType: 'coinbaseWalletSDK' as const - }, - [ConstantsUtil.AUTH_CONNECTOR_ID]: { - getProvider: () => this.authProvider, - providerType: 'w3mAuth' as const - } - } - - const selectedConnector = connectorConfig[id] - - if (!selectedConnector) { - throw new Error(`Unsupported connector ID: ${id}`) - } - - const selectedProvider = selectedConnector.getProvider() as Provider + return { signature } + } catch (error) { + throw new Error('EthersAdapter:signMessage - Sign message failed') + } + } - if (!selectedProvider) { - throw new Error(`Provider for connector ${id} is undefined`) - } + public async sendTransaction( + params: AdapterBlueprint.SendTransactionParams + ): Promise { + if (!params.provider) { + throw new Error('Provider is undefined') + } - try { - if (selectedProvider && id !== ConstantsUtil.AUTH_CONNECTOR_ID) { - await selectedProvider.request({ method: 'eth_requestAccounts' }) - } - await this.setProvider( - selectedProvider, - selectedConnector.providerType as ProviderIdType, - info?.name - ) - } catch (error) { - if (id === ConstantsUtil.COINBASE_SDK_CONNECTOR_ID) { - throw new Error((error as CoinbaseProviderError).message) - } - } + const tx = await EthersMethods.sendTransaction( + { + value: params.value as bigint, + to: params.to as `0x${string}`, + data: params.data as `0x${string}`, + gas: params.gas as bigint, + gasPrice: params.gasPrice as bigint, + address: params.address }, + params.provider as Provider, + params.address, + Number(params.caipNetwork?.id) + ) - checkInstalled: (ids?: string[]) => { - if (!ids) { - return Boolean(window.ethereum) - } + return { hash: tx } + } - if (this.ethersConfig?.injected) { - if (!window?.ethereum) { - return false - } - } + public async writeContract( + params: AdapterBlueprint.WriteContractParams + ): Promise { + if (!params.provider) { + throw new Error('Provider is undefined') + } - return ids.some(id => Boolean(window.ethereum?.[String(id)])) + const result = await EthersMethods.writeContract( + { + abi: params.abi, + method: params.method, + fromAddress: params.caipAddress as `0x${string}`, + receiverAddress: params.receiverAddress as `0x${string}`, + tokenAmount: params.tokenAmount, + tokenAddress: params.tokenAddress as `0x${string}` }, + params.provider as Provider, + params.caipAddress, + Number(params.caipNetwork?.id) + ) - disconnect: async () => { - const provider = ProviderUtil.getProvider( - 'eip155' - ) - const providerId = ProviderUtil.state.providerIds['eip155'] - - this.appKit?.setClientId(null) - if (this.options?.siweConfig?.options?.signOutOnDisconnect) { - const { SIWEController } = await import('@reown/appkit-siwe') - await SIWEController.signOut() - } - - const disconnectConfig = { - [ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID]: async () => - await this.appKit?.universalAdapter?.connectionControllerClient?.disconnect(), - - coinbaseWalletSDK: async () => { - if (provider && 'disconnect' in provider) { - await provider.disconnect() - } - }, - - [ConstantsUtil.AUTH_CONNECTOR_ID]: async () => { - await this.authProvider?.disconnect() - }, - - [ConstantsUtil.EIP6963_CONNECTOR_ID]: async () => { - if (provider) { - await this.revokeProviderPermissions(provider as Provider) - } - }, - [ConstantsUtil.INJECTED_CONNECTOR_ID]: async () => { - if (provider) { - ;(provider as Provider).emit('disconnect') - await this.revokeProviderPermissions(provider as Provider) - } - } - } - const disconnectFunction = disconnectConfig[providerId as string] - - if (disconnectFunction) { - await disconnectFunction() - } else { - console.warn(`No disconnect function found for provider type: ${providerId}`) - } + return { hash: result } + } - // Common cleanup actions - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_ID) - this.appKit?.resetAccount(this.chainNamespace) - this.removeListeners(provider as Provider) - }, - signMessage: async (message: string) => { - const provider = ProviderUtil.getProvider(this.chainNamespace) - const caipAddress = ChainController.state.activeCaipAddress - const address = CoreHelperUtil.getPlainAddress(caipAddress) + public async estimateGas( + params: AdapterBlueprint.EstimateGasTransactionArgs + ): Promise { + const { provider, caipNetwork, address } = params + if (!provider) { + throw new Error('Provider is undefined') + } - if (!address) { - throw new Error('Address is undefined') - } + try { + const result = await EthersMethods.estimateGas( + { + data: params.data as `0x${string}`, + to: params.to as `0x${string}`, + address: address as `0x${string}` + }, + provider as Provider, + address as `0x${string}`, + Number(caipNetwork?.id) + ) - if (!provider) { - throw new Error('Provider is undefined') - } + return { gas: result } + } catch (error) { + throw new Error('EthersAdapter:estimateGas - Estimate gas failed') + } + } - return await EthersMethods.signMessage(message, provider, address) - }, + public async getEnsAddress( + params: AdapterBlueprint.GetEnsAddressParams + ): Promise { + const { name, caipNetwork } = params + if (caipNetwork) { + const result = await EthersMethods.getEnsAddress(name, caipNetwork) - parseUnits: EthersMethods.parseUnits, - formatUnits: EthersMethods.formatUnits, + return { address: result as string } + } - estimateGas: async data => { - if (data.chainNamespace && data.chainNamespace !== 'eip155') { - throw new Error(`Invalid chain namespace - Expected eip155, got ${data.chainNamespace}`) - } - const provider = ProviderUtil.getProvider('eip155') - const caipAddress = ChainController.state.activeCaipAddress - const address = CoreHelperUtil.getPlainAddress(caipAddress) - const caipNetwork = this.appKit?.getCaipNetwork() + return { address: '' } + } - if (!address) { - throw new Error('Address is undefined') - } + public parseUnits(params: AdapterBlueprint.ParseUnitsParams): AdapterBlueprint.ParseUnitsResult { + return EthersMethods.parseUnits(params.value, params.decimals) + } - if (!provider) { - throw new Error('Provider is undefined') - } + public formatUnits( + params: AdapterBlueprint.FormatUnitsParams + ): AdapterBlueprint.FormatUnitsResult { + return EthersMethods.formatUnits(params.value, params.decimals) + } - return await EthersMethods.estimateGas(data, provider, address, Number(caipNetwork?.id)) - }, + public async syncConnection( + params: AdapterBlueprint.SyncConnectionParams + ): Promise { + const { id, chainId } = params - getCapabilities: async (params: string) => { - const provider = ProviderUtil.getProvider(CommonConstantsUtil.CHAIN.EVM) + const connector = this.connectors.find(c => c.id === id) + const selectedProvider = connector?.provider as Provider - if (!provider) { - throw new Error('Provider is undefined') - } + if (!selectedProvider) { + throw new Error('Provider not found') + } - const walletCapabilitiesString = provider.session?.sessionProperties?.['capabilities'] - if (walletCapabilitiesString) { - const walletCapabilities = EthersMethods.parseWalletCapabilities(walletCapabilitiesString) - const accountCapabilities = walletCapabilities[params] - if (accountCapabilities) { - return accountCapabilities - } - } + const accounts: string[] = await selectedProvider.request({ + method: 'eth_requestAccounts' + }) - return await provider.request({ method: 'wallet_getCapabilities', params: [params] }) - }, + this.listenProviderEvents(selectedProvider) - grantPermissions: async params => { - const provider = ProviderUtil.getProvider(CommonConstantsUtil.CHAIN.EVM) + if (!accounts[0]) { + throw new Error('No accounts found') + } - if (!provider) { - throw new Error('Provider is undefined') - } + if (!connector?.type) { + throw new Error('Connector type not found') + } - return await provider.request({ method: 'wallet_grantPermissions', params }) - }, + return { + address: accounts[0], + chainId: Number(chainId), + provider: selectedProvider, + type: connector.type, + id + } + } - revokePermissions: async session => { - const provider = ProviderUtil.getProvider(CommonConstantsUtil.CHAIN.EVM) + public syncConnectors(options: AppKitOptions) { + this.ethersConfig = this.createEthersConfig(options) + if (this.ethersConfig?.EIP6963) { + this.listenInjectedConnector(true) + } - if (!provider) { - throw new Error('Provider is undefined') - } + const connectors = Object.keys(this.ethersConfig || {}).filter( + key => key !== 'metadata' && key !== 'EIP6963' + ) - return await provider.request({ method: 'wallet_revokePermissions', params: [session] }) - }, + connectors.forEach(connector => { + const key = connector === 'coinbase' ? 'coinbaseWalletSDK' : connector + if (this.namespace) { + this.addConnector({ + id: connector, + explorerId: PresetsUtil.ConnectorExplorerIds[key], + imageUrl: options?.connectorImages?.[key], + name: PresetsUtil.ConnectorNamesMap[key], + imageId: PresetsUtil.ConnectorImageIds[key], + type: PresetsUtil.ConnectorTypesMap[key] ?? 'EXTERNAL', + info: { rdns: key }, + chain: this.namespace, + chains: [], + provider: this.ethersConfig?.[connector as keyof ProviderType] as Provider + }) + } + }) + } - sendTransaction: async data => { - if (data.chainNamespace && data.chainNamespace !== 'eip155') { - throw new Error(`Invalid chain namespace - Expected eip155, got ${data.chainNamespace}`) - } - const provider = ProviderUtil.getProvider('eip155') - const caipAddress = ChainController.state.activeCaipAddress - const address = CoreHelperUtil.getPlainAddress(caipAddress) - const caipNetwork = this.appKit?.getCaipNetwork() + public async connectWalletConnect(onUri: (uri: string) => void) { + const connector = this.connectors.find(c => c.type === 'WALLET_CONNECT') - if (!address) { - throw new Error('Address is undefined') - } + const provider = connector?.provider as UniversalProvider - if (!provider) { - throw new Error('Provider is undefined') - } + if (!this.caipNetworks || !provider) { + throw new Error( + 'UniversalAdapter:connectWalletConnect - caipNetworks or provider is undefined' + ) + } - return await EthersMethods.sendTransaction(data, provider, address, Number(caipNetwork?.id)) - }, + provider.on('display_uri', (uri: string) => { + onUri(uri) + }) - writeContract: async data => { - const provider = ProviderUtil.getProvider('eip155') - const caipAddress = ChainController.state.activeCaipAddress - const address = CoreHelperUtil.getPlainAddress(caipAddress) - const caipNetwork = this.appKit?.getCaipNetwork() + const namespaces = WcHelpersUtil.createNamespaces(this.caipNetworks) - if (!address) { - throw new Error('Address is undefined') - } + await provider.connect({ optionalNamespaces: namespaces }) + } - if (!provider) { - throw new Error('Provider is undefined') - } + private eip6963EventHandler(event: CustomEventInit) { + if (event.detail) { + const { info, provider } = event.detail + const existingConnector = this.connectors?.find(c => c.name === info?.name) + const coinbaseConnector = this.connectors?.find( + c => c.id === ConstantsUtil.COINBASE_SDK_CONNECTOR_ID + ) + const isCoinbaseDuplicated = + coinbaseConnector && + event.detail.info?.rdns === + ConstantsUtil.CONNECTOR_RDNS_MAP[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID] - return await EthersMethods.writeContract(data, provider, address, Number(caipNetwork?.id)) - }, + if (!existingConnector && !isCoinbaseDuplicated) { + const type = PresetsUtil.ConnectorTypesMap[ConstantsUtil.EIP6963_CONNECTOR_ID] - getEnsAddress: async (value: string) => { - if (this.appKit) { - return await EthersMethods.getEnsAddress(value, this.appKit) + if (type && this.namespace) { + this.addConnector({ + id: info?.rdns || '', + type, + imageUrl: info?.icon, + name: info?.name, + provider, + info, + chain: this.namespace, + chains: [] + }) } - - return false - }, - - getEnsAvatar: async (value: string) => { - const caipNetwork = this.appKit?.getCaipNetwork() - - return await EthersMethods.getEnsAvatar(value, Number(caipNetwork?.id)) } } + } - ChainController.state.chains.set(this.chainNamespace, { - chainNamespace: this.chainNamespace, - connectionControllerClient: this.connectionControllerClient, - networkControllerClient: this.networkControllerClient, - adapterType: this.adapterType, - caipNetworks: this.caipNetworks - }) - - if (this.ethersConfig) { - this.syncConnectors(this.ethersConfig) + private listenInjectedConnector(enableEIP6963: boolean) { + if (typeof window !== 'undefined' && enableEIP6963) { + const handler = this.eip6963EventHandler.bind(this) + window.addEventListener(ConstantsUtil.EIP6963_ANNOUNCE_EVENT, handler) + window.dispatchEvent(new Event(ConstantsUtil.EIP6963_REQUEST_EVENT)) } + } - if (typeof window !== 'undefined') { - this.listenConnectors(true) + public async connect({ + id, + type, + chainId + }: AdapterBlueprint.ConnectParams): Promise { + const connector = this.connectors.find(c => c.id === id) + const selectedProvider = connector?.provider as Provider + if (!selectedProvider) { + throw new Error('Provider not found') } - this.appKit?.setEIP6963Enabled(this.ethersConfig?.EIP6963) + let accounts: string[] = [] - const emailEnabled = - options.features?.email === undefined - ? CoreConstantsUtil.DEFAULT_FEATURES.email - : options.features?.email - const socialsEnabled = options.features?.socials - ? options.features?.socials?.length > 0 - : CoreConstantsUtil.DEFAULT_FEATURES.socials + if (type === 'AUTH') { + const { address } = await (selectedProvider as unknown as W3mFrameProvider).connect({ + chainId + }) - if (emailEnabled || socialsEnabled) { - this.syncAuthConnector(this.options.projectId) - } + accounts = [address] + } else { + accounts = await selectedProvider.request({ + method: 'eth_requestAccounts' + }) - if (this.ethersConfig) { - this.checkActiveProviders(this.ethersConfig) + this.listenProviderEvents(selectedProvider) } - this.syncRequestedNetworks(this.caipNetworks) - } - - public subscribeState(callback: (state: PublicStateControllerState) => void) { - return this.appKit?.subscribeState(state => callback(state)) + return { + address: accounts[0] as `0x${string}`, + chainId: Number(chainId), + provider: selectedProvider, + type: type as ConnectorType, + id + } } - public async disconnect() { - await this.connectionControllerClient?.disconnect() - } + public override async reconnect(params: AdapterBlueprint.ConnectParams): Promise { + const { id, chainId } = params - // -- Private ----------------------------------------------------------------- - private async revokeProviderPermissions(provider: Provider | CombinedProvider) { - try { - const permissions: { parentCapability: string }[] = await provider.request({ - method: 'wallet_getPermissions' - }) - const ethAccountsPermission = permissions.find( - permission => permission.parentCapability === 'eth_accounts' - ) + const connector = this.connectors.find(c => c.id === id) - if (ethAccountsPermission) { - await provider.request({ - method: 'wallet_revokePermissions', - params: [{ eth_accounts: {} }] - }) - } - } catch (error) { - // eslint-disable-next-line no-console - console.info('Could not revoke permissions from wallet. Disconnecting...', error) + if (connector && connector.type === 'AUTH' && chainId) { + await (connector.provider as W3mFrameProvider).connect({ chainId }) } } - private getApprovedCaipNetworksData(): Promise<{ - supportsAllNetworks: boolean - approvedCaipNetworkIds: CaipNetworkId[] - }> { - return new Promise(resolve => { - const walletId = SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_ID) - - if (!walletId) { - throw new Error('No wallet id found to get approved networks data') - } + public async disconnect(params: AdapterBlueprint.DisconnectParams): Promise { + if (!params.provider || !params.providerType) { + throw new Error('Provider or providerType not provided') + } - const providerConfigs = { - [ConstantsUtil.AUTH_CONNECTOR_ID]: { - supportsAllNetworks: true, - approvedCaipNetworkIds: PresetsUtil.WalletConnectRpcChainIds.map( - id => `${ConstantsUtil.EIP155}:${id}` - ) as CaipNetworkId[] + switch (params.providerType) { + case 'WALLET_CONNECT': + if ((params.provider as UniversalProvider).session) { + ;(params.provider as UniversalProvider).disconnect() } - } - - const networkData = providerConfigs[walletId as unknown as keyof typeof providerConfigs] - - if (networkData) { - resolve(networkData) - } else { - resolve({ - supportsAllNetworks: true, - approvedCaipNetworkIds: [] - }) - } - }) + break + case 'AUTH': + await params.provider.disconnect() + break + case 'ANNOUNCED': + case 'EXTERNAL': + await this.revokeProviderPermissions(params.provider as Provider) + break + default: + throw new Error('Unsupported provider type') + } } - /** - * Checks the active providers and sets the provider. We call this when we initialize the adapter. - * @param config - The provider config - */ - private checkActiveProviders(config: ProviderType) { - const walletId = SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_ID) - const walletName = SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_NAME) + public async getBalance( + params: AdapterBlueprint.GetBalanceParams + ): Promise { + const caipNetwork = this.caipNetworks?.find((c: CaipNetwork) => c.id === params.chainId) - if (!walletId) { - return - } + if (caipNetwork) { + const jsonRpcProvider = new JsonRpcProvider(caipNetwork.rpcUrls.default.http[0], { + chainId: caipNetwork.id as number, + name: caipNetwork.name + }) - const providerConfigs = { - [ConstantsUtil.INJECTED_CONNECTOR_ID]: { - provider: config.injected - }, - [ConstantsUtil.COINBASE_SDK_CONNECTOR_ID]: { - provider: config.coinbase as unknown as ExternalProvider - }, - [ConstantsUtil.EIP6963_CONNECTOR_ID]: { - provider: this.EIP6963Providers.find(p => p.info.name === walletName)?.provider - } - } + const balance = await jsonRpcProvider.getBalance(params.address) + const formattedBalance = formatEther(balance) - const activeConfig = providerConfigs[walletId as unknown as keyof typeof providerConfigs] - if (activeConfig?.provider) { - this.setProvider(activeConfig.provider, walletId as ProviderIdType) - this.setupProviderListeners(activeConfig.provider, walletId as ProviderIdType) + return { balance: formattedBalance, symbol: caipNetwork.nativeCurrency.symbol } } - } - /** - * Sets the provider and updates the local storage. We call this when we connect with external providers or via checkActiveProviders function. - * @param provider - The provider to set - * @param providerId - The provider id - * @param name - The name of the provider - */ - private async setProvider(provider: Provider, providerId: ProviderIdType, name?: string) { - if (providerId === 'w3mAuth') { - this.setAuthProvider() - } else { - const walletId = providerId - SafeLocalStorage.setItem(SafeLocalStorageKeys.WALLET_ID, walletId) - - if (name) { - SafeLocalStorage.setItem(SafeLocalStorageKeys.WALLET_NAME, name) - } - - if (provider) { - const { addresses, chainId } = await EthersHelpersUtil.getUserInfo(provider) - const firstAddress = addresses?.[0] - const caipNetwork = this.caipNetworks.find(c => c.id === chainId) ?? this.caipNetworks[0] - const caipAddress = - `${this.chainNamespace}:${caipNetwork?.id}:${firstAddress}` as CaipAddress - - if (firstAddress && caipNetwork) { - this.appKit?.setCaipNetwork(caipNetwork) - this.appKit?.setCaipAddress(caipAddress, this.chainNamespace) - ProviderUtil.setProviderId('eip155', providerId) - ProviderUtil.setProvider('eip155', provider) - this.appKit?.setStatus('connected', this.chainNamespace) - this.appKit?.setAllAccounts( - addresses.map(address => ({ address, type: 'eoa' })), - this.chainNamespace - ) - } - } - } + return { balance: '', symbol: '' } } - private async setAuthProvider() { - SafeLocalStorage.setItem(SafeLocalStorageKeys.WALLET_ID, ConstantsUtil.AUTH_CONNECTOR_ID) - - if (this.authProvider) { - this.appKit?.setLoading(true) - const { - address, - chainId, - smartAccountDeployed, - preferredAccountType, - accounts = [] - } = await this.authProvider.connect({ - chainId: Number(this.appKit?.getCaipNetwork()?.id || this.caipNetworks[0]?.id) - }) + public async getProfile( + params: AdapterBlueprint.GetProfileParams + ): Promise { + if (params.chainId === 1) { + const ensProvider = new InfuraProvider('mainnet') + const name = await ensProvider.lookupAddress(params.address) + const avatar = await ensProvider.getAvatar(params.address) - const { smartAccountEnabledNetworks } = - await this.authProvider.getSmartAccountEnabledNetworks() - - this.appKit?.setSmartAccountEnabledNetworks(smartAccountEnabledNetworks, this.chainNamespace) - if (address && chainId) { - this.appKit?.setAllAccounts( - accounts.length > 0 - ? accounts - : [{ address, type: preferredAccountType as 'eoa' | 'smartAccount' }], - this.chainNamespace - ) - this.appKit?.setStatus('connected', this.chainNamespace) - this.appKit?.setCaipAddress( - `${this.chainNamespace}:${chainId}:${address}`, - this.chainNamespace - ) - this.appKit?.setPreferredAccountType( - preferredAccountType as W3mFrameTypes.AccountType, - this.chainNamespace - ) - this.appKit?.setSmartAccountDeployed(Boolean(smartAccountDeployed), this.chainNamespace) - ProviderUtil.setProvider('eip155', this.authProvider as unknown as Provider) - ProviderUtil.setProviderId('eip155', ConstantsUtil.AUTH_CONNECTOR_ID as ProviderIdType) - this.setupProviderListeners(this.authProvider as unknown as Provider, 'w3mAuth') - this.watchModal() - } - this.appKit?.setLoading(false) + return { profileName: name || undefined, profileImage: avatar || undefined } } - } - private watchModal() { - if (this.authProvider) { - this.subscribeState(val => { - if (!val.open) { - this.authProvider?.rejectRpcRequests() - } - }) - } + return { profileName: undefined, profileImage: undefined } } - private setupProviderListeners(provider: Provider, providerId: ProviderIdType) { + private providerHandlers: { + disconnect: () => void + accountsChanged: (accounts: string[]) => void + chainChanged: (chainId: string) => void + } | null = null + + private listenProviderEvents(provider: Provider | CombinedProvider) { const disconnectHandler = () => { - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_ID) - this.removeListeners(provider) + this.removeProviderListeners(provider) + this.emit('disconnect') } const accountsChangedHandler = (accounts: string[]) => { - const currentAccount = accounts?.[0] as Address | undefined - - if (currentAccount) { - const chainId = this.appKit?.getCaipNetwork()?.id - const caipAddress = `${this.chainNamespace}:${chainId}:${currentAccount}` as CaipAddress - - this.appKit?.setCaipAddress(caipAddress, this.chainNamespace) - - if (providerId === ConstantsUtil.EIP6963_CONNECTOR_ID) { - this.appKit?.setAllAccounts( - accounts.map(address => ({ address, type: 'eoa' })), - this.chainNamespace - ) - } - } else { - if (providerId === ConstantsUtil.EIP6963_CONNECTOR_ID) { - this.appKit?.setAllAccounts([], this.chainNamespace) - } - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_ID) - this.appKit?.resetAccount(this.chainNamespace) + if (accounts.length > 0) { + this.emit('accountChanged', { + address: accounts[0] as `0x${string}` + }) } } const chainChangedHandler = (chainId: string) => { const chainIdNumber = typeof chainId === 'string' ? EthersHelpersUtil.hexStringToNumber(chainId) : Number(chainId) - const caipNetwork = this.caipNetworks.find(c => c.id === chainIdNumber) - const currentCaipNetwork = this.appKit?.getCaipNetwork() - if (!currentCaipNetwork || currentCaipNetwork?.id !== caipNetwork?.id) { - this.appKit?.setCaipNetwork(caipNetwork) - } + this.emit('switchNetwork', { chainId: chainIdNumber }) } - if (providerId === ConstantsUtil.AUTH_CONNECTOR_ID) { - this.setupAuthListeners(provider as unknown as W3mFrameProvider) - } else { - provider.on('disconnect', disconnectHandler) - provider.on('accountsChanged', accountsChangedHandler) - provider.on('chainChanged', chainChangedHandler) - } + provider.on('disconnect', disconnectHandler) + provider.on('accountsChanged', accountsChangedHandler) + provider.on('chainChanged', chainChangedHandler) this.providerHandlers = { disconnect: disconnectHandler, @@ -786,13 +473,7 @@ export class EthersAdapter { } } - private providerHandlers: { - disconnect: () => void - accountsChanged: (accounts: string[]) => void - chainChanged: (networkId: string) => void - } | null = null - - private removeListeners(provider: Provider) { + private removeProviderListeners(provider: Provider | CombinedProvider) { if (this.providerHandlers) { provider.removeListener('disconnect', this.providerHandlers.disconnect) provider.removeListener('accountsChanged', this.providerHandlers.accountsChanged) @@ -801,272 +482,19 @@ export class EthersAdapter { } } - private setupAuthListeners(authProvider: W3mFrameProvider) { - authProvider.onRpcRequest(request => { - if (W3mFrameHelpers.checkIfRequestExists(request)) { - if (!W3mFrameHelpers.checkIfRequestIsSafe(request)) { - this.appKit?.handleUnsafeRPCRequest() - } - } else { - this.handleInvalidAuthRequest() - } - }) - - authProvider.onRpcError(() => this.handleAuthRpcError()) - authProvider.onRpcSuccess((_, request) => this.handleAuthRpcSuccess(_, request)) - authProvider.onNotConnected(() => this.handleAuthNotConnected()) - authProvider.onConnect(({ preferredAccountType }) => - this.handleAuthIsConnected(preferredAccountType) - ) - authProvider.onSetPreferredAccount(({ address, type }) => { - if (address) { - this.handleAuthSetPreferredAccount(address, type) - } - }) - } - - private handleInvalidAuthRequest() { - this.appKit?.open() - setTimeout(() => { - this.appKit?.showErrorMessage(W3mFrameRpcConstants.RPC_METHOD_NOT_ALLOWED_UI_MESSAGE) - }, 300) - } - - private handleAuthRpcError() { - if (this.appKit?.isOpen()) { - if (this.appKit?.isTransactionStackEmpty()) { - this.appKit?.close() - } else { - this.appKit?.popTransactionStack(true) - } - } - } - - private handleAuthRpcSuccess(_: W3mFrameTypes.FrameEvent, request: W3mFrameTypes.RPCRequest) { - const isSafeRequest = W3mFrameHelpers.checkIfRequestIsSafe(request) - if (isSafeRequest) { - return - } - - if (this.appKit?.isTransactionStackEmpty()) { - this.appKit?.close() - } else { - this.appKit?.popTransactionStack() - } - } - - private handleAuthNotConnected() { - this.appKit?.setCaipAddress(undefined, this.chainNamespace) - } - - private handleAuthIsConnected(preferredAccountType: string | undefined) { - const activeNamespace = this.appKit?.getActiveChainNamespace() - - if (activeNamespace !== this.chainNamespace) { - return - } - - this.appKit?.setPreferredAccountType( - preferredAccountType as W3mFrameTypes.AccountType, - this.chainNamespace - ) - } - - private handleAuthSetPreferredAccount(address: string, type: string) { - if (!address) { - return - } - - this.appKit?.setLoading(true) - const chainId = this.appKit?.getCaipNetwork()?.id - this.appKit?.setCaipAddress(`${this.chainNamespace}:${chainId}:${address}`, this.chainNamespace) - this.appKit?.setStatus('connected', this.chainNamespace) - this.appKit?.setPreferredAccountType(type as W3mFrameTypes.AccountType, this.chainNamespace) - - this.syncAccount({ - address: address as Address - }).then(() => this.appKit?.setLoading(false)) - this.appKit?.setLoading(false) - } - - private async syncReownName(address: Address) { - try { - const registeredWcNames = await this.appKit?.getReownName(address) - if (registeredWcNames?.[0]) { - const wcName = registeredWcNames[0] - this.appKit?.setProfileName(wcName.name, this.chainNamespace) - } else { - this.appKit?.setProfileName(null, this.chainNamespace) - } - } catch { - this.appKit?.setProfileName(null, this.chainNamespace) - } - } - - /** - * Syncs the account state depending on the given parameters. We call this in different conditions like when caipNetwork or caipAddress changes, when the user switches account or network. - * @param param0 - The address and caipNetwork. Both are optional. - */ - private async syncAccount({ - address, - caipNetwork - }: { - address?: Address - caipNetwork?: CaipNetwork - }) { - const currentCaipNetwork = caipNetwork || this.appKit?.getCaipNetwork() - const preferredAccountType = this.appKit?.getPreferredAccountType() - const isEipNetwork = currentCaipNetwork?.chainNamespace === CommonConstantsUtil.CHAIN.EVM - const caipNetworkId = currentCaipNetwork?.id as CaipNetworkId - - if (address) { - if (isEipNetwork) { - this.appKit?.setPreferredAccountType(preferredAccountType, this.chainNamespace) - this.appKit?.setCaipAddress( - `${this.chainNamespace}:${caipNetworkId}:${address}`, - this.chainNamespace - ) - - this.syncConnectedWalletInfo() - - if (currentCaipNetwork?.blockExplorers?.default.url) { - this.appKit?.setAddressExplorerUrl( - `${currentCaipNetwork.blockExplorers.default.url}/address/${address}`, - this.chainNamespace - ) - } - - await Promise.all([ - this.syncProfile(address), - this.appKit?.setApprovedCaipNetworksData(this.chainNamespace) - ]) - } - } else { - this.appKit?.resetWcConnection() - this.appKit?.resetNetwork(this.chainNamespace) - this.appKit?.setAllAccounts([], this.chainNamespace) - } - } - - private async syncProfile(address: Address) { - const caipNetwork = this.appKit?.getCaipNetwork() - - try { - const identity = await this.appKit?.fetchIdentity({ - address - }) - const name = identity?.name - const avatar = identity?.avatar - - this.appKit?.setProfileName(name, this.chainNamespace) - this.appKit?.setProfileImage(avatar, this.chainNamespace) - - if (!name) { - await this.syncReownName(address) - } - } catch { - if (caipNetwork?.id === 1) { - const ensProvider = new InfuraProvider('mainnet') - const name = await ensProvider.lookupAddress(address) - const avatar = await ensProvider.getAvatar(address) - - if (name) { - this.appKit?.setProfileName(name, this.chainNamespace) - } else { - await this.syncReownName(address) - } - if (avatar) { - this.appKit?.setProfileImage(avatar, this.chainNamespace) - } - } else { - await this.syncReownName(address) - this.appKit?.setProfileImage(null, this.chainNamespace) - } - } - } - - private async syncBalance(address: Address, caipNetwork: CaipNetwork) { - const isExistingNetwork = this.appKit - ?.getCaipNetworks(caipNetwork.chainNamespace) - .find(network => network.id === caipNetwork.id) - const isEVMNetwork = caipNetwork.chainNamespace === CommonConstantsUtil.CHAIN.EVM - - if (caipNetwork && isExistingNetwork && isEVMNetwork) { - const jsonRpcProvider = new JsonRpcProvider(caipNetwork.rpcUrls.default.http[0], { - chainId: caipNetwork.id as number, - name: caipNetwork.name + public async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise { + const { caipNetwork, provider, providerType } = params + if (providerType === 'WALLET_CONNECT') { + ;(provider as UniversalProvider).setDefaultChain(String(`eip155:${String(caipNetwork.id)}`)) + } else if (providerType === 'AUTH') { + const authProvider = provider as W3mFrameProvider + await authProvider.switchNetwork(caipNetwork.id) + await authProvider.connect({ + chainId: caipNetwork.id }) - - if (jsonRpcProvider) { - const balance = await jsonRpcProvider.getBalance(address) - const formattedBalance = formatEther(balance) - - this.appKit?.setBalance( - formattedBalance, - caipNetwork.nativeCurrency.symbol, - this.chainNamespace - ) - } - } - } - - private syncConnectedWalletInfo() { - const currentActiveWallet = SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_ID) - const providerType = ProviderUtil.state.providerIds['eip155'] - - if (providerType === ConstantsUtil.EIP6963_CONNECTOR_ID) { - if (currentActiveWallet) { - const currentProvider = this.EIP6963Providers.find( - provider => provider.info.name === currentActiveWallet - ) - - if (currentProvider) { - this.appKit?.setConnectedWalletInfo({ ...currentProvider.info }, this.chainNamespace) - } - } - } else if (providerType === ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID) { - const provider = ProviderUtil.getProvider('eip155') - - if (provider?.session) { - this.appKit?.setConnectedWalletInfo( - { - ...provider.session.peer.metadata, - name: provider.session.peer.metadata.name, - icon: provider.session.peer.metadata.icons?.[0] - }, - this.chainNamespace - ) - } - } else if (providerType === ConstantsUtil.COINBASE_SDK_CONNECTOR_ID) { - const connector = this.appKit - ?.getConnectors() - .find(c => c.id === ConstantsUtil.COINBASE_SDK_CONNECTOR_ID) - - this.appKit?.setConnectedWalletInfo( - { name: 'Coinbase Wallet', icon: this.appKit?.getConnectorImage(connector) }, - this.chainNamespace - ) - } else if (currentActiveWallet) { - this.appKit?.setConnectedWalletInfo({ name: currentActiveWallet }, this.chainNamespace) - } - } - - private syncRequestedNetworks(caipNetworks: CaipNetwork[]) { - const uniqueChainNamespaces = [ - ...new Set(caipNetworks.map(caipNetwork => caipNetwork.chainNamespace)) - ] - uniqueChainNamespaces.forEach(chainNamespace => { - this.appKit?.setRequestedCaipNetworks( - caipNetworks.filter(caipNetwork => caipNetwork.chainNamespace === chainNamespace), - chainNamespace - ) - }) - } - - public async switchNetwork(caipNetwork: CaipNetwork) { - async function requestSwitchNetwork(provider: Provider) { + } else { try { - await provider.request({ + await (provider as Provider).request({ method: 'wallet_switchEthereumChain', params: [{ chainId: EthersHelpersUtil.numberToHexString(caipNetwork.id) }] }) @@ -1078,167 +506,40 @@ export class EthersAdapter { switchError?.data?.originalError?.code === WcConstantsUtil.ERROR_CODE_UNRECOGNIZED_CHAIN_ID ) { - await EthersHelpersUtil.addEthereumChain(provider, caipNetwork) - } else { + await EthersHelpersUtil.addEthereumChain(provider as Provider, caipNetwork) + } else if ( + providerType === 'ANNOUNCED' || + providerType === 'EXTERNAL' || + providerType === 'INJECTED' + ) { throw new Error('Chain is not supported') } } } - - const provider = ProviderUtil.getProvider('eip155') - const providerType = ProviderUtil.state.providerIds['eip155'] - - if (provider) { - switch (providerType) { - case ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID: - this.appKit?.universalAdapter?.networkControllerClient.switchCaipNetwork(caipNetwork) - break - case ConstantsUtil.INJECTED_CONNECTOR_ID: - case ConstantsUtil.EIP6963_CONNECTOR_ID: - case ConstantsUtil.COINBASE_SDK_CONNECTOR_ID: - if (provider) { - await requestSwitchNetwork(provider as Provider) - } - break - case ConstantsUtil.AUTH_CONNECTOR_ID: - if (this.authProvider) { - try { - this.appKit?.setLoading(true) - const { chainId } = await this.authProvider.switchNetwork(caipNetwork.id as number) - const { address, preferredAccountType } = await this.authProvider.connect({ - chainId: caipNetwork.id as number | undefined - }) - const caipAddress = `${this.chainNamespace}:${chainId}:${address}` - - this.appKit?.setCaipNetwork(caipNetwork) - this.appKit?.setCaipAddress(caipAddress as CaipAddress, this.chainNamespace) - this.appKit?.setPreferredAccountType( - preferredAccountType as W3mFrameTypes.AccountType, - this.chainNamespace - ) - await this.syncAccount({ address: address as Address }) - this.appKit?.setLoading(false) - } catch { - throw new Error('Switching chain failed') - } finally { - this.appKit?.setLoading(false) - } - } - break - default: - throw new Error('Unsupported provider type') - } - } } - private syncConnectors(config: ProviderType) { - const w3mConnectors: Connector[] = [] - - if (config.injected) { - const injectedConnectorType = - PresetsUtil.ConnectorTypesMap[ConstantsUtil.INJECTED_CONNECTOR_ID] - if (injectedConnectorType) { - w3mConnectors.push({ - id: ConstantsUtil.INJECTED_CONNECTOR_ID, - explorerId: PresetsUtil.ConnectorExplorerIds[ConstantsUtil.INJECTED_CONNECTOR_ID], - imageId: PresetsUtil.ConnectorImageIds[ConstantsUtil.INJECTED_CONNECTOR_ID], - imageUrl: this.options?.connectorImages?.[ConstantsUtil.INJECTED_CONNECTOR_ID], - name: PresetsUtil.ConnectorNamesMap[ConstantsUtil.INJECTED_CONNECTOR_ID], - type: injectedConnectorType, - chain: this.chainNamespace - }) - } - } - - if (config.coinbase) { - w3mConnectors.push({ - id: ConstantsUtil.COINBASE_SDK_CONNECTOR_ID, - explorerId: PresetsUtil.ConnectorExplorerIds[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID], - imageId: PresetsUtil.ConnectorImageIds[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID], - imageUrl: this.options?.connectorImages?.[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID], - name: PresetsUtil.ConnectorNamesMap[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID], - type: 'EXTERNAL', - chain: this.chainNamespace - }) - } - - this.appKit?.setConnectors(w3mConnectors) + public getWalletConnectProvider(): AdapterBlueprint.GetWalletConnectProviderResult { + return this.connectors.find(c => c.type === 'WALLET_CONNECT')?.provider as UniversalProvider } - private async syncAuthConnector(projectId: string, bypassWindowCheck = false) { - if (bypassWindowCheck || typeof window !== 'undefined') { - this.authProvider = W3mFrameProviderSingleton.getInstance({ - projectId, - onTimeout: () => { - AlertController.open(ErrorUtil.ALERT_ERRORS.SOCIALS_TIMEOUT, 'error') - } - }) - - this.appKit?.addConnector({ - id: ConstantsUtil.AUTH_CONNECTOR_ID, - type: 'AUTH', - name: 'Auth', - provider: this.authProvider, - chain: this.chainNamespace + private async revokeProviderPermissions(provider: Provider | CombinedProvider) { + try { + const permissions: { parentCapability: string }[] = await provider.request({ + method: 'wallet_getPermissions' }) - - this.appKit?.setLoading(true) - const isLoginEmailUsed = this.authProvider.getLoginEmailUsed() - this.appKit?.setLoading(isLoginEmailUsed) - if (isLoginEmailUsed) { - const { isConnected } = await this.authProvider.isConnected() - if (isConnected) { - await this.setAuthProvider() - } else { - this.appKit?.setLoading(false) - } - } - } - } - - private eip6963EventHandler(event: CustomEventInit) { - if (event.detail) { - const { info, provider } = event.detail - const connectors = this.appKit?.getConnectors() - const existingConnector = connectors?.find(c => c.name === info.name) - const coinbaseConnector = connectors?.find( - c => c.id === ConstantsUtil.COINBASE_SDK_CONNECTOR_ID + const ethAccountsPermission = permissions.find( + permission => permission.parentCapability === 'eth_accounts' ) - const isCoinbaseDuplicated = - coinbaseConnector && - event.detail.info.rdns === - ConstantsUtil.CONNECTOR_RDNS_MAP[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID] - - if (!existingConnector && !isCoinbaseDuplicated) { - const type = PresetsUtil.ConnectorTypesMap[ConstantsUtil.EIP6963_CONNECTOR_ID] - if (type) { - this.appKit?.addConnector({ - id: ConstantsUtil.EIP6963_CONNECTOR_ID, - type, - imageUrl: - info.icon ?? this.options?.connectorImages?.[ConstantsUtil.EIP6963_CONNECTOR_ID], - name: info.name, - provider, - info, - chain: this.chainNamespace - }) - - const eip6963ProviderObj = { - provider, - info - } - this.EIP6963Providers.push(eip6963ProviderObj) - } + if (ethAccountsPermission) { + await provider.request({ + method: 'wallet_revokePermissions', + params: [{ eth_accounts: {} }] + }) } - } - } - - private listenConnectors(enableEIP6963: boolean) { - if (typeof window !== 'undefined' && enableEIP6963) { - const handler = this.eip6963EventHandler.bind(this) - window.addEventListener(ConstantsUtil.EIP6963_ANNOUNCE_EVENT, handler) - window.dispatchEvent(new Event(ConstantsUtil.EIP6963_REQUEST_EVENT)) + } catch (error) { + // eslint-disable-next-line no-console + console.info('Could not revoke permissions from wallet. Disconnecting...', error) } } } diff --git a/packages/adapters/ethers/src/index.ts b/packages/adapters/ethers/src/index.ts index f4eacf667a..849b42b98d 100644 --- a/packages/adapters/ethers/src/index.ts +++ b/packages/adapters/ethers/src/index.ts @@ -4,5 +4,4 @@ export { EthersAdapter } from './client.js' export * from '@reown/appkit-utils/ethers' // -- Types -export type { AdapterOptions } from './client.js' export type { ProviderType } from '@reown/appkit-utils/ethers' diff --git a/packages/adapters/ethers/src/tests/client.test.ts b/packages/adapters/ethers/src/tests/client.test.ts index bdbea1acc6..d2579c96b7 100644 --- a/packages/adapters/ethers/src/tests/client.test.ts +++ b/packages/adapters/ethers/src/tests/client.test.ts @@ -1,961 +1,323 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { describe, it, expect, beforeEach, vi } from 'vitest' import { EthersAdapter } from '../client' -import type { EIP6963ProviderDetail } from '../client' -import { mockOptions } from './mocks/Options' -import { mockCreateEthersConfig } from './mocks/EthersConfig' -import mockAppKit from './mocks/AppKit' -import { mockAuthConnector } from './mocks/AuthConnector' -import { EthersHelpersUtil, type ProviderType } from '@reown/appkit-utils/ethers' -import { CaipNetworksUtil, ConstantsUtil } from '@reown/appkit-utils' -import { - arbitrum as AppkitArbitrum, - mainnet as AppkitMainnet, - polygon as AppkitPolygon, - optimism as AppkitOptimism, - bsc as AppkitBsc, - harmonyOne as AppkitHarmonyOne -} from '@reown/appkit/networks' -import { ProviderUtil, type ProviderIdType } from '@reown/appkit/store' -import { SafeLocalStorage, SafeLocalStorageKeys } from '@reown/appkit-common' -import { type BlockchainApiLookupEnsName } from '@reown/appkit' -import { InfuraProvider, JsonRpcProvider } from 'ethers' - -import type { CaipNetwork, ChainNamespace } from '@reown/appkit-common' - -const [mainnet, arbitrum, polygon, optimism, bsc] = CaipNetworksUtil.extendCaipNetworks( - [AppkitMainnet, AppkitArbitrum, AppkitPolygon, AppkitOptimism, AppkitBsc], - { - customNetworkImageUrls: mockOptions.chainImages, - projectId: '1234' - } -) as [CaipNetwork, CaipNetwork, CaipNetwork, CaipNetwork, CaipNetwork] - -const caipNetworks = [mainnet, arbitrum, polygon] as [CaipNetwork, ...CaipNetwork[]] - -vi.mock('@reown/appkit-wallet', () => ({ - W3mFrameProvider: vi.fn().mockImplementation(() => mockAuthConnector), - W3mFrameHelpers: { - checkIfRequestExists: vi.fn(), - checkIfRequestIsSafe: vi.fn() - }, - W3mFrameRpcConstants: { - RPC_METHOD_NOT_ALLOWED_UI_MESSAGE: 'RPC method not allowed' - } -})) - -vi.mock('@reown/appkit-utils', async importOriginal => { - const actual = await importOriginal() - const INJECTED_CONNECTOR_ID = 'injected' - const COINBASE_SDK_CONNECTOR_ID = 'coinbaseWallet' - const EIP6963_CONNECTOR_ID = 'eip6963' - const WALLET_CONNECT_CONNECTOR_ID = 'walletConnect' - const AUTH_CONNECTOR_ID = 'w3mAuth' +import { CaipNetworksUtil } from '@reown/appkit-utils' +import type { Provider } from '@reown/appkit-core' +import type { W3mFrameProvider } from '@reown/appkit-wallet' +import UniversalProvider from '@walletconnect/universal-provider' +import { JsonRpcProvider, InfuraProvider } from 'ethers' +import { mainnet } from '@reown/appkit/networks' +import { EthersMethods } from '../utils/EthersMethods' + +// Mock external dependencies +vi.mock('ethers', async importOriginal => { + const actual = await importOriginal() return { - // @ts-expect-error - actual is not typed ...actual, - PresetsUtil: { - ConnectorTypesMap: { - [INJECTED_CONNECTOR_ID]: 'INJECTED', - [COINBASE_SDK_CONNECTOR_ID]: 'EXTERNAL', - [EIP6963_CONNECTOR_ID]: 'INJECTED' - }, - ConnectorExplorerIds: { - [INJECTED_CONNECTOR_ID]: 'injected-explorer', - [COINBASE_SDK_CONNECTOR_ID]: 'coinbase-explorer', - [EIP6963_CONNECTOR_ID]: 'eip6963-explorer' - }, - ConnectorImageIds: { - [INJECTED_CONNECTOR_ID]: 'injected-image', - [COINBASE_SDK_CONNECTOR_ID]: 'coinbase-image', - [EIP6963_CONNECTOR_ID]: 'eip6963-image' - }, - ConnectorNamesMap: { - [INJECTED_CONNECTOR_ID]: 'Injected', - [COINBASE_SDK_CONNECTOR_ID]: 'Coinbase', - [EIP6963_CONNECTOR_ID]: 'EIP6963' - }, - WalletConnectRpcChainIds: [1, 137, 10, 42161, 56, 43114, 250, 25, 1313161554, 1284] - }, - ConstantsUtil: { - INJECTED_CONNECTOR_ID, - COINBASE_SDK_CONNECTOR_ID, - EIP6963_CONNECTOR_ID, - WALLET_CONNECT_CONNECTOR_ID, - AUTH_CONNECTOR_ID, - EIP155: 'eip155' - }, - HelpersUtil: { - getCaipTokens: vi.fn().mockReturnValue([]) - } + formatEther: vi.fn(() => '1.5'), + InfuraProvider: vi.fn(() => ({ + lookupAddress: vi.fn(), + getAvatar: vi.fn() + })), + JsonRpcProvider: vi.fn(() => ({ + getBalance: vi.fn() + })) } }) -vi.mock('@reown/appkit/store', () => ({ - ProviderUtil: { - setProvider: vi.fn(), - setProviderId: vi.fn(), - state: { - providerIds: {} - }, - getProvider: vi.fn() +vi.mock('../utils/EthersMethods', () => ({ + EthersMethods: { + signMessage: vi.fn(), + sendTransaction: vi.fn(), + writeContract: vi.fn(), + estimateGas: vi.fn(), + getEnsAddress: vi.fn(), + parseUnits: vi.fn(), + formatUnits: vi.fn(), + hexStringToNumber: vi.fn(hex => parseInt(hex, 16)), + numberToHexString: vi.fn(num => `0x${num.toString(16)}`) } })) -vi.mock('ethers', async () => { - return { - InfuraProvider: vi.fn(), - JsonRpcProvider: vi.fn(), - formatEther: vi.fn().mockReturnValue('1.0'), - parseUnits: vi.fn(), - formatUnits: vi.fn() - } -}) - -vi.mock('@reown/appkit-common', async importOriginal => { - const actual = await importOriginal() - return { - // @ts-expect-error - actual is not typed - ...actual, - SafeLocalStorage: { - getItem: vi.fn(key => { - const values = { - '@appkit/wallet_id': 'injected' - } - return values[key as keyof typeof values] - }), - setItem: vi.fn(), - removeItem: vi.fn() - } - } +const mockProvider = { + request: vi.fn(), + on: vi.fn(), + removeListener: vi.fn() +} as unknown as Provider + +const mockWalletConnectProvider = { + connect: vi.fn(), + disconnect: vi.fn(), + on: vi.fn(), + removeListener: vi.fn(), + session: true, + setDefaultChain: vi.fn() +} as unknown as UniversalProvider + +const mockAuthProvider = { + connect: vi.fn(), + disconnect: vi.fn(), + switchNetwork: vi.fn() +} as unknown as W3mFrameProvider + +const mockNetworks = [mainnet] +const mockCaipNetworks = CaipNetworksUtil.extendCaipNetworks(mockNetworks, { + projectId: 'test-project-id', + customNetworkImageUrls: {} }) describe('EthersAdapter', () => { - let client: EthersAdapter + let adapter: EthersAdapter beforeEach(() => { vi.clearAllMocks() - const ethersConfig = mockCreateEthersConfig() - client = new EthersAdapter() - vi.spyOn(client as any, 'createEthersConfig').mockImplementation(() => ({ - metadata: ethersConfig.metadata, - injected: ethersConfig.injected - })) - const optionsWithEthersConfig = { - ...mockOptions, - networks: caipNetworks, - defaultNetwork: undefined, - ethersConfig - } - client.construct(mockAppKit, optionsWithEthersConfig) + adapter = new EthersAdapter() }) - afterEach(() => { - vi.clearAllMocks() - }) - - describe('EthersClient - Initialization', () => { - it('should initialize with default values', () => { - expect(client.chainNamespace).toBe('eip155') - expect(client.adapterType).toBe('ethers') + describe('EthersAdapter -constructor', () => { + it('should initialize with correct parameters', () => { + expect(adapter.adapterType).toBe('ethers') + expect(adapter.namespace).toBe('eip155') }) + }) - it('should set caipNetworks to provided caipNetworks options', () => { - expect(client.caipNetworks).toEqual(caipNetworks) - }) + describe('EthersAdapter - signMessage', () => { + it('should sign message successfully', async () => { + const mockSignature = '0xmocksignature' + vi.mocked(EthersMethods.signMessage).mockResolvedValue(mockSignature) - it('should set chain images', () => { - Object.entries(mockOptions.chainImages!).map(([networkId, imageUrl]) => { - const caipNetwork = client.caipNetworks.find( - caipNetwork => caipNetwork.id === Number(networkId) - ) - expect(caipNetwork).toBeDefined() - expect(caipNetwork?.assets?.imageUrl).toEqual(imageUrl) + const result = await adapter.signMessage({ + message: 'Hello', + address: '0x123', + provider: mockProvider }) - }) - it('should set defaultNetwork to first caipNetwork option', () => { - expect(client.defaultCaipNetwork).toEqual(mainnet) + expect(result.signature).toBe(mockSignature) }) - it('should create ethers config', () => { - expect(client['ethersConfig']).toBeDefined() + it('should throw error when provider is undefined', async () => { + await expect( + adapter.signMessage({ + message: 'Hello', + address: '0x123' + }) + ).rejects.toThrow('Provider is undefined') }) }) - describe('EthersClient - Networks', () => { - const mockProvider = { - request: vi.fn(), - on: vi.fn(), - removeListener: vi.fn() - } - - beforeEach(() => { - vi.spyOn(ProviderUtil, 'getProvider').mockReturnValue(mockProvider) - vi.spyOn(ProviderUtil.state, 'providerIds', 'get').mockReturnValue({ - eip155: 'injected', - solana: undefined, - polkadot: undefined + describe('EthersAdapter -sendTransaction', () => { + it('should send transaction successfully', async () => { + const mockTxHash = '0xtxhash' + vi.mocked(EthersMethods.sendTransaction).mockResolvedValue(mockTxHash) + + const result = await adapter.sendTransaction({ + value: BigInt(1000), + to: '0x456', + data: '0x', + gas: BigInt(21000), + gasPrice: BigInt(2000000000), + address: '0x123', + provider: mockProvider, + caipNetwork: mockCaipNetworks[0] }) - }) - it('should switch network for injected provider', async () => { - mockProvider.request.mockResolvedValueOnce(null) - - await client.switchNetwork(polygon) - - expect(mockProvider.request).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'wallet_switchEthereumChain', - params: expect.arrayContaining([ - expect.objectContaining({ - chainId: expect.stringMatching(/^0x/) - }) - ]) - }) - ) + expect(result.hash).toBe(mockTxHash) }) - it('should add network if not recognized by wallet', async () => { - const switchError = { code: 4902 } - mockProvider.request.mockRejectedValueOnce(switchError) - mockProvider.request.mockResolvedValueOnce(null) - - await client.switchNetwork(arbitrum) - - expect(mockProvider.request).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'wallet_addEthereumChain', - params: expect.arrayContaining([ - expect.objectContaining({ - chainId: expect.stringMatching(/^0x/) - }) - ]) + it('should throw error when provider is undefined', async () => { + await expect( + adapter.sendTransaction({ + value: BigInt(1000), + to: '0x456', + data: '0x', + gas: BigInt(21000), + gasPrice: BigInt(2000000000), + address: '0x123' }) - ) - }) - - it('should throw error if switching fails', async () => { - const switchError = new Error('User rejected the request') - mockProvider.request.mockRejectedValueOnce(switchError) - - await expect(client.switchNetwork(bsc)).rejects.toThrow('Chain is not supported') + ).rejects.toThrow('Provider is undefined') }) + }) - it('should use universal adapter for WalletConnect', async () => { - vi.spyOn(ProviderUtil.state, 'providerIds', 'get').mockReturnValue({ - eip155: 'walletConnect', - solana: undefined, - polkadot: undefined + describe('EthersAdapter -writeContract', () => { + it('should write contract successfully', async () => { + const mockTxHash = '0xtxhash' + vi.mocked(EthersMethods.writeContract).mockResolvedValue(mockTxHash) + + const result = await adapter.writeContract({ + abi: [], + method: 'transfer', + caipAddress: 'eip155:1:0x123', + fromAddress: '0x123', + receiverAddress: '0x456', + tokenAmount: BigInt(1000), + tokenAddress: '0x789', + provider: mockProvider, + caipNetwork: mockCaipNetworks[0] }) - await client.switchNetwork(optimism) - - expect( - mockAppKit.universalAdapter?.networkControllerClient.switchCaipNetwork - ).toHaveBeenCalledWith(optimism) - }) - - it('should set requested CAIP networks for each unique chain namespace', () => { - const caipNetworks = [mainnet, arbitrum, polygon] - - client['syncRequestedNetworks'](caipNetworks) - - expect(mockAppKit.setRequestedCaipNetworks).toHaveBeenCalledWith( - [mainnet, arbitrum, polygon], - 'eip155' - ) + expect(result.hash).toBe(mockTxHash) }) }) - describe('EthersClient - Auth Connector', () => { - it('should sync auth connector', async () => { - const projectId = 'test-project-id' - - await client['syncAuthConnector'](projectId, true) - - expect(mockAppKit.addConnector).toHaveBeenCalledWith({ - id: ConstantsUtil.AUTH_CONNECTOR_ID, - type: 'AUTH', - name: 'Auth', - provider: expect.any(Object), - chain: 'eip155' - }) - expect(mockAppKit.setLoading).toHaveBeenCalledWith(false) - }) - - describe('Auth Connector Handle Requests', () => { - beforeEach(() => { - client['appKit'] = mockAppKit - }) - - it('should handle RPC request correctly when modal is closed', () => { - vi.spyOn(mockAppKit, 'isOpen').mockReturnValue(false) - mockAppKit['handleUnsafeRPCRequest']() - expect(mockAppKit.open).toHaveBeenCalledWith({ view: 'ApproveTransaction' }) - }) - - it('should handle RPC request correctly when modal is open and transaction stack is not empty', () => { - vi.spyOn(mockAppKit, 'isOpen').mockReturnValue(true) - vi.spyOn(mockAppKit, 'isTransactionStackEmpty').mockReturnValue(false) - vi.spyOn(mockAppKit, 'isTransactionShouldReplaceView').mockReturnValue(true) - mockAppKit['handleUnsafeRPCRequest']() - expect(mockAppKit.redirect).toHaveBeenCalledWith('ApproveTransaction') - }) - - it('should handle invalid auth request', () => { - vi.useFakeTimers() - client['handleInvalidAuthRequest']() - expect(mockAppKit.open).toHaveBeenCalled() - vi.advanceTimersByTime(300) - expect(mockAppKit.showErrorMessage).toHaveBeenCalledWith('RPC method not allowed') - vi.useRealTimers() - }) - - it('should handle auth RPC error when modal is open and transaction stack is empty', () => { - vi.spyOn(mockAppKit, 'isOpen').mockReturnValue(true) - vi.spyOn(mockAppKit, 'isTransactionStackEmpty').mockReturnValue(true) - client['handleAuthRpcError']() - expect(mockAppKit.close).toHaveBeenCalled() - }) - - it('should handle auth RPC error when modal is open and transaction stack is not empty', () => { - vi.spyOn(mockAppKit, 'isOpen').mockReturnValue(true) - vi.spyOn(mockAppKit, 'isTransactionStackEmpty').mockReturnValue(false) - client['handleAuthRpcError']() - expect(mockAppKit.popTransactionStack).toHaveBeenCalledWith(true) - }) - - it('should handle auth RPC success when transaction stack is empty', () => { - vi.spyOn(mockAppKit, 'isTransactionStackEmpty').mockReturnValue(true) - client['handleAuthRpcSuccess']( - { type: '@w3m-frame/SWITCH_NETWORK_SUCCESS', payload: { chainId: '137' } }, - { method: 'eth_accounts' } - ) - expect(mockAppKit.close).toHaveBeenCalled() + describe('EthersAdapter -connect', () => { + it('should connect with external provider', async () => { + vi.mocked(mockProvider.request).mockResolvedValue(['0x123']) + const connectors = [ + { + id: 'test', + provider: mockProvider, + chains: [1], + type: 'EXTERNAL', + chain: 1 + } + ] + Object.defineProperty(adapter, 'connectors', { + value: connectors }) - it('should handle auth RPC success when transaction stack is not empty', () => { - vi.spyOn(mockAppKit, 'isTransactionStackEmpty').mockReturnValue(false) - client['handleAuthRpcSuccess']( - { type: '@w3m-frame/SWITCH_NETWORK_SUCCESS', payload: { chainId: '137' } }, - { method: 'eth_accounts' } - ) - expect(mockAppKit.popTransactionStack).toHaveBeenCalledWith() + const result = await adapter.connect({ + id: 'test', + provider: mockProvider, + type: 'EXTERNAL', + chainId: 1 }) - it('should handle auth not connected', () => { - client['handleAuthNotConnected']() - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith(undefined, 'eip155') - }) + expect(result.address).toBe('0x123') + expect(result.chainId).toBe(1) + }) + }) - it('should handle auth is connected', () => { - vi.spyOn(mockAppKit, 'getActiveChainNamespace').mockReturnValue('eip155') - const preferredAccountType = 'eoa' - client['handleAuthIsConnected'](preferredAccountType) - expect(mockAppKit.setPreferredAccountType).toHaveBeenCalledWith( - preferredAccountType, - 'eip155' - ) + describe('EthersAdapter -disconnect', () => { + it('should disconnect WalletConnect provider', async () => { + await adapter.disconnect({ + provider: mockWalletConnectProvider, + providerType: 'WALLET_CONNECT' }) - it('should handle auth set preferred account', async () => { - const address = '0x1234567890123456789012345678901234567890' - const type = 'eoa' - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(mainnet) - client['caipNetworks'] = [mainnet] - - vi.spyOn(client as any, 'syncAccount').mockResolvedValue(undefined) - - await client['handleAuthSetPreferredAccount'](address, type) - - expect(mockAppKit.setLoading).toHaveBeenCalledWith(true) - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith(`eip155:${1}:${address}`, 'eip155') - expect(mockAppKit.setStatus).toHaveBeenCalledWith('connected', 'eip155') - expect(mockAppKit.setPreferredAccountType).toHaveBeenCalledWith(type, 'eip155') - - await new Promise(resolve => setImmediate(resolve)) - - expect(mockAppKit.setLoading).toHaveBeenLastCalledWith(false) - }) + expect(mockWalletConnectProvider.disconnect).toHaveBeenCalled() }) - describe('setAuthConnector', () => { - beforeEach(() => { - client['appKit'] = mockAppKit - client['authProvider'] = mockAuthConnector as any - - vi.mocked(ProviderUtil.setProvider).mockClear() - vi.mocked(ProviderUtil.setProviderId).mockClear() + it('should disconnect Auth provider', async () => { + await adapter.disconnect({ + provider: mockAuthProvider, + providerType: 'AUTH' }) - it('should set auth provider and update appKit state', async () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - const mockChainId = 1 - const mockSmartAccountDeployed = true - const mockPreferredAccountType = 'eoa' - - mockAuthConnector.connect.mockResolvedValueOnce({ - address: mockAddress, - chainId: mockChainId, - smartAccountDeployed: mockSmartAccountDeployed, - preferredAccountType: mockPreferredAccountType, - accounts: [] - }) - - mockAuthConnector.getSmartAccountEnabledNetworks.mockResolvedValueOnce({ - smartAccountEnabledNetworks: [1, 137] - }) - - await client['setAuthProvider']() - - expect(mockAppKit.setSmartAccountEnabledNetworks).toHaveBeenCalledWith([1, 137], 'eip155') - expect(mockAppKit.setAllAccounts).toHaveBeenCalledWith( - [{ address: mockAddress, type: mockPreferredAccountType }], - 'eip155' - ) - expect(mockAppKit.setStatus).toHaveBeenCalledWith('connected', 'eip155') - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith( - `eip155:${mockChainId}:${mockAddress}`, - 'eip155' - ) - expect(mockAppKit.setPreferredAccountType).toHaveBeenCalledWith( - mockPreferredAccountType, - 'eip155' - ) - expect(mockAppKit.setSmartAccountDeployed).toHaveBeenCalledWith( - mockSmartAccountDeployed, - 'eip155' - ) - expect(ProviderUtil.setProvider).toHaveBeenCalledWith('eip155', expect.any(Object)) - expect(ProviderUtil.setProviderId).toHaveBeenCalledWith( - 'eip155', - ConstantsUtil.AUTH_CONNECTOR_ID - ) - }) + expect(mockAuthProvider.disconnect).toHaveBeenCalled() }) }) - describe('EthersClient - Sync Connectors', () => { - it('should handle sync EIP-6963 connector', () => { - const mockEIP6963Event: CustomEventInit = { - detail: { - info: { - uuid: 'mock-uuid', - name: 'MockWallet', - icon: 'mock-icon-url', - rdns: 'com.mockwallet' - }, - provider: { - request: vi.fn(), - on: vi.fn(), - removeListener: vi.fn(), - emit: function (event: string): void { - throw new Error(event) - } - } - } - } - - client['eip6963EventHandler'](mockEIP6963Event) - - expect(mockAppKit.addConnector).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'eip6963', - name: 'MockWallet' - }) + describe('EthersAdapter -getBalance', () => { + it('should get balance successfully', async () => { + adapter.caipNetworks = mockCaipNetworks + const mockBalance = BigInt(1500000000000000000) + vi.mocked(JsonRpcProvider).mockImplementation( + () => + ({ + getBalance: vi.fn().mockResolvedValue(mockBalance) + }) as any ) - }) - it('should sync injected connector when config.injected is true', () => { - const config = { injected: true, coinbase: false, metadata: {} } - client['syncConnectors'](config as unknown as ProviderType) - - expect(mockAppKit.setConnectors).toHaveBeenCalledWith([ - expect.objectContaining({ - id: 'injected', - name: 'Injected', - type: 'INJECTED', - chain: 'eip155' - }) - ]) - }) - - it('should sync coinbase connector when config.coinbase is true', () => { - const config = { injected: false, coinbase: true, metadata: {} } - client['syncConnectors'](config as unknown as ProviderType) - - expect(mockAppKit.setConnectors).toHaveBeenCalledWith([ - expect.objectContaining({ - id: 'coinbaseWallet', - name: 'Coinbase', - type: 'EXTERNAL', - chain: 'eip155' - }) - ]) - }) - - it('should sync both connectors when both are true in config', () => { - const config = { injected: true, coinbase: true, metadata: {} } - client['syncConnectors'](config as unknown as ProviderType) - - expect(mockAppKit.setConnectors).toHaveBeenCalledWith([ - expect.objectContaining({ - id: 'injected', - name: 'Injected', - type: 'INJECTED', - chain: 'eip155' - }), - expect.objectContaining({ - id: 'coinbaseWallet', - name: 'Coinbase', - type: 'EXTERNAL', - chain: 'eip155' - }) - ]) - }) - - it('should not sync any connectors when both are false in config', () => { - const config = { injected: false, coinbase: false, metadata: {} } - client['syncConnectors'](config as unknown as ProviderType) - expect(mockAppKit.setConnectors).toHaveBeenCalledWith([]) - }) - }) - - describe('EthersClient - State Subscription', () => { - it('should subscribe to state changes', () => { - const mockCallback = vi.fn() - client.subscribeState(mockCallback) - - expect(mockAppKit.subscribeState).toHaveBeenCalled() - }) - }) - - describe('EthersClient - setProvider', () => { - beforeEach(() => { - vi.spyOn(SafeLocalStorage, 'setItem') - vi.spyOn(EthersHelpersUtil, 'getUserInfo').mockResolvedValue({ - addresses: ['0x1234567890123456789012345678901234567890'], + const result = await adapter.getBalance({ + address: '0x123', chainId: 1 }) - }) - - it('should set provider for non-auth providers', async () => { - const mockProvider = { request: vi.fn() } - await client['setProvider'](mockProvider as any, 'injected', 'MetaMask') - expect(SafeLocalStorage.setItem).toHaveBeenCalledWith( - SafeLocalStorageKeys.WALLET_ID, - 'injected' - ) - expect(SafeLocalStorage.setItem).toHaveBeenCalledWith( - SafeLocalStorageKeys.WALLET_NAME, - 'MetaMask' - ) - expect(mockAppKit.setCaipAddress).toHaveBeenCalled() - expect(ProviderUtil.setProviderId).toHaveBeenCalledWith('eip155', 'injected') - expect(ProviderUtil.setProvider).toHaveBeenCalledWith('eip155', mockProvider) - expect(mockAppKit.setStatus).toHaveBeenCalledWith('connected', 'eip155') - expect(mockAppKit.setAllAccounts).toHaveBeenCalled() - }) - }) - - describe('EthersClient - setupProviderListeners', () => { - let mockProvider: any - - beforeEach(() => { - mockProvider = { - on: vi.fn(), - removeListener: vi.fn() - } - }) - - it('should set up listeners for non-auth providers', () => { - client['setupProviderListeners'](mockProvider, 'injected') - - expect(mockProvider.on).toHaveBeenCalledWith('disconnect', expect.any(Function)) - expect(mockProvider.on).toHaveBeenCalledWith('accountsChanged', expect.any(Function)) - expect(mockProvider.on).toHaveBeenCalledWith('chainChanged', expect.any(Function)) - }) - - it('should handle disconnect event', async () => { - vi.spyOn(SafeLocalStorage, 'removeItem') - client['setupProviderListeners'](mockProvider, 'injected') - - const disconnectHandler = mockProvider.on.mock.calls.find( - (call: string[]) => call[0] === 'disconnect' - )[1] - await disconnectHandler() - - expect(SafeLocalStorage.removeItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) - expect(mockProvider.removeListener).toHaveBeenCalledTimes(3) - }) - - it('should handle accountsChanged event', async () => { - client['setupProviderListeners'](mockProvider, 'injected') - - const address = '0x1234567890123456789012345678901234567890' - const accountsChangedHandler = mockProvider.on.mock.calls.find( - (call: string[]) => call[0] === 'accountsChanged' - )[1] - await accountsChangedHandler([address]) - - expect(mockAppKit.setCaipAddress).toHaveBeenCalled() - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith(`eip155:1:${address}`, 'eip155') - }) - - it('should handle chainChanged event', async () => { - client['setupProviderListeners'](mockProvider, 'injected') - - const chainChangedHandler = mockProvider.on.mock.calls.find( - (call: string[]) => call[0] === 'chainChanged' - )[1] - await chainChangedHandler('0x10') - - expect(mockAppKit.setCaipNetwork).toHaveBeenCalled() - }) - }) - - describe('EthersClient - checkActiveProviders', () => { - let mockProvider: any - - beforeEach(() => { - mockProvider = { - request: vi.fn(), - on: vi.fn(), - removeListener: vi.fn() - } - - vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation(key => { - if (key === SafeLocalStorageKeys.WALLET_ID) return ConstantsUtil.INJECTED_CONNECTOR_ID - if (key === SafeLocalStorageKeys.WALLET_NAME) return 'MetaMask' - return undefined + expect(result).toEqual({ + balance: '1.5', + symbol: 'ETH' }) - - vi.spyOn(client as any, 'setProvider').mockImplementation(() => Promise.resolve()) - vi.spyOn(client as any, 'setupProviderListeners').mockImplementation(() => {}) - }) - - it('should check and set active provider for injected and coinbase wallet', () => { - const mockConfig = { - injected: mockProvider, - coinbase: mockProvider, - metadata: {} - } as ProviderType - - const providers = { - [ConstantsUtil.INJECTED_CONNECTOR_ID]: 'MetaMask', - [ConstantsUtil.COINBASE_SDK_CONNECTOR_ID]: 'Coinbase Wallet' - } as const - - for (const [key, name] of Object.entries(providers)) { - vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation(localStorageKey => { - if (localStorageKey === SafeLocalStorageKeys.WALLET_ID) return key - if (localStorageKey === SafeLocalStorageKeys.WALLET_NAME) return name - return undefined - }) - - client['checkActiveProviders'](mockConfig) - - expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) - expect(client['setProvider']).toHaveBeenCalledWith(mockProvider, key) - expect(client['setupProviderListeners']).toHaveBeenCalledWith(mockProvider, key) - } - }) - - it('should not set provider when wallet ID is not found', () => { - vi.spyOn(SafeLocalStorage, 'getItem').mockReturnValue(undefined) - - const mockConfig = { - injected: mockProvider, - coinbase: undefined, - metadata: {} - } - - client['checkActiveProviders'](mockConfig as ProviderType) - - expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) - expect(client['setProvider']).not.toHaveBeenCalled() - expect(client['setupProviderListeners']).not.toHaveBeenCalled() - }) - - it('should not set provider when injected provider is not available', () => { - const mockConfig = { - injected: undefined, - coinbase: undefined, - metadata: {} - } - - client['checkActiveProviders'](mockConfig as ProviderType) - - expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) - expect(client['setProvider']).not.toHaveBeenCalled() - expect(client['setupProviderListeners']).not.toHaveBeenCalled() }) }) - describe('EthersClient - syncAccount', () => { - beforeEach(() => { - vi.spyOn(client as any, 'syncConnectedWalletInfo').mockImplementation(() => {}) - vi.spyOn(client as any, 'setupProviderListeners').mockImplementation(() => {}) - vi.spyOn(client as any, 'setProvider').mockImplementation(() => Promise.resolve()) - vi.spyOn(client as any, 'syncProfile').mockImplementation(() => Promise.resolve()) - vi.spyOn(client as any, 'syncBalance').mockImplementation(() => Promise.resolve()) - vi.spyOn(mockAppKit, 'getIsConnectedState').mockReturnValue(true) - vi.spyOn(mockAppKit, 'getPreferredAccountType').mockReturnValue('eoa') - }) - - it('should sync account when connected and address is provided', async () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - const mockCaipNetwork = mainnet + describe('EthersAdapter -getProfile', () => { + it('should get profile successfully', async () => { + const mockEnsName = 'test.eth' + const mockAvatar = 'https://avatar.com/test.jpg' + + vi.mocked(InfuraProvider).mockImplementation( + () => + ({ + lookupAddress: vi.fn().mockResolvedValue(mockEnsName), + getAvatar: vi.fn().mockResolvedValue(mockAvatar) + }) as any + ) - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(mockCaipNetwork) - vi.spyOn(EthersHelpersUtil, 'getUserInfo').mockResolvedValue({ - addresses: ['0x1234567890123456789012345678901234567890'], + const result = await adapter.getProfile({ + address: '0x123', chainId: 1 }) - await client['syncAccount']({ address: mockAddress }) - - expect(mockAppKit.setPreferredAccountType).toHaveBeenCalledWith('eoa', 'eip155') - expect(mockAppKit.setAddressExplorerUrl).toHaveBeenCalledWith( - `https://etherscan.io/address/${mockAddress}`, - 'eip155' - ) - expect(client['syncConnectedWalletInfo']).toHaveBeenCalled() - expect(client['syncProfile']).toHaveBeenCalledWith(mockAddress) - expect(mockAppKit.setCaipAddress).toHaveBeenCalledTimes(2) - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith( - `eip155:${mainnet.id}:${mockAddress}`, - 'eip155' - ) - expect(mockAppKit.setCaipNetwork).toHaveBeenCalledOnce() - expect(mockAppKit.setCaipNetwork).toHaveBeenCalledWith(mainnet) - expect(mockAppKit.setApprovedCaipNetworksData).toHaveBeenCalledWith('eip155') - }) - - it('it should fallback to first available chain if current chain is unsupported', async () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - - vi.spyOn(EthersHelpersUtil, 'getUserInfo').mockResolvedValue({ - addresses: [mockAddress], - chainId: AppkitHarmonyOne.id as number + expect(result).toEqual({ + profileName: mockEnsName, + profileImage: mockAvatar }) - - await client['syncAccount']({ address: mockAddress }) - - expect(mockAppKit.setCaipAddress).toHaveBeenCalledTimes(2) - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith( - `eip155:${mainnet.id}:${mockAddress}`, - 'eip155' - ) - expect(mockAppKit.setCaipNetwork).toHaveBeenCalledOnce() - expect(mockAppKit.setCaipNetwork).toHaveBeenCalledWith(mainnet) - }) - - it('should reset connection when not connected', async () => { - vi.spyOn(mockAppKit, 'getIsConnectedState').mockReturnValue(false) - - await client['syncAccount']({}) - - expect(mockAppKit.resetWcConnection).toHaveBeenCalled() - expect(mockAppKit.resetNetwork).toHaveBeenCalled() - expect(mockAppKit.setAllAccounts).toHaveBeenCalledWith([], 'eip155') }) }) - describe('EthersClient - syncProfile', () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - - beforeEach(() => { - vi.spyOn(client as any, 'syncReownName').mockImplementation(() => Promise.resolve()) - }) - - it('should set profile from fetchIdentity when successful', async () => { - const mockIdentity = { name: 'Test Name', avatar: 'https://example.com/avatar.png' } - vi.spyOn(mockAppKit, 'fetchIdentity').mockResolvedValue(mockIdentity) - - await client['syncProfile'](mockAddress) - - expect(mockAppKit.setProfileName).toHaveBeenCalledWith('Test Name', 'eip155') - expect(mockAppKit.setProfileImage).toHaveBeenCalledWith( - 'https://example.com/avatar.png', - 'eip155' - ) - }) - - it('should use ENS for mainnet when fetchIdentity fails', async () => { - vi.spyOn(mockAppKit, 'fetchIdentity').mockRejectedValue(new Error('Fetch failed')) - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(mainnet) - - const mockEnsProvider = { - lookupAddress: vi.fn().mockResolvedValue('test.eth'), - getAvatar: vi.fn().mockResolvedValue('https://example.com/ens-avatar.png') - } - vi.mocked(InfuraProvider).mockImplementation(() => mockEnsProvider as any) - - await client['syncProfile'](mockAddress) + describe('EthersAdapter - switchNetwork', () => { + it('should switch network with WalletConnect provider', async () => { + await adapter.switchNetwork({ + caipNetwork: mockCaipNetworks[0], + provider: mockWalletConnectProvider, + providerType: 'WALLET_CONNECT' + }) - expect(mockAppKit.setProfileName).toHaveBeenCalledWith('test.eth', 'eip155') - expect(mockAppKit.setProfileImage).toHaveBeenCalledWith( - 'https://example.com/ens-avatar.png', - 'eip155' - ) + expect(mockWalletConnectProvider.setDefaultChain).toHaveBeenCalledWith('eip155:1') }) - it('should fallback to syncReownName for non-mainnet chains', async () => { - vi.spyOn(mockAppKit, 'fetchIdentity').mockRejectedValue(new Error('Fetch failed')) - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(polygon) // Polygon - - await client['syncProfile'](mockAddress) + it('should switch network with Auth provider', async () => { + await adapter.switchNetwork({ + caipNetwork: mockCaipNetworks[0], + provider: mockAuthProvider, + providerType: 'AUTH' + }) - expect(client['syncReownName']).toHaveBeenCalledWith(mockAddress) - expect(mockAppKit.setProfileImage).toHaveBeenCalledWith(null, 'eip155') + expect(mockAuthProvider.switchNetwork).toHaveBeenCalledWith(1) + expect(mockAuthProvider.connect).toHaveBeenCalledWith({ chainId: 1 }) }) }) - describe('EthersClient - syncBalance', () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - - it.skip('should set balance when caipNetwork is available', async () => { - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(mainnet) - - const mockJsonRpcProvider = { - getBalance: vi.fn().mockResolvedValue(BigInt(1000000000000000000)) // 1 ETH in wei - } - vi.mocked(JsonRpcProvider).mockImplementation(() => mockJsonRpcProvider as any) - - await client['syncBalance'](mockAddress, mainnet) - - expect(JsonRpcProvider).toHaveBeenCalledWith(mainnet.rpcUrls.default.http[0], { - chainId: 1, - name: 'Ethereum' + describe('EthersAdapter -getWalletConnectProvider', () => { + it('should return WalletConnect provider', () => { + Object.defineProperty(adapter, 'availableConnectors', { + value: [ + { + id: 'walletconnect', + type: 'WALLET_CONNECT', + provider: mockWalletConnectProvider, + chain: 'eip155', + chains: [] + } + ] }) - expect(mockJsonRpcProvider.getBalance).toHaveBeenCalledWith(mockAddress) - expect(mockAppKit.setBalance).toHaveBeenCalledWith('1.0', 'ETH', 'eip155') - }) - - it('should not set balance when caipNetwork is unavailable', async () => { - vi.spyOn(mockAppKit, 'getCaipNetworks').mockReturnValue([]) - await client['syncBalance'](mockAddress, mainnet) - - expect(JsonRpcProvider).not.toHaveBeenCalled() - expect(mockAppKit.setBalance).not.toHaveBeenCalled() + const result = adapter.getWalletConnectProvider() + expect(result).toBe(mockWalletConnectProvider) }) }) - describe('EthersClient - syncReownName', () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - - it('should set profile name when WalletConnect name is available', async () => { - const mockWcNames = [ - { name: 'WC Wallet', registered: 1, updated: 1234567890, addresses: [], attributes: {} } - ] as unknown as BlockchainApiLookupEnsName[] - vi.spyOn(mockAppKit, 'getReownName').mockResolvedValue(mockWcNames) - - await client['syncReownName'](mockAddress) - - expect(mockAppKit.getReownName).toHaveBeenCalledWith(mockAddress) - expect(mockAppKit.setProfileName).toHaveBeenCalledWith('WC Wallet', 'eip155') - }) - - it('should set profile name to null when no WalletConnect name is available', async () => { - vi.spyOn(mockAppKit, 'getReownName').mockResolvedValue([]) + describe('EthersAdapter -parseUnits and formatUnits', () => { + it('should parse units correctly', () => { + const mockBigInt = BigInt('1500000000000000000') + vi.mocked(EthersMethods.parseUnits).mockReturnValue(mockBigInt) - await client['syncReownName'](mockAddress) - - expect(mockAppKit.getReownName).toHaveBeenCalledWith(mockAddress) - expect(mockAppKit.setProfileName).toHaveBeenCalledWith(null, 'eip155') - }) - - it('should set profile name to null when getReownName throws an error', async () => { - vi.spyOn(mockAppKit, 'getReownName').mockRejectedValue(new Error('API Error')) - - await client['syncReownName'](mockAddress) - - expect(mockAppKit.getReownName).toHaveBeenCalledWith(mockAddress) - expect(mockAppKit.setProfileName).toHaveBeenCalledWith(null, 'eip155') - }) - }) - - describe('EthersClient - syncConnectedWalletInfo', () => { - beforeEach(() => { - vi.spyOn(SafeLocalStorage, 'getItem').mockReturnValue('MetaMask') - vi.spyOn(ProviderUtil.state, 'providerIds', 'get').mockReturnValue({ - eip155: 'injected', - solana: undefined, - polkadot: undefined + const result = adapter.parseUnits({ + value: '1.5', + decimals: 18 }) - }) - it('should set connected wallet info for EIP6963 provider', () => { - vi.spyOn(ProviderUtil.state, 'providerIds', 'get').mockReturnValue({ - eip155: ConstantsUtil.EIP6963_CONNECTOR_ID, - solana: undefined - } as Record) - client['EIP6963Providers'] = [ - { - info: { name: 'MetaMask', icon: 'icon-url', uuid: 'test-uuid', rdns: 'com.metamask' }, - provider: {} as any - } - ] - - client['syncConnectedWalletInfo']() - expect(mockAppKit.setConnectedWalletInfo).toHaveBeenCalledWith( - { name: 'MetaMask', icon: 'icon-url', uuid: 'test-uuid', rdns: 'com.metamask' }, - 'eip155' - ) + expect(result).toBe(mockBigInt) }) - it('should set connected wallet info for WalletConnect provider', () => { - vi.spyOn(ProviderUtil.state, 'providerIds', 'get').mockReturnValue({ - eip155: ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID, - solana: undefined - } as Record) - const mockProvider = { - session: { - peer: { - metadata: { - name: 'WC Wallet', - icons: ['wc-icon-url'] - } - } - } - } - vi.spyOn(ProviderUtil, 'getProvider').mockReturnValue(mockProvider as any) - - client['syncConnectedWalletInfo']() + it('should format units correctly', () => { + vi.mocked(EthersMethods.formatUnits).mockReturnValue('1.5') - expect(mockAppKit.setConnectedWalletInfo).toHaveBeenCalledWith( - { - name: 'WC Wallet', - icon: 'wc-icon-url', - icons: ['wc-icon-url'] - }, - 'eip155' - ) - }) - - it('should set connected wallet info for Coinbase provider', () => { - vi.spyOn(ProviderUtil.state, 'providerIds', 'get').mockReturnValue({ - eip155: ConstantsUtil.COINBASE_SDK_CONNECTOR_ID, - solana: undefined - } as Record) - vi.spyOn(mockAppKit, 'getConnectors').mockReturnValue([ - { - id: ConstantsUtil.COINBASE_SDK_CONNECTOR_ID, - type: 'INJECTED', - chain: 'eip155' - } - ]) - vi.spyOn(mockAppKit, 'getConnectorImage').mockReturnValue('coinbase-icon-url') - - client['syncConnectedWalletInfo']() + const result = adapter.formatUnits({ + value: BigInt('1500000000000000000'), + decimals: 18 + }) - expect(mockAppKit.setConnectedWalletInfo).toHaveBeenCalledWith( - { name: 'Coinbase Wallet', icon: 'coinbase-icon-url' }, - 'eip155' - ) + expect(result).toBe('1.5') }) }) }) diff --git a/packages/adapters/ethers/src/utils/EthersMethods.ts b/packages/adapters/ethers/src/utils/EthersMethods.ts index 87aebcf707..559a480ac1 100644 --- a/packages/adapters/ethers/src/utils/EthersMethods.ts +++ b/packages/adapters/ethers/src/utils/EthersMethods.ts @@ -10,14 +10,14 @@ import { parseUnits, formatUnits } from 'ethers' -import { type Provider } from '@reown/appkit-utils/ethers' import type { EstimateGasTransactionArgs, + Provider, SendTransactionArgs, WriteContractArgs } from '@reown/appkit-core' -import { isReownName } from '@reown/appkit-common' -import type { AppKit } from '@reown/appkit' +import { isReownName, type CaipNetwork } from '@reown/appkit-common' +import { WcHelpersUtil } from '@reown/appkit' export const EthersMethods = { signMessage: async (message: string, provider: Provider, address: string) => { @@ -117,14 +117,14 @@ export const EthersMethods = { throw new Error('Contract method is undefined') }, - getEnsAddress: async (value: string, appKit: AppKit) => { + getEnsAddress: async (value: string, caipNetwork: CaipNetwork) => { try { - const chainId = Number(appKit.getCaipNetwork()?.id) + const chainId = Number(caipNetwork.id) let ensName: string | null = null let wcName: boolean | string = false if (isReownName(value)) { - wcName = (await appKit?.resolveReownName(value)) || false + wcName = (await WcHelpersUtil.resolveReownName(value)) || false } // If on mainnet, fetch from ENS diff --git a/packages/adapters/ethers5/src/client.ts b/packages/adapters/ethers5/src/client.ts index d9d2cfe349..c15e1c6191 100644 --- a/packages/adapters/ethers5/src/client.ts +++ b/packages/adapters/ethers5/src/client.ts @@ -1,114 +1,35 @@ -import type { AppKitOptions, AppKitOptionsWithCaipNetworks } from '@reown/appkit' -import { - SafeLocalStorage, - SafeLocalStorageKeys, - type AdapterType, - type CaipAddress, - type CaipNetwork, - type CaipNetworkId, - type ChainNamespace -} from '@reown/appkit-common' +import { AdapterBlueprint } from '@reown/appkit/adapters' +import type { CaipNetwork } from '@reown/appkit-common' +import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' import { - AccountController, - ChainController, - CoreHelperUtil, - AlertController, type CombinedProvider, - type Connector + type Connector, + type ConnectorType, + type Provider } from '@reown/appkit-core' -import { - EthersHelpersUtil, - type Provider, - type ProviderType, - type Address -} from '@reown/appkit-utils/ethers' -import type { AppKit } from '@reown/appkit' -import { - W3mFrameHelpers, - W3mFrameProvider, - W3mFrameRpcConstants, - type W3mFrameTypes -} from '@reown/appkit-wallet' -import { ConstantsUtil as CoreConstantsUtil } from '@reown/appkit-core' -import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' -import { - CaipNetworksUtil, - ConstantsUtil, - ErrorUtil, - HelpersUtil, - PresetsUtil -} from '@reown/appkit-utils' +import { ConstantsUtil, PresetsUtil } from '@reown/appkit-utils' +import { EthersHelpersUtil, type ProviderType } from '@reown/appkit-utils/ethers' +import { WcConstantsUtil, WcHelpersUtil, type AppKitOptions } from '@reown/appkit' import UniversalProvider from '@walletconnect/universal-provider' -import type { ConnectionControllerClient, NetworkControllerClient } from '@reown/appkit-core' -import { WcConstantsUtil } from '@reown/appkit' -import { Ethers5Methods } from './utils/Ethers5Methods.js' -import { ethers } from 'ethers' -import type { PublicStateControllerState } from '@reown/appkit-core' -import { ProviderUtil, type ProviderIdType } from '@reown/appkit/store' +import * as ethers from 'ethers' import { CoinbaseWalletSDK, type ProviderInterface } from '@coinbase/wallet-sdk' -import { W3mFrameProviderSingleton } from '@reown/appkit/auth-provider' - -// -- Types --------------------------------------------------------------------- -export interface AdapterOptions { - ethersConfig: ProviderType - defaultCaipNetwork?: CaipNetwork -} - -type CoinbaseProviderError = { - code: number - message: string - data: string | undefined -} - -interface ExternalProvider extends Provider { - accounts: string[] -} - -declare global { - interface Window { - ethereum?: Record - } -} - -interface Info { - uuid: string - name: string - icon: string - rdns: string -} +import type { W3mFrameProvider } from '@reown/appkit-wallet' +import { Ethers5Methods } from './utils/Ethers5Methods.js' +import { formatEther } from 'ethers/lib/utils.js' export interface EIP6963ProviderDetail { - info: Info + info: Connector['info'] provider: Provider } -export class Ethers5Adapter { - private appKit: AppKit | undefined = undefined - - private EIP6963Providers: EIP6963ProviderDetail[] = [] - - private ethersConfig?: AdapterOptions['ethersConfig'] - - private authProvider?: W3mFrameProvider - - // -- Public variables -------------------------------------------------------- - public options: AppKitOptions | undefined = undefined +export class Ethers5Adapter extends AdapterBlueprint { + private ethersConfig?: ProviderType + public adapterType = 'ethers' - public caipNetworks: CaipNetwork[] = [] - - public chainNamespace: ChainNamespace = CommonConstantsUtil.CHAIN.EVM - - public networkControllerClient?: NetworkControllerClient - - public connectionControllerClient?: ConnectionControllerClient - - public siweControllerClient = this.options?.siweConfig - - public tokens = HelpersUtil.getCaipTokens(this.options?.tokens) - - public defaultCaipNetwork: CaipNetwork | undefined = undefined - - public adapterType: AdapterType = 'ethers' + constructor() { + super({}) + this.namespace = CommonConstantsUtil.CHAIN.EVM + } private createEthersConfig(options: AppKitOptions) { if (!options.metadata) { @@ -175,887 +96,409 @@ export class Ethers5Adapter { return providers } - // -- Public ------------------------------------------------------------------- - // eslint-disable-next-line @typescript-eslint/no-useless-constructor, @typescript-eslint/no-empty-function - public constructor() { - ChainController.subscribeKey('activeCaipNetwork', val => { - const caipAddress = this.appKit?.getCaipAddress(this.chainNamespace) - const isEVMAddress = caipAddress?.startsWith('eip155:') - const isEVMNetwork = val?.chainNamespace === this.chainNamespace - - if (isEVMAddress && isEVMNetwork && caipAddress) { - this.syncBalance(CoreHelperUtil.getPlainAddress(caipAddress) as Address, val) - this.syncAccount({ - address: CoreHelperUtil.getPlainAddress(caipAddress) as Address | undefined, - caipNetwork: val - }) - } - }) - ChainController.subscribeKey('activeCaipAddress', val => { - const isEVMAddress = val?.startsWith('eip155:') - const caipNetwork = ChainController.state.activeCaipNetwork - const isEVMNetwork = caipNetwork?.chainNamespace === this.chainNamespace - - if (isEVMAddress) { - if (isEVMNetwork) { - this.syncBalance(CoreHelperUtil.getPlainAddress(val) as Address, caipNetwork) - } - this.syncAccount({ address: CoreHelperUtil.getPlainAddress(val) as Address }) - } - }) - AccountController.subscribeKey( - 'shouldUpdateToAddress', - newAddress => { - const isEVMAddress = newAddress?.startsWith('0x') - - if (isEVMAddress) { - this.syncAccount({ address: newAddress as Address }) - } - }, - this.chainNamespace - ) - } - - public construct(appKit: AppKit, options: AppKitOptionsWithCaipNetworks) { - this.appKit = appKit - this.options = options - this.caipNetworks = options.networks - this.defaultCaipNetwork = options.defaultNetwork - ? CaipNetworksUtil.extendCaipNetwork(options.defaultNetwork, { - customNetworkImageUrls: options.chainImages, - projectId: options.projectId - }) - : this.caipNetworks[0] - this.tokens = HelpersUtil.getCaipTokens(options.tokens) - this.ethersConfig = this.createEthersConfig(options) - - this.networkControllerClient = { - switchCaipNetwork: async caipNetwork => { - if (caipNetwork?.id) { - try { - await this.switchNetwork(caipNetwork) - } catch (error) { - throw new Error('networkControllerClient:switchCaipNetwork - unable to switch chain') - } - } - }, + public async signMessage( + params: AdapterBlueprint.SignMessageParams + ): Promise { + const { message, address, provider } = params - // eslint-disable-next-line @typescript-eslint/require-await - getApprovedCaipNetworksData: async () => this.getApprovedCaipNetworksData() + if (!provider) { + throw new Error('Provider is undefined') } + try { + const signature = await Ethers5Methods.signMessage(message, provider as Provider, address) - this.connectionControllerClient = { - connectWalletConnect: async onUri => { - await this.appKit?.universalAdapter?.connectionControllerClient?.connectWalletConnect?.( - onUri - ) - }, - - // @ts-expect-error TODO expected types in arguments are incomplete - connectExternal: async ({ - id, - info, - provider - }: { - id: string - info?: Info - provider: Provider - }) => { - this.appKit?.setClientId(null) - - const connectorConfig = { - [ConstantsUtil.INJECTED_CONNECTOR_ID]: { - getProvider: () => this.ethersConfig?.injected, - providerType: 'injected' as const - }, - [ConstantsUtil.EIP6963_CONNECTOR_ID]: { - getProvider: () => provider, - providerType: 'eip6963' as const - }, - [ConstantsUtil.COINBASE_SDK_CONNECTOR_ID]: { - getProvider: () => this.ethersConfig?.coinbase, - providerType: 'coinbaseWalletSDK' as const - }, - [ConstantsUtil.AUTH_CONNECTOR_ID]: { - getProvider: () => this.authProvider, - providerType: 'w3mAuth' as const - } - } - - const selectedConnector = connectorConfig[id] - - if (!selectedConnector) { - throw new Error(`Unsupported connector ID: ${id}`) - } - - const selectedProvider = selectedConnector.getProvider() as Provider - - if (!selectedProvider) { - throw new Error(`Provider for connector ${id} is undefined`) - } - - try { - if (selectedProvider && id !== ConstantsUtil.AUTH_CONNECTOR_ID) { - await selectedProvider.request({ method: 'eth_requestAccounts' }) - } - await this.setProvider( - selectedProvider, - selectedConnector.providerType as ProviderIdType, - info?.name - ) - } catch (error) { - if (id === ConstantsUtil.COINBASE_SDK_CONNECTOR_ID) { - throw new Error((error as CoinbaseProviderError).message) - } - } - }, - - checkInstalled: (ids?: string[]) => { - if (!ids) { - return Boolean(window.ethereum) - } - - if (this.ethersConfig?.injected) { - if (!window?.ethereum) { - return false - } - } - - return ids.some(id => Boolean(window.ethereum?.[String(id)])) - }, - - disconnect: async () => { - const provider = ProviderUtil.getProvider( - 'eip155' - ) - const providerId = ProviderUtil.state.providerIds['eip155'] - - this.appKit?.setClientId(null) - if (this.options?.siweConfig?.options?.signOutOnDisconnect) { - const { SIWEController } = await import('@reown/appkit-siwe') - await SIWEController.signOut() - } - - const disconnectConfig = { - [ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID]: async () => - await this.appKit?.universalAdapter?.connectionControllerClient?.disconnect(), - - coinbaseWalletSDK: async () => { - if (provider && 'disconnect' in provider) { - await provider.disconnect() - } - }, - - [ConstantsUtil.AUTH_CONNECTOR_ID]: async () => { - await this.authProvider?.disconnect() - }, - - [ConstantsUtil.EIP6963_CONNECTOR_ID]: async () => { - if (provider) { - await this.revokeProviderPermissions(provider as Provider) - } - }, - [ConstantsUtil.INJECTED_CONNECTOR_ID]: async () => { - if (provider) { - ;(provider as Provider).emit('disconnect') - await this.revokeProviderPermissions(provider as Provider) - } - } - } - const disconnectFunction = disconnectConfig[providerId as string] - - if (disconnectFunction) { - await disconnectFunction() - } else { - console.warn(`No disconnect function found for provider type: ${providerId}`) - } - - // Common cleanup actions - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_ID) - this.appKit?.resetAccount(this.chainNamespace) - this.removeListeners(provider as Provider) - }, - signMessage: async (message: string) => { - const provider = ProviderUtil.getProvider(this.chainNamespace) - const caipAddress = ChainController.state.activeCaipAddress - const address = CoreHelperUtil.getPlainAddress(caipAddress) - - if (!address) { - throw new Error('Address is undefined') - } - - if (!provider) { - throw new Error('Provider is undefined') - } - - return await Ethers5Methods.signMessage(message, provider, address) - }, - - parseUnits: Ethers5Methods.parseUnits, - formatUnits: Ethers5Methods.formatUnits, - - estimateGas: async data => { - if (data.chainNamespace && data.chainNamespace !== 'eip155') { - throw new Error(`Invalid chain namespace - Expected eip155, got ${data.chainNamespace}`) - } - const provider = ProviderUtil.getProvider('eip155') - const caipAddress = ChainController.state.activeCaipAddress - const address = CoreHelperUtil.getPlainAddress(caipAddress) - const caipNetwork = this.appKit?.getCaipNetwork() - - if (!address) { - throw new Error('Address is undefined') - } + return { signature } + } catch (error) { + throw new Error('EthersAdapter:signMessage - Sign message failed') + } + } - if (!provider) { - throw new Error('Provider is undefined') - } + public async sendTransaction( + params: AdapterBlueprint.SendTransactionParams + ): Promise { + if (!params.provider) { + throw new Error('Provider is undefined') + } - return await Ethers5Methods.estimateGas(data, provider, address, Number(caipNetwork?.id)) + const tx = await Ethers5Methods.sendTransaction( + { + value: params.value as bigint, + to: params.to as `0x${string}`, + data: params.data as `0x${string}`, + gas: params.gas as bigint, + gasPrice: params.gasPrice as bigint, + address: params.address }, + params.provider as Provider, + params.address, + Number(params.caipNetwork?.id) + ) - sendTransaction: async data => { - if (data.chainNamespace && data.chainNamespace !== 'eip155') { - throw new Error(`Invalid chain namespace - Expected eip155, got ${data.chainNamespace}`) - } - const provider = ProviderUtil.getProvider('eip155') - const caipAddress = ChainController.state.activeCaipAddress - const address = CoreHelperUtil.getPlainAddress(caipAddress) - const caipNetwork = this.appKit?.getCaipNetwork() - - if (!address) { - throw new Error('Address is undefined') - } + return { hash: tx } + } - if (!provider) { - throw new Error('Provider is undefined') - } + public async writeContract( + params: AdapterBlueprint.WriteContractParams + ): Promise { + if (!params.provider) { + throw new Error('Provider is undefined') + } - return await Ethers5Methods.sendTransaction( - data, - provider, - address, - Number(caipNetwork?.id) - ) + const result = await Ethers5Methods.writeContract( + { + abi: params.abi, + method: params.method, + fromAddress: params.caipAddress as `0x${string}`, + receiverAddress: params.receiverAddress as `0x${string}`, + tokenAmount: params.tokenAmount, + tokenAddress: params.tokenAddress as `0x${string}` }, + params.provider as Provider, + params.caipAddress, + Number(params.caipNetwork?.id) + ) - writeContract: async data => { - const provider = ProviderUtil.getProvider('eip155') - const caipAddress = ChainController.state.activeCaipAddress - const address = CoreHelperUtil.getPlainAddress(caipAddress) - const caipNetwork = this.appKit?.getCaipNetwork() - - if (!address) { - throw new Error('Address is undefined') - } - - if (!provider) { - throw new Error('Provider is undefined') - } + return { hash: result } + } - return await Ethers5Methods.writeContract(data, provider, address, Number(caipNetwork?.id)) - }, + public async estimateGas( + params: AdapterBlueprint.EstimateGasTransactionArgs + ): Promise { + const { provider, caipNetwork, address } = params + if (!provider) { + throw new Error('Provider is undefined') + } - getEnsAddress: async (value: string) => { - if (this.appKit) { - return await Ethers5Methods.getEnsAddress(value, this.appKit) - } + try { + const result = await Ethers5Methods.estimateGas( + { + data: params.data as `0x${string}`, + to: params.to as `0x${string}`, + address: address as `0x${string}` + }, + provider as Provider, + address as `0x${string}`, + Number(caipNetwork?.id) + ) - return false - }, + return { gas: result } + } catch (error) { + throw new Error('EthersAdapter:estimateGas - Estimate gas failed') + } + } - getEnsAvatar: async (value: string) => { - const caipNetwork = this.appKit?.getCaipNetwork() + public async getEnsAddress( + params: AdapterBlueprint.GetEnsAddressParams + ): Promise { + const { name, caipNetwork } = params + if (caipNetwork) { + const result = await Ethers5Methods.getEnsAddress(name, caipNetwork) - return await Ethers5Methods.getEnsAvatar(value, Number(caipNetwork?.id)) - }, + return { address: result as string } + } - grantPermissions: async params => { - const provider = ProviderUtil.getProvider(CommonConstantsUtil.CHAIN.EVM) + return { address: '' } + } - if (!provider) { - throw new Error('Provider is undefined') - } + public parseUnits(params: AdapterBlueprint.ParseUnitsParams): AdapterBlueprint.ParseUnitsResult { + return Ethers5Methods.parseUnits(params.value, params.decimals) + } - return await provider.request({ method: 'wallet_grantPermissions', params }) - }, + public formatUnits( + params: AdapterBlueprint.FormatUnitsParams + ): AdapterBlueprint.FormatUnitsResult { + return Ethers5Methods.formatUnits(params.value, params.decimals) + } - revokePermissions: async session => { - const provider = ProviderUtil.getProvider(CommonConstantsUtil.CHAIN.EVM) + public async syncConnection( + params: AdapterBlueprint.SyncConnectionParams + ): Promise { + const { id, chainId } = params - if (!provider) { - throw new Error('Provider is undefined') - } + const connector = this.connectors.find(c => c.id === id) + const selectedProvider = connector?.provider as Provider - return await provider.request({ method: 'wallet_revokePermissions', params: [session] }) - } + if (!selectedProvider) { + throw new Error('Provider not found') } - ChainController.state.chains.set(this.chainNamespace, { - chainNamespace: this.chainNamespace, - connectionControllerClient: this.connectionControllerClient, - networkControllerClient: this.networkControllerClient, - adapterType: this.adapterType, - caipNetworks: this.caipNetworks + const accounts: string[] = await selectedProvider.request({ + method: 'eth_requestAccounts' }) - if (this.ethersConfig) { - this.syncConnectors(this.ethersConfig) - } + this.listenProviderEvents(selectedProvider) - if (typeof window !== 'undefined') { - this.listenConnectors(true) + if (!accounts[0]) { + throw new Error('No accounts found') } - this.appKit?.setEIP6963Enabled(this.ethersConfig?.EIP6963) - - const emailEnabled = - options.features?.email === undefined - ? CoreConstantsUtil.DEFAULT_FEATURES.email - : options.features?.email - const socialsEnabled = options.features?.socials - ? options.features?.socials?.length > 0 - : CoreConstantsUtil.DEFAULT_FEATURES.socials - - if (emailEnabled || socialsEnabled) { - this.syncAuthConnector(this.options.projectId) + if (!connector?.type) { + throw new Error('Connector type not found') } - if (this.ethersConfig) { - this.checkActiveProviders(this.ethersConfig) + return { + address: accounts[0], + chainId: Number(chainId), + provider: selectedProvider, + type: connector.type, + id } - - this.syncRequestedNetworks(this.caipNetworks) - } - - public subscribeState(callback: (state: PublicStateControllerState) => void) { - return this.appKit?.subscribeState(state => callback(state)) } - public async disconnect() { - await this.connectionControllerClient?.disconnect() - } - - // -- Private ----------------------------------------------------------------- - private async revokeProviderPermissions(provider: Provider | CombinedProvider) { - try { - const permissions: { parentCapability: string }[] = await provider.request({ - method: 'wallet_getPermissions' - }) - const ethAccountsPermission = permissions.find( - permission => permission.parentCapability === 'eth_accounts' - ) - - if (ethAccountsPermission) { - await provider.request({ - method: 'wallet_revokePermissions', - params: [{ eth_accounts: {} }] - }) - } - } catch (error) { - // eslint-disable-next-line no-console - console.info('Could not revoke permissions from wallet. Disconnecting...', error) + public syncConnectors(options: AppKitOptions) { + this.ethersConfig = this.createEthersConfig(options) + if (this.ethersConfig?.EIP6963) { + this.listenInjectedConnector(true) } - } - - private getApprovedCaipNetworksData(): Promise<{ - supportsAllNetworks: boolean - approvedCaipNetworkIds: CaipNetworkId[] - }> { - return new Promise(resolve => { - const walletId = SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_ID) - if (!walletId) { - throw new Error('No wallet id found to get approved networks data') - } - - const providerConfigs = { - [ConstantsUtil.AUTH_CONNECTOR_ID]: { - supportsAllNetworks: true, - approvedCaipNetworkIds: PresetsUtil.WalletConnectRpcChainIds.map( - id => `${ConstantsUtil.EIP155}:${id}` - ) as CaipNetworkId[] - } - } - - const networkData = providerConfigs[walletId as unknown as keyof typeof providerConfigs] + const connectors = Object.keys(this.ethersConfig || {}).filter( + key => key !== 'metadata' && key !== 'EIP6963' + ) - if (networkData) { - resolve(networkData) - } else { - resolve({ - supportsAllNetworks: true, - approvedCaipNetworkIds: [] + connectors.forEach(connector => { + const key = connector === 'coinbase' ? 'coinbaseWalletSDK' : connector + if (this.namespace) { + this.addConnector({ + id: connector, + explorerId: PresetsUtil.ConnectorExplorerIds[key], + imageUrl: options?.connectorImages?.[key], + name: PresetsUtil.ConnectorNamesMap[key], + imageId: PresetsUtil.ConnectorImageIds[key], + type: PresetsUtil.ConnectorTypesMap[key] ?? 'EXTERNAL', + info: { rdns: key }, + chain: this.namespace, + chains: [], + provider: this.ethersConfig?.[connector as keyof ProviderType] as Provider }) } }) } - /** - * Checks the active providers and sets the provider. We call this when we initialize the adapter. - * @param config - The provider config - */ - private checkActiveProviders(config: ProviderType) { - const walletId = SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_ID) - const walletName = SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_NAME) + public async connectWalletConnect(onUri: (uri: string) => void) { + const connector = this.connectors.find(c => c.type === 'WALLET_CONNECT') - if (!walletId) { - return - } + const provider = connector?.provider as UniversalProvider - const providerConfigs = { - [ConstantsUtil.INJECTED_CONNECTOR_ID]: { - provider: config.injected - }, - [ConstantsUtil.COINBASE_SDK_CONNECTOR_ID]: { - provider: config.coinbase as unknown as ExternalProvider - }, - [ConstantsUtil.EIP6963_CONNECTOR_ID]: { - provider: this.EIP6963Providers.find(p => p.info.name === walletName)?.provider - } + if (!this.caipNetworks || !provider) { + throw new Error( + 'UniversalAdapter:connectWalletConnect - caipNetworks or provider is undefined' + ) } - const activeConfig = providerConfigs[walletId as unknown as keyof typeof providerConfigs] + provider.on('display_uri', (uri: string) => { + onUri(uri) + }) - if (activeConfig?.provider) { - this.setProvider(activeConfig.provider, walletId as ProviderIdType) - this.setupProviderListeners(activeConfig.provider, walletId as ProviderIdType) - } + const namespaces = WcHelpersUtil.createNamespaces(this.caipNetworks) + + await provider.connect({ optionalNamespaces: namespaces }) } - /** - * Sets the provider and updates the local storage. We call this when we connect with external providers or via checkActiveProviders function. - * @param provider - The provider to set - * @param providerId - The provider id - * @param name - The name of the provider - */ - private async setProvider(provider: Provider, providerId: ProviderIdType, name?: string) { - if (providerId === 'w3mAuth') { - this.setAuthProvider() - } else { - const walletId = providerId - SafeLocalStorage.setItem(SafeLocalStorageKeys.WALLET_ID, walletId) + private eip6963EventHandler(event: CustomEventInit) { + if (event.detail) { + const { info, provider } = event.detail + const existingConnector = this.connectors?.find(c => c.name === info?.name) + const coinbaseConnector = this.connectors?.find( + c => c.id === ConstantsUtil.COINBASE_SDK_CONNECTOR_ID + ) + const isCoinbaseDuplicated = + coinbaseConnector && + event.detail.info?.rdns === + ConstantsUtil.CONNECTOR_RDNS_MAP[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID] - if (name) { - SafeLocalStorage.setItem(SafeLocalStorageKeys.WALLET_NAME, name) - } + if (!existingConnector && !isCoinbaseDuplicated) { + const type = PresetsUtil.ConnectorTypesMap[ConstantsUtil.EIP6963_CONNECTOR_ID] - if (provider) { - const { addresses, chainId } = await EthersHelpersUtil.getUserInfo(provider) - const firstAddress = addresses?.[0] - const caipNetwork = this.caipNetworks.find(c => c.id === chainId) ?? this.caipNetworks[0] - const caipAddress = - `${this.chainNamespace}:${caipNetwork?.id}:${firstAddress}` as CaipAddress - - if (firstAddress && caipNetwork) { - this.appKit?.setCaipNetwork(caipNetwork) - this.appKit?.setCaipAddress(caipAddress, this.chainNamespace) - ProviderUtil.setProviderId('eip155', providerId) - ProviderUtil.setProvider('eip155', provider) - this.appKit?.setStatus('connected', this.chainNamespace) - this.appKit?.setAllAccounts( - addresses.map(address => ({ address, type: 'eoa' })), - this.chainNamespace - ) + if (type && this.namespace) { + this.addConnector({ + id: info?.rdns || '', + type, + imageUrl: info?.icon, + name: info?.name, + provider, + info, + chain: this.namespace, + chains: [] + }) } } } } - private async setAuthProvider() { - SafeLocalStorage.setItem(SafeLocalStorageKeys.WALLET_ID, ConstantsUtil.AUTH_CONNECTOR_ID) - - if (this.authProvider) { - this.appKit?.setLoading(true) - const { - address, - chainId, - smartAccountDeployed, - preferredAccountType, - accounts = [] - } = await this.authProvider.connect({ - chainId: Number(this.appKit?.getCaipNetwork()?.id || this.caipNetworks[0]?.id) - }) - - const { smartAccountEnabledNetworks } = - await this.authProvider.getSmartAccountEnabledNetworks() - - this.appKit?.setSmartAccountEnabledNetworks(smartAccountEnabledNetworks, this.chainNamespace) - if (address && chainId) { - this.appKit?.setAllAccounts( - accounts.length > 0 - ? accounts - : [{ address, type: preferredAccountType as 'eoa' | 'smartAccount' }], - this.chainNamespace - ) - this.appKit?.setStatus('connected', this.chainNamespace) - this.appKit?.setCaipAddress( - `${this.chainNamespace}:${chainId}:${address}`, - this.chainNamespace - ) - this.appKit?.setPreferredAccountType( - preferredAccountType as W3mFrameTypes.AccountType, - this.chainNamespace - ) - this.appKit?.setSmartAccountDeployed(Boolean(smartAccountDeployed), this.chainNamespace) - ProviderUtil.setProvider('eip155', this.authProvider as unknown as Provider) - ProviderUtil.setProviderId('eip155', ConstantsUtil.AUTH_CONNECTOR_ID as ProviderIdType) - this.setupProviderListeners(this.authProvider as unknown as Provider, 'w3mAuth') - this.watchModal() - } - this.appKit?.setLoading(false) + private listenInjectedConnector(enableEIP6963: boolean) { + if (typeof window !== 'undefined' && enableEIP6963) { + const handler = this.eip6963EventHandler.bind(this) + window.addEventListener(ConstantsUtil.EIP6963_ANNOUNCE_EVENT, handler) + window.dispatchEvent(new Event(ConstantsUtil.EIP6963_REQUEST_EVENT)) } } - private watchModal() { - if (this.authProvider) { - this.subscribeState(val => { - if (!val.open) { - this.authProvider?.rejectRpcRequests() - } - }) + public async connect({ + id, + type, + chainId + }: AdapterBlueprint.ConnectParams): Promise { + const connector = this.connectors.find(c => c.id === id) + const selectedProvider = connector?.provider as Provider + if (!selectedProvider) { + throw new Error('Provider not found') } - } - private setupProviderListeners(provider: Provider, providerId: ProviderIdType) { - const disconnectHandler = () => { - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_ID) - this.removeListeners(provider) - } + let accounts: string[] = [] - const accountsChangedHandler = (accounts: string[]) => { - const currentAccount = accounts?.[0] as CaipAddress | undefined - if (currentAccount) { - const chainId = this.appKit?.getCaipNetwork()?.id - const caipAddress = `${this.chainNamespace}:${chainId}:${currentAccount}` as CaipAddress - - this.appKit?.setCaipAddress(caipAddress, this.chainNamespace) - - if (providerId === ConstantsUtil.EIP6963_CONNECTOR_ID) { - this.appKit?.setAllAccounts( - accounts.map(address => ({ address, type: 'eoa' })), - this.chainNamespace - ) - } - } else { - if (providerId === ConstantsUtil.EIP6963_CONNECTOR_ID) { - this.appKit?.setAllAccounts([], this.chainNamespace) - } - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_ID) - this.appKit?.resetAccount(this.chainNamespace) - } - } - - const chainChangedHandler = (chainId: string) => { - const chainIdNumber = - typeof chainId === 'string' ? EthersHelpersUtil.hexStringToNumber(chainId) : Number(chainId) - const caipNetwork = this.caipNetworks.find(c => c.id === chainIdNumber) - const currentCaipNetwork = this.appKit?.getCaipNetwork() - - if (!currentCaipNetwork || currentCaipNetwork?.id !== caipNetwork?.id) { - this.appKit?.setCaipNetwork(caipNetwork) - } - } + if (type === 'AUTH') { + const { address } = await (selectedProvider as unknown as W3mFrameProvider).connect({ + chainId + }) - if (providerId === ConstantsUtil.AUTH_CONNECTOR_ID) { - this.setupAuthListeners(provider as unknown as W3mFrameProvider) + accounts = [address] } else { - provider.on('disconnect', disconnectHandler) - provider.on('accountsChanged', accountsChangedHandler) - provider.on('chainChanged', chainChangedHandler) - } + accounts = await selectedProvider.request({ + method: 'eth_requestAccounts' + }) - this.providerHandlers = { - disconnect: disconnectHandler, - accountsChanged: accountsChangedHandler, - chainChanged: chainChangedHandler + this.listenProviderEvents(selectedProvider) } - } - private providerHandlers: { - disconnect: () => void - accountsChanged: (accounts: string[]) => void - chainChanged: (networkId: string) => void - } | null = null - - private removeListeners(provider: Provider) { - if (this.providerHandlers) { - provider.removeListener('disconnect', this.providerHandlers.disconnect) - provider.removeListener('accountsChanged', this.providerHandlers.accountsChanged) - provider.removeListener('chainChanged', this.providerHandlers.chainChanged) - this.providerHandlers = null + return { + address: accounts[0] as `0x${string}`, + chainId: Number(chainId), + provider: selectedProvider, + type: type as ConnectorType, + id } } - private setupAuthListeners(authProvider: W3mFrameProvider) { - authProvider.onRpcRequest(request => { - if (W3mFrameHelpers.checkIfRequestExists(request)) { - if (!W3mFrameHelpers.checkIfRequestIsSafe(request)) { - this.appKit?.handleUnsafeRPCRequest() - } - } else { - this.handleInvalidAuthRequest() - } - }) - - authProvider.onRpcError(() => this.handleAuthRpcError()) - authProvider.onRpcSuccess((_, request) => this.handleAuthRpcSuccess(_, request)) - authProvider.onNotConnected(() => this.handleAuthNotConnected()) - authProvider.onConnect(({ preferredAccountType }) => - this.handleAuthIsConnected(preferredAccountType) - ) - authProvider.onSetPreferredAccount(({ address, type }) => { - if (address) { - this.handleAuthSetPreferredAccount(address, type) - } - }) - } + public override async reconnect(params: AdapterBlueprint.ConnectParams): Promise { + const { id, chainId } = params - private handleInvalidAuthRequest() { - this.appKit?.open() - setTimeout(() => { - this.appKit?.showErrorMessage(W3mFrameRpcConstants.RPC_METHOD_NOT_ALLOWED_UI_MESSAGE) - }, 300) - } + const connector = this.connectors.find(c => c.id === id) - private handleAuthRpcError() { - if (this.appKit?.isOpen()) { - if (this.appKit?.isTransactionStackEmpty()) { - this.appKit?.close() - } else { - this.appKit?.popTransactionStack(true) - } + if (connector && connector.type === 'AUTH' && chainId) { + await (connector.provider as W3mFrameProvider).connect({ chainId }) } } - private handleAuthRpcSuccess(_: W3mFrameTypes.FrameEvent, request: W3mFrameTypes.RPCRequest) { - const isSafeRequest = W3mFrameHelpers.checkIfRequestIsSafe(request) - if (isSafeRequest) { - return + public async disconnect(params: AdapterBlueprint.DisconnectParams): Promise { + if (!params.provider || !params.providerType) { + throw new Error('Provider or providerType not provided') } - if (this.appKit?.isTransactionStackEmpty()) { - this.appKit?.close() - } else { - this.appKit?.popTransactionStack() + switch (params.providerType) { + case 'WALLET_CONNECT': + if ((params.provider as UniversalProvider).session) { + ;(params.provider as UniversalProvider).disconnect() + } + break + case 'AUTH': + await params.provider.disconnect() + break + case 'ANNOUNCED': + case 'EXTERNAL': + await this.revokeProviderPermissions(params.provider as Provider) + break + default: + throw new Error('Unsupported provider type') } } - private handleAuthNotConnected() { - this.appKit?.setCaipAddress(undefined, this.chainNamespace) - } - - private handleAuthIsConnected(preferredAccountType: string | undefined) { - const activeNamespace = this.appKit?.getActiveChainNamespace() + public async getBalance( + params: AdapterBlueprint.GetBalanceParams + ): Promise { + const caipNetwork = this.caipNetworks?.find((c: CaipNetwork) => c.id === params.chainId) - if (activeNamespace !== this.chainNamespace) { - return - } + if (caipNetwork) { + const jsonRpcProvider = new ethers.providers.JsonRpcProvider( + caipNetwork.rpcUrls.default.http[0], + { + chainId: caipNetwork.id as number, + name: caipNetwork.name + } + ) - this.appKit?.setPreferredAccountType( - preferredAccountType as W3mFrameTypes.AccountType, - this.chainNamespace - ) - } + const balance = await jsonRpcProvider.getBalance(params.address) + const formattedBalance = formatEther(balance) - private handleAuthSetPreferredAccount(address: string, type: string) { - if (!address) { - return + return { balance: formattedBalance, symbol: caipNetwork.nativeCurrency.symbol } } - this.appKit?.setLoading(true) - const chainId = this.appKit?.getCaipNetwork()?.id - this.appKit?.setCaipAddress(`${this.chainNamespace}:${chainId}:${address}`, this.chainNamespace) - this.appKit?.setStatus('connected', this.chainNamespace) - this.appKit?.setPreferredAccountType(type as W3mFrameTypes.AccountType, this.chainNamespace) - - this.syncAccount({ - address: address as Address - }).then(() => this.appKit?.setLoading(false)) - this.appKit?.setLoading(false) - } - - private async syncReownName(address: Address) { - try { - const registeredWcNames = await this.appKit?.getReownName(address) - if (registeredWcNames?.[0]) { - const wcName = registeredWcNames[0] - this.appKit?.setProfileName(wcName.name, this.chainNamespace) - } else { - this.appKit?.setProfileName(null, this.chainNamespace) - } - } catch { - this.appKit?.setProfileName(null, this.chainNamespace) - } + return { balance: '', symbol: '' } } - /** - * Syncs the account state depending on the given parameters. We call this in different conditions like when caipNetwork or caipAddress changes, when the user switches account or network. - * @param param0 - The address and caipNetwork. Both are optional. - */ - private async syncAccount({ - address, - caipNetwork - }: { - address?: Address - caipNetwork?: CaipNetwork - }) { - const currentCaipNetwork = caipNetwork || this.appKit?.getCaipNetwork() - const preferredAccountType = this.appKit?.getPreferredAccountType() - const isEipNetwork = currentCaipNetwork?.chainNamespace === CommonConstantsUtil.CHAIN.EVM - const caipNetworkId = currentCaipNetwork?.id as CaipNetworkId - - if (address) { - if (isEipNetwork) { - this.appKit?.setPreferredAccountType(preferredAccountType, this.chainNamespace) - this.appKit?.setCaipAddress( - `${this.chainNamespace}:${caipNetworkId}:${address}`, - this.chainNamespace - ) - - this.syncConnectedWalletInfo() - - if (currentCaipNetwork?.blockExplorers) { - this.appKit?.setAddressExplorerUrl( - `${currentCaipNetwork.blockExplorers.default.url}/address/${address}`, - this.chainNamespace - ) - } + public async getProfile( + params: AdapterBlueprint.GetProfileParams + ): Promise { + if (params.chainId === 1) { + const ensProvider = new ethers.providers.InfuraProvider('mainnet') + const name = await ensProvider.lookupAddress(params.address) + const avatar = await ensProvider.getAvatar(params.address) - await Promise.all([ - this.syncProfile(address), - this.appKit?.setApprovedCaipNetworksData(this.chainNamespace) - ]) - } - } else { - this.appKit?.resetWcConnection() - this.appKit?.resetNetwork(this.chainNamespace) - this.appKit?.setAllAccounts([], this.chainNamespace) + return { profileName: name || undefined, profileImage: avatar || undefined } } - } - private async syncProfile(address: Address) { - const caipNetwork = this.appKit?.getCaipNetwork() + return { profileName: undefined, profileImage: undefined } + } - try { - const identity = await this.appKit?.fetchIdentity({ - address - }) - const name = identity?.name - const avatar = identity?.avatar + private providerHandlers: { + disconnect: () => void + accountsChanged: (accounts: string[]) => void + chainChanged: (chainId: string) => void + } | null = null - this.appKit?.setProfileName(name, this.chainNamespace) - this.appKit?.setProfileImage(avatar, this.chainNamespace) + private listenProviderEvents(provider: Provider | CombinedProvider) { + const disconnectHandler = () => { + this.removeProviderListeners(provider) + this.emit('disconnect') + } - if (!name) { - await this.syncReownName(address) - } - } catch { - if (caipNetwork?.id === 1) { - const ensProvider = new ethers.providers.InfuraProvider('mainnet') - const name = await ensProvider.lookupAddress(address) - const avatar = await ensProvider.getAvatar(address) - - if (name) { - this.appKit?.setProfileName(name, this.chainNamespace) - } else { - await this.syncReownName(address) - } - if (avatar) { - this.appKit?.setProfileImage(avatar, this.chainNamespace) - } - } else { - await this.syncReownName(address) - this.appKit?.setProfileImage(null, this.chainNamespace) + const accountsChangedHandler = (accounts: string[]) => { + if (accounts.length > 0) { + this.emit('accountChanged', { + address: accounts[0] as `0x${string}` + }) } } - } - - private async syncBalance(address: Address, caipNetwork: CaipNetwork) { - const isExistingNetwork = this.appKit - ?.getCaipNetworks(caipNetwork.chainNamespace) - .find(network => network.id === caipNetwork.id) - const isEVMNetwork = caipNetwork.chainNamespace === CommonConstantsUtil.CHAIN.EVM - if (caipNetwork && isExistingNetwork && isEVMNetwork) { - const jsonRpcProvider = new ethers.providers.JsonRpcProvider( - caipNetwork.rpcUrls.default.http[0], - { - chainId: caipNetwork.id as number, - name: caipNetwork.name - } - ) - - if (jsonRpcProvider) { - const balance = await jsonRpcProvider.getBalance(address) - const formattedBalance = ethers.utils.formatEther(balance) + const chainChangedHandler = (chainId: string) => { + const chainIdNumber = + typeof chainId === 'string' ? EthersHelpersUtil.hexStringToNumber(chainId) : Number(chainId) - this.appKit?.setBalance( - formattedBalance, - caipNetwork.nativeCurrency.symbol, - this.chainNamespace - ) - } + this.emit('switchNetwork', { chainId: chainIdNumber }) } - } - - private syncConnectedWalletInfo() { - const currentActiveWallet = SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_ID) - const providerType = ProviderUtil.state.providerIds['eip155'] - if (providerType === ConstantsUtil.EIP6963_CONNECTOR_ID) { - if (currentActiveWallet) { - const currentProvider = this.EIP6963Providers.find( - provider => provider.info.name === currentActiveWallet - ) + provider.on('disconnect', disconnectHandler) + provider.on('accountsChanged', accountsChangedHandler) + provider.on('chainChanged', chainChangedHandler) - if (currentProvider) { - this.appKit?.setConnectedWalletInfo({ ...currentProvider.info }, this.chainNamespace) - } - } - } else if (providerType === ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID) { - const provider = ProviderUtil.getProvider('eip155') - - if (provider?.session) { - this.appKit?.setConnectedWalletInfo( - { - ...provider.session.peer.metadata, - name: provider.session.peer.metadata.name, - icon: provider.session.peer.metadata.icons?.[0] - }, - this.chainNamespace - ) - } - } else if (providerType === ConstantsUtil.COINBASE_SDK_CONNECTOR_ID) { - const connector = this.appKit - ?.getConnectors() - .find(c => c.id === ConstantsUtil.COINBASE_SDK_CONNECTOR_ID) - - this.appKit?.setConnectedWalletInfo( - { name: 'Coinbase Wallet', icon: this.appKit?.getConnectorImage(connector) }, - this.chainNamespace - ) - } else if (currentActiveWallet) { - this.appKit?.setConnectedWalletInfo({ name: currentActiveWallet }, this.chainNamespace) + this.providerHandlers = { + disconnect: disconnectHandler, + accountsChanged: accountsChangedHandler, + chainChanged: chainChangedHandler } } - private syncRequestedNetworks(caipNetworks: CaipNetwork[]) { - const uniqueChainNamespaces = [ - ...new Set(caipNetworks.map(caipNetwork => caipNetwork.chainNamespace)) - ] - uniqueChainNamespaces.forEach(chainNamespace => { - this.appKit?.setRequestedCaipNetworks( - caipNetworks.filter(caipNetwork => caipNetwork.chainNamespace === chainNamespace), - chainNamespace - ) - }) + private removeProviderListeners(provider: Provider | CombinedProvider) { + if (this.providerHandlers) { + provider.removeListener('disconnect', this.providerHandlers.disconnect) + provider.removeListener('accountsChanged', this.providerHandlers.accountsChanged) + provider.removeListener('chainChanged', this.providerHandlers.chainChanged) + this.providerHandlers = null + } } - public async switchNetwork(caipNetwork: CaipNetwork) { - async function requestSwitchNetwork(provider: Provider) { + public async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise { + const { caipNetwork, provider, providerType } = params + if (providerType === 'WALLET_CONNECT') { + ;(provider as UniversalProvider).setDefaultChain(String(`eip155:${String(caipNetwork.id)}`)) + } else if (providerType === 'AUTH') { + const authProvider = provider as W3mFrameProvider + await authProvider.switchNetwork(caipNetwork.id) + await authProvider.connect({ + chainId: caipNetwork.id + }) + } else { try { - await provider.request({ + await (provider as Provider).request({ method: 'wallet_switchEthereumChain', params: [{ chainId: EthersHelpersUtil.numberToHexString(caipNetwork.id) }] }) @@ -1067,167 +510,40 @@ export class Ethers5Adapter { switchError?.data?.originalError?.code === WcConstantsUtil.ERROR_CODE_UNRECOGNIZED_CHAIN_ID ) { - await EthersHelpersUtil.addEthereumChain(provider, caipNetwork) - } else { + await EthersHelpersUtil.addEthereumChain(provider as Provider, caipNetwork) + } else if ( + providerType === 'ANNOUNCED' || + providerType === 'EXTERNAL' || + providerType === 'INJECTED' + ) { throw new Error('Chain is not supported') } } } - - const provider = ProviderUtil.getProvider('eip155') - const providerType = ProviderUtil.state.providerIds['eip155'] - - if (provider) { - switch (providerType) { - case ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID: - this.appKit?.universalAdapter?.networkControllerClient.switchCaipNetwork(caipNetwork) - break - case ConstantsUtil.INJECTED_CONNECTOR_ID: - case ConstantsUtil.EIP6963_CONNECTOR_ID: - case ConstantsUtil.COINBASE_SDK_CONNECTOR_ID: - if (provider) { - await requestSwitchNetwork(provider as Provider) - } - break - case ConstantsUtil.AUTH_CONNECTOR_ID: - if (this.authProvider) { - try { - this.appKit?.setLoading(true) - const { chainId } = await this.authProvider.switchNetwork(caipNetwork.id as number) - const { address, preferredAccountType } = await this.authProvider.connect({ - chainId: caipNetwork.id as number | undefined - }) - const caipAddress = `${this.chainNamespace}:${chainId}:${address}` - - this.appKit?.setCaipNetwork(caipNetwork) - this.appKit?.setCaipAddress(caipAddress as CaipAddress, this.chainNamespace) - this.appKit?.setPreferredAccountType( - preferredAccountType as W3mFrameTypes.AccountType, - this.chainNamespace - ) - await this.syncAccount({ address: address as Address }) - this.appKit?.setLoading(false) - } catch { - throw new Error('Switching chain failed') - } finally { - this.appKit?.setLoading(false) - } - } - break - default: - throw new Error('Unsupported provider type') - } - } } - private syncConnectors(config: ProviderType) { - const w3mConnectors: Connector[] = [] - - if (config.injected) { - const injectedConnectorType = - PresetsUtil.ConnectorTypesMap[ConstantsUtil.INJECTED_CONNECTOR_ID] - if (injectedConnectorType) { - w3mConnectors.push({ - id: ConstantsUtil.INJECTED_CONNECTOR_ID, - explorerId: PresetsUtil.ConnectorExplorerIds[ConstantsUtil.INJECTED_CONNECTOR_ID], - imageId: PresetsUtil.ConnectorImageIds[ConstantsUtil.INJECTED_CONNECTOR_ID], - imageUrl: this.options?.connectorImages?.[ConstantsUtil.INJECTED_CONNECTOR_ID], - name: PresetsUtil.ConnectorNamesMap[ConstantsUtil.INJECTED_CONNECTOR_ID], - type: injectedConnectorType, - chain: this.chainNamespace - }) - } - } - - if (config.coinbase) { - w3mConnectors.push({ - id: ConstantsUtil.COINBASE_SDK_CONNECTOR_ID, - explorerId: PresetsUtil.ConnectorExplorerIds[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID], - imageId: PresetsUtil.ConnectorImageIds[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID], - imageUrl: this.options?.connectorImages?.[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID], - name: PresetsUtil.ConnectorNamesMap[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID], - type: 'EXTERNAL', - chain: this.chainNamespace - }) - } - - this.appKit?.setConnectors(w3mConnectors) + public getWalletConnectProvider(): AdapterBlueprint.GetWalletConnectProviderResult { + return this.connectors.find(c => c.type === 'WALLET_CONNECT')?.provider as UniversalProvider } - private async syncAuthConnector(projectId: string, bypassWindowCheck = false) { - if (bypassWindowCheck || typeof window !== 'undefined') { - this.authProvider = W3mFrameProviderSingleton.getInstance({ - projectId, - onTimeout: () => { - AlertController.open(ErrorUtil.ALERT_ERRORS.SOCIALS_TIMEOUT, 'error') - } - }) - - this.appKit?.addConnector({ - id: ConstantsUtil.AUTH_CONNECTOR_ID, - type: 'AUTH', - name: 'Auth', - provider: this.authProvider, - chain: this.chainNamespace + private async revokeProviderPermissions(provider: Provider | CombinedProvider) { + try { + const permissions: { parentCapability: string }[] = await provider.request({ + method: 'wallet_getPermissions' }) - - this.appKit?.setLoading(true) - const isLoginEmailUsed = this.authProvider.getLoginEmailUsed() - this.appKit?.setLoading(isLoginEmailUsed) - if (isLoginEmailUsed) { - const { isConnected } = await this.authProvider.isConnected() - if (isConnected) { - await this.setAuthProvider() - } else { - this.appKit?.setLoading(false) - } - } - } - } - - private eip6963EventHandler(event: CustomEventInit) { - if (event.detail) { - const { info, provider } = event.detail - const connectors = this.appKit?.getConnectors() - const existingConnector = connectors?.find(c => c.name === info.name) - const coinbaseConnector = connectors?.find( - c => c.id === ConstantsUtil.COINBASE_SDK_CONNECTOR_ID + const ethAccountsPermission = permissions.find( + permission => permission.parentCapability === 'eth_accounts' ) - const isCoinbaseDuplicated = - coinbaseConnector && - event.detail.info.rdns === - ConstantsUtil.CONNECTOR_RDNS_MAP[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID] - if (!existingConnector && !isCoinbaseDuplicated) { - const type = PresetsUtil.ConnectorTypesMap[ConstantsUtil.EIP6963_CONNECTOR_ID] - if (type) { - this.appKit?.addConnector({ - id: ConstantsUtil.EIP6963_CONNECTOR_ID, - type, - imageUrl: - info.icon ?? this.options?.connectorImages?.[ConstantsUtil.EIP6963_CONNECTOR_ID], - name: info.name, - provider, - info, - chain: this.chainNamespace - }) - - const eip6963ProviderObj = { - provider, - info - } - - this.EIP6963Providers.push(eip6963ProviderObj) - } + if (ethAccountsPermission) { + await provider.request({ + method: 'wallet_revokePermissions', + params: [{ eth_accounts: {} }] + }) } - } - } - - private listenConnectors(enableEIP6963: boolean) { - if (typeof window !== 'undefined' && enableEIP6963) { - const handler = this.eip6963EventHandler.bind(this) - window.addEventListener(ConstantsUtil.EIP6963_ANNOUNCE_EVENT, handler) - window.dispatchEvent(new Event(ConstantsUtil.EIP6963_REQUEST_EVENT)) + } catch (error) { + // eslint-disable-next-line no-console + console.info('Could not revoke permissions from wallet. Disconnecting...', error) } } } diff --git a/packages/adapters/ethers5/src/index.ts b/packages/adapters/ethers5/src/index.ts index 022aad0c3b..b80201838f 100644 --- a/packages/adapters/ethers5/src/index.ts +++ b/packages/adapters/ethers5/src/index.ts @@ -4,5 +4,4 @@ export { Ethers5Adapter } from './client.js' export * from '@reown/appkit-utils/ethers' // -- Types -export type { AdapterOptions } from './client.js' export type { ProviderType } from '@reown/appkit-utils/ethers' diff --git a/packages/adapters/ethers5/src/tests/client.test.ts b/packages/adapters/ethers5/src/tests/client.test.ts index 6800d0cf24..2b12848a42 100644 --- a/packages/adapters/ethers5/src/tests/client.test.ts +++ b/packages/adapters/ethers5/src/tests/client.test.ts @@ -1,969 +1,325 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { describe, it, expect, beforeEach, vi } from 'vitest' import { Ethers5Adapter } from '../client' -import type { EIP6963ProviderDetail } from '../client' -import { mockOptions } from './mocks/Options' -import { mockCreateEthersConfig } from './mocks/EthersConfig' -import mockAppKit from './mocks/AppKit' -import { mockAuthConnector } from './mocks/AuthConnector' -import { EthersHelpersUtil, type ProviderType } from '@reown/appkit-utils/ethers' -import { CaipNetworksUtil, ConstantsUtil } from '@reown/appkit-utils' -import { - arbitrum as AppkitArbitrum, - mainnet as AppkitMainnet, - polygon as AppkitPolygon, - optimism as AppkitOptimism, - bsc as AppkitBsc, - harmonyOne as AppkitHarmonyOne -} from '@reown/appkit/networks' -import { ProviderUtil, type ProviderIdType } from '@reown/appkit/store' -import { SafeLocalStorage, SafeLocalStorageKeys } from '@reown/appkit-common' -import { type BlockchainApiLookupEnsName } from '@reown/appkit' -import { ethers } from 'ethers' - -import type { CaipNetwork, ChainNamespace } from '@reown/appkit-common' - -const [mainnet, arbitrum, polygon, optimism, bsc] = CaipNetworksUtil.extendCaipNetworks( - [AppkitMainnet, AppkitArbitrum, AppkitPolygon, AppkitOptimism, AppkitBsc], - { customNetworkImageUrls: mockOptions.chainImages, projectId: '1234' } -) as [CaipNetwork, CaipNetwork, CaipNetwork, CaipNetwork, CaipNetwork] - -const caipNetworks = [mainnet, arbitrum, polygon] as [CaipNetwork, ...CaipNetwork[]] - -vi.mock('@reown/appkit-wallet', () => ({ - W3mFrameProvider: vi.fn().mockImplementation(() => mockAuthConnector), - W3mFrameHelpers: { - checkIfRequestExists: vi.fn(), - checkIfRequestIsSafe: vi.fn() - }, - W3mFrameRpcConstants: { - RPC_METHOD_NOT_ALLOWED_UI_MESSAGE: 'RPC method not allowed' - } -})) - -vi.mock('@reown/appkit-utils', async importOriginal => { - const actual = await importOriginal() - const INJECTED_CONNECTOR_ID = 'injected' - const COINBASE_SDK_CONNECTOR_ID = 'coinbaseWallet' - const EIP6963_CONNECTOR_ID = 'eip6963' - const WALLET_CONNECT_CONNECTOR_ID = 'walletConnect' - const AUTH_CONNECTOR_ID = 'w3mAuth' +import { CaipNetworksUtil } from '@reown/appkit-utils' +import type { Provider } from '@reown/appkit-core' +import type { W3mFrameProvider } from '@reown/appkit-wallet' +import UniversalProvider from '@walletconnect/universal-provider' +import { providers } from 'ethers' +import { mainnet } from '@reown/appkit/networks' +import { Ethers5Methods } from '../utils/Ethers5Methods' + +// Mock external dependencies +vi.mock('ethers', async importOriginal => { + const actual = await importOriginal() return { - // @ts-expect-error - actual is not typed ...actual, - PresetsUtil: { - ConnectorTypesMap: { - [INJECTED_CONNECTOR_ID]: 'INJECTED', - [COINBASE_SDK_CONNECTOR_ID]: 'EXTERNAL', - [EIP6963_CONNECTOR_ID]: 'INJECTED' - }, - ConnectorExplorerIds: { - [INJECTED_CONNECTOR_ID]: 'injected-explorer', - [COINBASE_SDK_CONNECTOR_ID]: 'coinbase-explorer', - [EIP6963_CONNECTOR_ID]: 'eip6963-explorer' - }, - ConnectorImageIds: { - [INJECTED_CONNECTOR_ID]: 'injected-image', - [COINBASE_SDK_CONNECTOR_ID]: 'coinbase-image', - [EIP6963_CONNECTOR_ID]: 'eip6963-image' - }, - ConnectorNamesMap: { - [INJECTED_CONNECTOR_ID]: 'Injected', - [COINBASE_SDK_CONNECTOR_ID]: 'Coinbase', - [EIP6963_CONNECTOR_ID]: 'EIP6963' - }, - WalletConnectRpcChainIds: [1, 137, 10, 42161, 56, 43114, 250, 25, 1313161554, 1284] - }, - ConstantsUtil: { - INJECTED_CONNECTOR_ID, - COINBASE_SDK_CONNECTOR_ID, - EIP6963_CONNECTOR_ID, - WALLET_CONNECT_CONNECTOR_ID, - AUTH_CONNECTOR_ID, - EIP155: 'eip155' - }, - HelpersUtil: { - getCaipTokens: vi.fn().mockReturnValue([]) + formatEther: vi.fn(() => '1.5'), + providers: { + InfuraProvider: vi.fn(() => ({ + lookupAddress: vi.fn(), + getAvatar: vi.fn() + })), + JsonRpcProvider: vi.fn(() => ({ + getBalance: vi.fn() + })) } } }) -vi.mock('@reown/appkit/store', () => ({ - ProviderUtil: { - setProvider: vi.fn(), - setProviderId: vi.fn(), - state: { - providerIds: {} - }, - getProvider: vi.fn() +vi.mock('../utils/Ethers5Methods', () => ({ + Ethers5Methods: { + signMessage: vi.fn(), + sendTransaction: vi.fn(), + writeContract: vi.fn(), + estimateGas: vi.fn(), + getEnsAddress: vi.fn(), + parseUnits: vi.fn(), + formatUnits: vi.fn(), + hexStringToNumber: vi.fn(hex => parseInt(hex, 16)), + numberToHexString: vi.fn(num => `0x${num.toString(16)}`) } })) -vi.mock('ethers', async () => { - return { - ethers: { - providers: { - InfuraProvider: vi.fn(), - JsonRpcProvider: vi.fn() - }, - utils: { - formatEther: vi.fn().mockReturnValue('1.0'), - parseUnits: vi.fn(), - formatUnits: vi.fn() - } - } - } -}) - -vi.mock('@reown/appkit-common', async importOriginal => { - const actual = await importOriginal() - return { - // @ts-expect-error - actual is not typed - ...actual, - SafeLocalStorage: { - getItem: vi.fn(key => { - const values = { - '@appkit/wallet_id': 'injected' - } - return values[key as keyof typeof values] - }), - setItem: vi.fn(), - removeItem: vi.fn() - } - } +const mockProvider = { + request: vi.fn(), + on: vi.fn(), + removeListener: vi.fn() +} as unknown as Provider + +const mockWalletConnectProvider = { + connect: vi.fn(), + disconnect: vi.fn(), + on: vi.fn(), + removeListener: vi.fn(), + session: true, + setDefaultChain: vi.fn() +} as unknown as UniversalProvider + +const mockAuthProvider = { + connect: vi.fn(), + disconnect: vi.fn(), + switchNetwork: vi.fn() +} as unknown as W3mFrameProvider + +const mockNetworks = [mainnet] +const mockCaipNetworks = CaipNetworksUtil.extendCaipNetworks(mockNetworks, { + projectId: 'test-project-id', + customNetworkImageUrls: {} }) -describe('EthersAdapter', () => { - let client: Ethers5Adapter +describe('Ethers5Adapter', () => { + let adapter: Ethers5Adapter beforeEach(() => { vi.clearAllMocks() - const ethersConfig = mockCreateEthersConfig() - client = new Ethers5Adapter() - vi.spyOn(client as any, 'createEthersConfig').mockImplementation(() => ({ - metadata: ethersConfig.metadata, - injected: ethersConfig.injected - })) - const optionsWithEthersConfig = { - ...mockOptions, - networks: caipNetworks, - defaultNetwork: undefined, - ethersConfig - } - client.construct(mockAppKit, optionsWithEthersConfig) - }) - - afterEach(() => { - vi.clearAllMocks() + adapter = new Ethers5Adapter() }) - describe('EthersClient - Initialization', () => { - it('should initialize with default values', () => { - expect(client.chainNamespace).toBe('eip155') - expect(client.adapterType).toBe('ethers') + describe('Ethers5Adapter -constructor', () => { + it('should initialize with correct parameters', () => { + expect(adapter.adapterType).toBe('ethers') + expect(adapter.namespace).toBe('eip155') }) + }) - it('should set caipNetworks to provided caipNetworks options', () => { - expect(client.caipNetworks).toEqual(caipNetworks) - }) + describe('Ethers5Adapter - signMessage', () => { + it('should sign message successfully', async () => { + const mockSignature = '0xmocksignature' + vi.mocked(Ethers5Methods.signMessage).mockResolvedValue(mockSignature) - it('should set chain images', () => { - Object.entries(mockOptions.chainImages!).map(([networkId, imageUrl]) => { - const caipNetwork = client.caipNetworks.find( - caipNetwork => caipNetwork.id === Number(networkId) - ) - expect(caipNetwork).toBeDefined() - expect(caipNetwork?.assets?.imageUrl).toEqual(imageUrl) + const result = await adapter.signMessage({ + message: 'Hello', + address: '0x123', + provider: mockProvider }) - }) - it('should set defaultNetwork to first caipNetwork option', () => { - expect(client.defaultCaipNetwork).toEqual(mainnet) + expect(result.signature).toBe(mockSignature) }) - it('should create ethers config', () => { - expect(client['ethersConfig']).toBeDefined() + it('should throw error when provider is undefined', async () => { + await expect( + adapter.signMessage({ + message: 'Hello', + address: '0x123' + }) + ).rejects.toThrow('Provider is undefined') }) }) - describe('EthersClient - Networks', () => { - const mockProvider = { - request: vi.fn(), - on: vi.fn(), - removeListener: vi.fn() - } - - beforeEach(() => { - vi.spyOn(ProviderUtil, 'getProvider').mockReturnValue(mockProvider) - vi.spyOn(ProviderUtil.state, 'providerIds', 'get').mockReturnValue({ - eip155: 'injected', - solana: undefined, - polkadot: undefined + describe('Ethers5Adapter -sendTransaction', () => { + it('should send transaction successfully', async () => { + const mockTxHash = '0xtxhash' + vi.mocked(Ethers5Methods.sendTransaction).mockResolvedValue(mockTxHash) + + const result = await adapter.sendTransaction({ + value: BigInt(1000), + to: '0x456', + data: '0x', + gas: BigInt(21000), + gasPrice: BigInt(2000000000), + address: '0x123', + provider: mockProvider, + caipNetwork: mockCaipNetworks[0] }) - }) - - it('should switch network for injected provider', async () => { - mockProvider.request.mockResolvedValueOnce(null) - await client.switchNetwork(polygon) - - expect(mockProvider.request).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'wallet_switchEthereumChain', - params: expect.arrayContaining([ - expect.objectContaining({ - chainId: expect.stringMatching(/^0x/) - }) - ]) - }) - ) + expect(result.hash).toBe(mockTxHash) }) - it('should add network if not recognized by wallet', async () => { - const switchError = { code: 4902 } - mockProvider.request.mockRejectedValueOnce(switchError) - mockProvider.request.mockResolvedValueOnce(null) - - await client.switchNetwork(arbitrum) - - expect(mockProvider.request).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'wallet_addEthereumChain', - params: expect.arrayContaining([ - expect.objectContaining({ - chainId: expect.stringMatching(/^0x/) - }) - ]) + it('should throw error when provider is undefined', async () => { + await expect( + adapter.sendTransaction({ + value: BigInt(1000), + to: '0x456', + data: '0x', + gas: BigInt(21000), + gasPrice: BigInt(2000000000), + address: '0x123' }) - ) - }) - - it('should throw error if switching fails', async () => { - const switchError = new Error('User rejected the request') - mockProvider.request.mockRejectedValueOnce(switchError) - - await expect(client.switchNetwork(bsc)).rejects.toThrow('Chain is not supported') + ).rejects.toThrow('Provider is undefined') }) + }) - it('should use universal adapter for WalletConnect', async () => { - vi.spyOn(ProviderUtil.state, 'providerIds', 'get').mockReturnValue({ - eip155: 'walletConnect', - solana: undefined, - polkadot: undefined + describe('Ethers5Adapter -writeContract', () => { + it('should write contract successfully', async () => { + const mockTxHash = '0xtxhash' + vi.mocked(Ethers5Methods.writeContract).mockResolvedValue(mockTxHash) + + const result = await adapter.writeContract({ + abi: [], + method: 'transfer', + caipAddress: 'eip155:1:0x123', + fromAddress: '0x123', + receiverAddress: '0x456', + tokenAmount: BigInt(1000), + tokenAddress: '0x789', + provider: mockProvider, + caipNetwork: mockCaipNetworks[0] }) - await client.switchNetwork(optimism) - - expect( - mockAppKit.universalAdapter?.networkControllerClient.switchCaipNetwork - ).toHaveBeenCalledWith(optimism) - }) - - it('should set requested CAIP networks for each unique chain namespace', () => { - const caipNetworks = [mainnet, arbitrum, polygon] - - client['syncRequestedNetworks'](caipNetworks) - - expect(mockAppKit.setRequestedCaipNetworks).toHaveBeenCalledWith( - [mainnet, arbitrum, polygon], - 'eip155' - ) + expect(result.hash).toBe(mockTxHash) }) }) - describe('EthersClient - Auth Connector', () => { - it('should sync auth connector', async () => { - const projectId = 'test-project-id' - - await client['syncAuthConnector'](projectId, true) - - expect(mockAppKit.addConnector).toHaveBeenCalledWith({ - id: ConstantsUtil.AUTH_CONNECTOR_ID, - type: 'AUTH', - name: 'Auth', - provider: expect.any(Object), - chain: 'eip155' - }) - expect(mockAppKit.setLoading).toHaveBeenCalledWith(false) - }) - - describe('Auth Connector Handle Requests', () => { - beforeEach(() => { - client['appKit'] = mockAppKit - }) - - it('should handle RPC request correctly when modal is closed', () => { - vi.spyOn(mockAppKit, 'isOpen').mockReturnValue(false) - mockAppKit['handleUnsafeRPCRequest']() - expect(mockAppKit.open).toHaveBeenCalledWith({ view: 'ApproveTransaction' }) - }) - - it('should handle RPC request correctly when modal is open and transaction stack is not empty', () => { - vi.spyOn(mockAppKit, 'isOpen').mockReturnValue(true) - vi.spyOn(mockAppKit, 'isTransactionStackEmpty').mockReturnValue(false) - vi.spyOn(mockAppKit, 'isTransactionShouldReplaceView').mockReturnValue(true) - mockAppKit['handleUnsafeRPCRequest']() - expect(mockAppKit.redirect).toHaveBeenCalledWith('ApproveTransaction') - }) - - it('should handle invalid auth request', () => { - vi.useFakeTimers() - client['handleInvalidAuthRequest']() - expect(mockAppKit.open).toHaveBeenCalled() - vi.advanceTimersByTime(300) - expect(mockAppKit.showErrorMessage).toHaveBeenCalledWith('RPC method not allowed') - vi.useRealTimers() - }) - - it('should handle auth RPC error when modal is open and transaction stack is empty', () => { - vi.spyOn(mockAppKit, 'isOpen').mockReturnValue(true) - vi.spyOn(mockAppKit, 'isTransactionStackEmpty').mockReturnValue(true) - client['handleAuthRpcError']() - expect(mockAppKit.close).toHaveBeenCalled() - }) - - it('should handle auth RPC error when modal is open and transaction stack is not empty', () => { - vi.spyOn(mockAppKit, 'isOpen').mockReturnValue(true) - vi.spyOn(mockAppKit, 'isTransactionStackEmpty').mockReturnValue(false) - client['handleAuthRpcError']() - expect(mockAppKit.popTransactionStack).toHaveBeenCalledWith(true) - }) - - it('should handle auth RPC success when transaction stack is empty', () => { - vi.spyOn(mockAppKit, 'isTransactionStackEmpty').mockReturnValue(true) - client['handleAuthRpcSuccess']( - { type: '@w3m-frame/SWITCH_NETWORK_SUCCESS', payload: { chainId: '137' } }, - { method: 'eth_accounts' } - ) - expect(mockAppKit.close).toHaveBeenCalled() + describe('Ethers5Adapter -connect', () => { + it('should connect with external provider', async () => { + vi.mocked(mockProvider.request).mockResolvedValue(['0x123']) + const connectors = [ + { + id: 'test', + provider: mockProvider, + chains: [1], + type: 'EXTERNAL', + chain: 1 + } + ] + Object.defineProperty(adapter, 'connectors', { + value: connectors }) - it('should handle auth RPC success when transaction stack is not empty', () => { - vi.spyOn(mockAppKit, 'isTransactionStackEmpty').mockReturnValue(false) - client['handleAuthRpcSuccess']( - { type: '@w3m-frame/SWITCH_NETWORK_SUCCESS', payload: { chainId: '137' } }, - { method: 'eth_accounts' } - ) - expect(mockAppKit.popTransactionStack).toHaveBeenCalledWith() + const result = await adapter.connect({ + id: 'test', + provider: mockProvider, + type: 'EXTERNAL', + chainId: 1 }) - it('should handle auth not connected', () => { - client['handleAuthNotConnected']() - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith(undefined, 'eip155') - }) + expect(result.address).toBe('0x123') + expect(result.chainId).toBe(1) + }) + }) - it('should handle auth is connected', () => { - vi.spyOn(mockAppKit, 'getActiveChainNamespace').mockReturnValue('eip155') - const preferredAccountType = 'eoa' - client['handleAuthIsConnected'](preferredAccountType) - expect(mockAppKit.setPreferredAccountType).toHaveBeenCalledWith( - preferredAccountType, - 'eip155' - ) + describe('Ethers5Adapter -disconnect', () => { + it('should disconnect WalletConnect provider', async () => { + await adapter.disconnect({ + provider: mockWalletConnectProvider, + providerType: 'WALLET_CONNECT' }) - it('should handle auth set preferred account', async () => { - const address = '0x1234567890123456789012345678901234567890' - const type = 'eoa' - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(mainnet) - client['caipNetworks'] = [mainnet] - - vi.spyOn(client as any, 'syncAccount').mockResolvedValue(undefined) - - await client['handleAuthSetPreferredAccount'](address, type) - - expect(mockAppKit.setLoading).toHaveBeenCalledWith(true) - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith(`eip155:${1}:${address}`, 'eip155') - expect(mockAppKit.setStatus).toHaveBeenCalledWith('connected', 'eip155') - expect(mockAppKit.setPreferredAccountType).toHaveBeenCalledWith(type, 'eip155') - - await new Promise(resolve => setImmediate(resolve)) - - expect(mockAppKit.setLoading).toHaveBeenLastCalledWith(false) - }) + expect(mockWalletConnectProvider.disconnect).toHaveBeenCalled() }) - describe('setAuthConnector', () => { - beforeEach(() => { - client['appKit'] = mockAppKit - client['authProvider'] = mockAuthConnector as any - - vi.mocked(ProviderUtil.setProvider).mockClear() - vi.mocked(ProviderUtil.setProviderId).mockClear() + it('should disconnect Auth provider', async () => { + await adapter.disconnect({ + provider: mockAuthProvider, + providerType: 'AUTH' }) - it('should set auth provider and update appKit state', async () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - const mockChainId = 1 - const mockSmartAccountDeployed = true - const mockPreferredAccountType = 'eoa' - - mockAuthConnector.connect.mockResolvedValueOnce({ - address: mockAddress, - chainId: mockChainId, - smartAccountDeployed: mockSmartAccountDeployed, - preferredAccountType: mockPreferredAccountType, - accounts: [] - }) - - mockAuthConnector.getSmartAccountEnabledNetworks.mockResolvedValueOnce({ - smartAccountEnabledNetworks: [1, 137] - }) - - await client['setAuthProvider']() - - expect(mockAppKit.setSmartAccountEnabledNetworks).toHaveBeenCalledWith([1, 137], 'eip155') - expect(mockAppKit.setAllAccounts).toHaveBeenCalledWith( - [{ address: mockAddress, type: mockPreferredAccountType }], - 'eip155' - ) - expect(mockAppKit.setStatus).toHaveBeenCalledWith('connected', 'eip155') - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith( - `eip155:${mockChainId}:${mockAddress}`, - 'eip155' - ) - expect(mockAppKit.setPreferredAccountType).toHaveBeenCalledWith( - mockPreferredAccountType, - 'eip155' - ) - expect(mockAppKit.setSmartAccountDeployed).toHaveBeenCalledWith( - mockSmartAccountDeployed, - 'eip155' - ) - expect(ProviderUtil.setProvider).toHaveBeenCalledWith('eip155', expect.any(Object)) - expect(ProviderUtil.setProviderId).toHaveBeenCalledWith( - 'eip155', - ConstantsUtil.AUTH_CONNECTOR_ID - ) - }) + expect(mockAuthProvider.disconnect).toHaveBeenCalled() }) }) - describe('EthersClient - Sync Connectors', () => { - it('should handle sync EIP-6963 connector', () => { - const mockEIP6963Event: CustomEventInit = { - detail: { - info: { - uuid: 'mock-uuid', - name: 'MockWallet', - icon: 'mock-icon-url', - rdns: 'com.mockwallet' - }, - provider: { - request: vi.fn(), - on: vi.fn(), - removeListener: vi.fn(), - emit: function (event: string): void { - throw new Error(event) - } - } - } - } - - client['eip6963EventHandler'](mockEIP6963Event) - - expect(mockAppKit.addConnector).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'eip6963', - name: 'MockWallet' - }) + describe('Ethers5Adapter -getBalance', () => { + it('should get balance successfully', async () => { + adapter.caipNetworks = mockCaipNetworks + const mockBalance = BigInt(1500000000000000000) + vi.mocked(providers.JsonRpcProvider).mockImplementation( + () => + ({ + getBalance: vi.fn().mockResolvedValue(mockBalance) + }) as any ) - }) - it('should sync injected connector when config.injected is true', () => { - const config = { injected: true, coinbase: false, metadata: {} } - client['syncConnectors'](config as unknown as ProviderType) - - expect(mockAppKit.setConnectors).toHaveBeenCalledWith([ - expect.objectContaining({ - id: 'injected', - name: 'Injected', - type: 'INJECTED', - chain: 'eip155' - }) - ]) - }) - - it('should sync coinbase connector when config.coinbase is true', () => { - const config = { injected: false, coinbase: true, metadata: {} } - client['syncConnectors'](config as unknown as ProviderType) - - expect(mockAppKit.setConnectors).toHaveBeenCalledWith([ - expect.objectContaining({ - id: 'coinbaseWallet', - name: 'Coinbase', - type: 'EXTERNAL', - chain: 'eip155' - }) - ]) - }) - it('should sync both connectors when both are true in config', () => { - const config = { injected: true, coinbase: true, metadata: {} } - client['syncConnectors'](config as unknown as ProviderType) - - expect(mockAppKit.setConnectors).toHaveBeenCalledWith([ - expect.objectContaining({ - id: 'injected', - name: 'Injected', - type: 'INJECTED', - chain: 'eip155' - }), - expect.objectContaining({ - id: 'coinbaseWallet', - name: 'Coinbase', - type: 'EXTERNAL', - chain: 'eip155' - }) - ]) - }) - - it('should not sync any connectors when both are false in config', () => { - const config = { injected: false, coinbase: false, metadata: {} } - client['syncConnectors'](config as unknown as ProviderType) - - expect(mockAppKit.setConnectors).toHaveBeenCalledWith([]) - }) - }) - - describe('EthersClient - State Subscription', () => { - it('should subscribe to state changes', () => { - const mockCallback = vi.fn() - client.subscribeState(mockCallback) - - expect(mockAppKit.subscribeState).toHaveBeenCalled() - }) - }) - - describe('EthersClient - setProvider', () => { - beforeEach(() => { - vi.spyOn(SafeLocalStorage, 'setItem') - vi.spyOn(EthersHelpersUtil, 'getUserInfo').mockResolvedValue({ - addresses: ['0x1234567890123456789012345678901234567890'], + const result = await adapter.getBalance({ + address: '0x123', chainId: 1 }) - }) - - it('should set provider for non-auth providers', async () => { - const mockProvider = { request: vi.fn() } - await client['setProvider'](mockProvider as any, 'injected', 'MetaMask') - - expect(SafeLocalStorage.setItem).toHaveBeenCalledWith( - SafeLocalStorageKeys.WALLET_ID, - 'injected' - ) - expect(SafeLocalStorage.setItem).toHaveBeenCalledWith( - SafeLocalStorageKeys.WALLET_NAME, - 'MetaMask' - ) - expect(mockAppKit.setCaipAddress).toHaveBeenCalled() - expect(ProviderUtil.setProviderId).toHaveBeenCalledWith('eip155', 'injected') - expect(ProviderUtil.setProvider).toHaveBeenCalledWith('eip155', mockProvider) - expect(mockAppKit.setStatus).toHaveBeenCalledWith('connected', 'eip155') - expect(mockAppKit.setAllAccounts).toHaveBeenCalled() - }) - }) - - describe('EthersClient - setupProviderListeners', () => { - let mockProvider: any - beforeEach(() => { - mockProvider = { - on: vi.fn(), - removeListener: vi.fn() - } - }) - - it('should set up listeners for non-auth providers', () => { - client['setupProviderListeners'](mockProvider, 'injected') - - expect(mockProvider.on).toHaveBeenCalledWith('disconnect', expect.any(Function)) - expect(mockProvider.on).toHaveBeenCalledWith('accountsChanged', expect.any(Function)) - expect(mockProvider.on).toHaveBeenCalledWith('chainChanged', expect.any(Function)) - }) - - it('should handle disconnect event', async () => { - vi.spyOn(SafeLocalStorage, 'removeItem') - client['setupProviderListeners'](mockProvider, 'injected') - - const disconnectHandler = mockProvider.on.mock.calls.find( - (call: string[]) => call[0] === 'disconnect' - )[1] - await disconnectHandler() - - expect(SafeLocalStorage.removeItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) - expect(mockProvider.removeListener).toHaveBeenCalledTimes(3) - }) - - it('should handle accountsChanged event', async () => { - client['setupProviderListeners'](mockProvider, 'injected') - - const address = '0x1234567890123456789012345678901234567890' - const accountsChangedHandler = mockProvider.on.mock.calls.find( - (call: string[]) => call[0] === 'accountsChanged' - )[1] - await accountsChangedHandler([address]) - - expect(mockAppKit.setCaipAddress).toHaveBeenCalled() - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith(`eip155:1:${address}`, 'eip155') - }) - - it('should handle chainChanged event', async () => { - client['setupProviderListeners'](mockProvider, 'injected') - - const chainChangedHandler = mockProvider.on.mock.calls.find( - (call: string[]) => call[0] === 'chainChanged' - )[1] - await chainChangedHandler('0x10') - - expect(mockAppKit.setCaipNetwork).toHaveBeenCalled() - }) - }) - - describe('EthersClient - checkActiveProviders', () => { - let mockProvider: any - - beforeEach(() => { - mockProvider = { - request: vi.fn(), - on: vi.fn(), - removeListener: vi.fn() - } - - vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation(key => { - if (key === SafeLocalStorageKeys.WALLET_ID) return ConstantsUtil.INJECTED_CONNECTOR_ID - if (key === SafeLocalStorageKeys.WALLET_NAME) return 'MetaMask' - return undefined + expect(result).toEqual({ + balance: '1.5', + symbol: 'ETH' }) - - vi.spyOn(client as any, 'setProvider').mockImplementation(() => Promise.resolve()) - vi.spyOn(client as any, 'setupProviderListeners').mockImplementation(() => {}) - }) - - it('should check and set active provider for injected and coinbase wallet', () => { - const mockConfig = { - injected: mockProvider, - coinbase: mockProvider, - metadata: {} - } as ProviderType - - const providers = { - [ConstantsUtil.INJECTED_CONNECTOR_ID]: 'MetaMask', - [ConstantsUtil.COINBASE_SDK_CONNECTOR_ID]: 'Coinbase Wallet' - } as const - - for (const [key, name] of Object.entries(providers)) { - vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation(localStorageKey => { - if (localStorageKey === SafeLocalStorageKeys.WALLET_ID) return key - if (localStorageKey === SafeLocalStorageKeys.WALLET_NAME) return name - return undefined - }) - - client['checkActiveProviders'](mockConfig) - - expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) - expect(client['setProvider']).toHaveBeenCalledWith(mockProvider, key) - expect(client['setupProviderListeners']).toHaveBeenCalledWith(mockProvider, key) - } - }) - - it('should not set provider when wallet ID is not found', () => { - vi.spyOn(SafeLocalStorage, 'getItem').mockReturnValue(undefined) - - const mockConfig = { - injected: mockProvider, - coinbase: undefined, - metadata: {} - } - - client['checkActiveProviders'](mockConfig as ProviderType) - - expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) - expect(client['setProvider']).not.toHaveBeenCalled() - expect(client['setupProviderListeners']).not.toHaveBeenCalled() - }) - - it('should not set provider when injected provider is not available', () => { - const mockConfig = { - injected: undefined, - coinbase: undefined, - metadata: {} - } - - client['checkActiveProviders'](mockConfig as ProviderType) - - expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) - expect(client['setProvider']).not.toHaveBeenCalled() - expect(client['setupProviderListeners']).not.toHaveBeenCalled() }) }) - describe('EthersClient - syncAccount', () => { - beforeEach(() => { - vi.spyOn(client as any, 'syncConnectedWalletInfo').mockImplementation(() => {}) - vi.spyOn(client as any, 'setupProviderListeners').mockImplementation(() => {}) - vi.spyOn(client as any, 'setProvider').mockImplementation(() => Promise.resolve()) - vi.spyOn(client as any, 'syncProfile').mockImplementation(() => Promise.resolve()) - vi.spyOn(client as any, 'syncBalance').mockImplementation(() => Promise.resolve()) - vi.spyOn(mockAppKit, 'getIsConnectedState').mockReturnValue(true) - vi.spyOn(mockAppKit, 'getPreferredAccountType').mockReturnValue('eoa') - }) - - it('should sync account when connected and address is provided', async () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - const mockCaipNetwork = mainnet + describe('Ethers5Adapter -getProfile', () => { + it('should get profile successfully', async () => { + const mockEnsName = 'test.eth' + const mockAvatar = 'https://avatar.com/test.jpg' + + vi.mocked(providers.InfuraProvider).mockImplementation( + () => + ({ + lookupAddress: vi.fn().mockResolvedValue(mockEnsName), + getAvatar: vi.fn().mockResolvedValue(mockAvatar) + }) as any + ) - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(mockCaipNetwork) - vi.spyOn(EthersHelpersUtil, 'getUserInfo').mockResolvedValue({ - addresses: ['0x1234567890123456789012345678901234567890'], + const result = await adapter.getProfile({ + address: '0x123', chainId: 1 }) - await client['syncAccount']({ address: mockAddress }) - - expect(mockAppKit.setPreferredAccountType).toHaveBeenCalledWith('eoa', 'eip155') - expect(mockAppKit.setAddressExplorerUrl).toHaveBeenCalledWith( - `https://etherscan.io/address/${mockAddress}`, - 'eip155' - ) - expect(client['syncConnectedWalletInfo']).toHaveBeenCalled() - expect(client['syncProfile']).toHaveBeenCalledWith(mockAddress) - expect(mockAppKit.setCaipAddress).toHaveBeenCalledTimes(2) - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith( - `eip155:${mainnet.id}:${mockAddress}`, - 'eip155' - ) - expect(mockAppKit.setCaipNetwork).toHaveBeenCalledOnce() - expect(mockAppKit.setCaipNetwork).toHaveBeenCalledWith(mainnet) - expect(mockAppKit.setApprovedCaipNetworksData).toHaveBeenCalledWith('eip155') - }) - - it('it should fallback to first available chain if current chain is unsupported', async () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - - vi.spyOn(EthersHelpersUtil, 'getUserInfo').mockResolvedValue({ - addresses: [mockAddress], - chainId: AppkitHarmonyOne.id as number + expect(result).toEqual({ + profileName: mockEnsName, + profileImage: mockAvatar }) - - await client['syncAccount']({ address: mockAddress }) - - expect(mockAppKit.setCaipAddress).toHaveBeenCalledTimes(2) - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith( - `eip155:${mainnet.id}:${mockAddress}`, - 'eip155' - ) - expect(mockAppKit.setCaipNetwork).toHaveBeenCalledOnce() - expect(mockAppKit.setCaipNetwork).toHaveBeenCalledWith(mainnet) - }) - - it('should reset connection when not connected', async () => { - vi.spyOn(mockAppKit, 'getIsConnectedState').mockReturnValue(false) - - await client['syncAccount']({}) - - expect(mockAppKit.resetWcConnection).toHaveBeenCalled() - expect(mockAppKit.resetNetwork).toHaveBeenCalled() - expect(mockAppKit.setAllAccounts).toHaveBeenCalledWith([], 'eip155') }) }) - describe('EthersClient - syncProfile', () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - - beforeEach(() => { - vi.spyOn(client as any, 'syncReownName').mockImplementation(() => Promise.resolve()) - }) - - it('should set profile from fetchIdentity when successful', async () => { - const mockIdentity = { name: 'Test Name', avatar: 'https://example.com/avatar.png' } - vi.spyOn(mockAppKit, 'fetchIdentity').mockResolvedValue(mockIdentity) - - await client['syncProfile'](mockAddress) - - expect(mockAppKit.setProfileName).toHaveBeenCalledWith('Test Name', 'eip155') - expect(mockAppKit.setProfileImage).toHaveBeenCalledWith( - 'https://example.com/avatar.png', - 'eip155' - ) - }) - - it('should use ENS for mainnet when fetchIdentity fails', async () => { - vi.spyOn(mockAppKit, 'fetchIdentity').mockRejectedValue(new Error('Fetch failed')) - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(mainnet) - - const mockEnsProvider = { - lookupAddress: vi.fn().mockResolvedValue('test.eth'), - getAvatar: vi.fn().mockResolvedValue('https://example.com/ens-avatar.png') - } - vi.mocked(ethers.providers.InfuraProvider).mockImplementation(() => mockEnsProvider as any) - - await client['syncProfile'](mockAddress) + describe('Ethers5Adapter - switchNetwork', () => { + it('should switch network with WalletConnect provider', async () => { + await adapter.switchNetwork({ + caipNetwork: mockCaipNetworks[0], + provider: mockWalletConnectProvider, + providerType: 'WALLET_CONNECT' + }) - expect(mockAppKit.setProfileName).toHaveBeenCalledWith('test.eth', 'eip155') - expect(mockAppKit.setProfileImage).toHaveBeenCalledWith( - 'https://example.com/ens-avatar.png', - 'eip155' - ) + expect(mockWalletConnectProvider.setDefaultChain).toHaveBeenCalledWith('eip155:1') }) - it('should fallback to syncReownName for non-mainnet chains', async () => { - vi.spyOn(mockAppKit, 'fetchIdentity').mockRejectedValue(new Error('Fetch failed')) - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(polygon) // Polygon - - await client['syncProfile'](mockAddress) + it('should switch network with Auth provider', async () => { + await adapter.switchNetwork({ + caipNetwork: mockCaipNetworks[0], + provider: mockAuthProvider, + providerType: 'AUTH' + }) - expect(client['syncReownName']).toHaveBeenCalledWith(mockAddress) - expect(mockAppKit.setProfileImage).toHaveBeenCalledWith(null, 'eip155') + expect(mockAuthProvider.switchNetwork).toHaveBeenCalledWith(1) + expect(mockAuthProvider.connect).toHaveBeenCalledWith({ chainId: 1 }) }) }) - describe('EthersClient - syncBalance', () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - - it.skip('should set balance when caipNetwork is available', async () => { - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(mainnet) - - const mockJsonRpcProvider = { - getBalance: vi.fn().mockResolvedValue(BigInt(1000000000000000000)) // 1 ETH in wei - } - vi.mocked(ethers.providers.JsonRpcProvider).mockImplementation( - () => mockJsonRpcProvider as any - ) - - await client['syncBalance'](mockAddress, mainnet) - - expect(ethers.providers.JsonRpcProvider).toHaveBeenCalledWith( - mainnet.rpcUrls.default.http[0], - { - chainId: 1, - name: 'Ethereum' - } - ) - expect(mockJsonRpcProvider.getBalance).toHaveBeenCalledWith(mockAddress) - expect(mockAppKit.setBalance).toHaveBeenCalledWith('1.0', 'ETH', 'eip155') - }) - - it('should not set balance when caipNetwork is unavailable', async () => { - vi.spyOn(mockAppKit, 'getCaipNetworks').mockReturnValue([]) - - await client['syncBalance'](mockAddress, mainnet) + describe('Ethers5Adapter -getWalletConnectProvider', () => { + it('should return WalletConnect provider', () => { + Object.defineProperty(adapter, 'availableConnectors', { + value: [ + { + id: 'walletconnect', + type: 'WALLET_CONNECT', + provider: mockWalletConnectProvider, + chain: 'eip155', + chains: [] + } + ] + }) - expect(ethers.providers.JsonRpcProvider).not.toHaveBeenCalled() - expect(mockAppKit.setBalance).not.toHaveBeenCalled() + const result = adapter.getWalletConnectProvider() + expect(result).toBe(mockWalletConnectProvider) }) }) - describe('EthersClient - syncReownName', () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - - it('should set profile name when WalletConnect name is available', async () => { - const mockWcNames = [ - { name: 'WC Wallet', registered: 1, updated: 1234567890, addresses: [], attributes: {} } - ] as unknown as BlockchainApiLookupEnsName[] - vi.spyOn(mockAppKit, 'getReownName').mockResolvedValue(mockWcNames) - - await client['syncReownName'](mockAddress) - - expect(mockAppKit.getReownName).toHaveBeenCalledWith(mockAddress) - expect(mockAppKit.setProfileName).toHaveBeenCalledWith('WC Wallet', 'eip155') - }) - - it('should set profile name to null when no WalletConnect name is available', async () => { - vi.spyOn(mockAppKit, 'getReownName').mockResolvedValue([]) - - await client['syncReownName'](mockAddress) - - expect(mockAppKit.getReownName).toHaveBeenCalledWith(mockAddress) - expect(mockAppKit.setProfileName).toHaveBeenCalledWith(null, 'eip155') - }) - - it('should set profile name to null when getReownName throws an error', async () => { - vi.spyOn(mockAppKit, 'getReownName').mockRejectedValue(new Error('API Error')) - - await client['syncReownName'](mockAddress) - - expect(mockAppKit.getReownName).toHaveBeenCalledWith(mockAddress) - expect(mockAppKit.setProfileName).toHaveBeenCalledWith(null, 'eip155') - }) - }) + describe('Ethers5Adapter -parseUnits and formatUnits', () => { + it('should parse units correctly', () => { + const mockBigInt = BigInt('1500000000000000000') + vi.mocked(Ethers5Methods.parseUnits).mockReturnValue(mockBigInt) - describe('EthersClient - syncConnectedWalletInfo', () => { - beforeEach(() => { - vi.spyOn(SafeLocalStorage, 'getItem').mockReturnValue('MetaMask') - vi.spyOn(ProviderUtil.state, 'providerIds', 'get').mockReturnValue({ - eip155: 'injected', - solana: undefined, - polkadot: undefined + const result = adapter.parseUnits({ + value: '1.5', + decimals: 18 }) - }) - it('should set connected wallet info for EIP6963 provider', () => { - vi.spyOn(ProviderUtil.state, 'providerIds', 'get').mockReturnValue({ - eip155: ConstantsUtil.EIP6963_CONNECTOR_ID, - solana: undefined - } as Record) - client['EIP6963Providers'] = [ - { - info: { name: 'MetaMask', icon: 'icon-url', uuid: 'test-uuid', rdns: 'com.metamask' }, - provider: {} as any - } - ] - - client['syncConnectedWalletInfo']() - expect(mockAppKit.setConnectedWalletInfo).toHaveBeenCalledWith( - { name: 'MetaMask', icon: 'icon-url', uuid: 'test-uuid', rdns: 'com.metamask' }, - 'eip155' - ) + expect(result).toBe(mockBigInt) }) - it('should set connected wallet info for WalletConnect provider', () => { - vi.spyOn(ProviderUtil.state, 'providerIds', 'get').mockReturnValue({ - eip155: ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID, - solana: undefined - } as Record) - const mockProvider = { - session: { - peer: { - metadata: { - name: 'WC Wallet', - icons: ['wc-icon-url'] - } - } - } - } - vi.spyOn(ProviderUtil, 'getProvider').mockReturnValue(mockProvider as any) - - client['syncConnectedWalletInfo']() + it('should format units correctly', () => { + vi.mocked(Ethers5Methods.formatUnits).mockReturnValue('1.5') - expect(mockAppKit.setConnectedWalletInfo).toHaveBeenCalledWith( - { - name: 'WC Wallet', - icon: 'wc-icon-url', - icons: ['wc-icon-url'] - }, - 'eip155' - ) - }) - - it('should set connected wallet info for Coinbase provider', () => { - vi.spyOn(ProviderUtil.state, 'providerIds', 'get').mockReturnValue({ - eip155: ConstantsUtil.COINBASE_SDK_CONNECTOR_ID, - solana: undefined - } as Record) - vi.spyOn(mockAppKit, 'getConnectors').mockReturnValue([ - { - id: ConstantsUtil.COINBASE_SDK_CONNECTOR_ID, - type: 'INJECTED', - chain: 'eip155' - } - ]) - vi.spyOn(mockAppKit, 'getConnectorImage').mockReturnValue('coinbase-icon-url') - - client['syncConnectedWalletInfo']() + const result = adapter.formatUnits({ + value: BigInt('1500000000000000000'), + decimals: 18 + }) - expect(mockAppKit.setConnectedWalletInfo).toHaveBeenCalledWith( - { name: 'Coinbase Wallet', icon: 'coinbase-icon-url' }, - 'eip155' - ) + expect(result).toBe('1.5') }) }) }) diff --git a/packages/adapters/ethers5/src/utils/Ethers5Methods.ts b/packages/adapters/ethers5/src/utils/Ethers5Methods.ts index 69c973b943..fb8d3dc368 100644 --- a/packages/adapters/ethers5/src/utils/Ethers5Methods.ts +++ b/packages/adapters/ethers5/src/utils/Ethers5Methods.ts @@ -6,8 +6,8 @@ import type { SendTransactionArgs, WriteContractArgs } from '@reown/appkit-core' -import { isReownName } from '@reown/appkit-common' -import type { AppKit } from '@reown/appkit' +import { isReownName, type CaipNetwork } from '@reown/appkit-common' +import { WcHelpersUtil } from '@reown/appkit' export const Ethers5Methods = { signMessage: async (message: string, provider: Provider, address: string) => { @@ -111,14 +111,14 @@ export const Ethers5Methods = { throw new Error('Contract method is undefined') }, - getEnsAddress: async (value: string, appKit: AppKit) => { + getEnsAddress: async (value: string, caipNetwork: CaipNetwork) => { try { - const chainId = Number(appKit.getCaipNetwork()?.id) + const chainId = Number(caipNetwork.id) let ensName: string | null = null let wcName: boolean | string = false if (isReownName(value)) { - wcName = (await appKit?.resolveReownName(value)) || false + wcName = (await WcHelpersUtil.resolveReownName(value)) || false } if (chainId === 1) { diff --git a/packages/adapters/solana/src/client.ts b/packages/adapters/solana/src/client.ts index 5f4c05b344..5bdae738b7 100644 --- a/packages/adapters/solana/src/client.ts +++ b/packages/adapters/solana/src/client.ts @@ -1,699 +1,457 @@ -import { Connection } from '@solana/web3.js' +import { AdapterBlueprint } from '@reown/appkit/adapters' import { - AccountController, - ApiController, - ChainController, + ConstantsUtil as CommonConstantsUtil, + type CaipNetwork, + type ChainNamespace +} from '@reown/appkit-common' +import { + AlertController, CoreHelperUtil, EventsController, - AlertController + type ConnectorType, + type Provider } from '@reown/appkit-core' -import { - ConstantsUtil as CommonConstantsUtil, - SafeLocalStorage, - SafeLocalStorageKeys -} from '@reown/appkit-common' - +import { ConstantsUtil, ErrorUtil } from '@reown/appkit-utils' +import { Connection, PublicKey } from '@solana/web3.js' +import type { Commitment, ConnectionConfig } from '@solana/web3.js' import { SolConstantsUtil } from '@reown/appkit-utils/solana' import { SolStoreUtil } from './utils/SolanaStoreUtil.js' -import type { Provider } from '@reown/appkit-utils/solana' - -import type { BaseWalletAdapter } from '@solana/wallet-adapter-base' -import { PublicKey, type Commitment, type ConnectionConfig } from '@solana/web3.js' -import UniversalProvider, { type UniversalProviderOpts } from '@walletconnect/universal-provider' -import type { - ChainAdapter, - ConnectionControllerClient, - NetworkControllerClient, - Connector -} from '@reown/appkit-core' -import type { AdapterType, CaipAddress, CaipNetwork } from '@reown/appkit-common' -import type { ChainNamespace } from '@reown/appkit-common' - import { watchStandard } from './utils/watchStandard.js' -import { WalletConnectProvider } from './providers/WalletConnectProvider.js' import { AuthProvider } from './providers/AuthProvider.js' import { - W3mFrameHelpers, - W3mFrameProvider, - W3mFrameRpcConstants, - type W3mFrameTypes -} from '@reown/appkit-wallet' -import { ConstantsUtil as CoreConstantsUtil } from '@reown/appkit-core' -import { withSolanaNamespace } from './utils/withSolanaNamespace.js' -import type { AppKit, AppKitOptionsWithCaipNetworks } from '@reown/appkit' -import type { AppKitOptions as CoreOptions } from '@reown/appkit' -import { ProviderUtil } from '@reown/appkit/store' + CoinbaseWalletProvider, + type SolanaCoinbaseWallet +} from './providers/CoinbaseWalletProvider.js' +import type { W3mFrameProvider } from '@reown/appkit-wallet' +import { WcHelpersUtil, type AppKit, type AppKitOptions } from '@reown/appkit' import { W3mFrameProviderSingleton } from '@reown/appkit/auth-provider' -import { ConstantsUtil, ErrorUtil } from '@reown/appkit-utils' +import { withSolanaNamespace } from './utils/withSolanaNamespace.js' +import UniversalProvider from '@walletconnect/universal-provider' import { createSendTransaction } from './utils/createSendTransaction.js' -import { CoinbaseWalletProvider } from './providers/CoinbaseWalletProvider.js' -import base58 from 'bs58' +import type { WalletStandardProvider } from './providers/WalletStandardProvider.js' +import { handleMobileWalletRedirection } from './utils/handleMobileWalletRedirection.js' +import type { BaseWalletAdapter } from '@solana/wallet-adapter-base' +import { WalletConnectProvider } from './providers/WalletConnectProvider.js' export interface AdapterOptions { connectionSettings?: Commitment | ConnectionConfig wallets?: BaseWalletAdapter[] } -export type AppKitOptions = Omit - -// -- Client -------------------------------------------------------------------- -export class SolanaAdapter implements ChainAdapter { - private appKit: AppKit | undefined = undefined - - private authProvider?: Provider - +export class SolanaAdapter extends AdapterBlueprint { + private connectionSettings: Commitment | ConnectionConfig private w3mFrameProvider?: W3mFrameProvider - - public options: CoreOptions | undefined = undefined - + private authProvider?: AuthProvider + private authSession?: AuthProvider.Session + public adapterType = 'solana' public wallets?: BaseWalletAdapter[] - public caipNetworks: CaipNetwork[] = [] - - public readonly chainNamespace: ChainNamespace = CommonConstantsUtil.CHAIN.SOLANA - - public networkControllerClient?: NetworkControllerClient + constructor(options: AdapterOptions = {}) { + super({}) + this.namespace = CommonConstantsUtil.CHAIN.SOLANA + this.connectionSettings = options.connectionSettings || 'confirmed' + this.wallets = options.wallets - public connectionControllerClient?: ConnectionControllerClient - - public connectionSettings: Commitment | ConnectionConfig - - private availableProviders: Provider[] = [] - - private provider: Provider | undefined - - private authSession: AuthProvider.Session | undefined - - public defaultCaipNetwork: CaipNetwork | undefined = undefined - - public readonly adapterType: AdapterType = 'solana' - - public constructor(options: AdapterOptions) { - const { wallets, connectionSettings = 'confirmed' } = options - - this.wallets = wallets - this.connectionSettings = connectionSettings - - ChainController.subscribeKey('activeCaipNetwork', caipNetwork => { - const caipAddress = this.appKit?.getCaipAddress(this.chainNamespace) - const isSolanaAddress = caipAddress?.startsWith('solana:') - const isSolanaNetwork = caipNetwork?.chainNamespace === this.chainNamespace + EventsController.subscribe(state => { + if (state.data.event === 'SELECT_WALLET') { + const isMobile = CoreHelperUtil.isMobile() + const isClient = CoreHelperUtil.isClient() - if (caipAddress && isSolanaAddress && isSolanaNetwork) { - this.syncAccount({ - address: CoreHelperUtil.getPlainAddress(caipAddress), - caipNetwork - }) + if (isMobile && isClient) { + handleMobileWalletRedirection(state.data.properties) + } } }) - - AccountController.subscribeKey( - 'caipAddress', - caipAddress => { - const isSolanaAddress = caipAddress?.startsWith('solana:') - const caipNetwork = ChainController.state.activeCaipNetwork - const isSolanaNetwork = caipNetwork?.chainNamespace === this.chainNamespace - - if (caipAddress && isSolanaAddress && isSolanaNetwork) { - this.syncAccount({ - address: CoreHelperUtil.getPlainAddress(caipAddress), - caipNetwork - }) - } - }, - this.chainNamespace - ) } - public construct(appKit: AppKit, options: AppKitOptionsWithCaipNetworks) { - const { projectId } = options - - this.appKit = appKit - this.options = options - this.caipNetworks = options.networks - this.defaultCaipNetwork = options.defaultNetwork - - if (!projectId) { - throw new Error('Solana:construct - projectId is undefined') + public syncConnectors(options: AppKitOptions, appKit: AppKit) { + if (!options.projectId) { + AlertController.open(ErrorUtil.ALERT_ERRORS.PROJECT_ID_NOT_CONFIGURED, 'error') } - this.networkControllerClient = { - switchCaipNetwork: async caipNetwork => { - if (caipNetwork) { - try { - await this.switchNetwork(caipNetwork) - } catch (error) { - console.warn('Error switching network', error) - } + // Initialize Auth Provider if email/socials enabled + const emailEnabled = options.features?.email !== false + const socialsEnabled = + options.features?.socials !== false && + Array.isArray(options.features?.socials) && + options.features.socials.length > 0 + + if (emailEnabled || socialsEnabled) { + this.w3mFrameProvider = W3mFrameProviderSingleton.getInstance({ + projectId: options.projectId, + chainId: withSolanaNamespace(appKit?.getCaipNetwork(this.namespace)?.id), + onTimeout: () => { + AlertController.open(ErrorUtil.ALERT_ERRORS.INVALID_APP_CONFIGURATION, 'error') } - }, + }) - getApprovedCaipNetworksData: async () => { - if (this.provider) { - return Promise.resolve({ - supportsAllNetworks: false, - approvedCaipNetworkIds: this.provider.chains.map(chain => chain.caipNetworkId) - }) - } + this.authProvider = new AuthProvider({ + getProvider: () => this.w3mFrameProvider as W3mFrameProvider, + getActiveChain: () => appKit.getCaipNetwork(this.namespace), + getActiveNamespace: () => appKit.getActiveChainNamespace(), + getSession: () => this.authSession, + setSession: session => { + this.authSession = session + }, + chains: this.caipNetworks as CaipNetwork[] + }) - return Promise.resolve({ - supportsAllNetworks: false, - approvedCaipNetworkIds: [] - }) - } + this.addConnector({ + id: ConstantsUtil.AUTH_CONNECTOR_ID, + type: 'AUTH', + provider: this.authProvider as unknown as W3mFrameProvider, + name: 'Auth', + chain: this.namespace as ChainNamespace, + chains: [] + }) } - this.connectionControllerClient = { - // eslint-disable-next-line @typescript-eslint/require-await - connectExternal: async ({ id }) => { - const externalProvider = this.availableProviders.find( - provider => provider.name.toLocaleLowerCase() === id.toLocaleLowerCase() - ) - const isAuthProvider = - id.toLocaleLowerCase() === ConstantsUtil.AUTH_CONNECTOR_ID.toLocaleLowerCase() - - if (!externalProvider) { - throw Error('connectionControllerClient:connectExternal - adapter was undefined') - } - - const chainNamespace = this.appKit?.getActiveChainNamespace() - - // If it's not the auth provider, we should auto connect the provider - if (chainNamespace === this.chainNamespace || !isAuthProvider) { - await this.setProvider(externalProvider) - } - }, - - disconnect: async () => { - await ProviderUtil.getProvider('solana')?.disconnect() - - this.appKit?.resetAccount(this.chainNamespace) - }, - - signMessage: async (message: string) => { - const provider = ProviderUtil.state.providers['solana'] as Provider - if (!provider) { - throw new Error('connectionControllerClient:signMessage - provider is undefined') - } - - const signature = await provider.signMessage(new TextEncoder().encode(message)) - - return base58.encode(signature) - }, - - estimateGas: async params => { - if (params.chainNamespace !== CommonConstantsUtil.CHAIN.SOLANA) { - throw new Error('Chain namespace is not supported') - } - - const connection = SolStoreUtil.state.connection - - if (!connection || !this.provider) { - throw new Error('Connection is not set') - } + // Add Coinbase Wallet if available + if ('coinbaseSolana' in window) { + this.addConnector({ + id: 'coinbaseWallet', + type: 'EXTERNAL', + // @ts-expect-error window.coinbaseSolana exists + provider: new CoinbaseWalletProvider({ + provider: window.coinbaseSolana as SolanaCoinbaseWallet, + chains: this.caipNetworks as CaipNetwork[], + getActiveChain: () => appKit.getCaipNetwork(this.namespace) as CaipNetwork + }), + name: 'Coinbase Wallet', + chain: this.namespace as ChainNamespace, + chains: [] + }) + } - const transaction = await createSendTransaction({ - provider: this.provider, - connection, - to: '11111111111111111111111111111111', - value: 1 + // Watch for standard wallet adapters + watchStandard( + this.caipNetworks as CaipNetwork[], + () => appKit.getCaipNetwork(this.namespace), + (...providers: WalletStandardProvider[]) => { + providers.forEach(provider => { + this.addConnector({ + id: provider.name, + type: 'ANNOUNCED', + provider: provider as unknown as Provider, + imageUrl: provider.icon, + name: provider.name, + chain: CommonConstantsUtil.CHAIN.SOLANA, + chains: [] + }) }) + } + ) + } - const fee = await transaction.getEstimatedFee(connection) - - return BigInt(fee || 0) - }, - // -- Transaction methods --------------------------------------------------- - /** - * - * These methods are supported only on `wagmi` and `ethers` since the Solana SDK does not support them in the same way. - * These function definition is to have a type parity between the clients. Currently not in use. - */ - getEnsAvatar: async (value: string) => await Promise.resolve(value), - - getEnsAddress: async (value: string) => await Promise.resolve(value), - - writeContract: async () => await Promise.resolve('0x'), - - getCapabilities: async () => await Promise.resolve('0x'), + // -- Transaction methods --------------------------------------------------- + /** + * + * These methods are supported only on `wagmi` and `ethers` since the Solana SDK does not support them in the same way. + * These function definition is to have a type parity between the clients. Currently not in use. + */ + // eslint-disable-next-line @typescript-eslint/require-await + public async getEnsAddress( + params: AdapterBlueprint.GetEnsAddressParams + ): Promise { + return { address: params.name } + } - grantPermissions: async () => await Promise.resolve('0x'), + public async writeContract(): Promise { + return Promise.resolve({ + hash: '' + }) + } - revokePermissions: async () => await Promise.resolve('0x'), + public async signMessage( + params: AdapterBlueprint.SignMessageParams + ): Promise { + const walletStandardProvider = params.provider as unknown as WalletStandardProvider + if (!walletStandardProvider) { + throw new Error('connectionControllerClient:signMessage - provider is undefined') + } - sendTransaction: async params => { - if (params.chainNamespace !== CommonConstantsUtil.CHAIN.SOLANA) { - throw new Error('Chain namespace is not supported') - } + const signature = await walletStandardProvider.signMessage( + new TextEncoder().encode(params.message) + ) - const connection = SolStoreUtil.state.connection - const address = this.appKit?.getAddress(this.chainNamespace) + return { + signature: new TextDecoder().decode(signature) + } + } - if (!connection || !address || !this.provider) { - throw new Error('Connection is not set') - } + public async estimateGas( + params: AdapterBlueprint.EstimateGasTransactionArgs + ): Promise { + const connection = SolStoreUtil.state.connection - const transaction = await createSendTransaction({ - provider: this.provider, - connection, - to: params.to, - value: params.value - }) + if (!connection || !params.provider) { + throw new Error('Connection is not set') + } - const result = await this.provider.sendTransaction(transaction, connection) + const transaction = await createSendTransaction({ + provider: params.provider as unknown as WalletStandardProvider, + connection, + to: '11111111111111111111111111111111', + value: 1 + }) - await new Promise(resolve => { - const interval = setInterval(async () => { - const status = await connection.getSignatureStatus(result) + const fee = await transaction.getEstimatedFee(connection) - if (status?.value) { - clearInterval(interval) - resolve() - } - }, 1000) - }) + return { + gas: BigInt(fee || 0) + } + } - await this.syncBalance(address) + public async sendTransaction( + params: AdapterBlueprint.SendTransactionParams + ): Promise { + const connection = SolStoreUtil.state.connection - return result - }, + if (!connection || !params.address || !params.provider) { + throw new Error('Connection is not set') + } - parseUnits: () => BigInt(0), + const walletStandardProvider = params.provider as unknown as WalletStandardProvider - formatUnits: () => '', + const transaction = await createSendTransaction({ + provider: walletStandardProvider, + connection, + to: params.to, + value: params.value as number + }) - checkInstalled: (ids: string[] = []) => { - const availableIds = new Set(this.availableProviders.map(provider => provider.name)) + const result = await walletStandardProvider.sendTransaction(transaction, connection) - return ids.some(id => availableIds.has(id)) - } - } + await new Promise(resolve => { + const interval = setInterval(async () => { + const status = await connection.getSignatureStatus(result) - ChainController.state.chains.set(this.chainNamespace, { - chainNamespace: this.chainNamespace, - connectionControllerClient: this.connectionControllerClient, - networkControllerClient: this.networkControllerClient, - adapterType: this.adapterType, - caipNetworks: this.caipNetworks + if (status?.value) { + clearInterval(interval) + resolve() + } + }, 1000) }) - ProviderUtil.subscribeProviders(providers => { - if (providers['solana'] && providers['solana'] instanceof UniversalProvider) { - const walletConnectProvider = this.getSolanaWalletConnectProvider(providers['solana']) - ProviderUtil.setProvider(this.chainNamespace, walletConnectProvider) - } - }) + return { + hash: result + } + } - this.syncRequestedNetworks(this.caipNetworks) + public parseUnits(): bigint { + return 0n + } - this.initializeProviders({ - relayUrl: 'wss://relay.walletconnect.com', - metadata: options.metadata, - projectId: options.projectId - }) + public formatUnits(): string { + return '' + } - this.syncRequestedNetworks(this.caipNetworks) + public async connect( + params: AdapterBlueprint.ConnectParams + ): Promise { + const { id, type, rpcUrl } = params - ChainController.subscribeKey('activeCaipNetwork', (newCaipNetwork: CaipNetwork | undefined) => { - const newChain = this.caipNetworks.find(_chain => _chain.id === newCaipNetwork?.id) + const selectedProvider = this.connectors.find(c => c.id === id)?.provider as Provider - if (!newChain) { - return - } + if (!selectedProvider) { + throw new Error('Provider not found') + } - if (ChainController.state.activeCaipNetwork && this.appKit?.getIsConnectedState()) { - ApiController.reFetchWallets() - } - }) + // eslint-disable-next-line init-declarations + let address: string - EventsController.subscribe(state => { - if (state.data.event === 'SELECT_WALLET') { - const isMobile = CoreHelperUtil.isMobile() - const isClient = CoreHelperUtil.isClient() + if (type === 'AUTH') { + const data = await this.authProvider?.connect() - if (isMobile && isClient) { - if (state.data.properties?.name === 'Phantom' && !('phantom' in window)) { - const href = window.location.href - const protocol = href.startsWith('https') ? 'https' : 'http' - const host = href.split('/')[2] - const ref = `${protocol}://${host}` - window.location.href = `https://phantom.app/ul/browse/${href}?ref=${ref}` - } - - if (state.data.properties?.name === 'Coinbase Wallet' && !('coinbaseSolana' in window)) { - const href = window.location.href - window.location.href = `https://go.cb-w.com/dapp?cb_url=${href}` - } - } + if (!data) { + throw new Error('No address found') } - }) - } - public getWalletConnection() { - return SolStoreUtil.state.connection - } - - // -- Private ----------------------------------------------------------------- - private async syncAccount({ - address, - caipNetwork - }: { - address: string | undefined - caipNetwork: CaipNetwork | undefined - }) { - if (address && caipNetwork) { - SolStoreUtil.setConnection( - new Connection(caipNetwork.rpcUrls.default.http?.[0] as string, this.connectionSettings) - ) - this.appKit?.setAllAccounts([{ address, type: 'eoa' }], this.chainNamespace) - this.appKit?.setCaipAddress( - `${this.chainNamespace}:${caipNetwork.id}:${address}`, - this.chainNamespace - ) - await this.syncNetwork(address) + address = data } else { - this.appKit?.resetWcConnection() - this.appKit?.resetNetwork(this.chainNamespace) - this.appKit?.resetAccount(this.chainNamespace) + address = await selectedProvider.connect() } - } - private async syncBalance(address = this.appKit?.getAddress(this.chainNamespace)) { - if (!address) { - return - } + this.listenProviderEvents(selectedProvider as unknown as WalletStandardProvider) - if (!SolStoreUtil.state.connection) { - throw new Error('Connection is not set') - } + SolStoreUtil.setConnection(new Connection(rpcUrl as string, 'confirmed')) - if (!this.appKit?.getCaipNetwork()) { - this.appKit?.setCaipNetwork(this.defaultCaipNetwork) + return { + address, + chainId: params.chainId as string, + provider: selectedProvider, + type: type as ConnectorType, + id } - - const balance = - (await SolStoreUtil.state.connection.getBalance(new PublicKey(address))) / - SolConstantsUtil.LAMPORTS_PER_SOL - - this.appKit?.setBalance( - balance.toString(), - this.appKit?.getCaipNetwork()?.nativeCurrency.symbol, - this.chainNamespace - ) } - private syncRequestedNetworks(caipNetworks: CaipNetwork[]) { - const uniqueChainNamespaces = Array.from( - new Set(caipNetworks.map(caipNetwork => caipNetwork.chainNamespace)) + public async getBalance( + params: AdapterBlueprint.GetBalanceParams + ): Promise { + const connection = new Connection( + params.caipNetwork?.rpcUrls?.default?.http?.[0] as string, + this.connectionSettings ) - uniqueChainNamespaces.forEach(chainNamespace => { - this.appKit?.setRequestedCaipNetworks( - caipNetworks.filter(caipNetwork => caipNetwork.chainNamespace === chainNamespace), - chainNamespace - ) - }) - } - - private getAuthSession() { - return this.authSession - } - public async switchNetwork(caipNetwork: CaipNetwork) { - const connectedConnector = SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR) - const isConnectedWithAuth = connectedConnector === 'AUTH' + const balance = await connection.getBalance(new PublicKey(params.address)) + const formattedBalance = (balance / SolConstantsUtil.LAMPORTS_PER_SOL).toString() - if (isConnectedWithAuth) { - // If user is connected with auth provider, we need to switch the network on the auth provider and await the get user - await this.w3mFrameProvider?.switchNetwork(caipNetwork.caipNetworkId) - const user = await this.w3mFrameProvider?.getUser({ - chainId: caipNetwork?.caipNetworkId - }) - this.authSession = user - if (user) { - const caipAddress = `${caipNetwork.caipNetworkId}:${user.address}` - ProviderUtil.setProvider(this.chainNamespace, this.authProvider) - ProviderUtil.setProviderId(this.chainNamespace, 'walletConnect') - this.appKit?.setCaipAddress(caipAddress as CaipAddress, this.chainNamespace) - this.syncAccount({ - address: user.address, - caipNetwork - }) - } - } else { - this.appKit?.setCaipNetwork(caipNetwork) + if (!params.caipNetwork) { + throw new Error('caipNetwork is required') + } - const address = this.appKit?.getAddress(this.chainNamespace) as string - await this.syncAccount({ - address, - caipNetwork - }) + return { + balance: formattedBalance, + symbol: params.caipNetwork?.nativeCurrency.symbol } } - private async syncNetwork(address: string | undefined) { - const caipNetwork = this.appKit?.getCaipNetwork(this.chainNamespace) - const connection = SolStoreUtil.state.connection + public async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise { + const { caipNetwork, provider, providerType } = params - if (!address || !caipNetwork || !connection) { - return + if (providerType === 'ID_AUTH') { + await (provider as unknown as W3mFrameProvider).switchNetwork(caipNetwork.id) + const user = await (provider as unknown as W3mFrameProvider).getUser({ + chainId: caipNetwork.id + }) + this.authSession = user + this.emit('switchNetwork', { chainId: caipNetwork.id, address: user.address }) } - this.appKit?.setAddressExplorerUrl( - caipNetwork.blockExplorers?.default.url - ? `${caipNetwork.blockExplorers.default.url}/account/${address}` - : undefined, - this.chainNamespace - ) - await this.syncBalance(address) + if (caipNetwork?.rpcUrls?.default?.http?.[0]) { + SolStoreUtil.setConnection( + new Connection(caipNetwork.rpcUrls.default.http[0], this.connectionSettings) + ) + } } - private async setProvider(provider: Provider) { - try { - this.appKit?.setLoading(true) - const address = await provider.connect() - const caipNetworkId = SafeLocalStorage.getItem(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID) - - const connectionChain = - provider.chains.find(chain => chain.caipNetworkId === caipNetworkId) || provider.chains[0] - - if (connectionChain) { - const caipAddress = `${connectionChain.caipNetworkId}:${address}` as const - this.appKit?.setCaipAddress(caipAddress, this.chainNamespace) - - await this.switchNetwork(connectionChain) - - ProviderUtil.setProvider(this.chainNamespace, provider) - this.provider = provider - - switch (provider.type) { - case 'WALLET_CONNECT': - ProviderUtil.setProviderId(this.chainNamespace, 'walletConnect') - break - case 'AUTH': - ProviderUtil.setProviderId(this.chainNamespace, 'w3mAuth') - break - default: - ProviderUtil.setProviderId(this.chainNamespace, 'injected') - } - - SafeLocalStorage.setItem(SafeLocalStorageKeys.WALLET_ID, provider.name) - - await this.appKit?.setApprovedCaipNetworksData(this.chainNamespace) + private listenProviderEvents(provider: WalletStandardProvider) { + const disconnectHandler = () => { + this.removeProviderListeners(provider) + this.emit('disconnect') + } - this.watchProvider(provider) + const accountsChangedHandler = (publicKey: PublicKey) => { + const address = publicKey.toBase58() + if (address) { + this.emit('accountChanged', { address }) } - } finally { - this.appKit?.setLoading(false) } - } - private watchProvider(provider: Provider) { - /* - * The auth RPC request handlers should be moved to the primary scaffold (appkit). - * They are replicated in wagmi and ethers clients and the behavior should be kept the same - * between any client. - */ - - // eslint-disable-next-line func-style - const rpcRequestHandler = (request: W3mFrameTypes.RPCRequest) => { - if (!this.appKit) { - return - } + provider.on('disconnect', disconnectHandler) + provider.on('accountsChanged', accountsChangedHandler) + provider.on('connect', accountsChangedHandler) - if (W3mFrameHelpers.checkIfRequestExists(request)) { - if (!W3mFrameHelpers.checkIfRequestIsSafe(request)) { - this.appKit?.handleUnsafeRPCRequest() - } - } else { - this.appKit.open() - // eslint-disable-next-line no-console - console.error(W3mFrameRpcConstants.RPC_METHOD_NOT_ALLOWED_MESSAGE, { - method: request.method - }) - setTimeout(() => { - this.appKit?.showErrorMessage(W3mFrameRpcConstants.RPC_METHOD_NOT_ALLOWED_UI_MESSAGE) - }, 300) - } + this.providerHandlers = { + disconnect: disconnectHandler, + accountsChanged: accountsChangedHandler } + } - // eslint-disable-next-line func-style - const rpcSuccessHandler = (_response: W3mFrameTypes.FrameEvent) => { - if (!this.appKit) { - return - } - - if (this.appKit.isTransactionStackEmpty()) { - this.appKit.close() - } else { - this.appKit.popTransactionStack() - } + private providerHandlers: { + disconnect: () => void + accountsChanged: (publicKey: PublicKey) => void + } | null = null + + private removeProviderListeners(provider: WalletStandardProvider) { + if (this.providerHandlers) { + provider.removeListener('disconnect', this.providerHandlers.disconnect) + provider.removeListener('accountsChanged', this.providerHandlers.accountsChanged) + provider.removeListener('connect', this.providerHandlers.accountsChanged) + this.providerHandlers = null } + } - // eslint-disable-next-line func-style - const rpcErrorHandler = (_error: Error) => { - if (!this.appKit) { - return - } + public async connectWalletConnect(onUri: (uri: string) => void): Promise { + const connector = this.connectors.find(c => c.type === 'WALLET_CONNECT') + const provider = connector?.provider as UniversalProvider - if (this.appKit.isOpen()) { - if (this.appKit.isTransactionStackEmpty()) { - this.appKit.close() - } else { - this.appKit.popTransactionStack(true) - } - } + if (!this.caipNetworks || !provider) { + throw new Error( + 'UniversalAdapter:connectWalletConnect - caipNetworks or provider is undefined' + ) } - function disconnectHandler(appKit?: AppKit) { - appKit?.resetAccount('solana') - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_ID) + provider.on('display_uri', (uri: string) => { + onUri(uri) + }) - provider.removeListener('disconnect', disconnectHandler) - provider.removeListener('accountsChanged', accountsChangedHandler) - provider.removeListener('connect', accountsChangedHandler) - provider.removeListener('auth_rpcRequest', rpcRequestHandler) - provider.removeListener('auth_rpcSuccess', rpcSuccessHandler) - provider.removeListener('auth_rpcError', rpcErrorHandler) - } + const namespaces = WcHelpersUtil.createNamespaces(this.caipNetworks) + await provider.connect({ optionalNamespaces: namespaces }) + } - function accountsChangedHandler(publicKey: PublicKey, appKit?: AppKit) { - const currentAccount: string = publicKey.toBase58() - const caipNetworkId = SafeLocalStorage.getItem(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID) - const chainId = caipNetworkId?.split(':')[1] - if (currentAccount && chainId) { - appKit?.setCaipAddress(`solana:${chainId}:${currentAccount}`, 'solana') - } else { - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_ID) - appKit?.resetAccount('solana') - } + public async disconnect(params: AdapterBlueprint.DisconnectParams): Promise { + if (!params.provider || !params.providerType) { + throw new Error('Provider or providerType not provided') } - provider.on('disconnect', () => disconnectHandler(this.appKit)) - provider.on('accountsChanged', (publicKey: PublicKey) => - accountsChangedHandler(publicKey, this.appKit) - ) - provider.on('connect', accountsChangedHandler) - provider.on('auth_rpcRequest', rpcRequestHandler) - provider.on('auth_rpcSuccess', rpcSuccessHandler) - provider.on('auth_rpcError', rpcErrorHandler) + await params.provider.disconnect() } - private getSolanaWalletConnectProvider(provider: UniversalProvider) { - const walletConnectProvider = new WalletConnectProvider({ - provider, - chains: this.caipNetworks, - getActiveChain: () => this.appKit?.getCaipNetwork() + public async getProfile(): Promise { + return Promise.resolve({ + profileName: undefined, + profileImage: undefined }) - - this.addProvider(walletConnectProvider) - - return walletConnectProvider } - private initializeProviders(opts: UniversalProviderOpts) { - if (CoreHelperUtil.isClient()) { - if (!opts.projectId) { - throw new Error('projectId is required for AuthProvider') - } + public async syncConnection( + params: AdapterBlueprint.SyncConnectionParams + ): Promise { + const { id, rpcUrl } = params + const connector = this.connectors.find(c => c.id === id) + const selectedProvider = connector?.provider as Provider - const getActiveChain = () => this.appKit?.getCaipNetwork(this.chainNamespace) - - const emailEnabled = - this.options?.features?.email === undefined - ? CoreConstantsUtil.DEFAULT_FEATURES.email - : this.options?.features?.email - const socialsEnabled = this.options?.features?.socials - ? this.options?.features?.socials?.length > 0 - : CoreConstantsUtil.DEFAULT_FEATURES.socials - - if (emailEnabled || socialsEnabled) { - this.w3mFrameProvider = W3mFrameProviderSingleton.getInstance({ - projectId: opts.projectId, - chainId: withSolanaNamespace(this.appKit?.getCaipNetwork(this.chainNamespace)?.id), - onTimeout: () => { - AlertController.open(ErrorUtil.ALERT_ERRORS.SOCIALS_TIMEOUT, 'error') - } - }) + if (!selectedProvider) { + throw new Error('Provider not found') + } - this.authProvider = new AuthProvider({ - getProvider: () => this.w3mFrameProvider as W3mFrameProvider, - getActiveChain, - getActiveNamespace: () => this.appKit?.getActiveChainNamespace(), - getSession: () => this.getAuthSession(), - setSession: (session: AuthProvider.Session | undefined) => { - this.authSession = session - }, - chains: this.caipNetworks - }) - this.addProvider(this.authProvider) - } + // Handle different provider types + if (connector?.type === 'AUTH') { + const authProvider = selectedProvider as unknown as W3mFrameProvider + const user = await authProvider.getUser({ + chainId: Number(this.caipNetworks?.[0]?.id) + }) - if ('coinbaseSolana' in window) { - this.addProvider( - new CoinbaseWalletProvider({ - // @ts-expect-error - window is not typed - provider: window.coinbaseSolana, - chains: this.caipNetworks, - getActiveChain - }) - ) + if (!user?.address) { + throw new Error('No address found') } - watchStandard(this.caipNetworks, getActiveChain, this.addProvider.bind(this)) + return { + address: user.address, + chainId: typeof user.chainId === 'string' ? Number(user.chainId.split(':')[1]) : 1, + provider: selectedProvider, + type: connector.type, + id + } } - } - private addProvider(...providers: Provider[]) { - const activeProviderName = SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_ID) - const activeNamespace = this.appKit?.getActiveChainNamespace() - const isSolana = activeNamespace === this.chainNamespace + // For standard Solana wallets + const address = await selectedProvider.connect() + const chainId = this.caipNetworks?.[0]?.id || 1 - for (const provider of providers) { - this.availableProviders = this.availableProviders.filter(p => p.name !== provider.name) - this.availableProviders.push(provider) + this.listenProviderEvents(selectedProvider as unknown as WalletStandardProvider) - if (provider.name === activeProviderName && isSolana) { - this.setProvider(provider) - } - } + SolStoreUtil.setConnection(new Connection(rpcUrl, 'confirmed')) - this.syncConnectors() + return { + address, + chainId, + provider: selectedProvider, + type: connector?.type as ConnectorType, + id + } } - private syncConnectors() { - const connectors = this.availableProviders.map(provider => ({ - id: provider.name, - type: provider.type, - imageUrl: provider.icon, - name: provider.name, - /** - * When the provider is different from 'AUTH', we don't need to pass it to the connector. - * This avoids issues with the valtio proxy and non-serializable state and follows same logic from other clients. - */ - provider: provider.type === 'AUTH' ? provider : undefined, - chain: CommonConstantsUtil.CHAIN.SOLANA - })) - - this.appKit?.setConnectors(connectors) + public getWalletConnectProvider( + params: AdapterBlueprint.GetWalletConnectProviderParams + ): AdapterBlueprint.GetWalletConnectProviderResult { + const walletConnectProvider = new WalletConnectProvider({ + provider: params.provider as UniversalProvider, + chains: params.caipNetworks, + getActiveChain: () => params.activeCaipNetwork + }) + + return walletConnectProvider as unknown as UniversalProvider } } diff --git a/packages/adapters/solana/src/tests/client.test.ts b/packages/adapters/solana/src/tests/client.test.ts index a413a96864..daa96e590b 100644 --- a/packages/adapters/solana/src/tests/client.test.ts +++ b/packages/adapters/solana/src/tests/client.test.ts @@ -1,234 +1,169 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { describe, it, expect, beforeEach, vi } from 'vitest' import { SolanaAdapter } from '../client' -import { mockOptions } from './mocks/Options' -import mockAppKit from './mocks/AppKit' -import { mockAuthConnector } from './mocks/AuthConnector' -import { Connection } from '@solana/web3.js' -import { SafeLocalStorage, type CaipNetwork } from '@reown/appkit-common' -import { ProviderUtil } from '@reown/appkit/store' -import { SolStoreUtil } from '../utils/SolanaStoreUtil.js' -import { WalletConnectProvider } from '../providers/WalletConnectProvider' -import UniversalProvider from '@walletconnect/universal-provider' -import { - solana as AppKitSolana, - solanaTestnet as AppKitSolanaTestnet -} from '@reown/appkit/networks' import { CaipNetworksUtil } from '@reown/appkit-utils' +import { solana } from '@reown/appkit/networks' +import type { Provider } from '@reown/appkit-core' +import type { W3mFrameProvider } from '@reown/appkit-wallet' +import UniversalProvider from '@walletconnect/universal-provider' +import { SolStoreUtil } from '../utils/SolanaStoreUtil' +import type { WalletStandardProvider } from '../providers/WalletStandardProvider' -const [solana, solanaTestnet] = CaipNetworksUtil.extendCaipNetworks( - [AppKitSolana, AppKitSolanaTestnet], - { - customNetworkImageUrls: mockOptions.chainImages, - projectId: '1234' - } -) as [CaipNetwork, CaipNetwork] - -const mockOptionsExtended = { - ...mockOptions, - networks: [solana, solanaTestnet] as [CaipNetwork, ...CaipNetwork[]], - defaultNetwork: solana -} - +// Mock external dependencies vi.mock('@solana/web3.js', () => ({ - Connection: vi.fn(), - PublicKey: vi.fn() -})) - -vi.mock('@reown/appkit-wallet', () => ({ - W3mFrameProvider: vi.fn().mockImplementation(() => mockAuthConnector), - W3mFrameHelpers: { - checkIfRequestExists: vi.fn(), - checkIfRequestIsSafe: vi.fn() - }, - W3mFrameRpcConstants: { - RPC_METHOD_NOT_ALLOWED_UI_MESSAGE: 'RPC method not allowed' - } -})) - -vi.mock('@reown/appkit/store', () => ({ - ProviderUtil: { - setProvider: vi.fn(), - setProviderId: vi.fn(), - state: { - providers: {} - }, - getProvider: vi.fn(), - subscribeProviders: vi.fn() - } + Connection: vi.fn(() => ({ + getBalance: vi.fn().mockResolvedValue(1500000000), + getSignatureStatus: vi.fn().mockResolvedValue({ value: true }) + })), + PublicKey: vi.fn(key => ({ toBase58: () => key })) })) -vi.mock('../utils/SolanaStoreUtil.js', () => ({ +vi.mock('../utils/SolanaStoreUtil', () => ({ SolStoreUtil: { - setConnection: vi.fn(), state: { connection: null - } + }, + setConnection: vi.fn() } })) -vi.mock('@reown/appkit-utils/solana', () => ({ - SolHelpersUtil: { - detectRpcUrl: vi.fn() - }, - SolConstantsUtil: { - LAMPORTS_PER_SOL: 1_000_000_000 - } -})) +const mockProvider = { + connect: vi.fn().mockResolvedValue('mock-address'), + disconnect: vi.fn(), + on: vi.fn(), + removeListener: vi.fn(), + signMessage: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])), + sendTransaction: vi.fn().mockResolvedValue('mock-signature') +} as unknown as WalletStandardProvider + +const mockWalletConnectProvider = { + connect: vi.fn(), + disconnect: vi.fn(), + on: vi.fn(), + removeListener: vi.fn(), + session: true, + setDefaultChain: vi.fn() +} as unknown as UniversalProvider + +const mockAuthProvider = { + id: 'auth', + connect: vi.fn().mockResolvedValue('mock-auth-address'), + disconnect: vi.fn(), + switchNetwork: vi.fn(), + getUser: vi.fn().mockResolvedValue({ + address: 'mock-auth-address', + chainId: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' + }) +} as unknown as W3mFrameProvider + +const mockNetworks = [solana] +const mockCaipNetworks = CaipNetworksUtil.extendCaipNetworks(mockNetworks, { + projectId: 'test-project-id', + customNetworkImageUrls: {} +}) describe('SolanaAdapter', () => { - let client: SolanaAdapter + let adapter: SolanaAdapter beforeEach(() => { vi.clearAllMocks() - client = new SolanaAdapter({}) - client.construct(mockAppKit, mockOptionsExtended) - }) - - afterEach(() => { - vi.clearAllMocks() + adapter = new SolanaAdapter() }) - describe('SolanaAdapter - Initialization', () => { - it('should initialize with default values', () => { - expect(client.chainNamespace).toBe('solana') - expect(client.adapterType).toBe('solana') + describe('SolanaAdapter - constructor', () => { + it('should initialize with correct parameters', () => { + expect(adapter.adapterType).toBe('solana') + expect(adapter.namespace).toBe('solana') }) + }) - it('should set caipNetworks to provided caipNetworks options', () => { - expect(client['caipNetworks']).toEqual(mockOptionsExtended.networks) - }) + describe('SolanaAdapter - connect', () => { + it('should connect with external provider', async () => { + const connectors = [ + { + id: 'test', + provider: mockProvider, + type: 'EXTERNAL' + } + ] + Object.defineProperty(adapter, 'connectors', { + value: connectors + }) - it('should set chain images', () => { - Object.entries(mockOptionsExtended.chainImages!).map(([networkId, imageUrl]) => { - const caipNetwork = client.caipNetworks.find(caipNetwork => caipNetwork.id === networkId) - expect(caipNetwork).toBeDefined() - expect(caipNetwork?.assets?.imageUrl).toEqual(imageUrl) + const result = await adapter.connect({ + id: 'test', + provider: mockProvider, + type: 'EXTERNAL', + chainId: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + rpcUrl: 'https://api.mainnet-beta.solana.com' }) - }) - it('should create network and connection controller clients', () => { - expect(client.networkControllerClient).toBeDefined() - expect(client.connectionControllerClient).toBeDefined() + expect(result.address).toBe('mock-address') + expect(result.chainId).toBe('5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp') + expect(SolStoreUtil.setConnection).toHaveBeenCalled() }) }) - describe('SolanaAdapter - Network', () => { - it('should switch network', async () => { - vi.spyOn(SolStoreUtil.state, 'connection', 'get').mockReturnValue({ - getBalance: vi.fn() - } as unknown as Connection) - await client.switchNetwork(solana) - - expect(mockAppKit.setCaipNetwork).toHaveBeenCalledWith(solana) - }) - - it('should sync network', async () => { - const mockAddress = 'DjPi1LtwrXJMAh2AUvuUMajCpMJEKg8N1J1PbLGjCH5B' - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(solana) - vi.spyOn(mockAppKit, 'getAddress').mockReturnValue(mockAddress) - vi.spyOn(client as any, 'syncBalance') - - await client['syncNetwork'](mockAddress) + describe('SolanaAdapter - disconnect', () => { + it('should disconnect provider', async () => { + await adapter.disconnect({ + provider: mockProvider as unknown as Provider, + providerType: 'EXTERNAL' + }) - expect(mockAppKit.setAddressExplorerUrl).toHaveBeenCalledWith( - `${solana.blockExplorers?.default.url}/account/${mockAddress}`, - 'solana' - ) - expect(client['syncBalance']).toHaveBeenCalledWith(mockAddress) + expect(mockProvider.disconnect).toHaveBeenCalled() }) }) - describe('SolanaAdapter - Account', () => { - it('should sync account', async () => { - const mockAddress = 'DjPi1LtwrXJMAh2AUvuUMajCpMJEKg8N1J1PbLGjCH5B' - vi.spyOn(mockAppKit, 'getAddress').mockReturnValue(mockAddress) - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(solana) - vi.spyOn(mockAppKit, 'getIsConnectedState').mockReturnValue(true) - vi.spyOn(client as any, 'syncBalance').mockResolvedValue(undefined) - - await client['syncAccount']({ - address: mockAddress, - caipNetwork: solana + describe('SolanaAdapter - getBalance', () => { + it('should get balance successfully', async () => { + const result = await adapter.getBalance({ + address: 'mock-address', + chainId: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + caipNetwork: mockCaipNetworks[0] }) - expect(SolStoreUtil.setConnection).toHaveBeenCalled() - expect(client['syncBalance']).toHaveBeenCalledWith(mockAddress) - }) - - it('should sync balance', async () => { - const mockAddress = 'DjPi1LtwrXJMAh2AUvuUMajCpMJEKg8N1J1PbLGjCH5B' - const mockBalance = 1000000000 - vi.spyOn(SolStoreUtil.state, 'connection', 'get').mockReturnValue({ - getBalance: vi.fn().mockResolvedValue(mockBalance) - } as unknown as Connection) - vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(solana) - - await client['syncBalance'](mockAddress) - - expect(mockAppKit.setBalance).toHaveBeenCalledWith('1', 'SOL', 'solana') + expect(result).toEqual({ + balance: '1.5', + symbol: 'SOL' + }) }) }) - describe('SolanaAdapter - Provider', () => { - it('should set provider', async () => { - const mockProvider = { - connect: vi.fn().mockResolvedValue('DjPi1LtwrXJMAh2AUvuUMajCpMJEKg8N1J1PbLGjCH5B'), - name: 'MockProvider', - on: vi.fn(), - chains: [solana, solanaTestnet] - } - vi.spyOn(SafeLocalStorage, 'getItem').mockReturnValue( - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' - ) - - await client['setProvider'](mockProvider as any) - - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith( - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:DjPi1LtwrXJMAh2AUvuUMajCpMJEKg8N1J1PbLGjCH5B', - 'solana' - ) - expect(ProviderUtil.setProvider).toHaveBeenCalledWith('solana', mockProvider) - expect(ProviderUtil.setProviderId).toHaveBeenCalledWith('solana', 'injected') - }) + describe('SolanaAdapter - signMessage', () => { + it('should sign message successfully', async () => { + const result = await adapter.signMessage({ + message: 'Hello', + address: 'mock-address', + provider: mockProvider as unknown as Provider + }) - it('should add provider', () => { - const mockProvider = { - name: 'MockProvider', - type: 'INJECTED', - icon: 'mock-icon', - chains: [{ chainId: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' }] - } - client['addProvider'](mockProvider as any) - - expect(client['availableProviders']).toContain(mockProvider) - expect(mockAppKit.setConnectors).toHaveBeenCalled() + expect(result.signature).toBeDefined() + expect(mockProvider.signMessage).toHaveBeenCalled() }) }) - describe('SolanaAdapter - GetWalletConnectProvider', () => { - it('should get Solana WalletConnect provider', () => { - const mockUniversalProvider = {} as UniversalProvider - const result = client['getSolanaWalletConnectProvider'](mockUniversalProvider) + describe('SolanaAdapter - switchNetwork', () => { + it('should switch network with auth provider', async () => { + await adapter.switchNetwork({ + caipNetwork: mockCaipNetworks[0], + provider: mockAuthProvider, + providerType: 'ID_AUTH' + }) - expect(result).toBeInstanceOf(WalletConnectProvider) + expect(mockAuthProvider.switchNetwork).toHaveBeenCalled() + expect(SolStoreUtil.setConnection).toHaveBeenCalled() }) }) - describe('SolanaAdapter - Events', () => { - it('should set up provider event listeners', () => { - const mockProvider = { - on: vi.fn(), - removeListener: vi.fn() - } - - client['watchProvider'](mockProvider as any) - - expect(mockProvider.on).toHaveBeenCalledWith('disconnect', expect.any(Function)) - expect(mockProvider.on).toHaveBeenCalledWith('accountsChanged', expect.any(Function)) - expect(mockProvider.on).toHaveBeenCalledWith('connect', expect.any(Function)) - expect(mockProvider.on).toHaveBeenCalledWith('auth_rpcRequest', expect.any(Function)) - expect(mockProvider.on).toHaveBeenCalledWith('auth_rpcSuccess', expect.any(Function)) - expect(mockProvider.on).toHaveBeenCalledWith('auth_rpcError', expect.any(Function)) + describe('SolanaAdapter - getWalletConnectProvider', () => { + it('should return WalletConnect provider', () => { + const result = adapter.getWalletConnectProvider({ + provider: mockWalletConnectProvider, + caipNetworks: mockCaipNetworks, + activeCaipNetwork: mockCaipNetworks[0] + }) + + expect(result).toBeDefined() }) }) }) diff --git a/packages/adapters/solana/src/utils/handleMobileWalletRedirection.ts b/packages/adapters/solana/src/utils/handleMobileWalletRedirection.ts new file mode 100644 index 0000000000..fc982c7bc5 --- /dev/null +++ b/packages/adapters/solana/src/utils/handleMobileWalletRedirection.ts @@ -0,0 +1,17 @@ +export function handleMobileWalletRedirection(properties: { + name: string + platform: string +}): void { + if (properties?.name === 'Phantom' && !('phantom' in window)) { + const href = window.location.href + const protocol = href.startsWith('https') ? 'https' : 'http' + const host = href.split('/')[2] + const ref = `${protocol}://${host}` + window.location.href = `https://phantom.app/ul/browse/${href}?ref=${ref}` + } + + if (properties?.name === 'Coinbase Wallet' && !('coinbaseSolana' in window)) { + const href = window.location.href + window.location.href = `https://go.cb-w.com/dapp?cb_url=${href}` + } +} diff --git a/packages/adapters/wagmi/src/client.ts b/packages/adapters/wagmi/src/client.ts index 70552cb39d..6c8ef0b6e1 100644 --- a/packages/adapters/wagmi/src/client.ts +++ b/packages/adapters/wagmi/src/client.ts @@ -1,162 +1,94 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -/* eslint-disable no-console */ +import type UniversalProvider from '@walletconnect/universal-provider' +import type { AppKitNetwork, BaseNetwork, CaipNetwork } from '@reown/appkit-common' +import { AdapterBlueprint } from '@reown/appkit/adapters' import { connect, - disconnect, - signMessage, + disconnect as wagmiDisconnect, + createConfig, + type Config, + type CreateConfigParameters, + type CreateConnectorFn, + getConnections, + switchChain, + injected, + type Connector, + watchAccount, + watchConnections, getBalance, - getEnsAvatar as wagmiGetEnsAvatar, getEnsName, - watchAccount, - watchConnectors, + getEnsAvatar, + signMessage, estimateGas as wagmiEstimateGas, - writeContract as wagmiWriteContract, - getAccount, + sendTransaction as wagmiSendTransaction, getEnsAddress as wagmiGetEnsAddress, - reconnect, - switchChain, + writeContract as wagmiWriteContract, waitForTransactionReceipt, - getConnections, - switchAccount, - injected, - createConfig + getAccount, + prepareTransactionRequest, + reconnect } from '@wagmi/core' +import { type Chain } from '@wagmi/core/chains' + +import { + ConstantsUtil as CommonConstantsUtil, + isReownName, + NetworkUtil +} from '@reown/appkit-common' +import { authConnector } from './connectors/AuthConnector.js' +import { AppKit, WcHelpersUtil, type AppKitOptions } from '@reown/appkit' +import { walletConnect } from './connectors/UniversalConnector.js' +import { coinbaseWallet } from '@wagmi/connectors' import { - ChainController, ConstantsUtil as CoreConstantsUtil, - CoreHelperUtil, - StorageUtil + type ConnectorType, + type Provider } from '@reown/appkit-core' -import type UniversalProvider from '@walletconnect/universal-provider' -import type { ChainAdapter } from '@reown/appkit-core' -import { prepareTransactionRequest, sendTransaction as wagmiSendTransaction } from '@wagmi/core' -import type { Chain } from '@wagmi/core/chains' -import { mainnet } from 'viem/chains' -import type { - GetAccountReturnType, - GetEnsAddressReturnType, - Config, - CreateConnectorFn, - CreateConfigParameters -} from '@wagmi/core' -import type { - ConnectionControllerClient, - Connector, - NetworkControllerClient, - PublicStateControllerState, - SendTransactionArgs, - WriteContractArgs -} from '@reown/appkit-core' -import { formatUnits, parseUnits } from 'viem' -import type { Hex, HttpTransport } from 'viem' +import { CaipNetworksUtil, ConstantsUtil, PresetsUtil } from '@reown/appkit-utils' import { - ConstantsUtil, - PresetsUtil, - HelpersUtil, - ErrorUtil, - CaipNetworksUtil -} from '@reown/appkit-utils' -import { isReownName, SafeLocalStorage, SafeLocalStorageKeys } from '@reown/appkit-common' -import { - getEmailCaipNetworks, - getWalletConnectCaipNetworks, - parseWalletCapabilities, - requireCaipAddress -} from './utils/helpers.js' -import { W3mFrameHelpers, W3mFrameRpcConstants } from '@reown/appkit-wallet' -import type { W3mFrameProvider, W3mFrameTypes } from '@reown/appkit-wallet' -import { NetworkUtil } from '@reown/appkit-common' + formatUnits, + parseUnits, + type GetEnsAddressReturnType, + type Hex, + type HttpTransport +} from 'viem' +import type { W3mFrameProvider } from '@reown/appkit-wallet' import { normalize } from 'viem/ens' -import type { AppKitOptions, AppKitOptionsWithCaipNetworks } from '@reown/appkit' -import type { - CaipAddress, - BaseNetwork, - ChainNamespace, - AdapterType, - CaipNetwork, - AppKitNetwork -} from '@reown/appkit-common' -import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' -import type { AppKit } from '@reown/appkit' -import { walletConnect } from './connectors/UniversalConnector.js' -import { coinbaseWallet } from '@wagmi/connectors' -import { authConnector } from './connectors/AuthConnector.js' -import { ProviderUtil, type ProviderIdType } from '@reown/appkit/store' - -// -- Types --------------------------------------------------------------------- -export interface AdapterOptions - extends Pick { - wagmiConfig: C - defaultNetwork?: Chain -} - -const OPTIONAL_METHODS = [ - 'eth_accounts', - 'eth_requestAccounts', - 'eth_sendRawTransaction', - 'eth_sign', - 'eth_signTransaction', - 'eth_signTypedData', - 'eth_signTypedData_v3', - 'eth_signTypedData_v4', - 'eth_sendTransaction', - 'personal_sign', - 'wallet_switchEthereumChain', - 'wallet_addEthereumChain', - 'wallet_getPermissions', - 'wallet_requestPermissions', - 'wallet_registerOnboarding', - 'wallet_watchAsset', - 'wallet_scanQRCode', - 'wallet_getCallsStatus', - 'wallet_sendCalls', - 'wallet_getCapabilities', - 'wallet_grantPermissions' -] - -// @ts-expect-error: Overridden state type is correct -interface AppKitState extends PublicStateControllerState { - selectedNetworkId: number | undefined -} - -// -- Client -------------------------------------------------------------------- -export class WagmiAdapter implements ChainAdapter { - // -- Private variables ------------------------------------------------------- - private appKit: AppKit | undefined = undefined - // -- Public variables -------------------------------------------------------- - public options: AppKitOptions | undefined = undefined +export class WagmiAdapter extends AdapterBlueprint { + public wagmiChains: readonly [Chain, ...Chain[]] | undefined + public wagmiConfig!: Config + public adapterType = 'wagmi' - public chainNamespace: ChainNamespace = CommonConstantsUtil.CHAIN.EVM - - public caipNetworks: [CaipNetwork, ...CaipNetwork[]] - - public wagmiChains: [BaseNetwork, ...BaseNetwork[]] - - public wagmiConfig: AdapterOptions['wagmiConfig'] - - public networkControllerClient?: NetworkControllerClient - - public connectionControllerClient?: ConnectionControllerClient - - public defaultCaipNetwork: CaipNetwork | undefined = undefined - - public tokens = HelpersUtil.getCaipTokens(this.options?.tokens) - - public siweControllerClient = this.options?.siweConfig - - public adapterType: AdapterType = 'wagmi' - - public constructor( + constructor( configParams: Partial & { networks: AppKitNetwork[] projectId: string } ) { - if (!configParams.projectId) { - throw new Error(ErrorUtil.ALERT_ERRORS.PROJECT_ID_NOT_CONFIGURED.shortMessage) - } + super({ + projectId: configParams.projectId, + networks: CaipNetworksUtil.extendCaipNetworks(configParams.networks, { + projectId: configParams.projectId, + customNetworkImageUrls: {} + }) as [CaipNetwork, ...CaipNetwork[]] + }) + this.namespace = CommonConstantsUtil.CHAIN.EVM + this.createConfig({ + networks: CaipNetworksUtil.extendCaipNetworks(configParams.networks, { + projectId: configParams.projectId, + customNetworkImageUrls: {} + }) as [CaipNetwork, ...CaipNetwork[]], + projectId: configParams.projectId + }) + this.setupWatchers() + } + private createConfig( + configParams: Partial & { + networks: CaipNetwork[] + projectId: string + } + ) { this.caipNetworks = CaipNetworksUtil.extendCaipNetworks(configParams.networks, { projectId: configParams.projectId, customNetworkImageUrls: {} @@ -189,32 +121,38 @@ export class WagmiAdapter implements ChainAdapter { transports, connectors }) + } - ChainController.subscribeKey('activeCaipAddress', val => { - const isEVMAddress = val?.startsWith('eip155:') - const caipNetwork = ChainController.state.activeCaipNetwork - const isEVMNetwork = caipNetwork?.chainNamespace === this.chainNamespace + private setupWatchers() { + watchAccount(this.wagmiConfig, { + onChange: accountData => { + if (accountData.address) { + this.emit('accountChanged', { + address: accountData.address, + chainId: accountData.chainId + }) + } + if (accountData.chainId) { + this.emit('switchNetwork', { + address: accountData.address, + chainId: accountData.chainId + }) + } + } + }) - if (caipNetwork && isEVMAddress && isEVMNetwork) { - this.setProfileAndBalance( - CoreHelperUtil.getPlainAddress(val) as Hex, - Number(caipNetwork.id) - ) + watchConnections(this.wagmiConfig, { + onChange: connections => { + if (connections.length === 0) { + this.emit('disconnect') + } } }) } - private setCustomConnectors(options: AppKitOptions, appKit: AppKit) { + private addWagmiConnectors(options: AppKitOptions, appKit: AppKit) { const customConnectors: CreateConnectorFn[] = [] - if (options.enableWalletConnect !== false) { - customConnectors.push(walletConnect(options, appKit, this.caipNetworks)) - } - - if (options.enableInjected !== false) { - customConnectors.push(injected({ shimDisconnect: true })) - } - if (options.enableCoinbase !== false) { customConnectors.push( coinbaseWallet({ @@ -226,6 +164,16 @@ export class WagmiAdapter implements ChainAdapter { ) } + if (options.enableWalletConnect !== false) { + customConnectors.push( + walletConnect(options, appKit, this.caipNetworks as [CaipNetwork, ...CaipNetwork[]]) + ) + } + + if (options.enableInjected !== false) { + customConnectors.push(injected({ shimDisconnect: true })) + } + const emailEnabled = options.features?.email === undefined ? CoreConstantsUtil.DEFAULT_FEATURES.email @@ -238,7 +186,9 @@ export class WagmiAdapter implements ChainAdapter { customConnectors.push( authConnector({ chains: this.wagmiChains, - options: { projectId: options.projectId } + options: { projectId: options.projectId }, + provider: this.availableConnectors.find(c => c.id === ConstantsUtil.AUTH_CONNECTOR_ID) + ?.provider as W3mFrameProvider }) ) } @@ -249,843 +199,292 @@ export class WagmiAdapter implements ChainAdapter { }) } - public construct(appKit: AppKit, options: AppKitOptionsWithCaipNetworks) { - this.appKit = appKit - this.options = options - this.caipNetworks = options.networks - this.defaultCaipNetwork = options.defaultNetwork || options.networks?.[0] - this.tokens = HelpersUtil.getCaipTokens(options.tokens) - this.setCustomConnectors(options, appKit) + public async signMessage( + params: AdapterBlueprint.SignMessageParams + ): Promise { + try { + const signature = await signMessage(this.wagmiConfig, { + message: params.message, + account: params.address as Hex + }) - if (!this.wagmiConfig) { - throw new Error('appkit:wagmiConfig - is undefined') + return { signature } + } catch (error) { + throw new Error('WagmiAdapter:signMessage - Sign message failed') } + } - this.networkControllerClient = { - switchCaipNetwork: async caipNetwork => { - const chainId = caipNetwork?.id as number | undefined - - if (chainId && this.wagmiConfig) { - await switchChain(this.wagmiConfig, { chainId }) - } - }, - getApprovedCaipNetworksData: async () => { - if (!this.wagmiConfig) { - throw new Error( - 'networkControllerClient:getApprovedCaipNetworksData - wagmiConfig is undefined' - ) - } - - return new Promise(resolve => { - const connections = new Map(this.wagmiConfig.state.connections) - const connection = connections.get(this.wagmiConfig.state.current || '') - if (connection?.connector?.id === ConstantsUtil.AUTH_CONNECTOR_ID) { - resolve(getEmailCaipNetworks()) - } else if (connection?.connector?.id === ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID) { - const connector = this.wagmiConfig.connectors.find( - c => c.id === ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID - ) - resolve(getWalletConnectCaipNetworks(connector)) - } - resolve({ approvedCaipNetworkIds: [], supportsAllNetworks: true }) - }) - } + public async sendTransaction( + params: AdapterBlueprint.SendTransactionParams + ): Promise { + const { chainId } = getAccount(this.wagmiConfig) + const txParams = { + account: params.address, + to: params.to as Hex, + value: params.value as bigint, + gas: params.gas as bigint, + gasPrice: params.gasPrice as bigint, + data: params.data as Hex, + chainId, + type: 'legacy' as const } + await prepareTransactionRequest(this.wagmiConfig, txParams) + const tx = await wagmiSendTransaction(this.wagmiConfig, txParams) + await waitForTransactionReceipt(this.wagmiConfig, { hash: tx, timeout: 25000 }) - this.connectionControllerClient = { - connectWalletConnect: async () => { - if (!this.wagmiConfig) { - throw new Error( - 'connectionControllerClient:getWalletConnectUri - wagmiConfig is undefined' - ) - } - const connector = this.wagmiConfig.connectors.find( - c => c.id === ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID - ) - - if (!connector) { - throw new Error('connectionControllerClient:getWalletConnectUri - connector is undefined') - } - - const provider = (await connector.getProvider()) as Awaited< - ReturnType<(typeof UniversalProvider)['init']> - > + return { hash: tx } + } - const clientId = await provider?.client?.core?.crypto?.getClientId() - if (clientId) { - this.appKit?.setClientId(clientId) - } + public async writeContract( + params: AdapterBlueprint.WriteContractParams + ): Promise { + const { caipNetwork, ...data } = params + const chainId = Number(NetworkUtil.caipNetworkIdToNumber(caipNetwork.caipNetworkId)) + + const tx = await wagmiWriteContract(this.wagmiConfig, { + chain: this.wagmiChains?.[chainId], + chainId, + address: data.tokenAddress as Hex, + account: data.fromAddress as Hex, + abi: data.abi, + functionName: data.method, + args: [data.receiverAddress, data.tokenAmount] + }) - const siweParams = await this.options?.siweConfig?.getMessageParams?.() - const isSiweEnabled = this.options?.siweConfig?.options?.enabled - const isProviderSupported = typeof provider?.authenticate === 'function' - const isSiweParamsValid = siweParams && Object.keys(siweParams || {}).length > 0 - const siweConfig = this.options?.siweConfig - - if (isSiweEnabled && isProviderSupported && isSiweParamsValid && siweConfig) { - // @ts-expect-error - setting requested chains beforehand avoids wagmi auto disconnecting the session when `connect` is called because it things chains are stale - await connector.setRequestedChainsIds(siweParams.chains) - - const { SIWEController, getDidChainId, getDidAddress } = await import( - '@reown/appkit-siwe' - ) - - const chains = this.caipNetworks - ?.filter(network => network.chainNamespace === 'eip155') - .map(chain => chain.caipNetworkId) as string[] - - siweParams.chains = this.caipNetworks - ?.filter(network => network.chainNamespace === 'eip155') - .map(chain => chain.id) as number[] - - const result = await provider.authenticate({ - nonce: await siweConfig.getNonce(), - methods: [...OPTIONAL_METHODS], - ...siweParams, - chains - }) - // Auths is an array of signed CACAO objects https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-74.md - const signedCacao = result?.auths?.[0] - - if (signedCacao) { - const { p, s } = signedCacao - const cacaoChainId = getDidChainId(p.iss) - const address = getDidAddress(p.iss) - if (address && cacaoChainId) { - SIWEController.setSession({ - address, - chainId: parseInt(cacaoChainId, 10) - }) - } - - try { - // Kicks off verifyMessage and populates external states - const message = provider.client.formatAuthMessage({ - request: p, - iss: p.iss - }) - - await SIWEController.verifyMessage({ - message, - signature: s.s, - cacao: signedCacao - }) - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error verifying message', error) - // eslint-disable-next-line no-console - await provider.disconnect().catch(console.error) - // eslint-disable-next-line no-console - await SIWEController.signOut().catch(console.error) - throw error - } - } - } + return { hash: tx } + } - const chainId = this.appKit?.getCaipNetworkId() - await connect(this.wagmiConfig, { connector, chainId }) - }, - connectExternal: async ({ id, provider, info }) => { - if (!this.wagmiConfig) { - throw new Error( - 'networkControllerClient:getApprovedCaipNetworksData - wagmiConfig is undefined' - ) - } - const connector = this.wagmiConfig.connectors.find(c => c.id === id) - if (!connector) { - throw new Error('connectionControllerClient:connectExternal - connector is undefined') - } - this.appKit?.setClientId(null) - if (provider && info && connector.id === ConstantsUtil.EIP6963_CONNECTOR_ID) { - // @ts-expect-error Exists on EIP6963Connector - connector.setEip6963Wallet?.({ provider, info }) - } - const chainId = this.appKit?.getCaipNetworkId() - await connect(this.wagmiConfig, { connector, chainId }) - }, - reconnectExternal: async ({ id }) => { - if (!this.wagmiConfig) { - throw new Error('networkControllerClient:reconnectExternal - wagmiConfig is undefined') - } - const connector = this.wagmiConfig.connectors.find(c => c.id === id) - if (!connector) { - throw new Error('connectionControllerClient:reconnectExternal - connector is undefined') - } - await reconnect(this.wagmiConfig, { connectors: [connector] }) - }, - checkInstalled: ids => { - const injectedConnector = this.appKit - ?.getConnectors() - .find((c: Connector) => c.type === 'INJECTED') - if (!ids) { - return Boolean(window.ethereum) - } - if (injectedConnector) { - if (!window?.ethereum) { - return false - } + public async getEnsAddress( + params: AdapterBlueprint.GetEnsAddressParams + ): Promise { + const { name, caipNetwork } = params - return ids.some(id => Boolean(window.ethereum?.[String(id)])) - } + try { + if (!this.wagmiConfig) { + throw new Error( + 'networkControllerClient:getApprovedCaipNetworksData - wagmiConfig is undefined' + ) + } - return false - }, - disconnect: async () => { - await disconnect(this.wagmiConfig) - if (this.options?.siweConfig?.options?.signOutOnDisconnect) { - const { SIWEController } = await import('@reown/appkit-siwe') - await SIWEController.signOut() - } - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_ID) - SafeLocalStorage.removeItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR) - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_NAME) - this.appKit?.setClientId(null) - this.syncAccount({ - address: undefined, - chainId: undefined, - connector: undefined, - addresses: undefined, - status: 'disconnected' + let ensName: boolean | GetEnsAddressReturnType = false + let wcName: boolean | string = false + if (isReownName(name)) { + wcName = (await WcHelpersUtil.resolveReownName(name)) || false + } + if (caipNetwork.id === 1) { + ensName = await wagmiGetEnsAddress(this.wagmiConfig, { + name: normalize(name), + chainId: caipNetwork.id }) - // Should we do this? - this.appKit?.resetAccount('solana') - }, - signMessage: async message => { - const caipAddress = this.appKit?.getCaipAddress() || '' - const account = requireCaipAddress(caipAddress) - - return signMessage(this.wagmiConfig, { message, account }) - }, - estimateGas: async args => { - if (args.chainNamespace && args.chainNamespace !== 'eip155') { - throw new Error(`Invalid chain namespace - Expected eip155, got ${args.chainNamespace}`) - } - try { - return await wagmiEstimateGas(this.wagmiConfig, { - account: args.address, - to: args.to, - data: args.data, - type: 'legacy' - }) - } catch (error) { - return BigInt(0) - } - }, - - getCapabilities: async (params: string) => { - if (!this.wagmiConfig) { - throw new Error('connectionControllerClient:getCapabilities - wagmiConfig is undefined') - } - - const connections = getConnections(this.wagmiConfig) - const connection = connections[0] - - if (!connection?.connector) { - throw new Error('connectionControllerClient:getCapabilities - connector is undefined') - } - - const provider = (await connection.connector.getProvider()) as UniversalProvider - - if (!provider) { - throw new Error('connectionControllerClient:getCapabilities - provider is undefined') - } - - const walletCapabilitiesString = provider.session?.sessionProperties?.['capabilities'] - if (walletCapabilitiesString) { - const walletCapabilities = parseWalletCapabilities(walletCapabilitiesString) - const accountCapabilities = walletCapabilities[params] - if (accountCapabilities) { - return accountCapabilities - } - } - - return await provider.request({ method: 'wallet_getCapabilities', params: [params] }) - }, - - grantPermissions: async params => { - if (!this.wagmiConfig) { - throw new Error('connectionControllerClient:grantPermissions - wagmiConfig is undefined') - } - - const connections = getConnections(this.wagmiConfig) - const connection = connections[0] - - if (!connection?.connector) { - throw new Error('connectionControllerClient:grantPermissions - connector is undefined') - } - - const provider = (await connection.connector.getProvider()) as UniversalProvider - - if (!provider) { - throw new Error('connectionControllerClient:grantPermissions - provider is undefined') - } - - return provider.request({ method: 'wallet_grantPermissions', params }) - }, - revokePermissions: async session => { - if (!this.wagmiConfig) { - throw new Error('connectionControllerClient:revokePermissions - wagmiConfig is undefined') - } - - const connections = getConnections(this.wagmiConfig) - const connection = connections[0] - - if (!connection?.connector) { - throw new Error('connectionControllerClient:revokePermissions - connector is undefined') - } - - const provider = (await connection.connector.getProvider()) as UniversalProvider + } - if (!provider) { - throw new Error('connectionControllerClient:revokePermissions - provider is undefined') - } + return { address: (ensName as string) || (wcName as string) || false } + } catch { + return { address: false } + } + } - return provider.request({ method: 'wallet_revokePermissions', params: [session] }) - }, + public async estimateGas( + params: AdapterBlueprint.EstimateGasTransactionArgs + ): Promise { + try { + const result = await wagmiEstimateGas(this.wagmiConfig, { + account: params.address as Hex, + to: params.to as Hex, + data: params.data as Hex, + type: 'legacy' + }) - sendTransaction: async (data: SendTransactionArgs) => { - if (data.chainNamespace && data.chainNamespace !== 'eip155') { - throw new Error(`Invalid chain namespace - Expected eip155, got ${data.chainNamespace}`) - } - const { chainId } = getAccount(this.wagmiConfig) - const txParams = { - account: data.address, - to: data.to, - value: data.value, - gas: data.gas, - gasPrice: data.gasPrice, - data: data.data, - chainId, - type: 'legacy' as const - } - await prepareTransactionRequest(this.wagmiConfig, txParams) - const tx = await wagmiSendTransaction(this.wagmiConfig, txParams) - await waitForTransactionReceipt(this.wagmiConfig, { hash: tx, timeout: 25000 }) - - return tx - }, - writeContract: async (data: WriteContractArgs) => { - const caipAddress = this.appKit?.getCaipAddress() || '' - const account = requireCaipAddress(caipAddress) - const chainId = this.appKit?.getCaipNetworkId() - - if (!chainId) { - throw new Error('networkControllerClient:writeContract - chainId is undefined') - } + return { gas: result } + } catch (error) { + throw new Error('WagmiAdapter:estimateGas - error estimating gas') + } + } - const tx = await wagmiWriteContract(this.wagmiConfig, { - chain: this.wagmiChains?.[chainId], - chainId, - address: data.tokenAddress, - account, - abi: data.abi, - functionName: data.method, - args: [data.receiverAddress, data.tokenAmount] - }) + public parseUnits(params: AdapterBlueprint.ParseUnitsParams): AdapterBlueprint.ParseUnitsResult { + return parseUnits(params.value, params.decimals) + } - return tx - }, - getEnsAddress: async (value: string) => { - try { - if (!this.wagmiConfig) { - throw new Error( - 'networkControllerClient:getApprovedCaipNetworksData - wagmiConfig is undefined' - ) - } - const chainId = this.appKit?.getCaipNetworkId() - - let ensName: boolean | GetEnsAddressReturnType = false - let wcName: boolean | string = false - - if (isReownName(value)) { - wcName = (await this.appKit?.resolveReownName(value)) || false - } - if (chainId === 1) { - ensName = await wagmiGetEnsAddress(this.wagmiConfig, { - name: normalize(value), - chainId - }) - } - - return ensName || wcName || false - } catch { - return false - } - }, - getEnsAvatar: async (value: string) => { - const chainId = this.appKit?.getCaipNetworkId() - if (chainId !== mainnet.id) { - return false - } - const avatar = await wagmiGetEnsAvatar(this.wagmiConfig, { - name: normalize(value), - chainId - }) + public formatUnits( + params: AdapterBlueprint.FormatUnitsParams + ): AdapterBlueprint.FormatUnitsResult { + return formatUnits(params.value, params.decimals) + } - return avatar || false - }, - parseUnits, - formatUnits - } + public syncConnectors(options: AppKitOptions, appKit: AppKit) { + this.addWagmiConnectors(options, appKit) - ChainController.state.chains.set(this.chainNamespace, { - chainNamespace: this.chainNamespace, - connectionControllerClient: this.connectionControllerClient, - networkControllerClient: this.networkControllerClient, - adapterType: this.adapterType, - caipNetworks: this.caipNetworks - }) + const connectors = this.wagmiConfig.connectors.map(connector => ({ + ...connector, + chain: this.namespace + })) - this.syncConnectors(this.wagmiConfig.connectors) - this.syncAuthConnector( - this.wagmiConfig?.connectors.find(c => c.id === ConstantsUtil.AUTH_CONNECTOR_ID) - ) - this.syncRequestedNetworks(this.caipNetworks) + const uniqueIds = new Set() + const filteredConnectors = connectors.filter(item => { + const isDuplicate = uniqueIds.has(item.id) + uniqueIds.add(item.id) - watchConnectors(this.wagmiConfig, { - onChange: _connectors => { - this.syncConnectors(_connectors) - this.syncAuthConnector(_connectors.find(c => c.id === ConstantsUtil.AUTH_CONNECTOR_ID)) - } - }) - watchAccount(this.wagmiConfig, { - onChange: accountData => { - this.syncAccount(accountData) - } + return !isDuplicate }) - this.appKit?.setEIP6963Enabled(options.enableEIP6963 !== false) - this.appKit?.subscribeShouldUpdateToAddress((newAddress?: string) => { - if (newAddress) { - const connections = getConnections(this.wagmiConfig) - const connector = connections[0]?.connector - if (connector) { - switchAccount(this.wagmiConfig, { - connector - }).then(response => - this.syncAccount({ - address: newAddress as Hex, - isConnected: true, - addresses: response.accounts, - connector, - chainId: response.chainId, - status: 'connected' - }) - ) - } + filteredConnectors.forEach(connector => { + const shouldSkip = ConstantsUtil.AUTH_CONNECTOR_ID === connector.id + + if (!shouldSkip && this.namespace) { + this.addConnector({ + id: connector.id, + explorerId: PresetsUtil.ConnectorExplorerIds[connector.id], + imageUrl: options?.connectorImages?.[connector.id] ?? connector.icon, + name: PresetsUtil.ConnectorNamesMap[connector.id] ?? connector.name, + imageId: PresetsUtil.ConnectorImageIds[connector.id], + type: PresetsUtil.ConnectorTypesMap[connector.type] ?? 'EXTERNAL', + info: { rdns: connector.id }, + chain: this.namespace, + chains: [] + }) } }) } - // @ts-expect-error: Overriden state type is correct - public override subscribeState(callback: (state: AppKitState) => void) { - return this.appKit?.subscribeState((state: PublicStateControllerState) => - callback({ - ...state, - selectedNetworkId: state.selectedNetworkId - ? Number(NetworkUtil.caipNetworkIdToNumber(state.selectedNetworkId)) - : undefined - }) - ) - } - - // -- Private ----------------------------------------------------------------- - private syncRequestedNetworks(caipNetworks: CaipNetwork[]) { - const uniqueChainNamespaces = Array.from( - new Set(caipNetworks.map(caipNetwork => caipNetwork.chainNamespace)) - ) - uniqueChainNamespaces - .filter(c => Boolean(c)) - .forEach(chainNamespace => { - this.appKit?.setRequestedCaipNetworks( - caipNetworks.filter(caipNetwork => caipNetwork.chainNamespace === chainNamespace), - chainNamespace - ) - }) + public async syncConnection( + params: AdapterBlueprint.SyncConnectionParams + ): Promise { + const { id, chainId } = params + const connections = getConnections(this.wagmiConfig) + const connection = connections.find(c => c.connector.id === id) + const connector = this.wagmiConfig.connectors.find(c => c.id === id) + const provider = (await connector?.getProvider()) as Provider + + return { + chainId: Number(chainId), + address: connection?.accounts[0] as string, + provider, + type: connection?.connector.type as ConnectorType, + id: connection?.connector.id as string + } } - private async setProfileAndBalance(address: Hex, chainId: number) { - await Promise.all([this.syncProfile(address, chainId), this.syncBalance(address, chainId)]) - } + public async connectWalletConnect(onUri: (uri: string) => void, chainId?: number | string) { + const connector = this.wagmiConfig.connectors.find( + c => c.type === 'walletConnect' + ) as unknown as Connector - private async syncAccount({ - address, - chainId, - connector, - addresses, - status - }: Partial< - Pick< - GetAccountReturnType, - | 'address' - | 'isConnected' - | 'isDisconnected' - | 'chainId' - | 'connector' - | 'addresses' - | 'status' - > - >) { - const isAuthConnector = connector?.id === ConstantsUtil.AUTH_CONNECTOR_ID - if (status === 'disconnected') { - this.appKit?.resetAccount(this.chainNamespace) - this.appKit?.resetWcConnection() - this.appKit?.resetNetwork(this.chainNamespace) - this.appKit?.setAllAccounts([], this.chainNamespace) - this.appKit?.setStatus(status, this.chainNamespace) - this.appKit?.setLoading(false) - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_ID) - if (isAuthConnector) { - await connector.disconnect() - } + const provider = (await connector.getProvider()) as UniversalProvider - return + if (!this.caipNetworks || !provider) { + throw new Error( + 'UniversalAdapter:connectWalletConnect - caipNetworks or provider is undefined' + ) } - if (this.wagmiConfig) { - if (connector) { - if (connector.name === 'WalletConnect' && connector.getProvider && address) { - const activeCaipNetwork = this.appKit?.getCaipNetwork() - const currentChainId = chainId || (activeCaipNetwork?.id as number | undefined) - const provider = (await connector.getProvider()) as UniversalProvider - - const namespaces = provider?.session?.namespaces || {} - const namespaceKeys = namespaces ? Object.keys(namespaces) : [] - - const preferredAccountType = this.appKit?.getPreferredAccountType() - namespaceKeys.forEach(key => { - const chainNamespace = key as ChainNamespace - const caipAddress = namespaces?.[key]?.accounts[0] as CaipAddress - - ProviderUtil.setProvider(chainNamespace, provider) - ProviderUtil.setProviderId(chainNamespace, 'walletConnect') + provider.on('display_uri', (uri: string) => { + onUri(uri) + }) - this.appKit?.setPreferredAccountType(preferredAccountType, chainNamespace) - this.appKit?.setCaipAddress(caipAddress, chainNamespace) - this.appKit?.setStatus(status, chainNamespace) - }) - if ( - this.appKit?.getCaipNetwork()?.chainNamespace !== CommonConstantsUtil.CHAIN.SOLANA && - currentChainId - ) { - this.syncNetwork(address, currentChainId, true) - await Promise.all([ - this.syncConnectedWalletInfo(connector), - this.appKit?.setApprovedCaipNetworksData(this.chainNamespace) - ]) - } - this.appKit?.setLoading(false) - } else if (status === 'connected' && address && chainId) { - ProviderUtil.setProvider(this.chainNamespace, await connector.getProvider()) - ProviderUtil.setProviderId(this.chainNamespace, connector.id as ProviderIdType) - const caipAddress = `eip155:${chainId}:${address}` as CaipAddress - this.syncNetwork(address, chainId, true) - await Promise.all([ - this.syncConnectedWalletInfo(connector), - this.appKit?.setApprovedCaipNetworksData(this.chainNamespace) - ]) - this.appKit?.setLoading(false) - this.appKit?.setCaipAddress(caipAddress, this.chainNamespace) - this.appKit?.setStatus('connected', this.chainNamespace) - // Set by authConnector.onIsConnectedHandler as we need the account type - if (!isAuthConnector && addresses?.length) { - this.appKit?.setAllAccounts( - addresses.map(addr => ({ address: addr, type: 'eoa' })), - this.chainNamespace - ) - } - } else if (status === 'reconnecting') { - this.appKit?.setLoading(true) - } - } - } + await connect(this.wagmiConfig, { connector, chainId: chainId ? Number(chainId) : undefined }) } - private syncNetwork(address?: Hex, chainId?: number, isConnected?: boolean) { - const caipNetwork = this.caipNetworks.find((c: CaipNetwork) => c.id === chainId) - - if (caipNetwork && chainId) { - this.appKit?.setCaipNetwork(caipNetwork) + public async connect( + params: AdapterBlueprint.ConnectParams + ): Promise { + const { id, provider, type, info, chainId } = params - if (isConnected && address && chainId) { - const caipAddress: CaipAddress = `eip155:${chainId}:${address}` - this.appKit?.setCaipAddress(caipAddress, this.chainNamespace) - if (caipNetwork?.blockExplorers?.default.url) { - const url = `${caipNetwork.blockExplorers.default.url}/address/${address}` - this.appKit?.setAddressExplorerUrl(url, this.chainNamespace) - } else { - this.appKit?.setAddressExplorerUrl(undefined, this.chainNamespace) - } - } + const connector = this.wagmiConfig.connectors.find(c => c.id === id) + if (!connector) { + throw new Error('connectionControllerClient:connectExternal - connector is undefined') } - } - private async syncReownName(address: Hex) { - if (!this.appKit) { - throw new Error('syncReownName - appKit is undefined') + if (provider && info && connector.id === ConstantsUtil.EIP6963_CONNECTOR_ID) { + // @ts-expect-error Exists on EIP6963Connector + connector.setEip6963Wallet?.({ provider, info }) } - try { - const registeredWcNames = await this.appKit.getReownName(address) - if (registeredWcNames[0]) { - const wcName = registeredWcNames[0] - this.appKit?.setProfileName(wcName.name, this.chainNamespace) - } else { - this.appKit?.setProfileName(null, this.chainNamespace) - } - } catch { - this.appKit?.setProfileName(null, this.chainNamespace) - } - } + const res = await connect(this.wagmiConfig, { + connector, + chainId: chainId ? Number(chainId) : undefined + }) - private async syncProfile(address: Hex, chainId: Chain['id']) { - if (!this.appKit) { - throw new Error('syncProfile - appKit is undefined') + return { + address: res.accounts[0], + chainId: res.chainId, + provider: provider as Provider, + type: type as ConnectorType, + id } + } - try { - const { name, avatar } = await this.appKit.fetchIdentity({ - address - }) - this.appKit?.setProfileName(name, this.chainNamespace) - this.appKit?.setProfileImage(avatar, this.chainNamespace) + public override async reconnect(params: AdapterBlueprint.ConnectParams): Promise { + const { id } = params - if (!name) { - await this.syncReownName(address) - } - } catch { - if (chainId === mainnet.id) { - const profileName = await getEnsName(this.wagmiConfig, { address, chainId }) - if (profileName) { - this.appKit?.setProfileName(profileName, this.chainNamespace) - const profileImage = await wagmiGetEnsAvatar(this.wagmiConfig, { - name: profileName, - chainId - }) - if (profileImage) { - this.appKit?.setProfileImage(profileImage, this.chainNamespace) - } - } else { - await this.syncReownName(address) - this.appKit?.setProfileImage(null, this.chainNamespace) - } - } else { - await this.syncReownName(address) - this.appKit?.setProfileImage(null, this.chainNamespace) - } + const connector = this.wagmiConfig.connectors.find(c => c.id === id) + if (!connector) { + throw new Error('connectionControllerClient:connectExternal - connector is undefined') } + + await reconnect(this.wagmiConfig, { + connectors: [connector] + }) } - private async syncBalance(address: Hex, chainId: number) { - const caipNetwork = this.caipNetworks.find((c: CaipNetwork) => c.id === chainId) + public async getBalance( + params: AdapterBlueprint.GetBalanceParams + ): Promise { + const caipNetwork = this.caipNetworks?.find(network => network.id === params.chainId) if (caipNetwork && this.wagmiConfig) { + const chainId = Number(params.chainId) const balance = await getBalance(this.wagmiConfig, { - address, + address: params.address as Hex, chainId, - token: this.options?.tokens?.[caipNetwork.caipNetworkId]?.address as Hex + token: params.tokens?.[caipNetwork.caipNetworkId]?.address as Hex }) - this.appKit?.setBalance(balance.formatted, balance.symbol, this.chainNamespace) - return - } - this.appKit?.setBalance(undefined, undefined, this.chainNamespace) - } - - private async syncConnectedWalletInfo(connector: GetAccountReturnType['connector']) { - if (!connector) { - throw Error('syncConnectedWalletInfo - connector is undefined') + return { balance: balance.formatted, symbol: balance.symbol } } - if (connector.id === ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID && connector.getProvider) { - const walletConnectProvider = (await connector.getProvider()) as Awaited< - ReturnType<(typeof UniversalProvider)['init']> - > - if (walletConnectProvider.session) { - this.appKit?.setConnectedWalletInfo( - { - ...walletConnectProvider.session.peer.metadata, - name: walletConnectProvider.session.peer.metadata.name, - icon: walletConnectProvider.session.peer.metadata.icons?.[0] - }, - this.chainNamespace - ) - } - } else { - const wagmiConnector = this.appKit?.getConnectors().find(c => c.id === connector.id) - this.appKit?.setConnectedWalletInfo( - { - name: connector.name, - icon: connector.icon || this.appKit.getConnectorImage(wagmiConnector) - }, - this.chainNamespace - ) - } + return { balance: '', symbol: '' } } - private syncConnectors(_connectors: AdapterOptions['wagmiConfig']['connectors']) { - const connectors = _connectors.map(connector => ({ ...connector, chain: this.chainNamespace })) - const uniqueIds = new Set() - const filteredConnectors = connectors.filter(item => { - const isDuplicate = uniqueIds.has(item.id) - uniqueIds.add(item.id) - - return !isDuplicate - }) - - const w3mConnectors: Connector[] = [] - - filteredConnectors.forEach(({ id, name, type, icon }) => { - // Auth connector is initialized separately - const shouldSkip = ConstantsUtil.AUTH_CONNECTOR_ID === id - if (!shouldSkip) { - const injectedConnector = id === ConstantsUtil.INJECTED_CONNECTOR_ID - - w3mConnectors.push({ - id, - explorerId: PresetsUtil.ConnectorExplorerIds[id], - imageUrl: this.options?.connectorImages?.[id] ?? icon, - name: PresetsUtil.ConnectorNamesMap[id] ?? name, - imageId: PresetsUtil.ConnectorImageIds[id], - type: PresetsUtil.ConnectorTypesMap[type] ?? 'EXTERNAL', - info: injectedConnector ? undefined : { rdns: id }, - chain: this.chainNamespace - }) - } + public async getProfile( + params: AdapterBlueprint.GetProfileParams + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const chainId = params.chainId as number + const profileName = await getEnsName(this.wagmiConfig, { + address: params.address as Hex, + chainId }) - - this.appKit?.setConnectors(w3mConnectors) - } - - private async syncAuthConnector( - _authConnector: AdapterOptions['wagmiConfig']['connectors'][number] | undefined - ) { - const connector = - _authConnector as unknown as AdapterOptions['wagmiConfig']['connectors'][0] - - if (connector) { - const provider = await connector.getProvider() - this.appKit?.addConnector({ - id: ConstantsUtil.AUTH_CONNECTOR_ID, - type: 'AUTH', - name: 'w3mAuth', - provider, - chain: this.chainNamespace + if (profileName) { + const profileImage = await getEnsAvatar(this.wagmiConfig, { + name: profileName, + chainId }) - this.initAuthConnectorListeners(_authConnector) - } - } - private async initAuthConnectorListeners( - _authConnector: AdapterOptions['wagmiConfig']['connectors'][number] | undefined - ) { - if (_authConnector) { - await this.listenAuthConnector(_authConnector) - await this.listenModal(_authConnector) + return { profileName, profileImage: profileImage ?? undefined } } - } - private async listenAuthConnector( - connector: AdapterOptions['wagmiConfig']['connectors'][number], - bypassWindowCheck = false - ) { - if (bypassWindowCheck || (typeof window !== 'undefined' && connector)) { - const provider = (await connector.getProvider()) as W3mFrameProvider - - provider.onRpcRequest((request: W3mFrameTypes.RPCRequest) => { - if (W3mFrameHelpers.checkIfRequestExists(request)) { - if (!W3mFrameHelpers.checkIfRequestIsSafe(request)) { - this.appKit?.handleUnsafeRPCRequest() - } - } else { - this.appKit?.open() - // eslint-disable-next-line no-console - console.error(W3mFrameRpcConstants.RPC_METHOD_NOT_ALLOWED_MESSAGE, { - method: request.method - }) - setTimeout(() => { - this.appKit?.showErrorMessage(W3mFrameRpcConstants.RPC_METHOD_NOT_ALLOWED_UI_MESSAGE) - }, 300) - provider.rejectRpcRequests() - } - }) - - provider.onRpcError(() => { - const isModalOpen = this.appKit?.isOpen() - - if (isModalOpen) { - if (this.appKit?.isTransactionStackEmpty()) { - this.appKit?.close() - } else { - this.appKit?.popTransactionStack(true) - } - } - }) - - provider.onRpcSuccess((_, request) => { - const isSafeRequest = W3mFrameHelpers.checkIfRequestIsSafe(request) - if (isSafeRequest) { - return - } - - if (this.appKit?.isTransactionStackEmpty()) { - this.appKit?.close() - } else { - this.appKit?.popTransactionStack() - } - }) + return { profileName: undefined, profileImage: undefined } + } - provider.onNotConnected(() => { - const isConnected = this.appKit?.getIsConnectedState() - const connectedConnector = SafeLocalStorage.getItem( - SafeLocalStorageKeys.CONNECTED_CONNECTOR - ) - const isConnectedWithAuth = connectedConnector === 'AUTH' + public getWalletConnectProvider(): AdapterBlueprint.GetWalletConnectProviderResult { + return this.wagmiConfig.connectors.find(c => c.type === 'walletConnect')?.[ + 'provider' + ] as UniversalProvider + } - if (!isConnected && isConnectedWithAuth) { - this.appKit?.setCaipAddress(undefined, this.chainNamespace) - this.appKit?.setLoading(false) + public async disconnect() { + const connections = getConnections(this.wagmiConfig) + await Promise.all( + connections.map(async connection => { + const connector = connection?.connector + if (connector) { + await wagmiDisconnect(this.wagmiConfig, { connector }) } }) - - provider.onConnect(user => { - const caipAddress = `eip155:${user.chainId}:${user.address}` as CaipAddress - this.appKit?.setCaipAddress(caipAddress, this.chainNamespace) - this.appKit?.setSmartAccountDeployed( - Boolean(user.smartAccountDeployed), - this.chainNamespace - ) - this.appKit?.setPreferredAccountType( - user.preferredAccountType as W3mFrameTypes.AccountType, - this.chainNamespace - ) - this.appKit?.setAllAccounts( - user.accounts || [ - { - address: user.address, - type: (user.preferredAccountType || 'eoa') as W3mFrameTypes.AccountType - } - ], - this.chainNamespace - ) - StorageUtil.setConnectedConnector('AUTH') - this.appKit?.setLoading(false) - }) - - provider.onGetSmartAccountEnabledNetworks(networks => { - this.appKit?.setSmartAccountEnabledNetworks(networks, this.chainNamespace) - }) - } + ) } - private async listenModal( - connector: AdapterOptions['wagmiConfig']['connectors'][number] - ) { - const provider = (await connector.getProvider()) as W3mFrameProvider - this.subscribeState(val => { - if (!val.open) { - provider.rejectRpcRequests() - } - }) + public async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams) { + await switchChain(this.wagmiConfig, { chainId: params.caipNetwork.id as number }) } } diff --git a/packages/adapters/wagmi/src/connectors/AuthConnector.ts b/packages/adapters/wagmi/src/connectors/AuthConnector.ts index 56942d8449..a82190a9a6 100644 --- a/packages/adapters/wagmi/src/connectors/AuthConnector.ts +++ b/packages/adapters/wagmi/src/connectors/AuthConnector.ts @@ -16,6 +16,7 @@ interface W3mFrameProviderOptions { export type AuthParameters = { chains?: CreateConfigParameters['chains'] options: W3mFrameProviderOptions + provider: W3mFrameProvider } // -- Connector ------------------------------------------------------------------------------------ @@ -33,7 +34,7 @@ export function authConnector(parameters: AuthParameters) { return createConnector(config => ({ id: ConstantsUtil.AUTH_CONNECTOR_ID, name: 'AppKit Auth', - type: 'w3mAuth', + type: 'ID_AUTH', chain: CommonConstantsUtil.CHAIN.EVM, async connect(options = {}) { diff --git a/packages/adapters/wagmi/src/connectors/AuthConnectorExport.ts b/packages/adapters/wagmi/src/connectors/AuthConnectorExport.ts index e724c12deb..684fe0f86a 100644 --- a/packages/adapters/wagmi/src/connectors/AuthConnectorExport.ts +++ b/packages/adapters/wagmi/src/connectors/AuthConnectorExport.ts @@ -1,5 +1,8 @@ import type { CreateConfigParameters } from '@wagmi/core' import { authConnector as authConnectorWagmi } from './AuthConnector.js' +import { ErrorUtil } from '@reown/appkit-utils' +import { AlertController } from '@reown/appkit-core' +import { W3mFrameProviderSingleton } from '@reown/appkit/auth-provider' interface W3mFrameProviderOptions { projectId: string @@ -11,5 +14,13 @@ export type AuthParameters = { } export function authConnector(parameters: AuthParameters) { - return authConnectorWagmi(parameters) + return authConnectorWagmi({ + ...parameters, + provider: W3mFrameProviderSingleton.getInstance({ + projectId: parameters.options.projectId, + onTimeout: () => { + AlertController.open(ErrorUtil.ALERT_ERRORS.INVALID_APP_CONFIGURATION, 'error') + } + }) + }) } diff --git a/packages/adapters/wagmi/src/connectors/UniversalConnector.ts b/packages/adapters/wagmi/src/connectors/UniversalConnector.ts index 69afd6172e..6b0e944551 100644 --- a/packages/adapters/wagmi/src/connectors/UniversalConnector.ts +++ b/packages/adapters/wagmi/src/connectors/UniversalConnector.ts @@ -216,7 +216,7 @@ export function walletConnect( return undefined } - const provider = appKit.universalAdapter?.getWalletConnectProvider() + const provider = await appKit.getUniversalProvider() if (!provider) { throw new Error('Provider not found') @@ -240,8 +240,6 @@ export function walletConnect( if (storedCaipNetwork && storedCaipNetwork.chainNamespace === ConstantsUtil.CHAIN.EVM) { await this.switchChain?.({ chainId: Number(storedCaipNetwork.id) }) - } else { - await this.switchChain?.({ chainId }) } } diff --git a/packages/adapters/wagmi/src/index.ts b/packages/adapters/wagmi/src/index.ts index 0fecfc9d21..5f23387c7b 100644 --- a/packages/adapters/wagmi/src/index.ts +++ b/packages/adapters/wagmi/src/index.ts @@ -2,8 +2,5 @@ import '@reown/appkit-polyfills' export { WagmiAdapter } from './client.js' -// -- Types -export type { AdapterOptions } from './client.js' - // -- Connectors export { authConnector } from './connectors/AuthConnector.js' diff --git a/packages/adapters/wagmi/src/tests/client.test.ts b/packages/adapters/wagmi/src/tests/client.test.ts index 4bef1ff2cb..223c21046b 100644 --- a/packages/adapters/wagmi/src/tests/client.test.ts +++ b/packages/adapters/wagmi/src/tests/client.test.ts @@ -1,614 +1,325 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -import { mockAccount, mockAppKit, mockOptions, mockWagmiClient } from './mocks/adapter.mock' -import { - arbitrum as AppkitArbitrum, - mainnet as AppkitMainnet, - polygon as AppkitPolygon, - optimism as AppkitOptimism, - bsc as AppkitBsc -} from '@reown/appkit/networks' -import { connect, disconnect, getAccount, getChainId, getEnsName, getBalance } from '@wagmi/core' -import { CaipNetworksUtil, ConstantsUtil } from '@reown/appkit-utils' -import type { CaipNetwork } from '@reown/appkit-common' -import { http } from 'viem' +import { describe, it, expect, beforeEach, vi } from 'vitest' import { WagmiAdapter } from '../client' - -const [mainnet, arbitrum] = CaipNetworksUtil.extendCaipNetworks( - [AppkitMainnet, AppkitArbitrum, AppkitPolygon, AppkitOptimism, AppkitBsc], - { customNetworkImageUrls: {}, projectId: '1234' } -) as [CaipNetwork, CaipNetwork, CaipNetwork, CaipNetwork, CaipNetwork] - -const mockOptionsExtended = { - ...mockOptions, - networks: mockAppKit.getCaipNetworks('eip155') as [CaipNetwork, ...CaipNetwork[]], - defaultNetwork: mainnet -} - -const mockConnector = mockWagmiClient.wagmiConfig.connectors[0]! +import type { Config } from '@wagmi/core' +import { + disconnect as wagmiDisconnect, + getConnections, + switchChain, + getBalance, + getEnsName, + getEnsAvatar, + signMessage, + estimateGas, + sendTransaction as wagmiSendTransaction, + getEnsAddress as wagmiGetEnsAddress, + writeContract as wagmiWriteContract, + waitForTransactionReceipt, + getAccount +} from '@wagmi/core' +import { mainnet } from '@wagmi/core/chains' +import { CaipNetworksUtil } from '@reown/appkit-utils' vi.mock('@wagmi/core', async () => { const actual = await vi.importActual('@wagmi/core') return { ...actual, - getEnsName: vi.fn(), + addConnector: vi.fn(), + connect: vi.fn(() => mockConnect()), + disconnect: vi.fn(), + createConfig: vi.fn(() => mockWagmiConfig), + getConnections: vi.fn(), + switchChain: vi.fn(), getBalance: vi.fn(), - getEnsAvatar: vi.fn() + getEnsName: vi.fn(), + getEnsAvatar: vi.fn(), + signMessage: vi.fn(), + estimateGas: vi.fn(), + sendTransaction: vi.fn(), + getEnsAddress: vi.fn(), + writeContract: vi.fn(), + waitForTransactionReceipt: vi.fn(), + getAccount: vi.fn(), + prepareTransactionRequest: vi.fn(), + reconnect: vi.fn(), + watchAccount: vi.fn(), + watchConnections: vi.fn() } }) -describe('Wagmi Client', () => { - beforeEach(() => { - vi.clearAllMocks() - ;(getEnsName as any).mockResolvedValue('mock.eth') - ;(getBalance as any).mockResolvedValue({ formatted: '1.0', symbol: 'ETH' }) - vi.spyOn(mockAppKit, 'fetchIdentity').mockResolvedValue({ name: 'example.eth', avatar: '' }) - vi.spyOn(mockAppKit, 'getReownName').mockImplementation(() => Promise.resolve([])) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - describe('Wagmi Client - Initialization', () => { - it('should initialize with default values', () => { - expect(mockWagmiClient.chainNamespace).toBe('eip155') - expect(mockWagmiClient.adapterType).toBe('wagmi') - }) - - it('should set caipNetworks to provided caipNetworks options', () => { - /** - * Specifically to Wagmi, we are mutating caipNetworks on both Wagmi constructor and when we set adapters. - * So there is not proper way to compare objects since imageId and imageUrl is added later. - */ - mockWagmiClient.caipNetworks.forEach((network, index) => { - expect(network.name).toEqual(mockOptionsExtended.networks[index]?.name) - }) - }) - - it('should set chain images', () => { - const client = new WagmiAdapter({ - projectId: '123', - networks: [mainnet, arbitrum] - }) +const mockProjectId = 'test-project-id' +const mockNetworks = [mainnet] +const mockCaipNetworks = CaipNetworksUtil.extendCaipNetworks(mockNetworks, { + projectId: mockProjectId, + customNetworkImageUrls: {} +}) - client.construct(mockAppKit, mockOptionsExtended) +const mockWagmiConfig = { + connectors: [ + { + id: 'test-connector' + } + ], + _internal: { + connectors: { + setup: vi.fn(), + setState: vi.fn() + } + } +} as unknown as Config - Object.entries(mockOptions.chainImages).map(([networkId, imageUrl]) => { - const caipNetwork = client.caipNetworks.find( - caipNetwork => caipNetwork.id === Number(networkId) - ) - expect(caipNetwork).toBeDefined() - expect(caipNetwork?.assets?.imageUrl).toEqual(imageUrl) - }) - }) +const mockConnect = vi.fn(() => ({ + chainId: 1, + address: '0x123', + accounts: ['0x123'] +})) - it('should set defaultNetwork to first caipNetwork option', () => { - /** - * Specifically to Wagmi, we are mutating caipNetworks on both Wagmi constructor and when we set adapters. - * So there is not proper way to compare objects since imageId and imageUrl is added later. - */ - expect(mockWagmiClient.defaultCaipNetwork?.id).toEqual(mainnet.id) - expect(mockWagmiClient.defaultCaipNetwork?.name).toEqual(mainnet.name) - }) +describe('WagmiAdapter', () => { + let adapter: WagmiAdapter - it('should create wagmi config', () => { - expect(mockWagmiClient['wagmiConfig']).toBeDefined() + beforeEach(() => { + vi.clearAllMocks() + adapter = new WagmiAdapter({ + networks: mockNetworks, + projectId: mockProjectId }) }) - describe('Wagmi Client - Network', () => { - it('should switch to correct chain', async () => { - await mockWagmiClient.networkControllerClient?.switchCaipNetwork(arbitrum) - - expect(getChainId(mockWagmiClient.wagmiConfig)).toBe(arbitrum.id) + describe('WagmiAdapter - constructor and initialization', () => { + it('should initialize with correct parameters', () => { + expect(adapter.projectId).toBe(mockProjectId) + expect(adapter.adapterType).toBe('wagmi') + expect(adapter.namespace).toBe('eip155') }) + }) - it('should sync the correct requested networks', async () => { - const setRequestedCaipNetworks = vi.spyOn(mockAppKit, 'setRequestedCaipNetworks') - - mockWagmiClient['syncRequestedNetworks']([mainnet, arbitrum]) + describe('WagmiAdapter - signMessage', () => { + it('should sign a message successfully', async () => { + const mockSignature = '0xmocksignature' + vi.mocked(signMessage).mockResolvedValueOnce(mockSignature) - /** - * Specifically to Wagmi, we are mutating caipNetworks on both Wagmi constructor and when we set adapters. - * So there is not proper way to compare objects since imageId and imageUrl is added later. - */ - mockWagmiClient.caipNetworks.forEach(network => { - expect(setRequestedCaipNetworks).toHaveBeenCalledWith( - expect.arrayContaining([expect.objectContaining({ id: network.id })]), - 'eip155' - ) + const result = await adapter.signMessage({ + message: 'Hello', + address: '0x123' }) + + expect(result.signature).toBe(mockSignature) }) }) - describe('Wagmi Client - Connection', () => { - it('should connect and disconnect to client', async () => { - expect(mockAppKit.getIsConnectedState()).toBe(false) - expect(mockAppKit.getCaipAddress()).toBeUndefined() - - const setApprovedCaipNetworksData = vi - .spyOn(mockAppKit, 'setApprovedCaipNetworksData') - .mockResolvedValue() - - expect(mockWagmiClient.wagmiConfig).toBeDefined() - - const syncAccountSpy = vi.spyOn(mockWagmiClient as any, 'syncAccount') - const mockConnectorConnectSpy = vi.spyOn(mockConnector, 'connect') - const mockConnectorGetAccountsSpy = vi.spyOn(mockConnector, 'connect') - - await connect(mockWagmiClient.wagmiConfig, { - connector: mockConnector + describe('WagmiAdapter - sendTransaction', () => { + it('should send transaction successfully', async () => { + const mockTxHash = '0xtxhash' + vi.mocked(getAccount).mockReturnValue({ + chainId: 1, + address: '0x123', + addresses: [], + chain: mainnet, + connector: {} as any, + isConnected: true, + isReconnecting: true, + isConnecting: false, + isDisconnected: false, + status: 'reconnecting' + }) + vi.mocked(wagmiSendTransaction).mockResolvedValue(mockTxHash) + vi.mocked(waitForTransactionReceipt).mockResolvedValue({} as any) + + const result = await adapter.sendTransaction({ + address: '0x123', + to: '0x456', + value: BigInt(1000), + gas: BigInt(21000), + gasPrice: BigInt(2000000000), + data: '0x' }) - expect(syncAccountSpy).toHaveBeenCalledTimes(2) - expect(mockConnectorConnectSpy).toHaveBeenCalledOnce() - expect(mockConnectorGetAccountsSpy).toHaveBeenCalledOnce() - expect(setApprovedCaipNetworksData).toHaveBeenCalledOnce() - - expect(mockAppKit.getCaipAddress()).toBe( - `${ConstantsUtil.EIP155}:${mainnet.id}:${mockAccount.address}` - ) - - const connectedWagmiAccount = getAccount(mockWagmiClient.wagmiConfig) - - expect(connectedWagmiAccount.status).toBe('connected') - expect(connectedWagmiAccount.address).toBe(mockAccount.address) - - const resetAccountSpy = vi.spyOn(mockAppKit, 'resetAccount') - const resetWcSpy = vi.spyOn(mockAppKit, 'resetWcConnection') - const resetNetworkSpy = vi.spyOn(mockAppKit, 'resetNetwork') - const setAllAccountsSpy = vi.spyOn(mockAppKit, 'setAllAccounts') - - const mockConnectorDisconnectSpy = vi.spyOn(mockConnector, 'disconnect') - - await disconnect(mockWagmiClient.wagmiConfig) - - expect(mockConnectorConnectSpy).toHaveBeenCalled() - expect(mockConnectorDisconnectSpy).toHaveBeenCalledOnce() - - const disconnectedWagmiAccount = getAccount(mockWagmiClient.wagmiConfig) - - expect(disconnectedWagmiAccount.status).toBe('disconnected') - expect(disconnectedWagmiAccount.address).toBeUndefined() - expect(resetAccountSpy).toHaveBeenCalledOnce() - expect(resetWcSpy).toHaveBeenCalledOnce() - expect(resetNetworkSpy).toHaveBeenCalledOnce() - expect(setAllAccountsSpy).toHaveBeenCalledTimes(2) + expect(result.hash).toBe(mockTxHash) }) }) - describe('Wagmi Client - Sync Account', () => { - it('should sync account correctly when connected', async () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - const mockChainId = 1 - const mockConnector = { - id: 'mockConnector', - name: 'Mock Connector', - getProvider: vi.fn().mockResolvedValue({}) - } - - const setCaipAddressSpy = vi.spyOn(mockAppKit, 'setCaipAddress') - - const syncNetworkSpy = vi.spyOn(mockWagmiClient as any, 'syncNetwork') - const syncProfileSpy = vi.spyOn(mockWagmiClient as any, 'syncProfile') - const syncBalanceSpy = vi.spyOn(mockWagmiClient as any, 'syncBalance') - const syncConnectedWalletInfoSpy = vi.spyOn(mockWagmiClient as any, 'syncConnectedWalletInfo') - - await (mockWagmiClient as any).syncAccount({ - address: mockAddress, - chainId: mockChainId, - connector: mockConnector, - status: 'connected' + describe('writeContract', () => { + it('should write contract successfully', async () => { + const mockTxHash = '0xtxhash' + vi.mocked(wagmiWriteContract).mockResolvedValue(mockTxHash) + + const result = await adapter.writeContract({ + caipNetwork: mockCaipNetworks[0], + caipAddress: 'eip155:1:0x123', + tokenAddress: '0x123', + fromAddress: '0x456', + receiverAddress: '0x789', + tokenAmount: BigInt(1000), + abi: [], + method: 'transfer' }) - expect(setCaipAddressSpy).toHaveBeenCalledWith( - `eip155:${mockChainId}:${mockAddress}`, - 'eip155' - ) - - expect(syncNetworkSpy).toHaveBeenCalledOnce() - expect(syncNetworkSpy).toHaveBeenCalledWith(mockAddress, mockChainId, true) - - expect(syncProfileSpy).toHaveBeenCalledOnce() - expect(syncProfileSpy).toHaveBeenCalledWith(mockAddress, mockChainId) - - expect(syncBalanceSpy).toHaveBeenCalledOnce() - expect(syncBalanceSpy).toHaveBeenCalledWith(mockAddress, mockChainId) - - expect(syncConnectedWalletInfoSpy).toHaveBeenCalledOnce - expect(syncConnectedWalletInfoSpy).toHaveBeenCalledWith(mockConnector) + expect(result.hash).toBe(mockTxHash) }) }) - describe('Wagmi Client - Sync Network', () => { - it('should sync network correctly', async () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - - mockWagmiClient.caipNetworks = [mainnet, arbitrum] - - const setCaipNetworkSpy = vi.spyOn(mockAppKit, 'setCaipNetwork') - const setCaipAddressSpy = vi.spyOn(mockAppKit, 'setCaipAddress') - const setAddressExplorerUrlSpy = vi.spyOn(mockAppKit, 'setAddressExplorerUrl') + describe('WagmiAdapter - getEnsAddress', () => { + it('should resolve ENS address successfully', async () => { + const mockAddress = '0x123' + vi.mocked(wagmiGetEnsAddress).mockResolvedValue(mockAddress) - await (mockWagmiClient as any).syncNetwork(mockAddress, mainnet.id, true) - - expect(setCaipNetworkSpy).toHaveBeenCalledWith( - expect.objectContaining({ - id: 1, - caipNetworkId: 'eip155:1', - name: 'Ethereum', - chainNamespace: 'eip155' - }) - ) + const result = await adapter.getEnsAddress({ + name: 'test.eth', + caipNetwork: mockCaipNetworks[0] + }) - expect(setCaipAddressSpy).toHaveBeenCalledWith( - `eip155:${mainnet.id}:${mockAddress}`, - 'eip155' - ) - expect(setAddressExplorerUrlSpy).toHaveBeenCalledWith( - 'https://etherscan.io/address/0x1234567890123456789012345678901234567890', - 'eip155' - ) + expect(result.address).toBe(mockAddress) }) - it('should not sync network if chain is not found', async () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - const mockChainId = 999 - - mockWagmiClient.options = mockOptionsExtended - const setCaipNetworkSpy = vi.spyOn(mockAppKit, 'setCaipNetwork') - const syncBalanceSpy = vi.spyOn(mockWagmiClient as any, 'syncBalance') + it('should return false for unresolvable ENS', async () => { + vi.mocked(wagmiGetEnsAddress).mockResolvedValue(null) - await (mockWagmiClient as any).syncNetwork(mockAddress, mockChainId, true) + const result = await adapter.getEnsAddress({ + name: 'nonexistent.eth', + caipNetwork: mockCaipNetworks[0] + }) - expect(setCaipNetworkSpy).not.toHaveBeenCalled() - expect(syncBalanceSpy).not.toHaveBeenCalled() + expect(result.address).toBe(false) }) }) - describe('Wagmi Client - Sync WalletConnect Name', () => { - it('should sync WalletConnect name correctly', async () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - const mockWcName = 'MockWallet' - - mockAppKit.getReownName = vi.fn().mockResolvedValue([{ name: mockWcName }]) - - const setProfileNameSpy = vi.spyOn(mockAppKit, 'setProfileName') - - await (mockWagmiClient as any).syncReownName(mockAddress) + describe('WagmiAdapter - estimateGas', () => { + it('should estimate gas successfully', async () => { + const mockGas = BigInt(21000) + vi.mocked(estimateGas).mockResolvedValue(mockGas) - expect(mockAppKit.getReownName).toHaveBeenCalledWith(mockAddress) - - expect(setProfileNameSpy).toHaveBeenCalledWith(mockWcName, 'eip155') - }) - - it('should set profile name to null if no WalletConnect name is found', async () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - - mockAppKit.getReownName = vi.fn().mockResolvedValue([]) - - const setProfileNameSpy = vi.spyOn(mockAppKit, 'setProfileName') - - await (mockWagmiClient as any).syncReownName(mockAddress) - - expect(mockAppKit.getReownName).toHaveBeenCalledWith(mockAddress) + const result = await adapter.estimateGas({ + address: '0x123', + to: '0x456', + data: '0x', + caipNetwork: mockCaipNetworks[0] + }) - expect(setProfileNameSpy).toHaveBeenCalledWith(null, 'eip155') + expect(result.gas).toBe(mockGas) }) - it('should handle errors and set profile name to null', async () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - - mockAppKit.getReownName = vi.fn().mockRejectedValue(new Error('Mock error')) + it('should throw error when estimation fails', async () => { + vi.mocked(estimateGas).mockRejectedValue(new Error()) - const setProfileNameSpy = vi.spyOn(mockAppKit, 'setProfileName') - - await (mockWagmiClient as any).syncReownName(mockAddress) - - expect(mockAppKit.getReownName).toHaveBeenCalledWith(mockAddress) - - expect(setProfileNameSpy).toHaveBeenCalledWith(null, 'eip155') + await expect( + adapter.estimateGas({ + address: '0x123', + to: '0x456', + data: '0x', + caipNetwork: mockCaipNetworks[0] + }) + ).rejects.toThrow('WagmiAdapter:estimateGas - error estimating gas') }) }) - describe('Wagmi Client - Sync Balance', () => { - const mockAddress = '0x1234567890123456789012345678901234567890' - const mockChainId = 1 // Ethereum mainnet + describe('WagmiAdapter - parseUnits and formatUnits', () => { + it('should parse units correctly', () => { + const result = adapter.parseUnits({ + value: '1.5', + decimals: 18 + }) - beforeEach(() => { - mockWagmiClient.options = { networks: [mainnet], projectId: '123' } - mockAppKit.setBalance = vi.fn() - ;(getBalance as any).mockReset() + expect(result).toBe(BigInt('1500000000000000000')) }) - it('should sync balance successfully', async () => { - const mockBalance = { formatted: '1.5', symbol: 'ETH' } - ;(getBalance as any).mockResolvedValue(mockBalance) - - await (mockWagmiClient as any).syncBalance(mockAddress, mockChainId) - - expect(getBalance).toHaveBeenCalledWith(mockWagmiClient.wagmiConfig, { - address: mockAddress, - chainId: mockChainId, - token: undefined + it('should format units correctly', () => { + const result = adapter.formatUnits({ + value: BigInt('1500000000000000000'), + decimals: 18 }) - expect(mockAppKit.setBalance).toHaveBeenCalledWith( - mockBalance.formatted, - mockBalance.symbol, - 'eip155' - ) - }) - - it('should not sync balance if chain is not found', async () => { - await (mockWagmiClient as any).syncBalance(mockAddress, 999) - - expect(getBalance).not.toHaveBeenCalled() - expect(mockAppKit.setBalance).toHaveBeenCalledWith(undefined, undefined, 'eip155') - }) - }) - describe('Wagmi Client - syncConnectedWalletInfo', () => { - it('should sync connected wallet info correctly', async () => { - const setConnectedWalletInfoSpy = vi.spyOn(mockAppKit, 'setConnectedWalletInfo') - const mockWalletConnectProvider = { - session: { - peer: { - metadata: { - name: 'WalletConnect Wallet', - icons: ['wc-icon-url'] - } - } - } - } - - const walletConnectConnector = { - id: ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID, - name: 'WalletConnect', - getProvider: vi.fn().mockResolvedValue(mockWalletConnectProvider) - } - - await (mockWagmiClient as any).syncConnectedWalletInfo(walletConnectConnector) - - expect(setConnectedWalletInfoSpy).toHaveBeenCalledWith( - { - name: 'WalletConnect Wallet', - icon: 'wc-icon-url', - icons: ['wc-icon-url'] - }, - 'eip155' - ) + expect(result).toBe('1.5') }) }) - describe('Wagmi Client - SyncConnectors', () => { - beforeEach(() => { - vi.resetAllMocks() - mockWagmiClient.options = { - ...mockOptions, - connectorImages: { - mockConnector: 'mock-connector-image-url' - } - } - mockAppKit.setConnectors = vi.fn() - }) + describe('WagmiAdapter - getBalance', () => { + it('should get balance successfully', async () => { + vi.mocked(getBalance).mockResolvedValue({ + formatted: '1.5', + symbol: 'ETH' + } as any) - it('should sync connectors correctly', () => { - const mockConnectors = [ - { id: 'mockConnector1', name: 'Mock Connector 1', type: 'injected' }, - { id: 'mockConnector2', name: 'Mock Connector 2', type: 'walletConnect' }, - { id: ConstantsUtil.AUTH_CONNECTOR_ID, name: 'Auth Connector', type: 'auth' } // This should be skipped - ] + const result = await adapter.getBalance({ + address: '0x123', + chainId: 1 + }) - ;(mockWagmiClient as any).syncConnectors(mockConnectors) - - expect(mockAppKit.setConnectors).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - id: 'mockConnector1', - name: 'Mock Connector 1', - type: 'INJECTED', - chain: 'eip155' - }), - expect.objectContaining({ - id: 'mockConnector2', - name: 'Mock Connector 2', - type: 'WALLET_CONNECT', - chain: 'eip155' - }) - ]) - ) + expect(result).toEqual({ + balance: '1.5', + symbol: 'ETH' + }) }) - it('should use custom connector image if provided', () => { - const mockConnectors = [{ id: 'mockConnector', name: 'Mock Connector', type: 'injected' }] - - ;(mockWagmiClient as any).syncConnectors(mockConnectors) + it('should return empty balance when network not found', async () => { + const result = await adapter.getBalance({ + address: '0x123', + chainId: 999 + }) - expect(mockAppKit.setConnectors).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - id: 'mockConnector', - imageUrl: 'mock-connector-image-url' - }) - ]) - ) + expect(result).toEqual({ + balance: '', + symbol: '' + }) }) + }) - it('should handle duplicate connectors', () => { - const mockConnectors = [ - { id: 'mockConnector', name: 'Mock Connector 1', type: 'injected' }, - { id: 'mockConnector', name: 'Mock Connector 2', type: 'walletConnect' } - ] + describe('WagmiAdapter - getProfile', () => { + it('should get profile successfully', async () => { + const mockEnsName = 'test.eth' + const mockAvatar = 'https://avatar.com/test.jpg' - ;(mockWagmiClient as any).syncConnectors(mockConnectors) + vi.mocked(getEnsName).mockResolvedValue(mockEnsName) + vi.mocked(getEnsAvatar).mockResolvedValue(mockAvatar) - const setConnectorsCalls = (mockAppKit.setConnectors as any).mock.calls - const syncedConnectors = setConnectorsCalls[0][0] - const mockConnectorCount = syncedConnectors.filter( - (c: any) => c.id === 'mockConnector' - ).length + const result = await adapter.getProfile({ + address: '0x123', + chainId: 1 + }) - expect(mockConnectorCount).toBe(1) + expect(result).toEqual({ + profileName: mockEnsName, + profileImage: mockAvatar + }) }) }) - describe('Wagmi Client - Sync Auth Connector', () => { - beforeEach(() => { - mockAppKit.addConnector = vi.fn() - ;(mockWagmiClient as any).initAuthConnectorListeners = vi.fn() - }) - - it('should sync auth connector correctly', async () => { - const mockAuthConnector = { - id: ConstantsUtil.AUTH_CONNECTOR_ID, - name: 'Auth Connector', - type: 'auth', - getProvider: vi.fn().mockResolvedValue('mockProvider') - } - - await (mockWagmiClient as any).syncAuthConnector(mockAuthConnector) - - expect(mockAppKit.addConnector).toHaveBeenCalledWith({ - id: ConstantsUtil.AUTH_CONNECTOR_ID, - type: 'AUTH', - name: 'w3mAuth', - provider: 'mockProvider', - chain: 'eip155' + describe('WagmiAdapter - connect and disconnect', () => { + it('should connect successfully', async () => { + const result = await adapter.connect({ + id: 'test-connector', + provider: {} as any, + type: 'injected', + chainId: 1 }) - expect((mockWagmiClient as any).initAuthConnectorListeners).toHaveBeenCalledWith( - mockAuthConnector - ) + expect(result.address).toBe('0x123') + expect(result.chainId).toBe(1) }) - it('should not set info property for injected connector', () => { - const mockConnectors = [ - { id: 'injected', name: 'Injected Connector', type: 'injected', info: { rdns: 'injected' } } + it('should disconnect successfully', async () => { + const mockConnections = [ + { connector: { id: 'connector1' } }, + { connector: { id: 'connector2' } } ] + vi.mocked(getConnections).mockReturnValue(mockConnections as any) - ;(mockWagmiClient as any).syncConnectors(mockConnectors) - - const setConnectorsCalls = (mockAppKit.setConnectors as any).mock.calls - const syncedConnectors = setConnectorsCalls[0][0] - const injectedConnector = syncedConnectors.filter((c: any) => c.id === 'injected')[0] - - expect(injectedConnector.info).toBeUndefined() - }) - }) - describe('Wagmi Client - Listen Auth Connector', () => { - let mockProvider: any - let mockConnector: any - - beforeEach(() => { - vi.resetAllMocks() - mockProvider = { - getLoginEmailUsed: vi.fn().mockReturnValue(false), - onRpcRequest: vi.fn(), - onRpcError: vi.fn(), - onRpcSuccess: vi.fn(), - onNotConnected: vi.fn(), - onIsConnected: vi.fn(), - onGetSmartAccountEnabledNetworks: vi.fn(), - onSetPreferredAccount: vi.fn(), - rejectRpcRequests: vi.fn(), - onConnect: vi.fn() - } - mockConnector = { - getProvider: vi.fn().mockResolvedValue(mockProvider) - } - - mockAppKit.open = vi.fn() - mockAppKit.isOpen = vi.fn().mockReturnValue(true) - mockAppKit.isTransactionStackEmpty = vi.fn().mockReturnValue(false) - mockAppKit.isTransactionShouldReplaceView = vi.fn().mockReturnValue(true) - mockAppKit.replace = vi.fn() - }) - - it('should set up event listeners correctly', async () => { - await (mockWagmiClient as any).listenAuthConnector(mockConnector, true) - - expect(mockProvider.onRpcRequest).toHaveBeenCalledWith(expect.any(Function)) - expect(mockProvider.onRpcError).toHaveBeenCalledWith(expect.any(Function)) - expect(mockProvider.onRpcSuccess).toHaveBeenCalledWith(expect.any(Function)) - expect(mockProvider.onNotConnected).toHaveBeenCalledWith(expect.any(Function)) - expect(mockProvider.onGetSmartAccountEnabledNetworks).toHaveBeenCalledWith( - expect.any(Function) - ) - }) - - it.skip('should handle RPC requests correctly', async () => { - await (mockWagmiClient as any).listenAuthConnector(mockConnector, true) + await adapter.disconnect() - const callback = mockProvider.onRpcRequest.mock.calls[0][0] - expect(callback).toBeDefined() - - callback({ method: 'eth_sendTransaction' }) - - expect(mockAppKit.redirect).toHaveBeenCalledWith('ApproveTransaction') + expect(vi.mocked(wagmiDisconnect)).toHaveBeenCalledTimes(2) }) }) - describe('Wagmi Client - Transports', () => { - it('should use default transports for networks without custom transports', () => { - const client = new WagmiAdapter({ - projectId: '123', - networks: [mainnet, arbitrum] - }) - - expect(client.wagmiConfig._internal.transports).toBeDefined() - expect(client.wagmiConfig._internal.transports[mainnet.id as number]).toBeDefined() - expect(client.wagmiConfig._internal.transports[arbitrum.id as number]).toBeDefined() - }) - - it('should merge user-provided transports with default transports', () => { - const customTransport = http('https://custom-rpc.example.com') - const client = new WagmiAdapter({ - projectId: '123', - networks: [mainnet, arbitrum], - transports: { - [mainnet.id]: customTransport - } + describe('WagmiAdapter - switchNetwork', () => { + it('should switch network successfully', async () => { + await adapter.switchNetwork({ + caipNetwork: mockCaipNetworks[0] }) - expect(client.wagmiConfig._internal.transports).toBeDefined() - expect(client.wagmiConfig._internal.transports[mainnet.id as number]).toBe(customTransport) - expect(client.wagmiConfig._internal.transports[arbitrum.id as number]).toBeDefined() - expect(client.wagmiConfig._internal.transports[arbitrum.id as number]).not.toBe( - customTransport + expect(switchChain).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + chainId: 1 + }) ) }) - - it('should prioritize user-provided transports over default ones', () => { - const customTransport1 = http('https://custom-rpc1.example.com') - const customTransport2 = http('https://custom-rpc2.example.com') - const client = new WagmiAdapter({ - projectId: '123', - networks: [mainnet, arbitrum], - transports: { - [mainnet.id]: customTransport1, - [arbitrum.id]: customTransport2 - } - }) - - expect(client.wagmiConfig._internal.transports).toBeDefined() - expect(client.wagmiConfig._internal.transports[mainnet.id as number]).toBe(customTransport1) - expect(client.wagmiConfig._internal.transports[arbitrum.id as number]).toBe(customTransport2) - }) - - it('should handle transports for networks not in the provided networks array', () => { - const customTransport = http('https://custom-rpc.example.com') - const client = new WagmiAdapter({ - projectId: '123', - networks: [mainnet], - transports: { - [arbitrum.id]: customTransport - } - }) - - expect(client.wagmiConfig._internal.transports).toBeDefined() - expect(client.wagmiConfig._internal.transports[mainnet.id as number]).toBeDefined() - expect(client.wagmiConfig._internal.transports[arbitrum.id as number]).toBe(customTransport) - }) }) }) diff --git a/packages/adapters/wagmi/src/tests/mocks/adapter.mock.ts b/packages/adapters/wagmi/src/tests/mocks/adapter.mock.ts index fc17d4f169..e69de29bb2 100644 --- a/packages/adapters/wagmi/src/tests/mocks/adapter.mock.ts +++ b/packages/adapters/wagmi/src/tests/mocks/adapter.mock.ts @@ -1,70 +0,0 @@ -import { mock } from 'wagmi/connectors' -import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' -import { AppKit } from '@reown/appkit' -import { WagmiAdapter } from '../../client' -import { arbitrum, mainnet } from '@reown/appkit/networks' -import type { CaipNetwork } from '@reown/appkit-common' -import type { SdkVersion } from '@reown/appkit-core' - -const privateKey = generatePrivateKey() -export const mockAccount = privateKeyToAccount(privateKey) - -export const mockWagmiClient = new WagmiAdapter({ - connectors: [mock({ accounts: [mockAccount.address] })], - networks: [mainnet, arbitrum], - projectId: '1234' -}) - -export const mockWagmiConfig = mockWagmiClient.wagmiConfig - -export const mockMainnetChainImage = - 'https://assets.coingecko.com/coins/images/279/large/ethereum.png' - -export const mockOptions = { - adapters: [mockWagmiClient], - networks: mockWagmiClient.caipNetworks, - enableInjected: false, - enableCoinbase: false, - enableWalletConnect: false, - features: { - email: false, - socials: [] - }, - chainImages: { - [mainnet.id]: mockMainnetChainImage - }, - metadata: { - description: 'Desc', - name: 'Name', - url: 'url.com', - icons: ['icon.png'] - }, - projectId: '1234', - sdkVersion: `html-wagmi-5.1.6` as SdkVersion -} - -export const mockAppKit = new AppKit(mockOptions) - -export const mockChain = { - id: 1, - name: 'Ethereum', - caipNetworkId: 'eip155:1', - chainNamespace: 'eip155', - nativeCurrency: { - name: 'Ethereum', - symbol: 'ETH', - decimals: 18 - }, - blockExplorers: { - default: { - name: 'etherscan', - url: 'https://etherscan.io', - standard: 'EIP3091' - } - }, - rpcUrls: { - default: { - http: ['https://rpc.example.com'] - } - } -} as CaipNetwork diff --git a/packages/appkit-utils/src/ConstantsUtil.ts b/packages/appkit-utils/src/ConstantsUtil.ts index 19d66d3fb3..e2bb4d476b 100644 --- a/packages/appkit-utils/src/ConstantsUtil.ts +++ b/packages/appkit-utils/src/ConstantsUtil.ts @@ -9,7 +9,7 @@ export const ConstantsUtil = { SAFE_CONNECTOR_ID: 'safe', LEDGER_CONNECTOR_ID: 'ledger', EIP6963_CONNECTOR_ID: 'eip6963', - AUTH_CONNECTOR_ID: 'w3mAuth', + AUTH_CONNECTOR_ID: 'ID_AUTH', EIP155: 'eip155' as ChainNamespace, ADD_CHAIN_METHOD: 'wallet_addEthereumChain', EIP6963_ANNOUNCE_EVENT: 'eip6963:announceProvider', @@ -17,5 +17,12 @@ export const ConstantsUtil = { CONNECTOR_RDNS_MAP: { coinbaseWallet: 'com.coinbase.wallet', coinbaseWalletSDK: 'com.coinbase.wallet' - } as Record + } as Record, + CONNECTOR_TYPE_EXTERNAL: 'EXTERNAL', + CONNECTOR_TYPE_WALLET_CONNECT: 'WALLET_CONNECT', + CONNECTOR_TYPE_INJECTED: 'INJECTED', + CONNECTOR_TYPE_ANNOUNCED: 'ANNOUNCED', + CONNECTOR_TYPE_AUTH: 'AUTH', + CONNECTOR_TYPE_MULTI_CHAIN: 'MULTI_CHAIN', + CONNECTOR_TYPE_W3M_AUTH: 'ID_AUTH' } diff --git a/packages/appkit-utils/src/ethers/EthersStoreUtil.ts b/packages/appkit-utils/src/ethers/EthersStoreUtil.ts index 78849fd8aa..3017f4c23f 100644 --- a/packages/appkit-utils/src/ethers/EthersStoreUtil.ts +++ b/packages/appkit-utils/src/ethers/EthersStoreUtil.ts @@ -14,7 +14,7 @@ export interface EthersStoreUtilState { | 'injected' | 'coinbaseWallet' | 'eip6963' - | 'w3mAuth' + | 'ID_AUTH' | 'coinbaseWalletSDK' address?: Address chainId?: number diff --git a/packages/appkit/exports/adapters.ts b/packages/appkit/exports/adapters.ts new file mode 100644 index 0000000000..389498c2e3 --- /dev/null +++ b/packages/appkit/exports/adapters.ts @@ -0,0 +1 @@ +export * from '../src/adapters/index.js' diff --git a/packages/appkit/exports/constants.ts b/packages/appkit/exports/constants.ts index ab8422f4dd..a3c6559487 100644 --- a/packages/appkit/exports/constants.ts +++ b/packages/appkit/exports/constants.ts @@ -1 +1 @@ -export const PACKAGE_VERSION = '1.3.0' +export const PACKAGE_VERSION = '1.3.2' diff --git a/packages/appkit/package.json b/packages/appkit/package.json index b44cb2a594..29278f9059 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -61,6 +61,11 @@ "types": "./dist/types/exports/auth-provider.d.ts", "import": "./dist/esm/exports/auth-provider.js", "default": "./dist/esm/exports/auth-provider.js" + }, + "./adapters": { + "types": "./dist/types/exports/adapters.d.ts", + "import": "./dist/esm/exports/adapters.js", + "default": "./dist/esm/exports/adapters.js" } }, "typesVersions": { @@ -85,6 +90,9 @@ ], "auth-provider": [ "./dist/types/exports/auth-provider.d.ts" + ], + "adapters": [ + "./dist/types/exports/adapters.d.ts" ] } }, diff --git a/packages/appkit/src/adapters/ChainAdapterBlueprint.ts b/packages/appkit/src/adapters/ChainAdapterBlueprint.ts new file mode 100644 index 0000000000..cb407bd185 --- /dev/null +++ b/packages/appkit/src/adapters/ChainAdapterBlueprint.ts @@ -0,0 +1,484 @@ +import { + getW3mThemeVariables, + type CaipAddress, + type CaipNetwork, + type ChainNamespace +} from '@reown/appkit-common' +import type { ChainAdapterConnector } from './ChainAdapterConnector.js' +import { + OptionsController, + ThemeController, + type Connector as AppKitConnector, + type AuthConnector, + type Metadata, + type Tokens +} from '@reown/appkit-core' +import type UniversalProvider from '@walletconnect/universal-provider' +import type { W3mFrameProvider } from '@reown/appkit-wallet' +import { ConstantsUtil, PresetsUtil } from '@reown/appkit-utils' +import type { AppKitOptions } from '../utils/index.js' +import type { AppKit } from '../client.js' +import { snapshot } from 'valtio' + +type EventName = 'disconnect' | 'accountChanged' | 'switchNetwork' +type EventData = { + disconnect: () => void + accountChanged: { address: string; chainId?: number | string } + switchNetwork: { address?: string; chainId: number | string } +} +type EventCallback = (data: EventData[T]) => void + +/** + * Abstract class representing a chain adapter blueprint. + * @template Connector - The type of connector extending ChainAdapterConnector + */ +export abstract class AdapterBlueprint< + Connector extends ChainAdapterConnector = ChainAdapterConnector +> { + public namespace: ChainNamespace | undefined + public caipNetworks?: CaipNetwork[] + public projectId?: string + + protected availableConnectors: Connector[] = [] + protected connector?: Connector + protected provider?: Connector['provider'] + + private eventListeners = new Map>>() + + /** + * Creates an instance of AdapterBlueprint. + * @param {AdapterBlueprint.Params} params - The parameters for initializing the adapter + */ + constructor(params?: AdapterBlueprint.Params) { + if (params) { + this.construct(params) + } + } + + /** + * Initializes the adapter with the given parameters. + * @param {AdapterBlueprint.Params} params - The parameters for initializing the adapter + */ + construct(params: AdapterBlueprint.Params) { + this.caipNetworks = params.networks + this.projectId = params.projectId + this.namespace = params.namespace + } + + /** + * Gets the available connectors. + * @returns {Connector[]} An array of available connectors + */ + public get connectors(): Connector[] { + return this.availableConnectors + } + + /** + * Gets the supported networks. + * @returns {CaipNetwork[]} An array of supported networks + */ + public get networks(): CaipNetwork[] { + return this.caipNetworks || [] + } + + /** + * Sets the universal provider for WalletConnect. + * @param {UniversalProvider} universalProvider - The universal provider instance + */ + public setUniversalProvider(universalProvider: UniversalProvider) { + this.addConnector({ + id: ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID, + type: 'WALLET_CONNECT', + name: PresetsUtil.ConnectorNamesMap[ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID], + provider: universalProvider, + imageId: PresetsUtil.ConnectorImageIds[ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID], + chain: this.namespace, + chains: [] + } as unknown as Connector) + } + + /** + * Sets the auth provider. + * @param {W3mFrameProvider} authProvider - The auth provider instance + */ + public setAuthProvider(authProvider: W3mFrameProvider): void { + this.addConnector({ + id: ConstantsUtil.AUTH_CONNECTOR_ID, + type: 'AUTH', + name: 'Auth', + provider: authProvider, + imageId: PresetsUtil.ConnectorImageIds[ConstantsUtil.AUTH_CONNECTOR_ID], + chain: this.namespace, + chains: [] + } as unknown as Connector) + } + + /** + * Adds one or more connectors to the available connectors list. + * @param {...Connector} connectors - The connectors to add + */ + protected addConnector(...connectors: Connector[]) { + if (connectors.some(connector => connector.id === 'ID_AUTH')) { + const authConnector = connectors.find( + connector => connector.id === 'ID_AUTH' + ) as AuthConnector + + const optionsState = snapshot(OptionsController.state) + const themeMode = ThemeController.getSnapshot().themeMode + const themeVariables = ThemeController.getSnapshot().themeVariables + + authConnector?.provider?.syncDappData?.({ + metadata: optionsState.metadata as Metadata, + sdkVersion: optionsState.sdkVersion, + projectId: optionsState.projectId, + sdkType: optionsState.sdkType + }) + authConnector.provider.syncTheme({ + themeMode, + themeVariables, + w3mThemeVariables: getW3mThemeVariables(themeVariables, themeMode) + }) + } + this.availableConnectors = [ + ...this.availableConnectors.filter( + existing => !connectors.some(newConnector => newConnector.id === existing.id) + ), + ...connectors + ] + } + + /** + * Adds an event listener for a specific event. + * @template T + * @param {T} eventName - The name of the event + * @param {EventCallback} callback - The callback function to be called when the event is emitted + */ + public on(eventName: T, callback: EventCallback) { + if (!this.eventListeners.has(eventName)) { + this.eventListeners.set(eventName, new Set()) + } + + this.eventListeners.get(eventName)?.add(callback as EventCallback) + } + + /** + * Removes an event listener for a specific event. + * @template T + * @param {T} eventName - The name of the event + * @param {EventCallback} callback - The callback function to be removed + */ + public off(eventName: T, callback: EventCallback) { + const listeners = this.eventListeners.get(eventName) + if (listeners) { + listeners.delete(callback as EventCallback) + } + } + + /** + * Emits an event with the given name and optional data. + * @template T + * @param {T} eventName - The name of the event to emit + * @param {EventData[T]} [data] - The optional data to be passed to the event listeners + */ + protected emit(eventName: T, data?: EventData[T]) { + const listeners = this.eventListeners.get(eventName) + if (listeners) { + listeners.forEach(callback => callback(data as EventData[T])) + } + } + + /** + * Connects to WalletConnect. + * @param {(uri: string) => void} onUri - Callback function to handle the WalletConnect URI + * @param {number | string} [chainId] - Optional chain ID to connect to + */ + public abstract connectWalletConnect( + onUri: (uri: string) => void, + chainId?: number | string + ): Promise + + /** + * Connects to a wallet. + * @param {AdapterBlueprint.ConnectParams} params - Connection parameters + * @returns {Promise} Connection result + */ + public abstract connect( + params: AdapterBlueprint.ConnectParams + ): Promise + + /** + * Switches the network. + * @param {AdapterBlueprint.SwitchNetworkParams} params - Network switching parameters + */ + public abstract switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise + + /** + * Disconnects the current wallet. + */ + public abstract disconnect(params?: AdapterBlueprint.DisconnectParams): Promise + + /** + * Gets the balance for a given address and chain ID. + * @param {AdapterBlueprint.GetBalanceParams} params - Balance retrieval parameters + * @returns {Promise} Balance result + */ + public abstract getBalance( + params: AdapterBlueprint.GetBalanceParams + ): Promise + + /** + * Gets the profile for a given address and chain ID. + * @param {AdapterBlueprint.GetProfileParams} params - Profile retrieval parameters + * @returns {Promise} Profile result + */ + public abstract getProfile( + params: AdapterBlueprint.GetProfileParams + ): Promise + + /** + * Synchronizes the connectors with the given options and AppKit instance. + * @param {AppKitOptions} [options] - Optional AppKit options + * @param {AppKit} [appKit] - Optional AppKit instance + */ + public abstract syncConnectors(options?: AppKitOptions, appKit?: AppKit): void + + /** + * Synchronizes the connection with the given parameters. + * @param {AdapterBlueprint.SyncConnectionParams} params - Synchronization parameters + * @returns {Promise} Connection result + */ + public abstract syncConnection( + params: AdapterBlueprint.SyncConnectionParams + ): Promise + + /** + * Signs a message with the connected wallet. + * @param {AdapterBlueprint.SignMessageParams} params - Parameters including message to sign, address, and optional provider + * @returns {Promise} Object containing the signature + */ + public abstract signMessage( + params: AdapterBlueprint.SignMessageParams + ): Promise + + /** + * Estimates gas for a transaction. + * @param {AdapterBlueprint.EstimateGasTransactionArgs} params - Parameters including address, to, data, and optional provider + * @returns {Promise} Object containing the gas estimate + */ + public abstract estimateGas( + params: AdapterBlueprint.EstimateGasTransactionArgs + ): Promise + + /** + * Sends a transaction. + * @param {AdapterBlueprint.SendTransactionParams} params - Parameters including address, to, data, value, gasPrice, gas, and optional provider + * @returns {Promise} Object containing the transaction hash + */ + public abstract sendTransaction( + params: AdapterBlueprint.SendTransactionParams + ): Promise + + /** + * Writes a contract transaction. + * @param {AdapterBlueprint.WriteContractParams} params - Parameters including receiver address, token amount, token address, from address, method, and ABI + * @returns {Promise} Object containing the transaction hash + */ + public abstract writeContract( + params: AdapterBlueprint.WriteContractParams + ): Promise + + /** + * Gets the ENS address for a given name. + * @param {AdapterBlueprint.GetEnsAddressParams} params - Parameters including name + * @returns {Promise} Object containing the ENS address + */ + public abstract getEnsAddress( + params: AdapterBlueprint.GetEnsAddressParams + ): Promise + + /** + * Parses a decimal string value into a bigint with the specified number of decimals. + * @param {AdapterBlueprint.ParseUnitsParams} params - Parameters including value and decimals + * @returns {AdapterBlueprint.ParseUnitsResult} The parsed bigint value + */ + public abstract parseUnits( + params: AdapterBlueprint.ParseUnitsParams + ): AdapterBlueprint.ParseUnitsResult + + /** + * Formats a bigint value into a decimal string with the specified number of decimals. + * @param {AdapterBlueprint.FormatUnitsParams} params - Parameters including value and decimals + * @returns {AdapterBlueprint.FormatUnitsResult} The formatted decimal string + */ + public abstract formatUnits( + params: AdapterBlueprint.FormatUnitsParams + ): AdapterBlueprint.FormatUnitsResult + + /** + * Gets the WalletConnect provider. + * @param {AdapterBlueprint.GetWalletConnectProviderParams} params - Parameters including provider, caip networks, and active caip network + * @returns {AdapterBlueprint.GetWalletConnectProviderResult} The WalletConnect provider + */ + public abstract getWalletConnectProvider( + params: AdapterBlueprint.GetWalletConnectProviderParams + ): AdapterBlueprint.GetWalletConnectProviderResult + + /** + * Reconnects to a wallet. + * @param {AdapterBlueprint.ReconnectParams} params - Reconnection parameters + */ + public reconnect?(params: AdapterBlueprint.ReconnectParams): Promise +} + +export namespace AdapterBlueprint { + export type Params = { + namespace?: ChainNamespace + networks?: CaipNetwork[] + projectId?: string + } + + export type SwitchNetworkParams = { + caipNetwork: CaipNetwork + provider?: AppKitConnector['provider'] + providerType?: AppKitConnector['type'] + } + + export type GetBalanceParams = { + address: string + chainId: number | string + caipNetwork?: CaipNetwork + tokens?: Tokens + } + + export type GetProfileParams = { + address: string + chainId: number | string + } + + export type DisconnectParams = { + provider?: AppKitConnector['provider'] + providerType?: AppKitConnector['type'] + } + + export type ConnectParams = { + id: string + provider?: unknown + info?: unknown + type: string + chain?: ChainNamespace + chainId?: number | string + rpcUrl?: string + } + + export type ReconnectParams = ConnectParams + + export type SyncConnectionParams = { + id: string + namespace: ChainNamespace + chainId?: number | string + rpcUrl: string + } + + export type SignMessageParams = { + message: string + address: string + provider?: AppKitConnector['provider'] + } + + export type SignMessageResult = { + signature: string + } + + export type EstimateGasTransactionArgs = { + address: string + to: string + data: string + caipNetwork: CaipNetwork + provider?: AppKitConnector['provider'] + } + + export type EstimateGasTransactionResult = { + gas: bigint + } + + export type WriteContractParams = { + receiverAddress: string + tokenAmount: bigint + tokenAddress: string + fromAddress: string + method: 'send' | 'transfer' | 'call' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + abi: any + caipNetwork: CaipNetwork + provider?: AppKitConnector['provider'] + caipAddress: CaipAddress + } + + export type WriteContractResult = { + hash: string + } + + export type ParseUnitsParams = { + value: string + decimals: number + } + + export type ParseUnitsResult = bigint + + export type FormatUnitsParams = { + value: bigint + decimals: number + } + + export type FormatUnitsResult = string + + export type GetWalletConnectProviderParams = { + provider: AppKitConnector['provider'] + caipNetworks: CaipNetwork[] + activeCaipNetwork: CaipNetwork + } + + export type GetWalletConnectProviderResult = AppKitConnector['provider'] + + export type SendTransactionParams = { + address: `0x${string}` + to: string + data: string + value: bigint | number + gasPrice: bigint | number + gas?: bigint | number + caipNetwork?: CaipNetwork + provider?: AppKitConnector['provider'] + } + + export type SendTransactionResult = { + hash: string + } + + export type GetEnsAddressParams = { + name: string + caipNetwork: CaipNetwork + } + + export type GetEnsAddressResult = { + address: string | false + } + + export type GetBalanceResult = { + balance: string + symbol: string + } + + export type GetProfileResult = { + profileImage?: string + profileName?: string + } + + export type ConnectResult = { + id: AppKitConnector['id'] + type: AppKitConnector['type'] + provider: AppKitConnector['provider'] + chainId: number | string + address: string + } +} diff --git a/packages/appkit/src/adapters/ChainAdapterConnector.ts b/packages/appkit/src/adapters/ChainAdapterConnector.ts new file mode 100644 index 0000000000..858a41eba9 --- /dev/null +++ b/packages/appkit/src/adapters/ChainAdapterConnector.ts @@ -0,0 +1,6 @@ +import type { CaipNetwork } from '@reown/appkit-common' +import type { Connector } from '@reown/appkit-core' + +export interface ChainAdapterConnector extends Connector { + chains: CaipNetwork[] +} diff --git a/packages/appkit/src/adapters/index.ts b/packages/appkit/src/adapters/index.ts new file mode 100644 index 0000000000..9babae1e07 --- /dev/null +++ b/packages/appkit/src/adapters/index.ts @@ -0,0 +1 @@ +export { AdapterBlueprint } from './ChainAdapterBlueprint.js' diff --git a/packages/appkit/src/client.ts b/packages/appkit/src/client.ts index cf82f50f61..d75d35ed4e 100644 --- a/packages/appkit/src/client.ts +++ b/packages/appkit/src/client.ts @@ -1,14 +1,25 @@ -import type { - EventsControllerState, - PublicStateControllerState, - ThemeControllerState, - ModalControllerState, - ConnectedWalletInfo, - RouterControllerState, - ChainAdapter, - SdkVersion, - UseAppKitAccountReturn, - UseAppKitNetworkReturn +/* eslint-disable max-depth */ +import { + type EventsControllerState, + type PublicStateControllerState, + type ThemeControllerState, + type ModalControllerState, + type ConnectedWalletInfo, + type RouterControllerState, + type ChainAdapter, + type SdkVersion, + type UseAppKitAccountReturn, + type UseAppKitNetworkReturn, + type NetworkControllerClient, + type ConnectionControllerClient, + ConstantsUtil as CoreConstantsUtil, + type ConnectorType, + type WriteContractArgs, + type Provider, + type SendTransactionArgs, + type EstimateGasTransactionArgs, + type AccountControllerState, + type AdapterNetworkState } from '@reown/appkit-core' import { AccountController, @@ -27,7 +38,8 @@ import { OptionsController, AssetUtil, ApiController, - AlertController + AlertController, + StorageUtil } from '@reown/appkit-core' import { setColorTheme, setThemeVariables } from '@reown/appkit-ui' import { @@ -35,14 +47,39 @@ import { type CaipNetwork, type ChainNamespace, SafeLocalStorage, - SafeLocalStorageKeys + SafeLocalStorageKeys, + type CaipAddress, + type CaipNetworkId } from '@reown/appkit-common' import type { AppKitOptions } from './utils/TypesUtil.js' -import { UniversalAdapterClient } from './universal-adapter/client.js' -import { CaipNetworksUtil, ErrorUtil } from '@reown/appkit-utils' -import type { W3mFrameTypes } from '@reown/appkit-wallet' +import { + UniversalAdapter, + UniversalAdapter as UniversalAdapterClient +} from './universal-adapter/client.js' +import { + CaipNetworksUtil, + ErrorUtil, + ConstantsUtil as UtilConstantsUtil +} from '@reown/appkit-utils' +import { + W3mFrameHelpers, + W3mFrameRpcConstants, + type W3mFrameProvider, + type W3mFrameTypes +} from '@reown/appkit-wallet' import { ProviderUtil } from './store/ProviderUtil.js' import type { AppKitNetwork } from '@reown/appkit/networks' +import type { AdapterBlueprint } from './adapters/ChainAdapterBlueprint.js' +import UniversalProvider from '@walletconnect/universal-provider' +import type { SessionTypes } from '@walletconnect/types' +import type { UniversalProviderOpts } from '@walletconnect/universal-provider' +import { W3mFrameProviderSingleton } from './auth-provider/W3MFrameProviderSingleton.js' + +declare global { + interface Window { + ethereum?: Record + } +} // -- Export Controllers ------------------------------------------------------- export { AccountController } @@ -52,6 +89,49 @@ export interface OpenOptions { view: 'Account' | 'Connect' | 'Networks' | 'ApproveTransaction' | 'OnRampProviders' } +type Adapters = Record + +// -- Constants ----------------------------------------- // +const accountState: AccountControllerState = { + currentTab: 0, + tokenBalance: [], + smartAccountDeployed: false, + addressLabels: new Map(), + allAccounts: [] +} + +const networkState: AdapterNetworkState = { + supportsAllNetworks: true, + smartAccountEnabledNetworks: [] +} + +const OPTIONAL_METHODS = [ + 'eth_accounts', + 'eth_requestAccounts', + 'eth_sendRawTransaction', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + 'eth_sendTransaction', + 'personal_sign', + 'wallet_switchEthereumChain', + 'wallet_addEthereumChain', + 'wallet_getPermissions', + 'wallet_requestPermissions', + 'wallet_registerOnboarding', + 'wallet_watchAsset', + 'wallet_scanQRCode', + // EIP-5792 + 'wallet_getCallsStatus', + 'wallet_sendCalls', + 'wallet_getCapabilities', + // EIP-7715 + 'wallet_grantPermissions', + 'wallet_revokePermissions' +] + // -- Helpers ------------------------------------------------------------------- let isInitialized = false @@ -59,17 +139,37 @@ let isInitialized = false export class AppKit { private static instance?: AppKit - public version: SdkVersion + public activeAdapter?: AdapterBlueprint - public adapter?: ChainAdapter + public options: AppKitOptions public adapters?: ChainAdapter[] + public activeChainNamespace?: ChainNamespace + + public chainNamespaces: ChainNamespace[] = [] + + public chainAdapters?: Adapters + public universalAdapter?: UniversalAdapterClient + private universalProvider?: UniversalProvider + + private connectionControllerClient?: ConnectionControllerClient + + private networkControllerClient?: NetworkControllerClient + + private universalProviderInitPromise?: Promise + + private authProvider?: W3mFrameProvider + private initPromise?: Promise = undefined - private caipNetworks: [CaipNetwork, ...CaipNetwork[]] + public version?: SdkVersion + + public adapter?: ChainAdapter + + private caipNetworks?: [CaipNetwork, ...CaipNetwork[]] private defaultCaipNetwork?: CaipNetwork @@ -80,19 +180,39 @@ export class AppKit { sdkVersion: SdkVersion } ) { - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - this.adapter = options.adapters?.[0] as ChainAdapter - this.caipNetworks = this.extendCaipNetworks(options) - this.defaultCaipNetwork = this.extendDefaultCaipNetwork(options) - this.initControllers(options) - this.initOrContinue() - this.version = options.sdkVersion + this.options = options + this.initialize(options) } public static getInstance() { return this.instance } + private async initialize( + options: AppKitOptions & { + adapters?: ChainAdapter[] + } & { + sdkVersion: SdkVersion + } + ) { + this.caipNetworks = this.extendCaipNetworks(options) + + this.createAuthProvider() + await this.createUniversalProvider() + this.createClients() + ChainController.initialize(options.adapters ?? []) + this.defaultCaipNetwork = this.extendDefaultCaipNetwork(options) + this.initControllers(options) + this.chainAdapters = await this.createAdapters( + options.adapters as unknown as AdapterBlueprint[] + ) + await this.initChainAdapters() + this.syncRequestedNetworks() + await this.initOrContinue() + await this.syncExistingConnection() + this.version = options.sdkVersion + } + // -- Public ------------------------------------------------------------------- public async open(options?: OpenOptions) { await this.initOrContinue() @@ -118,7 +238,7 @@ export class AppKit { } public switchNetwork(appKitNetwork: AppKitNetwork) { - const network = this.caipNetworks.find(n => n.id === appKitNetwork.id) + const network = this.caipNetworks?.find(n => n.id === appKitNetwork.id) if (!network) { AlertController.open(ErrorUtil.ALERT_ERRORS.SWITCH_NETWORK_NOT_FOUND, 'error') @@ -293,6 +413,9 @@ export class AppKit { return ChainController.getAccountProp('caipAddress', chainNamespace) } + public getAddressByChainNamespace = (chainNamespace: ChainNamespace) => + ChainController.getAccountProp('address', chainNamespace) + public getAddress = (chainNamespace?: ChainNamespace) => { if (ChainController.state.activeChain === chainNamespace || !chainNamespace) { return AccountController.state.address @@ -431,13 +554,6 @@ export class AppKit { public getReownName: (typeof EnsController)['getNamesForAddress'] = address => EnsController.getNamesForAddress(address) - public resolveReownName = async (name: string) => { - const wcNameAddress = await EnsController.resolveName(name) - const networkNameAddresses = Object.values(wcNameAddress?.addresses) || [] - - return networkNameAddresses[0]?.address || false - } - public setEIP6963Enabled: (typeof OptionsController)['setEIP6963Enabled'] = enabled => { OptionsController.setEIP6963Enabled(enabled) } @@ -490,8 +606,6 @@ export class AppKit { options.metadata = defaultMetaData } - this.initializeUniversalAdapter(options) - this.initializeAdapters(options) this.setDefaultNetwork() OptionsController.setAllWallets(options.allWallets) @@ -530,7 +644,7 @@ export class AppKit { } const evmAdapter = options.adapters?.find( - adapter => adapter.chainNamespace === ConstantsUtil.CHAIN.EVM + adapter => adapter.namespace === ConstantsUtil.CHAIN.EVM ) // Set the SIWE client for EVM chains @@ -577,40 +691,958 @@ export class AppKit { return extendedNetwork } - private initializeUniversalAdapter(options: AppKitOptions) { - const extendedOptions = { - ...options, - networks: this.caipNetworks, - defaultNetwork: this.defaultCaipNetwork + private createClients() { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + this.connectionControllerClient = { + connectWalletConnect: async (onUri: (uri: string) => void) => { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + + this.universalProvider?.on('display_uri', (uri: string) => { + onUri(uri) + }) + + if (this.options.siweConfig) { + const siweParams = await this.options.siweConfig?.getMessageParams?.() + const isSiweEnabled = this.options.siweConfig?.options?.enabled + const isProviderSupported = typeof this.universalProvider?.authenticate === 'function' + const isSiweParamsValid = siweParams && Object.keys(siweParams || {}).length > 0 + const clientId = await this.universalProvider?.client?.core?.crypto?.getClientId() + if (clientId) { + this.setClientId(clientId) + if ( + this.options.siweConfig && + isSiweEnabled && + siweParams && + isProviderSupported && + isSiweParamsValid && + ChainController.state.activeChain === ConstantsUtil.CHAIN.EVM + ) { + const { SIWEController, getDidChainId, getDidAddress } = await import( + '@reown/appkit-siwe' + ) + + const chains = this.caipNetworks + ?.filter(network => network.chainNamespace === ConstantsUtil.CHAIN.EVM) + .map(chain => chain.caipNetworkId) as string[] + + siweParams.chains = this.caipNetworks + ?.filter(network => network.chainNamespace === ConstantsUtil.CHAIN.EVM) + .map(chain => chain.id) as number[] + + const result = await this.universalProvider?.authenticate({ + nonce: await this.options.siweConfig?.getNonce?.(), + methods: [...OPTIONAL_METHODS], + ...siweParams, + chains + }) + // Auths is an array of signed CACAO objects https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-74.md + const signedCacao = result?.auths?.[0] + + if (signedCacao) { + const { p, s } = signedCacao + const cacaoChainId = getDidChainId(p.iss) + const address = getDidAddress(p.iss) + if (address && cacaoChainId) { + SIWEController.setSession({ + address, + chainId: parseInt(cacaoChainId, 10) + }) + } + + try { + // Kicks off verifyMessage and populates external states + const message = this.universalProvider?.client.formatAuthMessage({ + request: p, + iss: p.iss + }) + + await SIWEController.verifyMessage({ + message: message as string, + signature: s.s, + cacao: signedCacao + }) + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error verifying message', error) + // eslint-disable-next-line no-console + await this.universalProvider?.disconnect().catch(console.error) + // eslint-disable-next-line no-console + await SIWEController.signOut().catch(console.error) + throw error + } + } + } + } + } else { + await adapter?.connectWalletConnect(onUri, this.getCaipNetwork()?.id) + } + + await this.syncWalletConnectAccount() + }, + connectExternal: async ({ id, info, type, provider, chain, caipNetwork }) => { + if (chain && chain !== ChainController.state.activeChain && !caipNetwork) { + const toConnectNetwork = this.caipNetworks?.find( + network => network.chainNamespace === chain + ) + if (toConnectNetwork) { + this.setCaipNetwork(toConnectNetwork) + } + } + const adapter = chain + ? this.getAdapter(chain) + : this.getAdapter(ChainController.state.activeChain as ChainNamespace) + + const res = await adapter?.connect({ + id, + info, + type, + provider, + chainId: caipNetwork?.id || this.getCaipNetwork()?.id, + rpcUrl: + caipNetwork?.rpcUrls?.default?.http?.[0] || + this.getCaipNetwork()?.rpcUrls?.default?.http?.[0] + }) + + if (res) { + await this.syncAccount({ + ...res, + chainNamespace: chain || (ChainController.state.activeChain as ChainNamespace) + }) + this.syncProvider({ + ...res, + chainNamespace: chain || (ChainController.state.activeChain as ChainNamespace) + }) + } + }, + reconnectExternal: async ({ id, info, type, provider }) => { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + if (adapter?.reconnect) { + await adapter?.reconnect({ id, info, type, provider, chainId: this.getCaipNetwork()?.id }) + } + }, + disconnect: async () => { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + const provider = ProviderUtil.getProvider( + ChainController.state.activeChain as ChainNamespace + ) + + if (this.options.siweConfig?.options?.signOutOnDisconnect) { + const { SIWEController } = await import('@reown/appkit-siwe') + await SIWEController.signOut() + } + + const providerType = + ProviderUtil.state.providerIds[ChainController.state.activeChain as ChainNamespace] + + await adapter?.disconnect({ provider, providerType }) + + localStorage.removeItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR) + localStorage.removeItem(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID) + + ChainController.state.chains.forEach(chain => { + this.resetAccount(chain.namespace as ChainNamespace) + }) + }, + checkInstalled: (ids?: string[]) => { + if (!ids) { + return Boolean(window.ethereum) + } + + return ids.some(id => Boolean(window.ethereum?.[String(id)])) + }, + signMessage: async (message: string) => { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + const result = await adapter?.signMessage({ + message, + address: AccountController.state.address as string, + provider: ProviderUtil.getProvider(ChainController.state.activeChain as ChainNamespace) + }) + + return result?.signature || '' + }, + sendTransaction: async (args: SendTransactionArgs) => { + if (args.chainNamespace === 'eip155') { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + + const result = await adapter?.sendTransaction(args) + + return result?.hash || '' + } + + return '' + }, + estimateGas: async (args: EstimateGasTransactionArgs) => { + if (args.chainNamespace === 'eip155') { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + const provider = ProviderUtil.getProvider( + ChainController.state.activeChain as ChainNamespace + ) + const caipNetwork = this.getCaipNetwork() + if (!caipNetwork) { + throw new Error('CaipNetwork is undefined') + } + + const result = await adapter?.estimateGas({ + ...args, + provider, + caipNetwork + }) + + return result?.gas || 0n + } + + return 0n + }, + getEnsAvatar: async () => { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + const result = await adapter?.getProfile({ + address: AccountController.state.address as string, + chainId: Number(this.getCaipNetwork()?.id) + }) + + return result?.profileImage || false + }, + getEnsAddress: async (name: string) => { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + const caipNetwork = this.getCaipNetwork() + if (!caipNetwork) { + return false + } + const result = await adapter?.getEnsAddress({ + name, + caipNetwork + }) + + return result?.address || false + }, + writeContract: async (args: WriteContractArgs) => { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + const caipNetwork = this.getCaipNetwork() + const caipAddress = this.getCaipAddress() + const provider = ProviderUtil.getProvider( + ChainController.state.activeChain as ChainNamespace + ) + if (!caipNetwork || !caipAddress) { + throw new Error('CaipNetwork or CaipAddress is undefined') + } + + const result = await adapter?.writeContract({ ...args, caipNetwork, provider, caipAddress }) + + return result?.hash as `0x${string}` | null + }, + parseUnits: (value: string, decimals: number) => { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + + return adapter?.parseUnits({ value, decimals }) ?? 0n + }, + formatUnits: (value: bigint, decimals: number) => { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + + return adapter?.formatUnits({ value, decimals }) ?? '0' + } + } + + this.networkControllerClient = { + switchCaipNetwork: async caipNetwork => { + if (!caipNetwork) { + return + } + if ( + AccountController.state.address && + caipNetwork.chainNamespace === ChainController.state.activeChain + ) { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + const provider = ProviderUtil.getProvider< + UniversalProvider | Provider | W3mFrameProvider + >(ChainController.state.activeChain as ChainNamespace) + const providerType = + ProviderUtil.state.providerIds[ChainController.state.activeChain as ChainNamespace] + await adapter?.switchNetwork({ caipNetwork, provider, providerType }) + this.setCaipNetwork(caipNetwork) + await this.syncAccount({ + address: AccountController.state.address, + chainId: ChainController.state.activeCaipNetwork?.id as string | number, + chainNamespace: caipNetwork.chainNamespace + }) + } else if (AccountController.state.address) { + const providerType = + ProviderUtil.state.providerIds[ChainController.state.activeChain as ChainNamespace] + + if (providerType === 'AUTH') { + try { + ChainController.state.activeChain = caipNetwork.chainNamespace + await this.connectionControllerClient?.connectExternal?.({ + id: 'ID_AUTH', + provider: this.authProvider, + chain: caipNetwork.chainNamespace, + chainId: caipNetwork.id, + type: 'AUTH', + caipNetwork + }) + } catch (error) { + const adapter = this.getAdapter(caipNetwork.chainNamespace as ChainNamespace) + await adapter?.switchNetwork({ + caipNetwork, + provider: this.authProvider, + providerType + }) + } + } else if (providerType === 'WALLET_CONNECT') { + this.setCaipNetwork(caipNetwork) + this.syncWalletConnectAccount() + } else { + this.setCaipNetwork(caipNetwork) + const address = this.getAddressByChainNamespace(caipNetwork.chainNamespace) + if (address) { + this.syncAccount({ + address, + chainId: caipNetwork.id, + chainNamespace: caipNetwork.chainNamespace + }) + } + } + } else { + this.setCaipNetwork(caipNetwork) + } + }, + // eslint-disable-next-line @typescript-eslint/require-await + getApprovedCaipNetworksData: async () => { + const providerType = + ProviderUtil.state.providerIds[ChainController.state.activeChain as ChainNamespace] + + if (providerType === UtilConstantsUtil.CONNECTOR_TYPE_WALLET_CONNECT) { + const namespaces = this.universalProvider?.session?.namespaces + + return { + supportsAllNetworks: false, + approvedCaipNetworkIds: this.getChainsFromNamespaces(namespaces) + } + } + + return { supportsAllNetworks: true, approvedCaipNetworkIds: [] } + } + } + if (this.networkControllerClient && this.connectionControllerClient) { + ConnectionController.setClient(this.connectionControllerClient) + } + } + + private async handleDisconnect() { + await this.connectionControllerClient?.disconnect() + } + + private async listenAuthConnector(provider: W3mFrameProvider) { + this.setLoading(true) + const isLoginEmailUsed = provider.getLoginEmailUsed() + this.setLoading(isLoginEmailUsed) + const { isConnected } = await provider.isConnected() + if (isConnected && this.connectionControllerClient?.connectExternal) { + this.connectionControllerClient?.connectExternal({ + id: 'ID_AUTH', + info: { + name: 'ID_AUTH' + }, + type: 'AUTH', + provider, + chainId: ChainController.state.activeCaipNetwork?.id + }) + } else { + this.setLoading(false) } - this.universalAdapter = new UniversalAdapterClient(extendedOptions) - ChainController.initializeUniversalAdapter(this.universalAdapter, options.adapters || []) - this.universalAdapter.construct?.(this, extendedOptions) + provider.onRpcRequest((request: W3mFrameTypes.RPCRequest) => { + if (W3mFrameHelpers.checkIfRequestExists(request)) { + if (!W3mFrameHelpers.checkIfRequestIsSafe(request)) { + this.handleUnsafeRPCRequest() + } + } else { + this.open() + // eslint-disable-next-line no-console + console.error(W3mFrameRpcConstants.RPC_METHOD_NOT_ALLOWED_MESSAGE, { + method: request.method + }) + setTimeout(() => { + this.showErrorMessage(W3mFrameRpcConstants.RPC_METHOD_NOT_ALLOWED_UI_MESSAGE) + }, 300) + provider.rejectRpcRequests() + } + }) + provider.onRpcError(() => { + const isModalOpen = this.isOpen() + if (isModalOpen) { + if (this.isTransactionStackEmpty()) { + this.close() + } else { + this.popTransactionStack(true) + } + } + }) + provider.onRpcSuccess((_, request) => { + const isSafeRequest = W3mFrameHelpers.checkIfRequestIsSafe(request) + if (isSafeRequest) { + return + } + if (this.isTransactionStackEmpty()) { + this.close() + } else { + this.popTransactionStack() + } + }) + provider.onNotConnected(() => { + const connectedConnector = SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR) + const isConnectedWithAuth = connectedConnector === 'ID_AUTH' + if (!isConnected && isConnectedWithAuth) { + this.setCaipAddress(undefined, ChainController.state.activeChain as ChainNamespace) + this.setLoading(false) + } + }) + provider.onIsConnected(() => { + provider.connect() + }) + provider.onConnect(async user => { + const caipAddress = + ChainController.state.activeChain === 'eip155' + ? (`eip155:${user.chainId}:${user.address}` as CaipAddress) + : (`${user.chainId}:${user.address}` as CaipAddress) + this.setCaipAddress(caipAddress, ChainController.state.activeChain as ChainNamespace) + this.setSmartAccountDeployed( + Boolean(user.smartAccountDeployed), + ChainController.state.activeChain as ChainNamespace + ) + this.setPreferredAccountType( + user.preferredAccountType as W3mFrameTypes.AccountType, + ChainController.state.activeChain as ChainNamespace + ) + this.setAllAccounts( + user.accounts || [ + { + address: user.address, + type: (user.preferredAccountType || 'eoa') as W3mFrameTypes.AccountType + } + ], + ChainController.state.activeChain as ChainNamespace + ) + + await provider.getSmartAccountEnabledNetworks() + + this.setLoading(false) + }) + provider.onGetSmartAccountEnabledNetworks(networks => { + this.setSmartAccountEnabledNetworks( + networks, + ChainController.state.activeChain as ChainNamespace + ) + }) + provider.onSetPreferredAccount(({ address, type }) => { + if (!address) { + return + } + this.setPreferredAccountType( + type as W3mFrameTypes.AccountType, + ChainController.state.activeChain as ChainNamespace + ) + }) } - private initializeAdapters(options: AppKitOptions) { - const extendedOptions = { - ...options, - networks: this.caipNetworks, - defaultNetwork: this.defaultCaipNetwork + private listenWalletConnect() { + if (this.universalProvider) { + this.universalProvider.on('disconnect', () => { + this.chainNamespaces.forEach(namespace => { + this.resetAccount(namespace) + }) + ConnectionController.resetWcConnection() + }) + + this.universalProvider.on('chainChanged', (chainId: number | string) => { + const caipNetwork = this.caipNetworks?.find( + // eslint-disable-next-line eqeqeq + c => c.chainNamespace === ChainController.state.activeChain && c.id == chainId + ) + const currentCaipNetwork = this.getCaipNetwork() + + if (!caipNetwork) { + const namespace = this.getActiveChainNamespace() || ConstantsUtil.CHAIN.EVM + ChainController.setActiveCaipNetwork({ + id: chainId, + caipNetworkId: `${namespace}:${chainId}`, + name: 'Unknown Network', + chainNamespace: namespace, + nativeCurrency: { + name: '', + decimals: 0, + symbol: '' + }, + rpcUrls: { + default: { + http: [] + } + } + }) + + return + } + + if (!currentCaipNetwork || currentCaipNetwork?.id !== caipNetwork?.id) { + this.setCaipNetwork(caipNetwork) + } + }) } + } - ChainController.initialize(options.adapters || []) - options.adapters?.forEach(adapter => { - // @ts-expect-error will introduce construct later - adapter.construct?.(this, extendedOptions) + private listenAdapter(chainNamespace: ChainNamespace) { + const adapter = this.getAdapter(chainNamespace) + + adapter?.on('switchNetwork', ({ address, chainId }) => { + if (chainId && this.caipNetworks?.find(n => n.id === chainId)) { + if (ChainController.state.activeChain === chainNamespace && address) { + this.syncAccount({ address, chainId, chainNamespace }) + } else if ( + ChainController.state.activeChain === chainNamespace && + AccountController.state.address + ) { + this.syncAccount({ + address: AccountController.state.address, + chainId, + chainNamespace + }) + } + } else { + ChainController.showUnsupportedChainUI() + } + }) + + adapter?.on('disconnect', () => { + if (ChainController.state.activeChain === chainNamespace) { + this.handleDisconnect() + } + }) + + adapter?.on('accountChanged', ({ address, chainId }) => { + if (ChainController.state.activeChain === chainNamespace && chainId) { + this.syncAccount({ + address, + chainId, + chainNamespace + }) + } else if ( + ChainController.state.activeChain === chainNamespace && + ChainController.state.activeCaipNetwork?.id + ) { + this.syncAccount({ + address, + chainId: ChainController.state.activeCaipNetwork?.id, + chainNamespace + }) + } + }) + } + + private getChainsFromNamespaces(namespaces: SessionTypes.Namespaces = {}): CaipNetworkId[] { + return Object.values(namespaces).flatMap(namespace => { + const chains = (namespace.chains || []) as CaipNetworkId[] + const accountsChains = namespace.accounts.map(account => { + const [chainNamespace, chainId] = account.split(':') + + return `${chainNamespace}:${chainId}` as CaipNetworkId + }) + + return Array.from(new Set([...chains, ...accountsChains])) }) } + private async syncWalletConnectAccount() { + const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) + StorageUtil.setConnectedNamespace(ChainController.state.activeChain as ChainNamespace) + this.chainNamespaces.forEach(async chainNamespace => { + const caipAddress = this.universalProvider?.session?.namespaces?.[chainNamespace] + ?.accounts[0] as CaipAddress + + if (caipAddress) { + ProviderUtil.setProviderId( + chainNamespace, + UtilConstantsUtil.CONNECTOR_TYPE_WALLET_CONNECT as ConnectorType + ) + + if ( + this.caipNetworks && + ChainController.state.activeCaipNetwork && + (adapter as ChainAdapter)?.adapterType === 'solana' + ) { + const provider = adapter?.getWalletConnectProvider({ + caipNetworks: this.caipNetworks, + provider: this.universalProvider, + activeCaipNetwork: ChainController.state.activeCaipNetwork + }) + ProviderUtil.setProvider(chainNamespace, provider) + } else { + ProviderUtil.setProvider(chainNamespace, this.universalProvider) + } + + StorageUtil.setConnectedConnector( + UtilConstantsUtil.CONNECTOR_TYPE_WALLET_CONNECT as ConnectorType + ) + + let address = '' + + if (caipAddress.split(':').length === 3) { + address = caipAddress.split(':')[2] as string + } else { + address = AccountController.state.address as string + } + + if ((adapter as ChainAdapter)?.adapterType === 'wagmi') { + try { + await adapter?.connect({ + id: 'walletConnect', + type: 'WALLET_CONNECT', + chainId: ChainController.state.activeCaipNetwork?.id as string | number + }) + } catch (error) { + adapter?.switchNetwork({ + provider: this.universalProvider, + caipNetwork: ChainController.state.activeCaipNetwork as CaipNetwork + }) + } + } + + this.syncWalletConnectAccounts(chainNamespace) + + await this.syncAccount({ + address, + chainId: + ChainController.state.activeChain === chainNamespace + ? (ChainController.state.activeCaipNetwork?.id as string | number) + : (this.caipNetworks?.find(n => n.chainNamespace === chainNamespace)?.id as + | string + | number), + chainNamespace + }) + } + }) + + await ChainController.setApprovedCaipNetworksData( + ChainController.state.activeChain as ChainNamespace + ) + } + + private syncWalletConnectAccounts(chainNamespace: ChainNamespace) { + const addresses = this.universalProvider?.session?.namespaces?.[chainNamespace]?.accounts + ?.map(account => { + const [, , address] = account.split(':') + + return address + }) + .filter((address, index, self) => self.indexOf(address) === index) as string[] + + if (addresses) { + this.setAllAccounts( + addresses.map(address => ({ address, type: 'eoa' })), + chainNamespace + ) + } + } + + private syncProvider({ + type, + provider, + id, + chainNamespace + }: Pick & { + chainNamespace: ChainNamespace + }) { + ProviderUtil.setProviderId(chainNamespace, type) + ProviderUtil.setProvider(chainNamespace, provider) + + StorageUtil.setConnectedConnector(id as ConnectorType) + StorageUtil.setConnectedNamespace(ChainController.state.activeChain as ChainNamespace) + } + + private async syncAccount({ + address, + chainId, + chainNamespace + }: Pick & { + chainNamespace: ChainNamespace + }) { + this.setPreferredAccountType( + AccountController.state.preferredAccountType + ? AccountController.state.preferredAccountType + : 'eoa', + ChainController.state.activeChain as ChainNamespace + ) + + this.setCaipAddress( + `${chainNamespace}:${chainId}:${address}` as `${ChainNamespace}:${string}:${string}`, + chainNamespace + ) + + if (chainNamespace === ChainController.state.activeChain) { + const caipNetwork = this.caipNetworks?.find( + n => n.id === chainId && n.chainNamespace === chainNamespace + ) + + if (caipNetwork) { + this.setCaipNetwork(caipNetwork) + } else { + this.setCaipNetwork(this.caipNetworks?.find(n => n.chainNamespace === chainNamespace)) + } + + const adapter = this.getAdapter(chainNamespace) + + const balance = await adapter?.getBalance({ + address, + chainId, + caipNetwork: caipNetwork || this.getCaipNetwork(), + tokens: this.options.tokens + }) + if (balance) { + this.setBalance(balance.balance, balance.symbol, chainNamespace) + } + await this.syncIdentity({ address, chainId: Number(chainId), chainNamespace }) + } + } + + private async syncIdentity({ + address, + chainId, + chainNamespace + }: Pick & { + chainNamespace: ChainNamespace + }) { + try { + const { name, avatar } = await this.fetchIdentity({ + address + }) + + this.setProfileName(name, chainNamespace) + this.setProfileImage(avatar, chainNamespace) + + if (!name) { + await this.syncReownName(address, chainNamespace) + const adapter = this.getAdapter(chainNamespace) + const result = await adapter?.getProfile({ + address, + chainId: Number(chainId) + }) + + if (result?.profileName) { + this.setProfileName(result.profileName, chainNamespace) + if (result.profileImage) { + this.setProfileImage(result.profileImage, chainNamespace) + } + } else { + await this.syncReownName(address, chainNamespace) + this.setProfileImage(null, chainNamespace) + } + } + } catch { + if (chainId === 1) { + await this.syncReownName(address, chainNamespace) + } else { + await this.syncReownName(address, chainNamespace) + this.setProfileImage(null, chainNamespace) + } + } + } + + private async syncReownName(address: string, chainNamespace: ChainNamespace) { + try { + const registeredWcNames = await this.getReownName(address) + if (registeredWcNames[0]) { + const wcName = registeredWcNames[0] + this.setProfileName(wcName.name, chainNamespace) + } else { + this.setProfileName(null, chainNamespace) + } + } catch { + this.setProfileName(null, chainNamespace) + } + } + + private syncRequestedNetworks() { + const uniqueChainNamespaces = [ + ...new Set(this.caipNetworks?.map(caipNetwork => caipNetwork.chainNamespace)) + ] + this.chainNamespaces = uniqueChainNamespaces + + uniqueChainNamespaces.forEach(chainNamespace => + this.setRequestedCaipNetworks( + this.caipNetworks?.filter(caipNetwork => caipNetwork.chainNamespace === chainNamespace) ?? + [], + chainNamespace + ) + ) + } + + private async syncExistingConnection() { + const connectedConnector = SafeLocalStorage.getItem( + SafeLocalStorageKeys.CONNECTED_CONNECTOR + ) as ConnectorType + const connectedNamespace = SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_NAMESPACE) + + if ( + connectedConnector === UtilConstantsUtil.CONNECTOR_TYPE_WALLET_CONNECT && + connectedNamespace + ) { + this.syncWalletConnectAccount() + } else if ( + connectedConnector && + connectedConnector !== UtilConstantsUtil.CONNECTOR_TYPE_W3M_AUTH && + connectedNamespace + ) { + const adapter = this.getAdapter(connectedNamespace as ChainNamespace) + const res = await adapter?.syncConnection({ + id: connectedConnector, + chainId: this.getCaipNetwork()?.id, + namespace: connectedNamespace as ChainNamespace, + rpcUrl: this.getCaipNetwork()?.rpcUrls?.default?.http?.[0] as string + }) + + if (res) { + this.syncProvider({ ...res, chainNamespace: connectedNamespace as ChainNamespace }) + await this.syncAccount({ ...res, chainNamespace: connectedNamespace as ChainNamespace }) + } + } + } + + private getAdapter(namespace: ChainNamespace) { + return this.chainAdapters?.[namespace] + } + + private createUniversalProvider() { + if ( + !this.universalProviderInitPromise && + typeof window !== 'undefined' && + this.options?.projectId + ) { + this.universalProviderInitPromise = this.initializeUniversalAdapter() + } + + return this.universalProviderInitPromise + } + + private async initializeUniversalAdapter() { + const universalProviderOptions: UniversalProviderOpts = { + projectId: this.options?.projectId, + metadata: { + name: this.options?.metadata ? this.options?.metadata.name : '', + description: this.options?.metadata ? this.options?.metadata.description : '', + url: this.options?.metadata ? this.options?.metadata.url : '', + icons: this.options?.metadata ? this.options?.metadata.icons : [''] + } + } + + this.universalProvider = await UniversalProvider.init(universalProviderOptions) + } + + public async getUniversalProvider() { + if (!this.universalProvider) { + try { + await this.createUniversalProvider() + } catch (error) { + throw new Error('AppKit:getUniversalProvider - Cannot create provider') + } + } + + return this.universalProvider + } + + private createAuthProvider() { + const emailEnabled = + this.options?.features?.email === undefined + ? CoreConstantsUtil.DEFAULT_FEATURES.email + : this.options?.features?.email + const socialsEnabled = this.options?.features?.socials + ? this.options?.features?.socials?.length > 0 + : CoreConstantsUtil.DEFAULT_FEATURES.socials + if (this.options?.projectId && (emailEnabled || socialsEnabled)) { + this.authProvider = W3mFrameProviderSingleton.getInstance({ + projectId: this.options.projectId + }) + this.listenAuthConnector(this.authProvider) + } + } + + private async createAdapters(blueprints?: AdapterBlueprint[]): Promise { + if (!this.universalProvider) { + this.universalProvider = await this.getUniversalProvider() + } + + this.syncRequestedNetworks() + + return this.chainNamespaces.reduce((adapters, namespace) => { + const blueprint = blueprints?.find(b => b.namespace === namespace) + + if (blueprint) { + adapters[namespace] = blueprint + adapters[namespace].namespace = namespace + adapters[namespace].construct({ + namespace, + projectId: this.options?.projectId, + networks: this.caipNetworks + }) + if (this.universalProvider) { + adapters[namespace].setUniversalProvider(this.universalProvider) + } + if (this.authProvider) { + adapters[namespace].setAuthProvider(this.authProvider) + } + if (this.options?.features) { + adapters[namespace].syncConnectors(this.options, this) + } + } else { + adapters[namespace] = new UniversalAdapter({ + namespace, + networks: this.caipNetworks + }) + if (this.universalProvider) { + adapters[namespace].setUniversalProvider(this.universalProvider) + } + if (this.authProvider) { + adapters[namespace].setAuthProvider(this.authProvider) + } + } + + ChainController.state.chains.set(namespace, { + namespace, + connectionControllerClient: this.connectionControllerClient, + networkControllerClient: this.networkControllerClient, + networkState, + accountState, + caipNetworks: this.caipNetworks ?? [] + }) + + return adapters + // eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter + }, {} as Adapters) + } + + private async initChainAdapters() { + await Promise.all( + // eslint-disable-next-line @typescript-eslint/require-await + this.chainNamespaces.map(async namespace => { + if (this.options) { + this.listenAdapter(namespace) + + this.setConnectors(this.chainAdapters?.[namespace]?.connectors || []) + } + }) + ) + this.listenWalletConnect() + } + private setDefaultNetwork() { const previousNetwork = SafeLocalStorage.getItem(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID) - const caipNetwork = previousNetwork - ? this.caipNetworks.find(n => n.caipNetworkId === previousNetwork) - : undefined + const caipNetwork = + previousNetwork && this.caipNetworks?.length + ? this.caipNetworks.find(n => n.caipNetworkId === previousNetwork) + : undefined - const network = caipNetwork || this.defaultCaipNetwork || this.caipNetworks[0] - ChainController.setActiveCaipNetwork(network) + const network = caipNetwork || this.defaultCaipNetwork || this.caipNetworks?.[0] + if (network) { + ChainController.setActiveCaipNetwork(network) + } } private async initOrContinue() { diff --git a/packages/appkit/src/store/ProviderUtil.ts b/packages/appkit/src/store/ProviderUtil.ts index 496e45885d..381cd2464f 100644 --- a/packages/appkit/src/store/ProviderUtil.ts +++ b/packages/appkit/src/store/ProviderUtil.ts @@ -2,21 +2,22 @@ import { proxy, ref, subscribe } from 'valtio/vanilla' import { subscribeKey as subKey } from 'valtio/vanilla/utils' import type UniversalProvider from '@walletconnect/universal-provider' import type { ChainNamespace } from '@reown/appkit-common' +import type { ConnectorType } from '@reown/appkit-core' type StateKey = keyof ProviderStoreUtilState export interface ProviderStoreUtilState { // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents providers: Record - providerIds: Record + providerIds: Record } -export type ProviderIdType = +export type ProviderType = | 'walletConnect' | 'injected' | 'coinbaseWallet' | 'eip6963' - | 'w3mAuth' + | 'ID_AUTH' | 'coinbaseWalletSDK' const state = proxy({ @@ -45,13 +46,13 @@ export const ProviderUtil = { return state.providers[chainNamespace] as T | undefined }, - setProviderId(chainNamespace: ChainNamespace, providerId: ProviderIdType) { + setProviderId(chainNamespace: ChainNamespace, providerId: ConnectorType) { if (providerId) { state.providerIds[chainNamespace] = providerId } }, - getProviderId(chainNamespace: ChainNamespace): ProviderIdType | undefined { + getProviderId(chainNamespace: ChainNamespace): ConnectorType | undefined { return state.providerIds[chainNamespace] }, diff --git a/packages/appkit/src/store/index.ts b/packages/appkit/src/store/index.ts index 52e8028197..453344943a 100644 --- a/packages/appkit/src/store/index.ts +++ b/packages/appkit/src/store/index.ts @@ -1,2 +1,2 @@ export { ProviderUtil } from './ProviderUtil.js' -export type { ProviderStoreUtilState, ProviderIdType } from './ProviderUtil.js' +export type { ProviderStoreUtilState, ProviderType } from './ProviderUtil.js' diff --git a/packages/appkit/src/tests/appkit.test.ts b/packages/appkit/src/tests/appkit.test.ts index b66b4c61ef..70b64d79bb 100644 --- a/packages/appkit/src/tests/appkit.test.ts +++ b/packages/appkit/src/tests/appkit.test.ts @@ -32,6 +32,13 @@ describe('Base', () => { beforeEach(() => { vi.resetAllMocks() + + vi.mocked(ChainController).state = { + chains: new Map(), + activeChain: 'eip155' + } as any + + vi.mocked(ConnectorController).getConnectors = vi.fn().mockReturnValue([]) appKit = new AppKit(mockOptions) }) @@ -40,12 +47,6 @@ describe('Base', () => { expect(OptionsController.setSdkVersion).toHaveBeenCalledWith(mockOptions.sdkVersion) expect(OptionsController.setProjectId).toHaveBeenCalledWith(mockOptions.projectId) expect(OptionsController.setMetadata).toHaveBeenCalledWith(mockOptions.metadata) - expect(appKit.universalAdapter?.construct).toHaveBeenCalledWith( - appKit, - expect.objectContaining({ - metadata: mockOptions.metadata - }) - ) }) it('should initialize adapters in ChainController', () => { @@ -234,9 +235,17 @@ describe('Base', () => { }) it('should set CAIP address', () => { + // First mock AccountController.setCaipAddress to update ChainController state + vi.mocked(AccountController.setCaipAddress).mockImplementation(() => { + vi.mocked(ChainController).state = { + ...vi.mocked(ChainController).state, + activeCaipAddress: 'eip155:1:0x123' + } as any + }) + appKit.setCaipAddress('eip155:1:0x123', 'eip155') - expect(appKit.getIsConnectedState()).toBe(true) expect(AccountController.setCaipAddress).toHaveBeenCalledWith('eip155:1:0x123', 'eip155') + expect(appKit.getIsConnectedState()).toBe(true) }) it('should set provider', () => { @@ -297,14 +306,19 @@ describe('Base', () => { { id: 'phantom', name: 'Phantom', chain: 'eip155', type: 'INJECTED' } ] as Connector[] + // Mock getConnectors to return existing connectors vi.mocked(ConnectorController.getConnectors).mockReturnValue(existingConnectors) - const connectors = [ + + const newConnectors = [ { id: 'metamask', name: 'MetaMask', chain: 'eip155', type: 'INJECTED' } ] as Connector[] - appKit.setConnectors(connectors) + + appKit.setConnectors(newConnectors) + + // Verify that setConnectors was called with combined array expect(ConnectorController.setConnectors).toHaveBeenCalledWith([ ...existingConnectors, - ...connectors + ...newConnectors ]) }) @@ -414,19 +428,6 @@ describe('Base', () => { ]) }) - it('should resolve Reown name', async () => { - vi.mocked(EnsController.resolveName).mockResolvedValue({ - addresses: { eip155: { address: '0x123', created: '0' } }, - name: 'john.reown.id', - registered: 0, - updated: 0, - attributes: [] - }) - const result = await appKit.resolveReownName('john.reown.id') - expect(EnsController.resolveName).toHaveBeenCalledWith('john.reown.id') - expect(result).toBe('0x123') - }) - it('should set EIP6963 enabled', () => { appKit.setEIP6963Enabled(true) expect(OptionsController.setEIP6963Enabled).toHaveBeenCalledWith(true) diff --git a/packages/appkit/src/tests/universal-adapter.test.ts b/packages/appkit/src/tests/universal-adapter.test.ts index c90e03da13..da68b791a4 100644 --- a/packages/appkit/src/tests/universal-adapter.test.ts +++ b/packages/appkit/src/tests/universal-adapter.test.ts @@ -1,233 +1,145 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' -import { mainnet as mainnetAppKit, solana as solanaAppKit } from '../networks' -import { UniversalAdapterClient } from '../universal-adapter' -import { mockOptions } from './mocks/Options' -import mockProvider from './mocks/UniversalProvider' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { UniversalAdapter } from '../universal-adapter/client' import type UniversalProvider from '@walletconnect/universal-provider' -import { AlertController, ChainController } from '@reown/appkit-core' -import { ProviderUtil } from '../store/index.js' -import { CaipNetworksUtil, ConstantsUtil, ErrorUtil, PresetsUtil } from '@reown/appkit-utils' -import mockAppKit from './mocks/AppKit' import type { CaipNetwork } from '@reown/appkit-common' -const [mainnet, solana] = CaipNetworksUtil.extendCaipNetworks([mainnetAppKit, solanaAppKit], { - customNetworkImageUrls: {}, - projectId: 'test-project-id' -}) - -const mockOptionsExtended = { - ...mockOptions, - networks: [mainnet, solana] as [CaipNetwork, ...CaipNetwork[]], - defaultNetwork: mainnet +// Mock provider +const mockProvider = { + on: vi.fn(), + connect: vi.fn(), + disconnect: vi.fn(), + setDefaultChain: vi.fn() +} as unknown as UniversalProvider + +// Mock CaipNetwork +const mockCaipNetwork: CaipNetwork = { + id: 1, + name: 'Ethereum', + chainNamespace: 'eip155', + caipNetworkId: 'eip155:1', + rpcUrls: { + default: { http: ['https://ethereum.rpc.com'] } + }, + nativeCurrency: { + name: 'Ether', + symbol: 'ETH', + decimals: 18 + } } -vi.mock('@reown/appkit-core') - describe('UniversalAdapter', () => { - let universalAdapter: UniversalAdapterClient + let adapter: UniversalAdapter beforeEach(() => { - universalAdapter = new UniversalAdapterClient(mockOptionsExtended) - universalAdapter.walletConnectProvider = mockProvider - universalAdapter.construct(mockAppKit, mockOptionsExtended) - }) - - afterEach(() => { - vi.clearAllMocks() - }) + adapter = new UniversalAdapter() - describe('UniversalAdapter - Initialization', () => { - it('should set caipNetworks to provided caipNetworks options', () => { - expect(universalAdapter?.caipNetworks).toEqual(mockOptionsExtended.networks) + // Mock internal state using Object.defineProperty + Object.defineProperty(adapter, 'connectors', { + value: [ + { + id: 'WALLET_CONNECT', + type: 'WALLET_CONNECT', + provider: mockProvider + } + ], + writable: true }) - it('should set metadata to metadata options', () => { - expect((universalAdapter as any).appKit).toEqual(mockAppKit) + Object.defineProperty(adapter, 'caipNetworks', { + value: [mockCaipNetwork], + writable: true }) - }) - describe('UniversalAdapter - Public Methods', () => { - it('should return walletConnectProvider when getWalletConnectProvider is invoked', async () => { - const switchNetworkSpy = vi.spyOn(universalAdapter, 'switchNetwork') - const mainnet = universalAdapter.caipNetworks[0] - await universalAdapter.networkControllerClient.switchCaipNetwork(mainnet) - expect(switchNetworkSpy).toHaveBeenCalledWith(mainnet) - }) + vi.clearAllMocks() }) - describe('UniversalAdapter - Network', () => { - it('should call switchCaipNetwork when networkControllerClient.switchCaipNetwork is invoked', async () => { - const provider = await universalAdapter.getWalletConnectProvider() - expect(provider).toEqual(mockProvider) - }) + describe('connectWalletConnect', () => { + it('should connect successfully', async () => { + const onUri = vi.fn() - it('should call return correct approvedCaipNetworksData', async () => { - const approvedCaipNetworksData = - await universalAdapter.networkControllerClient.getApprovedCaipNetworksData() + await adapter.connectWalletConnect(onUri) - expect(approvedCaipNetworksData).toMatchObject({ - supportsAllNetworks: false, - approvedCaipNetworkIds: [ - mockProvider.session?.namespaces['eip155']?.chains?.[0], - mockProvider.session?.namespaces['solana']?.chains?.[0] - ] + expect(mockProvider.on).toHaveBeenCalledWith('display_uri', expect.any(Function)) + expect(mockProvider.connect).toHaveBeenCalledWith({ + optionalNamespaces: expect.any(Object) }) }) - // Something is making it so it never recognizes ChainController as the correct instance - it.skip('should call setDefaultNetwork and set first caipNetwork on setActiveCaipNetwork when there is no active caipNetwork', async () => { - const adapterSpy = vi.spyOn(universalAdapter as any, 'setDefaultNetwork') - ChainController.setRequestedCaipNetworks([mainnet], 'eip155') - const setActiveCaipNetworkSpy = vi.spyOn(ChainController, 'setActiveCaipNetwork') - const mockOnUri = vi.fn() - await universalAdapter?.connectionControllerClient?.connectWalletConnect?.(mockOnUri) - - expect(adapterSpy).toHaveBeenCalledWith(mockProvider.session?.namespaces) - expect(setActiveCaipNetworkSpy).toHaveBeenCalledWith(mainnet) - }) - - it('should set correct requestedCaipNetworks in AppKit when syncRequestedNetworks has been called', () => { - ;(universalAdapter as any).syncRequestedNetworks(mockOptionsExtended.networks) - const mainnet = universalAdapter.caipNetworks[0] - expect(mockAppKit.setRequestedCaipNetworks).toHaveBeenCalledWith([mainnet], 'eip155') - expect(mockAppKit.setRequestedCaipNetworks).toHaveBeenCalledWith([solana], 'solana') - }) - }) - - describe('UniversalAdapter - Connection', () => { - it('should connect the walletConnectProvider with the right namespaces when connectionControllerClient.connectWalletConnect is invoked', async () => { - const providerSpy = vi.spyOn( - universalAdapter.walletConnectProvider as UniversalProvider, - 'connect' - ) - const mockOnUri = vi.fn() - await universalAdapter?.connectionControllerClient?.connectWalletConnect?.(mockOnUri) - expect(providerSpy).toHaveBeenCalledWith({ - optionalNamespaces: universalAdapter.walletConnectProvider?.namespaces + it('should throw error if provider is undefined', async () => { + Object.defineProperty(adapter, 'connectors', { + value: [], + writable: true }) - }) - - it('should set the clientId in AppKit when connectionControllerClient.connectWalletConnect is invoked', async () => { - const mockOnUri = vi.fn() - await universalAdapter?.connectionControllerClient?.connectWalletConnect?.(mockOnUri) - expect(mockAppKit.setClientId).toHaveBeenCalledWith( - mockProvider.client?.core?.crypto?.getClientId() + await expect(adapter.connectWalletConnect(() => {})).rejects.toThrow( + 'UniversalAdapter:connectWalletConnect - caipNetworks or provider is undefined' ) }) - it('should update AppKit state when connectionControllerClient.connectWalletConnect is invoked', async () => { - const mockOnUri = vi.fn() - await universalAdapter?.connectionControllerClient?.connectWalletConnect?.(mockOnUri) + it('should call onUri when display_uri event is emitted', async () => { + const onUri = vi.fn() + const testUri = 'wc:test-uri' - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith( - mockProvider.session?.namespaces['eip155']?.accounts[0], - 'eip155' - ) - expect(mockAppKit.setCaipAddress).toHaveBeenCalledWith( - mockProvider.session?.namespaces['solana']?.accounts[0], - 'solana' - ) - }) - - it('should return correct provider data when getProviderData is invoked', async () => { - const data = (universalAdapter as any).getProviderData() - expect(data).toMatchObject({ - provider: mockProvider, - namespaces: mockProvider.session?.namespaces, - namespaceKeys: Object.keys(mockProvider.session?.namespaces || {}), - isConnected: true, - preferredAccountType: 'eoa' + // Call the callback directly when 'on' is called + vi.mocked(mockProvider.on).mockImplementation((event: string, callback: any) => { + if (event === 'display_uri') { + callback(testUri) + } + return mockProvider }) - }) - it('should disconnect the walletConnectProvider when disconnect is invoked', async () => { - const providerSpy = vi.spyOn( - universalAdapter.walletConnectProvider as UniversalProvider, - 'disconnect' - ) - await universalAdapter?.connectionControllerClient?.disconnect?.() - expect(providerSpy).toHaveBeenCalled() - expect(mockAppKit.resetAccount).toHaveBeenCalledWith('eip155') - expect(mockAppKit.resetAccount).toHaveBeenCalledWith('solana') - }) - - it('should set the ApprovedCaipNetworksData', async () => { - const mockOnUri = vi.fn() - await universalAdapter?.connectionControllerClient?.connectWalletConnect?.(mockOnUri) + await adapter.connectWalletConnect(onUri) - expect(mockAppKit.setApprovedCaipNetworksData).toHaveBeenCalledWith('eip155') - expect(mockAppKit.setApprovedCaipNetworksData).toHaveBeenCalledWith('solana') + expect(onUri).toHaveBeenCalledWith(testUri) }) }) - describe('UniversalAdapter - ProviderUtil', () => { - it('should set the provider in ProviderUtil when setWalletConnectProvider is called', async () => { - const mockSetProvider = vi.spyOn(ProviderUtil, 'setProvider') - const mockSetProviderId = vi.spyOn(ProviderUtil, 'setProviderId') - - await (universalAdapter as any).setWalletConnectProvider() + describe('disconnect', () => { + it('should disconnect successfully', async () => { + await adapter.disconnect() - expect(mockSetProvider).toHaveBeenCalledWith('eip155', universalAdapter.walletConnectProvider) - - expect(mockSetProviderId).toHaveBeenCalledWith('eip155', 'walletConnect') - - expect(mockSetProvider).toHaveBeenCalledWith('solana', universalAdapter.walletConnectProvider) - - expect(mockSetProviderId).toHaveBeenCalledWith('solana', 'walletConnect') + expect(mockProvider.disconnect).toHaveBeenCalled() }) - }) - - describe('UniversalAdapter - Provider', () => { - it('should set up event listeners when watchWalletConnect is called', async () => { - const provider = await universalAdapter.getWalletConnectProvider() - const providerOnSpy = vi.spyOn(provider as UniversalProvider, 'on') - await (universalAdapter as any).watchWalletConnect() + it('should handle missing provider gracefully', async () => { + Object.defineProperty(adapter, 'connectors', { + value: [], + writable: true + }) - expect(providerOnSpy).toHaveBeenCalledWith('disconnect', expect.any(Function)) - expect(providerOnSpy).toHaveBeenCalledWith('accountsChanged', expect.any(Function)) - expect(providerOnSpy).toHaveBeenCalledWith('chainChanged', expect.any(Function)) + await expect(adapter.disconnect()).resolves.not.toThrow() }) }) - describe('UniversalAdapter - Connectors', () => { - it('should sync connectors correctly', () => { - ;(universalAdapter as any).syncConnectors() - - expect(mockAppKit.setConnectors).toHaveBeenCalledWith([ - { - id: ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID, - explorerId: PresetsUtil.ConnectorExplorerIds[ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID], - imageId: PresetsUtil.ConnectorImageIds[ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID], - name: PresetsUtil.ConnectorNamesMap[ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID], - type: 'WALLET_CONNECT', - chain: universalAdapter.chainNamespace + describe('switchNetwork', () => { + it('should switch network successfully', async () => { + const polygonNetwork: CaipNetwork = { + ...mockCaipNetwork, + id: 137, + name: 'Polygon', + nativeCurrency: { + name: 'MATIC', + symbol: 'MATIC', + decimals: 18 } - ]) + } + + await adapter.switchNetwork({ caipNetwork: polygonNetwork }) + + expect(mockProvider.setDefaultChain).toHaveBeenCalledWith('eip155:137') }) - }) - describe('UniversalAdapter - Alert Errors', () => { - it('should handle alert errors based on error messages', () => { - const errors = [ - { - alert: ErrorUtil.ALERT_ERRORS.INVALID_APP_CONFIGURATION, - message: - 'Error: WebSocket connection closed abnormally with code: 3000 (Unauthorized: origin not allowed)' - }, - { - alert: ErrorUtil.ALERT_ERRORS.JWT_TOKEN_NOT_VALID, - message: - 'WebSocket connection closed abnormally with code: 3000 (JWT validation error: JWT Token is not yet valid:)' - } - ] + it('should throw error if provider is undefined', async () => { + Object.defineProperty(adapter, 'connectors', { + value: [], + writable: true + }) - for (const { alert, message } of errors) { - // @ts-expect-error - universalAdapter.handleAlertError(new Error(message)) - expect(AlertController.open).toHaveBeenCalledWith(alert, 'error') - } + await expect( + adapter.switchNetwork({ + caipNetwork: mockCaipNetwork + }) + ).rejects.toThrow('UniversalAdapter:switchNetwork - provider is undefined') }) }) }) diff --git a/packages/appkit/src/universal-adapter/client.ts b/packages/appkit/src/universal-adapter/client.ts index af7eb83d04..cc84396965 100644 --- a/packages/appkit/src/universal-adapter/client.ts +++ b/packages/appkit/src/universal-adapter/client.ts @@ -1,749 +1,145 @@ -/* eslint-disable max-depth */ -import { - AccountController, - ChainController, - ConnectionController, - CoreHelperUtil, - StorageUtil, - type ConnectionControllerClient, - type Connector, - type NetworkControllerClient, - AlertController, - BlockchainApiController -} from '@reown/appkit-core' -import { ConstantsUtil, ErrorUtil, LoggerUtil, PresetsUtil } from '@reown/appkit-utils' -import UniversalProvider from '@walletconnect/universal-provider' -import type { UniversalProviderOpts } from '@walletconnect/universal-provider' -import { WcHelpersUtil } from '../utils/HelpersUtil.js' -import type { AppKit } from '../client.js' -import type { SessionTypes } from '@walletconnect/types' -import type { CaipNetwork, CaipAddress, ChainNamespace, AdapterType } from '@reown/appkit-common' -import { - SafeLocalStorage, - SafeLocalStorageKeys, - ConstantsUtil as CommonConstantsUtil -} from '@reown/appkit-common' -import { ProviderUtil } from '../store/index.js' -import type { AppKitOptions, AppKitOptionsWithCaipNetworks } from '../utils/TypesUtil.js' -import bs58 from 'bs58' +import type UniversalProvider from '@walletconnect/universal-provider' +import { AdapterBlueprint } from '../adapters/ChainAdapterBlueprint.js' +import { WcHelpersUtil } from '../utils/index.js' -type Metadata = { - name: string - description: string - url: string - icons: string[] -} - -const OPTIONAL_METHODS = [ - 'eth_accounts', - 'eth_requestAccounts', - 'eth_sendRawTransaction', - 'eth_sign', - 'eth_signTransaction', - 'eth_signTypedData', - 'eth_signTypedData_v3', - 'eth_signTypedData_v4', - 'eth_sendTransaction', - 'personal_sign', - 'wallet_switchEthereumChain', - 'wallet_addEthereumChain', - 'wallet_getPermissions', - 'wallet_requestPermissions', - 'wallet_registerOnboarding', - 'wallet_watchAsset', - 'wallet_scanQRCode', - // EIP-5792 - 'wallet_getCallsStatus', - 'wallet_sendCalls', - 'wallet_getCapabilities', - // EIP-7715 - 'wallet_grantPermissions', - 'wallet_revokePermissions' -] - -// -- Client -------------------------------------------------------------------- -export class UniversalAdapterClient { - private walletConnectProviderInitPromise?: Promise - - private appKit: AppKit | undefined = undefined - - public caipNetworks: [CaipNetwork, ...CaipNetwork[]] - - public walletConnectProvider?: UniversalProvider - - public metadata?: Metadata - - public isUniversalAdapterClient = true - - public chainNamespace: ChainNamespace - - public networkControllerClient: NetworkControllerClient - - public connectionControllerClient: ConnectionControllerClient - - public options: AppKitOptions | undefined = undefined - - public adapterType: AdapterType = 'universal' - - public reportedAlertErrors: Record = {} - - public constructor(options: AppKitOptionsWithCaipNetworks) { - const { siweConfig, metadata } = options - - this.caipNetworks = options.networks - - this.chainNamespace = CommonConstantsUtil.CHAIN.EVM - - this.metadata = metadata - - this.networkControllerClient = { - // @ts-expect-error switchCaipNetwork is async for some adapter but not for this adapter - switchCaipNetwork: caipNetwork => { - if (caipNetwork) { - this.switchNetwork(caipNetwork) - } - }, - - getApprovedCaipNetworksData: async () => { - const provider = await this.getWalletConnectProvider() - - if (!provider) { - return Promise.resolve({ - supportsAllNetworks: false, - approvedCaipNetworkIds: [] - }) - } - - const approvedCaipNetworkIds = WcHelpersUtil.getChainsFromNamespaces( - provider.session?.namespaces - ) - - return Promise.resolve({ - supportsAllNetworks: false, - approvedCaipNetworkIds - }) - } - } - - this.connectionControllerClient = { - connectWalletConnect: async onUri => { - const WalletConnectProvider = await this.getWalletConnectProvider() - - if (!WalletConnectProvider) { - throw new Error('connectionControllerClient:getWalletConnectUri - provider is undefined') - } - - WalletConnectProvider.on('display_uri', (uri: string) => { - onUri(uri) - }) - - if ( - ChainController.state.activeChain && - ChainController.state?.chains?.get(ChainController.state.activeChain)?.adapterType === - 'wagmi' - ) { - const adapter = ChainController.state.chains.get(ChainController.state.activeChain) - await adapter?.connectionControllerClient?.connectWalletConnect?.(onUri) - this.setWalletConnectProvider() - } else { - const siweParams = await siweConfig?.getMessageParams?.() - const isSiweEnabled = siweConfig?.options?.enabled - const isProviderSupported = typeof WalletConnectProvider?.authenticate === 'function' - const isSiweParamsValid = siweParams && Object.keys(siweParams || {}).length > 0 - const clientId = await WalletConnectProvider?.client?.core?.crypto?.getClientId() - if (clientId) { - this.appKit?.setClientId(clientId) - } - if ( - siweConfig && - isSiweEnabled && - siweParams && - isProviderSupported && - isSiweParamsValid && - ChainController.state.activeChain === CommonConstantsUtil.CHAIN.EVM - ) { - const { SIWEController, getDidChainId, getDidAddress } = await import( - '@reown/appkit-siwe' - ) - - const chains = this.caipNetworks - ?.filter(network => network.chainNamespace === CommonConstantsUtil.CHAIN.EVM) - .map(chain => chain.caipNetworkId) as string[] - - siweParams.chains = this.caipNetworks - ?.filter(network => network.chainNamespace === CommonConstantsUtil.CHAIN.EVM) - .map(chain => chain.id) as number[] - - const result = await WalletConnectProvider.authenticate({ - nonce: await siweConfig?.getNonce?.(), - methods: [...OPTIONAL_METHODS], - ...siweParams, - chains - }) - // Auths is an array of signed CACAO objects https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-74.md - const signedCacao = result?.auths?.[0] - - if (signedCacao) { - const { p, s } = signedCacao - const cacaoChainId = getDidChainId(p.iss) - const address = getDidAddress(p.iss) - if (address && cacaoChainId) { - SIWEController.setSession({ - address, - chainId: parseInt(cacaoChainId, 10) - }) - } - - try { - // Kicks off verifyMessage and populates external states - const message = WalletConnectProvider.client.formatAuthMessage({ - request: p, - iss: p.iss - }) +export class UniversalAdapter extends AdapterBlueprint { + public async connectWalletConnect(onUri: (uri: string) => void) { + const connector = this.connectors.find(c => c.type === 'WALLET_CONNECT') - await SIWEController.verifyMessage({ - message, - signature: s.s, - cacao: signedCacao - }) - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error verifying message', error) - // eslint-disable-next-line no-console - await WalletConnectProvider.disconnect().catch(console.error) - // eslint-disable-next-line no-console - await SIWEController.signOut().catch(console.error) - throw error - } - } - } else { - const optionalNamespaces = WcHelpersUtil.createNamespaces(this.caipNetworks) - await WalletConnectProvider.connect({ optionalNamespaces }) - } - this.setWalletConnectProvider() - } - }, + const provider = connector?.provider as UniversalProvider - disconnect: async () => { - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_ID) - - if (siweConfig?.options?.signOutOnDisconnect) { - const { SIWEController } = await import('@reown/appkit-siwe') - await SIWEController.signOut() - } - - await this.walletConnectProvider?.disconnect() - - this.appKit?.resetAccount(CommonConstantsUtil.CHAIN.EVM) - this.appKit?.resetAccount(CommonConstantsUtil.CHAIN.SOLANA) - }, - - signMessage: async (message: string) => { - const provider = await this.getWalletConnectProvider() - const caipAddress = ChainController.state.activeCaipAddress - const address = CoreHelperUtil.getPlainAddress(caipAddress) - - if (!provider) { - throw new Error('connectionControllerClient:signMessage - provider is undefined') - } - - let signature = '' - - if ( - ChainController.state.activeCaipNetwork?.chainNamespace === - CommonConstantsUtil.CHAIN.SOLANA - ) { - const response = await provider.request( - { - method: 'solana_signMessage', - params: { - message: bs58.encode(new TextEncoder().encode(message)), - pubkey: address - } - }, - ChainController.state.activeCaipNetwork?.caipNetworkId - ) - - signature = (response as { signature: string }).signature - } else { - signature = await provider.request( - { - method: 'personal_sign', - params: [message, address] - }, - ChainController.state.activeCaipNetwork?.caipNetworkId - ) - } - - return signature - }, - - estimateGas: async () => await Promise.resolve(BigInt(0)), - // -- Transaction methods --------------------------------------------------- - /** - * - * These methods are supported only on `wagmi` and `ethers` since the Solana SDK does not support them in the same way. - * These function definition is to have a type parity between the clients. Currently not in use. - */ - getEnsAvatar: async (value: string) => await Promise.resolve(value), - - getEnsAddress: async (value: string) => await Promise.resolve(value), - - writeContract: async () => await Promise.resolve('0x'), - - getCapabilities: async (params: string) => { - const provider = await this.getWalletConnectProvider() - - if (!provider) { - throw new Error('connectionControllerClient:getCapabilities - provider is undefined') - } - - const walletCapabilitiesString = provider.session?.sessionProperties?.['capabilities'] - if (walletCapabilitiesString) { - const walletCapabilities = this.parseWalletCapabilities(walletCapabilitiesString) - const accountCapabilities = walletCapabilities[params] - if (accountCapabilities) { - return accountCapabilities - } - } - - return await provider.request({ method: 'wallet_getCapabilities', params: [params] }) - }, - - grantPermissions: async (params: object | readonly unknown[]) => { - const provider = await this.getWalletConnectProvider() - if (!provider) { - throw new Error('connectionControllerClient:grantPermissions - provider is undefined') - } - - return provider.request({ method: 'wallet_grantPermissions', params }) - }, - revokePermissions: async session => { - const provider = await this.getWalletConnectProvider() - if (!provider) { - throw new Error('connectionControllerClient:grantPermissions - provider is undefined') - } - - return provider.request({ method: 'wallet_revokePermissions', params: [session] }) - }, - - sendTransaction: async () => await Promise.resolve('0x'), - - parseUnits: () => BigInt(0), - - formatUnits: () => '' + if (!this.caipNetworks || !provider) { + throw new Error( + 'UniversalAdapter:connectWalletConnect - caipNetworks or provider is undefined' + ) } - ChainController.subscribeKey('activeCaipNetwork', val => { - const caipAddress = this.appKit?.getCaipAddress(this.chainNamespace) - - if (val && caipAddress) { - this.syncBalance(CoreHelperUtil.getPlainAddress(caipAddress) as `0x${string}`, val) - this.syncAccount() - } - }) - ChainController.subscribeKey('activeCaipAddress', val => { - const caipNetwork = ChainController.state.activeCaipNetwork - if (val && caipNetwork) { - this.syncBalance(CoreHelperUtil.getPlainAddress(val) as `0x${string}`, caipNetwork) - this.syncAccount() - } + provider.on('display_uri', (uri: string) => { + onUri(uri) }) - } - // -- Public ------------------------------------------------------------------ - public construct(appkit: AppKit, options: AppKitOptionsWithCaipNetworks) { - this.appKit = appkit - this.options = options + const namespaces = WcHelpersUtil.createNamespaces(this.caipNetworks) - this.createProvider() - this.syncRequestedNetworks(this.caipNetworks) - this.syncConnectors() + await provider.connect({ optionalNamespaces: namespaces }) } - - public switchNetwork(caipNetwork: CaipNetwork) { - if (caipNetwork) { - if (this.walletConnectProvider) { - this.walletConnectProvider.setDefaultChain(caipNetwork.caipNetworkId) - } - } + public async connect( + params: AdapterBlueprint.ConnectParams + ): Promise { + return Promise.resolve({ + id: 'WALLET_CONNECT', + type: 'WALLET_CONNECT' as const, + chainId: Number(params.chainId), + provider: this.provider as UniversalProvider, + address: '' + }) } public async disconnect() { - if (this.walletConnectProvider) { - await (this.walletConnectProvider as unknown as UniversalProvider).disconnect() - this.appKit?.resetAccount(CommonConstantsUtil.CHAIN.EVM) - this.appKit?.resetAccount(CommonConstantsUtil.CHAIN.SOLANA) - } + const connector = this.connectors.find(c => c.id === 'WALLET_CONNECT') + const provider = connector?.provider + await provider?.disconnect() } - public async getWalletConnectProvider() { - if (!this.walletConnectProvider) { - try { - await this.createProvider() - } catch (error) { - throw new Error('EthereumAdapter:getWalletConnectProvider - Cannot create provider') - } - } - - return this.walletConnectProvider + public async syncConnectors() { + return Promise.resolve() } - // -- Private ----------------------------------------------------------------- - private async syncBalance(address: `0x${string}`, caipNetwork: CaipNetwork) { - const isExistingNetwork = this.appKit - ?.getCaipNetworks(caipNetwork.chainNamespace) - .find(network => network.id === caipNetwork.id) - - // How to fetch balance on non-evm networks? - if (caipNetwork && isExistingNetwork) { - try { - const { balances } = await BlockchainApiController.getBalance( - address, - String(caipNetwork.id) - ) - const balance = balances.find(b => b.symbol === caipNetwork.nativeCurrency.symbol) - this.appKit?.setBalance( - balance?.quantity.numeric || '0', - caipNetwork.nativeCurrency.symbol, - this.chainNamespace - ) - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error fetching balance', error) - } - } + public async getBalance(): Promise { + return Promise.resolve({ + balance: '0', + decimals: 0, + symbol: '' + }) } - private createProvider() { - if ( - !this.walletConnectProviderInitPromise && - typeof window !== 'undefined' && - this.options?.projectId - ) { - this.walletConnectProviderInitPromise = this.initWalletConnectProvider( - this.options?.projectId - ) + public override async signMessage( + params: AdapterBlueprint.SignMessageParams + ): Promise { + const { provider, message, address } = params + if (!provider) { + throw new Error('UniversalAdapter:signMessage - provider is undefined') } - return this.walletConnectProviderInitPromise - } - - private handleAlertError(error: Error) { - const matchedUniversalProviderError = Object.entries(ErrorUtil.UniversalProviderErrors).find( - ([, { message }]) => error.message.includes(message) - ) - - const [errorKey, errorValue] = matchedUniversalProviderError ?? [] - - const { message, alertErrorKey } = errorValue ?? {} - - if (errorKey && message && !this.reportedAlertErrors[errorKey]) { - const alertError = - ErrorUtil.ALERT_ERRORS[alertErrorKey as keyof typeof ErrorUtil.ALERT_ERRORS] - - if (alertError) { - AlertController.open(alertError, 'error') - this.reportedAlertErrors[errorKey] = true - } - } - } - - private async initWalletConnectProvider(projectId: string) { - const logger = LoggerUtil.createLogger((error, ...args) => { - if (error) { - this.handleAlertError(error) - } - // eslint-disable-next-line no-console - console.error(...args) + const signature = await provider.request({ + method: 'personal_sign', + params: [message, address] }) - const walletConnectProviderOptions: UniversalProviderOpts = { - projectId, - metadata: { - name: this.metadata ? this.metadata.name : '', - description: this.metadata ? this.metadata.description : '', - url: this.metadata ? this.metadata.url : '', - icons: this.metadata ? this.metadata.icons : [''] - }, - logger - } - - this.walletConnectProvider = await UniversalProvider.init(walletConnectProviderOptions) - await this.checkActiveWalletConnectProvider() + return { signature: signature as `0x${string}` } } - private syncRequestedNetworks(caipNetworks: CaipNetwork[]) { - const uniqueChainNamespaces = [ - ...new Set(caipNetworks.map(caipNetwork => caipNetwork.chainNamespace)) - ] - uniqueChainNamespaces - .filter(c => Boolean(c)) - .forEach(chainNamespace => { - this.appKit?.setRequestedCaipNetworks( - caipNetworks.filter(caipNetwork => caipNetwork.chainNamespace === chainNamespace), - chainNamespace - ) - }) + // -- Transaction methods --------------------------------------------------- + /** + * + * These methods are supported only on `wagmi` and `ethers` since the Solana SDK does not support them in the same way. + * These function definition is to have a type parity between the clients. Currently not in use. + */ + public override async estimateGas(): Promise { + return Promise.resolve({ + gas: BigInt(0) + }) } - private async checkActiveWalletConnectProvider() { - const WalletConnectProvider = await this.getWalletConnectProvider() - const walletId = SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_ID) - - if (WalletConnectProvider) { - if (walletId === ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID) { - this.setWalletConnectProvider() - } - } + public async getProfile(): Promise { + return Promise.resolve({ + profileImage: '', + profileName: '' + }) } - private setWalletConnectProvider() { - SafeLocalStorage.setItem( - SafeLocalStorageKeys.WALLET_ID, - ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID - ) - - const nameSpaces = this.walletConnectProvider?.session?.namespaces - - if (nameSpaces) { - const reversedChainNamespaces = Object.keys(nameSpaces).reverse() as ChainNamespace[] - reversedChainNamespaces.forEach(chainNamespace => { - const caipAddress = nameSpaces?.[chainNamespace]?.accounts[0] as CaipAddress | undefined - - ProviderUtil.setProvider(chainNamespace, this.walletConnectProvider) - ProviderUtil.setProviderId(chainNamespace, 'walletConnect') - this.appKit?.setApprovedCaipNetworksData(chainNamespace) - - if (caipAddress) { - this.appKit?.setCaipAddress(caipAddress, chainNamespace) - } - }) - - const storedCaipNetwork = StorageUtil.getStoredActiveCaipNetwork() - const activeCaipNetwork = ChainController.state.activeCaipNetwork - try { - if (storedCaipNetwork) { - ChainController.setActiveCaipNetwork(storedCaipNetwork) - } else if ( - !activeCaipNetwork || - !ChainController.getAllApprovedCaipNetworkIds().includes(activeCaipNetwork.caipNetworkId) - ) { - this.setDefaultNetwork(nameSpaces) - } - } catch (error) { - console.warn('>>> Error setting active caip network', error) - } - } - - this.syncAccount() - this.watchWalletConnect() + public async sendTransaction(): Promise { + return Promise.resolve({ + hash: '' + }) } - private setDefaultNetwork(nameSpaces: SessionTypes.Namespaces) { - const chainNamespace = this.caipNetworks[0]?.chainNamespace - if (chainNamespace) { - const namespace = nameSpaces?.[chainNamespace] - - if (namespace?.chains) { - const chainId = namespace.chains[0] - - if (chainId) { - const requestedCaipNetworks = ChainController.getRequestedCaipNetworks(chainNamespace) - - if (requestedCaipNetworks) { - const network = requestedCaipNetworks.find(c => c.caipNetworkId === chainId) - - if (network) { - ChainController.setActiveCaipNetwork(network as unknown as CaipNetwork) - } - } - } - } - } + public async writeContract(): Promise { + return Promise.resolve({ + hash: '' + }) } - private async watchWalletConnect() { - const provider = await this.getWalletConnectProvider() - const namespaces = provider?.session?.namespaces || {} - - function disconnectHandler() { - Object.keys(namespaces).forEach(key => { - AccountController.resetAccount(key as ChainNamespace) - }) - ConnectionController.resetWcConnection() - - SafeLocalStorage.removeItem(SafeLocalStorageKeys.WALLET_ID) - - provider?.removeListener('disconnect', disconnectHandler) - provider?.removeListener('accountsChanged', accountsChangedHandler) - } - - const accountsChangedHandler = (accounts: string[]) => { - if (accounts.length > 0) { - this.syncAccount() - } - } - - const chainChanged = (chainId: number | string) => { - // eslint-disable-next-line eqeqeq - const caipNetwork = this.caipNetworks.find(c => c.id == chainId) - const currentCaipNetwork = this.appKit?.getCaipNetwork() - - if (!caipNetwork) { - const namespace = this.appKit?.getActiveChainNamespace() || CommonConstantsUtil.CHAIN.EVM - ChainController.setActiveCaipNetwork({ - id: chainId, - caipNetworkId: `${namespace}:${chainId}`, - name: 'Unknown Network', - chainNamespace: namespace, - nativeCurrency: { - name: '', - decimals: 0, - symbol: '' - }, - rpcUrls: { - default: { - http: [] - } - } - }) - - return - } - - if (!currentCaipNetwork || currentCaipNetwork?.id !== caipNetwork?.id) { - this.appKit?.setCaipNetwork(caipNetwork) - } - } - - if (provider) { - provider.on('disconnect', disconnectHandler) - provider.on('accountsChanged', accountsChangedHandler) - provider.on('chainChanged', chainChanged) - provider.on('connect', this.syncAccount.bind(this)) - } + public async getEnsAddress(): Promise { + return Promise.resolve({ + address: false + }) } - private getProviderData() { - const namespaces = this.walletConnectProvider?.session?.namespaces || {} - - const isConnected = this.appKit?.getIsConnectedState() || false - const preferredAccountType = this.appKit?.getPreferredAccountType() || '' - - return { - provider: this.walletConnectProvider, - namespaces, - namespaceKeys: namespaces ? Object.keys(namespaces) : [], - isConnected, - preferredAccountType - } + public parseUnits(): AdapterBlueprint.ParseUnitsResult { + return 0n } - private syncAccount() { - const { namespaceKeys, namespaces } = this.getProviderData() - - const preferredAccountType = this.appKit?.getPreferredAccountType() - - const isConnected = this.appKit?.getIsConnectedState() || false - - if (isConnected) { - namespaceKeys.forEach(async key => { - const chainNamespace = key as ChainNamespace - const address = namespaces?.[key]?.accounts[0] as CaipAddress - const isNamespaceConnected = this.appKit?.getCaipAddress(chainNamespace) - - if (!isNamespaceConnected) { - this.appKit?.setPreferredAccountType(preferredAccountType, chainNamespace) - this.appKit?.setCaipAddress(address, chainNamespace) - this.syncConnectedWalletInfo() - await Promise.all([this.appKit?.setApprovedCaipNetworksData(chainNamespace)]) - } - - this.syncAccounts() - }) - } else { - this.appKit?.resetWcConnection() - this.appKit?.resetNetwork(this.chainNamespace) - this.syncAccounts(true) - } + public formatUnits(): AdapterBlueprint.FormatUnitsResult { + return '0' } - private syncAccounts(reset = false) { - const { namespaces } = this.getProviderData() - const chainNamespaces = Object.keys(namespaces) as ChainNamespace[] - - chainNamespaces.forEach(chainNamespace => { - const addresses = namespaces?.[chainNamespace]?.accounts - ?.map(account => { - const [, , address] = account.split(':') - - return address - }) - .filter((address, index, self) => self.indexOf(address) === index) as string[] - - if (reset) { - this.appKit?.setAllAccounts([], chainNamespace) - } - - if (addresses) { - this.appKit?.setAllAccounts( - addresses.map(address => ({ address, type: 'eoa' })), - chainNamespace - ) - } + public async syncConnection() { + return Promise.resolve({ + id: 'WALLET_CONNECT', + type: 'WALLET_CONNECT' as const, + chainId: 1, + provider: this.provider as UniversalProvider, + address: '' }) } - private syncConnectedWalletInfo() { - const currentActiveWallet = SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_ID) - const namespaces = this.walletConnectProvider?.session?.namespaces || {} - const chainNamespaces = Object.keys(namespaces) as ChainNamespace[] + // eslint-disable-next-line @typescript-eslint/require-await + public async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams) { + const { caipNetwork } = params + const connector = this.connectors.find(c => c.type === 'WALLET_CONNECT') + const provider = connector?.provider as UniversalProvider - chainNamespaces.forEach(chainNamespace => { - if (this.walletConnectProvider?.session) { - this.appKit?.setConnectedWalletInfo( - { - ...this.walletConnectProvider.session.peer.metadata, - name: this.walletConnectProvider.session.peer.metadata.name, - icon: this.walletConnectProvider.session.peer.metadata.icons?.[0] - }, - chainNamespace - ) - } else if (currentActiveWallet) { - this.appKit?.setConnectedWalletInfo( - { name: currentActiveWallet }, - CommonConstantsUtil.CHAIN.EVM - ) - this.appKit?.setConnectedWalletInfo( - { name: currentActiveWallet }, - CommonConstantsUtil.CHAIN.SOLANA - ) - } - }) + if (!provider) { + throw new Error('UniversalAdapter:switchNetwork - provider is undefined') + } + provider.setDefaultChain(`${caipNetwork.chainNamespace}:${String(caipNetwork.id)}`) } - private syncConnectors() { - const w3mConnectors: Connector[] = [] + public getWalletConnectProvider() { + const connector = this.connectors.find(c => c.type === 'WALLET_CONNECT') - w3mConnectors.push({ - id: ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID, - explorerId: PresetsUtil.ConnectorExplorerIds[ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID], - imageId: PresetsUtil.ConnectorImageIds[ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID], - name: PresetsUtil.ConnectorNamesMap[ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID], - type: 'WALLET_CONNECT', - chain: this.chainNamespace - }) - - this.appKit?.setConnectors(w3mConnectors) - } - private parseWalletCapabilities(walletCapabilitiesString: string) { - try { - const walletCapabilities = JSON.parse(walletCapabilitiesString) + const provider = connector?.provider as UniversalProvider - return walletCapabilities - } catch (error) { - throw new Error('Error parsing wallet capabilities') - } + return provider } } diff --git a/packages/appkit/src/universal-adapter/index.ts b/packages/appkit/src/universal-adapter/index.ts index 139dd40809..7294a1134c 100644 --- a/packages/appkit/src/universal-adapter/index.ts +++ b/packages/appkit/src/universal-adapter/index.ts @@ -1 +1 @@ -export { UniversalAdapterClient } from './client.js' +export { UniversalAdapter } from './client.js' diff --git a/packages/appkit/src/utils/HelpersUtil.ts b/packages/appkit/src/utils/HelpersUtil.ts index 303c85c073..327fc91263 100644 --- a/packages/appkit/src/utils/HelpersUtil.ts +++ b/packages/appkit/src/utils/HelpersUtil.ts @@ -1,6 +1,7 @@ import type { NamespaceConfig, Namespace } from '@walletconnect/universal-provider' import type { CaipNetwork, CaipNetworkId, ChainNamespace } from '@reown/appkit-common' import type { SessionTypes } from '@walletconnect/types' +import { EnsController } from '@reown/appkit-core' import { solana, solanaDevnet } from '../networks/index.js' export const WcHelpersUtil = { @@ -80,6 +81,13 @@ export const WcHelpersUtil = { }, {}) }, + resolveReownName: async (name: string) => { + const wcNameAddress = await EnsController.resolveName(name) + const networkNameAddresses = Object.values(wcNameAddress?.addresses) || [] + + return networkNameAddresses[0]?.address || false + }, + getChainsFromNamespaces(namespaces: SessionTypes.Namespaces = {}): CaipNetworkId[] { return Object.values(namespaces).flatMap(namespace => { const chains = (namespace.chains || []) as CaipNetworkId[] diff --git a/packages/common/src/utils/SafeLocalStorage.ts b/packages/common/src/utils/SafeLocalStorage.ts index 19456c7524..040a0e1e1a 100644 --- a/packages/common/src/utils/SafeLocalStorage.ts +++ b/packages/common/src/utils/SafeLocalStorage.ts @@ -8,6 +8,8 @@ export type SafeLocalStorageItems = { '@appkit/connected_social': string '@appkit/connected_social_username': string '@appkit/recent_wallets': string + '@appkit/deeplink_choice': string + '@appkit/connected_namespace': string /* * DO NOT CHANGE: @walletconnect/universal-provider requires us to set this specific key * This value is a stringified version of { href: stiring; name: string } @@ -25,7 +27,9 @@ export const SafeLocalStorageKeys = { CONNECTED_SOCIAL: '@appkit/connected_social', CONNECTED_SOCIAL_USERNAME: '@appkit/connected_social_username', RECENT_WALLETS: '@appkit/recent_wallets', - DEEPLINK_CHOICE: 'WALLETCONNECT_DEEPLINK_CHOICE' + DEEPLINK_CHOICE: '@appkit/deeplink_choice', + CONNECTED_NAMESPACE: '@appkit/connected_namespace', + WALLETCONNECT_DEEPLINK_CHOICE: 'WALLETCONNECT_DEEPLINK_CHOICE' } as const export const SafeLocalStorage = { diff --git a/packages/core/src/controllers/AccountController.ts b/packages/core/src/controllers/AccountController.ts index 622b9fdf5f..ee47ef7cd0 100644 --- a/packages/core/src/controllers/AccountController.ts +++ b/packages/core/src/controllers/AccountController.ts @@ -86,7 +86,9 @@ export const AccountController = { 'accountState', accountState => { if (accountState) { - const nextValue = accountState[property] + const nextValue = accountState[ + property as keyof typeof accountState + ] as AccountControllerState[K] if (prev !== nextValue) { prev = nextValue callback(nextValue) @@ -117,7 +119,10 @@ export const AccountController = { ) { const newAddress = caipAddress ? CoreHelperUtil.getPlainAddress(caipAddress) : undefined - ChainController.state.activeCaipAddress = caipAddress + if (chain === ChainController.state.activeChain) { + ChainController.state.activeCaipAddress = caipAddress + } + ChainController.setAccountProp('caipAddress', caipAddress, chain) ChainController.setAccountProp('address', newAddress, chain) }, diff --git a/packages/core/src/controllers/ChainController.ts b/packages/core/src/controllers/ChainController.ts index 38a2cef187..01cc3ef6cd 100644 --- a/packages/core/src/controllers/ChainController.ts +++ b/packages/core/src/controllers/ChainController.ts @@ -1,6 +1,11 @@ import { proxyMap, subscribeKey as subKey } from 'valtio/vanilla/utils' import { proxy, ref, subscribe as sub } from 'valtio/vanilla' -import type { AdapterNetworkState, ChainAdapter, Connector } from '../utils/TypeUtil.js' +import type { + AdapterAccountState, + AdapterNetworkState, + ChainAdapter, + Connector +} from '../utils/TypeUtil.js' import { AccountController, type AccountControllerState } from './AccountController.js' import { PublicStateController } from './PublicStateController.js' @@ -13,35 +18,11 @@ import { type CaipNetworkId, type ChainNamespace } from '@reown/appkit-common' -import { StorageUtil } from '../utils/StorageUtil.js' import { CoreHelperUtil } from '../utils/CoreHelperUtil.js' import { ConstantsUtil } from '../utils/ConstantsUtil.js' import { ModalController } from './ModalController.js' import { EventsController } from './EventsController.js' -// -- Types --------------------------------------------- // -export interface ChainControllerState { - activeChain: ChainNamespace | undefined - activeCaipAddress: CaipAddress | undefined - activeCaipNetwork?: CaipNetwork - chains: Map - activeConnector?: Connector - universalAdapter: Pick - noAdapters: boolean -} - -type ChainControllerStateKey = keyof ChainControllerState - -type ChainsInitializerAdapter = Pick< - ChainAdapter, - | 'connectionControllerClient' - | 'networkControllerClient' - | 'defaultNetwork' - | 'chainNamespace' - | 'adapterType' - | 'caipNetworks' -> - // -- Constants ----------------------------------------- // const accountState: AccountControllerState = { currentTab: 0, @@ -56,6 +37,19 @@ const networkState: AdapterNetworkState = { smartAccountEnabledNetworks: [] } +// -- Types --------------------------------------------- // +export interface ChainControllerState { + activeChain: ChainNamespace | undefined + activeCaipAddress: CaipAddress | undefined + activeCaipNetwork?: CaipNetwork + chains: Map + activeConnector?: Connector + universalAdapter: Pick + noAdapters: boolean +} + +type ChainControllerStateKey = keyof ChainControllerState + // -- State --------------------------------------------- // const state = proxy({ chains: proxyMap(), @@ -106,20 +100,17 @@ export const ChainController = { }) }, - initialize(adapters: ChainsInitializerAdapter[]) { + initialize(adapters: ChainAdapter[]) { const adapterToActivate = adapters?.[0] - - if (adapters?.length === 0) { + if (adapters?.length === 0 || !adapterToActivate) { state.noAdapters = true } - if (!state.noAdapters) { - state.activeChain = adapterToActivate?.chainNamespace - PublicStateController.set({ activeChain: adapterToActivate?.chainNamespace }) - - adapters.forEach((adapter: ChainsInitializerAdapter) => { - state.chains.set(adapter.chainNamespace, { - chainNamespace: adapter.chainNamespace, + state.activeChain = adapterToActivate?.namespace + PublicStateController.set({ activeChain: adapterToActivate?.namespace }) + adapters.forEach((adapter: ChainAdapter) => { + state.chains.set(adapter.namespace as ChainNamespace, { + namespace: adapter.namespace, connectionControllerClient: adapter.connectionControllerClient, networkControllerClient: adapter.networkControllerClient, adapterType: adapter.adapterType, @@ -131,41 +122,6 @@ export const ChainController = { } }, - initializeUniversalAdapter( - adapter: ChainsInitializerAdapter, - adapters: ChainsInitializerAdapter[] - ) { - state.universalAdapter = adapter - - if (adapters.length === 0) { - const storedCaipNetwork = StorageUtil.getStoredActiveCaipNetwork() - - try { - if (storedCaipNetwork) { - state.activeChain = storedCaipNetwork.chainNamespace - } else { - state.activeChain = - adapter?.defaultNetwork?.chainNamespace ?? adapter.caipNetworks[0]?.chainNamespace - } - } catch (error) { - console.warn('>>> Error setting active caip network', error) - } - } - - const chains = [...new Set(adapter.caipNetworks.map(caipNetwork => caipNetwork.chainNamespace))] - chains.forEach((chain: ChainNamespace) => { - state.chains.set(chain, { - chainNamespace: chain, - connectionControllerClient: undefined, - networkControllerClient: undefined, - adapterType: adapter.adapterType, - accountState, - networkState, - caipNetworks: adapter.caipNetworks - }) - }) - }, - setAdapterNetworkState(chain: ChainNamespace, props: Partial) { const chainAdapter = state.chains.get(chain) @@ -182,7 +138,7 @@ export const ChainController = { setChainAccountData( chain: ChainNamespace | undefined, accountProps: Partial, - replaceState = true + _unknown = true ) { if (!chain) { throw new Error('Chain is required to update chain account data') @@ -196,7 +152,7 @@ export const ChainController = { ...accountProps } as AccountControllerState) state.chains.set(chain, chainAdapter) - if (replaceState || state.chains.size === 1 || state.activeChain === chain) { + if (state.chains.size === 1 || state.activeChain === chain) { if (accountProps.caipAddress) { state.activeCaipAddress = accountProps.caipAddress } @@ -273,7 +229,10 @@ export const ChainController = { return } - state.chains.get(caipNetwork.chainNamespace)?.caipNetworks.push(caipNetwork) + const chain = state.chains.get(caipNetwork.chainNamespace) + if (chain) { + chain?.caipNetworks?.push(caipNetwork) + } }, async switchActiveNetwork(network: CaipNetwork) { @@ -301,26 +260,9 @@ export const ChainController = { }, getNetworkControllerClient(chainNamespace?: ChainNamespace) { - const walletId = SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_ID) const chain = chainNamespace || state.activeChain - const isWcConnector = walletId === 'walletConnect' - const universalNetworkControllerClient = state.universalAdapter.networkControllerClient - - const shouldUseUniversalAdapter = isWcConnector || state.noAdapters - if (shouldUseUniversalAdapter) { - if (!universalNetworkControllerClient) { - throw new Error("Universal Adapter's networkControllerClient is not set") - } - - return universalNetworkControllerClient - } - - if (!chain) { - throw new Error('Chain is required to get network controller client') - } - - const chainAdapter = state.chains.get(chain) + const chainAdapter = state.chains.get(chain as ChainNamespace) if (!chainAdapter) { throw new Error('Chain adapter not found') @@ -335,20 +277,6 @@ export const ChainController = { getConnectionControllerClient(_chain?: ChainNamespace) { const chain = _chain || state.activeChain - const isWcConnector = - SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR) === 'WALLET_CONNECT' - const universalConnectionControllerClient = state.universalAdapter.connectionControllerClient - const hasWagmiAdapter = state.chains.get('eip155')?.adapterType === 'wagmi' - - const shouldUseUniversalAdapter = (isWcConnector && !hasWagmiAdapter) || state.noAdapters - - if (shouldUseUniversalAdapter) { - if (!universalConnectionControllerClient) { - throw new Error("Universal Adapter's ConnectionControllerClient is not set") - } - - return universalConnectionControllerClient - } if (!chain) { throw new Error('Chain is required to get connection controller client') @@ -356,18 +284,14 @@ export const ChainController = { const chainAdapter = state.chains.get(chain) - if (!chainAdapter) { - throw new Error('Chain adapter not found') - } - - if (!chainAdapter.connectionControllerClient) { + if (!chainAdapter?.connectionControllerClient) { throw new Error('ConnectionController client not set') } return chainAdapter.connectionControllerClient }, - getAccountProp( + getAccountProp( key: K, _chain?: ChainNamespace ): AccountControllerState[K] | undefined { @@ -419,7 +343,7 @@ export const ChainController = { const requestedCaipNetworks: CaipNetwork[] = [] state.chains.forEach(chainAdapter => { - const caipNetworks = this.getRequestedCaipNetworks(chainAdapter.chainNamespace) + const caipNetworks = this.getRequestedCaipNetworks(chainAdapter.namespace as ChainNamespace) requestedCaipNetworks.push(...caipNetworks) }) @@ -434,7 +358,7 @@ export const ChainController = { const approvedCaipNetworkIds: CaipNetworkId[] = [] state.chains.forEach(chainAdapter => { - const approvedIds = this.getApprovedCaipNetworkIds(chainAdapter.chainNamespace) + const approvedIds = this.getApprovedCaipNetworkIds(chainAdapter.namespace as ChainNamespace) approvedCaipNetworkIds.push(...approvedIds) }) @@ -459,6 +383,7 @@ export const ChainController = { async setApprovedCaipNetworksData(namespace: ChainNamespace) { const networkControllerClient = this.getNetworkControllerClient() const data = await networkControllerClient?.getApprovedCaipNetworksData() + this.setAdapterNetworkState(namespace, { approvedCaipNetworkIds: data?.approvedCaipNetworkIds, supportsAllNetworks: data?.supportsAllNetworks @@ -476,6 +401,16 @@ export const ChainController = { return requestedCaipNetworks?.some(network => network.id === activeCaipNetwork?.id) }, + checkIfSupportedChainId(chainId: number | string) { + if (!this.state.activeChain) { + return true + } + + const requestedCaipNetworks = this.getRequestedCaipNetworks(this.state.activeChain) + + return requestedCaipNetworks?.some(network => network.id === chainId) + }, + // Smart Account Network Handlers setSmartAccountEnabledNetworks(smartAccountEnabledNetworks: number[], chain: ChainNamespace) { this.setAdapterNetworkState(chain, { smartAccountEnabledNetworks }) diff --git a/packages/core/src/controllers/ConnectionController.ts b/packages/core/src/controllers/ConnectionController.ts index a4ebb545a5..be565be3e0 100644 --- a/packages/core/src/controllers/ConnectionController.ts +++ b/packages/core/src/controllers/ConnectionController.ts @@ -15,7 +15,7 @@ import { type W3mFrameTypes } from '@reown/appkit-wallet' import { ModalController } from './ModalController.js' import { ConnectorController } from './ConnectorController.js' import { EventsController } from './EventsController.js' -import type { ChainNamespace } from '@reown/appkit-common' +import type { CaipNetwork, ChainNamespace } from '@reown/appkit-common' import { OptionsController } from './OptionsController.js' // -- Types --------------------------------------------- // @@ -24,6 +24,9 @@ export interface ConnectExternalOptions { type: Connector['type'] provider?: Connector['provider'] info?: Connector['info'] + chain?: ChainNamespace + chainId?: number | string + caipNetwork?: CaipNetwork } export interface ConnectionControllerClient { @@ -62,6 +65,7 @@ export interface ConnectionControllerState { recentWallet?: WcWallet buffering: boolean status?: 'connecting' | 'connected' | 'disconnected' + connectionControllerClient?: ConnectionControllerClient } type StateKey = keyof ConnectionControllerState @@ -85,8 +89,8 @@ export const ConnectionController = { return subKey(state, key, callback) }, - _getClient(chain?: ChainNamespace) { - return ChainController.getConnectionControllerClient(chain) + _getClient() { + return state._client }, setClient(client: ConnectionControllerClient) { @@ -115,7 +119,7 @@ export const ConnectionController = { return } wcConnectionPromise = new Promise(async (resolve, reject) => { - await ChainController.state?.universalAdapter?.connectionControllerClient + await this._getClient() ?.connectWalletConnect?.(uri => { state.wcUri = uri state.wcPairingExpiry = CoreHelperUtil.getPairingExpiry() @@ -129,27 +133,24 @@ export const ConnectionController = { state.wcPairingExpiry = undefined this.state.status = 'connected' } else { - await ChainController.state?.universalAdapter?.connectionControllerClient?.connectWalletConnect?.( - uri => { - state.wcUri = uri - state.wcPairingExpiry = CoreHelperUtil.getPairingExpiry() - } - ) + await this._getClient()?.connectWalletConnect?.(uri => { + state.wcUri = uri + state.wcPairingExpiry = CoreHelperUtil.getPairingExpiry() + }) } }, async connectExternal(options: ConnectExternalOptions, chain: ChainNamespace, setChain = true) { - await this._getClient(chain).connectExternal?.(options) + await this._getClient()?.connectExternal?.(options) if (setChain) { ChainController.setActiveNamespace(chain) - StorageUtil.setConnectedConnector(options.type) } }, async reconnectExternal(options: ConnectExternalOptions) { - await this._getClient().reconnectExternal?.(options) - StorageUtil.setConnectedConnector(options.type) + await this._getClient()?.reconnectExternal?.(options) + StorageUtil.setConnectedConnector(options.type === 'AUTH' ? 'ID_AUTH' : options.type) }, async setPreferredAccountType(accountType: W3mFrameTypes.AccountType) { @@ -172,47 +173,47 @@ export const ConnectionController = { }, async signMessage(message: string) { - return this._getClient().signMessage(message) + return this._getClient()?.signMessage(message) }, parseUnits(value: string, decimals: number) { - return this._getClient().parseUnits(value, decimals) + return this._getClient()?.parseUnits(value, decimals) }, formatUnits(value: bigint, decimals: number) { - return this._getClient().formatUnits(value, decimals) + return this._getClient()?.formatUnits(value, decimals) }, async sendTransaction(args: SendTransactionArgs) { - return this._getClient().sendTransaction(args) + return this._getClient()?.sendTransaction(args) }, async getCapabilities(params: string) { - return this._getClient().getCapabilities(params) + return this._getClient()?.getCapabilities(params) }, async grantPermissions(params: object | readonly unknown[]) { - return this._getClient().grantPermissions(params) + return this._getClient()?.grantPermissions(params) }, async estimateGas(args: EstimateGasTransactionArgs) { - return this._getClient().estimateGas(args) + return this._getClient()?.estimateGas(args) }, async writeContract(args: WriteContractArgs) { - return this._getClient().writeContract(args) + return this._getClient()?.writeContract(args) }, async getEnsAddress(value: string) { - return this._getClient().getEnsAddress(value) + return this._getClient()?.getEnsAddress(value) }, async getEnsAvatar(value: string) { - return this._getClient().getEnsAvatar(value) + return this._getClient()?.getEnsAvatar(value) }, - checkInstalled(ids?: string[], chain?: ChainNamespace) { - return this._getClient(chain).checkInstalled?.(ids) || false + checkInstalled(ids?: string[]) { + return this._getClient()?.checkInstalled?.(ids) || false }, resetWcConnection() { @@ -287,7 +288,7 @@ export const ConnectionController = { return } - const client = this._getClient(network?.chainNamespace) + const client = this._getClient() try { const sessions = await siwx.getSessions(network.caipNetworkId, address) @@ -297,7 +298,9 @@ export const ConnectionController = { await ModalController.open({ view: - StorageUtil.getConnectedConnector() === 'AUTH' ? 'ApproveTransaction' : 'SIWXSignMessage' + StorageUtil.getConnectedConnector() === 'ID_AUTH' + ? 'ApproveTransaction' + : 'SIWXSignMessage' }) const siwxMessage = await siwx.createMessage({ @@ -307,20 +310,20 @@ export const ConnectionController = { const message = siwxMessage.toString() - const signature = await client.signMessage(message) + const signature = await client?.signMessage(message) await siwx.addSession({ data: siwxMessage, message, - signature + signature: signature as `0x${string}` }) ModalController.close() - } catch (error) { + } catch (error: unknown) { // eslint-disable-next-line no-console console.error('Failed to initialize SIWX', error) ModalController.setLoading(true) - await client.disconnect().finally(() => { + await client?.disconnect().finally(() => { ModalController.setLoading(false) }) } diff --git a/packages/core/src/controllers/ConnectorController.ts b/packages/core/src/controllers/ConnectorController.ts index 6a7b48ffc0..a02d64b114 100644 --- a/packages/core/src/controllers/ConnectorController.ts +++ b/packages/core/src/controllers/ConnectorController.ts @@ -70,7 +70,7 @@ export const ConnectorController = { connectorsByNameMap.forEach(keyConnectors => { const firstItem = keyConnectors[0] - const isAuthConnector = firstItem?.id === 'w3mAuth' + const isAuthConnector = firstItem?.id === 'ID_AUTH' if (keyConnectors.length > 1) { mergedConnectors.push({ @@ -138,7 +138,7 @@ export const ConnectorController = { }, addConnector(connector: Connector | AuthConnector) { - if (connector.id === 'w3mAuth') { + if (connector.id === 'ID_AUTH') { const authConnector = connector as AuthConnector const optionsState = snapshot(OptionsController.state) as typeof OptionsController.state @@ -164,7 +164,7 @@ export const ConnectorController = { getAuthConnector(): AuthConnector | undefined { const activeNamespace = ChainController.state.activeChain - const authConnector = state.connectors.find(c => c.id === 'w3mAuth') + const authConnector = state.connectors.find(c => c.id === 'ID_AUTH') if (!authConnector) { return undefined } @@ -191,7 +191,7 @@ export const ConnectorController = { }, syncIfAuthConnector(connector: Connector | AuthConnector) { - if (connector.id !== 'w3mAuth') { + if (connector.id !== 'ID_AUTH') { return } diff --git a/packages/core/src/controllers/EnsController.ts b/packages/core/src/controllers/EnsController.ts index aa8366e9dc..a52ed8307b 100644 --- a/packages/core/src/controllers/EnsController.ts +++ b/packages/core/src/controllers/EnsController.ts @@ -139,7 +139,7 @@ export const EnsController = { await BlockchainApiController.registerEnsName({ coinType, address: address as `0x${string}`, - signature, + signature: signature as `0x${string}`, message }) diff --git a/packages/core/src/controllers/RouterController.ts b/packages/core/src/controllers/RouterController.ts index f6c7021f4f..7b12a52035 100644 --- a/packages/core/src/controllers/RouterController.ts +++ b/packages/core/src/controllers/RouterController.ts @@ -185,7 +185,7 @@ export const RouterController = { }, goBack() { - if (state.history.length > 1) { + if (state.history.length > 1 && !state.history.includes('UnsupportedChain')) { state.history.pop() const [last] = state.history.slice(-1) if (last) { diff --git a/packages/core/src/controllers/SendController.ts b/packages/core/src/controllers/SendController.ts index 1b26fe5bee..f20a4e5e2c 100644 --- a/packages/core/src/controllers/SendController.ts +++ b/packages/core/src/controllers/SendController.ts @@ -170,12 +170,14 @@ export const SendController = { try { await ConnectionController.sendTransaction({ + chainNamespace: 'eip155', to, address, data, - value, + value: value ?? BigInt(0), gasPrice: params.gasPrice }) + SnackController.showSuccess('Transaction started') EventsController.sendEvent({ type: 'track', @@ -233,10 +235,11 @@ export const SendController = { fromAddress: AccountController.state.address as `0x${string}`, tokenAddress, receiverAddress: params.receiverAddress as `0x${string}`, - tokenAmount: amount, + tokenAmount: amount ?? BigInt(0), method: 'transfer', abi: ContractUtil.getERC20Abi(tokenAddress) }) + SnackController.showSuccess('Transaction started') this.resetSend() } @@ -259,7 +262,7 @@ export const SendController = { ConnectionController.sendTransaction({ chainNamespace: 'solana', - to: this.state.receiverAddress, + to: this.state.receiverAddress as `0x${string}`, value: this.state.sendTokenAmount }) .then(() => { diff --git a/packages/core/src/controllers/SwapController.ts b/packages/core/src/controllers/SwapController.ts index 4b392b96a9..0a52741e10 100644 --- a/packages/core/src/controllers/SwapController.ts +++ b/packages/core/src/controllers/SwapController.ts @@ -454,7 +454,7 @@ export const SwapController = { switch (ChainController.state?.activeCaipNetwork?.chainNamespace) { case 'solana': - state.gasFee = res.standard + state.gasFee = res.standard ?? '0' state.gasPriceInUSD = NumberUtil.multiply(res.standard, state.networkPrice) .dividedBy(1e9) .toNumber() @@ -467,7 +467,7 @@ export const SwapController = { case 'eip155': default: // eslint-disable-next-line no-case-declarations - const value = res.standard + const value = res.standard ?? '0' // eslint-disable-next-line no-case-declarations const gasFee = BigInt(value) // eslint-disable-next-line no-case-declarations @@ -610,11 +610,24 @@ export const SwapController = { value: BigInt(response.tx.value), toAmount: state.toTokenAmount } - state.swapTransaction = undefined - state.approvalTransaction = transaction + state.approvalTransaction = { + data: transaction.data, + to: transaction.to, + gas: transaction.gas ?? BigInt(0), + gasPrice: transaction.gasPrice, + value: transaction.value, + toAmount: transaction.toAmount + } - return transaction + return { + data: transaction.data, + to: transaction.to, + gas: transaction.gas ?? BigInt(0), + gasPrice: transaction.gasPrice, + value: transaction.value, + toAmount: transaction.toAmount + } } catch (error) { RouterController.goBack() SnackController.showError('Failed to create approval transaction') @@ -638,7 +651,7 @@ export const SwapController = { const amount = ConnectionController.parseUnits( sourceTokenAmount, sourceToken.decimals - ).toString() + )?.toString() try { const response = await BlockchainApiController.generateSwapCalldata({ @@ -646,7 +659,7 @@ export const SwapController = { userAddress: fromCaipAddress, from: sourceToken.address, to: toToken.address, - amount + amount: amount as string }) const isSourceTokenIsNetworkToken = sourceToken.address === networkAddress @@ -659,7 +672,7 @@ export const SwapController = { to: CoreHelperUtil.getPlainAddress(response.tx.to) as `0x${string}`, gas, gasPrice, - value: isSourceTokenIsNetworkToken ? BigInt(amount) : BigInt('0'), + value: isSourceTokenIsNetworkToken ? BigInt(amount ?? '0') : BigInt('0'), toAmount: state.toTokenAmount } diff --git a/packages/core/src/utils/StorageUtil.ts b/packages/core/src/utils/StorageUtil.ts index 22c86dfa3f..d392eae3f5 100644 --- a/packages/core/src/utils/StorageUtil.ts +++ b/packages/core/src/utils/StorageUtil.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import { SafeLocalStorage, SafeLocalStorageKeys } from '@reown/appkit-common' +import { SafeLocalStorage, SafeLocalStorageKeys, type ChainNamespace } from '@reown/appkit-common' import type { WcWallet, ConnectorType, SocialProvider } from './TypeUtil.js' import { ChainController } from '../controllers/ChainController.js' @@ -70,6 +70,14 @@ export const StorageUtil = { } }, + setConnectedNamespace(namespace: ChainNamespace) { + try { + SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTED_NAMESPACE, namespace) + } catch { + console.info('Unable to set Connected Namespace') + } + }, + getConnectedConnector() { try { return SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR) as ConnectorType diff --git a/packages/core/src/utils/SwapApiUtil.ts b/packages/core/src/utils/SwapApiUtil.ts index a1adc57ff6..13abb81852 100644 --- a/packages/core/src/utils/SwapApiUtil.ts +++ b/packages/core/src/utils/SwapApiUtil.ts @@ -58,8 +58,8 @@ export const SwapApiUtil = { case 'solana': // eslint-disable-next-line no-case-declarations const lamportsPerSignature = ( - await ConnectionController.estimateGas({ chainNamespace: 'solana' }) - ).toString() + await ConnectionController?.estimateGas({ chainNamespace: 'solana' }) + )?.toString() return { standard: lamportsPerSignature, diff --git a/packages/core/src/utils/TypeUtil.ts b/packages/core/src/utils/TypeUtil.ts index 75b0bc3f86..c08bf3e4cd 100644 --- a/packages/core/src/utils/TypeUtil.ts +++ b/packages/core/src/utils/TypeUtil.ts @@ -16,6 +16,7 @@ import type { AccountControllerState } from '../controllers/AccountController.js import type { OnRampProviderOption } from '../controllers/OnRampController.js' import type { ConstantsUtil } from './ConstantsUtil.js' import type { ReownName } from '../controllers/EnsController.js' +import type UniversalProvider from '@walletconnect/universal-provider' export type CaipNetworkCoinbaseNetwork = | 'Ethereum' @@ -49,6 +50,7 @@ export type ConnectorType = | 'ANNOUNCED' | 'AUTH' | 'MULTI_CHAIN' + | 'ID_AUTH' export type SocialProvider = | 'google' @@ -72,7 +74,7 @@ export type Connector = { icon?: string rdns?: string } - provider?: unknown + provider?: Provider | W3mFrameProvider | UniversalProvider chain: ChainNamespace connectors?: Connector[] } @@ -888,26 +890,18 @@ export type AdapterAccountState = { export type ChainAdapter = { connectionControllerClient?: ConnectionControllerClient networkControllerClient?: NetworkControllerClient - accountState?: AccountControllerState + accountState?: AdapterAccountState networkState?: AdapterNetworkState - defaultNetwork?: CaipNetwork - chainNamespace: ChainNamespace - isUniversalAdapterClient?: boolean - adapterType?: AdapterType - caipNetworks: CaipNetwork[] - getAddress?: () => string | undefined - getError?: () => unknown - getChainId?: () => number | string | undefined - switchNetwork?: ((caipNetwork: CaipNetwork) => void) | undefined - getIsConnected?: () => boolean | undefined - getWalletProvider?: () => unknown - getWalletProviderType?: () => string | undefined - subscribeProvider?: (callback: (newState: unknown) => void) => void + namespace?: ChainNamespace + caipNetworks?: CaipNetwork[] | AppKitNetwork[] + projectId?: string + adapterType?: string } type ProviderEventListener = { connect: (connectParams: { chainId: number }) => void disconnect: (error: Error) => void + display_uri: (uri: string) => void chainChanged: (chainId: string) => void accountsChanged: (accounts: string[]) => void message: (message: { type: string; data: unknown }) => void @@ -919,6 +913,8 @@ export interface RequestArguments { } export interface Provider { + connect: (params?: { onUri?: (uri: string) => void }) => Promise + disconnect: () => Promise request: (args: RequestArguments) => Promise on(event: T, listener: ProviderEventListener[T]): void removeListener: (event: string, listener: (data: T) => void) => void diff --git a/packages/core/tests/controllers/AccountController.test.ts b/packages/core/tests/controllers/AccountController.test.ts index 20729f95c7..4b1b5809b6 100644 --- a/packages/core/tests/controllers/AccountController.test.ts +++ b/packages/core/tests/controllers/AccountController.test.ts @@ -15,7 +15,7 @@ const chain = ConstantsUtil.CHAIN.EVM beforeAll(() => { ChainController.initialize([ { - chainNamespace: ConstantsUtil.CHAIN.EVM, + namespace: ConstantsUtil.CHAIN.EVM, caipNetworks: [] } ]) diff --git a/packages/core/tests/controllers/ApiController.test.ts b/packages/core/tests/controllers/ApiController.test.ts index d294b09a3b..91f3ba1650 100644 --- a/packages/core/tests/controllers/ApiController.test.ts +++ b/packages/core/tests/controllers/ApiController.test.ts @@ -16,7 +16,7 @@ const chain = ConstantsUtil.CHAIN.EVM beforeAll(() => { ChainController.initialize([ { - chainNamespace: ConstantsUtil.CHAIN.EVM, + namespace: ConstantsUtil.CHAIN.EVM, caipNetworks: [] } ]) diff --git a/packages/core/tests/controllers/ChainController.test.ts b/packages/core/tests/controllers/ChainController.test.ts index be296f4378..80109bd25e 100644 --- a/packages/core/tests/controllers/ChainController.test.ts +++ b/packages/core/tests/controllers/ChainController.test.ts @@ -111,7 +111,7 @@ const networkControllerClient: NetworkControllerClient = { } const evmAdapter = { - chainNamespace, + namespace: ConstantsUtil.CHAIN.EVM, connectionControllerClient, networkControllerClient, caipNetworks: [] diff --git a/packages/core/tests/controllers/ConnectionController.test.ts b/packages/core/tests/controllers/ConnectionController.test.ts index 783d2d4e88..8e57289ad2 100644 --- a/packages/core/tests/controllers/ConnectionController.test.ts +++ b/packages/core/tests/controllers/ConnectionController.test.ts @@ -16,7 +16,7 @@ import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' const chain = CommonConstantsUtil.CHAIN.EVM const walletConnectUri = 'wc://uri?=123' const externalId = 'coinbaseWallet' -const type = 'AUTH' as ConnectorType +const type = 'WALLET_CONNECT' as ConnectorType const storageSpy = vi.spyOn(StorageUtil, 'setConnectedConnector') const client: ConnectionControllerClient = { @@ -61,27 +61,22 @@ const partialClient: ConnectionControllerClient = { } const evmAdapter = { - chainNamespace: CommonConstantsUtil.CHAIN.EVM, + namespace: CommonConstantsUtil.CHAIN.EVM, connectionControllerClient: client } const adapters = [evmAdapter] as ChainAdapter[] -const universalAdapter = { - chainNamespace: 'eip155', - connectionControllerClient: client, - caipNetworks: [] -} as ChainAdapter // -- Tests -------------------------------------------------------------------- beforeAll(() => { ChainController.initialize(adapters) - ChainController.initializeUniversalAdapter(universalAdapter, adapters) + ConnectionController.setClient(evmAdapter.connectionControllerClient) }) describe('ConnectionController', () => { it('should have valid default state', () => { ChainController.initialize([ { - chainNamespace: CommonConstantsUtil.CHAIN.EVM, + namespace: CommonConstantsUtil.CHAIN.EVM, connectionControllerClient: client, caipNetworks: [] } @@ -90,7 +85,8 @@ describe('ConnectionController', () => { expect(ConnectionController.state).toEqual({ wcError: false, buffering: false, - status: 'disconnected' + status: 'disconnected', + _client: evmAdapter.connectionControllerClient }) }) @@ -137,7 +133,7 @@ describe('ConnectionController', () => { it('should not throw when optional methods are undefined', async () => { ChainController.initialize([ { - chainNamespace: CommonConstantsUtil.CHAIN.EVM, + namespace: CommonConstantsUtil.CHAIN.EVM, connectionControllerClient: partialClient, caipNetworks: [] } @@ -146,7 +142,7 @@ describe('ConnectionController', () => { ConnectionController.checkInstalled([externalId]) expect(clientCheckInstalledSpy).toHaveBeenCalledWith([externalId]) expect(clientCheckInstalledSpy).toHaveBeenCalledWith(undefined) - expect(ConnectionController._getClient()).toEqual(partialClient) + expect(ConnectionController._getClient()).toEqual(evmAdapter.connectionControllerClient) }) it('should update state correctly on resetWcConnection()', () => { diff --git a/packages/core/tests/controllers/ConnectorController.test.ts b/packages/core/tests/controllers/ConnectorController.test.ts index 9d1e16a03c..8a8333545c 100644 --- a/packages/core/tests/controllers/ConnectorController.test.ts +++ b/packages/core/tests/controllers/ConnectorController.test.ts @@ -3,6 +3,7 @@ import { ChainController, ConnectorController, OptionsController, + type AuthConnector, type Metadata, type SdkVersion, type ThemeMode, @@ -38,14 +39,14 @@ const externalConnector = { name: 'External' } as const const evmAuthConnector = { - id: 'w3mAuth', + id: 'ID_AUTH', type: 'AUTH', provider: authProvider, chain: ConstantsUtil.CHAIN.EVM, name: 'Auth' } as const const solanaAuthConnector = { - id: 'w3mAuth', + id: 'ID_AUTH', type: 'AUTH', provider: authProvider, chain: ConstantsUtil.CHAIN.SOLANA, @@ -161,7 +162,7 @@ describe('ConnectorController', () => { OptionsController.setSdkVersion(mockDappData.sdkVersion) OptionsController.setProjectId(mockDappData.projectId) - ConnectorController.addConnector(evmAuthConnector) + ConnectorController.addConnector(evmAuthConnector as unknown as AuthConnector) expect(ConnectorController.state.connectors).toEqual([ walletConnectConnector, externalConnector, @@ -184,7 +185,7 @@ describe('ConnectorController', () => { }) it('getAuthConnector() should return merged connector when already added on different network', () => { - ConnectorController.addConnector(solanaAuthConnector) + ConnectorController.addConnector(solanaAuthConnector as unknown as AuthConnector) const connector = ConnectorController.getAuthConnector() expect(connector).toEqual(evmAuthConnector) }) @@ -206,7 +207,7 @@ describe('ConnectorController', () => { zerionConnector, // Need to define inline to reference the spies { - id: 'w3mAuth', + id: 'ID_AUTH', imageId: undefined, imageUrl: undefined, name: 'Auth', @@ -215,7 +216,7 @@ describe('ConnectorController', () => { connectors: [ { chain: 'eip155', - id: 'w3mAuth', + id: 'ID_AUTH', name: 'Auth', provider: { syncDappData: syncDappDataSpy, @@ -225,7 +226,7 @@ describe('ConnectorController', () => { }, { chain: 'solana', - id: 'w3mAuth', + id: 'ID_AUTH', name: 'Auth', provider: { syncDappData: syncDappDataSpy, @@ -251,7 +252,7 @@ describe('ConnectorController', () => { } const mergedAuthConnector = { - id: 'w3mAuth', + id: 'ID_AUTH', imageId: undefined, imageUrl: undefined, name: 'Auth', @@ -260,7 +261,7 @@ describe('ConnectorController', () => { connectors: [ { chain: 'eip155', - id: 'w3mAuth', + id: 'ID_AUTH', name: 'Auth', provider: { syncDappData: syncDappDataSpy, @@ -270,7 +271,7 @@ describe('ConnectorController', () => { }, { chain: 'solana', - id: 'w3mAuth', + id: 'ID_AUTH', name: 'Auth', provider: { syncDappData: syncDappDataSpy, diff --git a/packages/core/tests/controllers/EnsController.test.ts b/packages/core/tests/controllers/EnsController.test.ts index 8a307ae083..9c296cf7e6 100644 --- a/packages/core/tests/controllers/EnsController.test.ts +++ b/packages/core/tests/controllers/EnsController.test.ts @@ -72,7 +72,7 @@ vi.mock('../../src/controllers/BlockchainApiController.js', async importOriginal beforeAll(() => { ChainController.initialize([ { - chainNamespace: ConstantsUtil.CHAIN.EVM, + namespace: ConstantsUtil.CHAIN.EVM, caipNetworks: [] } ]) @@ -188,7 +188,7 @@ describe('EnsController', () => { const getAuthConnectorSpy = vi.spyOn(ConnectorController, 'getAuthConnector').mockReturnValue({ provider: { getEmail: () => 'test@walletconnect.com' } as unknown as W3mFrameProvider, - id: 'w3mAuth', + id: 'ID_AUTH', type: 'AUTH', chain: ConstantsUtil.CHAIN.EVM }) diff --git a/packages/core/tests/controllers/SwapController.test.ts b/packages/core/tests/controllers/SwapController.test.ts index dae47c23e3..6eb01a3a1f 100644 --- a/packages/core/tests/controllers/SwapController.test.ts +++ b/packages/core/tests/controllers/SwapController.test.ts @@ -56,7 +56,7 @@ beforeAll(async () => { // -- Set Account and ChainController.initialize([ { - chainNamespace: ConstantsUtil.CHAIN.EVM, + namespace: ConstantsUtil.CHAIN.EVM, networkControllerClient: client, caipNetworks: [caipNetwork] } diff --git a/packages/core/tests/utils/StorageUtil.test.ts b/packages/core/tests/utils/StorageUtil.test.ts index 9686f0de4c..c9bedc0322 100644 --- a/packages/core/tests/utils/StorageUtil.test.ts +++ b/packages/core/tests/utils/StorageUtil.test.ts @@ -64,7 +64,7 @@ describe('StorageUtil', () => { it('should get WalletConnect deep link from localStorage', () => { const deepLink = { href: 'https://example.com', name: 'Example Wallet' } SafeLocalStorage.setItem( - 'WALLETCONNECT_DEEPLINK_CHOICE', + SafeLocalStorageKeys.DEEPLINK_CHOICE, JSON.stringify({ href: deepLink.href, name: deepLink.name }) ) expect(StorageUtil.getWalletConnectDeepLink()).toEqual({ @@ -91,11 +91,11 @@ describe('StorageUtil', () => { describe('deleteWalletConnectDeepLink', () => { it('should delete WalletConnect deep link from localStorage', () => { SafeLocalStorage.setItem( - 'WALLETCONNECT_DEEPLINK_CHOICE', + SafeLocalStorageKeys.DEEPLINK_CHOICE, JSON.stringify({ href: 'https://example.com', name: 'Example' }) ) StorageUtil.deleteWalletConnectDeepLink() - expect(SafeLocalStorage.getItem('WALLETCONNECT_DEEPLINK_CHOICE')).toBeUndefined() + expect(SafeLocalStorage.getItem(SafeLocalStorageKeys.DEEPLINK_CHOICE)).toBeUndefined() }) it('should handle errors when deleting deep link', () => { diff --git a/packages/experimental/src/smart-session/controllers/SmartSessionsController.ts b/packages/experimental/src/smart-session/controllers/SmartSessionsController.ts index 5e5a7fb4ff..3cc4071979 100644 --- a/packages/experimental/src/smart-session/controllers/SmartSessionsController.ts +++ b/packages/experimental/src/smart-session/controllers/SmartSessionsController.ts @@ -10,7 +10,6 @@ import { SnackController } from '@reown/appkit-core' import { ERROR_MESSAGES } from '../schema/index.js' -import { ConstantsUtil } from '@reown/appkit-common' import { CosignerService } from '../utils/CosignerService.js' import { ProviderUtil } from '@reown/appkit/store' @@ -93,12 +92,10 @@ export const SmartSessionsController = { throw new Error(ERROR_MESSAGES.INVALID_ADDRESS) } // Fetch the ConnectionController client - const connectionControllerClient = ConnectionController._getClient( - CommonConstantsUtil.CHAIN.EVM - ) + const connectionControllerClient = ConnectionController._getClient() //Check for connected wallet supports permissions capabilities - const walletCapabilities = (await connectionControllerClient.getCapabilities( + const walletCapabilities = (await connectionControllerClient?.getCapabilities( chainAndAddress.address )) as WalletCapabilities @@ -123,7 +120,7 @@ export const SmartSessionsController = { goBack: false }) - const rawResponse = await connectionControllerClient.grantPermissions([request]) + const rawResponse = await connectionControllerClient?.grantPermissions([request]) // Validate and type guard the response const response = assertWalletGrantPermissionsResponse(rawResponse) @@ -202,7 +199,7 @@ export const SmartSessionsController = { throw new Error(ERROR_MESSAGES.INVALID_ADDRESS) } // Fetch the ConnectionController client - const connectionControllerClient = ConnectionController._getClient(ConstantsUtil.CHAIN.EVM) + const connectionControllerClient = ConnectionController._getClient() // Retrieve state values const { projectId } = OptionsController.state @@ -215,7 +212,7 @@ export const SmartSessionsController = { goBack: false }) - const signature = await connectionControllerClient.revokePermissions({ + const signature = await connectionControllerClient?.revokePermissions({ pci: session.pci, permissions: [...session.permissions.map(p => JSON.parse(JSON.stringify(p)))], expiry: Math.floor(session.expiry / 1000), @@ -223,7 +220,11 @@ export const SmartSessionsController = { }) // Activate the permissions using CosignerService - await cosignerService.revokePermissions(activeCaipAddress, session.pci, signature) + await cosignerService.revokePermissions( + activeCaipAddress, + session.pci, + signature as `0x${string}` + ) state.sessions = state.sessions.filter(s => s.pci !== session.pci) } catch (e) { SnackController.showError('Error revoking smart session') diff --git a/packages/experimental/src/smart-session/grantPermissions.ts b/packages/experimental/src/smart-session/grantPermissions.ts index 1292dda75f..170f4568f0 100644 --- a/packages/experimental/src/smart-session/grantPermissions.ts +++ b/packages/experimental/src/smart-session/grantPermissions.ts @@ -55,10 +55,10 @@ export async function grantPermissions( throw new Error(ERROR_MESSAGES.INVALID_ADDRESS) } // Fetch the ConnectionController client - const connectionControllerClient = ConnectionController._getClient(CommonConstantsUtil.CHAIN.EVM) + const connectionControllerClient = ConnectionController._getClient() //Check for connected wallet supports permissions capabilities - const walletCapabilities = (await connectionControllerClient.getCapabilities( + const walletCapabilities = (await connectionControllerClient?.getCapabilities( chainAndAddress.address )) as WalletCapabilities @@ -83,7 +83,7 @@ export async function grantPermissions( goBack: false }) - const rawResponse = await connectionControllerClient.grantPermissions([request]) + const rawResponse = await connectionControllerClient?.grantPermissions([request]) // Validate and type guard the response const response = assertWalletGrantPermissionsResponse(rawResponse) diff --git a/packages/scaffold-ui/src/modal/w3m-account-button/index.ts b/packages/scaffold-ui/src/modal/w3m-account-button/index.ts index 67dd7fdc41..b08e0704d8 100644 --- a/packages/scaffold-ui/src/modal/w3m-account-button/index.ts +++ b/packages/scaffold-ui/src/modal/w3m-account-button/index.ts @@ -49,7 +49,9 @@ class W3mAccountButtonBase extends LitElement { AssetController.subscribeNetworkImages(() => { this.networkImage = AssetUtil.getNetworkImage(this.network) }), - ChainController.subscribeKey('activeCaipAddress', val => (this.caipAddress = val)), + ChainController.subscribeKey('activeCaipAddress', val => { + this.caipAddress = val + }), AccountController.subscribeKey('balance', val => (this.balanceVal = val)), AccountController.subscribeKey('balanceSymbol', val => (this.balanceSymbol = val)), AccountController.subscribeKey('profileName', val => (this.profileName = val)), diff --git a/packages/scaffold-ui/src/modal/w3m-modal/index.ts b/packages/scaffold-ui/src/modal/w3m-modal/index.ts index d5f6d3604b..3b39e17f79 100644 --- a/packages/scaffold-ui/src/modal/w3m-modal/index.ts +++ b/packages/scaffold-ui/src/modal/w3m-modal/index.ts @@ -100,6 +100,7 @@ export class W3mModal extends LitElement { private async handleClose() { const isSiweSignScreen = RouterController.state.view === 'ConnectingSiwe' const isApproveSignScreen = RouterController.state.view === 'ApproveTransaction' + const isUnsupportedChain = RouterController.state.view === 'UnsupportedChain' if (this.isSiweEnabled) { const { SIWEController } = await import('@reown/appkit-siwe') @@ -109,6 +110,8 @@ export class W3mModal extends LitElement { } else { ModalController.close() } + } else if (isUnsupportedChain) { + ModalController.shake() } else { ModalController.close() } diff --git a/packages/scaffold-ui/src/partials/w3m-account-auth-button/index.ts b/packages/scaffold-ui/src/partials/w3m-account-auth-button/index.ts index 53efa0ba20..8192090a5a 100644 --- a/packages/scaffold-ui/src/partials/w3m-account-auth-button/index.ts +++ b/packages/scaffold-ui/src/partials/w3m-account-auth-button/index.ts @@ -20,7 +20,7 @@ export class W3mAccountAuthButton extends LitElement { const type = StorageUtil.getConnectedConnector() const authConnector = ConnectorController.getAuthConnector() - if (!authConnector || type !== 'AUTH') { + if (!authConnector || type !== 'ID_AUTH') { this.style.cssText = `display: none` return null diff --git a/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts b/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts index b17f8e02a0..8f649c4893 100644 --- a/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts +++ b/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts @@ -75,7 +75,7 @@ export class W3mAccountDefaultWidget extends LitElement { // -- Render -------------------------------------------- // public override render() { if (!this.caipAddress) { - throw new Error('w3m-account-view: No account provided') + return null } const shouldShowMultiAccount = @@ -174,7 +174,7 @@ export class W3mAccountDefaultWidget extends LitElement { const type = StorageUtil.getConnectedConnector() const authConnector = ConnectorController.getAuthConnector() const { origin } = location - if (!authConnector || type !== 'AUTH' || origin.includes(CommonConstantsUtil.SECURE_SITE)) { + if (!authConnector || type !== 'ID_AUTH' || origin.includes(CommonConstantsUtil.SECURE_SITE)) { return null } diff --git a/packages/scaffold-ui/src/partials/w3m-connect-external-widget/index.ts b/packages/scaffold-ui/src/partials/w3m-connect-external-widget/index.ts index 7ec2d75b24..dd262cf97b 100644 --- a/packages/scaffold-ui/src/partials/w3m-connect-external-widget/index.ts +++ b/packages/scaffold-ui/src/partials/w3m-connect-external-widget/index.ts @@ -29,11 +29,8 @@ export class W3mConnectExternalWidget extends LitElement { // -- Render -------------------------------------------- // public override render() { const externalConnectors = this.connectors.filter(connector => connector.type === 'EXTERNAL') - const filteredOutCoinbaseConnectors = externalConnectors.filter( - connector => connector.id !== 'coinbaseWalletSDK' - ) - if (!filteredOutCoinbaseConnectors?.length) { + if (!externalConnectors?.length) { this.style.cssText = `display: none` return null @@ -41,7 +38,7 @@ export class W3mConnectExternalWidget extends LitElement { return html` - ${filteredOutCoinbaseConnectors.map( + ${externalConnectors.map( connector => html` connector.type === 'ANNOUNCED') const injected = this.connectors.filter(connector => connector.type === 'INJECTED') const external = this.connectors.filter(connector => connector.type === 'EXTERNAL') - const isEVM = ChainController.state.activeChain === CommonConstantsUtil.CHAIN.EVM - const includeAnnouncedAndInjected = isEVM ? OptionsController.state.enableEIP6963 : true return { custom, recent, external, multiChain, - announced: includeAnnouncedAndInjected ? announced : [], - injected: includeAnnouncedAndInjected ? injected : [], + announced, + injected, recommended: filteredRecommended, featured: filteredFeatured } diff --git a/packages/scaffold-ui/src/partials/w3m-header/index.ts b/packages/scaffold-ui/src/partials/w3m-header/index.ts index 05c3c28bd9..9e5d5c5558 100644 --- a/packages/scaffold-ui/src/partials/w3m-header/index.ts +++ b/packages/scaffold-ui/src/partials/w3m-header/index.ts @@ -153,6 +153,8 @@ export class W3mHeader extends LitElement { } private async onClose() { + const isUnsupportedChain = RouterController.state.view === 'UnsupportedChain' + if (this.isSiweEnabled) { const { SIWEController } = await import('@reown/appkit-siwe') const isApproveSignScreen = RouterController.state.view === 'ApproveTransaction' @@ -163,6 +165,8 @@ export class W3mHeader extends LitElement { } else { ModalController.close() } + } else if (isUnsupportedChain) { + ModalController.shake() } else { ModalController.close() } diff --git a/packages/scaffold-ui/src/views/w3m-account-settings-view/index.ts b/packages/scaffold-ui/src/views/w3m-account-settings-view/index.ts index b8f3f32a27..ef08edcdfb 100644 --- a/packages/scaffold-ui/src/views/w3m-account-settings-view/index.ts +++ b/packages/scaffold-ui/src/views/w3m-account-settings-view/index.ts @@ -155,7 +155,7 @@ export class W3mAccountSettingsView extends LitElement { const type = StorageUtil.getConnectedConnector() const authConnector = ConnectorController.getAuthConnector() const hasNetworkSupport = ChainController.checkIfNamesSupported() - if (!hasNetworkSupport || !authConnector || type !== 'AUTH' || this.profileName) { + if (!hasNetworkSupport || !authConnector || type !== 'ID_AUTH' || this.profileName) { return null } @@ -178,7 +178,7 @@ export class W3mAccountSettingsView extends LitElement { const type = StorageUtil.getConnectedConnector() const authConnector = ConnectorController.getAuthConnector() const { origin } = location - if (!authConnector || type !== 'AUTH' || origin.includes(ConstantsUtil.SECURE_SITE)) { + if (!authConnector || type !== 'ID_AUTH' || origin.includes(ConstantsUtil.SECURE_SITE)) { return null } @@ -217,7 +217,7 @@ export class W3mAccountSettingsView extends LitElement { const type = StorageUtil.getConnectedConnector() const authConnector = ConnectorController.getAuthConnector() - if (!authConnector || type !== 'AUTH' || !networkEnabled) { + if (!authConnector || type !== 'ID_AUTH' || !networkEnabled) { return null } @@ -250,6 +250,7 @@ export class W3mAccountSettingsView extends LitElement { private async changePreferredAccountType() { const smartAccountEnabled = ChainController.checkIfSmartAccountEnabled() + const accountTypeTarget = this.preferredAccountType === W3mFrameRpcConstants.ACCOUNT_TYPES.SMART_ACCOUNT || !smartAccountEnabled diff --git a/packages/scaffold-ui/src/views/w3m-account-view/index.ts b/packages/scaffold-ui/src/views/w3m-account-view/index.ts index 4b56b05800..c87a2bf37d 100644 --- a/packages/scaffold-ui/src/views/w3m-account-view/index.ts +++ b/packages/scaffold-ui/src/views/w3m-account-view/index.ts @@ -11,7 +11,7 @@ export class W3mAccountView extends LitElement { const authConnector = ConnectorController.getAuthConnector() return html` - ${authConnector && connectedConnectorType === 'AUTH' + ${authConnector && connectedConnectorType === 'ID_AUTH' ? this.walletFeaturesTemplate() : this.defaultTemplate()} ` diff --git a/packages/scaffold-ui/src/views/w3m-network-switch-view/index.ts b/packages/scaffold-ui/src/views/w3m-network-switch-view/index.ts index a1360b25ea..ffe6c414b3 100644 --- a/packages/scaffold-ui/src/views/w3m-network-switch-view/index.ts +++ b/packages/scaffold-ui/src/views/w3m-network-switch-view/index.ts @@ -133,7 +133,7 @@ export class W3mNetworkSwitchView extends LitElement { if (this.network) { await ChainController.switchActiveNetwork(this.network) } - } catch { + } catch (error) { this.error = true } } diff --git a/packages/scaffold-ui/src/views/w3m-networks-view/index.ts b/packages/scaffold-ui/src/views/w3m-networks-view/index.ts index 92056b3f7e..b0bf928d44 100644 --- a/packages/scaffold-ui/src/views/w3m-networks-view/index.ts +++ b/packages/scaffold-ui/src/views/w3m-networks-view/index.ts @@ -100,6 +100,7 @@ export class W3mNetworksView extends LitElement { private networksTemplate() { const requestedCaipNetworks = ChainController.getAllRequestedCaipNetworks() const approvedCaipNetworkIds = ChainController.getAllApprovedCaipNetworkIds() + const sortedNetworks = CoreHelperUtil.sortRequestedNetworks( approvedCaipNetworkIds, requestedCaipNetworks @@ -136,7 +137,7 @@ export class W3mNetworksView extends LitElement { ChainController.getNetworkProp('supportsAllNetworks', networkNamespace) !== false const type = StorageUtil.getConnectedConnector() const authConnector = ConnectorController.getAuthConnector() - const isConnectedWithAuth = type === 'AUTH' && authConnector + const isConnectedWithAuth = type === 'ID_AUTH' && authConnector if (!isNamespaceConnected || supportsAllNetworks || isConnectedWithAuth) { return false @@ -159,7 +160,7 @@ export class W3mNetworksView extends LitElement { network.chainNamespace ) const isCurrentNetworkConnected = AccountController.state.caipAddress - const isAuthConnected = StorageUtil.getConnectedConnector() === 'AUTH' + const isAuthConnected = StorageUtil.getConnectedConnector() === 'ID_AUTH' if ( isDifferentNamespace && diff --git a/packages/siwe/src/client.ts b/packages/siwe/src/client.ts index 0f028a946e..7f4d4edfc6 100644 --- a/packages/siwe/src/client.ts +++ b/packages/siwe/src/client.ts @@ -136,7 +136,7 @@ export class AppKitSIWEClient { }) const type = StorageUtil.getConnectedConnector() - if (type === 'AUTH') { + if (type === 'ID_AUTH') { RouterController.pushTransactionStack({ view: null, goBack: false, @@ -149,7 +149,7 @@ export class AppKitSIWEClient { const signature = await ConnectionController.signMessage(message) - const isValid = await this.methods.verifyMessage({ message, signature }) + const isValid = await this.methods.verifyMessage({ message, signature: signature as string }) if (!isValid) { throw new Error('Error verifying SIWE signature') } diff --git a/packages/ui/src/composites/wui-list-account/index.ts b/packages/ui/src/composites/wui-list-account/index.ts index 8d359d6873..9f36b8ece0 100644 --- a/packages/ui/src/composites/wui-list-account/index.ts +++ b/packages/ui/src/composites/wui-list-account/index.ts @@ -68,7 +68,7 @@ export class WuiListAccount extends LitElement { const label = this.getLabel() // Only show icon for AUTH accounts - this.shouldShowIcon = this.connectedConnector === 'AUTH' + this.shouldShowIcon = this.connectedConnector === 'ID_AUTH' return html` ({ type: W3mFrameConstants.APP_GET_SMART_ACCOUNT_ENABLED_NETWORKS } as W3mFrameTypes.AppEvent) + this.persistSmartAccountEnabledNetworks(response.smartAccountEnabledNetworks) return response