@reown/appkit-wagmi-react-native 0.0.0-accounts-canary.1-20251023174733

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 ADDED
@@ -0,0 +1,227 @@
1
+ import {
2
+ EVMAdapter,
3
+ type AppKitNetwork,
4
+ type BlockchainAdapterInitParams,
5
+ type CaipAddress,
6
+ type ChainNamespace,
7
+ type GetBalanceParams,
8
+ type GetBalanceResponse
9
+ } from '@reown/appkit-common-react-native';
10
+ import {
11
+ type Config,
12
+ type CreateConfigParameters,
13
+ createConfig,
14
+ getBalance as getBalanceWagmi,
15
+ switchChain as switchChainWagmi,
16
+ disconnect as disconnectWagmiCore,
17
+ connect as connectWagmi,
18
+ type Connector,
19
+ watchAccount
20
+ } from '@wagmi/core';
21
+ import type { Chain } from 'wagmi/chains';
22
+ import { getTransport } from './utils/helpers';
23
+ import { formatUnits, type Hex } from 'viem';
24
+ import { UniversalConnector } from './connectors/UniversalConnector';
25
+
26
+ type ConfigParams = Partial<CreateConfigParameters> & {
27
+ networks: readonly [Chain, ...Chain[]];
28
+ projectId: string;
29
+ connectors?: Connector[];
30
+ };
31
+
32
+ export class WagmiAdapter extends EVMAdapter {
33
+ private static supportedNamespace: ChainNamespace = 'eip155';
34
+ public wagmiChains: readonly Chain[] | undefined;
35
+ public wagmiConfig!: Config;
36
+ private wagmiConfigConnector?: Connector;
37
+ private unsubscribeWatchAccount?: () => void;
38
+
39
+ constructor(configParams: ConfigParams) {
40
+ super({
41
+ supportedNamespace: WagmiAdapter.supportedNamespace,
42
+ adapterType: 'wagmi'
43
+ });
44
+ this.wagmiChains = configParams.networks;
45
+ this.wagmiConfig = this.createWagmiInternalConfig(configParams);
46
+ }
47
+
48
+ private createWagmiInternalConfig(configParams: ConfigParams): Config {
49
+ // Connectors are typically added via wagmiConfig.connectors, but here AppKit manages the connection.
50
+ // We'll use the `connect` action with our dynamically created connector instance.
51
+ // So, the `connectors` array for createConfig can be empty and is added later.
52
+ const initialConnectors: (() => Connector)[] = [];
53
+
54
+ const transportsArr = configParams.networks.map(chain => [
55
+ chain.id,
56
+ getTransport({ chainId: chain.id, projectId: configParams.projectId })
57
+ ]);
58
+ const transports = Object.fromEntries(transportsArr);
59
+
60
+ return createConfig({
61
+ chains: configParams.networks,
62
+ connectors: initialConnectors, // Empty, as we connect programmatically
63
+ transports,
64
+ multiInjectedProviderDiscovery: false
65
+ });
66
+ }
67
+
68
+ async switchNetwork(network: AppKitNetwork): Promise<void> {
69
+ if (!this.wagmiConfigConnector) {
70
+ throw new Error('WagmiAdapter: AppKit connector not set or not connected via Wagmi.');
71
+ }
72
+
73
+ await switchChainWagmi(this.wagmiConfig, {
74
+ chainId: network.id as number,
75
+ connector: this.wagmiConfigConnector
76
+ });
77
+ }
78
+
79
+ async getBalance(params: GetBalanceParams): Promise<GetBalanceResponse> {
80
+ const { network, address, tokens } = params;
81
+
82
+ if (!this.connector) throw new Error('No active AppKit connector (EVMAdapter.connector)');
83
+ if (!network) throw new Error('No network provided');
84
+
85
+ if (!this.wagmiConfigConnector) {
86
+ throw new Error('WagmiAdapter: AppKit connector not properly configured with Wagmi.');
87
+ }
88
+
89
+ const balanceAddress =
90
+ address ||
91
+ this.getAccounts()?.find((acc: CaipAddress) => acc.includes(network.id.toString()));
92
+
93
+ if (!balanceAddress) {
94
+ return Promise.resolve({ amount: '0.00', symbol: network.nativeCurrency.symbol || 'ETH' });
95
+ }
96
+
97
+ const accountHex = balanceAddress.split(':')[2] as Hex;
98
+
99
+ const token = network?.caipNetworkId && (tokens?.[network.caipNetworkId]?.address as Hex);
100
+
101
+ const balance = await getBalanceWagmi(this.wagmiConfig, {
102
+ address: accountHex,
103
+ chainId: network.id as number,
104
+ token
105
+ });
106
+
107
+ const formattedBalance = {
108
+ amount: formatUnits(balance.value, balance.decimals),
109
+ symbol: balance.symbol,
110
+ address: token ? (`${network.caipNetworkId}:${token}` as CaipAddress) : undefined
111
+ };
112
+
113
+ this.emit('balanceChanged', { address: balanceAddress, balance: formattedBalance });
114
+
115
+ return Promise.resolve(formattedBalance);
116
+ }
117
+
118
+ getAccounts(): CaipAddress[] | undefined {
119
+ if (!this.connector) {
120
+ return undefined;
121
+ }
122
+
123
+ const namespaces = this.connector.getNamespaces();
124
+ if (!namespaces) {
125
+ return undefined;
126
+ }
127
+
128
+ const supportedNamespaceKey = this.getSupportedNamespace();
129
+ const accountsForNamespace = namespaces[supportedNamespaceKey];
130
+
131
+ return accountsForNamespace?.accounts;
132
+ }
133
+
134
+ async disconnect(): Promise<void> {
135
+ if (this.unsubscribeWatchAccount) {
136
+ this.unsubscribeWatchAccount();
137
+ this.unsubscribeWatchAccount = undefined;
138
+ }
139
+
140
+ if (this.wagmiConfigConnector) {
141
+ await disconnectWagmiCore(this.wagmiConfig, { connector: this.wagmiConfigConnector });
142
+ this.wagmiConfigConnector = undefined;
143
+ } else if (this.connector) {
144
+ await this.connector.disconnect();
145
+ this.onDisconnect();
146
+ }
147
+
148
+ const evmAdapterInstance = this as any;
149
+ if ('connector' in evmAdapterInstance) {
150
+ evmAdapterInstance.connector = undefined;
151
+ }
152
+ }
153
+
154
+ getSupportedNamespace(): ChainNamespace {
155
+ return WagmiAdapter.supportedNamespace;
156
+ }
157
+
158
+ // Override subscribeToEvents to prevent double subscription
159
+ // Wagmi handles provider events through its connector system and watchAccount
160
+ override subscribeToEvents(): void {
161
+ // Do nothing - wagmi's watchAccount in setupWatchers handles all events
162
+ }
163
+
164
+ override init({ connector: _connector }: BlockchainAdapterInitParams): void {
165
+ super.init({ connector: _connector });
166
+
167
+ if (_connector && this.wagmiChains) {
168
+ if (!this.wagmiConfigConnector) {
169
+ // Manually add the connector to the wagmiConfig
170
+ const connectorInstance = this.wagmiConfig._internal.connectors.setup(
171
+ UniversalConnector(_connector)
172
+ );
173
+
174
+ this.wagmiConfig._internal.connectors.setState(prev => [...prev, connectorInstance]);
175
+ this.wagmiConfigConnector = connectorInstance;
176
+
177
+ connectorInstance.emitter.on('message', ({ type }: { type: string }) => {
178
+ if (type === 'externalDisconnect') {
179
+ this.onDisconnect();
180
+
181
+ this.wagmiConfigConnector = undefined;
182
+ }
183
+ });
184
+
185
+ try {
186
+ connectWagmi(this.wagmiConfig, { connector: connectorInstance });
187
+ } catch (error) {
188
+ this.wagmiConfigConnector = undefined;
189
+ }
190
+ }
191
+ }
192
+
193
+ this.setupWatchers();
194
+ }
195
+
196
+ setupWatchers() {
197
+ // Clean up existing subscription if any
198
+ this.unsubscribeWatchAccount?.();
199
+
200
+ this.unsubscribeWatchAccount = watchAccount(this.wagmiConfig, {
201
+ onChange: (accountData, prevAccountData) => {
202
+ if (!this.connector) return;
203
+
204
+ // Handle disconnect
205
+ if (accountData.status === 'disconnected' && prevAccountData.address) {
206
+ this.onDisconnect();
207
+
208
+ return;
209
+ }
210
+
211
+ // Handle account address changes
212
+ if (
213
+ accountData?.addresses &&
214
+ accountData?.address &&
215
+ accountData.address !== prevAccountData?.address
216
+ ) {
217
+ this.onAccountsChanged([...accountData.addresses]);
218
+ }
219
+
220
+ // Handle chain changes
221
+ if (accountData?.chainId && accountData.chainId !== prevAccountData?.chainId) {
222
+ this.onChainChanged(accountData.chainId?.toString());
223
+ }
224
+ }
225
+ });
226
+ }
227
+ }
@@ -0,0 +1,265 @@
1
+ import type { Provider, WalletConnector } from '@reown/appkit-common-react-native';
2
+
3
+ import {
4
+ getAddress,
5
+ numberToHex,
6
+ RpcError,
7
+ SwitchChainError,
8
+ UserRejectedRequestError,
9
+ type Hex
10
+ } from 'viem';
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
+ };
25
+
26
+ export function UniversalConnector(appKitProvidedConnector: WalletConnector) {
27
+ let provider: Provider | undefined;
28
+
29
+ let accountsChanged: UniversalConnector['onAccountsChanged'] | undefined;
30
+ let chainChanged: UniversalConnector['onChainChanged'] | undefined;
31
+ let sessionDelete: UniversalConnector['onSessionDelete'] | undefined;
32
+ let disconnect: UniversalConnector['onDisconnect'] | undefined;
33
+
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
+ }
52
+
53
+ return createConnector<Provider, Properties>(config => ({
54
+ id: 'walletconnect',
55
+ name: 'WalletConnect',
56
+ type: 'walletconnect' as const,
57
+ ready: !!appKitProvidedConnector.getProvider('eip155'),
58
+
59
+ async setup() {
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);
67
+ }
68
+ },
69
+
70
+ async connect({ chainId } = {}) {
71
+ try {
72
+ const _provider = appKitProvidedConnector.getProvider('eip155');
73
+ if (!_provider) throw new ProviderNotFoundError();
74
+
75
+ // AppKit connector is already connected or handles its own connection.
76
+ // We just need to sync its state with Wagmi.
77
+ const accountAddresses = await this.getAccounts();
78
+ if (!accountAddresses || accountAddresses.length === 0) {
79
+ throw new UserRejectedRequestError(
80
+ new Error('No accounts found or user rejected connection via AppKit.')
81
+ );
82
+ }
83
+
84
+ let currentChainId = await this.getChainId();
85
+
86
+ // Handle chain switching if requested and different
87
+ if (chainId && currentChainId !== chainId) {
88
+ await this.switchChain?.({ chainId });
89
+ currentChainId = chainId;
90
+ }
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
+ }
107
+
108
+ return { accounts: accountAddresses, chainId: currentChainId };
109
+ } catch (error) {
110
+ if (error instanceof UserRejectedRequestError) throw error;
111
+ throw new UserRejectedRequestError(error as Error); // Generalize other errors as user rejection for simplicity
112
+ }
113
+ },
114
+
115
+ async disconnect() {
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);
126
+ }
127
+ },
128
+
129
+ async getAccounts() {
130
+ const namespaces = appKitProvidedConnector.getNamespaces();
131
+ const eip155Accounts = namespaces?.['eip155']?.accounts as string[] | undefined;
132
+ if (!eip155Accounts) return [] as readonly Hex[];
133
+
134
+ return eip155Accounts
135
+ .map((caipAddr: string) => {
136
+ const parts = caipAddr.split(':');
137
+
138
+ return parts.length === 3 ? parts[2] : null;
139
+ })
140
+ .filter((addrPart): addrPart is string => !!addrPart)
141
+ .map((addrPart: string) => getAddress(addrPart)) as readonly Hex[];
142
+ },
143
+
144
+ async getChainId() {
145
+ const chainId = appKitProvidedConnector.getChainId('eip155')?.split(':')[1];
146
+
147
+ if (chainId) return parseInt(chainId, 10);
148
+
149
+ // Fallback: Try to get from CAIP accounts if available
150
+ const namespaces = appKitProvidedConnector.getNamespaces();
151
+ const eip155Accounts = namespaces?.['eip155']?.accounts as string[] | undefined;
152
+ if (eip155Accounts && eip155Accounts.length > 0) {
153
+ const parts = eip155Accounts[0]?.split(':');
154
+ if (parts && parts.length > 1 && typeof parts[1] === 'string') {
155
+ const chainIdNum = parseInt(parts[1], 10);
156
+ if (!isNaN(chainIdNum)) {
157
+ return chainIdNum;
158
+ }
159
+ }
160
+ }
161
+ if (config.chains && config.chains.length > 0) return config.chains[0].id;
162
+ throw new Error('Unable to determine chainId.');
163
+ },
164
+
165
+ async getProvider() {
166
+ if (!provider) {
167
+ provider = appKitProvidedConnector.getProvider('eip155');
168
+ }
169
+
170
+ return provider;
171
+ },
172
+
173
+ async isAuthorized() {
174
+ try {
175
+ const accounts = await this.getAccounts();
176
+
177
+ return !!(accounts && accounts.length > 0);
178
+ } catch {
179
+ return false;
180
+ }
181
+ },
182
+
183
+ async switchChain({ chainId }) {
184
+ const _provider = appKitProvidedConnector.getProvider('eip155');
185
+ if (!_provider) throw new Error('Provider not available for switching chain.');
186
+ const newChain = config.chains.find(c => c.id === chainId);
187
+
188
+ if (!newChain) throw new SwitchChainError(new ChainNotConfiguredError());
189
+
190
+ try {
191
+ await _provider.request({
192
+ method: 'wallet_switchEthereumChain',
193
+ params: [{ chainId: numberToHex(chainId) }]
194
+ });
195
+
196
+ return newChain;
197
+ } catch (err) {
198
+ const error = err as RpcError;
199
+
200
+ if (/(user rejected)/i.test(error.message)) throw new UserRejectedRequestError(error);
201
+
202
+ if ((error as any)?.code === 4902 || (error as any)?.data?.originalError?.code === 4902) {
203
+ // Indicates chain is not added to provider
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
+
213
+ await _provider.request({
214
+ method: 'wallet_addEthereumChain',
215
+ params: [addEthereumChainParams]
216
+ });
217
+
218
+ return newChain;
219
+ } catch (addError) {
220
+ throw new UserRejectedRequestError(addError as Error);
221
+ }
222
+ }
223
+ throw new SwitchChainError(error as Error);
224
+ }
225
+ },
226
+
227
+ onAccountsChanged(accounts: string[]) {
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
+ }
236
+ },
237
+
238
+ onChainChanged(chain: string) {
239
+ const chainId = Number(chain);
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
+ }
246
+ },
247
+
248
+ async onDisconnect() {
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();
263
+ }
264
+ }));
265
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,4 @@
1
+ import { WagmiAdapter } from './adapter';
2
+
3
+ export { WagmiAdapter };
4
+ export { formatNetworks, formatNetwork } from './utils/helpers';
@@ -0,0 +1,30 @@
1
+ import { http } from 'viem';
2
+ import {
3
+ PresetsUtil,
4
+ ConstantsUtil,
5
+ type AppKitNetwork,
6
+ type Network
7
+ } from '@reown/appkit-common-react-native';
8
+
9
+ export function getTransport({ chainId, projectId }: { chainId: number; projectId: string }) {
10
+ const RPC_URL = ConstantsUtil.BLOCKCHAIN_API_RPC_URL;
11
+
12
+ if (!PresetsUtil.RpcChainIds.includes(chainId)) {
13
+ return http();
14
+ }
15
+
16
+ return http(`${RPC_URL}/v1/?chainId=${ConstantsUtil.EIP155}:${chainId}&projectId=${projectId}`);
17
+ }
18
+
19
+ export function formatNetwork(network: Network): AppKitNetwork {
20
+ return {
21
+ ...network,
22
+ rpcUrls: { ...network.rpcUrls },
23
+ chainNamespace: 'eip155',
24
+ caipNetworkId: `eip155:${network.id}`
25
+ };
26
+ }
27
+
28
+ export function formatNetworks(networks: Network[]): AppKitNetwork[] {
29
+ return networks.map(formatNetwork);
30
+ }