@turtleclub/opportunities 0.1.0-beta.57 → 0.1.0-beta.59

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 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.59](https://github.com/turtle-dao/turtle-tools/compare/@turtleclub/opportunities@0.1.0-beta.58...@turtleclub/opportunities@0.1.0-beta.59) (2026-01-23)
7
+
8
+ **Note:** Version bump only for package @turtleclub/opportunities
9
+
10
+ # [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)
11
+
12
+ ### Features
13
+
14
+ - native deposit section ([#231](https://github.com/turtle-dao/turtle-tools/issues/231)) ([b05278e](https://github.com/turtle-dao/turtle-tools/commit/b05278ee2eb7b77429e50140e08ca00eb6d2745d))
15
+
6
16
  # [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)
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.57",
3
+ "version": "0.1.0-beta.59",
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
- "@turtleclub/hooks": "0.5.0-beta.41",
15
+ "@tanstack/react-table": "^8.21.3",
16
+ "@turtleclub/hooks": "0.5.0-beta.43",
16
17
  "@turtleclub/multichain": "0.5.0-beta.0",
17
- "@turtleclub/ui": "0.7.0-beta.21",
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": "4752e7b695cd8733bc41b21a79ffaa33b82aa7a0"
34
+ "gitHead": "11cf816a868a361c6745919cee9883c5dbedb788"
34
35
  }
@@ -1 +1 @@
1
- export * from "./balances-data-table";
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";