@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
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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.58](https://github.com/turtle-dao/turtle-tools/compare/@turtleclub/opportunities@0.1.0-beta.57...@turtleclub/opportunities@0.1.0-beta.58) (2026-01-23)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- native deposit section ([#231](https://github.com/turtle-dao/turtle-tools/issues/231)) ([b05278e](https://github.com/turtle-dao/turtle-tools/commit/b05278ee2eb7b77429e50140e08ca00eb6d2745d))
|
|
11
|
+
|
|
12
|
+
# [0.1.0-beta.57](https://github.com/turtle-dao/turtle-tools/compare/@turtleclub/opportunities@0.1.0-beta.56...@turtleclub/opportunities@0.1.0-beta.57) (2026-01-21)
|
|
13
|
+
|
|
14
|
+
**Note:** Version bump only for package @turtleclub/opportunities
|
|
15
|
+
|
|
6
16
|
# [0.1.0-beta.56](https://github.com/turtle-dao/turtle-tools/compare/@turtleclub/opportunities@0.1.0-beta.55...@turtleclub/opportunities@0.1.0-beta.56) (2026-01-20)
|
|
7
17
|
|
|
8
18
|
**Note:** Version bump only for package @turtleclub/opportunities
|
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# @turtleclub/opportunities
|
|
2
|
+
|
|
3
|
+
React components for interacting with Turtle Club opportunities, including deposits, withdrawals, and transaction status tracking.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @turtleclub/opportunities
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Components
|
|
12
|
+
|
|
13
|
+
### NativeDepositSection
|
|
14
|
+
|
|
15
|
+
A complete deposit interface component for opportunities that handles token selection, balance fetching, and deposit execution.
|
|
16
|
+
|
|
17
|
+
#### Props
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
interface NativeDepositSectionProps {
|
|
21
|
+
// Required
|
|
22
|
+
opportunity: Opportunity; // The opportunity to deposit into
|
|
23
|
+
address: string | undefined; // User's wallet address
|
|
24
|
+
distributorId: string; // Distributor ID for attribution
|
|
25
|
+
executeTransaction: (tx: TransactionRequest) => Promise<string | undefined>;
|
|
26
|
+
|
|
27
|
+
// Optional
|
|
28
|
+
onDepositSuccess?: () => void; // Callback after successful deposit
|
|
29
|
+
tokenDisplayMode?: 'deposit-tokens' | 'all-chain-tokens'; // Default: 'deposit-tokens'
|
|
30
|
+
showZeroBalances?: boolean; // Show tokens with 0 balance. Default: false
|
|
31
|
+
showExcludedTokens?: boolean; // Show excluded/blacklisted tokens. Default: false
|
|
32
|
+
renderAssetSelector?: (props: AssetSelectorProps) => ReactNode; // Custom token selector
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
#### Token Display Modes
|
|
37
|
+
|
|
38
|
+
- **`deposit-tokens`** (default): Only shows tokens from `opportunity.depositTokens`
|
|
39
|
+
- **`all-chain-tokens`**: Shows all tokens on the chain that the user has balance for
|
|
40
|
+
|
|
41
|
+
#### Basic Usage
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { NativeDepositSection } from '@turtleclub/opportunities';
|
|
45
|
+
import { useMultichainSendTransaction } from '@turtleclub/multichain';
|
|
46
|
+
|
|
47
|
+
function DepositPage({ opportunity, address }) {
|
|
48
|
+
const { sendTransaction, waitForReceipt } = useMultichainSendTransaction();
|
|
49
|
+
|
|
50
|
+
const executeTransaction = async (tx) => {
|
|
51
|
+
const hash = await sendTransaction({
|
|
52
|
+
to: tx.to,
|
|
53
|
+
data: tx.data,
|
|
54
|
+
value: tx.value ? BigInt(tx.value) : undefined,
|
|
55
|
+
});
|
|
56
|
+
if (hash) await waitForReceipt(hash);
|
|
57
|
+
return hash;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<NativeDepositSection
|
|
62
|
+
opportunity={opportunity}
|
|
63
|
+
address={address}
|
|
64
|
+
distributorId="your-distributor-id"
|
|
65
|
+
executeTransaction={executeTransaction}
|
|
66
|
+
onDepositSuccess={() => console.log('Deposit completed!')}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### With Custom Asset Selector
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { NativeDepositSection, type AssetSelectorProps } from '@turtleclub/opportunities';
|
|
76
|
+
|
|
77
|
+
function CustomAssetSelector({ tokens, selectedToken, onTokenChange, isLoading }: AssetSelectorProps) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="custom-selector">
|
|
80
|
+
{tokens.map((token) => (
|
|
81
|
+
<button
|
|
82
|
+
key={token.address}
|
|
83
|
+
onClick={() => onTokenChange(token.address)}
|
|
84
|
+
className={selectedToken === token.address ? 'selected' : ''}
|
|
85
|
+
>
|
|
86
|
+
{token.symbol} - {token.balance}
|
|
87
|
+
</button>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function DepositPage({ opportunity, address }) {
|
|
94
|
+
return (
|
|
95
|
+
<NativeDepositSection
|
|
96
|
+
opportunity={opportunity}
|
|
97
|
+
address={address}
|
|
98
|
+
distributorId="your-distributor-id"
|
|
99
|
+
executeTransaction={executeTransaction}
|
|
100
|
+
renderAssetSelector={(props) => <CustomAssetSelector {...props} />}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Show All Chain Tokens
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
<NativeDepositSection
|
|
110
|
+
opportunity={opportunity}
|
|
111
|
+
address={address}
|
|
112
|
+
distributorId="your-distributor-id"
|
|
113
|
+
executeTransaction={executeTransaction}
|
|
114
|
+
tokenDisplayMode="all-chain-tokens"
|
|
115
|
+
showZeroBalances={true}
|
|
116
|
+
/>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Features
|
|
120
|
+
|
|
121
|
+
- **Auto-selection**: Automatically selects the token with the highest balance
|
|
122
|
+
- **Native token support**: Handles both ERC20 and native tokens (ETH, MATIC, etc.)
|
|
123
|
+
- **USD value display**: Shows real-time USD equivalent of the amount
|
|
124
|
+
- **Insufficient balance detection**: Validates balance before allowing deposit
|
|
125
|
+
- **Max button**: Quick-fill maximum available balance
|
|
126
|
+
- **Geo-blocking**: Integrates with GeoCheckBlocker for geographic restrictions
|
|
127
|
+
- **Loading states**: Shows loading indicators while fetching balances
|
|
128
|
+
|
|
129
|
+
### Dependencies
|
|
130
|
+
|
|
131
|
+
This component uses the following hooks from `@turtleclub/hooks`:
|
|
132
|
+
|
|
133
|
+
- `useGetOnChainBalance` - Fetches on-chain token balances
|
|
134
|
+
- `useBalance` - Fetches consolidated balances from multiple sources
|
|
135
|
+
- `useTokenBalance` - Calculates USD value and validates amounts
|
|
136
|
+
- `useEarnDeposit` - Handles the deposit transaction flow
|
|
137
|
+
|
|
138
|
+
## Other Components
|
|
139
|
+
|
|
140
|
+
### GeoCheckBlocker
|
|
141
|
+
|
|
142
|
+
Wraps content that should be blocked in certain geographic regions.
|
|
143
|
+
|
|
144
|
+
### ConfirmButton
|
|
145
|
+
|
|
146
|
+
Button component with confirmation state for critical actions.
|
|
147
|
+
|
|
148
|
+
## Types
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// Re-exported from @turtleclub/hooks
|
|
152
|
+
export type { Opportunity, TransactionRequest } from '@turtleclub/hooks';
|
|
153
|
+
|
|
154
|
+
// Component-specific types
|
|
155
|
+
export interface AssetSelectorProps {
|
|
156
|
+
tokens: SwapInputToken[];
|
|
157
|
+
selectedToken: string | undefined;
|
|
158
|
+
onTokenChange: (address: string) => void;
|
|
159
|
+
isLoading: boolean;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Testing
|
|
164
|
+
|
|
165
|
+
The showcase app includes a testing page at `/actions` where you can:
|
|
166
|
+
|
|
167
|
+
1. Connect a wallet (EVM, Solana, or TON)
|
|
168
|
+
2. Test NativeDepositSection with different configurations
|
|
169
|
+
3. Toggle between token display modes
|
|
170
|
+
4. Test with zero balance visibility
|
|
171
|
+
|
|
172
|
+
Run the showcase:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
cd apps/showcase
|
|
176
|
+
pnpm dev
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Navigate to `http://localhost:3000/actions` to test the component.
|
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.58",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
"@nexusmutual/sdk": "^1.26.0",
|
|
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.42",
|
|
16
17
|
"@turtleclub/multichain": "0.5.0-beta.0",
|
|
17
|
-
"@turtleclub/ui": "0.7.0-beta.
|
|
18
|
+
"@turtleclub/ui": "0.7.0-beta.22",
|
|
18
19
|
"@turtleclub/utils": "0.4.0-beta.0",
|
|
19
20
|
"jotai": "^2.10.3",
|
|
20
21
|
"lucide-react": "^0.542.0",
|
|
@@ -30,5 +31,5 @@
|
|
|
30
31
|
"@types/react-dom": "^18.3.5",
|
|
31
32
|
"typescript": "^5.7.2"
|
|
32
33
|
},
|
|
33
|
-
"gitHead": "
|
|
34
|
+
"gitHead": "dfb8123923c249371734e5168b024e81615ea12c"
|
|
34
35
|
}
|
package/src/components/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
// No exports - BalancesDataTable is now exported from ./deposit/components
|
|
@@ -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";
|