@turtleclub/opportunities 0.1.0-beta.60 → 0.1.0-beta.62
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 +12 -0
- package/package.json +5 -5
- package/src/deposit/NativeDepositSection.tsx +48 -27
- package/src/deposit/TemporalWrapper.tsx +15 -88
- package/src/index.ts +6 -0
- package/src/opportunity-actions/OpportunityActions.tsx +21 -12
- package/src/opportunity-table/components/opportunities-table.tsx +19 -9
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [0.1.0-beta.62](https://github.com/turtle-dao/turtle-tools/compare/@turtleclub/opportunities@0.1.0-beta.61...@turtleclub/opportunities@0.1.0-beta.62) (2026-01-28)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- implement turtle-basic UI scaffold with platform adapters ([#235](https://github.com/turtle-dao/turtle-tools/issues/235)) ([89dea4b](https://github.com/turtle-dao/turtle-tools/commit/89dea4b944a4e1dda8ae11ae17b6848ef1c81fa6))
|
|
11
|
+
|
|
12
|
+
# [0.1.0-beta.61](https://github.com/turtle-dao/turtle-tools/compare/@turtleclub/opportunities@0.1.0-beta.60...@turtleclub/opportunities@0.1.0-beta.61) (2026-01-28)
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- deposit improvements ([#234](https://github.com/turtle-dao/turtle-tools/issues/234)) ([aef8505](https://github.com/turtle-dao/turtle-tools/commit/aef8505b0768f9aeea65b7499dcb090553dd69df))
|
|
17
|
+
|
|
6
18
|
# [0.1.0-beta.60](https://github.com/turtle-dao/turtle-tools/compare/@turtleclub/opportunities@0.1.0-beta.59...@turtleclub/opportunities@0.1.0-beta.60) (2026-01-27)
|
|
7
19
|
|
|
8
20
|
**Note:** Version bump only for package @turtleclub/opportunities
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@turtleclub/opportunities",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.62",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"@tanstack/react-form": "^1.27.6",
|
|
14
14
|
"@tanstack/react-query": "^5.62.3",
|
|
15
15
|
"@tanstack/react-table": "^8.21.3",
|
|
16
|
-
"@turtleclub/hooks": "0.5.0-beta.
|
|
17
|
-
"@turtleclub/multichain": "0.5.0-beta.
|
|
18
|
-
"@turtleclub/ui": "0.7.0-beta.
|
|
16
|
+
"@turtleclub/hooks": "0.5.0-beta.44",
|
|
17
|
+
"@turtleclub/multichain": "0.5.0-beta.1",
|
|
18
|
+
"@turtleclub/ui": "0.7.0-beta.24",
|
|
19
19
|
"@turtleclub/utils": "0.4.0-beta.0",
|
|
20
20
|
"jotai": "^2.10.3",
|
|
21
21
|
"lucide-react": "^0.542.0",
|
|
@@ -31,5 +31,5 @@
|
|
|
31
31
|
"@types/react-dom": "^18.3.5",
|
|
32
32
|
"typescript": "^5.7.2"
|
|
33
33
|
},
|
|
34
|
-
"gitHead": "
|
|
34
|
+
"gitHead": "b4cd0702ae6d9df357b930c8c86a371be7cec516"
|
|
35
35
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
4
|
import { Button, cn, SegmentControl, SlippageSelector, TurtleTooltip } from "@turtleclub/ui";
|
|
5
5
|
import { InfoIcon } from "lucide-react";
|
|
6
6
|
import {
|
|
@@ -15,6 +15,19 @@ import { RouteDetailsV2 } from "../route-details/route-details-v2";
|
|
|
15
15
|
const DEFAULT_SLIPPAGE = 0.005; // 0.5%
|
|
16
16
|
const SECONDS_IN_DAY = 86400;
|
|
17
17
|
|
|
18
|
+
/** Returns true if error is expected user behavior (not a real error to show) */
|
|
19
|
+
function isUserRejectedError(error: Error | null): boolean {
|
|
20
|
+
if (!error) return false;
|
|
21
|
+
const message = error.message.toLowerCase();
|
|
22
|
+
return (
|
|
23
|
+
message.includes("user rejected") ||
|
|
24
|
+
message.includes("user denied") ||
|
|
25
|
+
message.includes("rejected the request") ||
|
|
26
|
+
message.includes("user cancelled") ||
|
|
27
|
+
message.includes("user canceled")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
export interface NativeDepositSectionProps {
|
|
19
32
|
opportunity: Opportunity;
|
|
20
33
|
address: string | undefined;
|
|
@@ -22,7 +35,12 @@ export interface NativeDepositSectionProps {
|
|
|
22
35
|
balances: TokenBalance[];
|
|
23
36
|
isBalancesLoading?: boolean;
|
|
24
37
|
refetchBalances?: () => void;
|
|
25
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Execute a transaction and wait for receipt confirmation.
|
|
40
|
+
* IMPORTANT: This function MUST wait for the transaction receipt before returning.
|
|
41
|
+
* The returned hash should only be provided after the transaction is confirmed on-chain.
|
|
42
|
+
*/
|
|
43
|
+
executeTransactionAndWait: (tx: TransactionRequest) => Promise<string | undefined>;
|
|
26
44
|
onDepositSuccess?: () => void;
|
|
27
45
|
depositMode?: "native" | "route";
|
|
28
46
|
onDepositModeChange?: (mode: "native" | "route") => void;
|
|
@@ -39,7 +57,7 @@ export function NativeDepositSection({
|
|
|
39
57
|
balances,
|
|
40
58
|
isBalancesLoading = false,
|
|
41
59
|
refetchBalances,
|
|
42
|
-
|
|
60
|
+
executeTransactionAndWait,
|
|
43
61
|
onDepositSuccess,
|
|
44
62
|
depositMode = "native",
|
|
45
63
|
onDepositModeChange,
|
|
@@ -48,6 +66,11 @@ export function NativeDepositSection({
|
|
|
48
66
|
}: NativeDepositSectionProps) {
|
|
49
67
|
const [slippage, setSlippage] = useState(DEFAULT_SLIPPAGE);
|
|
50
68
|
|
|
69
|
+
// Secondary market always uses swap mode and hides the mode selector
|
|
70
|
+
const isSecondaryOnly = opportunity.vaultConfig?.secondaryOnly === true;
|
|
71
|
+
const effectiveDepositMode = isSecondaryOnly ? "route" : depositMode;
|
|
72
|
+
const effectiveOnDepositModeChange = isSecondaryOnly ? undefined : onDepositModeChange;
|
|
73
|
+
|
|
51
74
|
// Convert slippage percentage to basis points (0.005 -> 50 bps)
|
|
52
75
|
const slippageBps = Math.round(slippage * 10000);
|
|
53
76
|
|
|
@@ -56,13 +79,20 @@ export function NativeDepositSection({
|
|
|
56
79
|
userAddress: address,
|
|
57
80
|
distributorId,
|
|
58
81
|
balances,
|
|
59
|
-
|
|
82
|
+
executeTransactionAndWait,
|
|
60
83
|
onDepositSuccess,
|
|
61
84
|
refetchBalances,
|
|
62
|
-
slippageBps:
|
|
85
|
+
slippageBps: effectiveDepositMode === "route" ? slippageBps : undefined,
|
|
63
86
|
walletChainId: chainId,
|
|
64
87
|
});
|
|
65
88
|
|
|
89
|
+
// Log deposit errors (moved from render to avoid side-effects)
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (deposit.error && !isUserRejectedError(deposit.error)) {
|
|
92
|
+
console.error("[NativeDepositSection] Deposit error:", deposit.error);
|
|
93
|
+
}
|
|
94
|
+
}, [deposit.error]);
|
|
95
|
+
|
|
66
96
|
const handleButtonClick = async () => {
|
|
67
97
|
if (validation.isWrongChain && validation.requiredChainId && switchChain) {
|
|
68
98
|
await switchChain(validation.requiredChainId);
|
|
@@ -77,10 +107,11 @@ export function NativeDepositSection({
|
|
|
77
107
|
|
|
78
108
|
return (
|
|
79
109
|
<div className="space-y-3">
|
|
80
|
-
{
|
|
110
|
+
{/* Mode selector: only show when mode can be changed */}
|
|
111
|
+
{effectiveOnDepositModeChange && (
|
|
81
112
|
<SegmentControl
|
|
82
|
-
value={
|
|
83
|
-
onChange={(value) =>
|
|
113
|
+
value={effectiveDepositMode === "native" ? "native" : "route"}
|
|
114
|
+
onChange={(value) => effectiveOnDepositModeChange(value as "native" | "route")}
|
|
84
115
|
items={[
|
|
85
116
|
{
|
|
86
117
|
value: "native",
|
|
@@ -121,18 +152,6 @@ export function NativeDepositSection({
|
|
|
121
152
|
size="sm"
|
|
122
153
|
className="w-fit mt-4"
|
|
123
154
|
/>
|
|
124
|
-
) : (
|
|
125
|
-
<span className="flex items-center gap-1.5 text-sm">
|
|
126
|
-
Direct Deposit
|
|
127
|
-
<TurtleTooltip
|
|
128
|
-
trigger={<InfoIcon className="size-3 opacity-60" />}
|
|
129
|
-
content={
|
|
130
|
-
<span className="p-2 block">
|
|
131
|
-
Deposit directly using the vault's accepted tokens. Lower fees, faster execution.
|
|
132
|
-
</span>
|
|
133
|
-
}
|
|
134
|
-
/>
|
|
135
|
-
</span>
|
|
136
155
|
)}
|
|
137
156
|
|
|
138
157
|
<SwapInputV3
|
|
@@ -147,11 +166,11 @@ export function NativeDepositSection({
|
|
|
147
166
|
showBalance={true}
|
|
148
167
|
className={cn(
|
|
149
168
|
"border border-border p-5",
|
|
150
|
-
validation.hasInsufficientBalance && "border-destructive/50"
|
|
169
|
+
validation.hasInsufficientBalance && "border-destructive/50"
|
|
151
170
|
)}
|
|
152
171
|
/>
|
|
153
172
|
|
|
154
|
-
{
|
|
173
|
+
{effectiveDepositMode === "route" && (
|
|
155
174
|
<SlippageSelector value={slippage} onChange={setSlippage} className="px-2" />
|
|
156
175
|
)}
|
|
157
176
|
|
|
@@ -162,8 +181,14 @@ export function NativeDepositSection({
|
|
|
162
181
|
approveAmount={selection.amount}
|
|
163
182
|
/>
|
|
164
183
|
)}
|
|
184
|
+
{/* Error messages */}
|
|
185
|
+
{deposit.error && !isUserRejectedError(deposit.error) && (
|
|
186
|
+
<p className="text-destructive text-sm">Something went wrong. Please try again.</p>
|
|
187
|
+
)}
|
|
165
188
|
|
|
166
|
-
{
|
|
189
|
+
{validation.validationMessage && validation.hasInsufficientBalance && (
|
|
190
|
+
<p className="text-destructive text-xs">{validation.validationMessage}</p>
|
|
191
|
+
)}
|
|
167
192
|
|
|
168
193
|
<GeoCheckBlocker>
|
|
169
194
|
<Button
|
|
@@ -175,10 +200,6 @@ export function NativeDepositSection({
|
|
|
175
200
|
</Button>
|
|
176
201
|
</GeoCheckBlocker>
|
|
177
202
|
|
|
178
|
-
{validation.validationMessage && validation.hasInsufficientBalance && (
|
|
179
|
-
<p className="text-destructive text-xs">{validation.validationMessage}</p>
|
|
180
|
-
)}
|
|
181
|
-
|
|
182
203
|
{/* {(validation.depositFee !== null ||
|
|
183
204
|
validation.performanceFee !== null ||
|
|
184
205
|
validation.withdrawalCooldownSecs !== null) && (
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState
|
|
3
|
+
import { useState } from "react";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
useBalance,
|
|
7
|
-
filterExcludedTokens,
|
|
5
|
+
useActionsDefaultParams,
|
|
8
6
|
type Opportunity,
|
|
9
|
-
type TokenBalance,
|
|
10
7
|
type TransactionRequest,
|
|
11
8
|
} from "@turtleclub/hooks";
|
|
12
9
|
import { NativeDepositSection } from "./NativeDepositSection";
|
|
@@ -17,7 +14,12 @@ export interface TemporalWrapperProps {
|
|
|
17
14
|
opportunity: Opportunity;
|
|
18
15
|
address: string | undefined;
|
|
19
16
|
distributorId: string;
|
|
20
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Execute a transaction and wait for receipt confirmation.
|
|
19
|
+
* IMPORTANT: This function MUST wait for the transaction receipt before returning.
|
|
20
|
+
* The returned hash should only be provided after the transaction is confirmed on-chain.
|
|
21
|
+
*/
|
|
22
|
+
executeTransactionAndWait: (tx: TransactionRequest) => Promise<string | undefined>;
|
|
21
23
|
onDepositSuccess?: () => void;
|
|
22
24
|
/** Current wallet chain ID */
|
|
23
25
|
chainId?: number;
|
|
@@ -29,16 +31,6 @@ export interface TemporalWrapperProps {
|
|
|
29
31
|
showDepositModeSelector?: boolean;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
function sortByBalance(balances: TokenBalance[]): TokenBalance[] {
|
|
33
|
-
return [...balances].sort((a, b) => {
|
|
34
|
-
const aValue = BigInt(a.amount);
|
|
35
|
-
const bValue = BigInt(b.amount);
|
|
36
|
-
if (bValue > aValue) return 1;
|
|
37
|
-
if (bValue < aValue) return -1;
|
|
38
|
-
return 0;
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
34
|
/**
|
|
43
35
|
* TemporalWrapper - A standalone wrapper for NativeDepositSection
|
|
44
36
|
*
|
|
@@ -49,7 +41,7 @@ export function TemporalWrapper({
|
|
|
49
41
|
opportunity,
|
|
50
42
|
address,
|
|
51
43
|
distributorId,
|
|
52
|
-
|
|
44
|
+
executeTransactionAndWait,
|
|
53
45
|
onDepositSuccess,
|
|
54
46
|
chainId,
|
|
55
47
|
switchChain,
|
|
@@ -58,80 +50,15 @@ export function TemporalWrapper({
|
|
|
58
50
|
}: TemporalWrapperProps) {
|
|
59
51
|
const [depositMode, setDepositMode] = useState<DepositMode>(initialDepositMode);
|
|
60
52
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const useOnChainBalances = depositMode === "native";
|
|
65
|
-
|
|
66
|
-
// Fetch deposit token balances (on-chain)
|
|
67
|
-
const {
|
|
68
|
-
balances: depositTokenBalances,
|
|
69
|
-
isLoading: isDepositBalancesLoading,
|
|
70
|
-
refetch: refetchDepositBalances,
|
|
71
|
-
} = useGetOnChainBalance({
|
|
72
|
-
tokens: opportunity.depositTokens,
|
|
73
|
-
chainId: opportunityChainId,
|
|
74
|
-
address,
|
|
75
|
-
enabled: !!address,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Fetch all balances for route/swap mode
|
|
79
|
-
const {
|
|
80
|
-
balances: allChainBalances,
|
|
81
|
-
isLoading: isAllChainBalancesLoading,
|
|
82
|
-
refetchAll: refetchAllBalances,
|
|
83
|
-
} = useBalance({
|
|
53
|
+
// Hook handles isSecondaryOnly internally for balance fetching
|
|
54
|
+
const { balances, isBalancesLoading, refetchBalances } = useActionsDefaultParams({
|
|
55
|
+
opportunity,
|
|
84
56
|
address,
|
|
85
|
-
|
|
86
|
-
depositOpportunity: opportunity,
|
|
57
|
+
depositMode,
|
|
87
58
|
});
|
|
88
59
|
|
|
89
|
-
const rawBalances = useMemo(() => {
|
|
90
|
-
if (useOnChainBalances) {
|
|
91
|
-
if (depositTokenBalances.length > 0) {
|
|
92
|
-
return depositTokenBalances;
|
|
93
|
-
}
|
|
94
|
-
// Fallback: create TokenBalance entries from deposit tokens with 0 balance
|
|
95
|
-
return opportunity.depositTokens.map((token) => ({
|
|
96
|
-
token,
|
|
97
|
-
amount: "0",
|
|
98
|
-
source: "onchain" as const,
|
|
99
|
-
}));
|
|
100
|
-
}
|
|
101
|
-
return allChainBalances;
|
|
102
|
-
}, [useOnChainBalances, depositTokenBalances, allChainBalances, opportunity.depositTokens]);
|
|
103
|
-
|
|
104
|
-
const isBalancesLoading = useOnChainBalances
|
|
105
|
-
? isDepositBalancesLoading
|
|
106
|
-
: isAllChainBalancesLoading;
|
|
107
|
-
|
|
108
|
-
const refetchBalances = useOnChainBalances
|
|
109
|
-
? refetchDepositBalances
|
|
110
|
-
: refetchAllBalances;
|
|
111
|
-
|
|
112
|
-
const balances = useMemo(() => {
|
|
113
|
-
let filtered = rawBalances;
|
|
114
|
-
|
|
115
|
-
// Filter by opportunity chain (only for route mode)
|
|
116
|
-
if (!useOnChainBalances && opportunityChainId) {
|
|
117
|
-
filtered = filtered.filter((b) => {
|
|
118
|
-
const tokenChainId = Number(b.token.chain?.chainId);
|
|
119
|
-
return tokenChainId === opportunityChainId;
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Filter zero balances (only for route mode)
|
|
124
|
-
if (!useOnChainBalances) {
|
|
125
|
-
filtered = filtered.filter((b) => BigInt(b.amount) > 0n);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Filter excluded tokens
|
|
129
|
-
filtered = filterExcludedTokens(filtered);
|
|
130
|
-
|
|
131
|
-
return sortByBalance(filtered);
|
|
132
|
-
}, [rawBalances, useOnChainBalances, opportunityChainId]);
|
|
133
|
-
|
|
134
60
|
// Determine if we should show the mode selector
|
|
61
|
+
// NativeDepositSection handles isSecondaryOnly internally for UI
|
|
135
62
|
const onDepositModeChange = showDepositModeSelector && opportunity.earnEnabled
|
|
136
63
|
? setDepositMode
|
|
137
64
|
: undefined;
|
|
@@ -144,7 +71,7 @@ export function TemporalWrapper({
|
|
|
144
71
|
balances={balances}
|
|
145
72
|
isBalancesLoading={isBalancesLoading}
|
|
146
73
|
refetchBalances={refetchBalances}
|
|
147
|
-
|
|
74
|
+
executeTransactionAndWait={executeTransactionAndWait}
|
|
148
75
|
onDepositSuccess={onDepositSuccess}
|
|
149
76
|
depositMode={depositMode}
|
|
150
77
|
onDepositModeChange={onDepositModeChange}
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,12 @@ export interface OpportunityActionsProps {
|
|
|
17
17
|
opportunity: Opportunity;
|
|
18
18
|
address: string | undefined;
|
|
19
19
|
distributorId: string;
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Execute a transaction and wait for receipt confirmation.
|
|
22
|
+
* IMPORTANT: This function MUST wait for the transaction receipt before returning.
|
|
23
|
+
* The returned hash should only be provided after the transaction is confirmed on-chain.
|
|
24
|
+
*/
|
|
25
|
+
executeTransactionAndWait: (tx: TransactionRequest) => Promise<string | undefined>;
|
|
21
26
|
onDepositSuccess?: () => void;
|
|
22
27
|
/** Current wallet chain ID */
|
|
23
28
|
chainId?: number;
|
|
@@ -39,7 +44,7 @@ export function OpportunityActions({
|
|
|
39
44
|
opportunity,
|
|
40
45
|
address,
|
|
41
46
|
distributorId,
|
|
42
|
-
|
|
47
|
+
executeTransactionAndWait,
|
|
43
48
|
onDepositSuccess,
|
|
44
49
|
chainId,
|
|
45
50
|
switchChain,
|
|
@@ -47,10 +52,14 @@ export function OpportunityActions({
|
|
|
47
52
|
const [actionMode, setActionMode] = useState<"deposit" | "withdraw">("deposit");
|
|
48
53
|
const [depositMode, setDepositMode] = useState<"native" | "route">("native");
|
|
49
54
|
|
|
55
|
+
const isSecondaryOnly = opportunity.vaultConfig?.secondaryOnly === true;
|
|
56
|
+
|
|
57
|
+
// For secondary market, always use swap mode
|
|
58
|
+
const effectiveDepositMode = isSecondaryOnly ? "route" : depositMode;
|
|
50
59
|
const opportunityChainId = Number(opportunity.receiptToken.chain.chainId)
|
|
51
60
|
|
|
52
61
|
// Use on-chain balances when: direct deposit OR withdraw
|
|
53
|
-
const useOnChainBalances =
|
|
62
|
+
const useOnChainBalances = effectiveDepositMode === "native" || actionMode === "withdraw";
|
|
54
63
|
|
|
55
64
|
// Always fetch both balance sources to avoid loading states when switching modes
|
|
56
65
|
const {
|
|
@@ -129,11 +138,11 @@ export function OpportunityActions({
|
|
|
129
138
|
value: "deposit",
|
|
130
139
|
label: opportunity.depositDisabled ? (
|
|
131
140
|
<TurtleTooltip
|
|
132
|
-
trigger={<span>Deposit</span>}
|
|
133
|
-
content={<span className="block p-2">Deposits are disabled for this opportunity</span>}
|
|
141
|
+
trigger={<span>{isSecondaryOnly ? "Buy" : "Deposit"}</span>}
|
|
142
|
+
content={<span className="block p-2">{isSecondaryOnly ? "Purchases are" : "Deposits are"} disabled for this opportunity</span>}
|
|
134
143
|
/>
|
|
135
144
|
) : (
|
|
136
|
-
"Deposit"
|
|
145
|
+
isSecondaryOnly ? "Buy" : "Deposit"
|
|
137
146
|
),
|
|
138
147
|
disabled: opportunity.depositDisabled,
|
|
139
148
|
},
|
|
@@ -141,11 +150,11 @@ export function OpportunityActions({
|
|
|
141
150
|
value: "withdraw",
|
|
142
151
|
label: opportunity.withdrawalDisabled ? (
|
|
143
152
|
<TurtleTooltip
|
|
144
|
-
trigger={<span>Withdraw</span>}
|
|
145
|
-
content={<span className="block p-2">Withdrawals are disabled for this opportunity</span>}
|
|
153
|
+
trigger={<span>{isSecondaryOnly ? "Sell" : "Withdraw"}</span>}
|
|
154
|
+
content={<span className="block p-2">{isSecondaryOnly ? "Sales are" : "Withdrawals are"} disabled for this opportunity</span>}
|
|
146
155
|
/>
|
|
147
156
|
) : (
|
|
148
|
-
"Withdraw"
|
|
157
|
+
isSecondaryOnly ? "Sell" : "Withdraw"
|
|
149
158
|
),
|
|
150
159
|
disabled: opportunity.withdrawalDisabled,
|
|
151
160
|
},
|
|
@@ -162,10 +171,10 @@ export function OpportunityActions({
|
|
|
162
171
|
balances={balances}
|
|
163
172
|
isBalancesLoading={isBalancesLoading}
|
|
164
173
|
refetchBalances={refetchBalances}
|
|
165
|
-
|
|
174
|
+
executeTransactionAndWait={executeTransactionAndWait}
|
|
166
175
|
onDepositSuccess={onDepositSuccess}
|
|
167
|
-
depositMode={
|
|
168
|
-
onDepositModeChange={opportunity.earnEnabled ? setDepositMode : undefined}
|
|
176
|
+
depositMode={effectiveDepositMode}
|
|
177
|
+
onDepositModeChange={opportunity.earnEnabled && !isSecondaryOnly ? setDepositMode : undefined}
|
|
169
178
|
chainId={chainId}
|
|
170
179
|
switchChain={switchChain}
|
|
171
180
|
/>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Opportunity, TokenBalance } from "@turtleclub/hooks";
|
|
2
|
-
import { APIStatus, DataTable } from "@turtleclub/ui";
|
|
2
|
+
import { APIStatus, DataTable, cn } from "@turtleclub/ui";
|
|
3
3
|
import type { ColumnDef, Row } from "@tanstack/react-table";
|
|
4
4
|
import { formatCurrency } from "@turtleclub/utils";
|
|
5
5
|
import { getTotalYield } from "../hooks/useTotalYield";
|
|
@@ -17,6 +17,7 @@ interface OpportunitiesTableProps {
|
|
|
17
17
|
userTvlByOpportunity?: Record<string, number>;
|
|
18
18
|
turtleTvl?: Record<string, number>;
|
|
19
19
|
isWidget?: boolean;
|
|
20
|
+
compact?: boolean;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export function OpportunitiesTable({
|
|
@@ -29,6 +30,7 @@ export function OpportunitiesTable({
|
|
|
29
30
|
userTvlByOpportunity = {},
|
|
30
31
|
turtleTvl = {},
|
|
31
32
|
isWidget = false,
|
|
33
|
+
compact = false,
|
|
32
34
|
}: OpportunitiesTableProps) {
|
|
33
35
|
const handleSelectOpportunity = (opportunity: Opportunity) => {
|
|
34
36
|
onSelectOpportunity(opportunity);
|
|
@@ -41,16 +43,21 @@ export function OpportunitiesTable({
|
|
|
41
43
|
onClose?.();
|
|
42
44
|
};
|
|
43
45
|
|
|
46
|
+
const iconSize = compact ? "size-5" : "size-6";
|
|
47
|
+
const nameTextSize = compact ? "text-sm" : "";
|
|
48
|
+
const tvlTextSize = compact ? "text-xs" : "";
|
|
49
|
+
const aprTextSize = compact ? "text-sm" : "";
|
|
50
|
+
|
|
44
51
|
const columns: ColumnDef<Opportunity>[] = [
|
|
45
52
|
{
|
|
46
53
|
accessorKey: "name",
|
|
47
54
|
header: () => <div>Opportunity</div>,
|
|
48
55
|
cell: ({ row }) => (
|
|
49
|
-
<div className="flex items-center gap-2">
|
|
56
|
+
<div className={cn("flex items-center gap-2", nameTextSize)}>
|
|
50
57
|
<div className="relative shrink-0">
|
|
51
58
|
<img
|
|
52
59
|
src={row.original.receiptToken.logoUrl}
|
|
53
|
-
className="
|
|
60
|
+
className={cn("rounded-full", iconSize)}
|
|
54
61
|
loading="lazy"
|
|
55
62
|
alt={row.original.receiptToken.name}
|
|
56
63
|
/>
|
|
@@ -89,7 +96,7 @@ export function OpportunitiesTable({
|
|
|
89
96
|
<div className="flex h-[30px] items-center gap-2">
|
|
90
97
|
<img
|
|
91
98
|
src={row.original.products[0].organization.iconUrl || ""}
|
|
92
|
-
className="
|
|
99
|
+
className={cn("rounded-full", iconSize)}
|
|
93
100
|
loading="lazy"
|
|
94
101
|
/>
|
|
95
102
|
{row.original.products[0].organization.name}
|
|
@@ -109,7 +116,7 @@ export function OpportunitiesTable({
|
|
|
109
116
|
{row.original.vaultConfig.curator.iconUrl && (
|
|
110
117
|
<img
|
|
111
118
|
src={row.original.vaultConfig.curator.iconUrl}
|
|
112
|
-
className="
|
|
119
|
+
className={cn("rounded-full", iconSize)}
|
|
113
120
|
loading="lazy"
|
|
114
121
|
alt={row.original.vaultConfig.curator.name}
|
|
115
122
|
/>
|
|
@@ -131,7 +138,7 @@ export function OpportunitiesTable({
|
|
|
131
138
|
{row.original.vaultConfig.infraProvider.iconUrl && (
|
|
132
139
|
<img
|
|
133
140
|
src={row.original.vaultConfig.infraProvider.iconUrl}
|
|
134
|
-
className="
|
|
141
|
+
className={cn("rounded-full", iconSize)}
|
|
135
142
|
loading="lazy"
|
|
136
143
|
alt={row.original.vaultConfig.infraProvider.name}
|
|
137
144
|
/>
|
|
@@ -175,7 +182,9 @@ export function OpportunitiesTable({
|
|
|
175
182
|
accessorKey: "tvl",
|
|
176
183
|
header: () => <div className="flex w-full justify-end">Total TVL</div>,
|
|
177
184
|
cell: ({ row }) => (
|
|
178
|
-
<div className="
|
|
185
|
+
<div className={compact ? cn("flex h-[30px] w-full justify-end items-center gap-2", tvlTextSize) : "text-end"}>
|
|
186
|
+
{formatCurrency(row.original.tvl, 0, true)}
|
|
187
|
+
</div>
|
|
179
188
|
),
|
|
180
189
|
enableSorting: true,
|
|
181
190
|
},
|
|
@@ -184,11 +193,12 @@ export function OpportunitiesTable({
|
|
|
184
193
|
accessorFn: (row) => calculateNetAPR(row.incentives, row.vaultConfig ?? undefined),
|
|
185
194
|
header: () => <div className="flex w-full justify-end">Est. APR</div>,
|
|
186
195
|
cell: ({ row }) => (
|
|
187
|
-
<div className="flex flex-row-reverse">
|
|
196
|
+
<div className={cn("flex flex-row-reverse", aprTextSize)}>
|
|
188
197
|
<APRBreakdownTooltip
|
|
189
198
|
incentives={row.original.incentives}
|
|
190
199
|
vaultConfig={row.original.vaultConfig ?? undefined}
|
|
191
|
-
isWidget={isWidget}
|
|
200
|
+
isWidget={compact || isWidget}
|
|
201
|
+
isPill={compact}
|
|
192
202
|
/>
|
|
193
203
|
</div>
|
|
194
204
|
),
|