diff --git a/.changeset/wallet-add-chain-unsupported.md b/.changeset/wallet-add-chain-unsupported.md new file mode 100644 index 000000000..70fce4566 --- /dev/null +++ b/.changeset/wallet-add-chain-unsupported.md @@ -0,0 +1,6 @@ +--- +'@relayprotocol/relay-sdk': patch +'@relayprotocol/relay-kit-ui': patch +--- + +Show an unsupported chain error instead of the locked-wallet message when a wallet rejects wallet_addEthereumChain diff --git a/packages/sdk/src/utils/viemWallet.test.ts b/packages/sdk/src/utils/viemWallet.test.ts index 163be57ec..e89c0b9dd 100644 --- a/packages/sdk/src/utils/viemWallet.test.ts +++ b/packages/sdk/src/utils/viemWallet.test.ts @@ -1,6 +1,26 @@ import { describe, it, expect, vi } from 'vitest' +import { createWalletClient, custom } from 'viem' import { adaptViemWallet } from './viemWallet' +const INK_CHAIN_ID = 57073 + +vi.mock('../client.js', () => ({ + getClient: () => ({ + chains: [ + { + id: 57073, + viemChain: { + id: 57073, + name: 'Ink', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { default: { http: ['https://rpc-gel.inkonchain.com'] } } + } + } + ], + log: vi.fn() + }) +})) + const buildWallet = (overrides: Record = {}) => ({ account: { address: '0x0000000000000000000000000000000000000001' }, @@ -48,3 +68,61 @@ describe('adaptViemWallet disableCapabilitiesCheck', () => { expect(hangingGetCapabilities).not.toHaveBeenCalled() }) }) + +describe('adaptViemWallet switchChain', () => { + // EIP-1193 provider that doesn't know the chain and responds to + // wallet_addEthereumChain with the given error (or success when undefined) + const buildProvider = (addChainError?: { code: number; message: string }) => ({ + request: vi.fn(async ({ method }: { method: string }) => { + if (method === 'wallet_switchEthereumChain') { + throw Object.assign(new Error('Unrecognized chain ID "0xdef1".'), { + code: 4902 + }) + } + if (method === 'wallet_addEthereumChain') { + if (addChainError) { + throw Object.assign(new Error(addChainError.message), { + code: addChainError.code + }) + } + return null + } + return null + }) + }) + + const adapt = (provider: ReturnType) => + adaptViemWallet( + createWalletClient({ transport: custom(provider, { retryCount: 0 }) }) + ) + + it('maps a rejected wallet_addEthereumChain to an unsupported chain error (Coinbase Wallet, Ink origin)', async () => { + const provider = buildProvider({ + code: -32004, + message: 'Method "wallet_addEthereumChain" is not supported.' + }) + + await expect(adapt(provider).switchChain(INK_CHAIN_ID)).rejects.toThrow( + 'Wallet does not support chain' + ) + expect(provider.request).toHaveBeenCalledWith( + expect.objectContaining({ method: 'wallet_addEthereumChain' }) + ) + }) + + it('rethrows other addChain errors unchanged', async () => { + const provider = buildProvider({ code: 4100, message: 'Wallet is locked' }) + + await expect(adapt(provider).switchChain(INK_CHAIN_ID)).rejects.toThrow( + 'Wallet is locked' + ) + }) + + it('resolves when addChain succeeds', async () => { + const provider = buildProvider() + + await expect( + adapt(provider).switchChain(INK_CHAIN_ID) + ).resolves.toBeUndefined() + }) +}) diff --git a/packages/sdk/src/utils/viemWallet.ts b/packages/sdk/src/utils/viemWallet.ts index 3cd8f4c28..4afde2985 100644 --- a/packages/sdk/src/utils/viemWallet.ts +++ b/packages/sdk/src/utils/viemWallet.ts @@ -8,7 +8,8 @@ import { custom, fallback, hexToBigInt, - http + http, + MethodNotSupportedRpcError } from 'viem' // Cache for expensive RPC calls (code, balance, tx count) @@ -212,6 +213,9 @@ export const adaptViemWallet = ( LogLevel.Verbose ) return + } else if (e?.code === MethodNotSupportedRpcError.code) { + // Wallet rejects wallet_addEthereumChain (EIP-1474 code -32004) + throw new Error('Wallet does not support chain') } else { throw e } diff --git a/packages/ui/src/components/common/ErrorWell.tsx b/packages/ui/src/components/common/ErrorWell.tsx index 2c0e0693d..63bccfd46 100644 --- a/packages/ui/src/components/common/ErrorWell.tsx +++ b/packages/ui/src/components/common/ErrorWell.tsx @@ -23,9 +23,11 @@ const ErrorWell: React.FC = ({ error, hasTxHashes, fromChain }) => { } else if ( error?.name && (error?.message?.includes('does not support chain') || - error?.message?.match(/Chain \d+ not supported/)) + error?.message?.match(/Chain \d+ not supported/) || + error?.message?.match(/wallet_addEthereumChain.*not supported/)) ) { - return `Your wallet does not support ${fromChain?.displayName ?? 'this chain'}` + const chainName = fromChain?.displayName ?? 'this chain' + return `Your wallet does not support ${chainName}. Add ${chainName} to your wallet manually or connect a different wallet, then try again.` } if ( !hasTxHashes || @@ -40,7 +42,7 @@ const ErrorWell: React.FC = ({ error, hasTxHashes, fromChain }) => { return 'Transaction Failed. Try adjusting slippage or gas limits and try again.' } return error?.message - }, [error?.message, hasTxHashes]) + }, [error?.message, hasTxHashes, fromChain?.displayName]) const shouldScrollErrorMessage = typeof renderedErrorMessage === 'string' &&