@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.
- package/README.md +2 -7
- package/dist/esm/components/ActionRow/ActionRow.d.ts +1 -0
- package/dist/esm/components/ExecutionStatusCard/ExecutionStatusCard.d.ts +1 -0
- package/dist/esm/components/ExecutionStatusCard/StatusIcon.d.ts +1 -0
- package/dist/esm/components/FeeBreakdownTooltip.d.ts +1 -0
- package/dist/esm/components/IconCircle/IconCircle.d.ts +1 -0
- package/dist/esm/components/Messages/WarningMessages.d.ts +1 -0
- package/dist/esm/components/RouteCard/RouteDetails.d.ts +1 -0
- package/dist/esm/components/Search/SearchInput.d.ts +1 -0
- package/dist/esm/components/StepActions/SentToWalletRow.d.ts +1 -0
- package/dist/esm/components/StepActions/StepActionsList.d.ts +1 -0
- package/dist/esm/components/TokenRate/TokenRate.d.ts +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/useAddressActivity.d.ts +1 -0
- package/dist/esm/hooks/useAddressValidation.d.ts +1 -0
- package/dist/esm/hooks/useAvailableChains.d.ts +1 -0
- package/dist/esm/hooks/useExplorer.d.ts +1 -0
- package/dist/esm/hooks/useListHeight.d.ts +1 -0
- package/dist/esm/hooks/usePacedValue.js +1 -1
- package/dist/esm/hooks/usePacedValue.js.map +1 -1
- package/dist/esm/hooks/useRouteExecution.d.ts +1 -0
- 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/hooks/useRoutes.d.ts +1 -0
- package/dist/esm/i18n/bn.json +567 -270
- package/dist/esm/i18n/de.json +552 -255
- package/dist/esm/i18n/en.json +7 -61
- package/dist/esm/i18n/es.json +522 -225
- package/dist/esm/i18n/fr.json +541 -244
- package/dist/esm/i18n/hi.json +592 -295
- package/dist/esm/i18n/id.json +485 -188
- package/dist/esm/i18n/it.json +546 -249
- package/dist/esm/i18n/ja.json +512 -215
- package/dist/esm/i18n/ko.json +553 -256
- package/dist/esm/i18n/pl.json +598 -301
- package/dist/esm/i18n/pt.json +512 -215
- package/dist/esm/i18n/th.json +554 -257
- package/dist/esm/i18n/tr.json +538 -241
- package/dist/esm/i18n/uk.json +514 -217
- package/dist/esm/i18n/vi.json +550 -253
- package/dist/esm/i18n/zh.json +528 -231
- package/dist/esm/pages/MainPage/MainWarningMessages.d.ts +1 -0
- package/dist/esm/pages/SelectTokenPage/SearchTokenInput.d.ts +1 -0
- package/dist/esm/pages/TransactionDetailsPage/ContactSupportButton.d.ts +1 -0
- package/dist/esm/pages/TransactionPage/ConfirmToAddressSheet.d.ts +1 -0
- package/dist/esm/pages/TransactionPage/ExchangeRateBottomSheet.d.ts +1 -0
- package/dist/esm/pages/TransactionPage/ExecutionDoneCard.d.ts +1 -0
- package/dist/esm/pages/TransactionPage/RouteTracker.d.ts +1 -0
- package/dist/esm/pages/TransactionPage/TokenValueBottomSheet.d.ts +1 -0
- package/dist/esm/pages/TransactionPage/TransactionDoneButtons.d.ts +1 -0
- 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/providers/I18nProvider/I18nProvider.js +2 -2
- package/dist/esm/providers/I18nProvider/I18nProvider.js.map +1 -1
- package/dist/esm/providers/I18nProvider/enResource.d.ts +2 -2
- package/dist/esm/providers/I18nProvider/enResource.js +1 -2
- package/dist/esm/providers/I18nProvider/types.d.ts +2 -2
- package/dist/esm/providers/I18nProvider/types.d.ts.map +1 -1
- package/dist/esm/providers/WalletProvider/WalletProvider.d.ts +1 -0
- package/dist/esm/shared.d.ts +2 -2
- package/dist/esm/shared.js +2 -2
- package/dist/esm/stores/chains/types.d.ts +1 -0
- package/dist/esm/stores/form/types.d.ts +1 -0
- package/dist/esm/stores/header/types.d.ts +1 -0
- package/dist/esm/stores/inputMode/useInputModeStore.d.ts +1 -0
- 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/getPriceImpact.d.ts +1 -0
- package/dist/esm/utils/navigationRoutes.d.ts +1 -0
- package/dist/esm/utils/scopeKeys.d.ts +1 -0
- package/package.json +11 -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
|
@@ -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.
|
|
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.
|
|
61
|
-
"@lifi/wallet-management": "4.3.
|
|
62
|
-
"@lifi/widget-provider": "4.6.
|
|
63
|
-
"@mui/icons-material": "^9.
|
|
64
|
-
"@mui/material": "^9.
|
|
65
|
-
"@mui/system": "^9.
|
|
66
|
-
"@tanstack/react-router": "^1.170.
|
|
67
|
-
"@tanstack/react-virtual": "^3.14.
|
|
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.
|
|
69
|
+
"i18next": "^26.4.4",
|
|
70
70
|
"microdiff": "^1.6.0",
|
|
71
|
-
"motion": "^14.
|
|
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",
|
package/src/config/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const name = '@lifi/widget'
|
|
2
|
-
export const version = '4.10.
|
|
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 {
|
|
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
|
|
80
|
-
|
|
81
|
-
)
|
|
82
|
-
if (!step) {
|
|
80
|
+
const failed = getFailedStepAction(route)
|
|
81
|
+
if (!failed) {
|
|
83
82
|
break
|
|
84
83
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
}
|