@reown/appkit-wagmi-react-native 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/adapter.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  EVMAdapter,
3
- WalletConnector,
4
3
  type AppKitNetwork,
4
+ type BlockchainAdapterInitParams,
5
5
  type CaipAddress,
6
6
  type ChainNamespace,
7
7
  type GetBalanceParams,
@@ -15,7 +15,8 @@ import {
15
15
  switchChain as switchChainWagmi,
16
16
  disconnect as disconnectWagmiCore,
17
17
  connect as connectWagmi,
18
- type Connector
18
+ type Connector,
19
+ watchAccount
19
20
  } from '@wagmi/core';
20
21
  import type { Chain } from 'wagmi/chains';
21
22
  import { getTransport } from './utils/helpers';
@@ -25,7 +26,6 @@ import { UniversalConnector } from './connectors/UniversalConnector';
25
26
  type ConfigParams = Partial<CreateConfigParameters> & {
26
27
  networks: readonly [Chain, ...Chain[]];
27
28
  projectId: string;
28
- connectors?: Connector[];
29
29
  };
30
30
 
31
31
  export class WagmiAdapter extends EVMAdapter {
@@ -33,22 +33,18 @@ export class WagmiAdapter extends EVMAdapter {
33
33
  public wagmiChains: readonly Chain[] | undefined;
34
34
  public wagmiConfig!: Config;
35
35
  private wagmiConfigConnector?: Connector;
36
+ private unsubscribeWatchAccount?: () => void;
36
37
 
37
38
  constructor(configParams: ConfigParams) {
38
39
  super({
39
- projectId: configParams.projectId,
40
- supportedNamespace: WagmiAdapter.supportedNamespace
40
+ supportedNamespace: WagmiAdapter.supportedNamespace,
41
+ adapterType: 'wagmi'
41
42
  });
42
43
  this.wagmiChains = configParams.networks;
43
44
  this.wagmiConfig = this.createWagmiInternalConfig(configParams);
44
45
  }
45
46
 
46
47
  private createWagmiInternalConfig(configParams: ConfigParams): Config {
47
- // Connectors are typically added via wagmiConfig.connectors, but here AppKit manages the connection.
48
- // We'll use the `connect` action with our dynamically created connector instance.
49
- // So, the `connectors` array for createConfig can be empty and is added later.
50
- const initialConnectors: (() => Connector)[] = [];
51
-
52
48
  const transportsArr = configParams.networks.map(chain => [
53
49
  chain.id,
54
50
  getTransport({ chainId: chain.id, projectId: configParams.projectId })
@@ -57,7 +53,7 @@ export class WagmiAdapter extends EVMAdapter {
57
53
 
58
54
  return createConfig({
59
55
  chains: configParams.networks,
60
- connectors: initialConnectors, // Empty, as we connect programmatically
56
+ connectors: [...(configParams.connectors ?? [])],
61
57
  transports,
62
58
  multiInjectedProviderDiscovery: false
63
59
  });
@@ -105,7 +101,7 @@ export class WagmiAdapter extends EVMAdapter {
105
101
  const formattedBalance = {
106
102
  amount: formatUnits(balance.value, balance.decimals),
107
103
  symbol: balance.symbol,
108
- contractAddress: token ? (`${network.caipNetworkId}:${token}` as CaipAddress) : undefined
104
+ address: token ? (`${network.caipNetworkId}:${token}` as CaipAddress) : undefined
109
105
  };
110
106
 
111
107
  this.emit('balanceChanged', { address: balanceAddress, balance: formattedBalance });
@@ -130,6 +126,11 @@ export class WagmiAdapter extends EVMAdapter {
130
126
  }
131
127
 
132
128
  async disconnect(): Promise<void> {
129
+ if (this.unsubscribeWatchAccount) {
130
+ this.unsubscribeWatchAccount();
131
+ this.unsubscribeWatchAccount = undefined;
132
+ }
133
+
133
134
  if (this.wagmiConfigConnector) {
134
135
  await disconnectWagmiCore(this.wagmiConfig, { connector: this.wagmiConfigConnector });
135
136
  this.wagmiConfigConnector = undefined;
@@ -148,8 +149,14 @@ export class WagmiAdapter extends EVMAdapter {
148
149
  return WagmiAdapter.supportedNamespace;
149
150
  }
150
151
 
151
- override setConnector(_connector: WalletConnector): void {
152
- super.setConnector(_connector);
152
+ // Override subscribeToEvents to prevent double subscription
153
+ // Wagmi handles provider events through its connector system and watchAccount
154
+ override subscribeToEvents(): void {
155
+ // Do nothing - wagmi's watchAccount in setupWatchers handles all events
156
+ }
157
+
158
+ override init({ connector: _connector }: BlockchainAdapterInitParams): void {
159
+ super.init({ connector: _connector });
153
160
 
154
161
  if (_connector && this.wagmiChains) {
155
162
  if (!this.wagmiConfigConnector) {
@@ -176,5 +183,39 @@ export class WagmiAdapter extends EVMAdapter {
176
183
  }
177
184
  }
178
185
  }
186
+
187
+ this.setupWatchers();
188
+ }
189
+
190
+ setupWatchers() {
191
+ // Clean up existing subscription if any
192
+ this.unsubscribeWatchAccount?.();
193
+
194
+ this.unsubscribeWatchAccount = watchAccount(this.wagmiConfig, {
195
+ onChange: (accountData, prevAccountData) => {
196
+ if (!this.connector) return;
197
+
198
+ // Handle disconnect
199
+ if (accountData.status === 'disconnected' && prevAccountData.address) {
200
+ this.onDisconnect();
201
+
202
+ return;
203
+ }
204
+
205
+ // Handle account address changes
206
+ if (
207
+ accountData?.addresses &&
208
+ accountData?.address &&
209
+ accountData.address !== prevAccountData?.address
210
+ ) {
211
+ this.onAccountsChanged([...accountData.addresses]);
212
+ }
213
+
214
+ // Handle chain changes
215
+ if (accountData?.chainId && accountData.chainId !== prevAccountData?.chainId) {
216
+ this.onChainChanged(accountData.chainId?.toString());
217
+ }
218
+ }
219
+ });
179
220
  }
180
221
  }
@@ -1,62 +1,75 @@
1
- import type {
2
- Provider,
3
- RequestArguments,
4
- WalletConnector
5
- } from '@reown/appkit-common-react-native';
1
+ import type { Provider, WalletConnector } from '@reown/appkit-common-react-native';
2
+
6
3
  import {
7
4
  getAddress,
8
5
  numberToHex,
6
+ RpcError,
9
7
  SwitchChainError,
10
8
  UserRejectedRequestError,
11
9
  type Hex
12
10
  } from 'viem';
13
- import { ChainNotConfiguredError, createConnector, ProviderNotFoundError } from 'wagmi';
14
- import { formatNetwork } from '../utils/helpers';
11
+ import {
12
+ ChainNotConfiguredError,
13
+ createConnector,
14
+ ProviderNotFoundError,
15
+ type Connector
16
+ } from 'wagmi';
17
+
18
+ type UniversalConnector = Connector & {
19
+ onSessionDelete(data: { topic: string }): void;
20
+ };
21
+
22
+ type Properties = {
23
+ onSessionDelete(data: { topic: string }): void;
24
+ };
15
25
 
16
26
  export function UniversalConnector(appKitProvidedConnector: WalletConnector) {
17
27
  let provider: Provider | undefined;
18
28
 
19
- let accountsChangedHandler: ((accounts: string[]) => void) | undefined;
20
- let chainChangedHandler: ((chainId: string | number) => void) | undefined;
21
- let disconnectHandler: ((error?: Error) => void) | undefined;
29
+ let accountsChanged: UniversalConnector['onAccountsChanged'] | undefined;
30
+ let chainChanged: UniversalConnector['onChainChanged'] | undefined;
31
+ let sessionDelete: UniversalConnector['onSessionDelete'] | undefined;
32
+ let disconnect: UniversalConnector['onDisconnect'] | undefined;
22
33
 
23
- type AppKitConnectorProperties = { ready: boolean };
34
+ function cleanupEventListeners(_provider?: Provider | null) {
35
+ if (accountsChanged) {
36
+ _provider?.off('accountsChanged', accountsChanged);
37
+ accountsChanged = undefined;
38
+ }
39
+ if (chainChanged) {
40
+ _provider?.off('chainChanged', chainChanged);
41
+ chainChanged = undefined;
42
+ }
43
+ if (disconnect) {
44
+ _provider?.off('disconnect', disconnect);
45
+ disconnect = undefined;
46
+ }
47
+ if (sessionDelete) {
48
+ _provider?.off('session_delete', sessionDelete);
49
+ sessionDelete = undefined;
50
+ }
51
+ }
24
52
 
25
- return createConnector<Provider, AppKitConnectorProperties>(config => ({
53
+ return createConnector<Provider, Properties>(config => ({
26
54
  id: 'walletconnect',
27
55
  name: 'WalletConnect',
28
56
  type: 'walletconnect' as const,
29
- ready: !!appKitProvidedConnector.getProvider(),
57
+ ready: !!appKitProvidedConnector.getProvider('eip155'),
30
58
 
31
59
  async setup() {
32
- provider = appKitProvidedConnector.getProvider();
33
- if (provider?.on) {
34
- accountsChangedHandler = (accounts: string[]) => {
35
- const hexAccounts = accounts.map(acc => getAddress(acc));
36
- config.emitter.emit('change', { accounts: hexAccounts });
37
- if (hexAccounts.length === 0) {
38
- config.emitter.emit('disconnect');
39
- }
40
- };
41
- chainChangedHandler = (chainId: string | number) => {
42
- const newChainId = typeof chainId === 'string' ? parseInt(chainId, 10) : chainId;
43
- config.emitter.emit('change', { chainId: newChainId });
44
- };
45
- disconnectHandler = (error?: Error) => {
46
- config.emitter.emit('disconnect');
47
- if (error) config.emitter.emit('error', { error });
48
- };
49
-
50
- if (accountsChangedHandler) provider.on('accountsChanged', accountsChangedHandler);
51
- if (chainChangedHandler) provider.on('chainChanged', chainChangedHandler);
52
- if (disconnectHandler) provider.on('disconnect', disconnectHandler);
53
- if (disconnectHandler) provider.on('session_delete', disconnectHandler);
60
+ const _provider = await this.getProvider().catch(() => null);
61
+ if (!_provider) {
62
+ return;
63
+ }
64
+ if (!sessionDelete) {
65
+ sessionDelete = this.onSessionDelete.bind(this);
66
+ _provider.on('session_delete', sessionDelete);
54
67
  }
55
68
  },
56
69
 
57
70
  async connect({ chainId } = {}) {
58
71
  try {
59
- const _provider = await this.getProvider();
72
+ const _provider = appKitProvidedConnector.getProvider('eip155');
60
73
  if (!_provider) throw new ProviderNotFoundError();
61
74
 
62
75
  // AppKit connector is already connected or handles its own connection.
@@ -75,8 +88,22 @@ export function UniversalConnector(appKitProvidedConnector: WalletConnector) {
75
88
  await this.switchChain?.({ chainId });
76
89
  currentChainId = chainId;
77
90
  }
78
-
79
- this.ready = true;
91
+ if (!accountsChanged) {
92
+ accountsChanged = this.onAccountsChanged.bind(this);
93
+ _provider.on('accountsChanged', accountsChanged);
94
+ }
95
+ if (!chainChanged) {
96
+ chainChanged = this.onChainChanged.bind(this);
97
+ _provider.on('chainChanged', chainChanged);
98
+ }
99
+ if (!disconnect) {
100
+ disconnect = this.onDisconnect.bind(this);
101
+ _provider.on('disconnect', disconnect);
102
+ }
103
+ if (!sessionDelete) {
104
+ sessionDelete = this.onSessionDelete.bind(this);
105
+ _provider.on('session_delete', sessionDelete);
106
+ }
80
107
 
81
108
  return { accounts: accountAddresses, chainId: currentChainId };
82
109
  } catch (error) {
@@ -86,24 +113,22 @@ export function UniversalConnector(appKitProvidedConnector: WalletConnector) {
86
113
  },
87
114
 
88
115
  async disconnect() {
89
- await appKitProvidedConnector.disconnect();
90
- config.emitter.emit('message', { type: 'externalDisconnect' });
91
- if (provider?.off && accountsChangedHandler && chainChangedHandler && disconnectHandler) {
92
- provider.off('accountsChanged', accountsChangedHandler);
93
- provider.off('chainChanged', chainChangedHandler);
94
- provider.off('disconnect', disconnectHandler);
95
- provider.off('session_delete', disconnectHandler);
96
- accountsChangedHandler = undefined;
97
- chainChangedHandler = undefined;
98
- disconnectHandler = undefined;
116
+ const _provider = await this.getProvider().catch(() => null);
117
+ try {
118
+ await appKitProvidedConnector.disconnect();
119
+ config.emitter.emit('message', { type: 'externalDisconnect' });
120
+ } catch (error) {
121
+ if (!/No matching key/i.test((error as Error).message)) {
122
+ throw error;
123
+ }
124
+ } finally {
125
+ cleanupEventListeners(_provider);
99
126
  }
100
- this.ready = false;
101
127
  },
102
128
 
103
129
  async getAccounts() {
104
130
  const namespaces = appKitProvidedConnector.getNamespaces();
105
- // @ts-ignore
106
- const eip155Accounts = namespaces?.eip155?.accounts;
131
+ const eip155Accounts = namespaces?.['eip155']?.accounts as string[] | undefined;
107
132
  if (!eip155Accounts) return [] as readonly Hex[];
108
133
 
109
134
  return eip155Accounts
@@ -123,8 +148,7 @@ export function UniversalConnector(appKitProvidedConnector: WalletConnector) {
123
148
 
124
149
  // Fallback: Try to get from CAIP accounts if available
125
150
  const namespaces = appKitProvidedConnector.getNamespaces();
126
- // @ts-ignore
127
- const eip155Accounts = namespaces?.eip155?.accounts;
151
+ const eip155Accounts = namespaces?.['eip155']?.accounts as string[] | undefined;
128
152
  if (eip155Accounts && eip155Accounts.length > 0) {
129
153
  const parts = eip155Accounts[0]?.split(':');
130
154
  if (parts && parts.length > 1 && typeof parts[1] === 'string') {
@@ -140,20 +164,10 @@ export function UniversalConnector(appKitProvidedConnector: WalletConnector) {
140
164
 
141
165
  async getProvider() {
142
166
  if (!provider) {
143
- provider = appKitProvidedConnector.getProvider();
167
+ provider = appKitProvidedConnector.getProvider('eip155');
144
168
  }
145
169
 
146
- const chainId = await this.getChainId();
147
-
148
- //TODO: Review this with gancho
149
- const _provider = {
150
- ...provider,
151
- request: (args: RequestArguments) => {
152
- return provider?.request(args, `eip155:${chainId}`);
153
- }
154
- };
155
-
156
- return Promise.resolve(_provider as Provider);
170
+ return provider;
157
171
  },
158
172
 
159
173
  async isAuthorized() {
@@ -167,7 +181,7 @@ export function UniversalConnector(appKitProvidedConnector: WalletConnector) {
167
181
  },
168
182
 
169
183
  async switchChain({ chainId }) {
170
- const _provider = await this.getProvider();
184
+ const _provider = appKitProvidedConnector.getProvider('eip155');
171
185
  if (!_provider) throw new Error('Provider not available for switching chain.');
172
186
  const newChain = config.chains.find(c => c.id === chainId);
173
187
 
@@ -179,28 +193,27 @@ export function UniversalConnector(appKitProvidedConnector: WalletConnector) {
179
193
  params: [{ chainId: numberToHex(chainId) }]
180
194
  });
181
195
 
182
- config.emitter.emit('change', { chainId });
183
-
184
196
  return newChain;
185
- } catch (error) {
186
- // Try to add chain if switch failed (common pattern)
187
- //4902 in MetaMask: Unrecognized chain ID
197
+ } catch (err) {
198
+ const error = err as RpcError;
199
+
200
+ if (/(user rejected)/i.test(error.message)) throw new UserRejectedRequestError(error);
201
+
188
202
  if ((error as any)?.code === 4902 || (error as any)?.data?.originalError?.code === 4902) {
203
+ // Indicates chain is not added to provider
189
204
  try {
205
+ const addEthereumChainParams = {
206
+ chainId: numberToHex(chainId),
207
+ chainName: newChain.name,
208
+ nativeCurrency: newChain.nativeCurrency,
209
+ rpcUrls: [newChain.rpcUrls.default?.http[0] ?? ''],
210
+ blockExplorerUrls: [newChain.blockExplorers?.default?.url]
211
+ };
212
+
190
213
  await _provider.request({
191
214
  method: 'wallet_addEthereumChain',
192
- params: [
193
- {
194
- chainId: numberToHex(chainId),
195
- chainName: newChain.name,
196
- nativeCurrency: newChain.nativeCurrency,
197
- rpcUrls: [newChain.rpcUrls.default?.http[0] ?? ''], // Take first default HTTP RPC URL
198
- blockExplorerUrls: [newChain.blockExplorers?.default?.url]
199
- }
200
- ]
215
+ params: [addEthereumChainParams]
201
216
  });
202
- await appKitProvidedConnector.switchNetwork(formatNetwork(newChain));
203
- config.emitter.emit('change', { chainId });
204
217
 
205
218
  return newChain;
206
219
  } catch (addError) {
@@ -212,17 +225,41 @@ export function UniversalConnector(appKitProvidedConnector: WalletConnector) {
212
225
  },
213
226
 
214
227
  onAccountsChanged(accounts: string[]) {
215
- if (accounts.length === 0) this.onDisconnect();
216
- else config.emitter.emit('change', { accounts: accounts.map(x => getAddress(x)) });
228
+ //Only emit if the account is an evm account
229
+ const shouldEmit = accounts.some(account => account.startsWith('0x'));
230
+
231
+ if (accounts.length === 0) {
232
+ this.onDisconnect();
233
+ } else if (shouldEmit) {
234
+ config.emitter.emit('change', { accounts: accounts.map(x => getAddress(x)) });
235
+ }
217
236
  },
218
237
 
219
238
  onChainChanged(chain: string) {
220
239
  const chainId = Number(chain);
221
- config.emitter.emit('change', { chainId });
240
+
241
+ //Only emit if the chain is in the config (evm)
242
+ const shouldEmit = config.chains.some(c => c.id === chainId);
243
+ if (shouldEmit) {
244
+ config.emitter.emit('change', { chainId });
245
+ }
222
246
  },
223
247
 
224
- onDisconnect: () => {
248
+ async onDisconnect() {
225
249
  config.emitter.emit('disconnect');
250
+
251
+ try {
252
+ const _provider = await this.getProvider();
253
+ cleanupEventListeners(_provider);
254
+ } catch (error) {
255
+ // If provider is not available, still clean up local references
256
+ // to prevent memory leaks
257
+ cleanupEventListeners(null);
258
+ }
259
+ },
260
+
261
+ onSessionDelete() {
262
+ this.onDisconnect();
226
263
  }
227
264
  }));
228
265
  }
@@ -1,14 +1,13 @@
1
- import { CoreHelperUtil } from '@reown/appkit-react-native';
1
+ import { http } from 'viem';
2
2
  import {
3
3
  PresetsUtil,
4
4
  ConstantsUtil,
5
5
  type AppKitNetwork,
6
6
  type Network
7
7
  } from '@reown/appkit-common-react-native';
8
- import { http } from 'viem';
9
8
 
10
9
  export function getTransport({ chainId, projectId }: { chainId: number; projectId: string }) {
11
- const RPC_URL = CoreHelperUtil.getBlockchainApiUrl();
10
+ const RPC_URL = ConstantsUtil.BLOCKCHAIN_API_RPC_URL;
12
11
 
13
12
  if (!PresetsUtil.RpcChainIds.includes(chainId)) {
14
13
  return http();