@turtleclub/opportunities 0.1.0-beta.6 → 0.1.0-beta.61

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/README.md +179 -0
  3. package/package.json +13 -6
  4. package/src/components/index.ts +1 -1
  5. package/src/cover-offer/README.md +46 -0
  6. package/src/cover-offer/components/CoverOfferCard.tsx +445 -0
  7. package/src/cover-offer/components/CoverRequestForm.tsx +342 -0
  8. package/src/cover-offer/components/CoveredEventsInfo.tsx +120 -0
  9. package/src/cover-offer/components/ExistingCoverInfo.tsx +74 -0
  10. package/src/cover-offer/components/NexusCoverSection.tsx +49 -0
  11. package/src/cover-offer/components/PurchaseButtonSection.tsx +106 -0
  12. package/src/cover-offer/constants.ts +32 -0
  13. package/src/cover-offer/hooks/useCheckNexusMembership.ts +32 -0
  14. package/src/cover-offer/hooks/useCoverQuote.ts +126 -0
  15. package/src/cover-offer/hooks/useCoverTokenSelection.ts +84 -0
  16. package/src/cover-offer/hooks/useDebouncedValue.ts +12 -0
  17. package/src/cover-offer/hooks/useExistingCovers.ts +101 -0
  18. package/src/cover-offer/hooks/useNexusProduct.ts +79 -0
  19. package/src/cover-offer/hooks/useNexusPurchase.ts +67 -0
  20. package/src/cover-offer/hooks/useTokenApproval.ts +118 -0
  21. package/src/cover-offer/hooks/useUserCoverNfts.ts +26 -0
  22. package/src/cover-offer/index.ts +4 -0
  23. package/src/cover-offer/types/index.ts +41 -0
  24. package/src/cover-offer/utils/index.ts +90 -0
  25. package/src/deposit/NativeDepositSection.tsx +220 -0
  26. package/src/deposit/TemporalWrapper.tsx +82 -0
  27. package/src/{components → deposit/components}/balances-data-table.tsx +6 -0
  28. package/src/deposit/components/index.ts +3 -0
  29. package/src/deposit/components/swap-input-v3.tsx +194 -0
  30. package/src/deposit/components/token-selector-v3.tsx +122 -0
  31. package/src/deposit/index.ts +4 -0
  32. package/src/index.ts +9 -0
  33. package/src/opportunity-actions/OpportunityActions.tsx +191 -0
  34. package/src/opportunity-actions/index.ts +1 -0
  35. package/src/opportunity-table/components/opportunities-table.tsx +6 -5
  36. package/src/route-details/index.ts +6 -0
  37. package/src/route-details/route-details-v2.tsx +137 -0
  38. package/src/route-details/route-details.tsx +5 -4
  39. package/src/route-details/types.ts +7 -0
  40. package/src/transaction-status/hooks/useTransactionQueue.ts +1 -1
  41. package/src/withdraw/NativeWithdrawSection.tsx +45 -0
  42. package/src/withdraw/index.ts +1 -0
@@ -0,0 +1,122 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useState, useEffect, useMemo } from "react";
4
+ import { ChevronDownIcon } from "lucide-react";
5
+ import {
6
+ cn,
7
+ Dialog,
8
+ DialogContent,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from "@turtleclub/ui";
12
+ import type { TokenBalance } from "@turtleclub/hooks";
13
+ import { BalancesDataTable } from "./balances-data-table";
14
+
15
+ interface TokenSelectorV3Props {
16
+ balances: TokenBalance[];
17
+ value?: string;
18
+ onValueChange?: (value: string) => void;
19
+ placeholder?: string;
20
+ disabled?: boolean;
21
+ className?: string;
22
+ size?: "xs" | "sm" | "default";
23
+ }
24
+
25
+ /**
26
+ * TokenSelectorV3 - Opens a dialog with BalancesDataTable for selection
27
+ */
28
+ const TokenSelectorV3 = ({
29
+ balances,
30
+ value,
31
+ onValueChange,
32
+ placeholder = "Select token",
33
+ disabled,
34
+ className,
35
+ size = "default",
36
+ }: TokenSelectorV3Props) => {
37
+ const [open, setOpen] = useState(false);
38
+
39
+ // Auto-select first token if no value is provided and balances exist
40
+ const effectiveValue =
41
+ value || (balances.length > 0 ? balances[0].token.address : undefined);
42
+
43
+ // Find the selected balance
44
+ const selectedBalance = useMemo(
45
+ () => balances.find((b) => b.token.address === effectiveValue),
46
+ [balances, effectiveValue]
47
+ );
48
+
49
+ // Auto-select first token on mount
50
+ useEffect(() => {
51
+ if (!value && balances.length > 0 && onValueChange) {
52
+ onValueChange(balances[0].token.address);
53
+ }
54
+ }, [value, balances, onValueChange]);
55
+
56
+ const handleSelect = (balance: TokenBalance) => {
57
+ onValueChange?.(balance.token.address);
58
+ setOpen(false);
59
+ };
60
+
61
+ return (
62
+ <>
63
+ {/* Trigger Button */}
64
+ <button
65
+ type="button"
66
+ onClick={() => !disabled && setOpen(true)}
67
+ disabled={disabled}
68
+ className={cn(
69
+ "bg-muted hover:bg-muted/80 flex items-center gap-1.5 rounded-full px-3 py-2 font-medium transition-colors",
70
+ "focus:ring-primary/20 focus:ring-2 focus:outline-none",
71
+ "disabled:cursor-not-allowed disabled:opacity-50",
72
+ size === "xs" && "px-2 py-1 text-xs",
73
+ size === "sm" && "px-2.5 py-1.5 text-sm",
74
+ size === "default" && "px-3 py-2 text-sm",
75
+ className
76
+ )}
77
+ >
78
+ {selectedBalance ? (
79
+ <>
80
+ {selectedBalance.token.logoUrl && (
81
+ <img
82
+ src={selectedBalance.token.logoUrl}
83
+ alt={selectedBalance.token.symbol}
84
+ className={cn(
85
+ "rounded-full",
86
+ size === "xs" && "size-3",
87
+ size === "sm" && "size-4",
88
+ size === "default" && "size-4"
89
+ )}
90
+ />
91
+ )}
92
+ <span>{selectedBalance.token.symbol}</span>
93
+ </>
94
+ ) : (
95
+ <span className="text-muted-foreground">{placeholder}</span>
96
+ )}
97
+ <ChevronDownIcon className="size-4 opacity-50" />
98
+ </button>
99
+
100
+ {/* Token Selection Dialog */}
101
+ <Dialog open={open} onOpenChange={setOpen}>
102
+ <DialogContent className="max-w-md p-0">
103
+ <DialogHeader className="px-4 pt-4">
104
+ <DialogTitle>Select Token</DialogTitle>
105
+ </DialogHeader>
106
+
107
+ {/* BalancesDataTable with native scroll */}
108
+ <div className="px-4 pb-4">
109
+ <BalancesDataTable
110
+ balances={balances}
111
+ onBalanceSelect={handleSelect}
112
+ emptyStateMessage="No tokens available"
113
+ size="450px"
114
+ />
115
+ </div>
116
+ </DialogContent>
117
+ </Dialog>
118
+ </>
119
+ );
120
+ };
121
+
122
+ export { TokenSelectorV3 };
@@ -1 +1,5 @@
1
1
  export * from "./components";
2
+ export { NativeDepositSection } from "./NativeDepositSection";
3
+ export type { NativeDepositSectionProps } from "./NativeDepositSection";
4
+ export { TemporalWrapper } from "./TemporalWrapper";
5
+ export type { TemporalWrapperProps, DepositMode } from "./TemporalWrapper";
package/src/index.ts CHANGED
@@ -4,9 +4,18 @@ export * from "./components";
4
4
  // Deposit Components
5
5
  export * from "./deposit";
6
6
 
7
+ // Withdraw Components
8
+ export * from "./withdraw";
9
+
10
+ // Opportunity Actions
11
+ export * from "./opportunity-actions";
12
+
7
13
  // Transaction Status
8
14
  export * from "./transaction-status";
9
15
 
16
+ // Cover Offer
17
+ export * from "./cover-offer";
18
+
10
19
  // Opportunity Table
11
20
  export * from "./opportunity-table/components";
12
21
 
@@ -0,0 +1,191 @@
1
+ "use client";
2
+
3
+ import { useState, useMemo } from "react";
4
+ import { SegmentControl, TurtleTooltip } from "@turtleclub/ui";
5
+ import {
6
+ useGetOnChainBalance,
7
+ useBalance,
8
+ filterExcludedTokens,
9
+ type Opportunity,
10
+ type TokenBalance,
11
+ type TransactionRequest,
12
+ } from "@turtleclub/hooks";
13
+ import { NativeDepositSection } from "../deposit/NativeDepositSection";
14
+ import { NativeWithdrawSection } from "../withdraw/NativeWithdrawSection";
15
+
16
+ export interface OpportunityActionsProps {
17
+ opportunity: Opportunity;
18
+ address: string | undefined;
19
+ distributorId: string;
20
+ /**
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>;
26
+ onDepositSuccess?: () => void;
27
+ /** Current wallet chain ID */
28
+ chainId?: number;
29
+ /** Function to switch chain */
30
+ switchChain?: (chainId: number) => Promise<void>;
31
+ }
32
+
33
+ function sortByBalance(balances: TokenBalance[]): TokenBalance[] {
34
+ return [...balances].sort((a, b) => {
35
+ const aValue = BigInt(a.amount);
36
+ const bValue = BigInt(b.amount);
37
+ if (bValue > aValue) return 1;
38
+ if (bValue < aValue) return -1;
39
+ return 0;
40
+ });
41
+ }
42
+
43
+ export function OpportunityActions({
44
+ opportunity,
45
+ address,
46
+ distributorId,
47
+ executeTransactionAndWait,
48
+ onDepositSuccess,
49
+ chainId,
50
+ switchChain,
51
+ }: OpportunityActionsProps) {
52
+ const [actionMode, setActionMode] = useState<"deposit" | "withdraw">("deposit");
53
+ const [depositMode, setDepositMode] = useState<"native" | "route">("native");
54
+
55
+ const isSecondaryOnly = opportunity.vaultConfig?.secondaryOnly === true;
56
+
57
+ // For secondary market, always use swap mode
58
+ const effectiveDepositMode = isSecondaryOnly ? "route" : depositMode;
59
+ const opportunityChainId = Number(opportunity.receiptToken.chain.chainId)
60
+
61
+ // Use on-chain balances when: direct deposit OR withdraw
62
+ const useOnChainBalances = effectiveDepositMode === "native" || actionMode === "withdraw";
63
+
64
+ // Always fetch both balance sources to avoid loading states when switching modes
65
+ const {
66
+ balances: depositTokenBalances,
67
+ isLoading: isDepositBalancesLoading,
68
+ refetch: refetchDepositBalances,
69
+ } = useGetOnChainBalance({
70
+ tokens: opportunity.depositTokens,
71
+ chainId: opportunityChainId,
72
+ address,
73
+ enabled: !!address,
74
+ });
75
+
76
+ const {
77
+ balances: allChainBalances,
78
+ isLoading: isAllChainBalancesLoading,
79
+ refetchAll: refetchAllBalances,
80
+ } = useBalance({
81
+ address,
82
+ chainIds: [opportunityChainId],
83
+ depositOpportunity: opportunity,
84
+ });
85
+
86
+ const rawBalances = useMemo(() => {
87
+ if (useOnChainBalances) {
88
+ if (depositTokenBalances.length > 0) {
89
+ return depositTokenBalances;
90
+ }
91
+ // Fallback: create TokenBalance entries from deposit tokens with 0 balance
92
+ return opportunity.depositTokens.map((token) => ({
93
+ token,
94
+ amount: "0",
95
+ source: "onchain" as const,
96
+ }));
97
+ }
98
+ return allChainBalances;
99
+ }, [useOnChainBalances, depositTokenBalances, allChainBalances, opportunity.depositTokens]);
100
+
101
+ const isBalancesLoading = useOnChainBalances
102
+ ? isDepositBalancesLoading
103
+ : isAllChainBalancesLoading;
104
+
105
+ const refetchBalances = useOnChainBalances
106
+ ? refetchDepositBalances
107
+ : refetchAllBalances;
108
+
109
+ const balances = useMemo(() => {
110
+ let filtered = rawBalances;
111
+
112
+ // Filter by opportunity chain
113
+ if (!useOnChainBalances && opportunityChainId) {
114
+ filtered = filtered.filter((b) => {
115
+ const tokenChainId = Number(b.token.chain?.chainId);
116
+ return tokenChainId === opportunityChainId;
117
+ });
118
+ }
119
+
120
+ // Filter zero balances (only for all-chain mode)
121
+ if (!useOnChainBalances) {
122
+ filtered = filtered.filter((b) => BigInt(b.amount) > 0n);
123
+ }
124
+
125
+ // Filter excluded tokens
126
+ filtered = filterExcludedTokens(filtered);
127
+
128
+ return sortByBalance(filtered);
129
+ }, [rawBalances, useOnChainBalances, opportunityChainId]);
130
+
131
+ return (
132
+ <div className="flex flex-col gap-3">
133
+ <SegmentControl
134
+ value={actionMode}
135
+ onChange={(value) => setActionMode(value as "deposit" | "withdraw")}
136
+ items={[
137
+ {
138
+ value: "deposit",
139
+ label: opportunity.depositDisabled ? (
140
+ <TurtleTooltip
141
+ trigger={<span>{isSecondaryOnly ? "Buy" : "Deposit"}</span>}
142
+ content={<span className="block p-2">{isSecondaryOnly ? "Purchases are" : "Deposits are"} disabled for this opportunity</span>}
143
+ />
144
+ ) : (
145
+ isSecondaryOnly ? "Buy" : "Deposit"
146
+ ),
147
+ disabled: opportunity.depositDisabled,
148
+ },
149
+ {
150
+ value: "withdraw",
151
+ label: opportunity.withdrawalDisabled ? (
152
+ <TurtleTooltip
153
+ trigger={<span>{isSecondaryOnly ? "Sell" : "Withdraw"}</span>}
154
+ content={<span className="block p-2">{isSecondaryOnly ? "Sales are" : "Withdrawals are"} disabled for this opportunity</span>}
155
+ />
156
+ ) : (
157
+ isSecondaryOnly ? "Sell" : "Withdraw"
158
+ ),
159
+ disabled: opportunity.withdrawalDisabled,
160
+ },
161
+ ]}
162
+ variant="segment"
163
+
164
+ />
165
+
166
+ {actionMode === "deposit" ? (
167
+ <NativeDepositSection
168
+ opportunity={opportunity}
169
+ address={address}
170
+ distributorId={distributorId}
171
+ balances={balances}
172
+ isBalancesLoading={isBalancesLoading}
173
+ refetchBalances={refetchBalances}
174
+ executeTransactionAndWait={executeTransactionAndWait}
175
+ onDepositSuccess={onDepositSuccess}
176
+ depositMode={effectiveDepositMode}
177
+ onDepositModeChange={opportunity.earnEnabled && !isSecondaryOnly ? setDepositMode : undefined}
178
+ chainId={chainId}
179
+ switchChain={switchChain}
180
+ />
181
+ ) : (
182
+ <NativeWithdrawSection
183
+ opportunity={opportunity}
184
+ address={address}
185
+ balances={balances}
186
+ isBalancesLoading={isBalancesLoading}
187
+ />
188
+ )}
189
+ </div>
190
+ );
191
+ }
@@ -0,0 +1 @@
1
+ export { OpportunityActions, type OpportunityActionsProps } from "./OpportunityActions";
@@ -1,6 +1,6 @@
1
1
  import type { Opportunity, TokenBalance } from "@turtleclub/hooks";
2
2
  import { APIStatus, DataTable } from "@turtleclub/ui";
3
- import type { ColumnDef } from "@tanstack/react-table";
3
+ import type { ColumnDef, Row } from "@tanstack/react-table";
4
4
  import { formatCurrency } from "@turtleclub/utils";
5
5
  import { getTotalYield } from "../hooks/useTotalYield";
6
6
  import { calculateNetAPR } from "../utils/calculateNetAPR";
@@ -145,7 +145,7 @@ export function OpportunitiesTable({
145
145
  },
146
146
  {
147
147
  id: "yourTvl",
148
- accessorKey: "yourTvl",
148
+ accessorFn: (row) => userTvlByOpportunity[row.id || ""] || 0,
149
149
  header: () => <div className="flex w-full justify-end">Your TVL</div>,
150
150
  cell: ({ row }) => {
151
151
  const yourTvl = userTvlByOpportunity[row.original.id || ""] || 0;
@@ -156,7 +156,7 @@ export function OpportunitiesTable({
156
156
  );
157
157
  },
158
158
  enableSorting: true,
159
- sortingFn: (rowA, rowB) => {
159
+ sortingFn: (rowA: Row<Opportunity>, rowB: Row<Opportunity>) => {
160
160
  const a = userTvlByOpportunity[rowA.original.id || ""] || 0;
161
161
  const b = userTvlByOpportunity[rowB.original.id || ""] || 0;
162
162
  return a - b;
@@ -180,7 +180,8 @@ export function OpportunitiesTable({
180
180
  enableSorting: true,
181
181
  },
182
182
  {
183
- accessorKey: "totalYield",
183
+ id: "totalYield",
184
+ accessorFn: (row) => calculateNetAPR(row.incentives, row.vaultConfig ?? undefined),
184
185
  header: () => <div className="flex w-full justify-end">Est. APR</div>,
185
186
  cell: ({ row }) => (
186
187
  <div className="flex flex-row-reverse">
@@ -192,7 +193,7 @@ export function OpportunitiesTable({
192
193
  </div>
193
194
  ),
194
195
  enableSorting: true,
195
- sortingFn: (rowA, rowB) => {
196
+ sortingFn: (rowA: Row<Opportunity>, rowB: Row<Opportunity>) => {
196
197
  const a = calculateNetAPR(rowA.original.incentives, rowA.original.vaultConfig ?? undefined);
197
198
  const b = calculateNetAPR(rowB.original.incentives, rowB.original.vaultConfig ?? undefined);
198
199
  return a - b;
@@ -1,2 +1,8 @@
1
+ // V1 - Legacy (uses TokenStep from hooks)
1
2
  export { RouteDetails } from "./route-details";
2
3
  export type { TokenStep } from "./route-details";
4
+
5
+ // V2 - New API structure (uses RouteMetadata)
6
+ export { RouteDetailsV2 } from "./route-details-v2";
7
+ export type { RouteDetailsV2Props } from "./route-details-v2";
8
+ export type { RouteMetadata, RouteStep, RouteToken } from "./types";
@@ -0,0 +1,137 @@
1
+ import { Card, CardContent } from "@turtleclub/ui";
2
+ import { formatBalance } from "@turtleclub/utils";
3
+ import { formatUnits } from "viem";
4
+ import { ArrowRight } from "lucide-react";
5
+ import type { RouteMetadata, RouteStep } from "./types";
6
+
7
+ export interface RouteDetailsV2Props {
8
+ metadata: RouteMetadata;
9
+ showApprove?: boolean;
10
+ approveAmount?: string;
11
+ className?: string;
12
+ }
13
+
14
+ const MIN_FORMAT_THRESHOLD = 0.0001;
15
+ const SMALL_NUMBER_DECIMALS = 4;
16
+
17
+ function formatSmallNumber(val: string | number): string {
18
+ const num = typeof val === "string" ? parseFloat(val) : val;
19
+ if (isNaN(num) || num === 0) return "0";
20
+ if (num < MIN_FORMAT_THRESHOLD) return `<${MIN_FORMAT_THRESHOLD}`;
21
+ return formatBalance(num, SMALL_NUMBER_DECIMALS);
22
+ }
23
+
24
+ function TokenIcon({ token }: { token: RouteStep["from"] }) {
25
+ if (token.logoUrl) {
26
+ return (
27
+ <div className="flex h-7 w-7 max-h-7 max-w-7 shrink-0 items-center justify-center overflow-hidden rounded-full">
28
+ <img
29
+ src={token.logoUrl}
30
+ alt={token.symbol}
31
+ className="h-5 w-5 max-h-5 max-w-5 rounded-full object-cover"
32
+ />
33
+ </div>
34
+ );
35
+ }
36
+
37
+ return (
38
+ <div className="flex h-7 w-7 max-h-7 max-w-7 shrink-0 items-center justify-center rounded-full">
39
+ <div className="bg-neutral-alpha-10 flex h-5 w-5 items-center justify-center rounded-full">
40
+ <span className="text-muted-foreground text-[10px] font-medium">
41
+ {token.symbol?.charAt(0).toUpperCase() || "?"}
42
+ </span>
43
+ </div>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ function StepCard({
49
+ label,
50
+ fromToken,
51
+ toToken,
52
+ amount,
53
+ }: {
54
+ label: string;
55
+ fromToken: RouteStep["from"];
56
+ toToken?: RouteStep["to"];
57
+ amount?: string;
58
+ }) {
59
+ return (
60
+ <Card variant="border" className="h-[72px] min-w-[120px] flex-1">
61
+ <CardContent className="flex h-full flex-col justify-between px-5 py-2.5">
62
+ <span className="text-muted-foreground text-xs">{label}</span>
63
+ <div className="flex items-center gap-1.5">
64
+ <TokenIcon token={fromToken} />
65
+ <span className="text-sm font-medium">
66
+ {amount ? `${formatSmallNumber(amount)} ${fromToken.symbol}` : fromToken.symbol || "?"}
67
+ </span>
68
+ {toToken && (
69
+ <>
70
+ <ArrowRight className="text-muted-foreground size-3 shrink-0" />
71
+ <TokenIcon token={toToken} />
72
+ <span className="text-sm font-medium">{toToken.symbol || "?"}</span>
73
+ </>
74
+ )}
75
+ </div>
76
+ </CardContent>
77
+ </Card>
78
+ );
79
+ }
80
+
81
+ /** Displays route details for routed deposits with provider info and step cards */
82
+ export function RouteDetailsV2({ metadata, showApprove, approveAmount, className }: RouteDetailsV2Props) {
83
+ const { provider, providerImg, route, amountOut } = metadata;
84
+
85
+ if (!route || route.length === 0) {
86
+ return null;
87
+ }
88
+
89
+ const approveToken = route[0]?.from;
90
+ const outputToken = route[route.length - 1]?.to;
91
+
92
+ // Format amountOut using the output token's decimals
93
+ const formattedAmountOut =
94
+ amountOut && outputToken
95
+ ? formatSmallNumber(formatUnits(BigInt(amountOut), outputToken.decimals))
96
+ : null;
97
+
98
+ return (
99
+ <div className={className}>
100
+ <div className="mb-3 space-y-3 px-2 text-xs">
101
+ {formattedAmountOut && outputToken && (
102
+ <div className="flex items-center gap-1.5">
103
+ <span className="text-muted-foreground">You receive</span>
104
+ <span className="text-sm font-medium">
105
+ {formattedAmountOut} {outputToken.symbol}
106
+ </span>
107
+ </div>
108
+ )}
109
+ <div className="flex items-center ">
110
+ <span className="text-muted-foreground">Route powered by</span>
111
+ {providerImg ? (
112
+ <img src={providerImg} alt={provider} className="h-2 px-2" />
113
+ ) : (
114
+ <span className="text-sm font-medium capitalize">{provider}</span>
115
+ )}
116
+ </div>
117
+ </div>
118
+
119
+ <div className="flex flex-col gap-2 sm:flex-row sm:overflow-x-auto">
120
+ {showApprove && approveToken && (
121
+ <StepCard label="Approve" fromToken={approveToken} amount={approveAmount} />
122
+ )}
123
+ {route.map((step, index) => {
124
+ const label = step.action.charAt(0).toUpperCase() + step.action.slice(1);
125
+ return (
126
+ <StepCard
127
+ key={`${step.action}-${step.from.address}-${index}`}
128
+ label={label}
129
+ fromToken={step.from}
130
+ toToken={step.to}
131
+ />
132
+ );
133
+ })}
134
+ </div>
135
+ </div>
136
+ );
137
+ }
@@ -1,8 +1,9 @@
1
1
  import * as React from "react";
2
2
  import { Card, CardContent, LabelWithIcon, ScrollArea } from "@turtleclub/ui";
3
3
  import { ChevronsRight } from "lucide-react";
4
- import ensoLogo from "../images/enso.png";
5
4
  import { useMemo } from "react";
5
+
6
+ const ENSO_LOGO_URL = "https://storage.googleapis.com/turtle-assets/partners/enso/enso.png";
6
7
  import type { TokenStep } from "@turtleclub/hooks";
7
8
 
8
9
  export type { TokenStep } from "@turtleclub/hooks";
@@ -38,7 +39,7 @@ export const RouteDetails = ({ value, className }: RouteDetailsProps) => {
38
39
  <div className="text-sm font-medium">Swap Route</div>
39
40
  <div className="mb-1 flex items-center gap-1 text-[10px]">
40
41
  <span className="text-sm opacity-50">Powered by</span>
41
- <img src={ensoLogo} className="w-10" alt="Enso" />
42
+ <img src={ENSO_LOGO_URL} className="w-10" alt="Enso" />
42
43
  </div>
43
44
  {/* Route visualization */}
44
45
  <ScrollArea className="mt-2 rounded-md whitespace-nowrap">
@@ -69,7 +70,7 @@ export const RouteDetails = ({ value, className }: RouteDetailsProps) => {
69
70
  )}
70
71
  </LabelWithIcon>
71
72
 
72
- {step.type !== "approve" && (
73
+ {step.type !== "approve" && step.out && (
73
74
  <>
74
75
  {/* Arrow between tokens */}
75
76
  <ChevronsRight className="text-primary size-4 shrink-0" />
@@ -82,7 +83,7 @@ export const RouteDetails = ({ value, className }: RouteDetailsProps) => {
82
83
  >
83
84
  {!isLongRoute && (
84
85
  <span className="hidden gap-2 sm:block">
85
- {step.in.symbol}
86
+ {step.out.symbol}
86
87
  </span>
87
88
  )}
88
89
  </LabelWithIcon>
@@ -0,0 +1,7 @@
1
+ // Re-export types from hooks package (source of truth from Zod schemas)
2
+ // Using earn-actions types for route metadata display
3
+ export type {
4
+ ActionRouteToken as RouteToken,
5
+ ActionRouteStep as RouteStep,
6
+ RouteMetadata,
7
+ } from "@turtleclub/hooks";
@@ -32,7 +32,7 @@ interface UseTransactionQueueProps {
32
32
  earnRoute: EarnRouteResponse | null;
33
33
  userAddress?: string;
34
34
  chainId?: number;
35
- sendTransaction: (transaction: StepTx) => Promise<`0x${string}` | undefined>;
35
+ sendTransaction: (transaction: StepTx & { chainId?: number }) => Promise<`0x${string}` | undefined>;
36
36
  onError?: (error: Error) => void;
37
37
  onSuccess?: () => void;
38
38
  }
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@turtleclub/ui";
5
+ import type { Opportunity, TokenBalance } from "@turtleclub/hooks";
6
+ import { GeoCheckBlocker, SwapInputV3 } from "../deposit/components";
7
+
8
+ export interface NativeWithdrawSectionProps {
9
+ opportunity: Opportunity;
10
+ address: string | undefined;
11
+ balances: TokenBalance[];
12
+ isBalancesLoading?: boolean;
13
+ }
14
+
15
+ export function NativeWithdrawSection({
16
+ opportunity,
17
+ address,
18
+ balances,
19
+ isBalancesLoading = false,
20
+ }: NativeWithdrawSectionProps) {
21
+ const [amount, setAmount] = useState<string | undefined>();
22
+ const [selectedTokenAddress, setSelectedTokenAddress] = useState<string | undefined>();
23
+
24
+ return (
25
+ <div className="flex flex-col gap-3">
26
+ <SwapInputV3
27
+ value={amount ?? ""}
28
+ balances={balances}
29
+ selectedTokenAddress={selectedTokenAddress}
30
+ disabled={isBalancesLoading}
31
+ isWalletConnected={!!address}
32
+ onChange={setAmount}
33
+ onTokenChange={setSelectedTokenAddress}
34
+ showBalance={true}
35
+ className="border-border border p-5"
36
+ />
37
+
38
+ <GeoCheckBlocker>
39
+ <Button disabled className="w-full">
40
+ Withdraw (Coming Soon)
41
+ </Button>
42
+ </GeoCheckBlocker>
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1 @@
1
+ export { NativeWithdrawSection, type NativeWithdrawSectionProps } from "./NativeWithdrawSection";