@rango-dev/widget-embedded 0.0.0-experimental-1624fbb4-20260708 → 0.0.0-experimental-99f1781b-20260920

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.
Files changed (190) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/QueueManager.d.ts.map +1 -1
  3. package/dist/components/ConfirmWalletsModal/ConfirmWallets.styles.d.ts +1 -1
  4. package/dist/components/ConfirmWalletsModal/ConfirmWallets.styles.d.ts.map +1 -1
  5. package/dist/components/CustomDestination/CustomDestination.styles.d.ts +4 -4
  6. package/dist/components/CustomDestination/CustomDestination.styles.d.ts.map +1 -1
  7. package/dist/components/CustomTokenModal/CustomTokenModal.styles.d.ts +169 -169
  8. package/dist/components/CustomTokenModal/CustomTokenModal.styles.d.ts.map +1 -1
  9. package/dist/components/FilterSelector/FilterSelector.style.d.ts +1 -1
  10. package/dist/components/FilterSelector/FilterSelector.style.d.ts.map +1 -1
  11. package/dist/components/HeaderButtons/HeaderButtons.styles.d.ts +1 -1
  12. package/dist/components/HeaderButtons/HeaderButtons.styles.d.ts.map +1 -1
  13. package/dist/components/NamespaceItem/NamespaceItem.d.ts.map +1 -1
  14. package/dist/components/NamespaceItem/NamespaceItem.styles.d.ts +1 -1
  15. package/dist/components/NamespaceItem/NamespaceItem.styles.d.ts.map +1 -1
  16. package/dist/components/NamespaceItem/NamespaceItem.types.d.ts +3 -3
  17. package/dist/components/NamespaceItem/NamespaceItem.types.d.ts.map +1 -1
  18. package/dist/components/Quote/Quote.d.ts.map +1 -1
  19. package/dist/components/Quote/Quote.styles.d.ts +171 -171
  20. package/dist/components/Quote/Quote.styles.d.ts.map +1 -1
  21. package/dist/components/Quote/Quote.types.d.ts +0 -2
  22. package/dist/components/Quote/Quote.types.d.ts.map +1 -1
  23. package/dist/components/Quote/QuoteCostDetails.d.ts.map +1 -1
  24. package/dist/components/QuoteWarningsAndErrors/QuoteWarningsAndErrors.styles.d.ts +169 -169
  25. package/dist/components/QuoteWarningsAndErrors/QuoteWarningsAndErrors.styles.d.ts.map +1 -1
  26. package/dist/components/RefreshModal/RefreshModal.styles.d.ts +168 -168
  27. package/dist/components/RefreshModal/RefreshModal.styles.d.ts.map +1 -1
  28. package/dist/components/StatefulConnectModal/StatefulConnectModal.d.ts +2 -1
  29. package/dist/components/StatefulConnectModal/StatefulConnectModal.d.ts.map +1 -1
  30. package/dist/components/SwapDetails/SwapDetails.d.ts.map +1 -1
  31. package/dist/components/SwapDetails/SwapDetails.styles.d.ts +1 -1
  32. package/dist/components/SwapDetails/SwapDetails.styles.d.ts.map +1 -1
  33. package/dist/components/SwapDetailsModal/SwapDetailsModal.ConnectWallet.d.ts.map +1 -1
  34. package/dist/components/SwapDetailsModal/SwapDetailsModal.InstallWallet.d.ts.map +1 -1
  35. package/dist/components/SwapDetailsModal/SwapDetailsModal.types.d.ts +3 -3
  36. package/dist/components/SwapDetailsModal/SwapDetailsModal.types.d.ts.map +1 -1
  37. package/dist/components/SwapMetrics/SwapMetrics.styles.d.ts +1 -1
  38. package/dist/components/SwapMetrics/SwapMetrics.styles.d.ts.map +1 -1
  39. package/dist/components/TokenList/TokenList.styles.d.ts +2 -2
  40. package/dist/components/TokenList/TokenList.styles.d.ts.map +1 -1
  41. package/dist/components/WalletStatefulConnect/DerivationPath.d.ts.map +1 -1
  42. package/dist/components/WalletStatefulConnect/DerivationPath.helpers.d.ts +7 -3
  43. package/dist/components/WalletStatefulConnect/DerivationPath.helpers.d.ts.map +1 -1
  44. package/dist/components/WalletStatefulConnect/DerivationPath.helpers.test.d.ts +2 -0
  45. package/dist/components/WalletStatefulConnect/DerivationPath.helpers.test.d.ts.map +1 -0
  46. package/dist/components/WalletStatefulConnect/DerivationPath.styles.d.ts +169 -169
  47. package/dist/components/WalletStatefulConnect/DerivationPath.styles.d.ts.map +1 -1
  48. package/dist/components/WalletStatefulConnect/Detached.d.ts.map +1 -1
  49. package/dist/components/WalletStatefulConnect/Detached.types.d.ts +2 -1
  50. package/dist/components/WalletStatefulConnect/Detached.types.d.ts.map +1 -1
  51. package/dist/components/WalletStatefulConnect/NamespaceDetachedItem.d.ts.map +1 -1
  52. package/dist/components/WalletStatefulConnect/Namespaces.d.ts.map +1 -1
  53. package/dist/components/WalletStatefulConnect/Namespaces.styles.d.ts +168 -168
  54. package/dist/components/WalletStatefulConnect/Namespaces.styles.d.ts.map +1 -1
  55. package/dist/components/WalletStatefulConnect/Namespaces.types.d.ts +3 -3
  56. package/dist/components/WalletStatefulConnect/Namespaces.types.d.ts.map +1 -1
  57. package/dist/constants/errors.d.ts +1 -0
  58. package/dist/constants/errors.d.ts.map +1 -1
  59. package/dist/containers/Wallets/Wallets.constants.d.ts +3 -0
  60. package/dist/containers/Wallets/Wallets.constants.d.ts.map +1 -0
  61. package/dist/containers/Wallets/Wallets.d.ts.map +1 -1
  62. package/dist/containers/Wallets/Wallets.helpers.d.ts +2 -2
  63. package/dist/containers/Wallets/Wallets.helpers.d.ts.map +1 -1
  64. package/dist/containers/Wallets/Wallets.helpers.test.d.ts +2 -0
  65. package/dist/containers/Wallets/Wallets.helpers.test.d.ts.map +1 -0
  66. package/dist/containers/Wallets/Wallets.types.d.ts +6 -4
  67. package/dist/containers/Wallets/Wallets.types.d.ts.map +1 -1
  68. package/dist/containers/Wallets/useUpdates.d.ts +1 -1
  69. package/dist/containers/Wallets/useUpdates.d.ts.map +1 -1
  70. package/dist/containers/WidgetProvider/WidgetProvider.d.ts.map +1 -1
  71. package/dist/containers/WidgetProvider/WidgetProvider.helpers.d.ts +5 -0
  72. package/dist/containers/WidgetProvider/WidgetProvider.helpers.d.ts.map +1 -0
  73. package/dist/containers/WidgetProvider/WidgetProvider.types.d.ts +1 -1
  74. package/dist/containers/WidgetProvider/WidgetProvider.types.d.ts.map +1 -1
  75. package/dist/hooks/useConnectWithLogging/index.d.ts +2 -0
  76. package/dist/hooks/useConnectWithLogging/index.d.ts.map +1 -0
  77. package/dist/hooks/useConnectWithLogging/useConnectWithLogging.d.ts +11 -0
  78. package/dist/hooks/useConnectWithLogging/useConnectWithLogging.d.ts.map +1 -0
  79. package/dist/hooks/useConnectWithLogging/useConnectWithLogging.test.d.ts +2 -0
  80. package/dist/hooks/useConnectWithLogging/useConnectWithLogging.test.d.ts.map +1 -0
  81. package/dist/hooks/useStatefulConnect/useStatefulConnect.d.ts +2 -1
  82. package/dist/hooks/useStatefulConnect/useStatefulConnect.d.ts.map +1 -1
  83. package/dist/hooks/useStatefulConnect/useStatefulConnect.types.d.ts +5 -4
  84. package/dist/hooks/useStatefulConnect/useStatefulConnect.types.d.ts.map +1 -1
  85. package/dist/hooks/useWalletList.d.ts.map +1 -1
  86. package/dist/hooks/useWalletProviders/useWalletProviders.d.ts +2 -2
  87. package/dist/hooks/useWalletProviders/useWalletProviders.d.ts.map +1 -1
  88. package/dist/hooks/useWalletProviders/useWalletProviders.helpers.d.ts +2 -3
  89. package/dist/hooks/useWalletProviders/useWalletProviders.helpers.d.ts.map +1 -1
  90. package/dist/index.d.ts +10 -7
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +3 -2
  93. package/dist/index.js.map +4 -4
  94. package/dist/libs/connectLogging/connectLogging.constants.d.ts +9 -0
  95. package/dist/libs/connectLogging/connectLogging.constants.d.ts.map +1 -0
  96. package/dist/libs/connectLogging/connectLogging.d.ts +12 -0
  97. package/dist/libs/connectLogging/connectLogging.d.ts.map +1 -0
  98. package/dist/libs/connectLogging/connectLogging.test.d.ts +2 -0
  99. package/dist/libs/connectLogging/connectLogging.test.d.ts.map +1 -0
  100. package/dist/libs/connectLogging/connectLogging.types.d.ts +57 -0
  101. package/dist/libs/connectLogging/connectLogging.types.d.ts.map +1 -0
  102. package/dist/libs/connectLogging/index.d.ts +3 -0
  103. package/dist/libs/connectLogging/index.d.ts.map +1 -0
  104. package/dist/pages/WalletsPage.d.ts.map +1 -1
  105. package/dist/store/AppStore.d.ts +2 -2
  106. package/dist/store/AppStore.d.ts.map +1 -1
  107. package/dist/store/app.d.ts +2 -2
  108. package/dist/store/app.d.ts.map +1 -1
  109. package/dist/store/slices/config.d.ts +5 -5
  110. package/dist/store/slices/config.d.ts.map +1 -1
  111. package/dist/test-utils/fixtures.d.ts +2 -2
  112. package/dist/test-utils/fixtures.d.ts.map +1 -1
  113. package/dist/types/config.d.ts +25 -4
  114. package/dist/types/config.d.ts.map +1 -1
  115. package/dist/types/wallets.d.ts +1 -1
  116. package/dist/types/wallets.d.ts.map +1 -1
  117. package/dist/utils/common.d.ts +1 -0
  118. package/dist/utils/common.d.ts.map +1 -1
  119. package/dist/utils/errors.d.ts +14 -0
  120. package/dist/utils/errors.d.ts.map +1 -0
  121. package/dist/utils/errors.test.d.ts +2 -0
  122. package/dist/utils/errors.test.d.ts.map +1 -0
  123. package/dist/utils/providers.d.ts +3 -6
  124. package/dist/utils/providers.d.ts.map +1 -1
  125. package/dist/utils/swap.d.ts +1 -1
  126. package/dist/utils/swap.d.ts.map +1 -1
  127. package/dist/utils/wallets.d.ts +2 -1
  128. package/dist/utils/wallets.d.ts.map +1 -1
  129. package/package.json +16 -16
  130. package/src/QueueManager.tsx +17 -16
  131. package/src/components/ConfirmWalletsModal/WalletList.tsx +2 -2
  132. package/src/components/NamespaceItem/NamespaceItem.styles.ts +1 -0
  133. package/src/components/NamespaceItem/NamespaceItem.tsx +5 -1
  134. package/src/components/NamespaceItem/NamespaceItem.types.ts +3 -3
  135. package/src/components/Quote/Quote.tsx +0 -2
  136. package/src/components/Quote/Quote.types.ts +0 -2
  137. package/src/components/Quote/QuoteCostDetails.tsx +15 -4
  138. package/src/components/StatefulConnectModal/StatefulConnectModal.tsx +4 -2
  139. package/src/components/SwapDetails/SwapDetails.tsx +17 -8
  140. package/src/components/SwapDetailsModal/SwapDetailsModal.ConnectWallet.tsx +12 -13
  141. package/src/components/SwapDetailsModal/SwapDetailsModal.InstallWallet.tsx +8 -2
  142. package/src/components/SwapDetailsModal/SwapDetailsModal.types.ts +3 -3
  143. package/src/components/TokenList/LoadingTokenList.tsx +2 -2
  144. package/src/components/WalletStatefulConnect/DerivationPath.helpers.test.ts +98 -0
  145. package/src/components/WalletStatefulConnect/DerivationPath.helpers.ts +14 -10
  146. package/src/components/WalletStatefulConnect/DerivationPath.tsx +14 -7
  147. package/src/components/WalletStatefulConnect/Detached.tsx +17 -10
  148. package/src/components/WalletStatefulConnect/Detached.types.ts +2 -1
  149. package/src/components/WalletStatefulConnect/NamespaceDetachedItem.tsx +2 -1
  150. package/src/components/WalletStatefulConnect/Namespaces.tsx +6 -12
  151. package/src/components/WalletStatefulConnect/Namespaces.types.tsx +3 -3
  152. package/src/constants/errors.ts +3 -0
  153. package/src/containers/Wallets/Wallets.constants.ts +14 -0
  154. package/src/containers/Wallets/Wallets.helpers.test.ts +67 -0
  155. package/src/containers/Wallets/Wallets.helpers.ts +9 -9
  156. package/src/containers/Wallets/Wallets.tsx +20 -3
  157. package/src/containers/Wallets/Wallets.types.ts +7 -7
  158. package/src/containers/Wallets/useUpdates.ts +7 -117
  159. package/src/containers/WidgetProvider/WidgetProvider.helpers.ts +10 -0
  160. package/src/containers/WidgetProvider/WidgetProvider.tsx +12 -1
  161. package/src/containers/WidgetProvider/WidgetProvider.types.ts +1 -1
  162. package/src/hooks/useConnectWithLogging/index.ts +1 -0
  163. package/src/hooks/useConnectWithLogging/useConnectWithLogging.test.ts +138 -0
  164. package/src/hooks/useConnectWithLogging/useConnectWithLogging.ts +36 -0
  165. package/src/hooks/useStatefulConnect/useStatefulConnect.ts +36 -29
  166. package/src/hooks/useStatefulConnect/useStatefulConnect.types.ts +6 -3
  167. package/src/hooks/useWalletList.ts +10 -7
  168. package/src/hooks/useWalletProviders/useWalletProviders.helpers.ts +3 -10
  169. package/src/hooks/useWalletProviders/useWalletProviders.ts +2 -2
  170. package/src/index.ts +10 -11
  171. package/src/libs/connectLogging/connectLogging.constants.ts +17 -0
  172. package/src/libs/connectLogging/connectLogging.test.ts +918 -0
  173. package/src/libs/connectLogging/connectLogging.ts +359 -0
  174. package/src/libs/connectLogging/connectLogging.types.ts +70 -0
  175. package/src/libs/connectLogging/index.ts +5 -0
  176. package/src/pages/WalletsPage.tsx +1 -0
  177. package/src/store/AppStore.tsx +4 -2
  178. package/src/store/app.ts +7 -2
  179. package/src/store/slices/config.ts +36 -25
  180. package/src/store/slices/data.test.ts +2 -2
  181. package/src/test-utils/fixtures.ts +6 -2
  182. package/src/types/config.ts +31 -4
  183. package/src/types/wallets.ts +1 -1
  184. package/src/utils/common.ts +6 -0
  185. package/src/utils/errors.test.ts +78 -0
  186. package/src/utils/errors.ts +69 -0
  187. package/src/utils/providers.ts +16 -50
  188. package/src/utils/swap.ts +8 -1
  189. package/src/utils/wallets.ts +12 -15
  190. package/dist/widget-embedded.build.json +0 -1
@@ -0,0 +1,918 @@
1
+ import type {
2
+ ConnectionAttempt,
3
+ ConnectLogEvent,
4
+ } from './connectLogging.types';
5
+ import type { WalletErrorCode } from '@hub3js/std/utils';
6
+ import type {
7
+ ConnectResult,
8
+ NamespaceInputForConnect,
9
+ } from '@rango-dev/wallets-react';
10
+
11
+ import {
12
+ USER_REJECTION_ERROR_CODE,
13
+ WALLET_LOCKED_ERROR_CODE,
14
+ WalletConnectionError,
15
+ } from '@hub3js/std/utils';
16
+ import { error as logError, warn as logWarn } from '@rango-dev/logging-core';
17
+ import {
18
+ AutoConnectionAttemptError,
19
+ WalletConnectionAttemptError,
20
+ } from '@rango-dev/wallets-react';
21
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22
+
23
+ import { logAutoConnectFailure, runConnectWithLogging } from './connectLogging';
24
+
25
+ vi.mock('@rango-dev/logging-core', () => ({ error: vi.fn(), warn: vi.fn() }));
26
+
27
+ const loggedError = vi.mocked(logError);
28
+ const loggedWarn = vi.mocked(logWarn);
29
+
30
+ const THREE_MINUTES = 180_000;
31
+
32
+ const REQUEST_ALREADY_PENDING_CODE = -32002;
33
+ const PROVIDER_INTERNAL_ERROR_CODE = -32603;
34
+
35
+ /** A thrown value that is neither an object nor a string. */
36
+ const THROWN_NUMBER = 42;
37
+
38
+ const REQUESTED: NamespaceInputForConnect[] = [
39
+ { namespace: 'EVM', network: undefined },
40
+ { namespace: 'Solana', network: undefined },
41
+ ];
42
+
43
+ const ATTEMPT: ConnectionAttempt = {
44
+ walletType: 'phantom',
45
+ trigger: 'wallets-page',
46
+ requestedNamespaces: REQUESTED,
47
+ };
48
+
49
+ const CONNECTED: ConnectResult[] = [{ accounts: [], network: null }];
50
+
51
+ function errorWithCode(message: string, code: WalletErrorCode): Error {
52
+ return Object.assign(new Error(message), { code });
53
+ }
54
+
55
+ function failure(namespace: string, cause: unknown): WalletConnectionError {
56
+ return new WalletConnectionError({ namespace, cause });
57
+ }
58
+
59
+ function rejection(namespace: string): WalletConnectionError {
60
+ return failure(
61
+ namespace,
62
+ errorWithCode('User rejected the request.', USER_REJECTION_ERROR_CODE)
63
+ );
64
+ }
65
+
66
+ function locked(namespace: string): WalletConnectionError {
67
+ return failure(
68
+ namespace,
69
+ errorWithCode('Please unlock your wallet.', WALLET_LOCKED_ERROR_CODE)
70
+ );
71
+ }
72
+
73
+ function chunkLoadFailure(): Error {
74
+ return new TypeError(
75
+ 'Failed to fetch dynamically imported module: https://cdn.example/solana.js'
76
+ );
77
+ }
78
+
79
+ function attemptError(
80
+ errors: unknown[],
81
+ requestedNamespaces: NamespaceInputForConnect[] = REQUESTED
82
+ ): WalletConnectionAttemptError {
83
+ return new WalletConnectionAttemptError({ errors, requestedNamespaces });
84
+ }
85
+
86
+ /** A `WalletConnectionError` from another copy of `@hub3js/std`. */
87
+ class ForeignWalletConnectionError extends Error {
88
+ name = 'WalletConnectionError';
89
+ constructor(
90
+ readonly namespace: string,
91
+ message: string,
92
+ readonly code?: WalletErrorCode
93
+ ) {
94
+ super(message);
95
+ }
96
+ }
97
+
98
+ /** An error that throws the moment anything reads the given field. */
99
+ function withUnreadable<Value extends object>(
100
+ value: Value,
101
+ field: string
102
+ ): Value {
103
+ Object.defineProperty(value, field, {
104
+ get: () => {
105
+ throw new Error(`This ${field} cannot be read.`);
106
+ },
107
+ });
108
+
109
+ return value;
110
+ }
111
+
112
+ /** A value that throws the moment anything reads anything from it. */
113
+ function unreadable(): unknown {
114
+ return new Proxy(
115
+ {},
116
+ {
117
+ get: () => {
118
+ throw new Error('Nothing can be read from this value.');
119
+ },
120
+ getPrototypeOf: () => {
121
+ throw new Error('Nothing can be read from this value.');
122
+ },
123
+ }
124
+ );
125
+ }
126
+
127
+ /** What the logger was given, or `null` when it was given nothing. */
128
+ function loggedEvent(): ConnectLogEvent | null {
129
+ const call = loggedError.mock.calls[0];
130
+
131
+ return call
132
+ ? ({
133
+ error: call[0],
134
+ tags: call[1]?.tags,
135
+ context: call[1]?.context,
136
+ } as ConnectLogEvent)
137
+ : null;
138
+ }
139
+
140
+ /** The event logged for an attempt that fails with `thrown`. */
141
+ async function eventFor(
142
+ thrown: unknown,
143
+ attempt: ConnectionAttempt = ATTEMPT
144
+ ): Promise<ConnectLogEvent | null> {
145
+ loggedError.mockClear();
146
+ await expect(
147
+ runConnectWithLogging(attempt, async () => Promise.reject(thrown))
148
+ ).rejects.toBe(thrown);
149
+
150
+ return loggedEvent();
151
+ }
152
+
153
+ /** The event the timer logs for an attempt that never settles. Needs fake timers. */
154
+ function timeoutEventFor(
155
+ attempt: ConnectionAttempt = ATTEMPT
156
+ ): ConnectLogEvent | null {
157
+ loggedError.mockClear();
158
+ void runConnectWithLogging(
159
+ attempt,
160
+ async () => new Promise<ConnectResult[]>(() => undefined)
161
+ );
162
+ vi.advanceTimersByTime(THREE_MINUTES);
163
+
164
+ return loggedEvent();
165
+ }
166
+
167
+ beforeEach(() => {
168
+ loggedError.mockReset();
169
+ loggedWarn.mockReset();
170
+ });
171
+
172
+ describe('classifying one connection failure of an attempt', () => {
173
+ it('sets aside a wallet connection error with the rejection code', async () => {
174
+ expect(await eventFor(attemptError([rejection('EVM')]))).toBeNull();
175
+ });
176
+
177
+ it('sets aside a wallet connection error with the locked code', async () => {
178
+ expect(await eventFor(attemptError([locked('Tron')]))).toBeNull();
179
+ });
180
+
181
+ it('matches the rejection code strictly, so one sent as a string is logged', async () => {
182
+ const event = await eventFor(
183
+ attemptError([
184
+ new ForeignWalletConnectionError(
185
+ 'EVM',
186
+ 'User rejected.',
187
+ String(USER_REJECTION_ERROR_CODE)
188
+ ),
189
+ ])
190
+ );
191
+
192
+ expect(event?.tags.category).toBe('wallet');
193
+ });
194
+
195
+ it('logs a wallet connection error caused by a chunk that failed to load as wallet', async () => {
196
+ const event = await eventFor(
197
+ attemptError([failure('Solana', chunkLoadFailure())])
198
+ );
199
+
200
+ expect(event?.tags.category).toBe('wallet');
201
+ });
202
+
203
+ it('classifies any other wallet connection error as wallet', async () => {
204
+ expect(
205
+ (await eventFor(attemptError([failure('Solana', new Error('boom'))])))
206
+ ?.tags.category
207
+ ).toBe('wallet');
208
+ expect(
209
+ (
210
+ await eventFor(
211
+ attemptError([
212
+ failure(
213
+ 'EVM',
214
+ errorWithCode(
215
+ 'Request already pending.',
216
+ REQUEST_ALREADY_PENDING_CODE
217
+ )
218
+ ),
219
+ ])
220
+ )
221
+ )?.tags.category
222
+ ).toBe('wallet');
223
+ expect(
224
+ (
225
+ await eventFor(
226
+ attemptError([
227
+ failure(
228
+ 'Solana',
229
+ new Error('No accounts were returned by the provider')
230
+ ),
231
+ ])
232
+ )
233
+ )?.tags.category
234
+ ).toBe('wallet');
235
+ });
236
+
237
+ it('classifies a failure that is not a wallet connection error as unknown', async () => {
238
+ expect(
239
+ (await eventFor(attemptError([new Error('Wallet is not registered.')])))
240
+ ?.tags.category
241
+ ).toBe('unknown');
242
+ expect(
243
+ (await eventFor(attemptError(['connection closed'])))?.tags.category
244
+ ).toBe('unknown');
245
+ });
246
+
247
+ it('classifies a failure that is itself a chunk that failed to load as unknown', async () => {
248
+ expect(
249
+ (await eventFor(attemptError([chunkLoadFailure()])))?.tags.category
250
+ ).toBe('unknown');
251
+ });
252
+
253
+ it('reads the code from the failure, not from its cause', async () => {
254
+ const walletFailure = new ForeignWalletConnectionError('EVM', 'boom');
255
+ Object.assign(walletFailure, {
256
+ cause: errorWithCode('User rejected.', USER_REJECTION_ERROR_CODE),
257
+ });
258
+
259
+ expect((await eventFor(attemptError([walletFailure])))?.tags.category).toBe(
260
+ 'wallet'
261
+ );
262
+ });
263
+
264
+ it('recognizes a wallet connection error from another copy of the package by its name', async () => {
265
+ const foreign = new ForeignWalletConnectionError('Solana', 'boom');
266
+
267
+ expect(foreign).not.toBeInstanceOf(WalletConnectionError);
268
+ expect((await eventFor(attemptError([foreign])))?.tags.category).toBe(
269
+ 'wallet'
270
+ );
271
+ expect(
272
+ await eventFor(
273
+ attemptError([
274
+ new ForeignWalletConnectionError(
275
+ 'EVM',
276
+ 'User rejected.',
277
+ USER_REJECTION_ERROR_CODE
278
+ ),
279
+ ])
280
+ )
281
+ ).toBeNull();
282
+ });
283
+ });
284
+
285
+ describe('resolving the category of a failed attempt', () => {
286
+ it('builds no event when every failure is a rejection or a locked wallet', async () => {
287
+ expect(
288
+ await eventFor(attemptError([rejection('EVM'), locked('Solana')]))
289
+ ).toBeNull();
290
+ });
291
+
292
+ it('resolves to wallet when only wallet connection errors are logged, uncoded or pending ones included', async () => {
293
+ const event = await eventFor(
294
+ attemptError([
295
+ failure('EVM', new Error('boom')),
296
+ failure(
297
+ 'Solana',
298
+ errorWithCode(
299
+ 'Request already pending.',
300
+ REQUEST_ALREADY_PENDING_CODE
301
+ )
302
+ ),
303
+ ])
304
+ );
305
+
306
+ expect(event?.tags.category).toBe('wallet');
307
+ });
308
+
309
+ it('resolves to unknown when any logged failure is not a wallet connection error', async () => {
310
+ const event = await eventFor(
311
+ attemptError([failure('EVM', new Error('boom')), new Error('bug')])
312
+ );
313
+
314
+ expect(event?.tags.category).toBe('unknown');
315
+ });
316
+
317
+ it('logs an attempt whose failures all failed to load a chunk', async () => {
318
+ const event = await eventFor(
319
+ attemptError([failure('UTXO', chunkLoadFailure()), chunkLoadFailure()])
320
+ );
321
+
322
+ expect(event?.tags.category).toBe('unknown');
323
+ });
324
+
325
+ it('logs an attempt that mixes ignored and logged failures', async () => {
326
+ const event = await eventFor(
327
+ attemptError([rejection('EVM'), failure('Solana', new Error('boom'))])
328
+ );
329
+
330
+ expect(event?.tags.category).toBe('wallet');
331
+ });
332
+
333
+ it('treats a value that is not an attempt error as an attempt with that value as its only failure', async () => {
334
+ const configError = new Error('Wallet is not registered.');
335
+ const event = await eventFor(configError);
336
+
337
+ expect(event?.tags.category).toBe('unknown');
338
+ expect(event?.context.failures).toEqual([
339
+ { name: 'Error', message: 'Wallet is not registered.' },
340
+ ]);
341
+ expect((await eventFor(chunkLoadFailure()))?.tags.category).toBe('unknown');
342
+ expect(await eventFor(rejection('EVM'))).toBeNull();
343
+ });
344
+
345
+ it('recognizes an attempt error from another copy of the package by its name', async () => {
346
+ const foreign = Object.assign(
347
+ new AggregateError([failure('Solana', new Error('boom'))], 'boom'),
348
+ { name: 'WalletConnectionAttemptError', requestedNamespaces: REQUESTED }
349
+ );
350
+
351
+ const event = await eventFor(foreign);
352
+
353
+ expect(event?.tags.category).toBe('wallet');
354
+ expect(event?.tags.namespaces).toBe('Solana');
355
+ });
356
+ });
357
+
358
+ describe('choosing the error to log', () => {
359
+ it('logs the received attempt error itself, unchanged', async () => {
360
+ const failures = [rejection('EVM'), failure('Solana', new Error('boom'))];
361
+ const received = attemptError(failures);
362
+
363
+ const event = await eventFor(received);
364
+
365
+ expect(event?.error).toBe(received);
366
+ expect(received.errors).toEqual(failures);
367
+ expect(received.message).toBe('User rejected the request.');
368
+ });
369
+
370
+ it('logs a received value that is not an error as an attempt error holding it', async () => {
371
+ const event = await eventFor('connection closed');
372
+
373
+ expect(event?.error).toBeInstanceOf(WalletConnectionAttemptError);
374
+ expect((event?.error as WalletConnectionAttemptError).errors).toEqual([
375
+ 'connection closed',
376
+ ]);
377
+ expect(event?.error.message).toBe('connection closed');
378
+ });
379
+ });
380
+
381
+ describe('tagging a failed attempt', () => {
382
+ it('lists only the namespaces of the logged wallet connection errors, sorted', async () => {
383
+ const event = await eventFor(
384
+ attemptError(
385
+ [
386
+ failure('Solana', new Error('boom')),
387
+ rejection('UTXO'),
388
+ failure('EVM', new Error('bang')),
389
+ ],
390
+ [...REQUESTED, { namespace: 'UTXO', network: undefined }]
391
+ )
392
+ );
393
+
394
+ expect(event?.tags.namespaces).toBe('EVM,Solana');
395
+ });
396
+
397
+ it('lists a namespace and a network asked for or blamed twice only once', async () => {
398
+ const event = await eventFor(
399
+ attemptError(
400
+ [failure('EVM', new Error('boom')), failure('EVM', new Error('bang'))],
401
+ [
402
+ { namespace: 'EVM', network: 'POLYGON' },
403
+ { namespace: 'EVM', network: 'POLYGON' },
404
+ ]
405
+ )
406
+ );
407
+
408
+ expect(event?.tags.namespaces).toBe('EVM');
409
+ expect(event?.tags.network).toBe('POLYGON');
410
+ });
411
+
412
+ it('falls back to the requested namespaces for an unknown event', async () => {
413
+ const event = await eventFor(
414
+ attemptError([failure('Solana', new Error('boom')), new Error('bug')])
415
+ );
416
+
417
+ expect(event?.tags.namespaces).toBe('EVM,Solana');
418
+ });
419
+
420
+ it('takes the network the attempt requested for each tagged namespace', async () => {
421
+ const event = await eventFor(
422
+ attemptError(
423
+ [failure('EVM', new Error('boom'))],
424
+ [
425
+ { namespace: 'EVM', network: 'POLYGON' },
426
+ { namespace: 'Solana', network: 'SOLANA' },
427
+ ]
428
+ )
429
+ );
430
+
431
+ expect(event?.tags.network).toBe('POLYGON');
432
+ });
433
+
434
+ it('sorts the networks of every requested namespace when no namespace can be blamed', async () => {
435
+ const event = await eventFor(
436
+ attemptError(
437
+ [new Error('bug')],
438
+ [
439
+ { namespace: 'Solana', network: 'SOLANA' },
440
+ { namespace: 'EVM', network: 'BSC' },
441
+ ]
442
+ )
443
+ );
444
+
445
+ expect(event?.tags.network).toBe('BSC,SOLANA');
446
+ });
447
+
448
+ it('omits the network tag when no network was requested, an empty string included', async () => {
449
+ const event = await eventFor(
450
+ attemptError(
451
+ [failure('EVM', new Error('boom'))],
452
+ [{ namespace: 'EVM', network: '' }]
453
+ )
454
+ );
455
+
456
+ expect(event?.tags).not.toHaveProperty('network');
457
+ });
458
+
459
+ it('omits the namespaces tag when there are none', async () => {
460
+ const event = await eventFor(new Error('boom'), {
461
+ ...ATTEMPT,
462
+ requestedNamespaces: [],
463
+ });
464
+
465
+ expect(event?.tags).not.toHaveProperty('namespaces');
466
+ });
467
+
468
+ it('tags the wallet, the namespaces, the network, the trigger and the category, and nothing else', async () => {
469
+ const event = await eventFor(
470
+ attemptError(
471
+ [failure('EVM', new Error('boom'))],
472
+ [{ namespace: 'EVM', network: 'POLYGON' }]
473
+ )
474
+ );
475
+
476
+ expect(event?.tags).toEqual({
477
+ walletType: 'phantom',
478
+ namespaces: 'EVM',
479
+ network: 'POLYGON',
480
+ trigger: 'wallets-page',
481
+ category: 'wallet',
482
+ });
483
+ });
484
+ });
485
+
486
+ describe('describing a failed attempt', () => {
487
+ it('describes every failure of the attempt, the ignored ones included', async () => {
488
+ const event = await eventFor(
489
+ attemptError([
490
+ rejection('EVM'),
491
+ failure(
492
+ 'Solana',
493
+ errorWithCode(
494
+ 'Provider is unavailable.',
495
+ PROVIDER_INTERNAL_ERROR_CODE
496
+ )
497
+ ),
498
+ new Error('bug'),
499
+ ])
500
+ );
501
+
502
+ expect(event?.context.failures).toEqual([
503
+ {
504
+ namespace: 'EVM',
505
+ name: 'WalletConnectionError',
506
+ message: 'User rejected the request.',
507
+ code: USER_REJECTION_ERROR_CODE,
508
+ },
509
+ {
510
+ namespace: 'Solana',
511
+ name: 'WalletConnectionError',
512
+ message: 'Provider is unavailable.',
513
+ code: PROVIDER_INTERNAL_ERROR_CODE,
514
+ },
515
+ { name: 'Error', message: 'bug' },
516
+ ]);
517
+ });
518
+
519
+ it('lists the requested namespaces in request order', async () => {
520
+ const requested: NamespaceInputForConnect[] = [
521
+ { namespace: 'Solana', network: undefined },
522
+ { namespace: 'EVM', network: undefined },
523
+ ];
524
+
525
+ expect(
526
+ (await eventFor(attemptError([new Error('bug')], requested)))?.context
527
+ .requestedNamespaces
528
+ ).toEqual(['Solana', 'EVM']);
529
+ });
530
+
531
+ it('keeps the requested namespaces present when there are none', async () => {
532
+ const event = await eventFor(new Error('boom'), {
533
+ ...ATTEMPT,
534
+ requestedNamespaces: [],
535
+ });
536
+
537
+ expect(event?.context.requestedNamespaces).toEqual([]);
538
+ });
539
+
540
+ it('describes failures only by namespace, name, message and code, never by accounts', async () => {
541
+ const cause = Object.assign(new Error('boom'), {
542
+ accounts: ['eip155:1:0x1234567890abcdef'],
543
+ address: '0x1234567890abcdef',
544
+ });
545
+
546
+ const event = await eventFor(attemptError([failure('EVM', cause)]));
547
+
548
+ expect(event?.context).toEqual({
549
+ failures: [
550
+ { namespace: 'EVM', name: 'WalletConnectionError', message: 'boom' },
551
+ ],
552
+ requestedNamespaces: ['EVM', 'Solana'],
553
+ });
554
+ });
555
+ });
556
+
557
+ describe('logging an attempt with a failure that cannot be read', () => {
558
+ it('logs a failure whose name cannot be read as unknown', async () => {
559
+ const unreadableName = withUnreadable(
560
+ new Error('Provider is unavailable.'),
561
+ 'name'
562
+ );
563
+
564
+ const event = await eventFor(attemptError([unreadableName]));
565
+
566
+ expect(event?.tags.category).toBe('unknown');
567
+ expect(event?.context.failures).toEqual([
568
+ { name: '(unreadable)', message: 'Provider is unavailable.' },
569
+ ]);
570
+ });
571
+
572
+ it('logs a wallet connection error whose code or cause cannot be read', async () => {
573
+ const unreadableCode = withUnreadable(
574
+ new ForeignWalletConnectionError('EVM', 'boom'),
575
+ 'code'
576
+ );
577
+ const unreadableCause = withUnreadable(
578
+ new ForeignWalletConnectionError('Solana', 'bang'),
579
+ 'cause'
580
+ );
581
+
582
+ const event = await eventFor(
583
+ attemptError([unreadableCode, unreadableCause])
584
+ );
585
+
586
+ expect(event?.tags.category).toBe('wallet');
587
+ expect(event?.tags.namespaces).toBe('EVM,Solana');
588
+ expect(event?.context.failures).toEqual([
589
+ { namespace: 'EVM', name: 'WalletConnectionError', message: 'boom' },
590
+ { namespace: 'Solana', name: 'WalletConnectionError', message: 'bang' },
591
+ ]);
592
+ });
593
+
594
+ it('stands in for a message that cannot be read', async () => {
595
+ const unreadableMessage = withUnreadable(
596
+ new ForeignWalletConnectionError(
597
+ 'EVM',
598
+ 'boom',
599
+ PROVIDER_INTERNAL_ERROR_CODE
600
+ ),
601
+ 'message'
602
+ );
603
+ const received = new WalletConnectionAttemptError({
604
+ errors: [unreadableMessage],
605
+ requestedNamespaces: REQUESTED,
606
+ message: 'boom',
607
+ });
608
+
609
+ expect((await eventFor(received))?.context.failures).toEqual([
610
+ {
611
+ namespace: 'EVM',
612
+ name: 'WalletConnectionError',
613
+ message: 'Failure could not be described',
614
+ code: PROVIDER_INTERNAL_ERROR_CODE,
615
+ },
616
+ ]);
617
+ });
618
+
619
+ it('leaves a namespace that cannot be read out of the failure and the tags', async () => {
620
+ const unreadableNamespace = withUnreadable(
621
+ new ForeignWalletConnectionError('EVM', 'boom'),
622
+ 'namespace'
623
+ );
624
+
625
+ const event = await eventFor(
626
+ attemptError([unreadableNamespace, failure('Solana', new Error('bang'))])
627
+ );
628
+
629
+ expect(event?.tags.namespaces).toBe('Solana');
630
+ expect(event?.context.failures[0]).toEqual({
631
+ name: 'WalletConnectionError',
632
+ message: 'boom',
633
+ });
634
+ });
635
+
636
+ it('carries only the two stand-ins for a failure nothing can be read from', async () => {
637
+ const event = await eventFor(
638
+ attemptError([failure('Solana', new Error('boom')), unreadable()])
639
+ );
640
+
641
+ expect(event?.tags.category).toBe('unknown');
642
+ expect(event?.context.failures[1]).toEqual({
643
+ name: '(unreadable)',
644
+ message: 'Failure could not be described',
645
+ });
646
+ });
647
+
648
+ it('carries only the two stand-ins for a failure that is not an object', async () => {
649
+ const event = await eventFor(
650
+ attemptError(['connection closed', THROWN_NUMBER])
651
+ );
652
+
653
+ expect(event?.context.failures).toEqual([
654
+ { name: '(unreadable)', message: 'Failure could not be described' },
655
+ { name: '(unreadable)', message: 'Failure could not be described' },
656
+ ]);
657
+ });
658
+
659
+ it('stands in for a missing message instead of stringifying the failure', async () => {
660
+ const plainFailure = {
661
+ name: 'ProviderError',
662
+ accounts: ['eip155:1:0x1234567890abcdef'],
663
+ toString: () => 'connection failed for 0x1234567890abcdef',
664
+ };
665
+
666
+ expect(
667
+ (await eventFor(attemptError([plainFailure])))?.context.failures
668
+ ).toEqual([
669
+ { name: 'ProviderError', message: 'Failure could not be described' },
670
+ ]);
671
+ });
672
+
673
+ it('leaves tag values that cannot be read or are not strings out of the tags', async () => {
674
+ const requested = [
675
+ { namespace: 'EVM', network: Symbol('POLYGON') },
676
+ withUnreadable({ namespace: 'Solana', network: 'SOLANA' }, 'network'),
677
+ withUnreadable({ namespace: 'UTXO', network: 'BTC' }, 'namespace'),
678
+ { namespace: 'Tron', network: 'TRON' },
679
+ ] as unknown as NamespaceInputForConnect[];
680
+
681
+ const event = await eventFor(attemptError([new Error('bug')], requested));
682
+
683
+ expect(event?.tags.namespaces).toBe('EVM,Solana,Tron');
684
+ expect(event?.tags.network).toBe('TRON');
685
+ });
686
+ });
687
+
688
+ describe('sending the event', () => {
689
+ it('logs every category at error level with its tags and context', async () => {
690
+ const received = attemptError([failure('Solana', new Error('boom'))]);
691
+ const bug = new Error('bug');
692
+
693
+ await expect(
694
+ runConnectWithLogging(ATTEMPT, async () => Promise.reject(received))
695
+ ).rejects.toBe(received);
696
+ await expect(
697
+ runConnectWithLogging(ATTEMPT, async () => Promise.reject(bug))
698
+ ).rejects.toBe(bug);
699
+
700
+ expect(loggedWarn).not.toHaveBeenCalled();
701
+ expect(loggedError).toHaveBeenCalledTimes(2);
702
+ expect(loggedError.mock.calls[0]).toEqual([
703
+ received,
704
+ {
705
+ tags: {
706
+ walletType: 'phantom',
707
+ namespaces: 'Solana',
708
+ trigger: 'wallets-page',
709
+ category: 'wallet',
710
+ },
711
+ context: {
712
+ failures: [
713
+ {
714
+ namespace: 'Solana',
715
+ name: 'WalletConnectionError',
716
+ message: 'boom',
717
+ },
718
+ ],
719
+ requestedNamespaces: ['EVM', 'Solana'],
720
+ },
721
+ },
722
+ ]);
723
+ expect(loggedError.mock.calls[1]?.[1]?.tags).toEqual(
724
+ expect.objectContaining({ category: 'unknown' })
725
+ );
726
+ });
727
+
728
+ it('sends nothing for an attempt with no logged failures', async () => {
729
+ const ignored = attemptError([rejection('EVM')]);
730
+
731
+ await expect(
732
+ runConnectWithLogging(ATTEMPT, async () => Promise.reject(ignored))
733
+ ).rejects.toBe(ignored);
734
+
735
+ expect(loggedError).not.toHaveBeenCalled();
736
+ });
737
+
738
+ it('drops an event it cannot send, without throwing', async () => {
739
+ loggedError.mockImplementationOnce(() => {
740
+ throw new Error('The logger is not ready.');
741
+ });
742
+ const bug = new Error('bug');
743
+
744
+ await expect(
745
+ runConnectWithLogging(ATTEMPT, async () => Promise.reject(bug))
746
+ ).rejects.toBe(bug);
747
+ });
748
+ });
749
+
750
+ describe('logging an auto-connect failure', () => {
751
+ it('logs the auto-connection attempt error unchanged with the auto-connect trigger', () => {
752
+ const received = new AutoConnectionAttemptError({
753
+ errors: [failure('EVM', new Error('boom'))],
754
+ requestedNamespaces: [
755
+ { namespace: 'EVM', network: 'POLYGON' },
756
+ { namespace: 'Solana', network: undefined },
757
+ ],
758
+ });
759
+
760
+ logAutoConnectFailure('metamask', received);
761
+
762
+ expect(loggedError).toHaveBeenCalledTimes(1);
763
+ expect(loggedError.mock.calls[0]?.[0]).toBe(received);
764
+ expect(loggedError.mock.calls[0]?.[1]).toEqual({
765
+ tags: {
766
+ walletType: 'metamask',
767
+ namespaces: 'EVM',
768
+ network: 'POLYGON',
769
+ trigger: 'auto-connect',
770
+ category: 'wallet',
771
+ },
772
+ context: {
773
+ failures: [
774
+ { namespace: 'EVM', name: 'WalletConnectionError', message: 'boom' },
775
+ ],
776
+ requestedNamespaces: ['EVM', 'Solana'],
777
+ },
778
+ });
779
+ });
780
+
781
+ it('logs nothing when every failure was rejected or locked', () => {
782
+ logAutoConnectFailure(
783
+ 'metamask',
784
+ new AutoConnectionAttemptError({
785
+ errors: [rejection('EVM'), locked('Solana')],
786
+ requestedNamespaces: REQUESTED,
787
+ })
788
+ );
789
+
790
+ expect(loggedError).not.toHaveBeenCalled();
791
+ });
792
+ });
793
+
794
+ describe('running a connection attempt', () => {
795
+ beforeEach(() => {
796
+ vi.useFakeTimers();
797
+ });
798
+
799
+ afterEach(() => {
800
+ vi.useRealTimers();
801
+ });
802
+
803
+ it('logs one timeout event when the attempt is still pending after three minutes', async () => {
804
+ let settle: (() => void) | undefined;
805
+ const attempt = runConnectWithLogging(
806
+ ATTEMPT,
807
+ async () =>
808
+ new Promise<ConnectResult[]>((resolve) => {
809
+ settle = () => resolve(CONNECTED);
810
+ })
811
+ );
812
+
813
+ vi.advanceTimersByTime(THREE_MINUTES);
814
+
815
+ expect(loggedError).toHaveBeenCalledTimes(1);
816
+ expect(loggedError.mock.calls[0]?.[0].message).toBe(
817
+ 'Wallet connection still pending after 3 minutes'
818
+ );
819
+ expect(loggedError.mock.calls[0]?.[1]?.tags).toEqual({
820
+ walletType: 'phantom',
821
+ namespaces: 'EVM,Solana',
822
+ trigger: 'wallets-page',
823
+ category: 'timeout',
824
+ });
825
+
826
+ settle?.();
827
+ await expect(attempt).resolves.toBe(CONNECTED);
828
+
829
+ vi.advanceTimersByTime(THREE_MINUTES);
830
+ expect(loggedError).toHaveBeenCalledTimes(1);
831
+ });
832
+
833
+ it('logs a timeout error with no failures, how long the attempt waited and the requested namespaces with their networks', () => {
834
+ const event = timeoutEventFor({
835
+ ...ATTEMPT,
836
+ requestedNamespaces: [
837
+ { namespace: 'EVM', network: 'POLYGON', derivationPath: "44'/60'/0'" },
838
+ { namespace: 'Solana', network: undefined },
839
+ ],
840
+ });
841
+ const error = event?.error as WalletConnectionAttemptError;
842
+
843
+ expect(error).toBeInstanceOf(WalletConnectionAttemptError);
844
+ expect(error.errors).toEqual([]);
845
+ expect(error.message).toBe(
846
+ 'Wallet connection still pending after 3 minutes'
847
+ );
848
+ expect(error.requestedNamespaces).toEqual([
849
+ { namespace: 'EVM', network: 'POLYGON' },
850
+ { namespace: 'Solana', network: undefined },
851
+ ]);
852
+ });
853
+
854
+ it('describes a timeout with no failures and the requested namespaces', () => {
855
+ expect(timeoutEventFor()?.context).toEqual({
856
+ failures: [],
857
+ requestedNamespaces: ['EVM', 'Solana'],
858
+ });
859
+ });
860
+
861
+ it('logs nothing when the attempt settles within three minutes', async () => {
862
+ await expect(
863
+ runConnectWithLogging(ATTEMPT, async () => CONNECTED)
864
+ ).resolves.toBe(CONNECTED);
865
+
866
+ vi.advanceTimersByTime(THREE_MINUTES);
867
+
868
+ expect(loggedError).not.toHaveBeenCalled();
869
+ });
870
+
871
+ it('logs a failure that arrives after the timeout as its own event', async () => {
872
+ let fail: ((reason: unknown) => void) | undefined;
873
+ const received = attemptError([failure('Solana', new Error('boom'))]);
874
+ const attempt = runConnectWithLogging(
875
+ ATTEMPT,
876
+ async () =>
877
+ new Promise<ConnectResult[]>((_, reject) => {
878
+ fail = reject;
879
+ })
880
+ );
881
+
882
+ vi.advanceTimersByTime(THREE_MINUTES);
883
+ fail?.(received);
884
+
885
+ await expect(attempt).rejects.toBe(received);
886
+ expect(loggedError.mock.calls.map(([, data]) => data?.tags)).toEqual([
887
+ expect.objectContaining({ category: 'timeout' }),
888
+ expect.objectContaining({ category: 'wallet' }),
889
+ ]);
890
+ expect(loggedError.mock.calls[1]?.[0]).toBe(received);
891
+ });
892
+
893
+ it('lets no throw escape the timer when the timeout event cannot be sent', () => {
894
+ loggedError.mockImplementationOnce(() => {
895
+ throw new Error('The logger is not ready.');
896
+ });
897
+
898
+ void runConnectWithLogging(
899
+ ATTEMPT,
900
+ async () => new Promise<ConnectResult[]>(() => undefined)
901
+ );
902
+
903
+ expect(() => vi.advanceTimersByTime(THREE_MINUTES)).not.toThrow();
904
+ });
905
+
906
+ it('rethrows what connect threw, unchanged, whether it was logged or not', async () => {
907
+ const ignored = attemptError([rejection('EVM')]);
908
+ const logged = attemptError([failure('EVM', new Error('boom'))]);
909
+
910
+ await expect(
911
+ runConnectWithLogging(ATTEMPT, async () => Promise.reject(ignored))
912
+ ).rejects.toBe(ignored);
913
+ await expect(
914
+ runConnectWithLogging(ATTEMPT, async () => Promise.reject(logged))
915
+ ).rejects.toBe(logged);
916
+ expect(loggedError).toHaveBeenCalledTimes(1);
917
+ });
918
+ });