Skip to content

Commit 9bdacb5

Browse files
committed
feat: new status manager
1 parent 7e18f67 commit 9bdacb5

66 files changed

Lines changed: 7962 additions & 5158 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ build/
2121
*.tmp
2222

2323
npm-debug.log*
24+
/.pnpm-store/
2425

2526
.next
2627
.cache

docs/EXECUTION_FLOW.md

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
# Widget Execution Flow
2+
3+
This document explains how transaction execution works in the LI.FI widget.
4+
5+
## Overview
6+
7+
The execution system handles swap and bridge transactions through a coordinated flow involving:
8+
- **LI.FI SDK** - handles blockchain interactions
9+
- **Zustand Store** - manages persistent state
10+
- **React Query** - coordinates async operations with the UI
11+
- **Event Emitter** - notifies integrators of execution progress
12+
13+
---
14+
15+
## Execution Flow
16+
17+
### 1. Route Selection → Transaction Page
18+
19+
When a user selects a route (swap/bridge), they navigate to `TransactionPage`, which displays the route details via the `Checkout` component.
20+
21+
### 2. Starting Execution
22+
23+
The flow begins when user clicks "Start Swapping/Bridging":
24+
25+
```typescript
26+
// packages/widget/src/pages/TransactionPage/TransactionPage.tsx
27+
28+
const handleExecuteRoute = () => {
29+
tokenValueBottomSheetRef.current?.close()
30+
executeRoute() // Triggers execution
31+
setFieldValue('fromAmount', '')
32+
// ...
33+
}
34+
```
35+
36+
Before execution starts, the widget may show confirmation dialogs for:
37+
- **High value loss** - if the output value is significantly less than input
38+
- **Low address activity** - if sending to an address with no transaction history
39+
40+
### 3. Core Execution Hook: `useRouteExecution`
41+
42+
The `executeRoute` function comes from the `useRouteExecution` hook which wraps the `@lifi/sdk`:
43+
44+
```typescript
45+
// packages/widget/src/hooks/useRouteExecution.ts
46+
47+
const executeRouteMutation = useMutation({
48+
mutationFn: () => {
49+
if (!account.isConnected) {
50+
throw new Error('Account is not connected.')
51+
}
52+
if (!routeExecution?.route) {
53+
throw new Error('Execution route not found.')
54+
}
55+
56+
return executeRoute(sdkClient, routeExecution.route, {
57+
updateRouteHook, // Called on every status update
58+
acceptExchangeRateUpdateHook, // For rate change confirmations
59+
infiniteApproval: false,
60+
executeInBackground,
61+
...sdkClient.config?.executionOptions,
62+
})
63+
},
64+
onMutate: () => {
65+
emitter.emit(WidgetEvent.RouteExecutionStarted, routeExecution.route)
66+
},
67+
})
68+
```
69+
70+
---
71+
72+
## Execution Status Flow
73+
74+
Routes transition through these statuses during execution:
75+
76+
```typescript
77+
// packages/widget/src/stores/routes/types.ts
78+
79+
export enum RouteExecutionStatus {
80+
Idle = 1 << 0, // Not started
81+
Pending = 1 << 1, // In progress
82+
Done = 1 << 2, // Completed successfully
83+
Failed = 1 << 3, // Failed
84+
Partial = 1 << 4, // Partially completed
85+
Refunded = 1 << 5, // Refunded
86+
}
87+
```
88+
89+
### Status Diagram
90+
91+
```
92+
┌──────┐ Start ┌─────────┐
93+
│ Idle │ ─────────────► │ Pending │
94+
└──────┘ └────┬────┘
95+
96+
┌──────────────┼──────────────┐
97+
│ │ │
98+
▼ ▼ ▼
99+
┌────────┐ ┌──────────┐ ┌────────┐
100+
│ Done │ │ Partial │ │ Failed │
101+
└────────┘ └──────────┘ └───┬────┘
102+
103+
│ Retry
104+
105+
┌─────────┐
106+
│ Pending │
107+
└─────────┘
108+
```
109+
110+
---
111+
112+
## Real-time Updates
113+
114+
### The `updateRouteHook` Callback
115+
116+
The SDK calls `updateRouteHook` whenever the route state changes:
117+
118+
```typescript
119+
// packages/widget/src/hooks/useRouteExecution.ts
120+
121+
const updateRouteHook = (updatedRoute: Route) => {
122+
const routeExecution = routeExecutionStoreContext.getState().routes[updatedRoute.id]
123+
if (!routeExecution) return
124+
125+
const clonedUpdatedRoute = structuredClone(updatedRoute)
126+
updateRoute(clonedUpdatedRoute)
127+
128+
// Detect execution changes
129+
const execution = getUpdatedExecution(routeExecution.route, clonedUpdatedRoute)
130+
131+
if (execution) {
132+
emitter.emit(WidgetEvent.RouteExecutionUpdated, {
133+
route: clonedUpdatedRoute,
134+
execution,
135+
})
136+
}
137+
138+
// Check completion status
139+
const executionCompleted = isRouteDone(clonedUpdatedRoute)
140+
const executionFailed = isRouteFailed(clonedUpdatedRoute)
141+
142+
if (executionCompleted) {
143+
emitter.emit(WidgetEvent.RouteExecutionCompleted, clonedUpdatedRoute)
144+
}
145+
146+
if (executionFailed && execution) {
147+
emitter.emit(WidgetEvent.RouteExecutionFailed, {
148+
route: clonedUpdatedRoute,
149+
execution,
150+
})
151+
}
152+
153+
// Invalidate token balance queries on completion
154+
if (executionCompleted || executionFailed) {
155+
queryClient.invalidateQueries({ queryKey: ['token-balances', ...] })
156+
}
157+
}
158+
```
159+
160+
### Route Status Helpers
161+
162+
```typescript
163+
// packages/widget/src/stores/routes/utils.ts
164+
165+
export const isRouteDone = (route: RouteExtended) => {
166+
return route.steps.every((step) => step.execution?.status === 'DONE')
167+
}
168+
169+
export const isRoutePartiallyDone = (route: RouteExtended) => {
170+
return route.steps.some((step) => step.execution?.substatus === 'PARTIAL')
171+
}
172+
173+
export const isRouteRefunded = (route: RouteExtended) => {
174+
return route.steps.some((step) => step.execution?.substatus === 'REFUNDED')
175+
}
176+
177+
export const isRouteFailed = (route: RouteExtended) => {
178+
return route.steps.some((step) => step.execution?.status === 'FAILED')
179+
}
180+
181+
export const isRouteActive = (route?: RouteExtended) => {
182+
if (!route) return false
183+
const isDone = isRouteDone(route)
184+
const isFailed = isRouteFailed(route)
185+
const alreadyStarted = route.steps.some((step) => step.execution)
186+
return !isDone && !isFailed && alreadyStarted
187+
}
188+
```
189+
190+
---
191+
192+
## State Persistence
193+
194+
### Zustand Store
195+
196+
Routes are stored in a persisted Zustand store so executions survive page reloads:
197+
198+
```typescript
199+
// packages/widget/src/stores/routes/createRouteExecutionStore.ts
200+
201+
export const createRouteExecutionStore = ({ namePrefix }: PersistStoreProps) =>
202+
create<RouteExecutionState>()(
203+
persist(
204+
(set, get) => ({
205+
routes: {},
206+
207+
setExecutableRoute: (route: Route, observableRouteIds?: string[]) => {
208+
// Stores new route with Idle status
209+
// Cleans up previous idle and done routes
210+
},
211+
212+
updateRoute: (route: RouteExtended) => {
213+
// Updates route and automatically sets status based on step execution:
214+
// - FAILED if any step failed
215+
// - DONE if all steps done (with Partial/Refunded flags if applicable)
216+
// - PENDING if any step has execution in progress
217+
},
218+
219+
deleteRoute: (routeId: string) => { /* ... */ },
220+
deleteRoutes: (type: 'completed' | 'active') => { /* ... */ },
221+
}),
222+
{
223+
name: `${namePrefix || 'li.fi'}-widget-routes`,
224+
version: 2,
225+
// Auto-cleanup: removes failed transactions after 1 day
226+
}
227+
)
228+
)
229+
```
230+
231+
---
232+
233+
## Auto-Resume on Page Reload
234+
235+
If a user refreshes the page mid-execution, the route automatically resumes:
236+
237+
```typescript
238+
// packages/widget/src/hooks/useRouteExecution.ts
239+
240+
useEffect(() => {
241+
const route = routeExecutionStoreContext.getState().routes[routeId]?.route
242+
243+
// Auto-resume active routes after mount
244+
if (isRouteActive(route) && account.isConnected && !resumedAfterMount.current) {
245+
resumedAfterMount.current = true
246+
_resumeRoute()
247+
}
248+
249+
// Move execution to background on unmount
250+
return () => {
251+
const route = routeExecutionStoreContext.getState().routes[routeId]?.route
252+
if (route && isRouteActive(route)) {
253+
updateRouteExecution(route, { executeInBackground: true })
254+
}
255+
}
256+
}, [account.isConnected, routeExecutionStoreContext, routeId])
257+
```
258+
259+
---
260+
261+
## Widget Events
262+
263+
The widget emits events throughout the execution lifecycle that integrators can listen to:
264+
265+
| Event | Payload | When |
266+
|-------|---------|------|
267+
| `RouteExecutionStarted` | `Route` | Execution begins |
268+
| `RouteExecutionUpdated` | `{ route: Route, execution: Execution }` | Each execution status change |
269+
| `RouteExecutionCompleted` | `Route` | All steps completed successfully |
270+
| `RouteExecutionFailed` | `{ route: Route, execution: Execution }` | Any step fails |
271+
| `RouteHighValueLoss` | `{ fromAmountUSD, toAmountUSD, gasCostUSD, feeCostUSD, valueLoss }` | User confirms high value loss |
272+
273+
### Listening to Events
274+
275+
```typescript
276+
import { useWidgetEvents, WidgetEvent } from '@lifi/widget'
277+
278+
const widgetEvents = useWidgetEvents()
279+
280+
useEffect(() => {
281+
const onRouteExecutionCompleted = (route) => {
282+
console.log('Route completed!', route)
283+
}
284+
285+
widgetEvents.on(WidgetEvent.RouteExecutionCompleted, onRouteExecutionCompleted)
286+
287+
return () => {
288+
widgetEvents.off(WidgetEvent.RouteExecutionCompleted, onRouteExecutionCompleted)
289+
}
290+
}, [widgetEvents])
291+
```
292+
293+
---
294+
295+
## UI Components
296+
297+
### `StepExecution`
298+
299+
Displays the current execution status with a progress indicator and transaction links:
300+
301+
```typescript
302+
// packages/widget/src/components/Checkout/StepExecution.tsx
303+
304+
export const StepExecution: React.FC<{ step: LiFiStepExtended }> = ({ step }) => {
305+
const { title } = useExecutionMessage(step)
306+
const { getTransactionLink } = useExplorer()
307+
308+
if (!step.execution) return null
309+
310+
// Renders:
311+
// - CircularProgress indicator
312+
// - Status message (e.g., "Waiting for signature", "Swap pending")
313+
// - Transaction links when available
314+
}
315+
```
316+
317+
### Execution Messages
318+
319+
Status messages are mapped based on transaction type and execution status:
320+
321+
```typescript
322+
// packages/widget/src/hooks/useExecutionMessage.ts
323+
324+
const processStatusMessages = {
325+
TOKEN_ALLOWANCE: {
326+
STARTED: 'Approving token allowance...',
327+
ACTION_REQUIRED: 'Please approve {tokenSymbol} in your wallet',
328+
PENDING: 'Waiting for approval confirmation...',
329+
DONE: '{tokenSymbol} approved',
330+
},
331+
SWAP: {
332+
STARTED: 'Preparing swap...',
333+
ACTION_REQUIRED: 'Please confirm the swap in your wallet',
334+
PENDING: 'Swap pending...',
335+
DONE: 'Swap completed',
336+
},
337+
CROSS_CHAIN: {
338+
STARTED: 'Preparing bridge transaction...',
339+
ACTION_REQUIRED: 'Please confirm the bridge in your wallet',
340+
PENDING: 'Bridge transaction pending...',
341+
DONE: 'Bridge completed',
342+
},
343+
// ...
344+
}
345+
```
346+
347+
---
348+
349+
## Architecture Summary
350+
351+
```
352+
┌─────────────────────────────────────────────────────────────────┐
353+
│ TransactionPage │
354+
│ ┌───────────────────────────────────────────────────────────┐ │
355+
│ │ Checkout │ │
356+
│ │ ┌─────────────────┐ ┌────────────────────────────────┐ │ │
357+
│ │ │ StepExecution │ │ Token Details & Route Info │ │ │
358+
│ │ │ (status, tx) │ │ │ │ │
359+
│ │ └─────────────────┘ └────────────────────────────────┘ │ │
360+
│ └───────────────────────────────────────────────────────────┘ │
361+
└─────────────────────────────────────────────────────────────────┘
362+
363+
│ executeRoute()
364+
365+
┌─────────────────────────────────────────────────────────────────┐
366+
│ useRouteExecution │
367+
│ - Wraps @lifi/sdk executeRoute/resumeRoute │
368+
│ - Manages mutations via React Query │
369+
│ - Emits widget events │
370+
│ - Handles auto-resume on mount │
371+
└─────────────────────────────────────────────────────────────────┘
372+
373+
│ updateRouteHook()
374+
375+
┌─────────────────────────────────────────────────────────────────┐
376+
│ RouteExecutionStore (Zustand) │
377+
│ - Persists routes to localStorage │
378+
│ - Tracks execution status │
379+
│ - Auto-cleans old failed routes │
380+
└─────────────────────────────────────────────────────────────────┘
381+
382+
383+
┌─────────────────────────────────────────────────────────────────┐
384+
│ @lifi/sdk │
385+
│ - Handles wallet interactions │
386+
│ - Manages transaction signing │
387+
│ - Tracks cross-chain status │
388+
└─────────────────────────────────────────────────────────────────┘
389+
```

0 commit comments

Comments
 (0)