diff --git a/playground/next-env.d.ts b/playground/next-env.d.ts index 40c3d680..4f11a03d 100644 --- a/playground/next-env.d.ts +++ b/playground/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/playground/src/app/disclaimer/page.tsx b/playground/src/app/disclaimer/page.tsx index 93ba5327..e1a49e1b 100644 --- a/playground/src/app/disclaimer/page.tsx +++ b/playground/src/app/disclaimer/page.tsx @@ -113,6 +113,43 @@ const DisclaimerPage: React.FC = () => { We may update this disclaimer from time to time. Continued use of GLAM GUI constitutes your acceptance of any changes. Please review this disclaimer periodically for updates.

+ +
+

12. Additional Information and Disclaimers

+ +

Nature of Information

+ + +

Informational Purposes

+

+ While GLAM GUI provides information about cryptoassets and related technologies, users should not rely solely on this information for making investment decisions. We encourage users to conduct their own research and seek professional advice when necessary. +

+ +

Performance Information

+

+ Any performance information or historical data presented on GLAM GUI must be considered in conjunction with applicable disclosures. Past performance is not indicative of future results. The cryptoasset market is highly volatile and unpredictable. +

+ +

Third-Party Content

+

+ GLAM GUI may include content from third-party sources. While we strive to provide accurate and up-to-date information, we do not endorse or guarantee the accuracy, completeness, or reliability of any third-party content. Users should exercise caution and critical thinking when interpreting such information. +

+ +

Regulatory Compliance

+

+ Users are responsible for ensuring their use of GLAM GUI complies with all applicable laws and regulations in their jurisdiction. Cryptoasset regulations vary by country and are subject to change. GLAM does not guarantee the legality of its services in all jurisdictions. +

+ +

Consultation Recommendation

+

+ Given the complex nature of cryptoassets and the potential risks involved, we strongly recommend that users consult with qualified professionals, including legal and tax advisors, before making any decisions based on the information provided by GLAM GUI. +

+
+ ); }; diff --git a/playground/src/app/products/[product]/page.tsx b/playground/src/app/products/[product]/page.tsx index 751a784f..84d51e00 100644 --- a/playground/src/app/products/[product]/page.tsx +++ b/playground/src/app/products/[product]/page.tsx @@ -2,11 +2,8 @@ import * as React from "react"; import { - CartesianGrid, + Cell, Label, - Line, - LineChart, - XAxis, Pie, PieChart, } from "recharts"; @@ -25,7 +22,7 @@ import { } from "@/components/ui/chart"; import { useGlam } from "@glam/anchor/react"; -import { useParams } from "next/navigation"; +import { redirect, useRouter, useParams } from "next/navigation"; import { useEffect, useMemo, useRef, useState } from "react"; import { PublicKey } from "@solana/web3.js"; import Sparkle from "@/utils/Sparkle"; @@ -35,151 +32,71 @@ import SparkleColorMatcher, { } from "@/utils/SparkleColorMatcher"; import TruncateAddress from "@/utils/TruncateAddress"; import PageContentWrapper from "@/components/PageContentWrapper"; -import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Button } from "@/components/ui/button"; -import { - CopyIcon, - DotsVerticalIcon, - DownloadIcon, -} from "@radix-ui/react-icons"; import { Separator } from "@/components/ui/separator"; // Adjust the import based on your setup import NumberFormatter from "@/utils/NumberFormatter"; import { ExplorerLink } from "@/components/ExplorerLink"; - -const holdersData = [ - { - holder: ( - - ), - shares: 689, - fill: "var(--color-hld0)", - }, - { - holder: ( - - ), - shares: 303, - fill: "var(--color-hld1)", - }, - { - holder: ( - - ), - shares: 130, - fill: "var(--color-hld2)", - }, - { - holder: ( - - ), - shares: 115, - fill: "var(--color-hld3)", - }, - { - holder: ( - - ), - shares: 112, - fill: "var(--color-hld4)", - }, -]; - -const headerConfig = { - views: { - label: "Overview", - }, - aum: { - label: "Assets Under Management", - color: "hsl(var(--chart-1))", - }, - nav: { - label: "NAV per Share", - color: "hsl(var(--chart-1))", - }, - mgmtFee: { - label: "Management Fee", - color: "hsl(var(--chart-1))", - }, -} satisfies ChartConfig; +import { Skeleton } from "@/components/ui/skeleton"; export default function ProductPage() { + const [clientReady, setClientReady] = useState(false); + const [holdersData, setHoldersData] = useState(null); + const [sparkleColor, setSparkleColor] = useState(""); // State for sparkle color + const [useDefaultColors, setUseDefaultColors] = useState(false); // State for default colors + const sparkleContainerRef = useRef(null); // Initialize the ref here + const [sparkleSize, setSparkleSize] = useState(50); // State for the size of the sparkle + const params = useParams(); + const router = useRouter(); const { product } = params; + const { allFunds } = useGlam(); + + const isAllFundsLoading = !allFunds; const publicKey = useMemo(() => { - if (!product) { - return; - } + if (!product) return null; try { return new PublicKey(product); } catch (e) { - console.log(`Invalid public key`, e); + console.log("Invalid public key", e); + return null; } }, [product]); - if (!publicKey) { - return
Error loading product
; - } - - const { allFunds } = useGlam(); const fund = useMemo(() => { - const fund = (allFunds || []).find((f) => f.idStr === product); - console.log("fundModel", fund); - return fund; - }, [allFunds]); + if (!allFunds || !publicKey) return null; + return allFunds.find((f) => f.idStr === product); + }, [allFunds, publicKey, product]); - const total = React.useMemo( - () => ({ - aum: 123456, - nav: 78.91, - mgmtFee: "1.00%", - }), - [] - ); + // Mark the client as ready once mounted (to prevent server-side rendering issues) + useEffect(() => { + setClientReady(true); + }, []); - let mintData = (fund?.shareClasses || []).map( - (shareClass: any, j: number) => ({ - mint: shareClass?.shareClassSymbol, - shares: - Number(shareClass?.shareClassSupply) / - 10 ** (shareClass?.shareClassDecimals || 0) || 0, - fill: `var(--color-sc${j})`, - }) - ); - const totalShares = mintData.reduce( - (acc: BigInt, cur: any) => acc + cur.shares, - 0 - ); - if (totalShares === 0) { - if (mintData.length > 0) { - mintData[0].shares = 1; - } else { - mintData = [{ mint: "", shares: 0, fill: "var(--color-sc0" }]; + // Redirect logic moved to an effect + useEffect(() => { + console.log("Effect running. ClientReady:", clientReady, "IsAllFundsLoading:", isAllFundsLoading); + console.log("PublicKey:", publicKey, "Fund:", fund, "AllFunds:", allFunds); + + if (clientReady && !isAllFundsLoading) { + if (!publicKey) { + console.log("Redirecting: Invalid public key"); + router.push("/"); + return; + } + if (allFunds && allFunds.length > 0 && !fund) { + console.log("Redirecting: Valid public key but no matching fund"); + router.push("/"); + } } - } - - const [sparkleColor, setSparkleColor] = useState(""); - const [useDefaultColors, setUseDefaultColors] = React.useState(false); - - const handleColorGenerated = (generatedColor: string) => { - setSparkleColor(generatedColor); - }; + }, [clientReady, publicKey, fund, isAllFundsLoading, router, allFunds]); + // Calculating color info based on sparkleColor (must be declared at the top level) const colorInfo: ColorInfo = useMemo(() => { - return getColorInfo(sparkleColor); + return getColorInfo(sparkleColor); // This hook runs whenever `sparkleColor` changes }, [sparkleColor]); - const sparkleContainerRef = useRef(null); - const [sparkleSize, setSparkleSize] = useState(50); // Default size - + // Chart colors based on useDefaultColors or colorInfo const chartColors = useMemo(() => { if (useDefaultColors) { return [ @@ -190,18 +107,15 @@ export default function ProductPage() { "212 97% 87%", ]; } - return colorInfo.colors; + return colorInfo.colors; // Use colors based on sparkleColor }, [useDefaultColors, colorInfo.colors]); - const mintConfig = useMemo( - () => ({ - shares: { label: "Shares" }, - sc0: { label: "Share Class 1", color: `hsl(${chartColors[0]})` }, - sc1: { label: "Share Class 2", color: `hsl(${chartColors[1]})` }, - sc2: { label: "Share Class 3", color: `hsl(${chartColors[2]})` }, - }), - [chartColors] - ); + const mintConfig = useMemo(() => ({ + shares: { label: "Shares" }, + sc0: { label: "Share Class 1", color: `hsl(${chartColors[0]})` }, + sc1: { label: "Share Class 2", color: `hsl(${chartColors[1]})` }, + sc2: { label: "Share Class 3", color: `hsl(${chartColors[2]})` }, + }), [chartColors]); const holdersConfig = useMemo( () => ({ @@ -210,7 +124,7 @@ export default function ProductPage() { hld1: { label: "Holder 2", color: `hsl(${chartColors[1]})` }, hld2: { label: "Holder 3", color: `hsl(${chartColors[2]})` }, hld3: { label: "Holder 4", color: `hsl(${chartColors[3]})` }, - hld4: { label: "Holder 5", color: `hsl(${chartColors[4]})` }, + hld4: { label: "Others", color: `hsl(${chartColors[4]})` }, // Changed label to "Others" }), [chartColors] ); @@ -218,8 +132,7 @@ export default function ProductPage() { useEffect(() => { const updateSparkleSize = () => { if (sparkleContainerRef.current) { - const { width, height } = - sparkleContainerRef.current.getBoundingClientRect(); + const { width, height } = sparkleContainerRef.current.getBoundingClientRect(); const minDimension = Math.min(width, height); setSparkleSize(Math.floor(minDimension)); } @@ -231,8 +144,329 @@ export default function ProductPage() { return () => window.removeEventListener("resize", updateSparkleSize); }, []); + if (!clientReady || isAllFundsLoading) { + return
Loading...
; + } + + if (!publicKey || (allFunds && allFunds.length > 0 && !fund)) { + router.push("/"); + return null; + } + + const handleColorGenerated = (generatedColor: string) => { + setSparkleColor(generatedColor); + }; + + interface ShareClass { + shareClassId: string; + shareClassSymbol: string; + shareClassDecimals: number; // Add this line to define 'shareClassDecimals' + } + + interface Fund { + shareClasses: ShareClass[]; + } + + interface HolderData { + mint: string; + holders: Array<{ holder: React.ReactNode; shares: number; fill: string }>; + totalHolders: number; + } + + const ChartComponent: React.FC<{ fund: Fund }> = ({ fund }) => { + const [isLoading, setIsLoading] = useState(true); + const [showSkeleton, setShowSkeleton] = useState(true); // **New State** + const [localHoldersData, setLocalHoldersData] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setIsLoading(true); + setError(null); + const updatedHoldersData = await updateHoldersData(fund); + setLocalHoldersData(updatedHoldersData); + } catch (err: any) { + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, [fund]); + + if (isLoading) { + return ; + } + + if (error) { + return
Error: {error}
; + } + + // Ensure we have data before rendering the chart + if (!localHoldersData || localHoldersData.length === 0 || !localHoldersData[0]) { + return
No holder data available
; + } + + const totalHolders = localHoldersData[0]?.totalHolders || 0; + + // Determine what to display + const displayTotalHolders = totalHolders > 0 ? totalHolders : 0; + + return ( + + + {totalHolders !== 0 && ( + } + /> + )} + + + + + ); + }; + + async function fetchHolderData(mint: string): Promise { + try { + console.log(`Fetching holder data for mint: ${mint}`); + const response = await fetch(`https://rpc.helius.xyz/?api-key=${process.env.NEXT_PUBLIC_HELIUS_API_KEY}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: '1', + method: 'getTokenAccounts', + params: { + mint: mint, + options: { + showZeroBalance: true, + }, + }, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (data.error) { + throw new Error(`API error: ${data.error.message}`); + } + + console.log('Fetched holder data:', data); + return data.result; + } catch (error) { + console.error('Error fetching holder data:', error); + return null; + } + } + + function processHolderData( + data: any, + decimals: number + ): { + holders: Array<{ holder: React.ReactNode; shares: number; fill: string }>; + totalHolders: number; + } { + if (!data || !Array.isArray(data.token_accounts)) { + console.error('Invalid data format:', data); + return { holders: [], totalHolders: 0 }; + } + + const accounts = data.token_accounts; + + // Extract holder addresses and share amounts, adjusting by decimals + const holdersArray = accounts + .reduce((acc: any[], account: any) => { + const amount = Number(account.amount) / 10 ** decimals; // Adjust shares + const address = account.owner; + if (amount > 0 && address) { + acc.push({ + holder: , + shares: amount, + fill: `var(--color-hld${acc.length % 5})`, // Dynamically assign colors + }); + } + return acc; + }, []); + + const totalHolders = holdersArray.length; + + holdersArray.sort((a: { shares: number }, b: { shares: number }) => b.shares - a.shares); + + let topHolders = holdersArray.slice(0, 4); + + const othersShares = holdersArray + .slice(4) + .reduce((sum: number, holder: { shares: number }) => sum + holder.shares, 0); + + if (othersShares > 0) { + topHolders.push({ + holder: 'Others', + shares: othersShares, + fill: `var(--color-hld4)`, + }); + } + + if (totalHolders === 0) { + topHolders = [ + { + holder: 'Others', + shares: 1, // To allow the PieChart to render + fill: `var(--color-hld0)`, + }, + ]; + } + + console.log('Processed holders:', topHolders); + + return { + holders: topHolders, + totalHolders: totalHolders, + }; + } + + + async function updateHoldersData(fund: Fund): Promise { + const holdersData = await Promise.all( + (fund.shareClasses || []).map(async (shareClass) => { + const mintAddress = shareClass.shareClassId; + + if (!mintAddress) { + console.error(`Mint address not found for share class: ${shareClass.shareClassSymbol}`); + return null; + } + + const holderData = await fetchHolderData(mintAddress); + if (!holderData) { + console.error(`Failed to fetch holder data for mint: ${mintAddress}`); + return null; + } + + const processedData = processHolderData(holderData, shareClass?.shareClassDecimals || 0); + + return { + mint: mintAddress, + holders: processedData.holders, + totalHolders: processedData.totalHolders, + }; + }) + ); + + console.log('Aggregated holders data:', holdersData); + + return holdersData.filter((item) => item !== null); + } + + let mintData = (fund?.shareClasses || []).map( + (shareClass: any, j: number) => ({ + mint: shareClass?.shareClassSymbol, + shares: + Number(shareClass?.shareClassSupply) / + 10 ** (shareClass?.shareClassDecimals || 0) || 0, + fill: `var(--color-sc${j})`, + }) + ); + + const totalShares = mintData.reduce( + (acc: number, cur: any) => acc + cur.shares, + 0 + ); + + let displayTotalShares = totalShares; + +// If totalShares is 0, set shares to 1 for the first share class to render the chart + if (totalShares === 0 && mintData.length > 0) { + mintData[0].shares = 1; + } + +// If totalShares is 0, set displayTotalShares to 0 + if (totalShares === 0) { + displayTotalShares = 0; + } + + // Determine if the supply is zero + const isZeroSupply = displayTotalShares === 0; + + // Updated Label for Shares PieChart + const SharesLabel = ({ viewBox }: any) => { + if (!viewBox || !("cx" in viewBox) || !("cy" in viewBox)) return null; + return displayTotalShares > 0 ? ( + + + {displayTotalShares.toLocaleString()} + + + Shares + + + ) : null; // Hide the label if displayTotalShares is 0 + }; + if (!fund) { - return; + redirect('/'); } return ( @@ -240,33 +474,34 @@ export default function ProductPage() {
{/* Top row */} - - + + - + + {fund.name} - + {fund.investmentObjective} - + NAV per Share - + {/* - + Assets Under Management - + {/* - +

Symbol

@@ -382,25 +617,35 @@ export default function ProductPage() {
- + Overview - Holdings - Details - Policies - Integrations - Access + + Holdings + + + Details + + + Policies + + + Integrations + + + Access +
- + - Holders + Supply - } - /> + {totalShares !== 0 && ( + } + /> + )} @@ -469,67 +713,15 @@ export default function ProductPage() { - - } - /> - - - + - + @@ -573,7 +765,7 @@ export default function ProductPage() { Share Class Asset
- {fund.shareClasses[0]?.shareClassLaunchDate} + {fund.shareClasses[0]?.shareClassCurrency}
@@ -588,13 +780,13 @@ export default function ProductPage() {
Lifecycle Stage
-
{fund.shareClasses[0]?.shareClassLifecycle}
+
{fund.shareClasses[0]?.shareClassLifecycle?.charAt(0).toUpperCase() + fund.shareClasses[0]?.shareClassLifecycle?.slice(1)}
- Investment Satus + Investment Status
-
{fund.shareClasses[0]?.investmentStatus}
+
{fund.shareClasses[0]?.investmentStatus?.charAt(0).toUpperCase() + fund.shareClasses[0]?.investmentStatus?.slice(1)}
@@ -620,12 +812,7 @@ export default function ProductPage() {
Distribution Policy
-
- { - fund.shareClasses[0] - ?.shareClassDistributionPolicy - } -
+
{fund.shareClasses[0]?.shareClassDistributionPolicy?.charAt(0).toUpperCase() + fund.shareClasses[0]?.shareClassDistributionPolicy?.slice(1)}
@@ -672,13 +859,29 @@ export default function ProductPage() { Openfunds + - (download) + XLSX + + CSV + + + JSON + + @@ -776,3 +979,18 @@ export default function ProductPage() { ); } + +// Skeleton component for the chart +const SkeletonChart = () => { + return ( +
+ +
+
+ + +
+
+
+ ); + }; diff --git a/playground/src/app/products/components/data-table.tsx b/playground/src/app/products/components/data-table.tsx index 26adbb0f..63b5317b 100644 --- a/playground/src/app/products/components/data-table.tsx +++ b/playground/src/app/products/components/data-table.tsx @@ -25,7 +25,6 @@ import { TableRow, } from "@/components/ui/table"; -import { DataTablePagination } from "./data-table-pagination"; import { DataTableToolbar } from "./data-table-toolbar"; import { columns as defaultColumns } from "./columns"; import { Product } from "../data/productSchema"; @@ -33,17 +32,17 @@ import { Product } from "../data/productSchema"; // Number of skeleton rows to display const SKELETON_ROW_COUNT = 10; -interface DataTableProps { +interface DataTableProps { columns?: ColumnDef[]; data: TData[]; isLoading: boolean; } -export function DataTable({ - columns = defaultColumns as ColumnDef[], - data, - isLoading, - }: DataTableProps) { +export function DataTable({ + columns = defaultColumns as ColumnDef[], + data, + isLoading, + }: DataTableProps) { const [rowSelection, setRowSelection] = React.useState({}); const [columnVisibility, setColumnVisibility] = React.useState({}); const [columnFilters, setColumnFilters] = React.useState([]); @@ -113,8 +112,8 @@ export function DataTable({ handleRowClick((row.original as Product).id)} + className="cursor-pointer" // Make cursor pointer + onClick={() => handleRowClick((row.original as Product).id)} > {row.getVisibleCells().map((cell) => ( diff --git a/playground/src/app/settings/page.tsx b/playground/src/app/settings/page.tsx new file mode 100644 index 00000000..eae2edc6 --- /dev/null +++ b/playground/src/app/settings/page.tsx @@ -0,0 +1,277 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { Check, ChevronsUpDown, X } from "lucide-react"; +import { cn } from "@/lib/utils"; +import PageContentWrapper from "@/components/PageContentWrapper"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { useCluster } from "@/components/solana-cluster-provider"; +import { toast } from "@/components/ui/use-toast"; +import { PlusIcon } from "@radix-ui/react-icons"; + +const formSchema = z.object({ + customLabel: z.string().min(1, "Label is required"), + customEndpoint: z.string().url("Must be a valid URL"), + activeEndpoint: z.string().min(1, "Active endpoint is required"), +}); + +type FormValues = z.infer; + +type Endpoint = { + value: string; + label: string; + url: string; + isCustom?: boolean; +}; + +// Helper function to capitalize the first letter of each word +const capitalizeWords = (str: string) => { + return str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' '); +}; + +// Helper function to truncate URL for privacy +const truncateUrl = (url: string) => { + const parsedUrl = new URL(url); + if (parsedUrl.hostname === 'localhost') { + return `${parsedUrl.protocol}//${parsedUrl.hostname}:${parsedUrl.port}`; + } + return `${parsedUrl.protocol}//${parsedUrl.hostname}`; +}; + +const SettingsPage: React.FC = () => { + const { cluster, clusters, setCluster } = useCluster(); + const [endpoints, setEndpoints] = useState([]); + const [open, setOpen] = useState(false); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + customLabel: '', + customEndpoint: '', + activeEndpoint: cluster.endpoint, + }, + }); + + useEffect(() => { + const clusterEndpoints = clusters.map(c => ({ + value: c.endpoint, + label: capitalizeWords(c.name), + url: truncateUrl(c.endpoint), + })); + + const storedEndpoints = localStorage.getItem('customEndpoints'); + const customEndpoints = storedEndpoints + ? JSON.parse(storedEndpoints).map((ce: {label: string, endpoint: string}) => ({ + value: ce.endpoint, + label: capitalizeWords(ce.label), + url: truncateUrl(ce.endpoint), + isCustom: true, + })) + : []; + + setEndpoints([...clusterEndpoints, ...customEndpoints]); + }, [clusters]); + + const onSubmit = (data: FormValues) => { + const newCustomEndpoint = { + value: data.customEndpoint, + label: capitalizeWords(data.customLabel), + url: truncateUrl(data.customEndpoint), + isCustom: true, + }; + const updatedEndpoints = [...endpoints, newCustomEndpoint]; + setEndpoints(updatedEndpoints); + localStorage.setItem('customEndpoints', JSON.stringify(updatedEndpoints.filter(e => e.isCustom))); + + // Automatically set the new endpoint as active + handleEndpointChange(data.customEndpoint); + + form.reset({ customLabel: '', customEndpoint: '' }); + + toast({ + title: "Custom endpoint added", + description: `${newCustomEndpoint.label} has been added and set as your active endpoint.`, + }); + }; + + const handleEndpointChange = (value: string) => { + const selectedEndpoint = endpoints.find(e => e.value === value); + if (selectedEndpoint) { + form.setValue('activeEndpoint', value); + const clusterEndpoint = clusters.find(c => c.endpoint === value); + if (clusterEndpoint) { + setCluster(clusterEndpoint); + } + toast({ + title: "Endpoint changed", + description: `Active endpoint set to ${selectedEndpoint.label}`, + }); + } + }; + + const deleteCustomEndpoint = (endpointToDelete: string) => { + const updatedEndpoints = endpoints.filter(e => e.value !== endpointToDelete); + setEndpoints(updatedEndpoints); + localStorage.setItem('customEndpoints', JSON.stringify(updatedEndpoints.filter(e => e.isCustom))); + + if (form.getValues('activeEndpoint') === endpointToDelete) { + form.setValue('activeEndpoint', clusters[0].endpoint); + setCluster(clusters[0]); + } + + toast({ + title: "Custom endpoint deleted", + description: "The selected custom endpoint has been removed.", + }); + }; + + return ( + +
+
+ +
+ ( + + Network Endpoint + + + + + + + + + No endpoint found. + + {endpoints.map((endpoint) => ( + { + handleEndpointChange( + currentValue === field.value + ? "" + : currentValue + ); + setOpen(false); + }} + > +
+
+ +
+ + {endpoint.label} + + + {endpoint.url} + +
+
+ {endpoint.isCustom && ( + + )} +
+
+ ))} +
+
+
+
+
+ +
+ )} + /> +
+ +
+
+ ( + + Label + + + + + + )} + /> + ( + + RPC Endpoint + + + + + + )} + /> + +
+
+
+ +
+
+ ); +}; + +export default SettingsPage; diff --git a/playground/src/app/trade/page.tsx b/playground/src/app/trade/page.tsx index e6b4a11e..838e1e97 100644 --- a/playground/src/app/trade/page.tsx +++ b/playground/src/app/trade/page.tsx @@ -30,9 +30,7 @@ import { FormDescription, } from "@/components/ui/form"; import { - CaretSortIcon, - CheckIcon, - ColumnSpacingIcon, + CaretSortIcon, CheckIcon, ColumnSpacingIcon, ExternalLinkIcon } from "@radix-ui/react-icons"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { Switch } from "@/components/ui/switch"; @@ -79,6 +77,7 @@ import { DRIFT_SPOT_MARKETS, } from "@/constants"; import { Skeleton } from "@/components/ui/skeleton"; +import TruncateAddress from "@/utils/TruncateAddress"; const spotMarkets = DRIFT_SPOT_MARKETS.map((x) => ({ label: x, value: x })); const perpsMarkets = DRIFT_PERP_MARKETS.map((x) => ({ label: x, value: x })); @@ -174,7 +173,7 @@ const swapSchema = z.object({ }); const spotSchema = z.object({ - venue: z.enum(["Jupiter", "Drift"]), + venue: z.enum(["Drift"]), spotMarket: z.enum(DRIFT_SPOT_MARKETS), spotType: z.enum(DRIFT_ORDER_TYPES), side: z.enum(["Buy", "Sell"]), @@ -558,6 +557,9 @@ export default function Trade() { } }; + const [cancelValue, setCancelValue] = React.useState("cancelAll"); + const [settleValue, setSettleValue] = React.useState("settlePnL"); + return (
@@ -670,28 +672,36 @@ export default function Trade() { > - (*/} + {/* */} + {/* Slippage*/} + {/* */} + {/* */} + {/* field.onChange(parseFloat(e.target.value))*/} + {/* }*/} + {/* value={field.value}*/} + {/* />*/} + {/* */} + {/* /!* *!/*/} + {/* */} + {/* */} + {/* )}*/} + {/*/>*/} + ( - - Slippage - - - field.onChange(parseFloat(e.target.value)) - } - value={field.value} - /> - - {/* */} - - - )} + label="Slippage" + balance={NaN} + selectedAsset="%" + hideBalance={true} + disableAssetChange={true} /> - Venues + Venues
( + Side
+ ) : spotOrderType === "Market" ? ( + <> +
+ + + + ({ + name: t.name, + symbol: t.symbol, + address: t.address, + decimals: t.decimals, + balance: 0, + } as Asset) + )} + balance={NaN} + selectedAsset={toAsset} + onSelectAsset={setToAsset} + /> +
+ ) : spotOrderType === "Trigger Limit" ? ( <>
@@ -1199,116 +1251,106 @@ export default function Trade() { className="min-w-1/2 w-1/2" name="notional" label="Notional" - assets={tokenList?.map( - (t) => - ({ - name: t.name, - symbol: t.symbol, - address: t.address, - decimals: t.decimals, - balance: 0, - } as Asset) - )} + disableAssetChange={true} balance={NaN} selectedAsset={toAsset} - onSelectAsset={setToAsset} />
) : null} - {spotOrderType !== "Trigger Limit" && !spotReduceOnly && ( - //
- // - //
- - )} - -
-
- {/**/} - {/* */} - {/* */} - {/* */} - {/*

Margin Trading Disabled

*/} - {/*
*/} - {/* */} - {/* Please view the Risk Management configuration of the*/} - {/* Venue Integration.*/} - {/* */} - {/*
*/} - {/*
*/} -
- -
- ( - - - - - - Reduce Only - - - )} - /> - ( - - - - - - Post - - - )} - /> -
- - {/* (*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* Show Confirmation*/} - {/* */} - {/* )}*/} - {/*/>*/} -
+ {spotOrderType !== "Trigger Limit" && + !spotReduceOnly && ( //
+ // + //
+ + )} + + {/*
*/} + {/*
*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/*

Margin Trading Disabled

*/} + {/*
*/} + {/* */} + {/* Please view the Risk Management configuration of the*/} + {/* Venue Integration.*/} + {/* */} + {/*
*/} + {/*
*/} + {/*
*/} + + {/*
*/} + {/* (*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* Reduce Only*/} + {/* */} + {/* */} + {/* )}*/} + {/* />*/} + {/* (*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* Post*/} + {/* */} + {/* */} + {/* )}*/} + {/* />*/} + {/*
*/} + + {/* (*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* Show Confirmation*/} + {/* */} + {/* )}*/} + {/* />*/} + {/*
*/}
+
+
+ + { + if (value) setCancelValue(value); + }} + className="border border-l-0 rounded-l-none h-10 gap-0 w-1/2" + > + + All + + + + {perpsForm + .watch("perpsMarket") + .replace("-PERP", "")} + + + +
+ + {/**/} +
@@ -1339,7 +1434,26 @@ export default function Trade() { name="venue" render={({ field }) => ( - Venue +
+ Venue + {field.value === "Drift" && ( + + + + + + + + + + + + + )} +
+