@turtleclub/opportunities 0.1.0-beta.0
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/CHANGELOG.md +10 -0
- package/package.json +28 -0
- package/src/components/balances-data-table.tsx +85 -0
- package/src/components/index.ts +1 -0
- package/src/constants.ts +1 -0
- package/src/deposit/components/confirm-button.tsx +137 -0
- package/src/deposit/components/geo-check-blocker.tsx +40 -0
- package/src/deposit/components/index.ts +2 -0
- package/src/deposit/index.ts +1 -0
- package/src/images/enso.png +0 -0
- package/src/index.ts +17 -0
- package/src/opportunity-table/components/apr-breakdown-tooltip.tsx +103 -0
- package/src/opportunity-table/components/chain-list.tsx +28 -0
- package/src/opportunity-table/components/incentives-breakdown.tsx +263 -0
- package/src/opportunity-table/components/index.ts +4 -0
- package/src/opportunity-table/components/opportunities-table.tsx +218 -0
- package/src/opportunity-table/hooks/index.ts +2 -0
- package/src/opportunity-table/hooks/useNetAPR.ts +15 -0
- package/src/opportunity-table/hooks/useTotalYield.ts +12 -0
- package/src/opportunity-table/utils/calculateNetAPR.ts +37 -0
- package/src/opportunity-table/utils/index.ts +1 -0
- package/src/route-details/index.ts +2 -0
- package/src/route-details/route-details.tsx +100 -0
- package/src/transaction-status/components/TransactionStatusSection.tsx +81 -0
- package/src/transaction-status/hooks/useTransactionQueue.ts +322 -0
- package/src/transaction-status/index.ts +8 -0
- package/src/transaction-status/types/index.ts +22 -0
- package/src/transaction-status/utils/index.ts +80 -0
- package/src/transaction-status/utils/selectors.ts +66 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { TxStatus } from "@turtleclub/ui";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import type { TransactionStep } from "../types";
|
|
4
|
+
import { formatNumber } from "@turtleclub/utils";
|
|
5
|
+
|
|
6
|
+
interface TransactionStatusSectionProps {
|
|
7
|
+
steps: TransactionStep[];
|
|
8
|
+
currentStep: TransactionStep;
|
|
9
|
+
allCompleted: boolean;
|
|
10
|
+
cancelled: boolean;
|
|
11
|
+
tokenSymbol: string;
|
|
12
|
+
amount: string;
|
|
13
|
+
resetQueue: () => void;
|
|
14
|
+
resetForm: () => void;
|
|
15
|
+
refetchBalances?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Transaction Status Section - Shows TxStatus in the info area
|
|
19
|
+
export function TransactionStatusSection({
|
|
20
|
+
steps,
|
|
21
|
+
currentStep,
|
|
22
|
+
allCompleted,
|
|
23
|
+
cancelled,
|
|
24
|
+
tokenSymbol,
|
|
25
|
+
amount,
|
|
26
|
+
resetQueue,
|
|
27
|
+
resetForm,
|
|
28
|
+
refetchBalances,
|
|
29
|
+
}: TransactionStatusSectionProps) {
|
|
30
|
+
// For completed transactions, use the last completed transaction hash
|
|
31
|
+
// For ongoing transactions, use the current step hash
|
|
32
|
+
const lastCompletedStep = steps
|
|
33
|
+
.filter((step) => step.status === "completed" && step.txHash)
|
|
34
|
+
.pop();
|
|
35
|
+
const currentStepWithHash = currentStep?.txHash ? currentStep : null;
|
|
36
|
+
const displayTxHash = allCompleted
|
|
37
|
+
? lastCompletedStep?.txHash
|
|
38
|
+
: currentStepWithHash?.txHash || lastCompletedStep?.txHash;
|
|
39
|
+
|
|
40
|
+
const txStatusProps = useMemo(
|
|
41
|
+
() => ({
|
|
42
|
+
title: cancelled
|
|
43
|
+
? "Transaction Cancelled"
|
|
44
|
+
: allCompleted
|
|
45
|
+
? "Transaction Completed!"
|
|
46
|
+
: "Processing Transaction",
|
|
47
|
+
description: cancelled
|
|
48
|
+
? "Transaction was cancelled by user"
|
|
49
|
+
: allCompleted
|
|
50
|
+
? "Your deposit has been successfully processed."
|
|
51
|
+
: "Please wait while your transaction is being confirmed...",
|
|
52
|
+
txHash: displayTxHash,
|
|
53
|
+
explorerUrl: "https://etherscan.io", // TODO: Get from config
|
|
54
|
+
estimatedTime: undefined,
|
|
55
|
+
amount: `${amount ? formatNumber(amount, 5, false, false) : "0"}`,
|
|
56
|
+
token: tokenSymbol,
|
|
57
|
+
protocol: "Turtle Protocol",
|
|
58
|
+
completed: allCompleted,
|
|
59
|
+
cancelled,
|
|
60
|
+
onViewDetails: undefined, // Don't show View Details button for now
|
|
61
|
+
onClose: () => {
|
|
62
|
+
resetQueue();
|
|
63
|
+
resetForm();
|
|
64
|
+
refetchBalances?.();
|
|
65
|
+
},
|
|
66
|
+
steps: steps.map((step) => ({
|
|
67
|
+
label: step.label,
|
|
68
|
+
completed: step.status === "completed",
|
|
69
|
+
current: step.status === "executing",
|
|
70
|
+
txHash: step.txHash,
|
|
71
|
+
})),
|
|
72
|
+
}),
|
|
73
|
+
[steps, allCompleted, cancelled, tokenSymbol, amount, resetQueue, resetForm, refetchBalances, displayTxHash]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="h-full overflow-hidden">
|
|
78
|
+
<TxStatus {...txStatusProps} className="bg-background border-border text-foreground h-full" />
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useReducer } from "react";
|
|
2
|
+
import type { EarnRouteResponse, RouterStep, StepTx } from "@turtleclub/hooks";
|
|
3
|
+
import {
|
|
4
|
+
TransactionStep,
|
|
5
|
+
TransactionQueueState,
|
|
6
|
+
TransactionStepStatus,
|
|
7
|
+
QueueStatus,
|
|
8
|
+
} from "../types";
|
|
9
|
+
import { getStepLabel, getStepDescription, isUserRejection, shouldAutoExecuteNextStep } from "../utils";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* useTransactionQueue Hook
|
|
13
|
+
*
|
|
14
|
+
* Manages the execution of multiple blockchain transactions in sequence.
|
|
15
|
+
* Key features:
|
|
16
|
+
* - Maintains a queue of transaction steps from an EarnRoute
|
|
17
|
+
* - Executes transactions one by one with proper state management
|
|
18
|
+
* - Auto-triggers the next transaction after the previous one completes
|
|
19
|
+
* - Handles errors, user cancellations, and retry logic
|
|
20
|
+
*
|
|
21
|
+
* Auto-trigger logic:
|
|
22
|
+
* 1. After a transaction completes, the hook updates currentIndex to point to the next step
|
|
23
|
+
* 2. A useEffect monitors state changes and checks if conditions are met for auto-execution
|
|
24
|
+
* 3. If conditions are met, it automatically triggers the next transaction after a 1s delay
|
|
25
|
+
* 4. This continues until all transactions in the queue are completed
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// Constants
|
|
29
|
+
const AUTO_EXECUTE_DELAY = 1000; // ms
|
|
30
|
+
|
|
31
|
+
interface UseTransactionQueueProps {
|
|
32
|
+
earnRoute: EarnRouteResponse | null;
|
|
33
|
+
userAddress?: string;
|
|
34
|
+
chainId?: number;
|
|
35
|
+
sendTransaction: (transaction: StepTx) => Promise<`0x${string}` | undefined>;
|
|
36
|
+
onError?: (error: Error) => void;
|
|
37
|
+
onSuccess?: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Reducer action types
|
|
41
|
+
type QueueAction =
|
|
42
|
+
| { type: "INITIALIZE_QUEUE"; steps: TransactionStep[] }
|
|
43
|
+
| { type: "START_EXECUTION"; index: number }
|
|
44
|
+
| { type: "COMPLETE_STEP"; index: number; txHash: string }
|
|
45
|
+
| { type: "FAIL_STEP"; index: number; error: string; isCancellation: boolean }
|
|
46
|
+
| { type: "RESET_QUEUE"; steps: TransactionStep[] };
|
|
47
|
+
|
|
48
|
+
// Helper functions
|
|
49
|
+
function createTransactionStep(step: RouterStep, index: number): TransactionStep | null {
|
|
50
|
+
// Only process steps that have transaction data
|
|
51
|
+
if (!step.tx) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const newStep: TransactionStep = {
|
|
56
|
+
id: index,
|
|
57
|
+
label: getStepLabel(step),
|
|
58
|
+
description: getStepDescription(step, index),
|
|
59
|
+
txData: step.tx,
|
|
60
|
+
status: "pending",
|
|
61
|
+
};
|
|
62
|
+
return newStep;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Reducer function
|
|
66
|
+
function queueReducer(state: TransactionQueueState, action: QueueAction): TransactionQueueState {
|
|
67
|
+
switch (action.type) {
|
|
68
|
+
case "INITIALIZE_QUEUE":
|
|
69
|
+
return {
|
|
70
|
+
steps: action.steps,
|
|
71
|
+
currentIndex: 0,
|
|
72
|
+
queueStatus: "idle" satisfies QueueStatus,
|
|
73
|
+
lastError: undefined,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
case "START_EXECUTION":
|
|
77
|
+
return {
|
|
78
|
+
...state,
|
|
79
|
+
steps: state.steps.map((step, idx) => {
|
|
80
|
+
if (idx === action.index) {
|
|
81
|
+
const updatedStep: TransactionStep = { ...step, status: "executing" };
|
|
82
|
+
return updatedStep;
|
|
83
|
+
}
|
|
84
|
+
return step;
|
|
85
|
+
}),
|
|
86
|
+
queueStatus: "executing" satisfies QueueStatus,
|
|
87
|
+
lastError: undefined,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
case "COMPLETE_STEP": {
|
|
91
|
+
const updatedSteps = state.steps.map((step, idx) => {
|
|
92
|
+
if (idx === action.index) {
|
|
93
|
+
const completedStep: TransactionStep = { ...step, status: "completed", txHash: action.txHash };
|
|
94
|
+
return completedStep;
|
|
95
|
+
}
|
|
96
|
+
return step;
|
|
97
|
+
});
|
|
98
|
+
const nextIndex = action.index + 1;
|
|
99
|
+
const allCompleted = nextIndex >= updatedSteps.length;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
...state,
|
|
103
|
+
steps: updatedSteps,
|
|
104
|
+
currentIndex: allCompleted ? state.currentIndex : nextIndex,
|
|
105
|
+
queueStatus: (allCompleted ? "completed" : "idle") satisfies QueueStatus,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case "FAIL_STEP":
|
|
110
|
+
return {
|
|
111
|
+
...state,
|
|
112
|
+
steps: state.steps.map((step, idx) => {
|
|
113
|
+
if (idx === action.index) {
|
|
114
|
+
const failedStep: TransactionStep = { ...step, status: "failed", error: action.error };
|
|
115
|
+
return failedStep;
|
|
116
|
+
}
|
|
117
|
+
return step;
|
|
118
|
+
}),
|
|
119
|
+
queueStatus: (action.isCancellation ? "cancelled" : "error") satisfies QueueStatus,
|
|
120
|
+
lastError: action.error,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
case "RESET_QUEUE":
|
|
124
|
+
return {
|
|
125
|
+
steps: action.steps,
|
|
126
|
+
currentIndex: 0,
|
|
127
|
+
queueStatus: "idle" satisfies QueueStatus,
|
|
128
|
+
lastError: undefined,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
default:
|
|
132
|
+
return state;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function useTransactionQueue({
|
|
137
|
+
earnRoute,
|
|
138
|
+
userAddress,
|
|
139
|
+
chainId,
|
|
140
|
+
sendTransaction,
|
|
141
|
+
onError = () => {},
|
|
142
|
+
onSuccess = () => {},
|
|
143
|
+
}: UseTransactionQueueProps) {
|
|
144
|
+
// Initialize state with reducer
|
|
145
|
+
const initialState: TransactionQueueState = {
|
|
146
|
+
steps: [],
|
|
147
|
+
currentIndex: 0,
|
|
148
|
+
queueStatus: "idle",
|
|
149
|
+
};
|
|
150
|
+
const [state, dispatch] = useReducer(queueReducer, initialState);
|
|
151
|
+
|
|
152
|
+
// Initialize queue when earnRoute changes
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!earnRoute?.steps?.length) {
|
|
155
|
+
dispatch({ type: "INITIALIZE_QUEUE", steps: [] });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const steps = earnRoute.steps
|
|
160
|
+
.map((step: RouterStep, index: number) => createTransactionStep(step, index))
|
|
161
|
+
.filter((step): step is TransactionStep => step !== null);
|
|
162
|
+
|
|
163
|
+
dispatch({ type: "INITIALIZE_QUEUE", steps });
|
|
164
|
+
}, [earnRoute]);
|
|
165
|
+
|
|
166
|
+
// Execute current transaction
|
|
167
|
+
const executeCurrentTransaction = useCallback(async () => {
|
|
168
|
+
// Validation checks
|
|
169
|
+
if (!userAddress || !chainId || !sendTransaction || state.queueStatus === "executing") {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const currentStep = state.steps[state.currentIndex];
|
|
174
|
+
if (!currentStep || (currentStep.status !== "pending" && currentStep.status !== "failed")) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Security validation: verify transaction 'from' matches connected wallet
|
|
179
|
+
const txFrom = currentStep.txData.from as `0x${string}`;
|
|
180
|
+
if (txFrom.toLowerCase() !== userAddress.toLowerCase()) {
|
|
181
|
+
const error = new Error("Transaction 'from' address doesn't match connected wallet");
|
|
182
|
+
dispatch({
|
|
183
|
+
type: "FAIL_STEP",
|
|
184
|
+
index: state.currentIndex,
|
|
185
|
+
error: error.message,
|
|
186
|
+
isCancellation: false,
|
|
187
|
+
});
|
|
188
|
+
if (onError) {
|
|
189
|
+
onError(error);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Start execution
|
|
195
|
+
dispatch({ type: "START_EXECUTION", index: state.currentIndex });
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
// Send transaction with chainId
|
|
199
|
+
const txHash = await sendTransaction({
|
|
200
|
+
...currentStep.txData,
|
|
201
|
+
chainId,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Transaction is already confirmed at this point because executeTransaction waits for confirmation
|
|
205
|
+
// Mark as completed
|
|
206
|
+
dispatch({ type: "COMPLETE_STEP", index: state.currentIndex, txHash: txHash ?? "" });
|
|
207
|
+
|
|
208
|
+
// Check if all completed and trigger success callback
|
|
209
|
+
const isLastStep = state.currentIndex + 1 >= state.steps.length;
|
|
210
|
+
if (isLastStep && onSuccess) {
|
|
211
|
+
onSuccess();
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
// Handle error
|
|
215
|
+
const errorMessage = error instanceof Error ? error.message : "Transaction failed";
|
|
216
|
+
const isCancellation = isUserRejection(error);
|
|
217
|
+
|
|
218
|
+
dispatch({
|
|
219
|
+
type: "FAIL_STEP",
|
|
220
|
+
index: state.currentIndex,
|
|
221
|
+
error: isCancellation ? "Transaction cancelled by user" : errorMessage,
|
|
222
|
+
isCancellation,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Call error callback if not a cancellation
|
|
226
|
+
if (!isCancellation && onError) {
|
|
227
|
+
onError(error instanceof Error ? error : new Error(errorMessage));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}, [
|
|
231
|
+
userAddress,
|
|
232
|
+
chainId,
|
|
233
|
+
sendTransaction,
|
|
234
|
+
state.queueStatus,
|
|
235
|
+
state.steps,
|
|
236
|
+
state.currentIndex,
|
|
237
|
+
onSuccess,
|
|
238
|
+
onError,
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
// Reset queue to initial state
|
|
242
|
+
const resetQueue = useCallback(() => {
|
|
243
|
+
if (!earnRoute?.steps?.length) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const steps = earnRoute.steps
|
|
248
|
+
.map((step: RouterStep, index: number) => createTransactionStep(step, index))
|
|
249
|
+
.filter((step): step is TransactionStep => step !== null);
|
|
250
|
+
|
|
251
|
+
dispatch({ type: "RESET_QUEUE", steps });
|
|
252
|
+
}, [earnRoute]);
|
|
253
|
+
|
|
254
|
+
// Computed values
|
|
255
|
+
const currentStep = state.steps[state.currentIndex] || null;
|
|
256
|
+
const isExecuting = state.queueStatus === "executing";
|
|
257
|
+
const allCompleted = state.queueStatus === "completed";
|
|
258
|
+
const hasError = state.queueStatus === "error";
|
|
259
|
+
const cancelled = state.queueStatus === "cancelled";
|
|
260
|
+
const completedSteps = state.steps.filter((step) => step.status === "completed").length;
|
|
261
|
+
const totalSteps = state.steps.length;
|
|
262
|
+
const progress = totalSteps > 0 ? (completedSteps / totalSteps) * 100 : 0;
|
|
263
|
+
|
|
264
|
+
const showTxStatus = useMemo(() => {
|
|
265
|
+
if (!currentStep) return false;
|
|
266
|
+
if (allCompleted) return true;
|
|
267
|
+
if (isExecuting && currentStep.id === 0) return true;
|
|
268
|
+
if (currentStep.id > 0) return true;
|
|
269
|
+
return false;
|
|
270
|
+
}, [currentStep, allCompleted, isExecuting]);
|
|
271
|
+
|
|
272
|
+
// Check if ready to execute
|
|
273
|
+
const canExecute = !!(
|
|
274
|
+
userAddress &&
|
|
275
|
+
chainId &&
|
|
276
|
+
state.steps.length > 0 &&
|
|
277
|
+
state.queueStatus !== "executing" &&
|
|
278
|
+
state.queueStatus !== "completed"
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Auto-execute next transaction when current one completes (only for multi-step transactions)
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
if (shouldAutoExecuteNextStep(state)) {
|
|
284
|
+
// Delay execution to allow UI to update
|
|
285
|
+
const timer = setTimeout(() => {
|
|
286
|
+
executeCurrentTransaction();
|
|
287
|
+
}, AUTO_EXECUTE_DELAY);
|
|
288
|
+
|
|
289
|
+
// Cleanup timeout on unmount or dependency change
|
|
290
|
+
return () => clearTimeout(timer);
|
|
291
|
+
}
|
|
292
|
+
}, [state, executeCurrentTransaction]);
|
|
293
|
+
|
|
294
|
+
// Return value maintaining the same signature
|
|
295
|
+
return {
|
|
296
|
+
steps: state.steps.map((step, index) => ({
|
|
297
|
+
...step,
|
|
298
|
+
completed: step.status === "completed",
|
|
299
|
+
current: index === state.currentIndex,
|
|
300
|
+
})),
|
|
301
|
+
currentIndex: state.currentIndex,
|
|
302
|
+
isExecuting,
|
|
303
|
+
allCompleted,
|
|
304
|
+
hasError,
|
|
305
|
+
cancelled,
|
|
306
|
+
error: state.lastError,
|
|
307
|
+
currentStep: currentStep
|
|
308
|
+
? {
|
|
309
|
+
...currentStep,
|
|
310
|
+
completed: currentStep.status === "completed",
|
|
311
|
+
current: true,
|
|
312
|
+
}
|
|
313
|
+
: null,
|
|
314
|
+
canExecute,
|
|
315
|
+
executeCurrentTransaction,
|
|
316
|
+
resetQueue,
|
|
317
|
+
totalSteps,
|
|
318
|
+
completedSteps,
|
|
319
|
+
progress,
|
|
320
|
+
showTxStatus,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { TransactionStatusSection } from "./components/TransactionStatusSection";
|
|
2
|
+
export { useTransactionQueue } from "./hooks/useTransactionQueue";
|
|
3
|
+
export type {
|
|
4
|
+
TransactionStep,
|
|
5
|
+
TransactionQueueState,
|
|
6
|
+
TransactionStepStatus,
|
|
7
|
+
QueueStatus,
|
|
8
|
+
} from "./types";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StepTx } from "@turtleclub/hooks";
|
|
2
|
+
|
|
3
|
+
export type TransactionStepStatus = "pending" | "executing" | "completed" | "failed";
|
|
4
|
+
|
|
5
|
+
export interface TransactionStep {
|
|
6
|
+
id: number;
|
|
7
|
+
label: string;
|
|
8
|
+
description: string;
|
|
9
|
+
txData: StepTx;
|
|
10
|
+
status: TransactionStepStatus;
|
|
11
|
+
txHash?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type QueueStatus = "idle" | "executing" | "completed" | "cancelled" | "error";
|
|
16
|
+
|
|
17
|
+
export interface TransactionQueueState {
|
|
18
|
+
steps: TransactionStep[];
|
|
19
|
+
currentIndex: number;
|
|
20
|
+
queueStatus: QueueStatus;
|
|
21
|
+
lastError?: string;
|
|
22
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { RouterStep } from "@turtleclub/hooks";
|
|
2
|
+
import type { TransactionQueueState } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a user-friendly label for a transaction step
|
|
6
|
+
* @param step - The router step from the API
|
|
7
|
+
* @returns A formatted label string
|
|
8
|
+
*/
|
|
9
|
+
export function getStepLabel(step: RouterStep): string {
|
|
10
|
+
if (step.kind === "approve" && step.token) {
|
|
11
|
+
return `Approve ${step.token.symbol}`;
|
|
12
|
+
}
|
|
13
|
+
if (step.kind === "enso") {
|
|
14
|
+
return `Execute Deposit`;
|
|
15
|
+
}
|
|
16
|
+
return `Execute ${step.kind}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate a user-friendly description for a transaction step
|
|
21
|
+
* @param step - The router step from the API
|
|
22
|
+
* @param index - The step index in the queue
|
|
23
|
+
* @returns A formatted description string
|
|
24
|
+
*/
|
|
25
|
+
export function getStepDescription(step: RouterStep, index: number): string {
|
|
26
|
+
if (step.kind === "approve" && step.token) {
|
|
27
|
+
return `Allow contract to spend ${step.token.symbol}`;
|
|
28
|
+
}
|
|
29
|
+
return `Transaction step ${index + 1}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if an error is a user rejection (e.g., cancelled transaction in wallet)
|
|
34
|
+
* @param error - The error to check
|
|
35
|
+
* @returns True if the error indicates user rejection
|
|
36
|
+
*/
|
|
37
|
+
export function isUserRejection(error: Error | unknown): boolean {
|
|
38
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
39
|
+
const rejectionPatterns = [
|
|
40
|
+
"user rejected",
|
|
41
|
+
"user denied",
|
|
42
|
+
"user cancelled",
|
|
43
|
+
"cancelled by user",
|
|
44
|
+
"transaction rejected",
|
|
45
|
+
"4001", // MetaMask user rejection code
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
return rejectionPatterns.some((pattern) =>
|
|
49
|
+
errorMessage.toLowerCase().includes(pattern.toLowerCase())
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Determine if the next transaction step should be auto-executed
|
|
55
|
+
* This is used for multi-step transaction flows where steps need to be executed sequentially
|
|
56
|
+
*
|
|
57
|
+
* @param state - The current transaction queue state
|
|
58
|
+
* @returns True if the next step should be automatically executed
|
|
59
|
+
*/
|
|
60
|
+
export function shouldAutoExecuteNextStep(state: TransactionQueueState): boolean {
|
|
61
|
+
// No auto-execute for single step transactions
|
|
62
|
+
if (state.steps.length <= 1 || state.queueStatus !== "idle") {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const previousStep = state.steps[state.currentIndex - 1];
|
|
67
|
+
const currentStep = state.steps[state.currentIndex];
|
|
68
|
+
|
|
69
|
+
// Auto-execute if:
|
|
70
|
+
// - We've already executed at least one transaction
|
|
71
|
+
// - Previous step is completed
|
|
72
|
+
// - Current step is pending
|
|
73
|
+
// - There are more steps to execute
|
|
74
|
+
return (
|
|
75
|
+
state.currentIndex > 0 &&
|
|
76
|
+
state.currentIndex < state.steps.length &&
|
|
77
|
+
previousStep?.status === "completed" &&
|
|
78
|
+
currentStep?.status === "pending"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { TransactionQueueState } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Selectors for computed values from transaction queue state
|
|
5
|
+
* These are pure functions that derive values from state
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface QueueMetrics {
|
|
9
|
+
completedSteps: number;
|
|
10
|
+
totalSteps: number;
|
|
11
|
+
progress: number;
|
|
12
|
+
isExecuting: boolean;
|
|
13
|
+
allCompleted: boolean;
|
|
14
|
+
hasError: boolean;
|
|
15
|
+
cancelled: boolean;
|
|
16
|
+
canExecute: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function selectQueueMetrics(
|
|
20
|
+
state: TransactionQueueState,
|
|
21
|
+
hasRequiredProps: { userAddress?: string; chainId?: number }
|
|
22
|
+
): QueueMetrics {
|
|
23
|
+
const completedSteps = state.steps.filter((step) => step.status === "completed").length;
|
|
24
|
+
const totalSteps = state.steps.length;
|
|
25
|
+
const progress = totalSteps > 0 ? (completedSteps / totalSteps) * 100 : 0;
|
|
26
|
+
|
|
27
|
+
const isExecuting = state.queueStatus === "executing";
|
|
28
|
+
const allCompleted = state.queueStatus === "completed";
|
|
29
|
+
const hasError = state.queueStatus === "error";
|
|
30
|
+
const cancelled = state.queueStatus === "cancelled";
|
|
31
|
+
|
|
32
|
+
const canExecute = !!(
|
|
33
|
+
hasRequiredProps.userAddress &&
|
|
34
|
+
hasRequiredProps.chainId &&
|
|
35
|
+
state.steps.length > 0 &&
|
|
36
|
+
state.queueStatus !== "executing" &&
|
|
37
|
+
state.queueStatus !== "completed"
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
completedSteps,
|
|
42
|
+
totalSteps,
|
|
43
|
+
progress,
|
|
44
|
+
isExecuting,
|
|
45
|
+
allCompleted,
|
|
46
|
+
hasError,
|
|
47
|
+
cancelled,
|
|
48
|
+
canExecute,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function selectCurrentStep(state: TransactionQueueState) {
|
|
53
|
+
return state.steps[state.currentIndex] || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function selectShouldShowTxStatus(state: TransactionQueueState): boolean {
|
|
57
|
+
const currentStep = selectCurrentStep(state);
|
|
58
|
+
const isExecuting = state.queueStatus === "executing";
|
|
59
|
+
const allCompleted = state.queueStatus === "completed";
|
|
60
|
+
|
|
61
|
+
if (!currentStep) return false;
|
|
62
|
+
if (allCompleted) return true;
|
|
63
|
+
if (isExecuting && currentStep.id === 0) return true;
|
|
64
|
+
if (currentStep.id > 0) return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"allowSyntheticDefaultImports": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"outDir": "dist",
|
|
18
|
+
"rootDir": "src"
|
|
19
|
+
},
|
|
20
|
+
"include": ["src"],
|
|
21
|
+
"exclude": ["node_modules", "dist"]
|
|
22
|
+
}
|