diff --git a/.changeset/lazy-windows-behave.md b/.changeset/lazy-windows-behave.md new file mode 100644 index 0000000000..eff014c3f7 --- /dev/null +++ b/.changeset/lazy-windows-behave.md @@ -0,0 +1,23 @@ +--- +'@reown/appkit-adapter-ethers5': patch +'@reown/appkit-adapter-ethers': patch +'@apps/demo': patch +'@apps/gallery': patch +'@apps/laboratory': patch +'@reown/appkit-adapter-polkadot': patch +'@reown/appkit-adapter-solana': patch +'@reown/appkit-adapter-wagmi': patch +'@reown/appkit': patch +'@reown/appkit-utils': patch +'@reown/appkit-cdn': patch +'@reown/appkit-common': patch +'@reown/appkit-core': patch +'@reown/appkit-experimental': patch +'@reown/appkit-polyfills': patch +'@reown/appkit-scaffold-ui': patch +'@reown/appkit-siwe': patch +'@reown/appkit-ui': patch +'@reown/appkit-wallet': patch +--- + +Fixes an issue where ethers and ethers5 adapters were causing infinite network requests diff --git a/packages/adapters/ethers/src/client.ts b/packages/adapters/ethers/src/client.ts index 365892f74e..244cec64f7 100644 --- a/packages/adapters/ethers/src/client.ts +++ b/packages/adapters/ethers/src/client.ts @@ -631,9 +631,12 @@ export class EthersAdapter { if (provider) { const { addresses, chainId } = await EthersHelpersUtil.getUserInfo(provider) const firstAddress = addresses?.[0] - const caipAddress = `${this.chainNamespace}:${chainId}:${firstAddress}` as CaipAddress + const caipNetwork = this.caipNetworks.find(c => c.id === chainId) ?? this.caipNetworks[0] + const caipAddress = + `${this.chainNamespace}:${caipNetwork?.id}:${firstAddress}` as CaipAddress - if (firstAddress && chainId) { + if (firstAddress && caipNetwork) { + this.appKit?.setCaipNetwork(caipNetwork) this.appKit?.setCaipAddress(caipAddress, this.chainNamespace) ProviderUtil.setProviderId('eip155', providerId) ProviderUtil.setProvider('eip155', provider) diff --git a/packages/adapters/ethers/src/tests/client.test.ts b/packages/adapters/ethers/src/tests/client.test.ts index 2c77249355..1e5b0f043c 100644 --- a/packages/adapters/ethers/src/tests/client.test.ts +++ b/packages/adapters/ethers/src/tests/client.test.ts @@ -12,7 +12,8 @@ import { mainnet as AppkitMainnet, polygon as AppkitPolygon, optimism as AppkitOptimism, - bsc as AppkitBsc + 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' @@ -107,17 +108,40 @@ vi.mock('ethers', async () => { } }) +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() + } + } +}) + describe('EthersAdapter', () => { let client: 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: mockCreateEthersConfig() + ethersConfig } client.construct(mockAppKit, optionsWithEthersConfig) }) @@ -650,17 +674,23 @@ describe('EthersAdapter', () => { 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 - vi.spyOn(mockAppKit, 'getIsConnectedState').mockReturnValue(true) vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(mockCaipNetwork) - vi.spyOn(mockAppKit, 'getPreferredAccountType').mockReturnValue('eoa') + vi.spyOn(EthersHelpersUtil, 'getUserInfo').mockResolvedValue({ + addresses: ['0x1234567890123456789012345678901234567890'], + chainId: 1 + }) await client['syncAccount']({ address: mockAddress }) @@ -671,9 +701,39 @@ describe('EthersAdapter', () => { ) expect(client['syncConnectedWalletInfo']).toHaveBeenCalled() expect(client['syncProfile']).toHaveBeenCalledWith(mockAddress) + expect(client['setupProviderListeners']).toHaveBeenCalledOnce() + expect(client['setProvider']).toHaveBeenCalledOnce() + 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 + }) + + await client['syncAccount']({ address: mockAddress }) + + expect(client['setupProviderListeners']).toHaveBeenCalledOnce() + expect(client['setProvider']).toHaveBeenCalledOnce() + 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) diff --git a/packages/adapters/ethers5/src/client.ts b/packages/adapters/ethers5/src/client.ts index 3c41595fdd..3187cc3b89 100644 --- a/packages/adapters/ethers5/src/client.ts +++ b/packages/adapters/ethers5/src/client.ts @@ -607,9 +607,12 @@ export class Ethers5Adapter { if (provider) { const { addresses, chainId } = await EthersHelpersUtil.getUserInfo(provider) const firstAddress = addresses?.[0] - const caipAddress = `${this.chainNamespace}:${chainId}:${firstAddress}` as CaipAddress + const caipNetwork = this.caipNetworks.find(c => c.id === chainId) ?? this.caipNetworks[0] + const caipAddress = + `${this.chainNamespace}:${caipNetwork?.id}:${firstAddress}` as CaipAddress - if (firstAddress && chainId) { + if (firstAddress && caipNetwork) { + this.appKit?.setCaipNetwork(caipNetwork) this.appKit?.setCaipAddress(caipAddress, this.chainNamespace) ProviderUtil.setProviderId('eip155', providerId) ProviderUtil.setProvider('eip155', provider) diff --git a/packages/adapters/ethers5/src/tests/client.test.ts b/packages/adapters/ethers5/src/tests/client.test.ts index 5609c6a93c..6a9d36226f 100644 --- a/packages/adapters/ethers5/src/tests/client.test.ts +++ b/packages/adapters/ethers5/src/tests/client.test.ts @@ -12,7 +12,8 @@ import { mainnet as AppkitMainnet, polygon as AppkitPolygon, optimism as AppkitOptimism, - bsc as AppkitBsc + 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' @@ -113,17 +114,40 @@ vi.mock('ethers', async () => { } }) +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() + } + } +}) + describe('EthersAdapter', () => { let client: 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: mockCreateEthersConfig() + ethersConfig } client.construct(mockAppKit, optionsWithEthersConfig) }) @@ -656,17 +680,23 @@ describe('EthersAdapter', () => { 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 - vi.spyOn(mockAppKit, 'getIsConnectedState').mockReturnValue(true) vi.spyOn(mockAppKit, 'getCaipNetwork').mockReturnValue(mockCaipNetwork) - vi.spyOn(mockAppKit, 'getPreferredAccountType').mockReturnValue('eoa') + vi.spyOn(EthersHelpersUtil, 'getUserInfo').mockResolvedValue({ + addresses: ['0x1234567890123456789012345678901234567890'], + chainId: 1 + }) await client['syncAccount']({ address: mockAddress }) @@ -677,9 +707,39 @@ describe('EthersAdapter', () => { ) expect(client['syncConnectedWalletInfo']).toHaveBeenCalled() expect(client['syncProfile']).toHaveBeenCalledWith(mockAddress) + expect(client['setupProviderListeners']).toHaveBeenCalledOnce() + expect(client['setProvider']).toHaveBeenCalledOnce() + 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 + }) + + await client['syncAccount']({ address: mockAddress }) + + expect(client['setupProviderListeners']).toHaveBeenCalledOnce() + expect(client['setProvider']).toHaveBeenCalledOnce() + 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)