From f72079fa72ce970215d1b327883f097c5a7cfa9e Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 31 Jul 2026 11:00:39 +0200 Subject: [PATCH] fix(walletconnect): scope EVM message signing --- .changeset/siwx-walletconnect-routing.md | 10 +++++ .../appkit/src/client/appkit-base-client.ts | 28 ++++++++++++- .../appkit/tests/client/sign-message.test.ts | 42 +++++++++++++++++-- .../connectors/WalletConnectConnector.test.ts | 38 +++++++++++++++++ .../WalletConnectConnector.ts | 42 ++++++++++++++++++- 5 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 .changeset/siwx-walletconnect-routing.md diff --git a/.changeset/siwx-walletconnect-routing.md b/.changeset/siwx-walletconnect-routing.md new file mode 100644 index 0000000000..4ddaa53298 --- /dev/null +++ b/.changeset/siwx-walletconnect-routing.md @@ -0,0 +1,10 @@ +--- +'@reown/appkit': patch +'@reown/appkit-controllers': patch +--- + +Route EVM WalletConnect signatures through the shared connector. + +The shared WalletConnect connector now passes the captured CAIP network to UniversalProvider +instead of relying on mutable provider state. This applies consistently across EVM adapters, and +provider errors retain their original cause. diff --git a/packages/appkit/src/client/appkit-base-client.ts b/packages/appkit/src/client/appkit-base-client.ts index 79432adeb3..3ad725b48c 100644 --- a/packages/appkit/src/client/appkit-base-client.ts +++ b/packages/appkit/src/client/appkit-base-client.ts @@ -78,6 +78,7 @@ import { SnackController, StorageUtil, ThemeController, + WalletConnectConnector, WalletUtil, WcHelpersUtil, getPreferredAccountType, @@ -769,12 +770,37 @@ export abstract class AppKitBaseClient { throw new Error('signMessage: connector changed before the request was sent') } + const connectorId = context?.connectorId || activeConnectorId + if ( + connectorId === ConstantsUtil.CONNECTOR_ID.WALLET_CONNECT && + namespace === ConstantsUtil.CHAIN.EVM + ) { + const connector = ConnectorController.getConnector({ + id: connectorId, + namespace + }) as WalletConnectConnector | undefined + + if (!connector || typeof connector.signEvmMessage !== 'function') { + throw new Error( + 'signMessage: WalletConnect connector does not support EVM message signing' + ) + } + + const result = await connector.signEvmMessage({ + message, + address, + caipNetworkId: caipNetwork.caipNetworkId + }) + + return result.signature + } + const result = await adapter.signMessage({ message, address, provider: ProviderController.getProvider(namespace), caipNetwork, - connectorId: context?.connectorId || activeConnectorId + connectorId }) return result?.signature || '' diff --git a/packages/appkit/tests/client/sign-message.test.ts b/packages/appkit/tests/client/sign-message.test.ts index bcb502e021..71fec638ba 100644 --- a/packages/appkit/tests/client/sign-message.test.ts +++ b/packages/appkit/tests/client/sign-message.test.ts @@ -5,7 +5,8 @@ import { ChainController, ConnectorController, ProviderController, - type SignMessageContext + type SignMessageContext, + type WalletConnectConnector } from '@reown/appkit-controllers' import { mockChainControllerState } from '@reown/appkit-controllers/testing' @@ -41,8 +42,8 @@ describe('AppKit message signing', () => { appKit = new TestAppKit(mockOptions) }) - it('uses the captured SIWX chain and account when the active namespace changes', async () => { - const provider = { request: vi.fn() } + it('routes WalletConnect signing through the captured EVM connector', async () => { + const signEvmMessage = vi.fn().mockResolvedValue({ signature: '0xsignature' }) const context: SignMessageContext = { chainId: mainnet.caipNetworkId, accountAddress: '0x1234567890123456789012345678901234567890', @@ -55,6 +56,41 @@ describe('AppKit message signing', () => { }) vi.spyOn(ChainController, 'getCaipNetworkById').mockReturnValue(mainnet) vi.spyOn(ConnectorController, 'getConnectorId').mockReturnValue('walletConnect') + vi.spyOn(ConnectorController, 'getConnector').mockReturnValue({ + signEvmMessage + } as unknown as WalletConnectConnector) + const getProviderSpy = vi.spyOn(ProviderController, 'getProvider') + + const result = await appKit.testConnectionControllerClient?.signMessage('Sign in', context) + + expect(ConnectorController.getConnector).toHaveBeenCalledWith({ + id: context.connectorId, + namespace: ConstantsUtil.CHAIN.EVM + }) + expect(signEvmMessage).toHaveBeenCalledWith({ + message: 'Sign in', + address: context.accountAddress, + caipNetworkId: context.chainId + }) + expect(getProviderSpy).not.toHaveBeenCalled() + expect(mockEvmAdapter.signMessage).not.toHaveBeenCalled() + expect(result).toBe('0xsignature') + }) + + it('keeps adapter signing for non-WalletConnect connectors', async () => { + const provider = { request: vi.fn() } + const context: SignMessageContext = { + chainId: mainnet.caipNetworkId, + accountAddress: '0x1234567890123456789012345678901234567890', + connectorId: 'injected' + } + + mockChainControllerState({ + activeChain: ConstantsUtil.CHAIN.SOLANA, + activeCaipNetwork: solana + }) + vi.spyOn(ChainController, 'getCaipNetworkById').mockReturnValue(mainnet) + vi.spyOn(ConnectorController, 'getConnectorId').mockReturnValue('injected') vi.spyOn(ProviderController, 'getProvider').mockReturnValue(provider) vi.spyOn(mockEvmAdapter, 'signMessage').mockResolvedValue({ signature: '0xsignature' }) diff --git a/packages/appkit/tests/connectors/WalletConnectConnector.test.ts b/packages/appkit/tests/connectors/WalletConnectConnector.test.ts index 80926e0a51..407fde9d50 100644 --- a/packages/appkit/tests/connectors/WalletConnectConnector.test.ts +++ b/packages/appkit/tests/connectors/WalletConnectConnector.test.ts @@ -19,6 +19,7 @@ describe('WalletConnectConnector', () => { let provider: typeof mockProvider beforeEach(() => { + vi.clearAllMocks() caipNetworks = [ { ...mainnet, caipNetworkId: 'eip155:1', chainNamespace: 'eip155' }, solana, @@ -79,6 +80,43 @@ describe('WalletConnectConnector', () => { }) }) + describe('signEvmMessage', () => { + it('routes personal_sign to the supplied CAIP network', async () => { + vi.mocked(provider.request).mockResolvedValueOnce('0xsignature') + + const result = await connector.signEvmMessage({ + message: 'Sign in', + address: '0x1234567890123456789012345678901234567890', + caipNetworkId: 'eip155:1' + }) + + expect(provider.request).toHaveBeenCalledWith( + { + method: 'personal_sign', + params: ['0x5369676e20696e', '0x1234567890123456789012345678901234567890'] + }, + 'eip155:1' + ) + expect(result).toEqual({ signature: '0xsignature' }) + }) + + it('preserves the provider error as the cause', async () => { + const cause = new Error('Wallet request failed') + vi.mocked(provider.request).mockRejectedValueOnce(cause) + + await expect( + connector.signEvmMessage({ + message: 'Sign in', + address: '0x1234567890123456789012345678901234567890', + caipNetworkId: 'eip155:1' + }) + ).rejects.toMatchObject({ + message: 'WalletConnectConnector:signEvmMessage - Sign message failed', + cause + }) + }) + }) + describe('disconnect', () => { it('should disconnect from the provider', async () => { await connector.disconnect() diff --git a/packages/controllers/src/controllers/AdapterController/WalletConnectConnector.ts b/packages/controllers/src/controllers/AdapterController/WalletConnectConnector.ts index c5aeb960c9..19e4df47c1 100644 --- a/packages/controllers/src/controllers/AdapterController/WalletConnectConnector.ts +++ b/packages/controllers/src/controllers/AdapterController/WalletConnectConnector.ts @@ -1,7 +1,14 @@ import type { SessionTypes } from '@walletconnect/types' import UniversalProvider from '@walletconnect/universal-provider' +import { isHex, stringToHex } from 'viem' -import { type CaipNetwork, type ChainNamespace, ConstantsUtil } from '@reown/appkit-common' +import { + type CaipNetwork, + type CaipNetworkId, + type ChainNamespace, + ConstantsUtil, + type Hex +} from '@reown/appkit-common' import { SIWXUtil } from '../../utils/SIWXUtil.js' import { WcHelpersUtil } from '../../utils/WalletConnectUtil.js' @@ -55,6 +62,29 @@ export class WalletConnectConnector { + try { + const hexMessage = isHex(message) ? message : stringToHex(message) + const signature = await this.provider.request( + { + method: 'personal_sign', + params: [hexMessage, address] + }, + caipNetworkId + ) + + return { signature } + } catch (error) { + throw new Error('WalletConnectConnector:signEvmMessage - Sign message failed', { + cause: error + }) + } + } + async authenticate(): Promise { const chains = this.chains.map(network => network.caipNetworkId) @@ -77,6 +107,16 @@ export namespace WalletConnectConnector { clientId: string | null session: SessionTypes.Struct } + + export type SignEvmMessageParams = { + message: string + address: string + caipNetworkId: CaipNetworkId + } + + export type SignEvmMessageResult = { + signature: Hex + } } const OPTIONAL_METHODS = [