diff --git a/.changeset/hot-areas-crash.md b/.changeset/hot-areas-crash.md new file mode 100644 index 0000000000..13afc710a6 --- /dev/null +++ b/.changeset/hot-areas-crash.md @@ -0,0 +1,37 @@ +--- +'@reown/appkit-adapter-ethers5': patch +'@reown/appkit-adapter-ethers': patch +'pay-test-exchange': patch +'@reown/appkit-adapter-bitcoin': patch +'@reown/appkit-adapter-solana': patch +'@reown/appkit-adapter-ton': patch +'@reown/appkit-adapter-tron': patch +'@reown/appkit-adapter-wagmi': patch +'@reown/appkit': patch +'@reown/appkit-utils': patch +'@reown/appkit-cdn': patch +'@reown/appkit-cli': patch +'@reown/appkit-codemod': patch +'@reown/appkit-common': patch +'@reown/appkit-controllers': patch +'@reown/appkit-core': patch +'@reown/appkit-experimental': patch +'@reown/appkit-pay': patch +'@reown/appkit-polyfills': patch +'@reown/appkit-scaffold-ui': patch +'@reown/appkit-siwe': patch +'@reown/appkit-siwx': patch +'@reown/appkit-testing': patch +'@reown/appkit-ui': patch +'@reown/appkit-universal-connector': patch +'@reown/appkit-wallet': patch +'@reown/appkit-wallet-button': 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. \ No newline at end of file diff --git a/packages/adapters/ethers/src/client.ts b/packages/adapters/ethers/src/client.ts index cad8f580bb..54413ac8c7 100644 --- a/packages/adapters/ethers/src/client.ts +++ b/packages/adapters/ethers/src/client.ts @@ -459,6 +459,18 @@ 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. + */ + const ethersProvider = + this.ethersProviders[connector.id as keyof Omit] + if (!connector.provider && 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..0c6886450f 100644 --- a/packages/adapters/ethers5/src/client.ts +++ b/packages/adapters/ethers5/src/client.ts @@ -454,6 +454,18 @@ 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. + */ + const ethersProvider = + this.ethersProviders[connector.id as keyof Omit] + if (!connector.provider && ethersProvider) { + await ethersProvider.initialize() + connector.provider = (await ethersProvider.getProvider()) as Provider | undefined + } + this.emit('accountChanged', { address: this.toChecksummedAddress(connection.account.address), chainId: caipNetwork.id,