@turtleclub/opportunities 0.1.0-beta.56 → 0.1.0-beta.58
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/README.md +179 -0
- package/package.json +5 -4
- package/src/components/index.ts +1 -1
- package/src/deposit/NativeDepositSection.tsx +199 -0
- package/src/deposit/TemporalWrapper.tsx +155 -0
- package/src/{components → deposit/components}/balances-data-table.tsx +6 -0
- package/src/deposit/components/index.ts +3 -0
- package/src/deposit/components/swap-input-v3.tsx +194 -0
- package/src/deposit/components/token-selector-v3.tsx +122 -0
- package/src/deposit/index.ts +4 -0
- package/src/index.ts +6 -0
- package/src/opportunity-actions/OpportunityActions.tsx +182 -0
- package/src/opportunity-actions/index.ts +1 -0
- package/src/opportunity-table/components/opportunities-table.tsx +6 -5
- package/src/route-details/index.ts +6 -0
- package/src/route-details/route-details-v2.tsx +137 -0
- package/src/route-details/route-details.tsx +5 -4
- package/src/route-details/types.ts +7 -0
- package/src/transaction-status/hooks/useTransactionQueue.ts +1 -1
- package/src/withdraw/NativeWithdrawSection.tsx +45 -0
- package/src/withdraw/index.ts +1 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { formatUnits } from "viem";
|
|
5
|
+
import { cn, Card, Input, Chip } from "@turtleclub/ui";
|
|
6
|
+
import type { TokenBalance } from "@turtleclub/hooks";
|
|
7
|
+
import { TokenSelectorV3 } from "./token-selector-v3";
|
|
8
|
+
|
|
9
|
+
const MIN_DISPLAY_AMOUNT = 0.0001;
|
|
10
|
+
const MIN_USD_DISPLAY_AMOUNT = 0.01;
|
|
11
|
+
interface SwapInputV3Props extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
12
|
+
/** Current input value */
|
|
13
|
+
value?: string;
|
|
14
|
+
/** Token balances to display in selector */
|
|
15
|
+
balances: TokenBalance[];
|
|
16
|
+
/** Currently selected token address */
|
|
17
|
+
selectedTokenAddress?: string;
|
|
18
|
+
/** Placeholder text for input */
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
/** Disable input and selector */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** Whether wallet is connected */
|
|
23
|
+
isWalletConnected?: boolean;
|
|
24
|
+
/** Called when input value changes */
|
|
25
|
+
onChange?: (value: string) => void;
|
|
26
|
+
/** Called when MAX button is clicked */
|
|
27
|
+
onMaxClick?: () => void;
|
|
28
|
+
/** Called when token selection changes */
|
|
29
|
+
onTokenChange?: (tokenAddress: string) => void;
|
|
30
|
+
/** Show balance info below input */
|
|
31
|
+
showBalance?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* SwapInputV3 - Uses TokenSelectorV3 with BalancesDataTable
|
|
36
|
+
*/
|
|
37
|
+
const SwapInputV3 = React.forwardRef<HTMLDivElement, SwapInputV3Props>(
|
|
38
|
+
(
|
|
39
|
+
{
|
|
40
|
+
className,
|
|
41
|
+
value = "",
|
|
42
|
+
balances,
|
|
43
|
+
selectedTokenAddress,
|
|
44
|
+
placeholder = "0",
|
|
45
|
+
disabled = false,
|
|
46
|
+
isWalletConnected = true,
|
|
47
|
+
onChange,
|
|
48
|
+
onMaxClick,
|
|
49
|
+
onTokenChange,
|
|
50
|
+
showBalance = true,
|
|
51
|
+
...props
|
|
52
|
+
},
|
|
53
|
+
ref,
|
|
54
|
+
) => {
|
|
55
|
+
// Find the selected balance
|
|
56
|
+
const selectedBalance = useMemo(
|
|
57
|
+
() => balances.find((b) => b.token.address === selectedTokenAddress) ?? null,
|
|
58
|
+
[balances, selectedTokenAddress]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Format balance for display
|
|
62
|
+
const formattedBalance = useMemo(() => {
|
|
63
|
+
if (!selectedBalance) return undefined;
|
|
64
|
+
const formatted = formatUnits(BigInt(selectedBalance.amount), selectedBalance.token.decimals);
|
|
65
|
+
const num = parseFloat(formatted);
|
|
66
|
+
if (num === 0) return "0";
|
|
67
|
+
if (num < MIN_DISPLAY_AMOUNT) return `<${MIN_DISPLAY_AMOUNT}`;
|
|
68
|
+
if (num < 1) return num.toFixed(4);
|
|
69
|
+
if (num < 1000) return num.toFixed(2);
|
|
70
|
+
return num.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
71
|
+
}, [selectedBalance]);
|
|
72
|
+
|
|
73
|
+
// Calculate USD value of input amount
|
|
74
|
+
const usdValue = useMemo(() => {
|
|
75
|
+
if (!value || !selectedBalance?.token.priceUsd) return undefined;
|
|
76
|
+
const amount = parseFloat(value);
|
|
77
|
+
if (isNaN(amount) || amount === 0) return undefined;
|
|
78
|
+
const usd = amount * selectedBalance.token.priceUsd;
|
|
79
|
+
if (usd < MIN_USD_DISPLAY_AMOUNT) return `<$${MIN_USD_DISPLAY_AMOUNT}`;
|
|
80
|
+
return `$${usd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
81
|
+
}, [value, selectedBalance]);
|
|
82
|
+
|
|
83
|
+
// Check if input amount exceeds balance
|
|
84
|
+
const hasInsufficientBalance = useMemo(() => {
|
|
85
|
+
if (!value || !selectedBalance) return false;
|
|
86
|
+
try {
|
|
87
|
+
const inputAmount = parseFloat(value);
|
|
88
|
+
const balance = parseFloat(formatUnits(BigInt(selectedBalance.amount), selectedBalance.token.decimals));
|
|
89
|
+
return inputAmount > balance;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}, [value, selectedBalance]);
|
|
94
|
+
|
|
95
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
96
|
+
const newValue = e.target.value;
|
|
97
|
+
// Allow only numbers and decimal point
|
|
98
|
+
if (/^\d*\.?\d*$/.test(newValue)) {
|
|
99
|
+
onChange?.(newValue);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// If wallet is not connected, show connection message
|
|
104
|
+
if (!isWalletConnected) {
|
|
105
|
+
return (
|
|
106
|
+
<Card
|
|
107
|
+
ref={ref}
|
|
108
|
+
className={cn("flex items-center justify-center py-8", className)}
|
|
109
|
+
{...props}
|
|
110
|
+
>
|
|
111
|
+
<div className="text-center">
|
|
112
|
+
<p className="text-muted-foreground text-sm">
|
|
113
|
+
Connect your wallet to continue
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
</Card>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<Card
|
|
122
|
+
variant="shadow"
|
|
123
|
+
ref={ref}
|
|
124
|
+
className={cn("space-y-3", disabled && "opacity-50", className)}
|
|
125
|
+
{...props}
|
|
126
|
+
>
|
|
127
|
+
{/* Main input row */}
|
|
128
|
+
<div className="flex items-center gap-6">
|
|
129
|
+
{/* Amount input */}
|
|
130
|
+
<div className="flex-1">
|
|
131
|
+
<Input
|
|
132
|
+
variant="nofocus"
|
|
133
|
+
value={value}
|
|
134
|
+
onChange={handleInputChange}
|
|
135
|
+
placeholder={placeholder}
|
|
136
|
+
disabled={disabled}
|
|
137
|
+
className="text-foreground/90 h-auto border-none bg-transparent p-0 text-3xl font-[400] shadow-none focus:ring-0"
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Token selector */}
|
|
142
|
+
<TokenSelectorV3
|
|
143
|
+
balances={balances}
|
|
144
|
+
value={selectedTokenAddress}
|
|
145
|
+
onValueChange={onTokenChange}
|
|
146
|
+
disabled={disabled}
|
|
147
|
+
placeholder="Select token"
|
|
148
|
+
size="sm"
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Bottom row: USD value and balance with MAX */}
|
|
153
|
+
{showBalance && (
|
|
154
|
+
<div className="text-muted-foreground flex items-center justify-between text-sm">
|
|
155
|
+
<div>
|
|
156
|
+
{usdValue ? (
|
|
157
|
+
<span>≈ {usdValue}</span>
|
|
158
|
+
) : value && hasInsufficientBalance ? (
|
|
159
|
+
<span className="text-destructive/50">
|
|
160
|
+
Insufficient balance
|
|
161
|
+
</span>
|
|
162
|
+
) : null}
|
|
163
|
+
</div>
|
|
164
|
+
<div className="flex items-center gap-2">
|
|
165
|
+
{formattedBalance !== undefined && (
|
|
166
|
+
<span className="text-sm">
|
|
167
|
+
Balance: {formattedBalance} {selectedBalance?.token.symbol || ""}
|
|
168
|
+
</span>
|
|
169
|
+
)}
|
|
170
|
+
{onMaxClick && (
|
|
171
|
+
<Chip
|
|
172
|
+
variant="default"
|
|
173
|
+
size="xs"
|
|
174
|
+
onClick={onMaxClick}
|
|
175
|
+
className={cn(
|
|
176
|
+
"hover:bg-primary hover:text-primary-foreground h-5 cursor-pointer px-2 py-0.5 text-xs transition-colors",
|
|
177
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
178
|
+
)}
|
|
179
|
+
>
|
|
180
|
+
MAX
|
|
181
|
+
</Chip>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
</Card>
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
SwapInputV3.displayName = "SwapInputV3";
|
|
192
|
+
|
|
193
|
+
export { SwapInputV3 };
|
|
194
|
+
export type { SwapInputV3Props };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { useState, useEffect, useMemo } from "react";
|
|
4
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
cn,
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from "@turtleclub/ui";
|
|
12
|
+
import type { TokenBalance } from "@turtleclub/hooks";
|
|
13
|
+
import { BalancesDataTable } from "./balances-data-table";
|
|
14
|
+
|
|
15
|
+
interface TokenSelectorV3Props {
|
|
16
|
+
balances: TokenBalance[];
|
|
17
|
+
value?: string;
|
|
18
|
+
onValueChange?: (value: string) => void;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
className?: string;
|
|
22
|
+
size?: "xs" | "sm" | "default";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* TokenSelectorV3 - Opens a dialog with BalancesDataTable for selection
|
|
27
|
+
*/
|
|
28
|
+
const TokenSelectorV3 = ({
|
|
29
|
+
balances,
|
|
30
|
+
value,
|
|
31
|
+
onValueChange,
|
|
32
|
+
placeholder = "Select token",
|
|
33
|
+
disabled,
|
|
34
|
+
className,
|
|
35
|
+
size = "default",
|
|
36
|
+
}: TokenSelectorV3Props) => {
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
|
|
39
|
+
// Auto-select first token if no value is provided and balances exist
|
|
40
|
+
const effectiveValue =
|
|
41
|
+
value || (balances.length > 0 ? balances[0].token.address : undefined);
|
|
42
|
+
|
|
43
|
+
// Find the selected balance
|
|
44
|
+
const selectedBalance = useMemo(
|
|
45
|
+
() => balances.find((b) => b.token.address === effectiveValue),
|
|
46
|
+
[balances, effectiveValue]
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Auto-select first token on mount
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!value && balances.length > 0 && onValueChange) {
|
|
52
|
+
onValueChange(balances[0].token.address);
|
|
53
|
+
}
|
|
54
|
+
}, [value, balances, onValueChange]);
|
|
55
|
+
|
|
56
|
+
const handleSelect = (balance: TokenBalance) => {
|
|
57
|
+
onValueChange?.(balance.token.address);
|
|
58
|
+
setOpen(false);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
{/* Trigger Button */}
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={() => !disabled && setOpen(true)}
|
|
67
|
+
disabled={disabled}
|
|
68
|
+
className={cn(
|
|
69
|
+
"bg-muted hover:bg-muted/80 flex items-center gap-1.5 rounded-full px-3 py-2 font-medium transition-colors",
|
|
70
|
+
"focus:ring-primary/20 focus:ring-2 focus:outline-none",
|
|
71
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
72
|
+
size === "xs" && "px-2 py-1 text-xs",
|
|
73
|
+
size === "sm" && "px-2.5 py-1.5 text-sm",
|
|
74
|
+
size === "default" && "px-3 py-2 text-sm",
|
|
75
|
+
className
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{selectedBalance ? (
|
|
79
|
+
<>
|
|
80
|
+
{selectedBalance.token.logoUrl && (
|
|
81
|
+
<img
|
|
82
|
+
src={selectedBalance.token.logoUrl}
|
|
83
|
+
alt={selectedBalance.token.symbol}
|
|
84
|
+
className={cn(
|
|
85
|
+
"rounded-full",
|
|
86
|
+
size === "xs" && "size-3",
|
|
87
|
+
size === "sm" && "size-4",
|
|
88
|
+
size === "default" && "size-4"
|
|
89
|
+
)}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
<span>{selectedBalance.token.symbol}</span>
|
|
93
|
+
</>
|
|
94
|
+
) : (
|
|
95
|
+
<span className="text-muted-foreground">{placeholder}</span>
|
|
96
|
+
)}
|
|
97
|
+
<ChevronDownIcon className="size-4 opacity-50" />
|
|
98
|
+
</button>
|
|
99
|
+
|
|
100
|
+
{/* Token Selection Dialog */}
|
|
101
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
102
|
+
<DialogContent className="max-w-md p-0">
|
|
103
|
+
<DialogHeader className="px-4 pt-4">
|
|
104
|
+
<DialogTitle>Select Token</DialogTitle>
|
|
105
|
+
</DialogHeader>
|
|
106
|
+
|
|
107
|
+
{/* BalancesDataTable with native scroll */}
|
|
108
|
+
<div className="px-4 pb-4">
|
|
109
|
+
<BalancesDataTable
|
|
110
|
+
balances={balances}
|
|
111
|
+
onBalanceSelect={handleSelect}
|
|
112
|
+
emptyStateMessage="No tokens available"
|
|
113
|
+
size="450px"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
</DialogContent>
|
|
117
|
+
</Dialog>
|
|
118
|
+
</>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export { TokenSelectorV3 };
|
package/src/deposit/index.ts
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
1
|
export * from "./components";
|
|
2
|
+
export { NativeDepositSection } from "./NativeDepositSection";
|
|
3
|
+
export type { NativeDepositSectionProps } from "./NativeDepositSection";
|
|
4
|
+
export { TemporalWrapper } from "./TemporalWrapper";
|
|
5
|
+
export type { TemporalWrapperProps, DepositMode } from "./TemporalWrapper";
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,12 @@ export * from "./components";
|
|
|
4
4
|
// Deposit Components
|
|
5
5
|
export * from "./deposit";
|
|
6
6
|
|
|
7
|
+
// Withdraw Components
|
|
8
|
+
export * from "./withdraw";
|
|
9
|
+
|
|
10
|
+
// Opportunity Actions
|
|
11
|
+
export * from "./opportunity-actions";
|
|
12
|
+
|
|
7
13
|
// Transaction Status
|
|
8
14
|
export * from "./transaction-status";
|
|
9
15
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from "react";
|
|
4
|
+
import { SegmentControl, TurtleTooltip } from "@turtleclub/ui";
|
|
5
|
+
import {
|
|
6
|
+
useGetOnChainBalance,
|
|
7
|
+
useBalance,
|
|
8
|
+
filterExcludedTokens,
|
|
9
|
+
type Opportunity,
|
|
10
|
+
type TokenBalance,
|
|
11
|
+
type TransactionRequest,
|
|
12
|
+
} from "@turtleclub/hooks";
|
|
13
|
+
import { NativeDepositSection } from "../deposit/NativeDepositSection";
|
|
14
|
+
import { NativeWithdrawSection } from "../withdraw/NativeWithdrawSection";
|
|
15
|
+
|
|
16
|
+
export interface OpportunityActionsProps {
|
|
17
|
+
opportunity: Opportunity;
|
|
18
|
+
address: string | undefined;
|
|
19
|
+
distributorId: string;
|
|
20
|
+
executeTransaction: (tx: TransactionRequest) => Promise<string | undefined>;
|
|
21
|
+
onDepositSuccess?: () => void;
|
|
22
|
+
/** Current wallet chain ID */
|
|
23
|
+
chainId?: number;
|
|
24
|
+
/** Function to switch chain */
|
|
25
|
+
switchChain?: (chainId: number) => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sortByBalance(balances: TokenBalance[]): TokenBalance[] {
|
|
29
|
+
return [...balances].sort((a, b) => {
|
|
30
|
+
const aValue = BigInt(a.amount);
|
|
31
|
+
const bValue = BigInt(b.amount);
|
|
32
|
+
if (bValue > aValue) return 1;
|
|
33
|
+
if (bValue < aValue) return -1;
|
|
34
|
+
return 0;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function OpportunityActions({
|
|
39
|
+
opportunity,
|
|
40
|
+
address,
|
|
41
|
+
distributorId,
|
|
42
|
+
executeTransaction,
|
|
43
|
+
onDepositSuccess,
|
|
44
|
+
chainId,
|
|
45
|
+
switchChain,
|
|
46
|
+
}: OpportunityActionsProps) {
|
|
47
|
+
const [actionMode, setActionMode] = useState<"deposit" | "withdraw">("deposit");
|
|
48
|
+
const [depositMode, setDepositMode] = useState<"native" | "route">("native");
|
|
49
|
+
|
|
50
|
+
const opportunityChainId = Number(opportunity.receiptToken.chain.chainId)
|
|
51
|
+
|
|
52
|
+
// Use on-chain balances when: direct deposit OR withdraw
|
|
53
|
+
const useOnChainBalances = depositMode === "native" || actionMode === "withdraw";
|
|
54
|
+
|
|
55
|
+
// Always fetch both balance sources to avoid loading states when switching modes
|
|
56
|
+
const {
|
|
57
|
+
balances: depositTokenBalances,
|
|
58
|
+
isLoading: isDepositBalancesLoading,
|
|
59
|
+
refetch: refetchDepositBalances,
|
|
60
|
+
} = useGetOnChainBalance({
|
|
61
|
+
tokens: opportunity.depositTokens,
|
|
62
|
+
chainId: opportunityChainId,
|
|
63
|
+
address,
|
|
64
|
+
enabled: !!address,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const {
|
|
68
|
+
balances: allChainBalances,
|
|
69
|
+
isLoading: isAllChainBalancesLoading,
|
|
70
|
+
refetchAll: refetchAllBalances,
|
|
71
|
+
} = useBalance({
|
|
72
|
+
address,
|
|
73
|
+
chainIds: [opportunityChainId],
|
|
74
|
+
depositOpportunity: opportunity,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const rawBalances = useMemo(() => {
|
|
78
|
+
if (useOnChainBalances) {
|
|
79
|
+
if (depositTokenBalances.length > 0) {
|
|
80
|
+
return depositTokenBalances;
|
|
81
|
+
}
|
|
82
|
+
// Fallback: create TokenBalance entries from deposit tokens with 0 balance
|
|
83
|
+
return opportunity.depositTokens.map((token) => ({
|
|
84
|
+
token,
|
|
85
|
+
amount: "0",
|
|
86
|
+
source: "onchain" as const,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
return allChainBalances;
|
|
90
|
+
}, [useOnChainBalances, depositTokenBalances, allChainBalances, opportunity.depositTokens]);
|
|
91
|
+
|
|
92
|
+
const isBalancesLoading = useOnChainBalances
|
|
93
|
+
? isDepositBalancesLoading
|
|
94
|
+
: isAllChainBalancesLoading;
|
|
95
|
+
|
|
96
|
+
const refetchBalances = useOnChainBalances
|
|
97
|
+
? refetchDepositBalances
|
|
98
|
+
: refetchAllBalances;
|
|
99
|
+
|
|
100
|
+
const balances = useMemo(() => {
|
|
101
|
+
let filtered = rawBalances;
|
|
102
|
+
|
|
103
|
+
// Filter by opportunity chain
|
|
104
|
+
if (!useOnChainBalances && opportunityChainId) {
|
|
105
|
+
filtered = filtered.filter((b) => {
|
|
106
|
+
const tokenChainId = Number(b.token.chain?.chainId);
|
|
107
|
+
return tokenChainId === opportunityChainId;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Filter zero balances (only for all-chain mode)
|
|
112
|
+
if (!useOnChainBalances) {
|
|
113
|
+
filtered = filtered.filter((b) => BigInt(b.amount) > 0n);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Filter excluded tokens
|
|
117
|
+
filtered = filterExcludedTokens(filtered);
|
|
118
|
+
|
|
119
|
+
return sortByBalance(filtered);
|
|
120
|
+
}, [rawBalances, useOnChainBalances, opportunityChainId]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="flex flex-col gap-3">
|
|
124
|
+
<SegmentControl
|
|
125
|
+
value={actionMode}
|
|
126
|
+
onChange={(value) => setActionMode(value as "deposit" | "withdraw")}
|
|
127
|
+
items={[
|
|
128
|
+
{
|
|
129
|
+
value: "deposit",
|
|
130
|
+
label: opportunity.depositDisabled ? (
|
|
131
|
+
<TurtleTooltip
|
|
132
|
+
trigger={<span>Deposit</span>}
|
|
133
|
+
content={<span className="block p-2">Deposits are disabled for this opportunity</span>}
|
|
134
|
+
/>
|
|
135
|
+
) : (
|
|
136
|
+
"Deposit"
|
|
137
|
+
),
|
|
138
|
+
disabled: opportunity.depositDisabled,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
value: "withdraw",
|
|
142
|
+
label: opportunity.withdrawalDisabled ? (
|
|
143
|
+
<TurtleTooltip
|
|
144
|
+
trigger={<span>Withdraw</span>}
|
|
145
|
+
content={<span className="block p-2">Withdrawals are disabled for this opportunity</span>}
|
|
146
|
+
/>
|
|
147
|
+
) : (
|
|
148
|
+
"Withdraw"
|
|
149
|
+
),
|
|
150
|
+
disabled: opportunity.withdrawalDisabled,
|
|
151
|
+
},
|
|
152
|
+
]}
|
|
153
|
+
variant="segment"
|
|
154
|
+
|
|
155
|
+
/>
|
|
156
|
+
|
|
157
|
+
{actionMode === "deposit" ? (
|
|
158
|
+
<NativeDepositSection
|
|
159
|
+
opportunity={opportunity}
|
|
160
|
+
address={address}
|
|
161
|
+
distributorId={distributorId}
|
|
162
|
+
balances={balances}
|
|
163
|
+
isBalancesLoading={isBalancesLoading}
|
|
164
|
+
refetchBalances={refetchBalances}
|
|
165
|
+
executeTransaction={executeTransaction}
|
|
166
|
+
onDepositSuccess={onDepositSuccess}
|
|
167
|
+
depositMode={depositMode}
|
|
168
|
+
onDepositModeChange={opportunity.earnEnabled ? setDepositMode : undefined}
|
|
169
|
+
chainId={chainId}
|
|
170
|
+
switchChain={switchChain}
|
|
171
|
+
/>
|
|
172
|
+
) : (
|
|
173
|
+
<NativeWithdrawSection
|
|
174
|
+
opportunity={opportunity}
|
|
175
|
+
address={address}
|
|
176
|
+
balances={balances}
|
|
177
|
+
isBalancesLoading={isBalancesLoading}
|
|
178
|
+
/>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OpportunityActions, type OpportunityActionsProps } from "./OpportunityActions";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Opportunity, TokenBalance } from "@turtleclub/hooks";
|
|
2
2
|
import { APIStatus, DataTable } from "@turtleclub/ui";
|
|
3
|
-
import type { ColumnDef } from "@tanstack/react-table";
|
|
3
|
+
import type { ColumnDef, Row } from "@tanstack/react-table";
|
|
4
4
|
import { formatCurrency } from "@turtleclub/utils";
|
|
5
5
|
import { getTotalYield } from "../hooks/useTotalYield";
|
|
6
6
|
import { calculateNetAPR } from "../utils/calculateNetAPR";
|
|
@@ -145,7 +145,7 @@ export function OpportunitiesTable({
|
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
147
|
id: "yourTvl",
|
|
148
|
-
|
|
148
|
+
accessorFn: (row) => userTvlByOpportunity[row.id || ""] || 0,
|
|
149
149
|
header: () => <div className="flex w-full justify-end">Your TVL</div>,
|
|
150
150
|
cell: ({ row }) => {
|
|
151
151
|
const yourTvl = userTvlByOpportunity[row.original.id || ""] || 0;
|
|
@@ -156,7 +156,7 @@ export function OpportunitiesTable({
|
|
|
156
156
|
);
|
|
157
157
|
},
|
|
158
158
|
enableSorting: true,
|
|
159
|
-
sortingFn: (rowA
|
|
159
|
+
sortingFn: (rowA: Row<Opportunity>, rowB: Row<Opportunity>) => {
|
|
160
160
|
const a = userTvlByOpportunity[rowA.original.id || ""] || 0;
|
|
161
161
|
const b = userTvlByOpportunity[rowB.original.id || ""] || 0;
|
|
162
162
|
return a - b;
|
|
@@ -180,7 +180,8 @@ export function OpportunitiesTable({
|
|
|
180
180
|
enableSorting: true,
|
|
181
181
|
},
|
|
182
182
|
{
|
|
183
|
-
|
|
183
|
+
id: "totalYield",
|
|
184
|
+
accessorFn: (row) => calculateNetAPR(row.incentives, row.vaultConfig ?? undefined),
|
|
184
185
|
header: () => <div className="flex w-full justify-end">Est. APR</div>,
|
|
185
186
|
cell: ({ row }) => (
|
|
186
187
|
<div className="flex flex-row-reverse">
|
|
@@ -192,7 +193,7 @@ export function OpportunitiesTable({
|
|
|
192
193
|
</div>
|
|
193
194
|
),
|
|
194
195
|
enableSorting: true,
|
|
195
|
-
sortingFn: (rowA
|
|
196
|
+
sortingFn: (rowA: Row<Opportunity>, rowB: Row<Opportunity>) => {
|
|
196
197
|
const a = calculateNetAPR(rowA.original.incentives, rowA.original.vaultConfig ?? undefined);
|
|
197
198
|
const b = calculateNetAPR(rowB.original.incentives, rowB.original.vaultConfig ?? undefined);
|
|
198
199
|
return a - b;
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
// V1 - Legacy (uses TokenStep from hooks)
|
|
1
2
|
export { RouteDetails } from "./route-details";
|
|
2
3
|
export type { TokenStep } from "./route-details";
|
|
4
|
+
|
|
5
|
+
// V2 - New API structure (uses RouteMetadata)
|
|
6
|
+
export { RouteDetailsV2 } from "./route-details-v2";
|
|
7
|
+
export type { RouteDetailsV2Props } from "./route-details-v2";
|
|
8
|
+
export type { RouteMetadata, RouteStep, RouteToken } from "./types";
|