diff --git a/.changeset/fix-ethers-account-switch-provider.md b/.changeset/fix-ethers-account-switch-provider.md new file mode 100644 index 0000000000..a63271a235 --- /dev/null +++ b/.changeset/fix-ethers-account-switch-provider.md @@ -0,0 +1,12 @@ +--- +"@reown/appkit-adapter-ethers": patch +"@reown/appkit-adapter-ethers5": patch +--- + +fix(ethers,ethers5): resolve walletProvider after account switch in modal + +`useAppKitProvider` returned a stale provider when switching accounts inside the +modal. In the early-return path of `connect()`, `connector.provider` was never +initialised, causing the base-client's `accountChanged` handler to skip +`syncProvider()`. The provider is now resolved from `ethersProviders` before the +event is emitted. diff --git a/packages/adapters/ethers/src/client.ts b/packages/adapters/ethers/src/client.ts index cad8f580bb..c3c763012c 100644 --- a/packages/adapters/ethers/src/client.ts +++ b/packages/adapters/ethers/src/client.ts @@ -459,6 +459,20 @@ export class EthersAdapter extends AdapterBlueprint { } if (connection.account) { + /* + * Resolve the provider before emitting so the base-client's accountChanged + * handler can call syncProvider() — keeping useAppKitProvider reactive + * when the user switches accounts inside the modal. + */ + if (!connector.provider) { + const ethersProvider = + this.ethersProviders[connector.id as keyof Omit] + if (ethersProvider) { + await ethersProvider.initialize() + connector.provider = (await ethersProvider.getProvider()) as Provider | undefined + } + } + this.emit('accountChanged', { address: this.toChecksummedAddress(connection.account.address), chainId: caipNetwork.id, diff --git a/packages/adapters/ethers/src/tests/client.test.ts b/packages/adapters/ethers/src/tests/client.test.ts index 0890bd410c..50a5638336 100644 --- a/packages/adapters/ethers/src/tests/client.test.ts +++ b/packages/adapters/ethers/src/tests/client.test.ts @@ -394,6 +394,99 @@ describe('EthersAdapter', () => { preferredAccountType: 'smartAccount' }) }) + + it('should resolve provider from ethersProviders when connector has no provider in early-return path', async () => { + const resolvedProvider = { + request: vi.fn(), + on: vi.fn(), + removeListener: vi.fn() + } as unknown as Provider + + const mockEthersProvider = { + initialize: vi.fn().mockResolvedValue(undefined), + getProvider: vi.fn().mockResolvedValue(resolvedProvider) + } + + const connector = { id: 'injected', provider: undefined, type: 'EXTERNAL', chain: 'eip155' } + + Object.defineProperty(adapter, 'connectors', { + value: [connector], + configurable: true, + writable: true + }) + + adapter['ethersProviders'] = { injected: mockEthersProvider as any } + + vi.spyOn(adapter as any, 'getConnection').mockReturnValue({ + connectorId: 'injected', + caipNetwork: mainnet, + account: { address: '0x1234567890123456789012345678901234567890' }, + accounts: [{ address: '0x1234567890123456789012345678901234567890' }] + }) + + const accountChangedSpy = vi.fn() + adapter.on('accountChanged', accountChangedSpy) + + const result = await adapter.connect({ id: 'injected', type: 'EXTERNAL', chainId: 1 }) + + expect(mockEthersProvider.initialize).toHaveBeenCalled() + expect(mockEthersProvider.getProvider).toHaveBeenCalled() + expect(accountChangedSpy).toHaveBeenCalledWith( + expect.objectContaining({ + connector: expect.objectContaining({ provider: resolvedProvider }) + }) + ) + expect(result.provider).toBe(resolvedProvider) + }) + + it('should not re-initialize provider when connector already has one in early-return path', async () => { + const existingProvider = { + request: vi.fn(), + on: vi.fn(), + removeListener: vi.fn() + } as unknown as Provider + + const mockEthersProvider = { + initialize: vi.fn().mockResolvedValue(undefined), + getProvider: vi.fn() + } + + const connector = { + id: 'injected', + provider: existingProvider, + type: 'EXTERNAL', + chain: 'eip155' + } + + Object.defineProperty(adapter, 'connectors', { + value: [connector], + configurable: true, + writable: true + }) + + adapter['ethersProviders'] = { injected: mockEthersProvider as any } + + vi.spyOn(adapter as any, 'getConnection').mockReturnValue({ + connectorId: 'injected', + caipNetwork: mainnet, + account: { address: '0x1234567890123456789012345678901234567890' }, + accounts: [{ address: '0x1234567890123456789012345678901234567890' }] + }) + + const accountChangedSpy = vi.fn() + adapter.on('accountChanged', accountChangedSpy) + + const result = await adapter.connect({ id: 'injected', type: 'EXTERNAL', chainId: 1 }) + + expect(mockEthersProvider.initialize).not.toHaveBeenCalled() + expect(mockEthersProvider.getProvider).not.toHaveBeenCalled() + expect(accountChangedSpy).toHaveBeenCalledWith( + expect.objectContaining({ + connector: expect.objectContaining({ provider: existingProvider }) + }) + ) + expect(result.provider).toBe(existingProvider) + }) }) describe('EthersAdapter -reconnect', () => { diff --git a/packages/adapters/ethers5/src/client.ts b/packages/adapters/ethers5/src/client.ts index bdcb1f17db..4bbef4067d 100644 --- a/packages/adapters/ethers5/src/client.ts +++ b/packages/adapters/ethers5/src/client.ts @@ -454,6 +454,20 @@ export class Ethers5Adapter extends AdapterBlueprint { } if (connection.account) { + /* + * Resolve the provider before emitting so the base-client's accountChanged + * handler can call syncProvider() — keeping useAppKitProvider reactive + * when the user switches accounts inside the modal. + */ + if (!connector.provider) { + const ethersProvider = + this.ethersProviders[connector.id as keyof Omit] + if (ethersProvider) { + await ethersProvider.initialize() + connector.provider = (await ethersProvider.getProvider()) as Provider | undefined + } + } + this.emit('accountChanged', { address: this.toChecksummedAddress(connection.account.address), chainId: caipNetwork.id,