diff --git a/.changeset/tall-sites-exist.md b/.changeset/tall-sites-exist.md new file mode 100644 index 0000000000..b00ce25b54 --- /dev/null +++ b/.changeset/tall-sites-exist.md @@ -0,0 +1,29 @@ +--- +'@reown/appkit-adapter-bitcoin': patch +'@reown/appkit-adapter-ethers5': patch +'@reown/appkit-adapter-ethers': patch +'@reown/appkit-adapter-solana': patch +'@reown/appkit-adapter-wagmi': patch +'@reown/appkit-controllers': patch +'@reown/appkit-scaffold-ui': patch +'@reown/appkit': patch +'@reown/appkit-common': patch +'pay-test-exchange': patch +'@reown/appkit-utils': patch +'@reown/appkit-cdn': patch +'@reown/appkit-cli': patch +'@reown/appkit-codemod': patch +'@reown/appkit-core': patch +'@reown/appkit-experimental': patch +'@reown/appkit-pay': patch +'@reown/appkit-polyfills': 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 +--- + +Fixed an issue where upon user connection rejection a `CONNECT_ERROR` event was logged. It now logs a new event error called `USER_REJECTED` \ No newline at end of file diff --git a/packages/adapters/bitcoin/src/adapter.ts b/packages/adapters/bitcoin/src/adapter.ts index e613cc75c1..4cb9a5e8ad 100644 --- a/packages/adapters/bitcoin/src/adapter.ts +++ b/packages/adapters/bitcoin/src/adapter.ts @@ -7,7 +7,7 @@ import { type Provider, WcHelpersUtil } from '@reown/appkit' -import { ConstantsUtil } from '@reown/appkit-common' +import { ConstantsUtil, UserRejectedRequestError } from '@reown/appkit-common' import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' import { ChainController, StorageUtil } from '@reown/appkit-controllers' import { HelpersUtil } from '@reown/appkit-utils' @@ -80,7 +80,9 @@ export class BitcoinAdapter extends AdapterBlueprint { } } - const address = await connector.connect() + const address = await connector.connect().catch(err => { + throw new UserRejectedRequestError(err) + }) const accounts = await this.getAccounts({ id: connector.id }) this.emit('accountChanged', { diff --git a/packages/adapters/ethers/src/client.ts b/packages/adapters/ethers/src/client.ts index c2a5331785..5f3a94111c 100644 --- a/packages/adapters/ethers/src/client.ts +++ b/packages/adapters/ethers/src/client.ts @@ -2,7 +2,12 @@ import UniversalProvider from '@walletconnect/universal-provider' import { JsonRpcProvider, formatEther, getAddress } from 'ethers' import { type AppKitOptions, WcConstantsUtil, WcHelpersUtil } from '@reown/appkit' -import { ConstantsUtil as CommonConstantsUtil, ParseUtil } from '@reown/appkit-common' +import { + ConstantsUtil as CommonConstantsUtil, + ErrorUtil, + ParseUtil, + UserRejectedRequestError +} from '@reown/appkit-common' import { AccountController, type CombinedProvider, @@ -401,134 +406,146 @@ export class EthersAdapter extends AdapterBlueprint { chainId, socialUri }: AdapterBlueprint.ConnectParams): Promise { - const connector = this.connectors.find(c => HelpersUtil.isLowerCaseMatch(c.id, id)) + try { + const connector = this.connectors.find(c => HelpersUtil.isLowerCaseMatch(c.id, id)) - if (!connector) { - throw new Error('Connector not found') - } + if (!connector) { + throw new Error('Connector not found') + } - const connection = this.connectionManager?.getConnection({ - address, - connectorId: id, - connections: this.connections, - connectors: this.connectors - }) + const connection = this.connectionManager?.getConnection({ + address, + connectorId: id, + connections: this.connections, + connectors: this.connectors + }) - if (connection) { - const caipNetwork = connection.caipNetwork + if (connection) { + const caipNetwork = connection.caipNetwork - if (!caipNetwork) { - throw new Error('EthersAdapter:connect - could not find the caipNetwork to connect') - } + if (!caipNetwork) { + throw new Error('EthersAdapter:connect - could not find the caipNetwork to connect') + } - if (connection.account) { - this.emit('accountChanged', { - address: this.toChecksummedAddress(connection.account.address), - chainId: caipNetwork.id, - connector - }) + if (connection.account) { + this.emit('accountChanged', { + address: this.toChecksummedAddress(connection.account.address), + chainId: caipNetwork.id, + connector + }) - return { - address: this.toChecksummedAddress(connection.account.address), - chainId: caipNetwork.id, - provider: connector.provider, - type: connector.type, - id + return { + address: this.toChecksummedAddress(connection.account.address), + chainId: caipNetwork.id, + provider: connector.provider, + type: connector.type, + id + } } } - } - const selectedProvider = connector?.provider as Provider + const selectedProvider = connector?.provider as Provider - if (!selectedProvider) { - throw new Error('Provider not found') - } + if (!selectedProvider) { + throw new Error('Provider not found') + } - let accounts: string[] = [] + let accounts: string[] = [] - let requestChainId: string | undefined = undefined + let requestChainId: string | undefined = undefined - if (type === ConstantsUtil.CONNECTOR_TYPE_AUTH) { - const { address: _address, accounts: authAccounts } = - await SIWXUtil.authConnectorAuthenticate({ - authConnector: selectedProvider as unknown as W3mFrameProvider, - chainNamespace: CommonConstantsUtil.CHAIN.EVM, - chainId, - socialUri, - preferredAccountType: getPreferredAccountType('eip155') - }) + if (type === ConstantsUtil.CONNECTOR_TYPE_AUTH) { + const { address: _address, accounts: authAccounts } = + await SIWXUtil.authConnectorAuthenticate({ + authConnector: selectedProvider as unknown as W3mFrameProvider, + chainNamespace: CommonConstantsUtil.CHAIN.EVM, + chainId, + socialUri, + preferredAccountType: getPreferredAccountType('eip155') + }) - const caipNetwork = this.getCaipNetworks().find(n => n.id.toString() === chainId?.toString()) + const caipNetwork = this.getCaipNetworks().find( + n => n.id.toString() === chainId?.toString() + ) - accounts = [_address] + accounts = [_address] + + this.addConnection({ + connectorId: id, + accounts: authAccounts + ? authAccounts.map(account => ({ address: account.address })) + : accounts.map(account => ({ address: account })), + caipNetwork, + auth: { + name: StorageUtil.getConnectedSocialProvider(), + username: StorageUtil.getConnectedSocialUsername() + } + }) - this.addConnection({ - connectorId: id, - accounts: authAccounts - ? authAccounts.map(account => ({ address: account.address })) - : accounts.map(account => ({ address: account })), - caipNetwork, - auth: { - name: StorageUtil.getConnectedSocialProvider(), - username: StorageUtil.getConnectedSocialUsername() - } - }) + this.emit('accountChanged', { + address: this.toChecksummedAddress(accounts[0] as Address), + chainId: Number(chainId), + connector + }) + } else { + accounts = await selectedProvider.request({ + method: 'eth_requestAccounts' + }) - this.emit('accountChanged', { - address: this.toChecksummedAddress(accounts[0] as Address), - chainId: Number(chainId), - connector - }) - } else { - accounts = await selectedProvider.request({ - method: 'eth_requestAccounts' - }) + requestChainId = await selectedProvider.request({ + method: 'eth_chainId' + }) - requestChainId = await selectedProvider.request({ - method: 'eth_chainId' - }) + const caipNetwork = this.getCaipNetworks().find( + n => n.id.toString() === chainId?.toString() + ) - const caipNetwork = this.getCaipNetworks().find(n => n.id.toString() === chainId?.toString()) + if (requestChainId !== chainId) { + if (!caipNetwork) { + throw new Error('EthersAdapter:connect - could not find the caipNetwork to switch') + } - if (requestChainId !== chainId) { - if (!caipNetwork) { - throw new Error('EthersAdapter:connect - could not find the caipNetwork to switch') + try { + await this.switchNetwork({ + caipNetwork, + provider: selectedProvider, + providerType: type as ConnectorType + }) + } catch (error) { + throw new Error('EthersAdapter:connect - Switch network failed') + } } - try { - await this.switchNetwork({ - caipNetwork, - provider: selectedProvider, - providerType: type as ConnectorType - }) - } catch (error) { - throw new Error('EthersAdapter:connect - Switch network failed') + this.emit('accountChanged', { + address: this.toChecksummedAddress(accounts[0] as Address), + chainId: Number(chainId), + connector + }) + + this.addConnection({ + connectorId: id, + accounts: accounts.map(account => ({ address: account })), + caipNetwork + }) + + if (connector.id !== CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT) { + this.listenProviderEvents(id, selectedProvider) } } - this.emit('accountChanged', { + return { address: this.toChecksummedAddress(accounts[0] as Address), chainId: Number(chainId), - connector - }) - - this.addConnection({ - connectorId: id, - accounts: accounts.map(account => ({ address: account })), - caipNetwork - }) - - if (connector.id !== CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT) { - this.listenProviderEvents(id, selectedProvider) + provider: selectedProvider, + type: type as ConnectorType, + id + } + } catch (err) { + if (ErrorUtil.isUserRejectedRequestError(err)) { + throw new UserRejectedRequestError(err) } - } - return { - address: this.toChecksummedAddress(accounts[0] as Address), - chainId: Number(chainId), - provider: selectedProvider, - type: type as ConnectorType, - id + throw err } } diff --git a/packages/adapters/ethers5/src/client.ts b/packages/adapters/ethers5/src/client.ts index 9ef7d073c7..c35258280c 100644 --- a/packages/adapters/ethers5/src/client.ts +++ b/packages/adapters/ethers5/src/client.ts @@ -3,7 +3,12 @@ import * as ethers from 'ethers' import { formatEther } from 'ethers/lib/utils.js' import { type AppKitOptions, WcConstantsUtil, WcHelpersUtil } from '@reown/appkit' -import { ConstantsUtil as CommonConstantsUtil, ParseUtil } from '@reown/appkit-common' +import { + ConstantsUtil as CommonConstantsUtil, + ErrorUtil, + ParseUtil, + UserRejectedRequestError +} from '@reown/appkit-common' import { AccountController, type CombinedProvider, @@ -404,134 +409,146 @@ export class Ethers5Adapter extends AdapterBlueprint { chainId, socialUri }: AdapterBlueprint.ConnectParams): Promise { - const connector = this.connectors.find(c => HelpersUtil.isLowerCaseMatch(c.id, id)) + try { + const connector = this.connectors.find(c => HelpersUtil.isLowerCaseMatch(c.id, id)) - if (!connector) { - throw new Error('Connector not found') - } + if (!connector) { + throw new Error('Connector not found') + } - const connection = this.connectionManager?.getConnection({ - address, - connectorId: id, - connections: this.connections, - connectors: this.connectors - }) + const connection = this.connectionManager?.getConnection({ + address, + connectorId: id, + connections: this.connections, + connectors: this.connectors + }) - if (connection) { - const caipNetwork = connection.caipNetwork + if (connection) { + const caipNetwork = connection.caipNetwork - if (!caipNetwork) { - throw new Error('Ethers5Adapter:connect - could not find the caipNetwork to connect') - } + if (!caipNetwork) { + throw new Error('Ethers5Adapter:connect - could not find the caipNetwork to connect') + } - if (connection.account) { - this.emit('accountChanged', { - address: this.toChecksummedAddress(connection.account.address), - chainId: caipNetwork.id, - connector - }) + if (connection.account) { + this.emit('accountChanged', { + address: this.toChecksummedAddress(connection.account.address), + chainId: caipNetwork.id, + connector + }) - return { - address: this.toChecksummedAddress(connection.account.address), - chainId: caipNetwork.id, - provider: connector.provider, - type: connector.type, - id + return { + address: this.toChecksummedAddress(connection.account.address), + chainId: caipNetwork.id, + provider: connector.provider, + type: connector.type, + id + } } } - } - const selectedProvider = connector?.provider as Provider + const selectedProvider = connector?.provider as Provider - if (!selectedProvider) { - throw new Error('Provider not found') - } + if (!selectedProvider) { + throw new Error('Provider not found') + } - let accounts: string[] = [] + let accounts: string[] = [] - let requestChainId: string | undefined = undefined + let requestChainId: string | undefined = undefined - if (type === 'AUTH') { - const { address: _address, accounts: authAccounts } = - await SIWXUtil.authConnectorAuthenticate({ - authConnector: selectedProvider as unknown as W3mFrameProvider, - chainNamespace: CommonConstantsUtil.CHAIN.EVM, - chainId, - socialUri, - preferredAccountType: getPreferredAccountType('eip155') - }) + if (type === 'AUTH') { + const { address: _address, accounts: authAccounts } = + await SIWXUtil.authConnectorAuthenticate({ + authConnector: selectedProvider as unknown as W3mFrameProvider, + chainNamespace: CommonConstantsUtil.CHAIN.EVM, + chainId, + socialUri, + preferredAccountType: getPreferredAccountType('eip155') + }) - const caipNetwork = this.getCaipNetworks().find(n => n.id.toString() === chainId?.toString()) + const caipNetwork = this.getCaipNetworks().find( + n => n.id.toString() === chainId?.toString() + ) - accounts = [_address] + accounts = [_address] + + this.addConnection({ + connectorId: id, + accounts: authAccounts + ? authAccounts.map(account => ({ address: account.address })) + : accounts.map(account => ({ address: account })), + caipNetwork, + auth: { + name: StorageUtil.getConnectedSocialProvider(), + username: StorageUtil.getConnectedSocialUsername() + } + }) - this.addConnection({ - connectorId: id, - accounts: authAccounts - ? authAccounts.map(account => ({ address: account.address })) - : accounts.map(account => ({ address: account })), - caipNetwork, - auth: { - name: StorageUtil.getConnectedSocialProvider(), - username: StorageUtil.getConnectedSocialUsername() - } - }) + this.emit('accountChanged', { + address: this.toChecksummedAddress(accounts[0] as Address), + chainId: Number(chainId), + connector + }) + } else { + accounts = await selectedProvider.request({ + method: 'eth_requestAccounts' + }) - this.emit('accountChanged', { - address: this.toChecksummedAddress(accounts[0] as Address), - chainId: Number(chainId), - connector - }) - } else { - accounts = await selectedProvider.request({ - method: 'eth_requestAccounts' - }) + requestChainId = await selectedProvider.request({ + method: 'eth_chainId' + }) - requestChainId = await selectedProvider.request({ - method: 'eth_chainId' - }) + const caipNetwork = this.getCaipNetworks().find( + n => n.id.toString() === chainId?.toString() + ) - const caipNetwork = this.getCaipNetworks().find(n => n.id.toString() === chainId?.toString()) + if (requestChainId !== chainId) { + if (!caipNetwork) { + throw new Error('Ethers5Adapter:connect - could not find the caipNetwork to switch') + } - if (requestChainId !== chainId) { - if (!caipNetwork) { - throw new Error('Ethers5Adapter:connect - could not find the caipNetwork to switch') + try { + await this.switchNetwork({ + caipNetwork, + provider: selectedProvider, + providerType: type as ConnectorType + }) + } catch (error) { + throw new Error('Ethers5Adapter:connect - Switch network failed') + } } - try { - await this.switchNetwork({ - caipNetwork, - provider: selectedProvider, - providerType: type as ConnectorType - }) - } catch (error) { - throw new Error('Ethers5Adapter:connect - Switch network failed') + this.emit('accountChanged', { + address: this.toChecksummedAddress(accounts[0] as Address), + chainId: Number(chainId), + connector + }) + + this.addConnection({ + connectorId: id, + accounts: accounts.map(account => ({ address: account })), + caipNetwork + }) + + if (connector.id !== CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT) { + this.listenProviderEvents(id, selectedProvider) } } - this.emit('accountChanged', { + return { address: this.toChecksummedAddress(accounts[0] as Address), chainId: Number(chainId), - connector - }) - - this.addConnection({ - connectorId: id, - accounts: accounts.map(account => ({ address: account })), - caipNetwork - }) - - if (connector.id !== CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT) { - this.listenProviderEvents(id, selectedProvider) + provider: selectedProvider, + type: type as ConnectorType, + id + } + } catch (err) { + if (ErrorUtil.isUserRejectedRequestError(err)) { + throw new UserRejectedRequestError(err) } - } - return { - address: this.toChecksummedAddress(accounts[0] as Address), - chainId: Number(chainId), - provider: selectedProvider, - type: type as ConnectorType, - id + throw err } } diff --git a/packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts b/packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts index 5834a61d5f..10c9c78feb 100644 --- a/packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts +++ b/packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts @@ -1,6 +1,6 @@ import type { Connection, PublicKey, SendOptions } from '@solana/web3.js' -import { type CaipNetwork, ConstantsUtil } from '@reown/appkit-common' +import { type CaipNetwork, ConstantsUtil, UserRejectedRequestError } from '@reown/appkit-common' import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' import type { RequestArguments } from '@reown/appkit-controllers' import type { Provider as CoreProvider } from '@reown/appkit-controllers' @@ -74,7 +74,7 @@ export class CoinbaseWalletProvider extends ProviderEventEmitter implements Sola return account.toBase58() } catch (error) { this.coinbase.emit('error', error) - throw error + throw new UserRejectedRequestError(error) } } diff --git a/packages/adapters/solana/src/providers/WalletStandardProvider.ts b/packages/adapters/solana/src/providers/WalletStandardProvider.ts index b9cc47e4eb..9f610936b2 100644 --- a/packages/adapters/solana/src/providers/WalletStandardProvider.ts +++ b/packages/adapters/solana/src/providers/WalletStandardProvider.ts @@ -28,7 +28,7 @@ import { } from '@wallet-standard/features' import base58 from 'bs58' -import { type CaipNetwork, ConstantsUtil } from '@reown/appkit-common' +import { type CaipNetwork, ConstantsUtil, UserRejectedRequestError } from '@reown/appkit-common' import type { RequestArguments } from '@reown/appkit-controllers' import type { Provider as CoreProvider } from '@reown/appkit-controllers' import { PresetsUtil } from '@reown/appkit-utils' @@ -124,7 +124,9 @@ export class WalletStandardProvider extends ProviderEventEmitter implements Sola public async connect(): Promise { const feature = this.getWalletFeature(StandardConnect) - await feature.connect() + await feature.connect().catch(err => { + throw new UserRejectedRequestError(err) + }) const account = this.getAccount(true) const publicKey = new PublicKey(account.publicKey) diff --git a/packages/adapters/wagmi/src/client.ts b/packages/adapters/wagmi/src/client.ts index cad584b69a..842aaa42c3 100644 --- a/packages/adapters/wagmi/src/client.ts +++ b/packages/adapters/wagmi/src/client.ts @@ -28,13 +28,14 @@ import type UniversalProvider from '@walletconnect/universal-provider' import { type Address, type Hex, - UserRejectedRequestError, + UserRejectedRequestError as ViemUserRejectedRequestError, checksumAddress, formatUnits, parseUnits } from 'viem' import { AppKit, type AppKitOptions } from '@reown/appkit' +import { ErrorUtil, UserRejectedRequestError } from '@reown/appkit-common' import type { AppKitNetwork, BaseNetwork, @@ -592,8 +593,12 @@ export class WagmiAdapter extends AdapterBlueprint { return { clientId: await walletConnectConnector.provider.client.core.crypto.getClientId() } } catch (err) { - if (err instanceof UserRejectedRequestError) { - throw new Error(err.shortMessage) + if (err instanceof ViemUserRejectedRequestError) { + throw new UserRejectedRequestError(err) + } + + if (ErrorUtil.isUserRejectedRequestError(err)) { + throw new UserRejectedRequestError(err) } throw err @@ -603,68 +608,80 @@ export class WagmiAdapter extends AdapterBlueprint { public async connect( params: AdapterBlueprint.ConnectParams ): Promise { - const { id, address, provider, type, info, chainId, socialUri } = params - const connector = this.getWagmiConnector(id) + try { + const { id, address, provider, type, info, chainId, socialUri } = params + const connector = this.getWagmiConnector(id) - if (!connector) { - throw new Error('connectionControllerClient:connectExternal - connector is undefined') - } + if (!connector) { + throw new Error('connectionControllerClient:connectExternal - connector is undefined') + } - if (provider && info && connector.id === CommonConstantsUtil.CONNECTOR_ID.EIP6963) { - // @ts-expect-error Exists on EIP6963Connector - connector.setEip6963Wallet?.({ provider, info }) - } + if (provider && info && connector.id === CommonConstantsUtil.CONNECTOR_ID.EIP6963) { + // @ts-expect-error Exists on EIP6963Connector + connector.setEip6963Wallet?.({ provider, info }) + } - const connection = this.wagmiConfig.state?.connections?.get(connector.uid) + const connection = this.wagmiConfig.state?.connections?.get(connector.uid) - if (connection) { - await this.wagmiConfig.storage?.setItem('recentConnectorId', connector.id) + if (connection) { + await this.wagmiConfig.storage?.setItem('recentConnectorId', connector.id) - const sortedAccounts = [...connection.accounts].sort((a, b) => { - if (HelpersUtil.isLowerCaseMatch(a, address)) { - return -1 - } + const sortedAccounts = [...connection.accounts].sort((a, b) => { + if (HelpersUtil.isLowerCaseMatch(a, address)) { + return -1 + } - if (HelpersUtil.isLowerCaseMatch(b, address)) { - return 1 - } + if (HelpersUtil.isLowerCaseMatch(b, address)) { + return 1 + } - return 0 - }) as [Address, ...Address[]] + return 0 + }) as [Address, ...Address[]] + + this.wagmiConfig?.setState(x => ({ + ...x, + connections: new Map(x.connections).set(connector.uid, { + accounts: sortedAccounts, + chainId: connection.chainId, + connector: connection.connector + }), + current: connector.uid, + status: 'connected' + })) - this.wagmiConfig?.setState(x => ({ - ...x, - connections: new Map(x.connections).set(connector.uid, { - accounts: sortedAccounts, + return { + address: this.toChecksummedAddress(sortedAccounts[0]), chainId: connection.chainId, - connector: connection.connector - }), - current: connector.uid, - status: 'connected' - })) + provider: provider as Provider, + type: type as ConnectorType, + id + } + } + + const res = await connect(this.wagmiConfig, { + connector, + chainId: chainId ? Number(chainId) : undefined, + // @ts-expect-error socialUri is needed for auth connector but not in wagmi types + socialUri + }) return { - address: this.toChecksummedAddress(sortedAccounts[0]), - chainId: connection.chainId, + address: this.toChecksummedAddress(res.accounts[0]), + chainId: res.chainId, provider: provider as Provider, type: type as ConnectorType, id } - } + } catch (err) { + if (err instanceof ViemUserRejectedRequestError) { + throw new UserRejectedRequestError(err) + } - const res = await connect(this.wagmiConfig, { - connector, - chainId: chainId ? Number(chainId) : undefined, - // @ts-expect-error socialUri is needed for auth connector but not in wagmi types - socialUri - }) + if (ErrorUtil.isUserRejectedRequestError(err)) { + throw new UserRejectedRequestError(err) + } - return { - address: this.toChecksummedAddress(res.accounts[0]), - chainId: res.chainId, - provider: provider as Provider, - type: type as ConnectorType, - id + throw err } } diff --git a/packages/appkit/src/adapters/ChainAdapterBlueprint.ts b/packages/appkit/src/adapters/ChainAdapterBlueprint.ts index 0d693a268b..a5c494fa54 100644 --- a/packages/appkit/src/adapters/ChainAdapterBlueprint.ts +++ b/packages/appkit/src/adapters/ChainAdapterBlueprint.ts @@ -8,7 +8,8 @@ import { ConstantsUtil as CommonConstantsUtil, type Connection, type Hex, - type ParsedCaipAddress + type ParsedCaipAddress, + UserRejectedRequestError } from '@reown/appkit-common' import { AccountController, @@ -30,7 +31,7 @@ import type { W3mFrameProvider, W3mFrameTypes } from '@reown/appkit-wallet' import type { AppKitBaseClient } from '../client/appkit-base-client.js' import { ConnectionManager } from '../connections/ConnectionManager.js' import { WalletConnectConnector } from '../connectors/WalletConnectConnector.js' -import type { AppKitOptions } from '../utils/index.js' +import { type AppKitOptions, WcHelpersUtil } from '../utils/index.js' import type { ChainAdapterConnector } from './ChainAdapterConnector.js' type EventName = @@ -306,11 +307,19 @@ export abstract class AdapterBlueprint< public async connectWalletConnect( _chainId?: number | string ): Promise { - const connector = this.getWalletConnectConnector() + try { + const connector = this.getWalletConnectConnector() - const result = await connector.connectWalletConnect() + const result = await connector.connectWalletConnect() - return { clientId: result.clientId } + return { clientId: result.clientId } + } catch (err) { + if (WcHelpersUtil.isUserRejectedRequestError(err)) { + throw new UserRejectedRequestError(err) + } + + throw err + } } /** diff --git a/packages/appkit/src/utils/HelpersUtil.ts b/packages/appkit/src/utils/HelpersUtil.ts index 5f2eb72f04..dd88af7000 100644 --- a/packages/appkit/src/utils/HelpersUtil.ts +++ b/packages/appkit/src/utils/HelpersUtil.ts @@ -66,6 +66,10 @@ export const DEFAULT_METHODS = { } export const WcHelpersUtil = { + RPC_ERROR_CODE: { + USER_REJECTED: 5000, + USER_REJECTED_METHODS: 5002 + }, getMethodsByChainNamespace(chainNamespace: ChainNamespace): string[] { return DEFAULT_METHODS[chainNamespace as keyof typeof DEFAULT_METHODS] || [] }, @@ -242,6 +246,26 @@ export const WcHelpersUtil = { ) }, + isUserRejectedRequestError(error: unknown) { + try { + if (typeof error === 'object' && error !== null) { + const objErr = error as Record + + const hasCode = typeof objErr['code'] === 'number' + const hasUserRejectedMethods = + hasCode && objErr['code'] === WcHelpersUtil.RPC_ERROR_CODE.USER_REJECTED_METHODS + const hasUserRejected = + hasCode && objErr['code'] === WcHelpersUtil.RPC_ERROR_CODE.USER_REJECTED + + return hasUserRejectedMethods || hasUserRejected + } + + return false + } catch { + return false + } + }, + isOriginAllowed( currentOrigin: string, allowedPatterns: string[], diff --git a/packages/appkit/tests/utils/HelpersUtil.test.ts b/packages/appkit/tests/utils/HelpersUtil.test.ts index 5dbd73b418..800646a49e 100644 --- a/packages/appkit/tests/utils/HelpersUtil.test.ts +++ b/packages/appkit/tests/utils/HelpersUtil.test.ts @@ -639,4 +639,41 @@ describe('WcHelpersUtil', () => { ).toBe(true) }) }) + + describe('isUserRejectedRequestError', () => { + test('returns true when error.code is USER_REJECTED (5000)', () => { + const error = { code: WcHelpersUtil.RPC_ERROR_CODE.USER_REJECTED } + expect(WcHelpersUtil.isUserRejectedRequestError(error)).toBe(true) + }) + + test('returns true when error.code is USER_REJECTED_METHODS (5002)', () => { + const error = { code: WcHelpersUtil.RPC_ERROR_CODE.USER_REJECTED_METHODS } + expect(WcHelpersUtil.isUserRejectedRequestError(error)).toBe(true) + }) + + test('returns false for other numeric codes', () => { + const error = { code: 1234 } + expect(WcHelpersUtil.isUserRejectedRequestError(error)).toBe(false) + }) + + test('returns false when code is a string number', () => { + const error = { code: '5000' } + expect(WcHelpersUtil.isUserRejectedRequestError(error)).toBe(false) + }) + + test('returns false when code is missing', () => { + const error = { message: 'Some error' } + expect(WcHelpersUtil.isUserRejectedRequestError(error)).toBe(false) + }) + + test('returns false for null/undefined/non-object inputs', () => { + expect(WcHelpersUtil.isUserRejectedRequestError(null)).toBe(false) + expect(WcHelpersUtil.isUserRejectedRequestError(undefined)).toBe(false) + expect(WcHelpersUtil.isUserRejectedRequestError('string')).toBe(false) + expect(WcHelpersUtil.isUserRejectedRequestError(5000)).toBe(false) + expect(WcHelpersUtil.isUserRejectedRequestError(true)).toBe(false) + expect(WcHelpersUtil.isUserRejectedRequestError(BigInt(0))).toBe(false) + expect(WcHelpersUtil.isUserRejectedRequestError(new Error('test'))).toBe(false) + }) + }) }) diff --git a/packages/common/index.ts b/packages/common/index.ts index 6b6029e849..daaa0ba885 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -9,6 +9,7 @@ export { NavigationUtil } from './src/utils/NavigationUtil.js' export { ConstantsUtil } from './src/utils/ConstantsUtil.js' export { Emitter } from './src/utils/EmitterUtil.js' export { ParseUtil } from './src/utils/ParseUtil.js' +export { ErrorUtil, UserRejectedRequestError } from './src/utils/ErrorUtil.js' export { SafeLocalStorage, SafeLocalStorageKeys, diff --git a/packages/common/src/utils/ErrorUtil.ts b/packages/common/src/utils/ErrorUtil.ts new file mode 100644 index 0000000000..cf54209d98 --- /dev/null +++ b/packages/common/src/utils/ErrorUtil.ts @@ -0,0 +1,73 @@ +/* eslint-disable max-classes-per-file */ +/* eslint-disable line-comment-position */ +/* eslint-disable no-inline-comments */ + +// -- Types ------------------------------------------------------------------- // +export type ProviderRpcErrorCode = + | 4001 // User Rejected Request + | 4100 // Unauthorized + | 4200 // Unsupported Method + | 4900 // Disconnected + | 4901 // Chain Disconnected + | 4902 // Chain Not Recognized + | 5710 // Unsupported chain id + +type RpcProviderError = { + message: string + code: ProviderRpcErrorCode +} + +// -- Classes ---------------------------------------------------------------- // +export class ProviderRpcError extends Error { + public code: ProviderRpcErrorCode + override name = 'ProviderRpcError' + + constructor(cause: unknown, options: RpcProviderError) { + super(options.message, { cause }) + this.code = options.code + } +} + +export class UserRejectedRequestError extends ProviderRpcError { + override name = 'UserRejectedRequestError' + + constructor(cause: unknown) { + super(cause, { + code: ErrorUtil.RPC_ERROR_CODE.USER_REJECTED_REQUEST, + message: 'User rejected the request' + }) + } +} + +// -- Utils ---------------------------------------------------------------- // +export const ErrorUtil = { + RPC_ERROR_CODE: { + USER_REJECTED_REQUEST: 4001 + } as const, + isRpcProviderError(error: unknown): error is RpcProviderError { + try { + if (typeof error === 'object' && error !== null) { + const objErr = error as Record + + const hasMessage = typeof objErr['message'] === 'string' + const hasCode = typeof objErr['code'] === 'number' + + return hasMessage && hasCode + } + + return false + } catch { + return false + } + }, + isUserRejectedRequestError(error: unknown) { + if (ErrorUtil.isRpcProviderError(error)) { + const isUserRejectedCode = error.code === ErrorUtil.RPC_ERROR_CODE.USER_REJECTED_REQUEST + const isUserRejectedMessage = error.message.toLowerCase().includes('user rejected') + + return isUserRejectedCode || isUserRejectedMessage + } + + return false + } +} diff --git a/packages/common/tests/ErrorUtil.test.ts b/packages/common/tests/ErrorUtil.test.ts new file mode 100644 index 0000000000..6079c7ec8c --- /dev/null +++ b/packages/common/tests/ErrorUtil.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest' + +import { ErrorUtil, ProviderRpcError, UserRejectedRequestError } from '../src/utils/ErrorUtil' + +describe('ErrorUtil', () => { + describe('isRpcProviderError', () => { + it('returns true for valid RpcProviderError shape', () => { + const error = { message: 'Some message', code: 4900 } + expect(ErrorUtil.isRpcProviderError(error)).toBe(true) + }) + + it('returns false when message is missing', () => { + const error = { code: 4900 } + expect(ErrorUtil.isRpcProviderError(error)).toBe(false) + }) + + it('returns false when code is missing', () => { + const error = { message: 'Some message' } + expect(ErrorUtil.isRpcProviderError(error)).toBe(false) + }) + + it('returns false for non-object inputs', () => { + expect(ErrorUtil.isRpcProviderError(null)).toBe(false) + expect(ErrorUtil.isRpcProviderError(undefined)).toBe(false) + expect(ErrorUtil.isRpcProviderError('error')).toBe(false) + expect(ErrorUtil.isRpcProviderError(4001)).toBe(false) + expect(ErrorUtil.isRpcProviderError(true)).toBe(false) + expect(ErrorUtil.isRpcProviderError(BigInt(0))).toBe(false) + expect(ErrorUtil.isRpcProviderError(new Error('test'))).toBe(false) + }) + }) + + describe('isUserRejectedRequestError', () => { + it('returns true when code is USER_REJECTED_REQUEST (4001)', () => { + const error = { message: 'Denied', code: ErrorUtil.RPC_ERROR_CODE.USER_REJECTED_REQUEST } + expect(ErrorUtil.isUserRejectedRequestError(error)).toBe(true) + }) + + it('returns true when message includes "user rejected" (case-insensitive)', () => { + const errorLower = { message: 'user rejected the action', code: 4100 } + const errorMixed = { message: 'User Rejected request', code: 4200 } + expect(ErrorUtil.isUserRejectedRequestError(errorLower)).toBe(true) + expect(ErrorUtil.isUserRejectedRequestError(errorMixed)).toBe(true) + }) + + it('returns false when neither code nor message indicates user rejection', () => { + const error = { message: 'Some other error', code: 4200 } + expect(ErrorUtil.isUserRejectedRequestError(error)).toBe(false) + }) + + it('returns false for non RpcProviderError inputs', () => { + expect(ErrorUtil.isUserRejectedRequestError({ code: 4001 })).toBe(false) + expect(ErrorUtil.isUserRejectedRequestError({ message: 'user rejected' })).toBe(false) + expect(ErrorUtil.isUserRejectedRequestError('user rejected')).toBe(false) + expect(ErrorUtil.isUserRejectedRequestError(BigInt(0))).toBe(false) + expect(ErrorUtil.isUserRejectedRequestError(new Error('test'))).toBe(false) + }) + }) + + describe('Error classes', () => { + it('ProviderRpcError sets message and code', () => { + const cause = new Error('original') + const err = new ProviderRpcError(cause, { message: 'boom', code: 4900 }) + expect(err).toBeInstanceOf(Error) + expect(err.message).toBe('boom') + expect(err.code).toBe(4900) + expect(err.cause).toBe(cause) + expect(err.name).toBe('ProviderRpcError') + }) + + it('UserRejectedRequestError sets proper name, code and message', () => { + const cause = new Error('original') + const err = new UserRejectedRequestError(cause) + expect(err.name).toBe('UserRejectedRequestError') + expect(err.message).toBe('User rejected the request') + expect(err.code).toBe(ErrorUtil.RPC_ERROR_CODE.USER_REJECTED_REQUEST) + expect(err.name).toBe('UserRejectedRequestError') + }) + }) +}) diff --git a/packages/controllers/src/utils/TypeUtil.ts b/packages/controllers/src/utils/TypeUtil.ts index bd98f2afb0..c7424266c8 100644 --- a/packages/controllers/src/utils/TypeUtil.ts +++ b/packages/controllers/src/utils/TypeUtil.ts @@ -484,6 +484,14 @@ export type Event = message: string } } + | { + type: 'track' + address?: string + event: 'USER_REJECTED' + properties: { + message: string + } + } | { type: 'track' address?: string diff --git a/packages/controllers/src/utils/withErrorBoundary.ts b/packages/controllers/src/utils/withErrorBoundary.ts index 13a637f8cc..ea7cf27969 100644 --- a/packages/controllers/src/utils/withErrorBoundary.ts +++ b/packages/controllers/src/utils/withErrorBoundary.ts @@ -7,6 +7,7 @@ export type Controller = Record export class AppKitError extends Error { public category: TelemetryErrorCategory public originalError: unknown + public originalName = 'AppKitError' constructor(message: string, category: TelemetryErrorCategory, originalError?: unknown) { super(message) @@ -14,6 +15,10 @@ export class AppKitError extends Error { this.category = category this.originalError = originalError + if (originalError && originalError instanceof Error) { + this.originalName = originalError.name + } + // Ensure `this instanceof AppKitError` is true, important for custom errors. Object.setPrototypeOf(this, AppKitError.prototype) diff --git a/packages/scaffold-ui/src/partials/w3m-connecting-wc-browser/index.ts b/packages/scaffold-ui/src/partials/w3m-connecting-wc-browser/index.ts index ce40129cf6..662ec9d234 100644 --- a/packages/scaffold-ui/src/partials/w3m-connecting-wc-browser/index.ts +++ b/packages/scaffold-ui/src/partials/w3m-connecting-wc-browser/index.ts @@ -1,5 +1,7 @@ +import { UserRejectedRequestError } from '@reown/appkit-common' import type { BaseError } from '@reown/appkit-controllers' import { + AppKitError, ConnectionController, ConnectorController, EventsController, @@ -59,11 +61,23 @@ export class W3mConnectingWcBrowser extends W3mConnectingWidget { } }) } catch (error) { - EventsController.sendEvent({ - type: 'track', - event: 'CONNECT_ERROR', - properties: { message: (error as BaseError)?.message ?? 'Unknown' } - }) + const isUserRejectedRequestError = + error instanceof AppKitError && error.originalName === UserRejectedRequestError.name + + if (isUserRejectedRequestError) { + EventsController.sendEvent({ + type: 'track', + event: 'USER_REJECTED', + properties: { message: error.message } + }) + } else { + EventsController.sendEvent({ + type: 'track', + event: 'CONNECT_ERROR', + properties: { message: (error as BaseError)?.message ?? 'Unknown' } + }) + } + this.error = true } } diff --git a/packages/scaffold-ui/src/views/w3m-connecting-external-view/index.ts b/packages/scaffold-ui/src/views/w3m-connecting-external-view/index.ts index 6721a2830e..816063c1d8 100644 --- a/packages/scaffold-ui/src/views/w3m-connecting-external-view/index.ts +++ b/packages/scaffold-ui/src/views/w3m-connecting-external-view/index.ts @@ -1,7 +1,12 @@ -import { type ChainNamespace, ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common' +import { + type ChainNamespace, + ConstantsUtil as CommonConstantsUtil, + UserRejectedRequestError +} from '@reown/appkit-common' import type { Connection } from '@reown/appkit-common' import type { BaseError, Connector } from '@reown/appkit-controllers' import { + AppKitError, ConnectionController, ConnectionControllerUtil, ConnectorController, @@ -104,11 +109,23 @@ export class W3mConnectingExternalView extends W3mConnectingWidget { } } } catch (error) { - EventsController.sendEvent({ - type: 'track', - event: 'CONNECT_ERROR', - properties: { message: (error as BaseError)?.message ?? 'Unknown' } - }) + const isUserRejectedRequestError = + error instanceof AppKitError && error.originalName === UserRejectedRequestError.name + + if (isUserRejectedRequestError) { + EventsController.sendEvent({ + type: 'track', + event: 'USER_REJECTED', + properties: { message: error.message } + }) + } else { + EventsController.sendEvent({ + type: 'track', + event: 'CONNECT_ERROR', + properties: { message: (error as BaseError)?.message ?? 'Unknown' } + }) + } + this.error = true } } diff --git a/packages/scaffold-ui/src/views/w3m-connecting-wc-view/index.ts b/packages/scaffold-ui/src/views/w3m-connecting-wc-view/index.ts index 8edefb2852..b1ea59fb1d 100644 --- a/packages/scaffold-ui/src/views/w3m-connecting-wc-view/index.ts +++ b/packages/scaffold-ui/src/views/w3m-connecting-wc-view/index.ts @@ -1,8 +1,10 @@ import { LitElement, html } from 'lit' import { property, state } from 'lit/decorators.js' +import { UserRejectedRequestError } from '@reown/appkit-common' import type { BaseError, Platform } from '@reown/appkit-controllers' import { + AppKitError, ChainController, ConnectionController, CoreHelperUtil, @@ -135,11 +137,23 @@ export class W3mConnectingWcView extends LitElement { } } - EventsController.sendEvent({ - type: 'track', - event: 'CONNECT_ERROR', - properties: { message: (error as BaseError)?.message ?? 'Unknown' } - }) + const isUserRejectedRequestError = + error instanceof AppKitError && error.originalName === UserRejectedRequestError.name + + if (isUserRejectedRequestError) { + EventsController.sendEvent({ + type: 'track', + event: 'USER_REJECTED', + properties: { message: error.message } + }) + } else { + EventsController.sendEvent({ + type: 'track', + event: 'CONNECT_ERROR', + properties: { message: (error as BaseError)?.message ?? 'Unknown' } + }) + } + ConnectionController.setWcError(true) SnackController.showError((error as BaseError).message ?? 'Connection error') ConnectionController.resetWcConnection()