@rango-dev/wallets-react 0.0.0-experimental-1cc1923b-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,14 +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
18
  import { runSequentiallyWithoutFailure } from './helpers.js';
12
19
  import { LastConnectedWalletsFromStorage } from './lastConnectedWallets.js';
13
20
  import {
14
21
  convertNamespaceNetworkToEvmChainId,
15
22
  isEvmNamespace,
23
+ transformHubResultToLegacyResult,
16
24
  } from './utils.js';
17
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
+
18
38
  function isEvmNamespaceInput(
19
39
  namespace: NamespaceInputForConnect
20
40
  ): namespace is NamespaceInputForConnect<'EVM'> {
@@ -35,9 +55,10 @@ async function eagerConnect(
35
55
  params: {
36
56
  getHub: () => Hub;
37
57
  allBlockChains: UseAdapterParams['allBlockChains'];
58
+ emitter?: WalletEventEmitter;
38
59
  }
39
60
  ) {
40
- const { getHub, allBlockChains } = params;
61
+ const { getHub, allBlockChains, emitter } = params;
41
62
  const wallet = getHub().get(type);
42
63
  if (!wallet) {
43
64
  throw new Error(
@@ -81,10 +102,25 @@ async function eagerConnect(
81
102
  } else {
82
103
  connectNamespacePromise = async () => namespace.connect();
83
104
  }
105
+ const eventPayload = createAutoConnectEventPayload(type, info);
84
106
  try {
85
- 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
+ });
86
118
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
119
  } catch (e: any) {
120
+ emitter?.emit(WalletEventChannel, {
121
+ type: WalletEventTypes.WALLET_CONNECT_FAILED,
122
+ payload: eventPayload,
123
+ });
88
124
  /*
89
125
  * Since we check for connect failures using `instanceof Error`
90
126
  * this check is added here to make sure the thrown error always is an instance of `Error`
@@ -182,8 +218,9 @@ export async function autoConnect(deps: {
182
218
  getHub: () => Hub;
183
219
  allBlockChains: UseAdapterParams['allBlockChains'];
184
220
  wallets?: (WalletType | Provider)[];
221
+ emitter?: WalletEventEmitter;
185
222
  }): Promise<void> {
186
- const { getHub, allBlockChains, wallets } = deps;
223
+ const { getHub, allBlockChains, wallets, emitter } = deps;
187
224
  const lastConnectedWallets = lastConnectedWalletsFromStorage.list();
188
225
  const walletIds = Object.keys(lastConnectedWallets);
189
226
 
@@ -219,9 +256,23 @@ export async function autoConnect(deps: {
219
256
  return;
220
257
  }
221
258
 
259
+ lastConnectedNamespaces.forEach((namespace) => {
260
+ emitter?.emit(WalletEventChannel, {
261
+ type: WalletEventTypes.WALLET_CONNECT_INITIATED,
262
+ payload: createAutoConnectEventPayload(providerName, namespace),
263
+ });
264
+ });
265
+
222
266
  const { successNamespaces, failedNamespaces } =
223
267
  await tryRunCanEagerConnect(lastConnectedNamespaces, wallet);
224
268
 
269
+ failedNamespaces.forEach((namespace) => {
270
+ emitter?.emit(WalletEventChannel, {
271
+ type: WalletEventTypes.WALLET_CONNECT_FAILED,
272
+ payload: createAutoConnectEventPayload(providerName, namespace),
273
+ });
274
+ });
275
+
225
276
  if (!successNamespaces.length) {
226
277
  walletsToRemoveFromPersistence.push(providerName);
227
278
  return;
@@ -235,6 +286,7 @@ export async function autoConnect(deps: {
235
286
  eagerConnect(providerName, successNamespaces, {
236
287
  allBlockChains,
237
288
  getHub,
289
+ emitter,
238
290
  }).catch((error) => console.warn(error))
239
291
  );
240
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
+ ];
@@ -1,11 +1,13 @@
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
 
11
13
  import { utils } from '@hub3js/evm';
@@ -16,7 +18,14 @@ import {
16
18
  import { useEffect, useRef, useState } from 'react';
17
19
  import { Ok, Result } from 'ts-results';
18
20
 
21
+ import {
22
+ emitWalletDetectedOnce,
23
+ toBlockchainName,
24
+ WalletEventChannel,
25
+ WalletEventTypes,
26
+ } from '../events.js';
19
27
  import { withErrorLoggingApi } from '../helpers.js';
28
+ import { Events } from '../legacy/mod.js';
20
29
 
21
30
  import { autoConnect } from './autoConnect.js';
22
31
  import { HUB_LAST_CONNECTED_WALLETS } from './constants.js';
@@ -25,7 +34,6 @@ import { LastConnectedWalletsFromStorage } from './lastConnectedWallets.js';
25
34
  import { useAutoConnect } from './useAutoConnect.js';
26
35
  import { useHubRefs } from './useHubRefs.js';
27
36
  import {
28
- findProviderByType,
29
37
  isEvmNamespace,
30
38
  isSolanaNamespace,
31
39
  isUtxoNamespace,
@@ -45,6 +53,7 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext {
45
53
  const dataRef = useRef({
46
54
  onUpdateState: params.onUpdateState,
47
55
  allBlockChains: params.allBlockChains,
56
+ emitter: params.emitter,
48
57
  });
49
58
  const hubInitiated = useRef(false);
50
59
 
@@ -75,30 +84,31 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext {
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
  });
@@ -258,7 +269,46 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext {
258
269
  },
259
270
  };
260
271
  });
261
- 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);
262
312
  }
263
313
  );
264
314
 
@@ -312,6 +362,10 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext {
312
362
  !namespaces || namespaces.includes(namespace.namespaceId);
313
363
  const namespaceIsConnected = namespace.state()[0]().connected;
314
364
  if (namespaceShouldBeDisconnected && namespaceIsConnected) {
365
+ dataRef.current.emitter?.emit(WalletEventChannel, {
366
+ type: WalletEventTypes.WALLET_DISCONNECTED,
367
+ payload: { walletName: type, namespace: namespace.namespaceId },
368
+ });
315
369
  return namespace.disconnect();
316
370
  }
317
371
  });
@@ -498,7 +552,12 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext {
498
552
  );
499
553
  },
500
554
  hubProvider(type) {
501
- 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);
502
561
 
503
562
  if (!provider) {
504
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?: {