Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .changeset/hot-areas-crash.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 12 additions & 0 deletions packages/adapters/ethers/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderType, 'metadata' | 'EIP6963'>]
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,
Expand Down
93 changes: 93 additions & 0 deletions packages/adapters/ethers/src/tests/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
12 changes: 12 additions & 0 deletions packages/adapters/ethers5/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
}

private async createEthersConfig() {
const { metadata, enableCoinbase, enableBaseAccount, enableInjected, enableEIP6963 } =

Check warning on line 66 in packages/adapters/ethers5/src/client.ts

View workflow job for this annotation

GitHub Actions / code_style (lint)

Variable name `enableEIP6963` must have one of the following prefixes: is, has, can, should, will, did

Check warning on line 66 in packages/adapters/ethers5/src/client.ts

View workflow job for this annotation

GitHub Actions / code_style (lint)

Variable name `enableInjected` must have one of the following prefixes: is, has, can, should, will, did

Check warning on line 66 in packages/adapters/ethers5/src/client.ts

View workflow job for this annotation

GitHub Actions / code_style (lint)

Variable name `enableBaseAccount` must have one of the following prefixes: is, has, can, should, will, did

Check warning on line 66 in packages/adapters/ethers5/src/client.ts

View workflow job for this annotation

GitHub Actions / code_style (lint)

Variable name `enableCoinbase` must have one of the following prefixes: is, has, can, should, will, did
OptionsController.state
if (!metadata) {
return undefined
Expand Down Expand Up @@ -454,6 +454,18 @@
}

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<ProviderType, 'metadata' | 'EIP6963'>]
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,
Expand Down
Loading