@turtleclub/opportunities 0.1.0-beta.6 → 0.1.0-beta.60
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 +224 -0
- package/README.md +179 -0
- package/package.json +13 -6
- package/src/components/index.ts +1 -1
- package/src/cover-offer/README.md +46 -0
- package/src/cover-offer/components/CoverOfferCard.tsx +445 -0
- package/src/cover-offer/components/CoverRequestForm.tsx +342 -0
- package/src/cover-offer/components/CoveredEventsInfo.tsx +120 -0
- package/src/cover-offer/components/ExistingCoverInfo.tsx +74 -0
- package/src/cover-offer/components/NexusCoverSection.tsx +49 -0
- package/src/cover-offer/components/PurchaseButtonSection.tsx +106 -0
- package/src/cover-offer/constants.ts +32 -0
- package/src/cover-offer/hooks/useCheckNexusMembership.ts +32 -0
- package/src/cover-offer/hooks/useCoverQuote.ts +126 -0
- package/src/cover-offer/hooks/useCoverTokenSelection.ts +84 -0
- package/src/cover-offer/hooks/useDebouncedValue.ts +12 -0
- package/src/cover-offer/hooks/useExistingCovers.ts +101 -0
- package/src/cover-offer/hooks/useNexusProduct.ts +79 -0
- package/src/cover-offer/hooks/useNexusPurchase.ts +67 -0
- package/src/cover-offer/hooks/useTokenApproval.ts +118 -0
- package/src/cover-offer/hooks/useUserCoverNfts.ts +26 -0
- package/src/cover-offer/index.ts +4 -0
- package/src/cover-offer/types/index.ts +41 -0
- package/src/cover-offer/utils/index.ts +90 -0
- 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 +9 -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,41 @@
|
|
|
1
|
+
import { CoverAsset } from "@nexusmutual/sdk";
|
|
2
|
+
import type { Opportunity } from "@turtleclub/hooks";
|
|
3
|
+
import type { Asset } from "@turtleclub/ui";
|
|
4
|
+
|
|
5
|
+
// The Nexus SDK doesn't export the NexusProduct type, so we need to define it ourselves.
|
|
6
|
+
// The product has more attributes than just id, name, and coverAssets, but this are the only ones we need for now.
|
|
7
|
+
export type NexusProduct = {
|
|
8
|
+
id: number;
|
|
9
|
+
name: string;
|
|
10
|
+
coverAssets: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface CoverOfferCardProps {
|
|
14
|
+
productId: number;
|
|
15
|
+
opportunity: Opportunity;
|
|
16
|
+
buyerAddress: string;
|
|
17
|
+
protocolName: string;
|
|
18
|
+
coverProductName: string;
|
|
19
|
+
coverAvailableAssets: string[];
|
|
20
|
+
onSuccess?: (message: string) => void;
|
|
21
|
+
onError?: (message: string) => void;
|
|
22
|
+
startExpanded?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface NexusCoverSectionProps {
|
|
26
|
+
opportunity: Opportunity;
|
|
27
|
+
buyerAddress: string;
|
|
28
|
+
onSuccess?: (message: string) => void;
|
|
29
|
+
onError?: (message: string) => void;
|
|
30
|
+
className?: string;
|
|
31
|
+
startExpanded?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CoverRequestFormProps {
|
|
35
|
+
protocolName: string;
|
|
36
|
+
baseApyLabel?: string;
|
|
37
|
+
onDismiss?: () => void;
|
|
38
|
+
onSuccess?: (message: string) => void;
|
|
39
|
+
onError?: (message: string) => void;
|
|
40
|
+
startExpanded?: boolean;
|
|
41
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { CoverAsset } from "@nexusmutual/sdk";
|
|
2
|
+
import type { Opportunity } from "@turtleclub/hooks";
|
|
3
|
+
import { BaseError, UserRejectedRequestError } from "viem";
|
|
4
|
+
|
|
5
|
+
export function getOpportunityAPY(opportunity: Opportunity): number {
|
|
6
|
+
if (!opportunity.incentives || opportunity.incentives.length === 0) {
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return opportunity.incentives.reduce((total, incentive) => {
|
|
11
|
+
return total + (incentive.yield ?? 0);
|
|
12
|
+
}, 0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Calculate the adjusted APY after subtracting the cover cost
|
|
17
|
+
* @param opportunityAPY - The original APY as a percentage (e.g., 5 for 5%)
|
|
18
|
+
* @param yearlyCostPerc - The yearly cost as a decimal (e.g., 0.025 for 2.5%)
|
|
19
|
+
* @returns The adjusted APY as a percentage
|
|
20
|
+
*/
|
|
21
|
+
export function calculateAdjustedAPY(
|
|
22
|
+
opportunityAPY: number,
|
|
23
|
+
yearlyCostPerc: number
|
|
24
|
+
): number {
|
|
25
|
+
const coverCostPercentage = yearlyCostPerc * 100;
|
|
26
|
+
return Math.max(0, opportunityAPY - coverCostPercentage);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
export function formatAPY(apy: number): string {
|
|
31
|
+
return `${apy.toFixed(2)}%`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatEthAmount(amount: string, decimals: number = 6): string {
|
|
35
|
+
const num = parseFloat(amount);
|
|
36
|
+
if (isNaN(num)) return "0";
|
|
37
|
+
return num.toFixed(decimals);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export const formatPurchaseError = (err: unknown): string => {
|
|
42
|
+
console.error("Purchase error:", err, typeof err);
|
|
43
|
+
if (err instanceof UserRejectedRequestError) {
|
|
44
|
+
return "Transaction cancelled";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (err instanceof BaseError) {
|
|
48
|
+
const userRejected =
|
|
49
|
+
typeof err.walk === "function"
|
|
50
|
+
? err.walk((error) => error instanceof UserRejectedRequestError)
|
|
51
|
+
: false;
|
|
52
|
+
if (userRejected) {
|
|
53
|
+
return "Transaction cancelled";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return err.shortMessage || err.message;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (err instanceof Error) {
|
|
60
|
+
return err.message;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return "Failed to purchase cover";
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
const SECS_IN_DAY = 24 * 60 * 60;
|
|
69
|
+
|
|
70
|
+
export function parseCoverTiming(startSec: number, periodSec: number, gracePeriodSec: number = 0) {
|
|
71
|
+
const expiresSec = startSec + periodSec;
|
|
72
|
+
const graceEndsSec = expiresSec + gracePeriodSec;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
startDate: new Date(startSec * 1000),
|
|
76
|
+
expiresDate: new Date(expiresSec * 1000),
|
|
77
|
+
graceEndsDate: new Date(graceEndsSec * 1000),
|
|
78
|
+
periodDays: periodSec / SECS_IN_DAY,
|
|
79
|
+
graceDays: gracePeriodSec / SECS_IN_DAY,
|
|
80
|
+
expiresSec,
|
|
81
|
+
graceEndsSec,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const TOKEN_SYMBOL_TO_COVER_ASSET: Record<string, CoverAsset> = {
|
|
86
|
+
ETH: CoverAsset.ETH,
|
|
87
|
+
DAI: CoverAsset.DAI,
|
|
88
|
+
USDC: CoverAsset.USDC,
|
|
89
|
+
cbBTC: CoverAsset.cbBTC,
|
|
90
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button, cn, SegmentControl, SlippageSelector, TurtleTooltip } from "@turtleclub/ui";
|
|
5
|
+
import { InfoIcon } from "lucide-react";
|
|
6
|
+
import {
|
|
7
|
+
useDepositFlow,
|
|
8
|
+
type Opportunity,
|
|
9
|
+
type TokenBalance,
|
|
10
|
+
type TransactionRequest,
|
|
11
|
+
} from "@turtleclub/hooks";
|
|
12
|
+
import { GeoCheckBlocker, SwapInputV3 } from "./components";
|
|
13
|
+
import { RouteDetailsV2 } from "../route-details/route-details-v2";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_SLIPPAGE = 0.005; // 0.5%
|
|
16
|
+
const SECONDS_IN_DAY = 86400;
|
|
17
|
+
|
|
18
|
+
export interface NativeDepositSectionProps {
|
|
19
|
+
opportunity: Opportunity;
|
|
20
|
+
address: string | undefined;
|
|
21
|
+
distributorId: string;
|
|
22
|
+
balances: TokenBalance[];
|
|
23
|
+
isBalancesLoading?: boolean;
|
|
24
|
+
refetchBalances?: () => void;
|
|
25
|
+
executeTransaction: (tx: TransactionRequest) => Promise<string | undefined>;
|
|
26
|
+
onDepositSuccess?: () => void;
|
|
27
|
+
depositMode?: "native" | "route";
|
|
28
|
+
onDepositModeChange?: (mode: "native" | "route") => void;
|
|
29
|
+
/** Current wallet chain ID */
|
|
30
|
+
chainId?: number;
|
|
31
|
+
/** Function to switch chain */
|
|
32
|
+
switchChain?: (chainId: number) => Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function NativeDepositSection({
|
|
36
|
+
opportunity,
|
|
37
|
+
address,
|
|
38
|
+
distributorId,
|
|
39
|
+
balances,
|
|
40
|
+
isBalancesLoading = false,
|
|
41
|
+
refetchBalances,
|
|
42
|
+
executeTransaction,
|
|
43
|
+
onDepositSuccess,
|
|
44
|
+
depositMode = "native",
|
|
45
|
+
onDepositModeChange,
|
|
46
|
+
chainId,
|
|
47
|
+
switchChain,
|
|
48
|
+
}: NativeDepositSectionProps) {
|
|
49
|
+
const [slippage, setSlippage] = useState(DEFAULT_SLIPPAGE);
|
|
50
|
+
|
|
51
|
+
// Convert slippage percentage to basis points (0.005 -> 50 bps)
|
|
52
|
+
const slippageBps = Math.round(slippage * 10000);
|
|
53
|
+
|
|
54
|
+
const { selection, validation, deposit } = useDepositFlow({
|
|
55
|
+
opportunity,
|
|
56
|
+
userAddress: address,
|
|
57
|
+
distributorId,
|
|
58
|
+
balances,
|
|
59
|
+
executeTransaction,
|
|
60
|
+
onDepositSuccess,
|
|
61
|
+
refetchBalances,
|
|
62
|
+
slippageBps: depositMode === "route" ? slippageBps : undefined,
|
|
63
|
+
walletChainId: chainId,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const handleButtonClick = async () => {
|
|
67
|
+
if (validation.isWrongChain && validation.requiredChainId && switchChain) {
|
|
68
|
+
await switchChain(validation.requiredChainId);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await deposit.execute();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Only show loading state if we don't have balances yet (initial load)
|
|
75
|
+
// This prevents disabling the input during background refetches
|
|
76
|
+
const isInitialBalancesLoading = isBalancesLoading && balances.length === 0;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="space-y-3">
|
|
80
|
+
{onDepositModeChange ? (
|
|
81
|
+
<SegmentControl
|
|
82
|
+
value={depositMode === "native" ? "native" : "route"}
|
|
83
|
+
onChange={(value) => onDepositModeChange(value as "native" | "route")}
|
|
84
|
+
items={[
|
|
85
|
+
{
|
|
86
|
+
value: "native",
|
|
87
|
+
label: (
|
|
88
|
+
<span className="flex items-center gap-1.5">
|
|
89
|
+
Direct Deposit
|
|
90
|
+
<TurtleTooltip
|
|
91
|
+
trigger={<InfoIcon className="size-3 opacity-60" />}
|
|
92
|
+
content={
|
|
93
|
+
<span className="p-2 block">
|
|
94
|
+
Deposit directly using the vault's accepted tokens. Lower fees, faster
|
|
95
|
+
execution.
|
|
96
|
+
</span>
|
|
97
|
+
}
|
|
98
|
+
/>
|
|
99
|
+
</span>
|
|
100
|
+
),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
value: "route",
|
|
104
|
+
label: (
|
|
105
|
+
<span className="flex items-center gap-1.5">
|
|
106
|
+
Swap
|
|
107
|
+
<TurtleTooltip
|
|
108
|
+
trigger={<InfoIcon className="size-3 opacity-60" />}
|
|
109
|
+
content={
|
|
110
|
+
<span className="p-2 block">
|
|
111
|
+
Deposit any token from the chain. Automatically swaps to the vault's deposit
|
|
112
|
+
token.
|
|
113
|
+
</span>
|
|
114
|
+
}
|
|
115
|
+
/>
|
|
116
|
+
</span>
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
]}
|
|
120
|
+
variant="pill"
|
|
121
|
+
size="sm"
|
|
122
|
+
className="w-fit mt-4"
|
|
123
|
+
/>
|
|
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
|
+
)}
|
|
137
|
+
|
|
138
|
+
<SwapInputV3
|
|
139
|
+
value={selection.amount ?? ""}
|
|
140
|
+
balances={balances}
|
|
141
|
+
selectedTokenAddress={selection.selectedTokenAddress}
|
|
142
|
+
disabled={isInitialBalancesLoading}
|
|
143
|
+
isWalletConnected={!!address}
|
|
144
|
+
onChange={selection.setAmount}
|
|
145
|
+
onMaxClick={selection.handleMaxClick}
|
|
146
|
+
onTokenChange={selection.setSelectedTokenAddress}
|
|
147
|
+
showBalance={true}
|
|
148
|
+
className={cn(
|
|
149
|
+
"border border-border p-5",
|
|
150
|
+
validation.hasInsufficientBalance && "border-destructive/50",
|
|
151
|
+
)}
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
{depositMode === "route" && (
|
|
155
|
+
<SlippageSelector value={slippage} onChange={setSlippage} className="px-2" />
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{deposit.metadata && (
|
|
159
|
+
<RouteDetailsV2
|
|
160
|
+
metadata={deposit.metadata}
|
|
161
|
+
showApprove={deposit.hasApprove}
|
|
162
|
+
approveAmount={selection.amount}
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{deposit.error && <p className="text-destructive text-sm">{deposit.error.message}</p>}
|
|
167
|
+
|
|
168
|
+
<GeoCheckBlocker>
|
|
169
|
+
<Button
|
|
170
|
+
onClick={handleButtonClick}
|
|
171
|
+
disabled={!validation.canDeposit && !validation.isWrongChain}
|
|
172
|
+
className="w-full"
|
|
173
|
+
>
|
|
174
|
+
{validation.buttonText}
|
|
175
|
+
</Button>
|
|
176
|
+
</GeoCheckBlocker>
|
|
177
|
+
|
|
178
|
+
{validation.validationMessage && validation.hasInsufficientBalance && (
|
|
179
|
+
<p className="text-destructive text-xs">{validation.validationMessage}</p>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{/* {(validation.depositFee !== null ||
|
|
183
|
+
validation.performanceFee !== null ||
|
|
184
|
+
validation.withdrawalCooldownSecs !== null) && (
|
|
185
|
+
<div className="text-muted-foreground text-xs space-y-1">
|
|
186
|
+
{validation.depositFee !== null && validation.depositFee > 0 && (
|
|
187
|
+
<p>Deposit fee: {validation.depositFee}%</p>
|
|
188
|
+
)}
|
|
189
|
+
{validation.performanceFee !== null && validation.performanceFee > 0 && (
|
|
190
|
+
<p>Performance fee: {validation.performanceFee}%</p>
|
|
191
|
+
)}
|
|
192
|
+
{validation.withdrawalCooldownSecs !== null && validation.withdrawalCooldownSecs > 0 && (
|
|
193
|
+
<p>Withdrawal cooldown: {Math.floor(validation.withdrawalCooldownSecs / SECONDS_IN_DAY)} days</p>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
)} */}
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
useGetOnChainBalance,
|
|
6
|
+
useBalance,
|
|
7
|
+
filterExcludedTokens,
|
|
8
|
+
type Opportunity,
|
|
9
|
+
type TokenBalance,
|
|
10
|
+
type TransactionRequest,
|
|
11
|
+
} from "@turtleclub/hooks";
|
|
12
|
+
import { NativeDepositSection } from "./NativeDepositSection";
|
|
13
|
+
|
|
14
|
+
export type DepositMode = "native" | "route";
|
|
15
|
+
|
|
16
|
+
export interface TemporalWrapperProps {
|
|
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
|
+
/** Initial deposit mode */
|
|
27
|
+
initialDepositMode?: DepositMode;
|
|
28
|
+
/** Whether to show the deposit mode selector (native/swap) */
|
|
29
|
+
showDepositModeSelector?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
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
|
+
/**
|
|
43
|
+
* TemporalWrapper - A standalone wrapper for NativeDepositSection
|
|
44
|
+
*
|
|
45
|
+
* Includes all balance fetching logic and deposit mode management.
|
|
46
|
+
* Use this when you only need deposit functionality without withdraw.
|
|
47
|
+
*/
|
|
48
|
+
export function TemporalWrapper({
|
|
49
|
+
opportunity,
|
|
50
|
+
address,
|
|
51
|
+
distributorId,
|
|
52
|
+
executeTransaction,
|
|
53
|
+
onDepositSuccess,
|
|
54
|
+
chainId,
|
|
55
|
+
switchChain,
|
|
56
|
+
initialDepositMode = "native",
|
|
57
|
+
showDepositModeSelector = true,
|
|
58
|
+
}: TemporalWrapperProps) {
|
|
59
|
+
const [depositMode, setDepositMode] = useState<DepositMode>(initialDepositMode);
|
|
60
|
+
|
|
61
|
+
const opportunityChainId = Number(opportunity.receiptToken.chain.chainId);
|
|
62
|
+
|
|
63
|
+
// Use on-chain balances for native mode, portfolio for route mode
|
|
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({
|
|
84
|
+
address,
|
|
85
|
+
chainIds: [opportunityChainId],
|
|
86
|
+
depositOpportunity: opportunity,
|
|
87
|
+
});
|
|
88
|
+
|
|
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
|
+
// Determine if we should show the mode selector
|
|
135
|
+
const onDepositModeChange = showDepositModeSelector && opportunity.earnEnabled
|
|
136
|
+
? setDepositMode
|
|
137
|
+
: undefined;
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<NativeDepositSection
|
|
141
|
+
opportunity={opportunity}
|
|
142
|
+
address={address}
|
|
143
|
+
distributorId={distributorId}
|
|
144
|
+
balances={balances}
|
|
145
|
+
isBalancesLoading={isBalancesLoading}
|
|
146
|
+
refetchBalances={refetchBalances}
|
|
147
|
+
executeTransaction={executeTransaction}
|
|
148
|
+
onDepositSuccess={onDepositSuccess}
|
|
149
|
+
depositMode={depositMode}
|
|
150
|
+
onDepositModeChange={onDepositModeChange}
|
|
151
|
+
chainId={chainId}
|
|
152
|
+
switchChain={switchChain}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import { useMemo } from "react";
|
|
2
4
|
import type { ColumnDef } from "@tanstack/react-table";
|
|
3
5
|
import { DataTable } from "@turtleclub/ui";
|
|
@@ -9,6 +11,8 @@ export interface BalancesDataTableProps {
|
|
|
9
11
|
isLoading?: boolean;
|
|
10
12
|
onBalanceSelect?: (balance: TokenBalance) => void;
|
|
11
13
|
emptyStateMessage?: string;
|
|
14
|
+
/** ScrollArea height - passed to DataTable size prop */
|
|
15
|
+
size?: string;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
export function BalancesDataTable({
|
|
@@ -16,6 +20,7 @@ export function BalancesDataTable({
|
|
|
16
20
|
isLoading = false,
|
|
17
21
|
onBalanceSelect,
|
|
18
22
|
emptyStateMessage = "No balances found",
|
|
23
|
+
size,
|
|
19
24
|
}: BalancesDataTableProps) {
|
|
20
25
|
const columns = useMemo<ColumnDef<TokenBalance>[]>(
|
|
21
26
|
() => [
|
|
@@ -80,6 +85,7 @@ export function BalancesDataTable({
|
|
|
80
85
|
onRowClick={onBalanceSelect}
|
|
81
86
|
emptyState={emptyState}
|
|
82
87
|
className="w-full"
|
|
88
|
+
size={size}
|
|
83
89
|
/>
|
|
84
90
|
);
|
|
85
91
|
}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
export { BalancesDataTable, type BalancesDataTableProps } from "./balances-data-table";
|
|
1
2
|
export { ConfirmButton } from "./confirm-button";
|
|
2
3
|
export { GeoCheckBlocker } from "./geo-check-blocker";
|
|
4
|
+
export { SwapInputV3, type SwapInputV3Props } from "./swap-input-v3";
|
|
5
|
+
export { TokenSelectorV3 } from "./token-selector-v3";
|