Skip to content
Open
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
10 changes: 10 additions & 0 deletions .changeset/siwx-walletconnect-routing.md
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 27 additions & 1 deletion packages/appkit/src/client/appkit-base-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
SnackController,
StorageUtil,
ThemeController,
WalletConnectConnector,
WalletUtil,
WcHelpersUtil,
getPreferredAccountType,
Expand Down Expand Up @@ -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 || ''
Expand Down
42 changes: 39 additions & 3 deletions packages/appkit/tests/client/sign-message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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',
Expand All @@ -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' })

Expand Down
38 changes: 38 additions & 0 deletions packages/appkit/tests/connectors/WalletConnectConnector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('WalletConnectConnector', () => {
let provider: typeof mockProvider

beforeEach(() => {
vi.clearAllMocks()
caipNetworks = [
{ ...mainnet, caipNetworkId: 'eip155:1', chainNamespace: 'eip155' },
solana,
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -55,6 +62,29 @@ export class WalletConnectConnector<Namespace extends ChainNamespace = ChainName
await this.provider.disconnect()
}

async signEvmMessage({
message,
address,
caipNetworkId
}: WalletConnectConnector.SignEvmMessageParams): Promise<WalletConnectConnector.SignEvmMessageResult> {
try {
const hexMessage = isHex(message) ? message : stringToHex(message)
const signature = await this.provider.request<Hex>(
{
method: 'personal_sign',
params: [hexMessage, address]
},
caipNetworkId
)

return { signature }
} catch (error) {
throw new Error('WalletConnectConnector:signEvmMessage - Sign message failed', {
cause: error
})
}
}

async authenticate(): Promise<boolean> {
const chains = this.chains.map(network => network.caipNetworkId)

Expand All @@ -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 = [
Expand Down
Loading