@lifi/widget 4.10.0 → 4.10.2

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 (91) hide show
  1. package/README.md +2 -7
  2. package/dist/esm/components/ActionRow/ActionRow.d.ts +1 -0
  3. package/dist/esm/components/ExecutionStatusCard/ExecutionStatusCard.d.ts +1 -0
  4. package/dist/esm/components/ExecutionStatusCard/StatusIcon.d.ts +1 -0
  5. package/dist/esm/components/FeeBreakdownTooltip.d.ts +1 -0
  6. package/dist/esm/components/IconCircle/IconCircle.d.ts +1 -0
  7. package/dist/esm/components/Messages/WarningMessages.d.ts +1 -0
  8. package/dist/esm/components/RouteCard/RouteDetails.d.ts +1 -0
  9. package/dist/esm/components/Search/SearchInput.d.ts +1 -0
  10. package/dist/esm/components/StepActions/SentToWalletRow.d.ts +1 -0
  11. package/dist/esm/components/StepActions/StepActionsList.d.ts +1 -0
  12. package/dist/esm/components/TokenRate/TokenRate.d.ts +1 -0
  13. package/dist/esm/config/version.d.ts +1 -1
  14. package/dist/esm/config/version.js +1 -1
  15. package/dist/esm/config/version.js.map +1 -1
  16. package/dist/esm/hooks/useAddressActivity.d.ts +1 -0
  17. package/dist/esm/hooks/useAddressValidation.d.ts +1 -0
  18. package/dist/esm/hooks/useAvailableChains.d.ts +1 -0
  19. package/dist/esm/hooks/useExplorer.d.ts +1 -0
  20. package/dist/esm/hooks/useListHeight.d.ts +1 -0
  21. package/dist/esm/hooks/usePacedValue.js +1 -1
  22. package/dist/esm/hooks/usePacedValue.js.map +1 -1
  23. package/dist/esm/hooks/useRouteExecution.d.ts +1 -0
  24. package/dist/esm/hooks/useRouteExecutionMessage.d.ts.map +1 -1
  25. package/dist/esm/hooks/useRouteExecutionMessage.js +4 -8
  26. package/dist/esm/hooks/useRouteExecutionMessage.js.map +1 -1
  27. package/dist/esm/hooks/useRoutes.d.ts +1 -0
  28. package/dist/esm/i18n/bn.json +567 -270
  29. package/dist/esm/i18n/de.json +552 -255
  30. package/dist/esm/i18n/en.json +7 -61
  31. package/dist/esm/i18n/es.json +522 -225
  32. package/dist/esm/i18n/fr.json +541 -244
  33. package/dist/esm/i18n/hi.json +592 -295
  34. package/dist/esm/i18n/id.json +485 -188
  35. package/dist/esm/i18n/it.json +546 -249
  36. package/dist/esm/i18n/ja.json +512 -215
  37. package/dist/esm/i18n/ko.json +553 -256
  38. package/dist/esm/i18n/pl.json +598 -301
  39. package/dist/esm/i18n/pt.json +512 -215
  40. package/dist/esm/i18n/th.json +554 -257
  41. package/dist/esm/i18n/tr.json +538 -241
  42. package/dist/esm/i18n/uk.json +514 -217
  43. package/dist/esm/i18n/vi.json +550 -253
  44. package/dist/esm/i18n/zh.json +528 -231
  45. package/dist/esm/pages/MainPage/MainWarningMessages.d.ts +1 -0
  46. package/dist/esm/pages/SelectTokenPage/SearchTokenInput.d.ts +1 -0
  47. package/dist/esm/pages/TransactionDetailsPage/ContactSupportButton.d.ts +1 -0
  48. package/dist/esm/pages/TransactionPage/ConfirmToAddressSheet.d.ts +1 -0
  49. package/dist/esm/pages/TransactionPage/ExchangeRateBottomSheet.d.ts +1 -0
  50. package/dist/esm/pages/TransactionPage/ExecutionDoneCard.d.ts +1 -0
  51. package/dist/esm/pages/TransactionPage/RouteTracker.d.ts +1 -0
  52. package/dist/esm/pages/TransactionPage/TokenValueBottomSheet.d.ts +1 -0
  53. package/dist/esm/pages/TransactionPage/TransactionDoneButtons.d.ts +1 -0
  54. package/dist/esm/pages/TransactionPage/TransactionFailedButtons.js +70 -18
  55. package/dist/esm/pages/TransactionPage/TransactionFailedButtons.js.map +1 -1
  56. package/dist/esm/pages/TransactionPage/utils.d.ts +3 -0
  57. package/dist/esm/pages/TransactionPage/utils.d.ts.map +1 -1
  58. package/dist/esm/pages/TransactionPage/utils.js +27 -1
  59. package/dist/esm/pages/TransactionPage/utils.js.map +1 -1
  60. package/dist/esm/providers/I18nProvider/I18nProvider.js +2 -2
  61. package/dist/esm/providers/I18nProvider/I18nProvider.js.map +1 -1
  62. package/dist/esm/providers/I18nProvider/enResource.d.ts +2 -2
  63. package/dist/esm/providers/I18nProvider/enResource.js +1 -2
  64. package/dist/esm/providers/I18nProvider/types.d.ts +2 -2
  65. package/dist/esm/providers/I18nProvider/types.d.ts.map +1 -1
  66. package/dist/esm/providers/WalletProvider/WalletProvider.d.ts +1 -0
  67. package/dist/esm/shared.d.ts +2 -2
  68. package/dist/esm/shared.js +2 -2
  69. package/dist/esm/stores/chains/types.d.ts +1 -0
  70. package/dist/esm/stores/form/types.d.ts +1 -0
  71. package/dist/esm/stores/header/types.d.ts +1 -0
  72. package/dist/esm/stores/inputMode/useInputModeStore.d.ts +1 -0
  73. package/dist/esm/stores/routes/utils.d.ts +1 -1
  74. package/dist/esm/stores/routes/utils.d.ts.map +1 -1
  75. package/dist/esm/stores/routes/utils.js +13 -1
  76. package/dist/esm/stores/routes/utils.js.map +1 -1
  77. package/dist/esm/utils/getErrorMessage.js +4 -0
  78. package/dist/esm/utils/getErrorMessage.js.map +1 -1
  79. package/dist/esm/utils/getPriceImpact.d.ts +1 -0
  80. package/dist/esm/utils/navigationRoutes.d.ts +1 -0
  81. package/dist/esm/utils/scopeKeys.d.ts +1 -0
  82. package/package.json +11 -11
  83. package/src/config/version.ts +1 -1
  84. package/src/hooks/usePacedValue.ts +1 -1
  85. package/src/hooks/useRouteExecutionMessage.ts +10 -13
  86. package/src/pages/TransactionPage/TransactionFailedButtons.test.tsx +497 -0
  87. package/src/pages/TransactionPage/TransactionFailedButtons.tsx +78 -9
  88. package/src/pages/TransactionPage/utils.ts +81 -0
  89. package/src/shared.ts +1 -0
  90. package/src/stores/routes/utils.ts +24 -1
  91. package/src/utils/getErrorMessage.ts +5 -0
@@ -25,4 +25,5 @@ export declare const navigationRoutes: {
25
25
  type NavigationRouteTypeKeys = keyof typeof navigationRoutes;
26
26
  export type NavigationRouteType = (typeof navigationRoutes)[NavigationRouteTypeKeys];
27
27
  //#endregion
28
+ export {};
28
29
  //# sourceMappingURL=navigationRoutes.d.ts.map
@@ -3,4 +3,5 @@ import { WidgetConfig } from "../types/widget.js";
3
3
  type ScopeKeyConfig = Pick<WidgetConfig, "storageScopeKey" | "queryScopeKey" | "keyPrefix">;
4
4
  export declare const resolveStorageScopeKey: (config: Partial<ScopeKeyConfig> | undefined) => string | undefined;
5
5
  //#endregion
6
+ export {};
6
7
  //# sourceMappingURL=scopeKeys.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifi/widget",
3
- "version": "4.10.0",
3
+ "version": "4.10.2",
4
4
  "description": "LI.FI Widget for cross-chain bridging and swapping. It will drive your multi-chain strategy and attract new users from everywhere.",
5
5
  "type": "module",
6
6
  "main": "./dist/esm/index.js",
@@ -57,18 +57,18 @@
57
57
  "dependencies": {
58
58
  "@emotion/react": "^11.14.0",
59
59
  "@emotion/styled": "^11.14.1",
60
- "@lifi/sdk": "^4.11.0",
61
- "@lifi/wallet-management": "4.3.2",
62
- "@lifi/widget-provider": "4.6.0",
63
- "@mui/icons-material": "^9.4.0",
64
- "@mui/material": "^9.4.0",
65
- "@mui/system": "^9.4.0",
66
- "@tanstack/react-router": "^1.170.41",
67
- "@tanstack/react-virtual": "^3.14.13",
60
+ "@lifi/sdk": "^4.12.1",
61
+ "@lifi/wallet-management": "4.3.4",
62
+ "@lifi/widget-provider": "4.6.2",
63
+ "@mui/icons-material": "^9.5.0",
64
+ "@mui/material": "^9.5.0",
65
+ "@mui/system": "^9.5.0",
66
+ "@tanstack/react-router": "^1.170.42",
67
+ "@tanstack/react-virtual": "^3.14.17",
68
68
  "eventemitter3": "^5.0.4",
69
- "i18next": "^26.4.2",
69
+ "i18next": "^26.4.4",
70
70
  "microdiff": "^1.6.0",
71
- "motion": "^14.0.0",
71
+ "motion": "^14.1.0",
72
72
  "react-i18next": "^17.0.16",
73
73
  "react-intersection-observer": "^11.0.1",
74
74
  "react-transition-group": "^4.4.5",
@@ -1,2 +1,2 @@
1
1
  export const name = '@lifi/widget'
2
- export const version = '4.10.0'
2
+ export const version = '4.10.2'
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useRef, useState } from 'react'
2
2
 
3
3
  /** Smallest gap between UI updates, in milliseconds. */
4
- const DISPLAY_INTERVAL = 1200
4
+ export const DISPLAY_INTERVAL = 1200
5
5
 
6
6
  /**
7
7
  * Returns `live`, but changes it at most once every {@link DISPLAY_INTERVAL}
@@ -1,7 +1,8 @@
1
- import type { ExecutionActionType, RouteExtended } from '@lifi/sdk'
1
+ import type { RouteExtended } from '@lifi/sdk'
2
2
  import { useTranslation } from 'react-i18next'
3
3
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
4
4
  import { RouteExecutionStatus } from '../stores/routes/types.js'
5
+ import { getFailedStepAction } from '../stores/routes/utils.js'
5
6
  import { getActionMessage } from '../utils/getActionMessage.js'
6
7
  import { getErrorMessage } from '../utils/getErrorMessage.js'
7
8
  import { useAvailableChains } from './useAvailableChains.js'
@@ -76,20 +77,16 @@ export const useRouteExecutionMessage = (
76
77
  break
77
78
  }
78
79
  case RouteExecutionStatus.Failed: {
79
- const step = route.steps.find(
80
- (step) => step.execution?.status === 'FAILED'
81
- )
82
- if (!step) {
80
+ const failed = getFailedStepAction(route)
81
+ if (!failed) {
83
82
  break
84
83
  }
85
- const action = step.execution?.actions?.find(
86
- (action) => action.status === 'FAILED'
87
- ) || {
88
- status: 'FAILED',
89
- type: 'EXECUTION' as ExecutionActionType,
90
- error: step.execution?.error,
91
- } // synthetic action to represent a failed execution with no actions
92
- const actionMessage = getErrorMessage(t, getChainById, step, action)
84
+ const actionMessage = getErrorMessage(
85
+ t,
86
+ getChainById,
87
+ failed.step,
88
+ failed.action
89
+ )
93
90
  title = actionMessage.title
94
91
  message = actionMessage.message
95
92
  break
@@ -0,0 +1,497 @@
1
+ /** @vitest-environment happy-dom */
2
+
3
+ import {
4
+ LiFiErrorCode,
5
+ type LiFiStepExtended,
6
+ type RouteExtended,
7
+ } from '@lifi/sdk'
8
+ import {
9
+ createMemoryHistory,
10
+ createRootRoute,
11
+ createRoute,
12
+ createRouter,
13
+ Outlet,
14
+ RouterProvider,
15
+ } from '@tanstack/react-router'
16
+ import type { ReactNode } from 'react'
17
+ import { createRoot, type Root } from 'react-dom/client'
18
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
19
+ import { create, type StoreApi, type UseBoundStore } from 'zustand'
20
+ import { DISPLAY_INTERVAL } from '../../hooks/usePacedValue.js'
21
+ import type { WidgetContextProps } from '../../providers/WidgetProvider/types.js'
22
+ import { WidgetContext } from '../../providers/WidgetProvider/WidgetProvider.js'
23
+ import { BookmarkStoreProvider } from '../../stores/bookmarks/BookmarkStore.js'
24
+ import type { Bookmark } from '../../stores/bookmarks/types.js'
25
+ import { useBookmarkActions } from '../../stores/bookmarks/useBookmarkActions.js'
26
+ import { useBookmarks } from '../../stores/bookmarks/useBookmarks.js'
27
+ import { createFormStore } from '../../stores/form/createFormStore.js'
28
+ import { FormStoreContext } from '../../stores/form/FormStoreContext.js'
29
+ import type { FormStoreStore } from '../../stores/form/types.js'
30
+ import { RouteExecutionStatus } from '../../stores/routes/types.js'
31
+
32
+ // Real page, buttons, router, form store and pacing; only wallet, SDK and theme leaves are stubbed.
33
+
34
+ const SENDER = '0x1111111111111111111111111111111111111111'
35
+
36
+ interface TestRouteState {
37
+ route: RouteExtended
38
+ status: RouteExecutionStatus | undefined
39
+ setStatus: (status: RouteExecutionStatus) => void
40
+ deleteRoute: () => void
41
+ }
42
+
43
+ type TestRouteStore = UseBoundStore<StoreApi<TestRouteState>>
44
+
45
+ const { routeState } = vi.hoisted(() => ({
46
+ routeState: { store: undefined as unknown },
47
+ }))
48
+ const testRouteStore = () => routeState.store as TestRouteStore
49
+ const useTestRoute = <T,>(selector: (state: TestRouteState) => T): T =>
50
+ testRouteStore()(selector)
51
+
52
+ vi.mock('../../hooks/useRouteExecution.js', () => ({
53
+ useRouteExecution: () => {
54
+ const { route, status, deleteRoute } = useTestRoute((state) => state)
55
+ return {
56
+ route: status ? route : undefined,
57
+ status,
58
+ deleteRoute,
59
+ executeRoute: () => {},
60
+ restartRoute: () => {},
61
+ }
62
+ },
63
+ }))
64
+ vi.mock('react-i18next', () => ({
65
+ useTranslation: () => ({ t: (key: string) => key }),
66
+ }))
67
+ vi.mock('../../hooks/useHeader.js', () => ({ useHeader: () => {} }))
68
+ vi.mock('../../hooks/useAddressActivity.js', () => ({
69
+ useAddressActivity: () => ({
70
+ toAddress: undefined,
71
+ hasActivity: true,
72
+ isLoading: false,
73
+ isFetched: true,
74
+ }),
75
+ }))
76
+ vi.mock('../../hooks/useSwapOnly.js', () => ({ useSwapOnly: () => false }))
77
+ vi.mock('../../components/PageContainer.js', () => ({
78
+ PageContainer: ({ children }: { children?: ReactNode }) => children,
79
+ }))
80
+ vi.mock('./RouteTracker.js', () => ({ RouteTracker: () => null }))
81
+ vi.mock('./ExchangeRateBottomSheet.js', () => ({
82
+ ExchangeRateBottomSheet: () => null,
83
+ }))
84
+ vi.mock('./StartTransactionButton.js', () => ({
85
+ StartTransactionButton: ({ text }: { text?: string }) => (
86
+ <button type="button">{text}</button>
87
+ ),
88
+ }))
89
+ vi.mock('./TokenValueBottomSheet.js', () => ({
90
+ TokenValueBottomSheet: () => null,
91
+ }))
92
+ vi.mock('./ConfirmToAddressSheet.js', () => ({
93
+ ConfirmToAddressSheet: () => null,
94
+ }))
95
+ vi.mock('./TransactionContent.js', async () => {
96
+ const { TransactionFailedButtons } = await import(
97
+ './TransactionFailedButtons.js'
98
+ )
99
+ return {
100
+ TransactionContent: ({
101
+ route,
102
+ status,
103
+ restartRoute,
104
+ deleteRoute,
105
+ }: {
106
+ route: RouteExtended
107
+ status: RouteExecutionStatus
108
+ restartRoute: () => void
109
+ deleteRoute: () => void
110
+ }) =>
111
+ status === RouteExecutionStatus.Failed ? (
112
+ <TransactionFailedButtons
113
+ route={route}
114
+ restartRoute={restartRoute}
115
+ deleteRoute={deleteRoute}
116
+ />
117
+ ) : null,
118
+ }
119
+ })
120
+
121
+ const { TransactionPage } = await import('./TransactionPage.js')
122
+
123
+ const failedExecution = (code: LiFiErrorCode) => ({
124
+ status: 'FAILED',
125
+ startedAt: 0,
126
+ actions: [{ type: 'SWAP', status: 'FAILED', error: { code, message: '' } }],
127
+ })
128
+
129
+ const step = {
130
+ id: 'step-0',
131
+ action: {
132
+ fromChainId: 1,
133
+ fromAmount: '1500000',
134
+ fromToken: { address: '0xfrom', decimals: 6, chainId: 1 },
135
+ fromAddress: SENDER,
136
+ toChainId: 8453,
137
+ toToken: { address: '0xto', decimals: 6, chainId: 8453 },
138
+ toAddress: SENDER,
139
+ },
140
+ estimate: { gasCosts: [], feeCosts: [] },
141
+ execution: failedExecution(LiFiErrorCode.CallBundleNotFound),
142
+ } as unknown as LiFiStepExtended
143
+
144
+ const route = {
145
+ id: 'r1',
146
+ fromChainId: 1,
147
+ fromToken: step.action.fromToken,
148
+ fromAmount: step.action.fromAmount,
149
+ fromAddress: SENDER,
150
+ toChainId: 8453,
151
+ toToken: step.action.toToken,
152
+ toAddress: SENDER,
153
+ fromAmountUSD: '1',
154
+ toAmountUSD: '1',
155
+ steps: [step],
156
+ } as unknown as RouteExtended
157
+
158
+ const SOLANA_RECEIVER = 'So1anaReceiver11111111111111111111111111111'
159
+ const routeToSolana = {
160
+ ...route,
161
+ toChainId: 1151111081099710,
162
+ toAddress: SOLANA_RECEIVER,
163
+ steps: [
164
+ {
165
+ ...step,
166
+ action: {
167
+ ...step.action,
168
+ toChainId: 1151111081099710,
169
+ toAddress: SOLANA_RECEIVER,
170
+ },
171
+ },
172
+ ],
173
+ } as unknown as RouteExtended
174
+
175
+ const routeFailedWith = (code: LiFiErrorCode) =>
176
+ ({
177
+ ...route,
178
+ steps: [{ ...step, execution: failedExecution(code) }],
179
+ }) as unknown as RouteExtended
180
+
181
+ // The first step is done, so a new swap would send its funds again.
182
+ const routeFailedInLaterStep = {
183
+ ...route,
184
+ steps: [
185
+ {
186
+ ...step,
187
+ execution: {
188
+ status: 'DONE',
189
+ startedAt: 0,
190
+ actions: [{ type: 'SWAP', status: 'DONE', txHash: '0xhash' }],
191
+ },
192
+ },
193
+ { ...step, id: 'step-1' },
194
+ ],
195
+ } as unknown as RouteExtended
196
+
197
+ let formStore: FormStoreStore
198
+ let bookmarks: {
199
+ selected?: Bookmark
200
+ select?: (bookmark?: Bookmark) => void
201
+ }
202
+
203
+ const BookmarkProbe = (): null => {
204
+ const { selectedBookmark } = useBookmarks()
205
+ const { setSelectedBookmark } = useBookmarkActions()
206
+ bookmarks.selected = selectedBookmark
207
+ bookmarks.select = setSelectedBookmark
208
+ return null
209
+ }
210
+
211
+ const makeRouter = () => {
212
+ const rootRoute = createRootRoute({
213
+ component: () => (
214
+ <>
215
+ <BookmarkProbe />
216
+ <Outlet />
217
+ </>
218
+ ),
219
+ })
220
+ const home = createRoute({
221
+ getParentRoute: () => rootRoute,
222
+ path: '/',
223
+ component: () => <div id="home" />,
224
+ })
225
+ const transaction = createRoute({
226
+ getParentRoute: () => rootRoute,
227
+ path: '/transaction-execution',
228
+ component: TransactionPage,
229
+ })
230
+ return createRouter({
231
+ routeTree: rootRoute.addChildren([home, transaction]),
232
+ history: createMemoryHistory({
233
+ initialEntries: ['/', '/transaction-execution?routeId=r1'],
234
+ }),
235
+ })
236
+ }
237
+
238
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
239
+
240
+ // Longer than one pacing window, so the paced status has caught up.
241
+ const PAST_PACING = DISPLAY_INTERVAL + 300
242
+
243
+ const waitFor = async (check: () => boolean) => {
244
+ for (let i = 0; i < 100 && !check(); i++) {
245
+ await sleep(20)
246
+ }
247
+ expect(check()).toBe(true)
248
+ }
249
+
250
+ let container: HTMLDivElement
251
+ let root: Root
252
+ let router: ReturnType<typeof makeRouter>
253
+
254
+ const render = async (
255
+ status: RouteExecutionStatus,
256
+ config: Partial<WidgetContextProps> = {},
257
+ testRoute: RouteExtended = route
258
+ ) => {
259
+ routeState.store = create<TestRouteState>((set) => ({
260
+ route: testRoute,
261
+ status,
262
+ setStatus: (status) => set({ status }),
263
+ deleteRoute: () => set({ status: undefined }),
264
+ }))
265
+ router = makeRouter()
266
+ root.render(
267
+ <WidgetContext
268
+ value={
269
+ { elementId: '', integrator: '', mode: 'default', ...config } as never
270
+ }
271
+ >
272
+ <FormStoreContext value={formStore}>
273
+ <BookmarkStoreProvider namePrefix="test">
274
+ <RouterProvider router={router} />
275
+ </BookmarkStoreProvider>
276
+ </FormStoreContext>
277
+ </WidgetContext>
278
+ )
279
+ await waitFor(
280
+ () =>
281
+ router.state.status === 'idle' &&
282
+ router.state.location.pathname === '/transaction-execution'
283
+ )
284
+ }
285
+
286
+ const newSwapButton = () =>
287
+ [...container.querySelectorAll('button')].find(
288
+ (button) => button.textContent === 'button.startNewSwap'
289
+ )
290
+
291
+ const deleteButton = () =>
292
+ [...container.querySelectorAll('button')].find(
293
+ (button) => button.textContent === 'button.delete'
294
+ )
295
+
296
+ // The mocks leave only the failed-route buttons on the page.
297
+ const buttonTexts = () =>
298
+ [...container.querySelectorAll('button')].map((button) => button.textContent)
299
+
300
+ const field = (name: 'fromAmount' | 'toAmount' | 'toAddress' | 'fromToken') =>
301
+ formStore.getState().userValues[name]?.value
302
+
303
+ beforeEach(() => {
304
+ ;(
305
+ globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
306
+ ).IS_REACT_ACT_ENVIRONMENT = false
307
+ vi.spyOn(console, 'warn').mockImplementation(() => {})
308
+ container = document.createElement('div')
309
+ document.body.appendChild(container)
310
+ root = createRoot(container)
311
+ formStore = createFormStore()
312
+ bookmarks = {}
313
+ })
314
+
315
+ afterEach(() => {
316
+ root.unmount()
317
+ container.remove()
318
+ localStorage.clear()
319
+ vi.restoreAllMocks()
320
+ })
321
+
322
+ describe('The buttons of a failed route', () => {
323
+ it('should offer Delete and a new swap, and no retry, when the wallet has no record of the bundle', async () => {
324
+ await render(RouteExecutionStatus.Failed)
325
+ await waitFor(() => buttonTexts().length > 0)
326
+
327
+ expect(buttonTexts()).toEqual(['button.delete', 'button.startNewSwap'])
328
+ })
329
+
330
+ it('should offer only Delete when the wallet has no record of the bundle and a new swap is not possible', async () => {
331
+ await render(RouteExecutionStatus.Failed, {}, routeFailedInLaterStep)
332
+ await waitFor(() => buttonTexts().length > 0)
333
+
334
+ expect(buttonTexts()).toEqual(['button.delete'])
335
+ })
336
+
337
+ it('should offer Delete and a retry for other errors', async () => {
338
+ await render(
339
+ RouteExecutionStatus.Failed,
340
+ {},
341
+ routeFailedWith(LiFiErrorCode.SignatureRejected)
342
+ )
343
+ await waitFor(() => buttonTexts().length > 0)
344
+
345
+ expect(buttonTexts()).toEqual(['button.delete', 'button.tryAgain'])
346
+ })
347
+
348
+ it('should delete the route and not fill the form when Delete is clicked', async () => {
349
+ await render(RouteExecutionStatus.Failed)
350
+ await waitFor(() => !!newSwapButton())
351
+
352
+ deleteButton()!.click()
353
+
354
+ await waitFor(() => !!container.querySelector('#home'))
355
+ await sleep(300)
356
+ expect(testRouteState().status).toBeUndefined()
357
+ expect(field('fromAmount')).toBeFalsy()
358
+ })
359
+ })
360
+
361
+ describe('Start a new swap and the transaction page cleanup', () => {
362
+ // Within one pacing window the page still holds the failed status when it unmounts.
363
+ it('should keep the filled amount when the route failed just before the click', async () => {
364
+ await render(RouteExecutionStatus.Pending)
365
+ formStore.getState().setFieldValue('toAmount', '7')
366
+ testRouteState().setStatus(RouteExecutionStatus.Failed)
367
+ await waitFor(() => !!newSwapButton())
368
+
369
+ newSwapButton()!.click()
370
+
371
+ await waitFor(() => !!container.querySelector('#home'))
372
+ await sleep(PAST_PACING)
373
+ expect(router.state.location.pathname).toBe('/')
374
+ expect(field('fromAmount')).toBe('1.5')
375
+ expect(field('fromToken')).toBe('0xfrom')
376
+ expect(field('toAmount')).toBe('')
377
+ })
378
+
379
+ it('should fill the form for a route that had already failed', async () => {
380
+ await render(RouteExecutionStatus.Failed)
381
+ await waitFor(() => !!newSwapButton())
382
+
383
+ newSwapButton()!.click()
384
+
385
+ await waitFor(() => !!container.querySelector('#home'))
386
+ await sleep(PAST_PACING)
387
+ expect(field('fromAmount')).toBe('1.5')
388
+ expect(field('toAddress')).toBe('')
389
+ })
390
+
391
+ it('should clear the receiver and the bookmark when the receiver is open to change', async () => {
392
+ await render(RouteExecutionStatus.Failed)
393
+ await waitFor(() => !!newSwapButton() && !!bookmarks.select)
394
+ formStore.getState().setFieldValue('toAddress', '0xold')
395
+ bookmarks.select?.({
396
+ name: 'Old',
397
+ address: '0xold',
398
+ chainType: 'EVM',
399
+ } as Bookmark)
400
+
401
+ newSwapButton()!.click()
402
+
403
+ await waitFor(() => !!container.querySelector('#home'))
404
+ await sleep(300)
405
+ expect(field('toAddress')).toBe('')
406
+ expect(bookmarks.selected).toBeUndefined()
407
+ })
408
+
409
+ it('should keep the bookmark that labels the same receiver', async () => {
410
+ await render(RouteExecutionStatus.Failed, {}, routeToSolana)
411
+ await waitFor(() => !!newSwapButton() && !!bookmarks.select)
412
+ const solana = {
413
+ name: 'Solana',
414
+ address: SOLANA_RECEIVER,
415
+ chainType: 'SVM',
416
+ } as Bookmark
417
+ bookmarks.select?.(solana)
418
+
419
+ newSwapButton()!.click()
420
+
421
+ await waitFor(() => !!container.querySelector('#home'))
422
+ await sleep(300)
423
+ expect(field('toAddress')).toBe(SOLANA_RECEIVER)
424
+ expect(bookmarks.selected).toEqual(solana)
425
+ })
426
+
427
+ it('should keep a required receiver that is the sender', async () => {
428
+ // A route to the sender has no top-level receiver.
429
+ await render(
430
+ RouteExecutionStatus.Failed,
431
+ { requiredUI: { toAddress: true } },
432
+ { ...route, toAddress: undefined }
433
+ )
434
+ await waitFor(() => !!newSwapButton())
435
+
436
+ newSwapButton()!.click()
437
+
438
+ await waitFor(() => !!container.querySelector('#home'))
439
+ await sleep(300)
440
+ expect(field('toAddress')).toBe(SENDER)
441
+ })
442
+
443
+ it("should keep the integrator's receiver and bookmark when the receiver is hidden", async () => {
444
+ await render(RouteExecutionStatus.Failed, { hiddenUI: { toAddress: true } })
445
+ await waitFor(() => !!newSwapButton() && !!bookmarks.select)
446
+ formStore.getState().setFieldValue('toAddress', '0xlocked')
447
+ const locked = {
448
+ name: 'Locked',
449
+ address: '0xlocked',
450
+ chainType: 'EVM',
451
+ } as Bookmark
452
+ bookmarks.select?.(locked)
453
+
454
+ newSwapButton()!.click()
455
+
456
+ await waitFor(() => !!container.querySelector('#home'))
457
+ await sleep(300)
458
+ expect(field('fromAmount')).toBe('1.5')
459
+ expect(field('toAddress')).toBe('0xlocked')
460
+ expect(bookmarks.selected).toEqual(locked)
461
+ })
462
+
463
+ it("should keep the integrator's receiver when the receiver is locked", async () => {
464
+ await render(RouteExecutionStatus.Failed, {
465
+ disabledUI: { toAddress: true },
466
+ })
467
+ await waitFor(() => !!newSwapButton())
468
+ formStore.getState().setFieldValue('toAddress', '0xlocked')
469
+
470
+ newSwapButton()!.click()
471
+
472
+ await waitFor(() => !!container.querySelector('#home'))
473
+ await sleep(300)
474
+ expect(field('fromAmount')).toBe('1.5')
475
+ expect(field('toAddress')).toBe('0xlocked')
476
+ })
477
+
478
+ it("should fill the route's receiver when a locked receiver is empty", async () => {
479
+ await render(
480
+ RouteExecutionStatus.Failed,
481
+ { disabledUI: { toAddress: true } },
482
+ routeToSolana
483
+ )
484
+ await waitFor(() => !!newSwapButton())
485
+
486
+ newSwapButton()!.click()
487
+
488
+ await waitFor(() => !!container.querySelector('#home'))
489
+ await sleep(300)
490
+ expect(field('fromAmount')).toBe('1.5')
491
+ expect(field('toAddress')).toBe(SOLANA_RECEIVER)
492
+ })
493
+ })
494
+
495
+ function testRouteState(): TestRouteState {
496
+ return testRouteStore().getState()
497
+ }