@rango-dev/wallets-react 0.0.0-experimental-c6bea4d8-20260930 → 0.0.0-experimental-767249ce-20261007

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/events.ts ADDED
@@ -0,0 +1,99 @@
1
+ import type { WalletType } from '@hub3js/core';
2
+ import type { Namespace } from '@hub3js/namespaces';
3
+ import type { BlockchainMeta } from 'rango-types';
4
+
5
+ import { getBlockChainNameFromId } from '@rango-dev/internal-blockchains';
6
+
7
+ import { WALLET_DETECTED_SKIPPED_IDS } from './hub/constants.js';
8
+
9
+ export const WalletEventChannel = 'walletEvent';
10
+
11
+ export enum WalletEventTypes {
12
+ WALLET_DETECTED = 'walletDetected',
13
+ WALLET_CONNECT_INITIATED = 'walletConnectInitiated',
14
+ WALLET_CONNECTED = 'walletConnected',
15
+ WALLET_CONNECT_FAILED = 'walletConnectFailed',
16
+ WALLET_DISCONNECTED = 'walletDisconnected',
17
+ }
18
+
19
+ export type WalletConnectOrigin = 'manual' | 'auto';
20
+
21
+ type WalletConnectEventPayload = {
22
+ /** Wallet type id, e.g. `metamask`. */
23
+ walletName: WalletType;
24
+ namespace: Namespace;
25
+ /** Rango blockchain name (e.g. `ETH`), or `null` when it isn't known. */
26
+ chain: string | null;
27
+ origin: WalletConnectOrigin;
28
+ };
29
+
30
+ export type WalletDetectedEventPayload = {
31
+ walletName: WalletType;
32
+ };
33
+ export type WalletConnectInitiatedEventPayload = WalletConnectEventPayload;
34
+ export type WalletConnectedEventPayload = WalletConnectEventPayload;
35
+ export type WalletConnectFailedEventPayload = WalletConnectEventPayload;
36
+ export type WalletDisconnectedEventPayload = {
37
+ walletName: WalletType;
38
+ namespace: Namespace;
39
+ };
40
+
41
+ type EventData<T extends WalletEventTypes, U> = { type: T; payload: U };
42
+
43
+ export type WalletEventData =
44
+ | EventData<WalletEventTypes.WALLET_DETECTED, WalletDetectedEventPayload>
45
+ | EventData<
46
+ WalletEventTypes.WALLET_CONNECT_INITIATED,
47
+ WalletConnectInitiatedEventPayload
48
+ >
49
+ | EventData<WalletEventTypes.WALLET_CONNECTED, WalletConnectedEventPayload>
50
+ | EventData<
51
+ WalletEventTypes.WALLET_CONNECT_FAILED,
52
+ WalletConnectFailedEventPayload
53
+ >
54
+ | EventData<
55
+ WalletEventTypes.WALLET_DISCONNECTED,
56
+ WalletDisconnectedEventPayload
57
+ >;
58
+
59
+ export type WalletEvents = {
60
+ [WalletEventChannel]: WalletEventData;
61
+ };
62
+
63
+ export interface Emitter<Events extends Record<string, unknown>> {
64
+ emit<K extends keyof Events>(type: K, event: Events[K]): void;
65
+ }
66
+
67
+ export type WalletEventEmitter = Emitter<WalletEvents>;
68
+
69
+ const detectedWallets = new Set<WalletType>();
70
+
71
+ export function emitWalletDetectedOnce(
72
+ walletName: WalletType,
73
+ emitter?: WalletEventEmitter
74
+ ): void {
75
+ if (
76
+ detectedWallets.has(walletName) ||
77
+ WALLET_DETECTED_SKIPPED_IDS.includes(walletName)
78
+ ) {
79
+ return;
80
+ }
81
+ detectedWallets.add(walletName);
82
+ emitter?.emit(WalletEventChannel, {
83
+ type: WalletEventTypes.WALLET_DETECTED,
84
+ payload: { walletName },
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Connect results may carry an EVM chain id (e.g. `0x1`); events always carry a Rango blockchain name.
90
+ */
91
+ export function toBlockchainName(
92
+ network: string | null | undefined,
93
+ allBlockChains: BlockchainMeta[] | undefined
94
+ ): string | null {
95
+ if (!network) {
96
+ return null;
97
+ }
98
+ return getBlockChainNameFromId(network, allBlockChains || []);
99
+ }
@@ -7,15 +7,34 @@ import type { Accounts, AccountsWithActiveChain } from '@hub3js/std/types';
7
7
 
8
8
  import { Result } from 'ts-results';
9
9
 
10
+ import {
11
+ toBlockchainName,
12
+ WalletEventChannel,
13
+ type WalletEventEmitter,
14
+ WalletEventTypes,
15
+ } from '../events.js';
16
+
10
17
  import { HUB_LAST_CONNECTED_WALLETS } from './constants.js';
11
- import { reportConnectionFailure } from './errors.js';
12
18
  import { runSequentiallyWithoutFailure } from './helpers.js';
13
19
  import { LastConnectedWalletsFromStorage } from './lastConnectedWallets.js';
14
20
  import {
15
21
  convertNamespaceNetworkToEvmChainId,
16
22
  isEvmNamespace,
23
+ transformHubResultToLegacyResult,
17
24
  } from './utils.js';
18
25
 
26
+ function createAutoConnectEventPayload(
27
+ walletName: WalletType,
28
+ namespaceInput: NamespaceInputForConnect
29
+ ) {
30
+ return {
31
+ walletName,
32
+ namespace: namespaceInput.namespace,
33
+ chain: namespaceInput.network || null,
34
+ origin: 'auto' as const,
35
+ };
36
+ }
37
+
19
38
  function isEvmNamespaceInput(
20
39
  namespace: NamespaceInputForConnect
21
40
  ): namespace is NamespaceInputForConnect<'EVM'> {
@@ -36,9 +55,10 @@ async function eagerConnect(
36
55
  params: {
37
56
  getHub: () => Hub;
38
57
  allBlockChains: UseAdapterParams['allBlockChains'];
58
+ emitter?: WalletEventEmitter;
39
59
  }
40
60
  ) {
41
- const { getHub, allBlockChains } = params;
61
+ const { getHub, allBlockChains, emitter } = params;
42
62
  const wallet = getHub().get(type);
43
63
  if (!wallet) {
44
64
  throw new Error(
@@ -82,14 +102,24 @@ async function eagerConnect(
82
102
  } else {
83
103
  connectNamespacePromise = async () => namespace.connect();
84
104
  }
105
+ const eventPayload = createAutoConnectEventPayload(type, info);
85
106
  try {
86
- await connectNamespacePromise();
107
+ const result = await connectNamespacePromise();
108
+ emitter?.emit(WalletEventChannel, {
109
+ type: WalletEventTypes.WALLET_CONNECTED,
110
+ payload: {
111
+ ...eventPayload,
112
+ chain: toBlockchainName(
113
+ transformHubResultToLegacyResult(result).network,
114
+ allBlockChains
115
+ ),
116
+ },
117
+ });
87
118
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
119
  } catch (e: any) {
89
- reportConnectionFailure(e, {
90
- walletType: type,
91
- namespace: info.namespace,
92
- origin: 'auto',
120
+ emitter?.emit(WalletEventChannel, {
121
+ type: WalletEventTypes.WALLET_CONNECT_FAILED,
122
+ payload: eventPayload,
93
123
  });
94
124
  /*
95
125
  * Since we check for connect failures using `instanceof Error`
@@ -188,8 +218,9 @@ export async function autoConnect(deps: {
188
218
  getHub: () => Hub;
189
219
  allBlockChains: UseAdapterParams['allBlockChains'];
190
220
  wallets?: (WalletType | Provider)[];
221
+ emitter?: WalletEventEmitter;
191
222
  }): Promise<void> {
192
- const { getHub, allBlockChains, wallets } = deps;
223
+ const { getHub, allBlockChains, wallets, emitter } = deps;
193
224
  const lastConnectedWallets = lastConnectedWalletsFromStorage.list();
194
225
  const walletIds = Object.keys(lastConnectedWallets);
195
226
 
@@ -225,9 +256,23 @@ export async function autoConnect(deps: {
225
256
  return;
226
257
  }
227
258
 
259
+ lastConnectedNamespaces.forEach((namespace) => {
260
+ emitter?.emit(WalletEventChannel, {
261
+ type: WalletEventTypes.WALLET_CONNECT_INITIATED,
262
+ payload: createAutoConnectEventPayload(providerName, namespace),
263
+ });
264
+ });
265
+
228
266
  const { successNamespaces, failedNamespaces } =
229
267
  await tryRunCanEagerConnect(lastConnectedNamespaces, wallet);
230
268
 
269
+ failedNamespaces.forEach((namespace) => {
270
+ emitter?.emit(WalletEventChannel, {
271
+ type: WalletEventTypes.WALLET_CONNECT_FAILED,
272
+ payload: createAutoConnectEventPayload(providerName, namespace),
273
+ });
274
+ });
275
+
231
276
  if (!successNamespaces.length) {
232
277
  walletsToRemoveFromPersistence.push(providerName);
233
278
  return;
@@ -241,6 +286,7 @@ export async function autoConnect(deps: {
241
286
  eagerConnect(providerName, successNamespaces, {
242
287
  allBlockChains,
243
288
  getHub,
289
+ emitter,
244
290
  }).catch((error) => console.warn(error))
245
291
  );
246
292
  });
@@ -1 +1,13 @@
1
+ import { WalletTypes } from '@rango-dev/provider-all';
2
+
1
3
  export const HUB_LAST_CONNECTED_WALLETS = 'hub-v1-last-connected-wallets';
4
+
5
+ // Wallets that always report themselves installed (hardware wallets and connection protocols).
6
+ export const WALLET_DETECTED_SKIPPED_IDS: string[] = [
7
+ WalletTypes.LEDGER,
8
+ WalletTypes.TREZOR,
9
+ WalletTypes.LEDGER_WALLET,
10
+ WalletTypes.WALLET_CONNECT_2,
11
+ WalletTypes.TON_CONNECT,
12
+ WalletTypes.DEFAULT,
13
+ ];
@@ -10,7 +10,6 @@ import {
10
10
  CAIP_LITECOIN_CHAIN_ID,
11
11
  CAIP_ZCASH_CHAIN_ID,
12
12
  } from '@hub3js/bip122';
13
- import { ConnectionErrorType, WalletConnectionError } from '@hub3js/std/utils';
14
13
  import { CAIP_TRON_CHAIN_ID } from '@hub3js/tron';
15
14
  import { CAIP_NAMESPACE as CAIP_TON_NAMESPACE } from '@hub3js/tvm';
16
15
  import { formatAddressWithNetwork } from '@rango-dev/internal-blockchains';
@@ -182,19 +181,6 @@ export function createQueue(options?: {
182
181
  return queueTask;
183
182
  }
184
183
 
185
- // Once the user rejects one of a wallet's namespaces, its remaining queued namespaces are abandoned.
186
- export function removeQueuedNamespacesOnRejection(
187
- error: unknown,
188
- actions: { removeCurrentKeyFromQueue: () => void }
189
- ) {
190
- if (
191
- error instanceof WalletConnectionError &&
192
- error.type === ConnectionErrorType.Rejected
193
- ) {
194
- actions.removeCurrentKeyFromQueue();
195
- }
196
- }
197
-
198
184
  export function shouldTryAutoConnect(
199
185
  props: Pick<ProviderProps, 'allBlockChains' | 'autoConnect'>
200
186
  ): boolean {
@@ -1,13 +1,16 @@
1
1
  import type { AllProxiedNamespaces, ExtensionLink } from './types.js';
2
2
  import type {
3
3
  ConnectResult,
4
+ EventHandler,
4
5
  NamespaceInputForConnect,
5
6
  WalletInfo,
6
7
  } from '../legacy/mod.js';
7
8
  import type { ProviderContext, ProviderProps } from '../types.js';
8
9
  import type { Provider, WalletType } from '@hub3js/core';
10
+ import type { Event } from '@hub3js/core/store';
9
11
  import type { Accounts, AccountsWithActiveChain } from '@hub3js/std/types';
10
12
 
13
+ import { utils } from '@hub3js/evm';
11
14
  import {
12
15
  getSupportedChainsFromNamespace,
13
16
  getSupportedChainsFromProvider,
@@ -15,21 +18,22 @@ import {
15
18
  import { useEffect, useRef, useState } from 'react';
16
19
  import { Ok, Result } from 'ts-results';
17
20
 
21
+ import {
22
+ emitWalletDetectedOnce,
23
+ toBlockchainName,
24
+ WalletEventChannel,
25
+ WalletEventTypes,
26
+ } from '../events.js';
18
27
  import { withErrorLoggingApi } from '../helpers.js';
28
+ import { Events } from '../legacy/mod.js';
19
29
 
20
30
  import { autoConnect } from './autoConnect.js';
21
31
  import { HUB_LAST_CONNECTED_WALLETS } from './constants.js';
22
- import { reportConnectionFailure } from './errors.js';
23
- import {
24
- createQueue,
25
- fromAccountIdToLegacyAddressFormat,
26
- removeQueuedNamespacesOnRejection,
27
- } from './helpers.js';
32
+ import { createQueue, fromAccountIdToLegacyAddressFormat } from './helpers.js';
28
33
  import { LastConnectedWalletsFromStorage } from './lastConnectedWallets.js';
29
34
  import { useAutoConnect } from './useAutoConnect.js';
30
35
  import { useHubRefs } from './useHubRefs.js';
31
36
  import {
32
- findProviderByType,
33
37
  isEvmNamespace,
34
38
  isSolanaNamespace,
35
39
  isUtxoNamespace,
@@ -49,6 +53,7 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext {
49
53
  const dataRef = useRef({
50
54
  onUpdateState: params.onUpdateState,
51
55
  allBlockChains: params.allBlockChains,
56
+ emitter: params.emitter,
52
57
  });
53
58
  const hubInitiated = useRef(false);
54
59
 
@@ -68,37 +73,42 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext {
68
73
  };
69
74
 
70
75
  const queueTask = createQueue({
71
- onError: removeQueuedNamespacesOnRejection,
76
+ onError: (error, actions) => {
77
+ if (utils.isUserRejectionError(error)) {
78
+ actions.removeCurrentKeyFromQueue();
79
+ }
80
+ },
72
81
  });
73
82
 
74
83
  useEffect(() => {
75
84
  dataRef.current = {
76
85
  onUpdateState: params.onUpdateState,
77
86
  allBlockChains: params.allBlockChains,
87
+ emitter: params.emitter,
78
88
  };
79
89
  }, [params]);
80
90
 
81
91
  useEffect(() => {
82
- getStore()
83
- .subscribe((event) => {
84
- if (dataRef.current.onUpdateState) {
85
- try {
86
- mapHubEventsToLegacy(
87
- getHub(),
88
- event,
89
- dataRef.current.onUpdateState,
90
- {
91
- allBlockChains: dataRef.current.allBlockChains,
92
- lastConnectAttemptParams: lastConnectAttemptParamsRef.current,
93
- }
94
- );
95
- } catch (e) {
96
- console.error(e);
97
- }
98
- }
99
- rerender((currentRender) => currentRender + 1);
100
- })
101
- .flushEvents();
92
+ const onUpdateState: EventHandler = (type, event, value, state, info) => {
93
+ if (event === Events.INSTALLED && state.installed) {
94
+ emitWalletDetectedOnce(type, dataRef.current.emitter);
95
+ }
96
+ dataRef.current.onUpdateState?.(type, event, value, state, info);
97
+ };
98
+
99
+ const onStoreEvent = (event: Event) => {
100
+ try {
101
+ mapHubEventsToLegacy(getHub(), event, onUpdateState, {
102
+ allBlockChains: dataRef.current.allBlockChains,
103
+ lastConnectAttemptParams: lastConnectAttemptParamsRef.current,
104
+ });
105
+ } catch (e) {
106
+ console.error(e);
107
+ }
108
+ rerender((currentRender) => currentRender + 1);
109
+ };
110
+
111
+ getStore().subscribe(onStoreEvent).flushEvents();
102
112
  }, []);
103
113
 
104
114
  // Initialize hub only after blockchain meta is available (e.g. WalletConnect needs it at init).
@@ -120,6 +130,7 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext {
120
130
  allBlockChains: params.allBlockChains,
121
131
  getHub,
122
132
  wallets: params.configs?.wallets,
133
+ emitter: dataRef.current.emitter,
123
134
  });
124
135
  },
125
136
  });
@@ -247,14 +258,6 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext {
247
258
 
248
259
  const connectNamespaceProcess = async () =>
249
260
  connectNamespacePromise()
250
- .catch((error: unknown) => {
251
- reportConnectionFailure(error, {
252
- walletType: type,
253
- namespace: namespaceInput.namespace,
254
- origin: 'manual',
255
- });
256
- throw error;
257
- })
258
261
  .then<ConnectResult>(transformHubResultToLegacyResult)
259
262
  .then((connectResult) => {
260
263
  return {
@@ -266,7 +269,46 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext {
266
269
  },
267
270
  };
268
271
  });
269
- return queueTask(connectNamespaceProcess, type);
272
+
273
+ // Reconnecting an already connected namespace (e.g. only to switch its network) isn't reported.
274
+ const connectNamespaceProcessWithEvents = async () => {
275
+ if (namespace.state()[0]().connected) {
276
+ return connectNamespaceProcess();
277
+ }
278
+
279
+ const eventPayload = {
280
+ walletName: type,
281
+ namespace: namespaceInput.namespace,
282
+ origin: 'manual' as const,
283
+ };
284
+ const requestedChain = namespaceInput.network || null;
285
+
286
+ dataRef.current.emitter?.emit(WalletEventChannel, {
287
+ type: WalletEventTypes.WALLET_CONNECT_INITIATED,
288
+ payload: { ...eventPayload, chain: requestedChain },
289
+ });
290
+ try {
291
+ const result = await connectNamespaceProcess();
292
+ dataRef.current.emitter?.emit(WalletEventChannel, {
293
+ type: WalletEventTypes.WALLET_CONNECTED,
294
+ payload: {
295
+ ...eventPayload,
296
+ chain: toBlockchainName(
297
+ result.response.network,
298
+ params.allBlockChains
299
+ ),
300
+ },
301
+ });
302
+ return result;
303
+ } catch (error) {
304
+ dataRef.current.emitter?.emit(WalletEventChannel, {
305
+ type: WalletEventTypes.WALLET_CONNECT_FAILED,
306
+ payload: { ...eventPayload, chain: requestedChain },
307
+ });
308
+ throw error;
309
+ }
310
+ };
311
+ return queueTask(connectNamespaceProcessWithEvents, type);
270
312
  }
271
313
  );
272
314
 
@@ -320,6 +362,10 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext {
320
362
  !namespaces || namespaces.includes(namespace.namespaceId);
321
363
  const namespaceIsConnected = namespace.state()[0]().connected;
322
364
  if (namespaceShouldBeDisconnected && namespaceIsConnected) {
365
+ dataRef.current.emitter?.emit(WalletEventChannel, {
366
+ type: WalletEventTypes.WALLET_DISCONNECTED,
367
+ payload: { walletName: type, namespace: namespace.namespaceId },
368
+ });
323
369
  return namespace.disconnect();
324
370
  }
325
371
  });
@@ -506,7 +552,12 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext {
506
552
  );
507
553
  },
508
554
  hubProvider(type) {
509
- const provider = findProviderByType(params.providers, type);
555
+ /*
556
+ * Return the instance registered in the hub, which holds the store. A
557
+ * provider list can be rebuilt with the same ids, and the hub keeps the
558
+ * instances it registered first, so a rebuilt one would have no store.
559
+ */
560
+ const provider = getHub().get(type);
510
561
 
511
562
  if (!provider) {
512
563
  throw new Error(
package/src/index.ts CHANGED
@@ -1,4 +1,16 @@
1
1
  export { default as Provider } from './provider.js';
2
2
  export { useWallets } from './hooks.js';
3
3
  export type * from './types.js';
4
+ export { WalletEventChannel, WalletEventTypes } from './events.js';
5
+ export type {
6
+ Emitter,
7
+ WalletConnectedEventPayload,
8
+ WalletConnectFailedEventPayload,
9
+ WalletConnectInitiatedEventPayload,
10
+ WalletConnectOrigin,
11
+ WalletDetectedEventPayload,
12
+ WalletDisconnectedEventPayload,
13
+ WalletEventData,
14
+ WalletEvents,
15
+ } from './events.js';
4
16
  export * from './legacy/types.js';
package/src/types.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Emitter, WalletEvents } from './events.js';
1
2
  import type {
2
3
  ConnectResult,
3
4
  EventHandler,
@@ -43,6 +44,8 @@ export type ProviderProps = PropsWithChildren<{
43
44
  allBlockChains?: BlockchainMeta[];
44
45
  autoConnect?: boolean;
45
46
  providers: Provider[];
47
+ /** Receives wallet events on the `walletEvent` channel. */
48
+ emitter?: Emitter<WalletEvents>;
46
49
  configs?: {
47
50
  wallets?: (WalletType | Provider)[];
48
51
  walletOptions?: {