@lifi/widget 4.9.0 → 4.10.1
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/README.md +28 -10
- package/dist/esm/components/TokenList/TokenDetailsSheet.js +5 -8
- package/dist/esm/components/TokenList/TokenDetailsSheet.js.map +1 -1
- package/dist/esm/components/TokenList/TokenDetailsSheetContent.js +30 -2
- package/dist/esm/components/TokenList/TokenDetailsSheetContent.js.map +1 -1
- package/dist/esm/components/TokenList/TokenListItem.js +36 -47
- package/dist/esm/components/TokenList/TokenListItem.js.map +1 -1
- package/dist/esm/components/TokenList/VirtualizedTokenList.d.ts.map +1 -1
- package/dist/esm/components/TokenList/VirtualizedTokenList.js +2 -2
- package/dist/esm/components/TokenList/VirtualizedTokenList.js.map +1 -1
- package/dist/esm/components/TokenList/tokenVerificationIcons.js +29 -0
- package/dist/esm/components/TokenList/tokenVerificationIcons.js.map +1 -0
- package/dist/esm/config/version.d.ts +1 -1
- package/dist/esm/config/version.js +1 -1
- package/dist/esm/config/version.js.map +1 -1
- package/dist/esm/hooks/usePacedValue.js +1 -1
- package/dist/esm/hooks/usePacedValue.js.map +1 -1
- package/dist/esm/hooks/useRouteExecutionMessage.d.ts.map +1 -1
- package/dist/esm/hooks/useRouteExecutionMessage.js +4 -8
- package/dist/esm/hooks/useRouteExecutionMessage.js.map +1 -1
- package/dist/esm/i18n/bn.json +567 -266
- package/dist/esm/i18n/de.json +552 -251
- package/dist/esm/i18n/en.json +12 -62
- package/dist/esm/i18n/es.json +522 -221
- package/dist/esm/i18n/fr.json +541 -240
- package/dist/esm/i18n/hi.json +592 -291
- package/dist/esm/i18n/id.json +485 -184
- package/dist/esm/i18n/it.json +546 -245
- package/dist/esm/i18n/ja.json +512 -211
- package/dist/esm/i18n/ko.json +553 -252
- package/dist/esm/i18n/pl.json +598 -297
- package/dist/esm/i18n/pt.json +513 -212
- package/dist/esm/i18n/th.json +554 -253
- package/dist/esm/i18n/tr.json +538 -237
- package/dist/esm/i18n/uk.json +514 -213
- package/dist/esm/i18n/vi.json +550 -249
- package/dist/esm/i18n/zh.json +528 -227
- package/dist/esm/pages/TransactionPage/TransactionFailedButtons.js +70 -18
- package/dist/esm/pages/TransactionPage/TransactionFailedButtons.js.map +1 -1
- package/dist/esm/pages/TransactionPage/utils.d.ts +3 -0
- package/dist/esm/pages/TransactionPage/utils.d.ts.map +1 -1
- package/dist/esm/pages/TransactionPage/utils.js +27 -1
- package/dist/esm/pages/TransactionPage/utils.js.map +1 -1
- package/dist/esm/shared.d.ts +2 -2
- package/dist/esm/shared.js +2 -2
- package/dist/esm/stores/routes/utils.d.ts +1 -1
- package/dist/esm/stores/routes/utils.d.ts.map +1 -1
- package/dist/esm/stores/routes/utils.js +13 -1
- package/dist/esm/stores/routes/utils.js.map +1 -1
- package/dist/esm/utils/getErrorMessage.js +4 -0
- package/dist/esm/utils/getErrorMessage.js.map +1 -1
- package/dist/esm/utils/token.js +13 -1
- package/dist/esm/utils/token.js.map +1 -1
- package/package.json +4 -4
- package/src/components/TokenList/TokenDetailsSheet.tsx +5 -9
- package/src/components/TokenList/TokenDetailsSheetContent.tsx +46 -4
- package/src/components/TokenList/TokenListItem.tsx +44 -68
- package/src/components/TokenList/VirtualizedTokenList.tsx +3 -6
- package/src/components/TokenList/tokenVerificationIcons.ts +15 -0
- package/src/components/TokenList/types.ts +3 -11
- package/src/config/version.ts +1 -1
- package/src/hooks/usePacedValue.ts +1 -1
- package/src/hooks/useRouteExecutionMessage.ts +10 -13
- package/src/pages/TransactionPage/TransactionFailedButtons.test.tsx +497 -0
- package/src/pages/TransactionPage/TransactionFailedButtons.tsx +78 -9
- package/src/pages/TransactionPage/utils.ts +81 -0
- package/src/shared.ts +1 -0
- package/src/stores/routes/utils.ts +24 -1
- package/src/utils/getErrorMessage.ts +5 -0
- package/src/utils/token.ts +48 -0
|
@@ -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
|
+
}
|
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
import type { RouteExtended } from '@lifi/sdk'
|
|
2
2
|
import { Box, Button } from '@mui/material'
|
|
3
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
3
4
|
import { useRef } from 'react'
|
|
4
5
|
import { useTranslation } from 'react-i18next'
|
|
5
6
|
import type { BottomSheetBase } from '../../components/BottomSheet/types.js'
|
|
6
7
|
import { useAddressActivity } from '../../hooks/useAddressActivity.js'
|
|
7
8
|
import { useNavigateBack } from '../../hooks/useNavigateBack.js'
|
|
9
|
+
import { useSwapOnly } from '../../hooks/useSwapOnly.js'
|
|
8
10
|
import { useWidgetEvents } from '../../hooks/useWidgetEvents.js'
|
|
9
11
|
import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'
|
|
12
|
+
import { useBookmarkActions } from '../../stores/bookmarks/useBookmarkActions.js'
|
|
13
|
+
import { useFieldActions } from '../../stores/form/useFieldActions.js'
|
|
10
14
|
import { WidgetEvent } from '../../types/events.js'
|
|
11
15
|
import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js'
|
|
16
|
+
import { navigationRoutes } from '../../utils/navigationRoutes.js'
|
|
12
17
|
import { ConfirmToAddressSheet } from './ConfirmToAddressSheet.js'
|
|
13
18
|
import { StartTransactionButton } from './StartTransactionButton.js'
|
|
14
19
|
import { TokenValueBottomSheet } from './TokenValueBottomSheet.js'
|
|
15
20
|
import type { RetryGate } from './utils.js'
|
|
16
21
|
import {
|
|
17
22
|
calculateValueLossPercentage,
|
|
23
|
+
canStartNewSwap,
|
|
24
|
+
getNewSwapFormValues,
|
|
18
25
|
getRetryGates,
|
|
19
26
|
getTokenValueLossThreshold,
|
|
27
|
+
isCallBundleNotFound,
|
|
20
28
|
openNextGate,
|
|
21
29
|
} from './utils.js'
|
|
22
30
|
|
|
@@ -32,7 +40,11 @@ export const TransactionFailedButtons: React.FC<
|
|
|
32
40
|
const { t } = useTranslation()
|
|
33
41
|
const emitter = useWidgetEvents()
|
|
34
42
|
const navigateBack = useNavigateBack()
|
|
35
|
-
const
|
|
43
|
+
const navigate = useNavigate()
|
|
44
|
+
const { mode, hiddenUI, disabledUI, requiredUI } = useWidgetConfig()
|
|
45
|
+
const swapOnly = useSwapOnly()
|
|
46
|
+
const { setFieldValue, getFieldValues } = useFieldActions()
|
|
47
|
+
const { setSelectedBookmark, getSelectedBookmark } = useBookmarkActions()
|
|
36
48
|
|
|
37
49
|
const tokenValueBottomSheetRef = useRef<BottomSheetBase>(null)
|
|
38
50
|
const confirmToAddressSheetRef = useRef<BottomSheetBase>(null)
|
|
@@ -49,6 +61,49 @@ export const TransactionFailedButtons: React.FC<
|
|
|
49
61
|
deleteRoute()
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
const callBundleNotFound = isCallBundleNotFound(route)
|
|
65
|
+
const showStartNewSwap =
|
|
66
|
+
callBundleNotFound && canStartNewSwap({ route, mode, swapOnly })
|
|
67
|
+
// Try again would only wait for the same bundle again.
|
|
68
|
+
const showRetry = !callBundleNotFound
|
|
69
|
+
|
|
70
|
+
// Home, not back: after a reload, the route is opened from Activities.
|
|
71
|
+
const handleStartNewSwap = () => {
|
|
72
|
+
// An empty hidden or locked field still needs the route's receiver.
|
|
73
|
+
const [formReceiver] = getFieldValues('toAddress')
|
|
74
|
+
const keepReceiver = Boolean(
|
|
75
|
+
(disabledUI?.toAddress || hiddenUI?.toAddress) && formReceiver
|
|
76
|
+
)
|
|
77
|
+
const values = getNewSwapFormValues(route, {
|
|
78
|
+
keepReceiver,
|
|
79
|
+
receiverRequired: requiredUI?.toAddress,
|
|
80
|
+
})
|
|
81
|
+
const fieldNames = Object.keys(values) as (keyof typeof values)[]
|
|
82
|
+
const receiver = keepReceiver ? formReceiver : values.toAddress
|
|
83
|
+
// Fill after navigate resolves: this page's unmount cleanup has run by then,
|
|
84
|
+
// as TransactionFailedButtons.test.tsx checks.
|
|
85
|
+
navigate({ to: navigationRoutes.home, replace: true })
|
|
86
|
+
.then(() => {
|
|
87
|
+
for (const fieldName of fieldNames) {
|
|
88
|
+
setFieldValue(fieldName, values[fieldName], {
|
|
89
|
+
isDirty: true,
|
|
90
|
+
isTouched: true,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
// A bookmark name from an earlier receiver must not label this one.
|
|
94
|
+
if (
|
|
95
|
+
getSelectedBookmark()?.address.toLowerCase() !==
|
|
96
|
+
receiver?.toLowerCase()
|
|
97
|
+
) {
|
|
98
|
+
setSelectedBookmark()
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.catch((error) => {
|
|
102
|
+
console.warn('Filling the new swap failed.', error)
|
|
103
|
+
})
|
|
104
|
+
deleteRoute()
|
|
105
|
+
}
|
|
106
|
+
|
|
52
107
|
const retryGates = (() => {
|
|
53
108
|
const { gasCostUSD, feeCostUSD } = getAccumulatedFeeCostsBreakdown(route)
|
|
54
109
|
return getRetryGates({
|
|
@@ -112,14 +167,28 @@ export const TransactionFailedButtons: React.FC<
|
|
|
112
167
|
{t('button.delete')}
|
|
113
168
|
</Button>
|
|
114
169
|
</Box>
|
|
115
|
-
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
170
|
+
{showStartNewSwap ? (
|
|
171
|
+
<Box sx={{ flex: 1 }}>
|
|
172
|
+
<Button
|
|
173
|
+
variant="contained"
|
|
174
|
+
color="primary"
|
|
175
|
+
onClick={handleStartNewSwap}
|
|
176
|
+
fullWidth
|
|
177
|
+
>
|
|
178
|
+
{t('button.startNewSwap')}
|
|
179
|
+
</Button>
|
|
180
|
+
</Box>
|
|
181
|
+
) : null}
|
|
182
|
+
{showRetry ? (
|
|
183
|
+
<Box sx={{ flex: 1 }}>
|
|
184
|
+
<StartTransactionButton
|
|
185
|
+
text={t('button.tryAgain')}
|
|
186
|
+
onClick={handleRetryClick}
|
|
187
|
+
route={route}
|
|
188
|
+
loading={isLoadingAddressActivity}
|
|
189
|
+
/>
|
|
190
|
+
</Box>
|
|
191
|
+
) : null}
|
|
123
192
|
</Box>
|
|
124
193
|
{mode !== 'custom' ? (
|
|
125
194
|
<TokenValueBottomSheet
|