@rango-dev/wallets-react 0.0.0-experimental-c6bea4d8-20260930 → 0.0.0-experimental-2938686c-20261012

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
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Hub } from '@hub3js/core';
2
2
 
3
- import { ConnectionErrorType, WalletConnectionError } from '@hub3js/std/utils';
3
+ import { Hub3Error } from '@hub3js/core';
4
4
  import { error as logError } from '@rango-dev/logging-core';
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
6
 
@@ -19,9 +19,8 @@ type FakeNamespace = {
19
19
  connect: () => Promise<string[]>;
20
20
  };
21
21
 
22
- function unknownConnectionError() {
23
- return new WalletConnectionError('Something went wrong', {
24
- type: ConnectionErrorType.Unknown,
22
+ function unexpectedError() {
23
+ return new Hub3Error('PROVIDER_UNEXPECTED', 'Something went wrong', {
25
24
  cause: new Error('Something went wrong'),
26
25
  });
27
26
  }
@@ -79,8 +78,8 @@ describe('autoConnect', () => {
79
78
  vi.restoreAllMocks();
80
79
  });
81
80
 
82
- it('report a saved namespace that fails to reconnect with an unknown connection error', async () => {
83
- const error = unknownConnectionError();
81
+ it('report a saved namespace that fails to reconnect with an unexpected error', async () => {
82
+ const error = unexpectedError();
84
83
  const solana = fakeNamespace('Solana', {
85
84
  connect: vi.fn(async () => Promise.reject(error)),
86
85
  });
@@ -92,8 +91,8 @@ describe('autoConnect', () => {
92
91
  expect(logError).toHaveBeenCalledOnce();
93
92
  expect(logError).toHaveBeenCalledWith(error, {
94
93
  tags: {
95
- name: 'WalletConnectionError',
96
- type: ConnectionErrorType.Unknown,
94
+ name: 'Hub3Error',
95
+ type: 'PROVIDER_UNEXPECTED',
97
96
  walletType: WALLET,
98
97
  namespace: 'Solana',
99
98
  origin: 'auto',
@@ -101,11 +100,45 @@ describe('autoConnect', () => {
101
100
  });
102
101
  });
103
102
 
103
+ it('report a plain error with the auto origin and no type', async () => {
104
+ const error = new Error('Before hook failed');
105
+ const solana = fakeNamespace('Solana', {
106
+ connect: vi.fn(async () => Promise.reject(error)),
107
+ });
108
+ const sui = fakeNamespace('Sui');
109
+
110
+ runAutoConnect([solana, sui]);
111
+ await settle(() => expect(sui.connect).toHaveBeenCalled());
112
+
113
+ expect(logError).toHaveBeenCalledOnce();
114
+ expect(logError).toHaveBeenCalledWith(error, {
115
+ tags: {
116
+ name: 'Error',
117
+ walletType: WALLET,
118
+ namespace: 'Solana',
119
+ origin: 'auto',
120
+ },
121
+ });
122
+ });
123
+
124
+ it.each(['PROVIDER_USER_REJECTED_REQUEST', 'PROVIDER_LOCKED'] as const)(
125
+ 'skip a namespace that fails with %s',
126
+ async (type) => {
127
+ const solana = fakeNamespace('Solana', {
128
+ connect: vi.fn(async () => Promise.reject(new Hub3Error(type))),
129
+ });
130
+ const sui = fakeNamespace('Sui');
131
+
132
+ runAutoConnect([solana, sui]);
133
+ await settle(() => expect(sui.connect).toHaveBeenCalled());
134
+
135
+ expect(logError).not.toHaveBeenCalled();
136
+ }
137
+ );
138
+
104
139
  it('skip a namespace that fails its eager connect check', async () => {
105
140
  const solana = fakeNamespace('Solana', {
106
- canEagerConnect: vi.fn(async () =>
107
- Promise.reject(unknownConnectionError())
108
- ),
141
+ canEagerConnect: vi.fn(async () => Promise.reject(unexpectedError())),
109
142
  });
110
143
  const sui = fakeNamespace('Sui');
111
144
 
@@ -117,11 +150,11 @@ describe('autoConnect', () => {
117
150
  });
118
151
 
119
152
  it('report only the namespace failure when no namespace connects', async () => {
120
- const solana = fakeNamespace('Solana', {
121
- connect: vi.fn(async () => Promise.reject(unknownConnectionError())),
122
- });
123
-
124
- runAutoConnect([solana]);
153
+ runAutoConnect([
154
+ fakeNamespace('Solana', {
155
+ connect: vi.fn(async () => Promise.reject(unexpectedError())),
156
+ }),
157
+ ]);
125
158
  await settle(() =>
126
159
  expect(console.warn).toHaveBeenCalledWith(
127
160
  new Error(`No namespace connected for ${WALLET}`)
@@ -130,9 +163,9 @@ describe('autoConnect', () => {
130
163
 
131
164
  expect(logError).toHaveBeenCalledOnce();
132
165
  expect(logError).toHaveBeenCalledWith(
133
- expect.any(WalletConnectionError),
166
+ expect.any(Hub3Error),
134
167
  expect.objectContaining({
135
- tags: expect.objectContaining({ namespace: 'Solana' }),
168
+ tags: expect.objectContaining({ namespace: 'Solana', origin: 'auto' }),
136
169
  })
137
170
  );
138
171
  });
@@ -7,6 +7,13 @@ 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 { reportConnectionFailure } from './errors.js';
12
19
  import { runSequentiallyWithoutFailure } from './helpers.js';
@@ -14,8 +21,21 @@ import { LastConnectedWalletsFromStorage } from './lastConnectedWallets.js';
14
21
  import {
15
22
  convertNamespaceNetworkToEvmChainId,
16
23
  isEvmNamespace,
24
+ transformHubResultToLegacyResult,
17
25
  } from './utils.js';
18
26
 
27
+ function createAutoConnectEventPayload(
28
+ walletName: WalletType,
29
+ namespaceInput: NamespaceInputForConnect
30
+ ) {
31
+ return {
32
+ walletName,
33
+ namespace: namespaceInput.namespace,
34
+ chain: namespaceInput.network || null,
35
+ origin: 'auto' as const,
36
+ };
37
+ }
38
+
19
39
  function isEvmNamespaceInput(
20
40
  namespace: NamespaceInputForConnect
21
41
  ): namespace is NamespaceInputForConnect<'EVM'> {
@@ -36,9 +56,10 @@ async function eagerConnect(
36
56
  params: {
37
57
  getHub: () => Hub;
38
58
  allBlockChains: UseAdapterParams['allBlockChains'];
59
+ emitter?: WalletEventEmitter;
39
60
  }
40
61
  ) {
41
- const { getHub, allBlockChains } = params;
62
+ const { getHub, allBlockChains, emitter } = params;
42
63
  const wallet = getHub().get(type);
43
64
  if (!wallet) {
44
65
  throw new Error(
@@ -82,8 +103,19 @@ async function eagerConnect(
82
103
  } else {
83
104
  connectNamespacePromise = async () => namespace.connect();
84
105
  }
106
+ const eventPayload = createAutoConnectEventPayload(type, info);
85
107
  try {
86
- await connectNamespacePromise();
108
+ const result = await connectNamespacePromise();
109
+ emitter?.emit(WalletEventChannel, {
110
+ type: WalletEventTypes.WALLET_CONNECTED,
111
+ payload: {
112
+ ...eventPayload,
113
+ chain: toBlockchainName(
114
+ transformHubResultToLegacyResult(result).network,
115
+ allBlockChains
116
+ ),
117
+ },
118
+ });
87
119
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
120
  } catch (e: any) {
89
121
  reportConnectionFailure(e, {
@@ -91,6 +123,10 @@ async function eagerConnect(
91
123
  namespace: info.namespace,
92
124
  origin: 'auto',
93
125
  });
126
+ emitter?.emit(WalletEventChannel, {
127
+ type: WalletEventTypes.WALLET_CONNECT_FAILED,
128
+ payload: eventPayload,
129
+ });
94
130
  /*
95
131
  * Since we check for connect failures using `instanceof Error`
96
132
  * this check is added here to make sure the thrown error always is an instance of `Error`
@@ -188,8 +224,9 @@ export async function autoConnect(deps: {
188
224
  getHub: () => Hub;
189
225
  allBlockChains: UseAdapterParams['allBlockChains'];
190
226
  wallets?: (WalletType | Provider)[];
227
+ emitter?: WalletEventEmitter;
191
228
  }): Promise<void> {
192
- const { getHub, allBlockChains, wallets } = deps;
229
+ const { getHub, allBlockChains, wallets, emitter } = deps;
193
230
  const lastConnectedWallets = lastConnectedWalletsFromStorage.list();
194
231
  const walletIds = Object.keys(lastConnectedWallets);
195
232
 
@@ -225,9 +262,23 @@ export async function autoConnect(deps: {
225
262
  return;
226
263
  }
227
264
 
265
+ lastConnectedNamespaces.forEach((namespace) => {
266
+ emitter?.emit(WalletEventChannel, {
267
+ type: WalletEventTypes.WALLET_CONNECT_INITIATED,
268
+ payload: createAutoConnectEventPayload(providerName, namespace),
269
+ });
270
+ });
271
+
228
272
  const { successNamespaces, failedNamespaces } =
229
273
  await tryRunCanEagerConnect(lastConnectedNamespaces, wallet);
230
274
 
275
+ failedNamespaces.forEach((namespace) => {
276
+ emitter?.emit(WalletEventChannel, {
277
+ type: WalletEventTypes.WALLET_CONNECT_FAILED,
278
+ payload: createAutoConnectEventPayload(providerName, namespace),
279
+ });
280
+ });
281
+
231
282
  if (!successNamespaces.length) {
232
283
  walletsToRemoveFromPersistence.push(providerName);
233
284
  return;
@@ -241,6 +292,7 @@ export async function autoConnect(deps: {
241
292
  eagerConnect(providerName, successNamespaces, {
242
293
  allBlockChains,
243
294
  getHub,
295
+ emitter,
244
296
  }).catch((error) => console.warn(error))
245
297
  );
246
298
  });
@@ -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,8 +1,4 @@
1
- import {
2
- CONNECTION_ERROR_MESSAGES,
3
- ConnectionErrorType,
4
- WalletConnectionError,
5
- } from '@hub3js/std/utils';
1
+ import { Hub3Error } from '@hub3js/core';
6
2
  import { error as logError } from '@rango-dev/logging-core';
7
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
8
4
 
@@ -18,26 +14,27 @@ describe('reportConnectionFailure', () => {
18
14
  namespace: 'EVM',
19
15
  origin: 'manual',
20
16
  } as const;
17
+ const context = {
18
+ walletType: 'metamask',
19
+ namespace: 'EVM',
20
+ origin: 'manual',
21
+ };
21
22
 
22
23
  beforeEach(() => {
23
24
  vi.mocked(logError).mockClear();
24
25
  });
25
26
 
26
- it.each([ConnectionErrorType.Rejected, ConnectionErrorType.Locked])(
27
- 'drop a %s connection error',
27
+ it.each(['PROVIDER_USER_REJECTED_REQUEST', 'PROVIDER_LOCKED'] as const)(
28
+ 'drop a %s error',
28
29
  (type) => {
29
- reportConnectionFailure(
30
- new WalletConnectionError(CONNECTION_ERROR_MESSAGES[type], { type }),
31
- options
32
- );
30
+ reportConnectionFailure(new Hub3Error(type, 'Declined'), options);
33
31
 
34
32
  expect(logError).not.toHaveBeenCalled();
35
33
  }
36
34
  );
37
35
 
38
- it('log an unknown connection error tagged only with its name, type, wallet, namespace and origin', () => {
39
- const error = new WalletConnectionError('Something went wrong', {
40
- type: ConnectionErrorType.Unknown,
36
+ it('log a PROVIDER_UNEXPECTED error tagged only with its name, type, wallet, namespace and origin', () => {
37
+ const error = new Hub3Error('PROVIDER_UNEXPECTED', 'Something went wrong', {
41
38
  cause: new Error('Something went wrong'),
42
39
  });
43
40
 
@@ -45,26 +42,50 @@ describe('reportConnectionFailure', () => {
45
42
 
46
43
  expect(logError).toHaveBeenCalledOnce();
47
44
  expect(logError).toHaveBeenCalledWith(error, {
48
- tags: {
49
- name: 'WalletConnectionError',
50
- type: ConnectionErrorType.Unknown,
51
- walletType: 'metamask',
52
- namespace: 'EVM',
53
- origin: 'manual',
54
- },
45
+ tags: { name: 'Hub3Error', type: 'PROVIDER_UNEXPECTED', ...context },
55
46
  });
56
47
  });
57
48
 
58
- it('drop a plain error', () => {
59
- reportConnectionFailure(new Error('Something went wrong'), options);
49
+ it('log a plain error with its name and no type', () => {
50
+ const error = new TypeError('Something went wrong');
60
51
 
61
- expect(logError).not.toHaveBeenCalled();
52
+ reportConnectionFailure(error, options);
53
+
54
+ expect(logError).toHaveBeenCalledOnce();
55
+ expect(logError).toHaveBeenCalledWith(error, {
56
+ tags: { name: 'TypeError', ...context },
57
+ });
62
58
  });
63
59
 
64
- it('drop a thrown value that is not an error', () => {
65
- reportConnectionFailure({ code: 4001, message: 'rejected' }, options);
66
- reportConnectionFailure('Something went wrong', options);
60
+ it.each([
61
+ ['a string', 'Something went wrong', 'Something went wrong'],
62
+ [
63
+ "an object's own message",
64
+ { code: 4001, message: 'rejected' },
65
+ 'rejected',
66
+ ],
67
+ [
68
+ 'the fixed message for an object without one',
69
+ { code: 4001 },
70
+ 'Non-Error value thrown during connect.',
71
+ ],
72
+ [
73
+ 'the fixed message for anything else',
74
+ undefined,
75
+ 'Non-Error value thrown during connect.',
76
+ ],
77
+ ])(
78
+ 'wrap a non-error as NonErrorThrown and log it, using %s',
79
+ (_, value, message) => {
80
+ reportConnectionFailure(value, options);
67
81
 
68
- expect(logError).not.toHaveBeenCalled();
69
- });
82
+ expect(logError).toHaveBeenCalledOnce();
83
+ const [logged, data] = vi.mocked(logError).mock.calls[0];
84
+ expect(logged).toBeInstanceOf(Error);
85
+ expect(logged.name).toBe('NonErrorThrown');
86
+ expect(logged.message).toBe(message);
87
+ expect(logged.cause).toBe(value);
88
+ expect(data).toEqual({ tags: { name: 'NonErrorThrown', ...context } });
89
+ }
90
+ );
70
91
  });
package/src/hub/errors.ts CHANGED
@@ -1,39 +1,45 @@
1
+ import type { WalletConnectOrigin } from '../events.js';
1
2
  import type { WalletType } from '@hub3js/core';
2
3
  import type { Namespace } from '@hub3js/namespaces';
3
4
 
4
- import { ConnectionErrorType, WalletConnectionError } from '@hub3js/std/utils';
5
+ import { getErrorMessage, Hub3Error } from '@hub3js/core';
5
6
  import { error as logError } from '@rango-dev/logging-core';
6
7
 
7
- type ConnectionOrigin = 'manual' | 'auto';
8
+ const NON_ERROR_THROWN_NAME = 'NonErrorThrown';
9
+ const NON_ERROR_THROWN_MESSAGE = 'Non-Error value thrown during connect.';
8
10
 
9
- // The user caused these failures. It's a drop list, so a type added later is reported.
10
- const UNREPORTED_CONNECTION_ERROR_TYPES: ConnectionErrorType[] = [
11
- ConnectionErrorType.Rejected,
12
- ConnectionErrorType.Locked,
13
- ];
11
+ function wrapNonError(value: unknown): Error {
12
+ const message =
13
+ typeof value === 'string'
14
+ ? value
15
+ : getErrorMessage(value) ?? NON_ERROR_THROWN_MESSAGE;
16
+ const wrapper = new Error(message, { cause: value });
17
+ wrapper.name = NON_ERROR_THROWN_NAME;
18
+ return wrapper;
19
+ }
14
20
 
21
+ /*
22
+ * An allow list: among `Hub3Error`s only `PROVIDER_UNEXPECTED` is logged, so a type added later
23
+ * (something the user can act on, like a rejection or a locked wallet) stays out of the logs.
24
+ */
15
25
  export function reportConnectionFailure(
16
26
  error: unknown,
17
27
  options: {
18
28
  walletType: WalletType;
19
29
  namespace: Namespace;
20
- origin: ConnectionOrigin;
30
+ origin: WalletConnectOrigin;
21
31
  }
22
32
  ): void {
23
- if (
24
- !(error instanceof WalletConnectionError) ||
25
- UNREPORTED_CONNECTION_ERROR_TYPES.includes(error.type)
26
- ) {
33
+ if (error instanceof Hub3Error) {
34
+ if (error.type !== 'PROVIDER_UNEXPECTED') {
35
+ return;
36
+ }
37
+ logError(error, {
38
+ tags: { name: error.name, type: error.type, ...options },
39
+ });
27
40
  return;
28
41
  }
29
42
 
30
- logError(error, {
31
- tags: {
32
- name: error.name,
33
- type: error.type,
34
- walletType: options.walletType,
35
- namespace: options.namespace,
36
- origin: options.origin,
37
- },
38
- });
43
+ const reported = error instanceof Error ? error : wrapNonError(error);
44
+ logError(reported, { tags: { name: reported.name, ...options } });
39
45
  }
@@ -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 {