├── .prettierrc ├── src ├── app │ ├── favicon.ico │ ├── opengraph-image.jpg │ ├── (root) │ │ ├── demo │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── layout.tsx │ └── docs │ │ ├── layout.tsx │ │ ├── utils │ │ ├── prices │ │ │ └── page.tsx │ │ └── helpers │ │ │ └── page.tsx │ │ ├── components │ │ ├── pk-input │ │ │ └── page.tsx │ │ ├── avatar │ │ │ └── page.tsx │ │ ├── sparkline │ │ │ └── page.tsx │ │ ├── price-change │ │ │ └── page.tsx │ │ ├── txn-settings │ │ │ └── page.tsx │ │ └── token-icon │ │ │ └── page.tsx │ │ └── page.tsx ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── collapsible.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── badge.tsx │ │ ├── tooltip.tsx │ │ ├── popover.tsx │ │ ├── toggle.tsx │ │ ├── alert.tsx │ │ ├── toggle-group.tsx │ │ ├── button.tsx │ │ ├── tabs.tsx │ │ ├── card.tsx │ │ ├── table.tsx │ │ ├── dialog.tsx │ │ ├── sheet.tsx │ │ ├── toast.tsx │ │ └── command.tsx │ ├── web │ │ ├── docs-wrapper.tsx │ │ ├── footer.tsx │ │ ├── docs-install-tabs.tsx │ │ ├── docs-heading.tsx │ │ ├── providers.tsx │ │ ├── cta-section.tsx │ │ ├── demo-swap.tsx │ │ ├── demo-dashboard.tsx │ │ ├── hero.tsx │ │ ├── props-table.tsx │ │ ├── features-grid.tsx │ │ ├── docs-tabs.tsx │ │ ├── code-showcase.tsx │ │ └── code.tsx │ └── sol │ │ ├── token-icon.tsx │ │ ├── price-change.tsx │ │ ├── avatar.tsx │ │ ├── pk-input.tsx │ │ ├── sparkline.tsx │ │ ├── token-card.tsx │ │ ├── connect-wallet-popover.tsx │ │ ├── connect-wallet-dialog.tsx │ │ ├── token-list.tsx │ │ ├── txn-list.tsx │ │ ├── user-dropdown.tsx │ │ └── wallet.tsx ├── lib │ ├── consts.ts │ ├── types.ts │ ├── prices │ │ └── birdeye.ts │ ├── assets │ │ ├── birdeye │ │ │ ├── trending.ts │ │ │ ├── wallet.ts │ │ │ ├── fetch.ts │ │ │ └── search.ts │ │ └── helius │ │ │ ├── fetch.ts │ │ │ └── wallet.ts │ └── utils.ts └── hooks │ └── use-mobile.tsx ├── public ├── img │ ├── demo-swap.png │ └── demo-dashboard.png └── token-icons │ ├── placeholder.jpg │ ├── DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263.png │ ├── EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm.png │ ├── EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.png │ ├── J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn.png │ ├── LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp.png │ ├── MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5.png │ └── So11111111111111111111111111111111111111112.png ├── component-sources.d.ts ├── postcss.config.mjs ├── .eslintrc.json ├── components.json ├── .gitignore ├── tsconfig.json ├── next.config.mjs ├── LICENSE.md ├── README.md ├── scripts └── generate-component-source.js ├── tailwind.config.ts └── package.json /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chambaz/solanaui/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/img/demo-swap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chambaz/solanaui/HEAD/public/img/demo-swap.png -------------------------------------------------------------------------------- /src/app/opengraph-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chambaz/solanaui/HEAD/src/app/opengraph-image.jpg -------------------------------------------------------------------------------- /public/img/demo-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chambaz/solanaui/HEAD/public/img/demo-dashboard.png -------------------------------------------------------------------------------- /component-sources.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.tsx.txt" { 2 | const content: string; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /public/token-icons/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chambaz/solanaui/HEAD/public/token-icons/placeholder.jpg -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/token-icons/DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chambaz/solanaui/HEAD/public/token-icons/DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263.png -------------------------------------------------------------------------------- /public/token-icons/EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chambaz/solanaui/HEAD/public/token-icons/EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm.png -------------------------------------------------------------------------------- /public/token-icons/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chambaz/solanaui/HEAD/public/token-icons/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.png -------------------------------------------------------------------------------- /public/token-icons/J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chambaz/solanaui/HEAD/public/token-icons/J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn.png -------------------------------------------------------------------------------- /public/token-icons/LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chambaz/solanaui/HEAD/public/token-icons/LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp.png -------------------------------------------------------------------------------- /public/token-icons/MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chambaz/solanaui/HEAD/public/token-icons/MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5.png -------------------------------------------------------------------------------- /public/token-icons/So11111111111111111111111111111111111111112.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chambaz/solanaui/HEAD/public/token-icons/So11111111111111111111111111111111111111112.png -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "ignorePatterns": ["node_modules", "build", "scripts"], 4 | "rules": { 5 | "@next/next/no-img-element": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/lib/consts.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | 3 | export const SOL_MINT = new PublicKey( 4 | "So11111111111111111111111111111111111111111", 5 | ); 6 | 7 | export const WSOL_MINT = new PublicKey( 8 | "So11111111111111111111111111111111111111112", 9 | ); 10 | 11 | export const USDC_MINT = new PublicKey( 12 | "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 13 | ); 14 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /src/app/(root)/demo/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | 3 | import { DemoWrapper } from "@/components/web/demo-wrapper"; 4 | 5 | export const metadata: Metadata = { 6 | title: "SolanaUI - Demo", 7 | }; 8 | 9 | export default function DemoPage({ 10 | searchParams, 11 | }: { 12 | searchParams: { view: string }; 13 | }) { 14 | const view = searchParams.view; 15 | 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # generated 39 | /public/generated 40 | 41 | -------------------------------------------------------------------------------- /src/components/web/docs-wrapper.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | import { useSidebar } from "@/components/ui/sidebar"; 6 | 7 | const DocsWrapper = ({ children }: { children: React.ReactNode }) => { 8 | const { open } = useSidebar(); 9 | return ( 10 |
16 | {children} 17 |
18 | ); 19 | }; 20 | 21 | export { DocsWrapper }; 22 | -------------------------------------------------------------------------------- /src/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const MOBILE_BREAKPOINT = 1024; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState( 7 | undefined, 8 | ); 9 | 10 | React.useEffect(() => { 11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 | const onChange = () => { 13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 14 | }; 15 | mql.addEventListener("change", onChange); 16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 17 | return () => mql.removeEventListener("change", onChange); 18 | }, []); 19 | 20 | return !!isMobile; 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /src/components/web/footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Footer = () => { 4 | return ( 5 |
6 |
7 |

solanaui © {new Date().getFullYear()}

8 |

9 | Built by{" "} 10 | 16 | @chambaz 17 | 18 |

19 |
20 |
21 | ); 22 | }; 23 | 24 | export { Footer }; 25 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "*.ipfs.nftstorage.link", 8 | }, 9 | { 10 | protocol: "https", 11 | hostname: "nftstorage.link", 12 | pathname: "/**/*", 13 | }, 14 | { 15 | protocol: "https", 16 | hostname: "arweave.net", 17 | pathname: "/*", 18 | }, 19 | { 20 | protocol: "https", 21 | hostname: "shdw-drive.genesysgo.net", 22 | pathname: "/**/*", 23 | }, 24 | { 25 | protocol: "https", 26 | hostname: "ipfs.io", 27 | pathname: "/ipfs/*", 28 | }, 29 | ], 30 | }, 31 | }; 32 | 33 | export default nextConfig; 34 | -------------------------------------------------------------------------------- /src/components/web/docs-install-tabs.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Badge } from "@/components/ui/badge"; 3 | 4 | const DocsInstallTabs = () => { 5 | return ( 6 |
7 | 10 | 19 |
20 | ); 21 | }; 22 | 23 | export { DocsInstallTabs }; 24 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, Connection } from "@solana/web3.js"; 2 | 3 | export type SolAsset = { 4 | mint: PublicKey; 5 | name: string; 6 | symbol: string; 7 | image: string; 8 | decimals: number; 9 | price: number; 10 | userTokenAccount?: { 11 | address: PublicKey; 12 | amount: number; 13 | }; 14 | }; 15 | 16 | export type FetchAssetsArgs = { 17 | addresses: PublicKey[]; 18 | owner?: PublicKey; 19 | connection?: Connection; 20 | combineNativeBalance?: boolean; 21 | }; 22 | 23 | export type FetchWalletArgs = { 24 | owner: PublicKey; 25 | limit?: number; 26 | connection?: Connection; 27 | combineNativeBalance?: boolean; 28 | }; 29 | 30 | export type SearchAssetsArgs = { 31 | query: string; 32 | owner?: PublicKey; 33 | connection?: Connection; 34 | combineNativeBalance?: boolean; 35 | }; 36 | 37 | export type TrendingAssetsArgs = { 38 | owner?: PublicKey; 39 | limit?: number; 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/web/docs-heading.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { IconLink } from "@tabler/icons-react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | type DocsHeadingProps = { 8 | children: React.ReactNode; 9 | href: string; 10 | className?: string; 11 | }; 12 | 13 | const DocsH1 = ({ children, href, className }: DocsHeadingProps) => { 14 | return ( 15 | 16 |

17 | {children} 18 |

19 | 20 | ); 21 | }; 22 | 23 | const DocsH2 = ({ children, href, className }: DocsHeadingProps) => { 24 | return ( 25 | 26 |

27 | 28 | {children} 29 |

30 | 31 | ); 32 | }; 33 | 34 | export { DocsH1, DocsH2 }; 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 shadcn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/app/(root)/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Hero } from "@/components/web/hero"; 4 | import { FeaturesGrid } from "@/components/web/features-grid"; 5 | import { CodeShowcase } from "@/components/web/code-showcase"; 6 | import { CtaSection } from "@/components/web/cta-section"; 7 | import { DemoWrapper } from "@/components/web/demo-wrapper"; 8 | 9 | export default function HomePage() { 10 | return ( 11 |
12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 |

26 | See SolanaUI in action 27 |

28 | 29 |
30 | 31 |
32 | 33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/web/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { 6 | ConnectionProvider, 7 | WalletProvider, 8 | } from "@solana/wallet-adapter-react"; 9 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 10 | 11 | import { TxnSettingsProvider } from "@/components/sol/txn-settings"; 12 | import { ThemeProvider } from "@/components/web/themes"; 13 | 14 | const Providers = ({ children }: { children: React.ReactNode }) => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 25 | {children} 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export { Providers }; 35 | -------------------------------------------------------------------------------- /src/components/sol/token-icon.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { SolAsset } from "@/lib/types"; 6 | 7 | type IconProps = { 8 | asset: SolAsset | null; 9 | size?: number; 10 | }; 11 | 12 | const TokenIcon = ({ asset, size = 24 }: IconProps) => { 13 | return ( 14 |
21 | {asset?.symbol 28 | {asset?.symbol 39 |
40 | ); 41 | }; 42 | 43 | export { TokenIcon }; 44 | -------------------------------------------------------------------------------- /src/components/sol/price-change.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { cn, formatUsd } from "@/lib/utils"; 6 | 7 | type PriceChangeProps = { 8 | data: { 9 | timestamp: number; 10 | price: number; 11 | }[]; 12 | type?: "%" | "$"; 13 | }; 14 | 15 | const PriceChange = ({ data, type = "%" }: PriceChangeProps) => { 16 | const [selectedType, setSelectedType] = React.useState(type); 17 | const startPrice = data[0]?.price || 0; 18 | const endPrice = data[data.length - 1]?.price || 0; 19 | const priceDifference = endPrice - startPrice; 20 | const percentageChange = (priceDifference / startPrice) * 100; 21 | const isPositive = priceDifference > 0; 22 | 23 | if (!data[0]?.price) return null; 24 | 25 | return ( 26 |
setSelectedType(selectedType === "%" ? "$" : "%")} 32 | > 33 | {isPositive && "+"} 34 | {selectedType === "%" 35 | ? `${percentageChange.toFixed(2)}%` 36 | : `${formatUsd(priceDifference)}`} 37 |
38 | ); 39 | }; 40 | 41 | export { PriceChange }; 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SolanaUI 2 | 3 | SolanaUI is a collection of beautifully designed UI components and utility functions, built for Solana. It extends the powerful [shadcn/ui](https://ui.shadcn.com/) library with Solana-specific components along with asset / price fetching utilites, making it easier to get started with Solana UI development. 4 | 5 | ## Getting Started 6 | 7 | To get started read the [docs](https://www.solanaui.dev/) or install the docs locally. 8 | 9 | ### Installation 10 | 11 | ```bash 12 | # install dependencies 13 | pnpm install 14 | ``` 15 | 16 | ### Set your RPC url in your .env.local 17 | ``` 18 | NEXT_PUBLIC_RPC_URL="https://your-rpc-url.com" 19 | ``` 20 | 21 | ### Development 22 | 23 | ```bash 24 | # start development server 25 | pnpm dev 26 | ``` 27 | 28 | Open [http://localhost:3000](http://localhost:3000) in your browser to see the result. All SolanaUI components are stored in `src/components/sol` and utility functions can all be found in `src/lib`. 29 | 30 | ## Documentation 31 | 32 | Visit the [docs](https://www.solanaui.dev/docs) to get started. 33 | 34 | ## Contributing 35 | 36 | Contributions are welcome! Please feel free to submit a Pull Request. 37 | 38 | ## License 39 | 40 | This project is licensed under the MIT License - see license for details. 41 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/components/sol/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { PublicKey } from "@solana/web3.js"; 6 | import { minidenticon } from "minidenticons"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | 10 | type AvatarProps = { 11 | address: PublicKey; 12 | size?: number; 13 | className?: string; 14 | alt?: string; 15 | }; 16 | 17 | const Avatar = ({ address, size = 48, className, alt }: AvatarProps) => { 18 | const pubkeyStr = React.useMemo(() => { 19 | if (!address) return ""; 20 | if (typeof address === "string") return address; 21 | return address.toBase58(); 22 | }, [address]); 23 | 24 | const identicon = React.useMemo(() => { 25 | if (!pubkeyStr) return ""; 26 | return ( 27 | "data:image/svg+xml;utf8," + 28 | encodeURIComponent(minidenticon(pubkeyStr, 90, 50)) 29 | ); 30 | }, [pubkeyStr]); 31 | 32 | return ( 33 |
40 | {alt 46 |
47 | ); 48 | }; 49 | 50 | export { Avatar }; 51 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 19 | 28 | 29 | )) 30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 31 | 32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 33 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 19 | 29 | 30 | )) 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 34 | -------------------------------------------------------------------------------- /src/app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter, Roboto_Mono } from "next/font/google"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | import { TxnToaster } from "@/components/sol/txn-toast"; 7 | 8 | import { Providers } from "@/components/web/providers"; 9 | import { Header } from "@/components/web/header"; 10 | import { Footer } from "@/components/web/footer"; 11 | 12 | import "@/app/globals.css"; 13 | 14 | const inter = Inter({ 15 | subsets: ["latin"], 16 | display: "swap", 17 | variable: "--font-inter", 18 | }); 19 | 20 | const roboto_mono = Roboto_Mono({ 21 | subsets: ["latin"], 22 | display: "swap", 23 | variable: "--font-roboto-mono", 24 | }); 25 | 26 | export const metadata: Metadata = { 27 | title: "SolanaUI - Build Solana apps faster", 28 | description: 29 | "Beautifully designed UI components and utilities, built for Solana. Extending the powerful @shadcn/ui library.", 30 | }; 31 | 32 | export default function RootLayout({ 33 | children, 34 | }: { 35 | children: React.ReactNode; 36 | }) { 37 | return ( 38 | 43 | 44 | 45 |
46 |
47 | {children} 48 |
49 |
50 | 51 |
52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/ui/toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TogglePrimitive from "@radix-ui/react-toggle" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const toggleVariants = cva( 10 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground", 11 | { 12 | variants: { 13 | variant: { 14 | default: "bg-transparent", 15 | outline: 16 | "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground", 17 | }, 18 | size: { 19 | default: "h-9 px-3", 20 | sm: "h-8 px-2", 21 | lg: "h-10 px-3", 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: "default", 26 | size: "default", 27 | }, 28 | } 29 | ) 30 | 31 | const Toggle = React.forwardRef< 32 | React.ElementRef, 33 | React.ComponentPropsWithoutRef & 34 | VariantProps 35 | >(({ className, variant, size, ...props }, ref) => ( 36 | 41 | )) 42 | 43 | Toggle.displayName = TogglePrimitive.Root.displayName 44 | 45 | export { Toggle, toggleVariants } 46 | -------------------------------------------------------------------------------- /src/components/sol/pk-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { validatePublicKey, cn } from "@/lib/utils"; 6 | import { Input } from "@/components/ui/input"; 7 | 8 | const PKInput = ({ ...props }: React.ComponentPropsWithoutRef<"input">) => { 9 | const [value, setValue] = React.useState(""); 10 | const [isInvalid, setIsInvalid] = React.useState(false); 11 | const [hasBlurred, setHasBlurred] = React.useState(false); 12 | const inputRef = React.useRef(null); 13 | 14 | const validateField = React.useCallback(() => { 15 | const isValid = validatePublicKey(value); 16 | 17 | if (inputRef.current) { 18 | if (!isValid) { 19 | inputRef.current.setCustomValidity("Invalid public key"); 20 | } else { 21 | inputRef.current.setCustomValidity(""); 22 | } 23 | 24 | setIsInvalid(!inputRef.current.validity.valid); 25 | } 26 | }, [value]); 27 | 28 | const handleBlur = React.useCallback(() => { 29 | setHasBlurred(true); 30 | validateField(); 31 | }, [validateField]); 32 | 33 | React.useEffect(() => { 34 | if (hasBlurred) { 35 | validateField(); 36 | } 37 | }, [value, validateField, hasBlurred]); 38 | 39 | return ( 40 | setValue(e.target.value)} 51 | onBlur={handleBlur} 52 | aria-invalid={isInvalid} 53 | /> 54 | ); 55 | }; 56 | 57 | export { PKInput }; 58 | -------------------------------------------------------------------------------- /src/app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter, Roboto_Mono } from "next/font/google"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | import { TxnToaster } from "@/components/sol/txn-toast"; 7 | 8 | import { Providers } from "@/components/web/providers"; 9 | import { Header } from "@/components/web/header"; 10 | import { Footer } from "@/components/web/footer"; 11 | import { AppSidebar } from "@/components/web/app-sidebar"; 12 | import { SidebarProvider } from "@/components/ui/sidebar"; 13 | 14 | import "@/app/globals.css"; 15 | 16 | const inter = Inter({ 17 | subsets: ["latin"], 18 | display: "swap", 19 | variable: "--font-inter", 20 | }); 21 | 22 | const roboto_mono = Roboto_Mono({ 23 | subsets: ["latin"], 24 | display: "swap", 25 | variable: "--font-roboto-mono", 26 | }); 27 | 28 | export const metadata: Metadata = { 29 | title: "Docs - SolanaUI", 30 | }; 31 | 32 | export default function DocsLayout({ 33 | children, 34 | }: { 35 | children: React.ReactNode; 36 | }) { 37 | return ( 38 | 43 | 44 | 45 |
46 | 47 | 48 |
49 |
50 |
{children}
51 |
52 |
53 |
54 |
55 | 56 |
57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/prices/birdeye.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | 3 | /** 4 | * Fetches historical price data for a token from Birdeye API 5 | * @param mint - Token mint address to fetch price history for 6 | * @param start - Start timestamp in seconds 7 | * @param end - End timestamp in seconds 8 | * @param interval - Time interval for price data points (default: "1H") 9 | * @returns Array of price data points with timestamps, or null if fetch fails 10 | * @example 11 | * const history = await fetchPriceHistoryBirdeye( 12 | * new PublicKey("So11111111111111111111111111111111111111112"), 13 | * 1672531200, // Jan 1, 2023 14 | * 1704067200, // Jan 1, 2024 15 | * "1H" 16 | * ); 17 | */ 18 | export async function fetchPriceHistoryBirdeye( 19 | mint: PublicKey, 20 | start: number, 21 | end: number, 22 | interval: string = "1H", 23 | ): Promise<{ timestamp: number; price: number }[] | null> { 24 | try { 25 | const response = await fetch( 26 | `https://public-api.birdeye.so/defi/history_price?address=${mint.toBase58()}&type=${interval}&time_from=${start}&time_to=${end}`, 27 | { 28 | headers: { 29 | "x-api-key": process.env.NEXT_PUBLIC_BIRDEYE_API_KEY!, 30 | }, 31 | }, 32 | ); 33 | 34 | const priceHistoryData = await response.json(); 35 | 36 | if (!priceHistoryData?.data || !priceHistoryData.data.items) { 37 | return null; 38 | } 39 | 40 | return priceHistoryData.data.items.map( 41 | (item: { unixTime: number; value: number }) => { 42 | return { 43 | timestamp: item.unixTime, 44 | price: item.value, 45 | }; 46 | }, 47 | ); 48 | } catch (error) { 49 | console.error("Error fetching price from Birdeye:", error); 50 | return null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-[11px] [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | }, 20 | ); 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )); 33 | Alert.displayName = "Alert"; 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )); 45 | AlertTitle.displayName = "AlertTitle"; 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )); 57 | AlertDescription.displayName = "AlertDescription"; 58 | 59 | export { Alert, AlertTitle, AlertDescription }; 60 | -------------------------------------------------------------------------------- /scripts/generate-component-source.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const glob = require("glob"); 4 | 5 | const COMPONENT_SOURCE_DIR = "./src/components/sol"; 6 | const UTILITY_SOURCE_DIRS = [ 7 | "./src/lib/assets/**/*.ts", 8 | "./src/lib/prices/**/*.ts", 9 | "./src/lib/utils.ts", 10 | ]; 11 | const OUTPUT_DIR = "./public/generated/component-sources"; 12 | 13 | // Ensure output directory exists 14 | fs.mkdirSync(OUTPUT_DIR, { recursive: true }); 15 | 16 | // Process component files 17 | const componentFiles = glob.sync(`${COMPONENT_SOURCE_DIR}/**/*.tsx`); 18 | 19 | componentFiles.forEach((filePath) => { 20 | const content = fs.readFileSync(filePath, "utf-8"); 21 | 22 | // Extract just the filename without the path 23 | const filename = path.basename(filePath); 24 | // Create the output path directly in the output directory 25 | const outputPath = path.join(OUTPUT_DIR, filename + ".txt"); 26 | 27 | fs.writeFileSync(outputPath, content); 28 | console.log(`Generated component: ${outputPath}`); 29 | }); 30 | 31 | // Process utility files 32 | UTILITY_SOURCE_DIRS.forEach((pattern) => { 33 | const utilityFiles = glob.sync(pattern); 34 | 35 | utilityFiles.forEach((filePath) => { 36 | const content = fs.readFileSync(filePath, "utf-8"); 37 | 38 | // Get just the relative path without the ./src/lib/ prefix 39 | const relPath = filePath.replace(/^\.\/src\/lib\//, ""); 40 | // Create a flat filename with underscores instead of slashes 41 | const flatFilename = relPath.replace(/\//g, "_"); 42 | // Remove any src_lib_ prefix that might have been added 43 | const cleanFilename = flatFilename.replace(/^src_lib_/, ""); 44 | const outputPath = path.join(OUTPUT_DIR, cleanFilename + ".txt"); 45 | 46 | fs.writeFileSync(outputPath, content); 47 | console.log(`Generated utility: ${outputPath}`); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/ui/toggle-group.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" 5 | import { type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { toggleVariants } from "@/components/ui/toggle" 9 | 10 | const ToggleGroupContext = React.createContext< 11 | VariantProps 12 | >({ 13 | size: "default", 14 | variant: "default", 15 | }) 16 | 17 | const ToggleGroup = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef & 20 | VariantProps 21 | >(({ className, variant, size, children, ...props }, ref) => ( 22 | 27 | 28 | {children} 29 | 30 | 31 | )) 32 | 33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName 34 | 35 | const ToggleGroupItem = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef & 38 | VariantProps 39 | >(({ className, children, variant, size, ...props }, ref) => { 40 | const context = React.useContext(ToggleGroupContext) 41 | 42 | return ( 43 | 54 | {children} 55 | 56 | ) 57 | }) 58 | 59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName 60 | 61 | export { ToggleGroup, ToggleGroupItem } 62 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex gap-1 items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /src/components/web/cta-section.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion } from "motion/react"; 4 | import { IconBrandGithub, IconArrowRight } from "@tabler/icons-react"; 5 | import Link from "next/link"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | 9 | const CtaSection = () => { 10 | return ( 11 |
12 |
13 | 20 | Ready to build your Solana app? 21 | 22 | 29 | Start building today with SolanaUI's components and utilities. 30 | 31 | 38 | 39 | 42 | 43 | 48 | 51 | 52 | 53 |
54 |
55 | ); 56 | }; 57 | 58 | export { CtaSection }; 59 | -------------------------------------------------------------------------------- /src/app/docs/utils/prices/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { Code } from "@/components/web/code"; 6 | import { DocsWrapper } from "@/components/web/docs-wrapper"; 7 | import { DocsH1, DocsH2 } from "@/components/web/docs-heading"; 8 | 9 | export default function PricesPage() { 10 | const [birdeyeSource, setBirdeyeSource] = React.useState(""); 11 | 12 | React.useEffect(() => { 13 | fetch("/generated/component-sources/prices_birdeye.ts.txt") 14 | .then((res) => res.text()) 15 | .then(setBirdeyeSource); 16 | }, []); 17 | 18 | return ( 19 | 20 |
21 | Price Utilities 22 |

23 | Some SolanaUI components require historical price data. These 24 | components are designed to be data source agnostic, however SolanaUI 25 | comes with an example using the birdeye API. 26 |

27 |
28 | 29 |
30 | 31 | Historical Price Data 32 | 33 |

34 | Copy the code below to lib/prices.ts, or wherever is 35 | convenient, just be sure to update the component imports too. 36 |

37 | 38 | 39 |

Use the fetchPriceHistory function in your codebase like so.

40 | 60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/sol/sparkline.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { LineChart, CartesianGrid, XAxis, YAxis, Line } from "recharts"; 6 | 7 | import { ChartConfig, ChartContainer } from "@/components/ui/chart"; 8 | 9 | import { PriceChange } from "@/components/sol/price-change"; 10 | 11 | type SparklineProps = { 12 | data: { 13 | timestamp: number; 14 | price: number; 15 | }[]; 16 | }; 17 | 18 | const chartConfig = { 19 | desktop: { 20 | label: "Price", 21 | color: "hsl(var(--chart-1))", 22 | }, 23 | } satisfies ChartConfig; 24 | 25 | const Sparkline = ({ data }: SparklineProps) => { 26 | if (!data.length) return null; 27 | 28 | const minPrice = Math.min(...data.map((d) => d.price)); 29 | const maxPrice = Math.max(...data.map((d) => d.price)); 30 | 31 | return ( 32 |
33 | 37 | 38 | 39 | 47 | 48 | 49 | 50 | = data[0].price 54 | ? "#75ba80" 55 | : "#e07d6f" 56 | } 57 | stopOpacity={0.8} 58 | /> 59 | = data[0].price 63 | ? "#75ba80" 64 | : "#e07d6f" 65 | } 66 | stopOpacity={0.1} 67 | /> 68 | 69 | 70 | = data[0].price 76 | ? "#75ba80" 77 | : "#e07d6f" 78 | } 79 | /> 80 | 81 | 82 |
83 | 84 |
85 |
86 | ); 87 | }; 88 | 89 | export { Sparkline }; 90 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: "hsl(var(--background))", 14 | foreground: "hsl(var(--foreground))", 15 | card: { 16 | DEFAULT: "hsl(var(--card))", 17 | foreground: "hsl(var(--card-foreground))", 18 | }, 19 | popover: { 20 | DEFAULT: "hsl(var(--popover))", 21 | foreground: "hsl(var(--popover-foreground))", 22 | }, 23 | primary: { 24 | DEFAULT: "hsl(var(--primary))", 25 | foreground: "hsl(var(--primary-foreground))", 26 | }, 27 | secondary: { 28 | DEFAULT: "hsl(var(--secondary))", 29 | foreground: "hsl(var(--secondary-foreground))", 30 | }, 31 | muted: { 32 | DEFAULT: "hsl(var(--muted))", 33 | foreground: "hsl(var(--muted-foreground))", 34 | }, 35 | accent: { 36 | DEFAULT: "hsl(var(--accent))", 37 | foreground: "hsl(var(--accent-foreground))", 38 | }, 39 | destructive: { 40 | DEFAULT: "hsl(var(--destructive))", 41 | foreground: "hsl(var(--destructive-foreground))", 42 | }, 43 | border: "hsl(var(--border))", 44 | input: "hsl(var(--input))", 45 | ring: "hsl(var(--ring))", 46 | chart: { 47 | "1": "hsl(var(--chart-1))", 48 | "2": "hsl(var(--chart-2))", 49 | "3": "hsl(var(--chart-3))", 50 | "4": "hsl(var(--chart-4))", 51 | "5": "hsl(var(--chart-5))", 52 | }, 53 | sidebar: { 54 | DEFAULT: "hsl(var(--sidebar-background))", 55 | foreground: "hsl(var(--sidebar-foreground))", 56 | primary: "hsl(var(--sidebar-primary))", 57 | "primary-foreground": "hsl(var(--sidebar-primary-foreground))", 58 | accent: "hsl(var(--sidebar-accent))", 59 | "accent-foreground": "hsl(var(--sidebar-accent-foreground))", 60 | border: "hsl(var(--sidebar-border))", 61 | ring: "hsl(var(--sidebar-ring))", 62 | }, 63 | solana: { 64 | green: "#14F195", 65 | purple: "#9945FF", 66 | }, 67 | }, 68 | borderRadius: { 69 | lg: "var(--radius)", 70 | md: "calc(var(--radius) - 2px)", 71 | sm: "calc(var(--radius) - 4px)", 72 | }, 73 | }, 74 | }, 75 | plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], 76 | }; 77 | export default config; 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solanaui", 3 | "version": "1.2.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "node scripts/generate-component-source.js && next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@bonfida/spl-name-service": "^3.0.3", 13 | "@metaplex-foundation/mpl-token-metadata": "^3.2.1", 14 | "@metaplex-foundation/umi": "^0.9.2", 15 | "@metaplex-foundation/umi-bundle-defaults": "^0.9.2", 16 | "@pythnetwork/client": "^2.22.0", 17 | "@radix-ui/react-collapsible": "^1.1.2", 18 | "@radix-ui/react-dialog": "^1.1.4", 19 | "@radix-ui/react-dropdown-menu": "^2.1.2", 20 | "@radix-ui/react-icons": "^1.3.2", 21 | "@radix-ui/react-label": "^2.1.0", 22 | "@radix-ui/react-popover": "^1.1.1", 23 | "@radix-ui/react-select": "^2.1.1", 24 | "@radix-ui/react-separator": "^1.1.1", 25 | "@radix-ui/react-slot": "^1.1.2", 26 | "@radix-ui/react-tabs": "^1.1.0", 27 | "@radix-ui/react-toast": "^1.2.2", 28 | "@radix-ui/react-toggle": "^1.1.0", 29 | "@radix-ui/react-toggle-group": "^1.1.0", 30 | "@radix-ui/react-tooltip": "^1.1.6", 31 | "@solana/spl-token": "^0.4.13", 32 | "@solana/wallet-adapter-base": "^0.9.23", 33 | "@solana/wallet-adapter-react": "^0.15.35", 34 | "@solana/wallet-adapter-wallets": "^0.19.32", 35 | "@solana/web3.js": "^1.95.3", 36 | "@tabler/icons-react": "^3.17.0", 37 | "@tailwindcss/typography": "^0.5.16", 38 | "@web3auth/auth-adapter": "^9.0.2", 39 | "@web3auth/base": "^9.0.2", 40 | "@web3auth/no-modal": "^9.1.0", 41 | "@web3auth/solana-provider": "^9.0.2", 42 | "class-variance-authority": "^0.7.1", 43 | "clsx": "^2.1.1", 44 | "cmdk": "1.0.0", 45 | "date-fns": "^4.1.0", 46 | "glob": "^11.0.1", 47 | "lodash": "^4.17.21", 48 | "lucide-react": "^0.453.0", 49 | "millify": "^6.1.0", 50 | "minidenticons": "^4.2.1", 51 | "motion": "^12.6.2", 52 | "next": "14.2.11", 53 | "next-themes": "^0.3.0", 54 | "pino-pretty": "^11.2.2", 55 | "react": "^18", 56 | "react-copy-to-clipboard": "^5.1.0", 57 | "react-dom": "^18", 58 | "react-number-format": "^5.4.4", 59 | "react-syntax-highlighter": "^15.5.0", 60 | "recharts": "^2.13.0", 61 | "tailwind-merge": "^2.5.2", 62 | "tailwindcss-animate": "^1.0.7" 63 | }, 64 | "devDependencies": { 65 | "@types/lodash": "^4.17.14", 66 | "@types/node": "^20", 67 | "@types/react": "^18", 68 | "@types/react-copy-to-clipboard": "^5.0.7", 69 | "@types/react-dom": "^18", 70 | "@types/react-syntax-highlighter": "^15.5.13", 71 | "eslint": "^8", 72 | "eslint-config-next": "14.2.11", 73 | "postcss": "^8", 74 | "prettier": "^3.3.3", 75 | "prettier-plugin-tailwindcss": "^0.6.6", 76 | "tailwindcss": "^3.4.1", 77 | "typescript": "^5" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/components/sol/token-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Link from "next/link"; 5 | import { ExternalLinkIcon } from "lucide-react"; 6 | 7 | import { formatUsd, shortAddress, cn } from "@/lib/utils"; 8 | import { SolAsset } from "@/lib/types"; 9 | 10 | import { 11 | Card, 12 | CardContent, 13 | CardDescription, 14 | CardHeader, 15 | CardTitle, 16 | } from "@/components/ui/card"; 17 | import { Skeleton } from "@/components/ui/skeleton"; 18 | 19 | import { TokenIcon } from "@/components/sol/token-icon"; 20 | import { Sparkline } from "@/components/sol/sparkline"; 21 | 22 | type TokenCardProps = { 23 | asset: SolAsset | null; 24 | chartData?: { timestamp: number; price: number }[]; 25 | size?: "sm" | "md"; 26 | }; 27 | 28 | const TokenCard = ({ asset, chartData = [], size = "md" }: TokenCardProps) => { 29 | if (!asset) { 30 | return ( 31 | 32 | 33 | 34 | Loading... 35 | 36 |
37 | 38 | 39 |
40 |
41 | Loading... 42 |
43 | 44 | 45 | 46 |
47 | ); 48 | } 49 | 50 | return ( 51 | 52 | 53 | 59 | 60 |
61 | {asset.symbol} 62 | 66 | 67 | {shortAddress(asset.mint)} 68 | 69 |
70 |
71 |
72 | 73 | {asset.price && ( 74 |

75 | {formatUsd(asset.price)} 76 |

77 | )} 78 | 79 |
80 |
81 | ); 82 | }; 83 | 84 | export { TokenCard }; 85 | -------------------------------------------------------------------------------- /src/app/docs/components/pk-input/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Link from "next/link"; 5 | 6 | 7 | 8 | import { DocsWrapper } from "@/components/web/docs-wrapper"; 9 | import { DocsTabs, DocsVariant } from "@/components/web/docs-tabs"; 10 | import { DocsH1, DocsH2 } from "@/components/web/docs-heading"; 11 | import { Code } from "@/components/web/code"; 12 | import { DocsInstallTabs } from "@/components/web/docs-install-tabs"; 13 | 14 | import { PKInput } from "@/components/sol/pk-input"; 15 | 16 | export default function TokenInputPage() { 17 | const [componentSource, setComponentSource] = React.useState(""); 18 | 19 | React.useEffect(() => { 20 | fetch("/generated/component-sources/pk-input.tsx.txt") 21 | .then((res) => res.text()) 22 | .then(setComponentSource); 23 | }, []); 24 | 25 | const variants: DocsVariant[] = [ 26 | { 27 | label: "Default", 28 | value: "default", 29 | preview: ( 30 |
31 | 32 |
33 | ), 34 | code: `import { PKInput } from "@/components/sol/pk-input"; 35 | 36 | export function PKInputDemo() { 37 | return ( 38 | 39 | ) 40 | }`, 41 | }, 42 | ]; 43 | 44 | return ( 45 | 46 |
47 | PKInput 48 |

49 | The PKInput component is an input field with support for inline public 50 | key validation. 51 |

52 | 53 |
54 | 55 | Installation 56 | 57 | 58 | 59 | 60 |

1. Install shadcn/ui input component

61 |

62 | Use shadcn/ui CLI or manually install the{" "} 63 | 68 | input 69 | {" "} 70 | component. 71 |

72 | 73 | 74 |

2. Install SolanaUI PKInput

75 |

76 | Copy the code below to src/components/sol/pk-input.tsx. 77 |

78 | 79 | 80 |

3. Use PKInput

81 |

82 | Import the PKInput component and use it in your app. 83 |

84 | 85 | `} /> 86 |
87 |
88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/components/sol/connect-wallet-popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { Wallet } from "@solana/wallet-adapter-react"; 6 | import { useWallet } from "@solana/wallet-adapter-react"; 7 | import { LoaderCircleIcon, WalletIcon } from "lucide-react"; 8 | 9 | import { 10 | Popover, 11 | PopoverContent, 12 | PopoverTrigger, 13 | } from "@/components/ui/popover"; 14 | import { Button } from "@/components/ui/button"; 15 | 16 | type ConnectWalletPopoverProps = { 17 | trigger?: React.ReactNode; 18 | title?: string | React.ReactNode; 19 | description?: string | React.ReactNode; 20 | } & Omit, "children">; 21 | 22 | const ConnectWalletPopover = ({ 23 | trigger, 24 | title, 25 | description, 26 | ...popoverProps 27 | }: ConnectWalletPopoverProps) => { 28 | const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); 29 | const { wallets, select, connecting, wallet } = useWallet(); 30 | 31 | return ( 32 | 37 | 38 | {trigger || ( 39 | 42 | )} 43 | 44 | 45 |
46 | {title && ( 47 |
48 |

{title}

49 | {description && ( 50 |

{description}

51 | )} 52 |
53 | )} 54 |
    55 | {wallets.map((walletItem: Wallet) => ( 56 |
  • 57 | 78 |
  • 79 | ))} 80 |
81 |
82 |
83 |
84 | ); 85 | }; 86 | 87 | export { ConnectWalletPopover }; 88 | -------------------------------------------------------------------------------- /src/components/web/demo-swap.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { useWallet, useConnection } from "@solana/wallet-adapter-react"; 6 | 7 | import { SearchAssetsArgs, SolAsset } from "@/lib/types"; 8 | import { fetchWalletAssets } from "@/lib/assets/birdeye/wallet"; 9 | import { fetchTrendingAssets } from "@/lib/assets/birdeye/trending"; 10 | import { searchAssets } from "@/lib/assets/birdeye/search"; 11 | 12 | import { Swap } from "@/components/sol/swap"; 13 | import { ConnectWalletDialog } from "@/components/sol/connect-wallet-dialog"; 14 | 15 | const DemoSwap = () => { 16 | const { publicKey } = useWallet(); 17 | const { connection } = useConnection(); 18 | const [inAssets, setInAssets] = React.useState([]); 19 | const [outAssets, setOutAssets] = React.useState([]); 20 | const [isFetching, setIsFetching] = React.useState(false); 21 | 22 | const onSearch = React.useCallback( 23 | async (args: SearchAssetsArgs) => { 24 | if (!publicKey || !connection) return []; 25 | 26 | const searchResults = await searchAssets({ 27 | ...args, 28 | owner: publicKey, 29 | connection, 30 | }); 31 | 32 | return searchResults; 33 | }, 34 | [publicKey, connection], 35 | ); 36 | 37 | const fetchData = React.useCallback(async () => { 38 | if (isFetching || !publicKey) return; 39 | 40 | try { 41 | setIsFetching(true); 42 | const fetchedAssets = await fetchWalletAssets({ 43 | owner: publicKey, 44 | connection, 45 | }); 46 | const trendingAssets = await fetchTrendingAssets({ 47 | owner: publicKey, 48 | }); 49 | const trendingSet = new Set( 50 | trendingAssets.map((asset) => asset.mint.toString()), 51 | ); 52 | const finalOutAssets = fetchedAssets.filter( 53 | (asset) => !trendingSet.has(asset.mint.toString()), 54 | ); 55 | setInAssets(fetchedAssets); 56 | setOutAssets([...trendingAssets, ...finalOutAssets]); 57 | } finally { 58 | setIsFetching(false); 59 | } 60 | }, [publicKey, isFetching, connection]); 61 | 62 | React.useEffect(() => { 63 | if (inAssets.length === 0 && !isFetching) { 64 | fetchData(); 65 | } 66 | }, [fetchData, inAssets.length, isFetching]); 67 | 68 | return ( 69 |
70 |
71 |

Swap Demo

72 |

73 | Search and swap for any token with SolanaUI. 74 |

75 |
76 | {!publicKey ? ( 77 | 78 | ) : ( 79 |
80 | { 85 | fetchData(); 86 | }} 87 | /> 88 |
89 | )} 90 |
91 | ); 92 | }; 93 | 94 | export { DemoSwap }; 95 | -------------------------------------------------------------------------------- /src/lib/assets/birdeye/trending.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import { SolAsset, TrendingAssetsArgs } from "@/lib/types"; 3 | 4 | /** 5 | * Fetches trending token assets from Birdeye API, including prices and owner balances if provided 6 | * @param args - Object containing fetch parameters 7 | * @returns Array of SolAsset objects containing trending token data 8 | */ 9 | const fetchTrendingAssets = async ({ 10 | owner, 11 | limit = 10, 12 | }: TrendingAssetsArgs): Promise => { 13 | const headers = { 14 | "x-api-key": process.env.NEXT_PUBLIC_BIRDEYE_API_KEY!, 15 | accept: "application/json", 16 | "x-chain": "solana", 17 | }; 18 | 19 | try { 20 | const trendingRes = await fetch( 21 | `https://public-api.birdeye.so/defi/token_trending?sort_by=rank&sort_type=asc&offset=0&limit=${limit}`, 22 | { headers }, 23 | ); 24 | const { success, data } = await trendingRes.json(); 25 | 26 | if (!success || !data?.tokens) { 27 | return []; 28 | } 29 | 30 | const tokens = data.tokens; 31 | const addresses = tokens.map((t: { address: string }) => t.address); 32 | 33 | // fetch prices for all trending tokens 34 | const priceRes = await fetch( 35 | `https://public-api.birdeye.so/defi/multi_price?list_address=${addresses.join(",")}`, 36 | { headers }, 37 | ); 38 | const priceData = (await priceRes.json()).data; 39 | 40 | // fetch balances for owner if provided 41 | let balanceData: Record = {}; 42 | if (owner) { 43 | const balancePromises = addresses.map((address: string) => 44 | fetch( 45 | `https://public-api.birdeye.so/v1/wallet/token_balance?wallet=${owner.toString()}&token_address=${address}`, 46 | { headers }, 47 | ).then((res) => res.json()), 48 | ); 49 | const balanceResponses = await Promise.all(balancePromises); 50 | balanceData = balanceResponses.reduce( 51 | (acc, response, idx) => { 52 | if (response.data) { 53 | acc[addresses[idx]] = response.data; 54 | } 55 | return acc; 56 | }, 57 | {} as Record, 58 | ); 59 | } 60 | 61 | // map to SolAsset 62 | return tokens.map( 63 | (token: { 64 | address: string; 65 | name: string; 66 | symbol: string; 67 | logoURI?: string; 68 | price?: number; 69 | decimals: number; 70 | }) => { 71 | const price = priceData?.[token.address]?.value ?? token.price ?? null; 72 | const balance = owner && balanceData[token.address]; 73 | return { 74 | mint: new PublicKey(token.address), 75 | name: token.name, 76 | symbol: token.symbol, 77 | image: token.logoURI || "", 78 | price, 79 | decimals: token.decimals, 80 | userTokenAccount: balance 81 | ? { 82 | address: new PublicKey(balance.address), 83 | amount: balance.uiAmount, 84 | } 85 | : undefined, 86 | }; 87 | }, 88 | ); 89 | } catch (error) { 90 | console.error("Error fetching trending assets:", error); 91 | return []; 92 | } 93 | }; 94 | 95 | export { fetchTrendingAssets }; 96 | -------------------------------------------------------------------------------- /src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )); 17 | Table.displayName = "Table"; 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )); 25 | TableHeader.displayName = "TableHeader"; 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )); 37 | TableBody.displayName = "TableBody"; 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className, 48 | )} 49 | {...props} 50 | /> 51 | )); 52 | TableFooter.displayName = "TableFooter"; 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )); 67 | TableRow.displayName = "TableRow"; 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
81 | )); 82 | TableHead.displayName = "TableHead"; 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | 93 | )); 94 | TableCell.displayName = "TableCell"; 95 | 96 | const TableCaption = React.forwardRef< 97 | HTMLTableCaptionElement, 98 | React.HTMLAttributes 99 | >(({ className, ...props }, ref) => ( 100 |
105 | )); 106 | TableCaption.displayName = "TableCaption"; 107 | 108 | export { 109 | Table, 110 | TableHeader, 111 | TableBody, 112 | TableFooter, 113 | TableHead, 114 | TableRow, 115 | TableCell, 116 | TableCaption, 117 | }; 118 | -------------------------------------------------------------------------------- /src/app/docs/utils/helpers/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { Code } from "@/components/web/code"; 6 | import { DocsWrapper } from "@/components/web/docs-wrapper"; 7 | import { DocsH1, DocsH2 } from "@/components/web/docs-heading"; 8 | 9 | export default function HelpersPage() { 10 | const [utilsSource, setUtilsSource] = React.useState(""); 11 | 12 | React.useEffect(() => { 13 | fetch("/generated/component-sources/utils.ts.txt") 14 | .then((res) => res.text()) 15 | .then(setUtilsSource); 16 | }, []); 17 | return ( 18 | 19 |
20 | Helper Utilities 21 |

22 | SolanaUI provides several utility functions for common operations like 23 | number formatting, address handling, and validation. 24 |

25 |
26 | 27 |
28 |

29 | Copy the code below to lib/utils.ts. 30 |

31 | 32 |
33 | 34 |
35 | 36 | Number Formatters 37 | 38 |

39 | Format numbers for display with various options: 40 |

41 | 72 |
73 | 74 |
75 | 76 | Public Key Helpers 77 | 78 |

79 | Functions for working with Solana addresses: 80 |

81 | 101 |
102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/app/docs/components/avatar/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | 3 | import path from "path"; 4 | import { promises as fs } from "fs"; 5 | import { PublicKey } from "@solana/web3.js"; 6 | 7 | import { Avatar } from "@/components/sol/avatar"; 8 | 9 | import { DocsWrapper } from "@/components/web/docs-wrapper"; 10 | import { DocsTabs, DocsVariant } from "@/components/web/docs-tabs"; 11 | import { DocsH1, DocsH2 } from "@/components/web/docs-heading"; 12 | import { Code } from "@/components/web/code"; 13 | import { PropsTable } from "@/components/web/props-table"; 14 | import { DocsInstallTabs } from "@/components/web/docs-install-tabs"; 15 | 16 | export const metadata: Metadata = { 17 | title: "Avatar - SolanaUI", 18 | }; 19 | 20 | export default async function AvatarPage() { 21 | const componentSource = await fs.readFile( 22 | path.join(process.cwd(), "src/components/sol/avatar.tsx"), 23 | "utf8", 24 | ); 25 | 26 | const variants: DocsVariant[] = [ 27 | { 28 | label: "Default", 29 | value: "default", 30 | preview: ( 31 | 36 | ), 37 | code: `import { Avatar } from "@/components/sol/avatar" 38 | 39 | export function AvatarDemo() { 40 | return ( 41 | 42 | ) 43 | }`, 44 | }, 45 | ]; 46 | 47 | return ( 48 | 49 |
50 | Avatar 51 |

52 | The Avatar component renders a minidenticon for a given public key. 53 |

54 | 55 |
56 | 57 | Installation 58 | 59 | 60 | 61 | 62 |

1. Install Dependencies

63 |

Avatar requires minidenticons.

64 | 65 | 66 |

2. Install SolanaUI Avatar

67 |

68 | Copy the code below to src/components/sol/avatar.tsx. 69 |

70 | 71 | 72 |

4. Use Avatar

73 |

74 | Import the Avatar component and use it in your app. 75 |

76 | `} /> 77 | 78 |
79 | 80 | Props 81 | 82 | 99 |
100 |
101 |
102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/components/web/demo-dashboard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { VersionedTransactionResponse } from "@solana/web3.js"; 6 | 7 | import { PriceChart, TimeScale } from "@/components/sol/price-chart"; 8 | import { TokenCard } from "@/components/sol/token-card"; 9 | import { TokenList } from "@/components/sol/token-list"; 10 | import { TxnList } from "@/components/sol/txn-list"; 11 | import { SolAsset } from "@/lib/types"; 12 | 13 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 14 | 15 | type DateRangeKey = "1D" | "1W" | "1M" | "1Y"; 16 | 17 | type DemoDashboardProps = { 18 | mainChartData: { 19 | timestamp: number; 20 | price: number; 21 | }[]; 22 | chartData: { 23 | timestamp: number; 24 | price: number; 25 | }[][]; 26 | timestamps: Record< 27 | DateRangeKey, 28 | { start: number; end: number; interval: string; timeScale: TimeScale } 29 | >; 30 | dateRange: DateRangeKey; 31 | setDateRange: (dateRange: DateRangeKey) => void; 32 | mainAsset: SolAsset | null; 33 | assets: SolAsset[]; 34 | transactions: VersionedTransactionResponse[]; 35 | }; 36 | 37 | const DemoDashboard = ({ 38 | mainChartData, 39 | chartData, 40 | timestamps, 41 | dateRange, 42 | setDateRange, 43 | mainAsset, 44 | assets, 45 | transactions, 46 | }: DemoDashboardProps) => { 47 | return ( 48 | <> 49 |
50 |
51 | { 58 | setDateRange(value as DateRangeKey); 59 | }} 60 | /> 61 |
62 |
63 | {assets.length === 0 ? ( 64 | <> 65 | {[...new Array(4)].map((_, index) => ( 66 | 67 | ))} 68 | 69 | ) : ( 70 | assets.map((asset, index) => ( 71 | 77 | )) 78 | )} 79 |
80 |
81 |
82 | 83 | 84 | Tokens 85 | Transactions 86 | 87 | 88 | { 91 | alert(`Clicked ${token.mint.toBase58()}`); 92 | console.log(token); 93 | }} 94 | /> 95 | 96 | 97 | { 100 | alert(txn.transaction.signatures[0]); 101 | }} 102 | /> 103 | 104 | 105 |
106 | 107 | ); 108 | }; 109 | 110 | export { DemoDashboard }; 111 | -------------------------------------------------------------------------------- /src/components/web/hero.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { IconBrandGithub, IconRocket } from "@tabler/icons-react"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | 7 | const Hero = () => { 8 | return ( 9 |
10 |

11 | Build{" "} 12 | 13 | Solana 14 | {" "} 15 | apps faster. 16 |

17 |

18 | Beautifully designed UI components and utilities, built for Solana. 19 |
Extending the powerful{" "} 20 | 26 | @shadcn/ui library 27 | 28 | . 29 |

30 |
31 |

32 | Built by{" "} 33 | 38 | @chambaz 39 | 40 |

41 | 47 | 53 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | marginfi 68 | 69 |
70 |
71 | 72 | 75 | 76 | 81 | 87 | 88 |
89 |
90 | ); 91 | }; 92 | 93 | export { Hero }; 94 | -------------------------------------------------------------------------------- /src/components/web/props-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; 4 | import jsx from "react-syntax-highlighter/dist/esm/languages/prism/jsx"; 5 | import ts from "react-syntax-highlighter/dist/esm/languages/prism/typescript"; 6 | import Dark from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus"; 7 | import Light from "react-syntax-highlighter/dist/esm/styles/prism/base16-ateliersulphurpool.light"; 8 | import { useTheme } from "next-themes"; 9 | import React from "react"; 10 | 11 | import { 12 | Table, 13 | TableBody, 14 | TableCell, 15 | TableHead, 16 | TableHeader, 17 | TableRow, 18 | } from "@/components/ui/table"; 19 | import { cn } from "@/lib/utils"; 20 | import { Skeleton } from "@/components/ui/skeleton"; 21 | 22 | SyntaxHighlighter.registerLanguage("jsx", jsx); 23 | SyntaxHighlighter.registerLanguage("ts", ts); 24 | 25 | type PropsTableProps = { 26 | data: { 27 | name: string; 28 | type: string; 29 | default?: string; 30 | }[]; 31 | }; 32 | 33 | const PropsTable = ({ data }: PropsTableProps) => { 34 | const { resolvedTheme } = useTheme(); 35 | const [mounted, setMounted] = React.useState(false); 36 | const hasDefaults = data.some((item) => item.default); 37 | 38 | React.useEffect(() => { 39 | setMounted(true); 40 | }, []); 41 | 42 | if (!mounted) { 43 | return ( 44 |
45 | 46 | 47 | 48 |
49 | ); 50 | } 51 | 52 | return ( 53 | 54 | 55 | 56 | Name 57 | Type 58 | {hasDefaults && ( 59 | Default 60 | )} 61 | 62 | 63 | 64 | {data.map((item, index) => ( 65 | 69 | 72 | {item.name} 73 | 74 | 75 | 85 | {item.type} 86 | 87 | 88 | {hasDefaults && ( 89 | 92 | {item.default && ( 93 | 103 | {item.default} 104 | 105 | )} 106 | 107 | )} 108 | 109 | ))} 110 | 111 |
112 | ); 113 | }; 114 | 115 | export { PropsTable }; 116 | -------------------------------------------------------------------------------- /src/components/sol/connect-wallet-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { Wallet } from "@solana/wallet-adapter-react"; 6 | import { useWallet } from "@solana/wallet-adapter-react"; 7 | import { XIcon, LoaderCircleIcon } from "lucide-react"; 8 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 9 | 10 | import { cn } from "@/lib/utils"; 11 | import { 12 | Dialog, 13 | DialogOverlay, 14 | DialogPortal, 15 | DialogDescription, 16 | DialogTitle, 17 | DialogHeader, 18 | DialogTrigger, 19 | DialogClose, 20 | } from "@/components/ui/dialog"; 21 | import { Button } from "@/components/ui/button"; 22 | 23 | type ConnectWalletDialogProps = { 24 | trigger?: React.ReactNode; 25 | title?: string | React.ReactNode; 26 | description?: string | React.ReactNode; 27 | } & Omit, "children">; 28 | 29 | const ConnectWalletDialog = ({ 30 | trigger, 31 | title, 32 | description, 33 | ...dialogProps 34 | }: ConnectWalletDialogProps) => { 35 | const [isDialogOpen, setIsDialogOpen] = React.useState(false); 36 | const { wallets, select, connecting, wallet } = useWallet(); 37 | 38 | return ( 39 | 40 | 41 | {trigger || } 42 | 43 | 44 | 45 | 46 | 47 | 48 | {title || "Connect Wallet"} 49 | 50 | 51 | {description || "Connect your wallet to continue"} 52 | 53 | 54 |
    55 | {wallets.map((walletItem: Wallet) => ( 56 |
  • 57 | 79 |
  • 80 | ))} 81 |
82 | 83 | 84 | Close 85 | 86 |
87 |
88 |
89 | ); 90 | }; 91 | 92 | export { ConnectWalletDialog }; 93 | -------------------------------------------------------------------------------- /src/components/web/features-grid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | IconComponents, 5 | IconCopy, 6 | IconCurrencySolana, 7 | IconDatabase, 8 | IconPalette, 9 | IconWallet, 10 | } from "@tabler/icons-react"; 11 | import { motion } from "motion/react"; 12 | 13 | import { cn } from "@/lib/utils"; 14 | import { 15 | Card, 16 | CardContent, 17 | CardDescription, 18 | CardHeader, 19 | CardTitle, 20 | } from "@/components/ui/card"; 21 | 22 | type FeatureCardProps = { 23 | title: string; 24 | description: string; 25 | icon: React.ReactNode; 26 | className?: string; 27 | delay?: number; 28 | }; 29 | 30 | const FeatureCard = ({ 31 | title, 32 | description, 33 | icon, 34 | className, 35 | delay = 0, 36 | }: FeatureCardProps) => { 37 | return ( 38 | 44 | 50 | 51 |
{icon}
52 | {title} 53 |
54 | 55 | 56 | {description} 57 | 58 | 59 |
60 |
61 | ); 62 | }; 63 | 64 | const FeaturesGrid = () => { 65 | const features = [ 66 | { 67 | title: "Solana Components", 68 | description: 69 | "UI components built for Solana apps. Wallet connect dialogs, token comboboxes, price charts, and more.", 70 | icon: , 71 | className: "md:col-span-2", 72 | }, 73 | { 74 | title: "Asset Fetching", 75 | description: 76 | "Built-in utilities for fetching assets and prices, including Birdeye, Helius, and Umi.", 77 | icon: , 78 | className: "md:col-span-2", 79 | }, 80 | { 81 | title: "Shadcn Integration", 82 | description: 83 | "Built on top of shadcn's powerful UI library, providing a consistent design system that's easy to customize.", 84 | icon: , 85 | }, 86 | { 87 | title: "Copy & Paste", 88 | description: 89 | "No dependencies and complex customizations. Copy and paste components directly into your project.", 90 | icon: , 91 | }, 92 | { 93 | title: "Customizable", 94 | description: 95 | "Easily customize components to match your brand with Tailwind CSS utility classes.", 96 | icon: , 97 | }, 98 | { 99 | title: "Scalable", 100 | description: 101 | "Prototype to production. Start with built-in utilities and swap in your own data sources and pipelines.", 102 | icon: , 103 | }, 104 | ]; 105 | 106 | return ( 107 |
108 | 115 | Everything you need to build Solana frontends 116 | 117 |
118 | {features.map((feature, index) => ( 119 | 127 | ))} 128 |
129 |
130 | ); 131 | }; 132 | 133 | export { FeaturesGrid }; 134 | -------------------------------------------------------------------------------- /src/lib/assets/birdeye/wallet.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import { SolAsset, FetchWalletArgs } from "@/lib/types"; 3 | import { WSOL_MINT, SOL_MINT } from "@/lib/consts"; 4 | 5 | /** 6 | * Fetches all token assets for a wallet address from Birdeye API 7 | * @param args - Object containing fetch parameters 8 | * @param args.owner - Wallet address to fetch token list for 9 | * @param args.combineNativeBalance - Optional boolean to combine WSOL and native SOL 10 | * @returns Array of SolAsset objects containing token data 11 | */ 12 | const fetchWalletAssets = async ({ 13 | owner, 14 | limit = 20, 15 | combineNativeBalance = true, 16 | }: FetchWalletArgs): Promise => { 17 | const headers = { 18 | "x-api-key": process.env.NEXT_PUBLIC_BIRDEYE_API_KEY!, 19 | accept: "application/json", 20 | "x-chain": "solana", 21 | }; 22 | 23 | try { 24 | const response = await fetch( 25 | `https://public-api.birdeye.so/v1/wallet/token_list?wallet=${owner.toString()}`, 26 | { headers }, 27 | ); 28 | 29 | const { success, data } = await response.json(); 30 | 31 | if (!success || !data?.items) { 32 | return []; 33 | } 34 | 35 | let nativeSolBalance = 0; 36 | let wsolBalance = 0; 37 | let solPrice = 0; 38 | 39 | const items = data.items 40 | .filter( 41 | (item: { 42 | symbol: string; 43 | address: string; 44 | uiAmount?: number; 45 | priceUsd: number; 46 | }) => { 47 | // Filter out entries without symbol 48 | if (!item.symbol) return false; 49 | 50 | // Handle native SOL 51 | if (item.address === SOL_MINT.toString()) { 52 | nativeSolBalance = item.uiAmount || 0; 53 | solPrice = item.priceUsd; 54 | return false; 55 | } 56 | 57 | // Handle WSOL 58 | if (item.address === WSOL_MINT.toString()) { 59 | wsolBalance = item.uiAmount || 0; 60 | return !combineNativeBalance; 61 | } 62 | 63 | return true; 64 | }, 65 | ) 66 | .map( 67 | (item: { 68 | address: string; 69 | name: string; 70 | symbol: string; 71 | icon?: string; 72 | logoURI?: string; 73 | priceUsd: number; 74 | decimals: number; 75 | uiAmount: number; 76 | }) => { 77 | // Fix WSOL display name if needed 78 | const isWsol = item.address === WSOL_MINT.toString(); 79 | return { 80 | mint: new PublicKey(item.address), 81 | name: isWsol ? "Wrapped SOL" : item.name, 82 | symbol: isWsol ? "WSOL" : item.symbol, 83 | image: item.icon || item.logoURI || "", 84 | price: item.priceUsd, 85 | decimals: item.decimals, 86 | userTokenAccount: { 87 | address: new PublicKey(item.address), 88 | amount: item.uiAmount, 89 | }, 90 | }; 91 | }, 92 | ); 93 | 94 | // Always add native SOL, combine with WSOL if specified 95 | const totalSolBalance = combineNativeBalance 96 | ? nativeSolBalance + wsolBalance 97 | : nativeSolBalance; 98 | 99 | items.push({ 100 | mint: SOL_MINT, 101 | name: "Solana", 102 | symbol: "SOL", 103 | image: 104 | "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png", 105 | price: solPrice, 106 | decimals: 9, 107 | userTokenAccount: { 108 | address: SOL_MINT, 109 | amount: totalSolBalance, 110 | }, 111 | }); 112 | 113 | return items 114 | .sort((a: SolAsset, b: SolAsset) => { 115 | const aValue = (a.userTokenAccount?.amount || 0) * (a.price || 0); 116 | const bValue = (b.userTokenAccount?.amount || 0) * (b.price || 0); 117 | return bValue - aValue; 118 | }) 119 | .slice(0, limit); 120 | } catch (error) { 121 | console.error("Error fetching wallet assets:", error); 122 | return []; 123 | } 124 | }; 125 | 126 | export { fetchWalletAssets }; 127 | -------------------------------------------------------------------------------- /src/components/sol/token-list.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Link from "next/link"; 5 | import { ExternalLinkIcon } from "lucide-react"; 6 | 7 | import { shortAddress, formatUsd, formatNumberShort, cn } from "@/lib/utils"; 8 | import { SolAsset } from "@/lib/types"; 9 | 10 | import { 11 | Table, 12 | TableBody, 13 | TableCell, 14 | TableHead, 15 | TableHeader, 16 | TableRow, 17 | } from "@/components/ui/table"; 18 | import { Skeleton } from "@/components/ui/skeleton"; 19 | 20 | import { TokenIcon } from "@/components/sol/token-icon"; 21 | 22 | type TokenListProps = { 23 | assets: SolAsset[]; 24 | showBalances?: boolean; 25 | onClick?: (token: SolAsset) => void; 26 | }; 27 | 28 | const TokenList = ({ 29 | assets, 30 | showBalances = true, 31 | onClick, 32 | }: TokenListProps) => { 33 | return ( 34 | 35 | 36 | 37 | Token 38 | Mint 39 | Price 40 | {showBalances && Balance} 41 | {showBalances && Value} 42 | 43 | 44 | 45 | {assets.length === 0 ? ( 46 | <> 47 | {[...Array(3)].map((_, index) => ( 48 | 49 | {[...Array(showBalances ? 5 : 3)].map((_, index) => ( 50 | 51 | {index === 0 ? ( 52 |
53 | 54 | 55 |
56 | ) : ( 57 | 58 | )} 59 |
60 | ))} 61 |
62 | ))} 63 | 64 | ) : ( 65 | assets.map((asset) => ( 66 | onClick && onClick(asset)} 73 | > 74 | 75 |
76 | 77 | {asset.symbol} 78 |
79 |
80 | 81 | e.stopPropagation()} 87 | > 88 | 89 | {shortAddress(asset.mint.toBase58())} 90 | 91 | 92 | 93 | 94 | {formatUsd(asset.price || 0)} 95 | {showBalances && ( 96 | <> 97 | 98 | {asset.userTokenAccount?.amount && 99 | formatNumberShort(asset.userTokenAccount.amount)} 100 | 101 | 102 | {asset.userTokenAccount?.amount && 103 | formatUsd( 104 | asset.userTokenAccount.amount * (asset.price || 0), 105 | )} 106 | 107 | 108 | )} 109 |
110 | )) 111 | )} 112 |
113 |
114 | ); 115 | }; 116 | 117 | export { TokenList }; 118 | -------------------------------------------------------------------------------- /src/components/web/docs-tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; 6 | import tsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx"; 7 | import Dark from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus"; 8 | import Light from "react-syntax-highlighter/dist/esm/styles/prism/base16-ateliersulphurpool.light"; 9 | import { useTheme } from "next-themes"; 10 | 11 | import { 12 | Select, 13 | SelectContent, 14 | SelectGroup, 15 | SelectItem, 16 | SelectTrigger, 17 | SelectValue, 18 | } from "@/components/ui/select"; 19 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 20 | import { Label } from "@/components/ui/label"; 21 | import { Skeleton } from "@/components/ui/skeleton"; 22 | 23 | type DocsVariant = { 24 | value: string; 25 | label?: string; 26 | preview?: React.ReactNode; 27 | code?: string; 28 | }; 29 | 30 | type DocsTabsProps = { 31 | variants: DocsVariant[]; 32 | }; 33 | 34 | SyntaxHighlighter.registerLanguage("tsx", tsx); 35 | 36 | const DocsTabs = ({ variants }: DocsTabsProps) => { 37 | const { resolvedTheme } = useTheme(); 38 | const [activeVariantIndex, setActiveVariantIndex] = React.useState(0); 39 | const [activeVariant, setActiveVariant] = React.useState( 40 | variants[activeVariantIndex], 41 | ); 42 | const [mounted, setMounted] = React.useState(false); 43 | 44 | React.useEffect(() => { 45 | setMounted(true); 46 | }, []); 47 | 48 | React.useEffect(() => { 49 | setActiveVariant(variants[activeVariantIndex]); 50 | }, [variants, activeVariantIndex]); 51 | 52 | return ( 53 |
54 | 55 | 56 | Preview 57 | Code 58 | 59 | 60 | 61 |
62 |
63 | {variants.length > 1 && ( 64 |
65 | 66 | 88 |
89 | )} 90 |
91 | {activeVariant.preview && activeVariant.preview} 92 |
93 |
94 |
95 |
96 | 97 |
98 | {activeVariant.code && mounted ? ( 99 | 104 | {activeVariant.code} 105 | 106 | ) : ( 107 | 108 | )} 109 |
110 |
111 |
112 |
113 | ); 114 | }; 115 | 116 | export { DocsTabs }; 117 | export type { DocsVariant }; 118 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import { Cross2Icon } from "@radix-ui/react-icons"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Dialog = DialogPrimitive.Root; 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger; 12 | 13 | const DialogPortal = DialogPrimitive.Portal; 14 | 15 | const DialogClose = DialogPrimitive.Close; 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )); 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )); 54 | DialogContent.displayName = DialogPrimitive.Content.displayName; 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ); 68 | DialogHeader.displayName = "DialogHeader"; 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ); 82 | DialogFooter.displayName = "DialogFooter"; 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )); 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )); 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | }; 123 | -------------------------------------------------------------------------------- /src/components/web/code-showcase.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { motion } from "motion/react"; 6 | 7 | import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; 8 | import { Code } from "@/components/web/code"; 9 | 10 | const codeExamples = { 11 | connectWallet: `Connect Wallet} 13 | title="Connect Wallet" 14 | description="Connect your wallet to continue" 15 | />`, 16 | fetchAssets: `import { useConnection } from "@solana/wallet-adapter-react" 17 | import { fetchAssets } from "@/lib/assets" 18 | 19 | const { publicKey } = useConnection(); 20 | 21 | const assets = await fetchAssets({ 22 | addresses: [ 23 | new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), 24 | new PublicKey("EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm"), 25 | new PublicKey("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"), 26 | ], 27 | owner: publicKey, 28 | }); 29 | `, 30 | tokenCombobox: `import { fetchAssets, searchAssets } from "@/lib/assets" 31 | 32 | const assets = await fetchAssets({ 33 | addresses: [ 34 | new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), 35 | new PublicKey("EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm"), 36 | new PublicKey("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"), 37 | ], 38 | owner: publicKey, 39 | }); 40 | 41 | return ( 42 | 47 | )`, 48 | }; 49 | 50 | const CodeShowcase = () => { 51 | return ( 52 |
53 | 60 | Simple, Copy & Paste Components 61 | 62 | 69 | No dependencies and complex customizations. Simply copy and paste 70 | components directly into your project. 71 |
72 | Use the built-in asset fetching utilities, or swap out for your own data 73 | sources. 74 |
75 | 76 | 83 |
84 | 85 | 86 | 87 | ConnectWalletDialog 88 | 89 | Fetch Assets 90 | TokenCombobox 91 | 92 | 93 | 98 | 99 | 100 | 105 | 106 | 107 | 112 | 113 | 114 |
115 |
116 |
117 | ); 118 | }; 119 | 120 | export { CodeShowcase }; 121 | -------------------------------------------------------------------------------- /src/components/web/code.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { CopyToClipboard } from "react-copy-to-clipboard"; 5 | 6 | import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; 7 | import tsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx"; 8 | import shell from "react-syntax-highlighter/dist/esm/languages/prism/shell-session"; 9 | import Dark from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus"; 10 | import Light from "react-syntax-highlighter/dist/esm/styles/prism/base16-ateliersulphurpool.light"; 11 | import { 12 | IconCheck, 13 | IconCopy, 14 | IconMaximize, 15 | IconMinimize, 16 | } from "@tabler/icons-react"; 17 | import { useTheme } from "next-themes"; 18 | 19 | import { cn } from "@/lib/utils"; 20 | 21 | import { Button } from "@/components/ui/button"; 22 | import { Skeleton } from "@/components/ui/skeleton"; 23 | 24 | SyntaxHighlighter.registerLanguage("tsx", tsx); 25 | SyntaxHighlighter.registerLanguage("shell", shell); 26 | 27 | type CodeProps = { 28 | code: string; 29 | language?: "tsx" | "shell"; 30 | reveal?: boolean; 31 | className?: string; 32 | showControls?: boolean; 33 | }; 34 | 35 | const Code = ({ 36 | code, 37 | language = "tsx", 38 | reveal = true, 39 | showControls = true, 40 | className, 41 | }: CodeProps) => { 42 | const { resolvedTheme } = useTheme(); 43 | const [expanded, setExpanded] = React.useState(false); 44 | const [copied, setCopied] = React.useState(false); 45 | const [mounted, setMounted] = React.useState(false); 46 | 47 | React.useEffect(() => { 48 | setMounted(true); 49 | }, []); 50 | 51 | const handleCopy = () => { 52 | setCopied(true); 53 | setTimeout(() => setCopied(false), 2000); 54 | }; 55 | 56 | // Don't render the syntax highlighter until mounted to prevent hydration mismatch 57 | const syntaxHighlighter = mounted ? ( 58 | 63 | {code} 64 | 65 | ) : ( 66 | 67 | ); 68 | 69 | return ( 70 |
78 | {showControls && ( 79 | 88 | )} 89 |
90 | {!reveal && ( 91 |
97 | )} 98 | {syntaxHighlighter} 99 |
100 | {!reveal && ( 101 | 111 | )} 112 |
113 | ); 114 | }; 115 | 116 | type CodeControlsProps = { 117 | code: string; 118 | copied: boolean; 119 | reveal: boolean; 120 | expanded: boolean; 121 | className?: string; 122 | handleCopy: () => void; 123 | setExpanded: (expanded: boolean) => void; 124 | }; 125 | 126 | const CodeControls = ({ 127 | code, 128 | copied, 129 | reveal, 130 | expanded, 131 | className, 132 | handleCopy, 133 | setExpanded, 134 | }: CodeControlsProps) => { 135 | return ( 136 |
137 | 138 | 145 | 146 | {!reveal && ( 147 | 154 | )} 155 |
156 | ); 157 | }; 158 | 159 | export { Code }; 160 | -------------------------------------------------------------------------------- /src/lib/assets/birdeye/fetch.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js"; 2 | import { SolAsset, FetchAssetsArgs } from "@/lib/types"; 3 | import { WSOL_MINT } from "@/lib/consts"; 4 | 5 | /** 6 | * Fetches token asset data from Birdeye API for a list of token addresses 7 | * @param args - Object containing fetch parameters 8 | * @param args.addresses - Array of token mint addresses to fetch data for 9 | * @param args.owner - Optional wallet address to fetch token balances for 10 | * @param args.connection - Optional web3 connection (required if fetching SOL balance) 11 | * @param args.combineNativeBalance - Optional boolean to combine WSOL and native SOL 12 | * @returns Array of SolAsset objects containing token data 13 | * @example 14 | * const assets = await fetchAssets({ 15 | * addresses: [new PublicKey("So11111111111111111111111111111111111111112")], 16 | * owner: new PublicKey("..."), 17 | * connection: new Connection("...") 18 | * }); 19 | */ 20 | const fetchAssets = async ({ 21 | addresses, 22 | owner, 23 | connection, 24 | combineNativeBalance = true, // Default to combining WSOL and native SOL 25 | }: FetchAssetsArgs): Promise => { 26 | const fetchedAssets: SolAsset[] = []; 27 | const addressList = addresses.map((a) => a.toString()).join(","); 28 | const headers = { 29 | "x-api-key": process.env.NEXT_PUBLIC_BIRDEYE_API_KEY!, 30 | }; 31 | 32 | try { 33 | // Fetch metadata, prices, and balances (if owner provided) in parallel 34 | const fetchPromises = [ 35 | fetch( 36 | `https://public-api.birdeye.so/defi/v3/token/meta-data/multiple?list_address=${addressList}`, 37 | { 38 | headers, 39 | }, 40 | ), 41 | fetch( 42 | `https://public-api.birdeye.so/defi/multi_price?list_address=${addressList}`, 43 | { 44 | headers, 45 | }, 46 | ), 47 | ]; 48 | 49 | if (owner) { 50 | addresses.forEach((address) => { 51 | fetchPromises.push( 52 | fetch( 53 | `https://public-api.birdeye.so/v1/wallet/token_balance?wallet=${owner.toString()}&token_address=${address.toString()}`, 54 | { 55 | headers, 56 | }, 57 | ), 58 | ); 59 | }); 60 | } 61 | 62 | const responses = await Promise.all(fetchPromises); 63 | const [metadataRes, pricesRes, ...balanceResponses] = await Promise.all( 64 | responses.map((res) => res.json()), 65 | ); 66 | 67 | const metadata = metadataRes.data; 68 | const prices = pricesRes.data; 69 | 70 | // If owner and connection are provided, fetch native SOL balance 71 | let nativeSolBalance = 0; 72 | if ( 73 | owner && 74 | connection && 75 | addresses.some((addr) => addr.equals(WSOL_MINT)) && 76 | combineNativeBalance 77 | ) { 78 | try { 79 | nativeSolBalance = await connection.getBalance(owner); 80 | nativeSolBalance = nativeSolBalance / LAMPORTS_PER_SOL; 81 | } catch (error) { 82 | console.error("Error fetching native SOL balance:", error); 83 | } 84 | } 85 | 86 | for (let i = 0; i < addresses.length; i++) { 87 | const addressStr = addresses[i].toString(); 88 | const tokenData = metadata[addressStr]; 89 | const priceData = prices[addressStr]; 90 | const balanceData = owner ? balanceResponses[i]?.data : undefined; 91 | 92 | if (tokenData) { 93 | const asset: SolAsset = { 94 | mint: new PublicKey(tokenData.address), 95 | name: tokenData.name, 96 | symbol: tokenData.symbol, 97 | image: tokenData.logo_uri, 98 | price: priceData?.value || null, 99 | decimals: tokenData.decimals, 100 | userTokenAccount: balanceData 101 | ? { 102 | address: new PublicKey(balanceData.address), 103 | amount: balanceData.uiAmount, 104 | } 105 | : undefined, 106 | }; 107 | 108 | // If this is WSOL and we have a native SOL balance, add it to the WSOL balance 109 | if ( 110 | addresses[i].equals(WSOL_MINT) && 111 | nativeSolBalance > 0 && 112 | combineNativeBalance 113 | ) { 114 | if (asset.userTokenAccount) { 115 | asset.userTokenAccount.amount += nativeSolBalance; 116 | } else { 117 | asset.userTokenAccount = { 118 | address: WSOL_MINT, 119 | amount: nativeSolBalance, 120 | }; 121 | } 122 | } 123 | 124 | fetchedAssets.push(asset); 125 | } 126 | } 127 | } catch (error) { 128 | console.error("Error fetching assets:", error); 129 | return []; 130 | } 131 | 132 | return fetchedAssets; 133 | }; 134 | 135 | export { fetchAssets }; 136 | -------------------------------------------------------------------------------- /src/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SheetPrimitive from "@radix-ui/react-dialog" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | import { cn } from "@/lib/utils" 7 | import { Cross2Icon } from "@radix-ui/react-icons" 8 | 9 | const Sheet = SheetPrimitive.Root 10 | 11 | const SheetTrigger = SheetPrimitive.Trigger 12 | 13 | const SheetClose = SheetPrimitive.Close 14 | 15 | const SheetPortal = SheetPrimitive.Portal 16 | 17 | const SheetOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName 31 | 32 | const sheetVariants = cva( 33 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", 34 | { 35 | variants: { 36 | side: { 37 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 38 | bottom: 39 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 40 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 41 | right: 42 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 43 | }, 44 | }, 45 | defaultVariants: { 46 | side: "right", 47 | }, 48 | } 49 | ) 50 | 51 | interface SheetContentProps 52 | extends React.ComponentPropsWithoutRef, 53 | VariantProps {} 54 | 55 | const SheetContent = React.forwardRef< 56 | React.ElementRef, 57 | SheetContentProps 58 | >(({ side = "right", className, children, ...props }, ref) => ( 59 | 60 | 61 | 66 | 67 | 68 | Close 69 | 70 | {children} 71 | 72 | 73 | )) 74 | SheetContent.displayName = SheetPrimitive.Content.displayName 75 | 76 | const SheetHeader = ({ 77 | className, 78 | ...props 79 | }: React.HTMLAttributes) => ( 80 |
87 | ) 88 | SheetHeader.displayName = "SheetHeader" 89 | 90 | const SheetFooter = ({ 91 | className, 92 | ...props 93 | }: React.HTMLAttributes) => ( 94 |
101 | ) 102 | SheetFooter.displayName = "SheetFooter" 103 | 104 | const SheetTitle = React.forwardRef< 105 | React.ElementRef, 106 | React.ComponentPropsWithoutRef 107 | >(({ className, ...props }, ref) => ( 108 | 113 | )) 114 | SheetTitle.displayName = SheetPrimitive.Title.displayName 115 | 116 | const SheetDescription = React.forwardRef< 117 | React.ElementRef, 118 | React.ComponentPropsWithoutRef 119 | >(({ className, ...props }, ref) => ( 120 | 125 | )) 126 | SheetDescription.displayName = SheetPrimitive.Description.displayName 127 | 128 | export { 129 | Sheet, 130 | SheetPortal, 131 | SheetOverlay, 132 | SheetTrigger, 133 | SheetClose, 134 | SheetContent, 135 | SheetHeader, 136 | SheetFooter, 137 | SheetTitle, 138 | SheetDescription, 139 | } 140 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | import { PublicKey } from "@solana/web3.js"; 4 | import millify from "millify"; 5 | 6 | /** 7 | * Combines Tailwind CSS classes with proper precedence 8 | * @param inputs - Array of class values to be merged 9 | * @returns Merged class string with proper Tailwind precedence 10 | */ 11 | export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); 12 | 13 | /** 14 | * Formats a number with appropriate decimal places based on its magnitude 15 | * @param num - Number to format 16 | * @param options - Optional Intl.NumberFormat options 17 | * @returns Formatted number string with dynamic decimal places 18 | * @example 19 | * formatNumber(1234.5678) // "1,234.57" 20 | * formatNumber(0.000123) // "0.000123" 21 | */ 22 | export const formatNumber = ( 23 | num: number, 24 | options: Intl.NumberFormatOptions = {}, 25 | ): string => { 26 | if (num === null || num === undefined) return "0"; 27 | 28 | const absNum = Math.abs(num); 29 | let decimals = 2; 30 | 31 | if (absNum < 1) { 32 | decimals = Math.max(2, Math.min(20, Math.ceil(-Math.log10(absNum)) + 2)); 33 | } 34 | 35 | return new Intl.NumberFormat("en-US", { 36 | minimumFractionDigits: 2, 37 | maximumFractionDigits: decimals, 38 | ...options, 39 | }).format(num); 40 | }; 41 | 42 | /** 43 | * Formats a number into a compact representation (K, M, B, etc.) 44 | * @param num - Number to format 45 | * @returns Shortened number string with appropriate suffix 46 | * @example 47 | * formatNumberShort(1234) // "1.23K" 48 | * formatNumberShort(1234567) // "1.23M" 49 | */ 50 | export const formatNumberShort = (num: number): string => { 51 | if (num < 1000) return formatNumber(num); 52 | return millify(num, { 53 | precision: 2, 54 | }); 55 | }; 56 | 57 | /** 58 | * Formats a number with grouped digits and optional exponential notation 59 | * @param value - Number to format 60 | * @param maxDecimals - Maximum number of decimal places to use in the formatted number (default: 2) 61 | * @param expThreshold - Threshold below which to use exponential notation (default: 0.0001) 62 | * @param expPrecision - Number of decimal places in exponential notation (default: 1) 63 | * @returns Formatted number string 64 | * @example 65 | * formatNumberGrouped(1234.5678) // "1,234.57" 66 | * formatNumberGrouped(1234.5678, 3) // "1,234.568" 67 | * formatNumberGrouped(0.0000123, 2, 0.0001) // "1.23e-5" 68 | */ 69 | export const formatNumberGrouped = ( 70 | value: number, 71 | maxDecimals: number = 2, 72 | expThreshold: number = 0.0001, 73 | expPrecision: number = 1, 74 | ) => { 75 | if (value === 0) return "0"; 76 | 77 | if (Math.abs(value) < expThreshold) { 78 | return value.toExponential(expPrecision); 79 | } 80 | 81 | if (Number.isInteger(value)) { 82 | return new Intl.NumberFormat("en-US", { useGrouping: true }).format(value); 83 | } 84 | 85 | const valueParts = value.toString().split("."); 86 | const decimalPart = valueParts[1] ?? ""; 87 | const leadingZeros = decimalPart.match(/^0*/)?.[0].length ?? 0; 88 | const minimumFractionDigits = 89 | leadingZeros > 0 ? leadingZeros + 1 : maxDecimals; 90 | 91 | return new Intl.NumberFormat("en-US", { 92 | useGrouping: true, 93 | minimumFractionDigits: minimumFractionDigits, 94 | maximumFractionDigits: Math.max(maxDecimals, minimumFractionDigits), 95 | }).format(value); 96 | }; 97 | 98 | /** 99 | * Formats a number as USD currency 100 | * @param num - Number to format as USD 101 | * @returns Formatted USD string 102 | * @example 103 | * formatUsd(1234.56) // "$1,234.56" 104 | */ 105 | export const formatUsd = (num: number): string => { 106 | return formatNumber(num, { style: "currency", currency: "USD" }); 107 | }; 108 | 109 | /** 110 | * Shortens a Solana public key or address string to a readable format 111 | * @param address - PublicKey object or base58 string to shorten 112 | * @returns Shortened address string (e.g., "Ax12...3456") 113 | * @example 114 | * shortAddress("AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQq") // "AaBb...PpQq" 115 | */ 116 | export const shortAddress = (address: PublicKey | string) => { 117 | const key = typeof address === "string" ? address : address.toBase58(); 118 | return `${key.slice(0, 4)}...${key.slice(-4)}`; 119 | }; 120 | 121 | /** 122 | * Validates if a string is a valid Solana public key 123 | * @param address - PublicKey object or base58 string to validate 124 | * @returns Boolean indicating if the string is a valid public key 125 | * @example 126 | * validatePublicKey("invalid") // false 127 | * validatePublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") // true 128 | * validatePublicKey(new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")) // true 129 | */ 130 | export const validatePublicKey = (address: PublicKey | string) => { 131 | try { 132 | if (typeof address === "string") { 133 | new PublicKey(address); 134 | } else { 135 | // Verify the PublicKey is valid by accessing its public methods 136 | address.toBase58(); 137 | } 138 | return true; 139 | } catch (error) { 140 | return false; 141 | } 142 | }; 143 | -------------------------------------------------------------------------------- /src/app/docs/components/sparkline/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import Link from "next/link"; 6 | 7 | import { PublicKey } from "@solana/web3.js"; 8 | import { IconInfoCircle } from "@tabler/icons-react"; 9 | 10 | import { fetchPriceHistoryBirdeye } from "@/lib/prices/birdeye"; 11 | 12 | 13 | import { DocsTabs, DocsVariant } from "@/components/web/docs-tabs"; 14 | import { DocsWrapper } from "@/components/web/docs-wrapper"; 15 | import { DocsH1, DocsH2 } from "@/components/web/docs-heading"; 16 | import { PropsTable } from "@/components/web/props-table"; 17 | import { Code } from "@/components/web/code"; 18 | import { DocsInstallTabs } from "@/components/web/docs-install-tabs"; 19 | 20 | import { Alert, AlertTitle } from "@/components/ui/alert"; 21 | 22 | import { Sparkline } from "@/components/sol/sparkline"; 23 | 24 | export default function SparklinePage() { 25 | const [chartData, setChartData] = React.useState< 26 | { 27 | timestamp: number; 28 | price: number; 29 | }[] 30 | >([]); 31 | const [isFetching, setIsFetching] = React.useState(false); 32 | const [componentSource, setComponentSource] = React.useState(""); 33 | const [priceChangeSource, setPriceChangeSource] = React.useState(""); 34 | 35 | const fetchChartData = React.useCallback(async () => { 36 | if (isFetching) return; 37 | 38 | try { 39 | setIsFetching(true); 40 | const data = await fetchPriceHistoryBirdeye( 41 | new PublicKey("ED5nyyWEzpPPiWimP8vYm7sD7TD3LAt3Q3gRTWHzPJBY"), 42 | 1729497600, 43 | 1730073600, 44 | "1H", 45 | ); 46 | if (data) setChartData(data); 47 | } finally { 48 | setIsFetching(false); 49 | } 50 | }, [isFetching]); 51 | 52 | React.useEffect(() => { 53 | if (chartData.length === 0 && !isFetching) { 54 | fetchChartData(); 55 | } 56 | }, [fetchChartData, chartData.length, isFetching]); 57 | 58 | React.useEffect(() => { 59 | fetch("/generated/component-sources/sparkline.tsx.txt") 60 | .then((res) => res.text()) 61 | .then(setComponentSource); 62 | fetch("/generated/component-sources/price-change.tsx.txt") 63 | .then((res) => res.text()) 64 | .then(setPriceChangeSource); 65 | }, []); 66 | 67 | const variants: DocsVariant[] = [ 68 | { 69 | label: "Default", 70 | value: "default", 71 | preview: ( 72 |
73 | 74 |
75 | ), 76 | code: `import { Sparkline } from "@/components/sol/sparkline" 77 | 78 | export function SparklineDemo() { 79 | return ( 80 | 81 | ) 82 | }`, 83 | }, 84 | ]; 85 | 86 | return ( 87 | 88 |
89 | Sparkline 90 |

91 | The Sparkline component is a line chart that displays the price of a 92 | token over time. 93 |

94 | 95 |
96 | 97 | Installation 98 | 99 | 100 | 101 | 102 |

1. Install SolanaUI Sparkline

103 |

104 | Copy the code below to src/components/sol/sparkline.tsx 105 | . 106 |

107 | 108 | 109 |

2. Install SolanaUI PriceChange

110 |

111 | The Sparkline component requires the PriceChange{" "} 112 | component. Copy the code below to{" "} 113 | src/components/sol/price-change.tsx. 114 |

115 | 116 | 117 |

3. Use Sparkline

118 |

119 | Import the Sparkline component and use it in your app. 120 |

121 | 122 | 123 | 124 | SolanaUI provides utilities to help with fetching historic price 125 | data. Learn more. 126 | 127 | 128 | `} /> 129 | 130 |
131 | 132 | Props 133 | 134 | 146 |
147 |
148 |
149 |
150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /src/lib/assets/birdeye/search.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js"; 2 | import { SolAsset, SearchAssetsArgs } from "@/lib/types"; 3 | import { WSOL_MINT } from "@/lib/consts"; 4 | 5 | /** 6 | * Searches for token assets using Birdeye API 7 | * @param params - Search parameters 8 | * @param params.query - Search query string 9 | * @param params.owner - Optional wallet address to fetch token balances for 10 | * @param params.connection - Optional web3 connection (required if fetching SOL balance) 11 | * @param params.combineNativeBalance - Optional boolean to combine native SOL balance with WSOL balance 12 | * @returns Array of SolAsset objects matching the search query 13 | * @example 14 | * const searchResults = await searchAssets({ 15 | * query: "SOL", 16 | * owner: new PublicKey("..."), 17 | * connection: new Connection("...") 18 | * }); 19 | */ 20 | const searchAssets = async ({ 21 | query, 22 | owner, 23 | connection, 24 | combineNativeBalance = true, 25 | }: SearchAssetsArgs): Promise => { 26 | const headers = { 27 | "x-api-key": process.env.NEXT_PUBLIC_BIRDEYE_API_KEY!, 28 | }; 29 | 30 | const params = new URLSearchParams({ 31 | chain: "solana", 32 | target: "token", 33 | sort_by: "liquidity", 34 | sort_type: "desc", 35 | offset: "0", 36 | limit: "10", 37 | keyword: query, 38 | }); 39 | 40 | try { 41 | const searchResponse = await fetch( 42 | `https://public-api.birdeye.so/defi/v3/search?${params.toString()}`, 43 | { 44 | headers, 45 | }, 46 | ); 47 | 48 | const searchResults = await searchResponse.json(); 49 | const results = searchResults.data.items[0].result.filter( 50 | (result: { symbol: string; address: string }) => { 51 | // Keep WSOL 52 | if (result.address === WSOL_MINT.toString()) { 53 | return true; 54 | } 55 | 56 | // Filter out native SOL and exact SOL symbol matches 57 | if (result.symbol === "SOL" && combineNativeBalance) { 58 | return false; 59 | } 60 | 61 | // Keep all other tokens 62 | return result.symbol; 63 | }, 64 | ); 65 | 66 | // If owner is provided, fetch balances for each token 67 | let balanceData: Record = {}; 68 | if (owner) { 69 | const balancePromises = results.map((result: { address: string }) => 70 | fetch( 71 | `https://public-api.birdeye.so/v1/wallet/token_balance?wallet=${owner.toString()}&token_address=${result.address}`, 72 | { 73 | headers, 74 | }, 75 | ).then((res) => res.json()), 76 | ); 77 | 78 | const balanceResponses = await Promise.all(balancePromises); 79 | balanceData = balanceResponses.reduce( 80 | ( 81 | acc: Record, 82 | response, 83 | index, 84 | ) => { 85 | acc[results[index].address] = response.data; 86 | return acc; 87 | }, 88 | {}, 89 | ); 90 | } 91 | 92 | // If owner and connection are provided, fetch native SOL balance 93 | let nativeSolBalance = 0; 94 | const wsolResult = results.find( 95 | (result: { address: string }) => result.address === WSOL_MINT.toString(), 96 | ); 97 | 98 | if (owner && connection && wsolResult && combineNativeBalance) { 99 | try { 100 | nativeSolBalance = await connection.getBalance(owner); 101 | nativeSolBalance = nativeSolBalance / LAMPORTS_PER_SOL; 102 | } catch (error) { 103 | console.error("Error fetching native SOL balance:", error); 104 | } 105 | } 106 | 107 | return results.map( 108 | (result: { 109 | address: string; 110 | name: string; 111 | symbol: string; 112 | logo_uri: string; 113 | price: number; 114 | decimals: number; 115 | }) => { 116 | const asset: SolAsset = { 117 | mint: new PublicKey(result.address), 118 | name: result.name, 119 | symbol: result.symbol, 120 | image: result.logo_uri, 121 | price: result.price, 122 | decimals: result.decimals, 123 | userTokenAccount: 124 | owner && balanceData[result.address] 125 | ? { 126 | address: new PublicKey(balanceData[result.address].address), 127 | amount: balanceData[result.address].uiAmount, 128 | } 129 | : undefined, 130 | }; 131 | 132 | // If this is WSOL and we have a native SOL balance, add it to the WSOL balance 133 | if ( 134 | result.address === WSOL_MINT.toString() && 135 | nativeSolBalance > 0 && 136 | combineNativeBalance 137 | ) { 138 | if (asset.userTokenAccount) { 139 | asset.userTokenAccount.amount += nativeSolBalance; 140 | } else { 141 | asset.userTokenAccount = { 142 | address: WSOL_MINT, 143 | amount: nativeSolBalance, 144 | }; 145 | } 146 | } 147 | 148 | return asset; 149 | }, 150 | ); 151 | } catch (error) { 152 | console.error("Error searching assets:", error); 153 | return []; 154 | } 155 | }; 156 | 157 | export { searchAssets }; 158 | -------------------------------------------------------------------------------- /src/components/ui/toast.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Cross2Icon } from "@radix-ui/react-icons" 5 | import * as ToastPrimitives from "@radix-ui/react-toast" 6 | import { cva, type VariantProps } from "class-variance-authority" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | const ToastProvider = ToastPrimitives.Provider 11 | 12 | const ToastViewport = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, ...props }, ref) => ( 16 | 24 | )) 25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName 26 | 27 | const toastVariants = cva( 28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", 29 | { 30 | variants: { 31 | variant: { 32 | default: "border bg-background text-foreground", 33 | destructive: 34 | "destructive group border-destructive bg-destructive text-destructive-foreground", 35 | }, 36 | }, 37 | defaultVariants: { 38 | variant: "default", 39 | }, 40 | } 41 | ) 42 | 43 | const Toast = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef & 46 | VariantProps 47 | >(({ className, variant, ...props }, ref) => { 48 | return ( 49 | 54 | ) 55 | }) 56 | Toast.displayName = ToastPrimitives.Root.displayName 57 | 58 | const ToastAction = React.forwardRef< 59 | React.ElementRef, 60 | React.ComponentPropsWithoutRef 61 | >(({ className, ...props }, ref) => ( 62 | 70 | )) 71 | ToastAction.displayName = ToastPrimitives.Action.displayName 72 | 73 | const ToastClose = React.forwardRef< 74 | React.ElementRef, 75 | React.ComponentPropsWithoutRef 76 | >(({ className, ...props }, ref) => ( 77 | 86 | 87 | 88 | )) 89 | ToastClose.displayName = ToastPrimitives.Close.displayName 90 | 91 | const ToastTitle = React.forwardRef< 92 | React.ElementRef, 93 | React.ComponentPropsWithoutRef 94 | >(({ className, ...props }, ref) => ( 95 | 100 | )) 101 | ToastTitle.displayName = ToastPrimitives.Title.displayName 102 | 103 | const ToastDescription = React.forwardRef< 104 | React.ElementRef, 105 | React.ComponentPropsWithoutRef 106 | >(({ className, ...props }, ref) => ( 107 | 112 | )) 113 | ToastDescription.displayName = ToastPrimitives.Description.displayName 114 | 115 | type ToastProps = React.ComponentPropsWithoutRef 116 | 117 | type ToastActionElement = React.ReactElement 118 | 119 | export { 120 | type ToastProps, 121 | type ToastActionElement, 122 | ToastProvider, 123 | ToastViewport, 124 | Toast, 125 | ToastTitle, 126 | ToastDescription, 127 | ToastClose, 128 | ToastAction, 129 | } 130 | -------------------------------------------------------------------------------- /src/lib/assets/helius/fetch.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js"; 2 | import { SolAsset, FetchAssetsArgs } from "@/lib/types"; 3 | import { WSOL_MINT } from "@/lib/consts"; 4 | 5 | /** 6 | * Fetches token asset data from Helius API for a list of token addresses 7 | * Includes metadata, balances (if owner provided), and native SOL balance for WSOL 8 | * @param args - Object containing fetch parameters 9 | * @param args.addresses - Array of token mint addresses to fetch data for 10 | * @param args.owner - Optional wallet address to fetch token balances for 11 | * @param args.connection - Optional web3 connection (required if fetching SOL balance) 12 | * @param args.combineNativeBalance - Optional boolean to combine WSOL and native SOL 13 | * @returns Array of SolAsset objects containing token data 14 | * @example 15 | * const assets = await fetchAssets({ 16 | * addresses: [new PublicKey("So11111111111111111111111111111111111111112")], 17 | * owner: new PublicKey("..."), 18 | * connection: new Connection("...") 19 | * }); 20 | */ 21 | const fetchAssets = async ({ 22 | addresses, 23 | owner, 24 | connection, 25 | combineNativeBalance = true, // Default to combining WSOL and native SOL 26 | }: FetchAssetsArgs): Promise => { 27 | const fetchedAssets: SolAsset[] = []; 28 | 29 | try { 30 | const metadataResponse = await fetch( 31 | `https://mainnet.helius-rpc.com/?api-key=${process.env.NEXT_PUBLIC_HELIUS_API_KEY}`, 32 | { 33 | method: "POST", 34 | headers: { 35 | "Content-Type": "application/json", 36 | }, 37 | body: JSON.stringify({ 38 | id: "test", 39 | jsonrpc: "2.0", 40 | method: "getAssetBatch", 41 | params: { 42 | ids: addresses.map((a) => a.toString()), 43 | options: { 44 | showFungible: true, 45 | }, 46 | }, 47 | }), 48 | }, 49 | ); 50 | 51 | const metadataData = await metadataResponse.json(); 52 | 53 | if (!metadataData || !metadataData.result || metadataData.error) { 54 | console.error("Error fetching assets:", metadataData.error); 55 | return []; 56 | } 57 | 58 | // If owner and connection are provided, fetch native SOL balance 59 | let nativeSolBalance = 0; 60 | if ( 61 | owner && 62 | connection && 63 | addresses.some((addr) => addr.equals(WSOL_MINT)) && 64 | combineNativeBalance 65 | ) { 66 | try { 67 | nativeSolBalance = await connection.getBalance(owner); 68 | nativeSolBalance = nativeSolBalance / LAMPORTS_PER_SOL; 69 | } catch (error) { 70 | console.error("Error fetching native SOL balance:", error); 71 | } 72 | } 73 | 74 | // If owner is provided, fetch token balances individually 75 | const balances: Record = {}; 76 | if (owner) { 77 | const balancePromises = addresses.map((address) => 78 | fetch( 79 | `https://mainnet.helius-rpc.com/?api-key=${process.env.NEXT_PUBLIC_HELIUS_API_KEY}`, 80 | { 81 | method: "POST", 82 | headers: { 83 | "Content-Type": "application/json", 84 | }, 85 | body: JSON.stringify({ 86 | jsonrpc: "2.0", 87 | id: "my-id", 88 | method: "getTokenAccounts", 89 | params: { 90 | owner: owner.toString(), 91 | mint: address.toString(), 92 | }, 93 | }), 94 | }, 95 | ).then((res) => res.json()), 96 | ); 97 | 98 | const balanceResults = await Promise.all(balancePromises); 99 | 100 | balanceResults.forEach((result, index) => { 101 | if (result.result && result.result.token_accounts.length > 0) { 102 | const mint = addresses[index].toString(); 103 | const tokenAccount = result.result.token_accounts[0]; 104 | const assetData = metadataData.result.find( 105 | (asset: { id: string }) => asset.id === mint, 106 | ); 107 | if (assetData) { 108 | balances[mint] = 109 | Number(tokenAccount.amount) / 110 | Math.pow(10, assetData.token_info.decimals); 111 | } 112 | } 113 | }); 114 | } 115 | 116 | for (const asset of metadataData.result) { 117 | const isWsol = asset.id === WSOL_MINT.toString(); 118 | const assetBalance = balances[asset.id] || 0; 119 | 120 | // Add native SOL balance to WSOL if applicable 121 | const totalBalance = 122 | isWsol && combineNativeBalance 123 | ? assetBalance + nativeSolBalance 124 | : assetBalance; 125 | 126 | fetchedAssets.push({ 127 | mint: new PublicKey(asset.id), 128 | name: asset.content.metadata.name, 129 | symbol: asset.content.metadata.symbol, 130 | image: asset.content.files[0].cdn_uri || asset.content.files[0].uri, 131 | price: asset.token_info.price_info.price_per_token, 132 | decimals: asset.token_info.decimals, 133 | userTokenAccount: owner 134 | ? { 135 | address: new PublicKey(asset.id), 136 | amount: totalBalance, 137 | } 138 | : undefined, 139 | }); 140 | } 141 | 142 | return fetchedAssets; 143 | } catch (error) { 144 | console.error("Error fetching assets:", error); 145 | return []; 146 | } 147 | }; 148 | 149 | export { fetchAssets }; 150 | -------------------------------------------------------------------------------- /src/lib/assets/helius/wallet.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, Connection, LAMPORTS_PER_SOL } from "@solana/web3.js"; 2 | import { SolAsset, FetchWalletArgs } from "@/lib/types"; 3 | import { SOL_MINT, WSOL_MINT } from "@/lib/consts"; 4 | 5 | /** 6 | * Fetches all token assets for a wallet address from Helius API 7 | * Includes metadata, balances, and native SOL balance 8 | * @param args - Object containing fetch parameters 9 | * @param args.owner - Wallet address to fetch token list for 10 | * @param args.connection - Optional web3 connection (required if fetching SOL balance) 11 | * @param args.combineNativeBalance - Optional boolean to combine WSOL and native SOL 12 | * @returns Array of SolAsset objects containing token data 13 | * @example 14 | * const assets = await fetchWalletAssets({ 15 | * owner: new PublicKey("...") 16 | * }); 17 | */ 18 | const fetchWalletAssets = async ({ 19 | owner, 20 | connection, 21 | limit = 20, 22 | combineNativeBalance = true, 23 | }: FetchWalletArgs & { connection?: Connection }): Promise => { 24 | const fetchedAssets: SolAsset[] = []; 25 | 26 | try { 27 | const response = await fetch( 28 | `https://mainnet.helius-rpc.com/?api-key=${process.env.NEXT_PUBLIC_HELIUS_API_KEY}`, 29 | { 30 | method: "POST", 31 | headers: { 32 | "Content-Type": "application/json", 33 | }, 34 | body: JSON.stringify({ 35 | jsonrpc: "2.0", 36 | id: "text", 37 | method: "getAssetsByOwner", 38 | params: { 39 | ownerAddress: owner.toString(), 40 | page: 1, 41 | limit: 1000, 42 | options: { 43 | showFungible: true, 44 | showNativeBalance: true, 45 | }, 46 | }, 47 | }), 48 | }, 49 | ); 50 | 51 | const data = await response.json(); 52 | 53 | if (!data || !data.result || data.error) { 54 | console.error("Error fetching assets:", data.error); 55 | return []; 56 | } 57 | 58 | let nativeSolBalance = 0; 59 | let wsolBalance = 0; 60 | let solPrice = 0; 61 | 62 | // Get native SOL balance from connection if provided 63 | if (connection) { 64 | try { 65 | nativeSolBalance = await connection.getBalance(owner); 66 | nativeSolBalance = nativeSolBalance / LAMPORTS_PER_SOL; 67 | } catch (error) { 68 | console.error("Error fetching native SOL balance:", error); 69 | } 70 | } 71 | 72 | // Get SOL price from Helius response 73 | if (data.result.nativeBalance) { 74 | solPrice = data.result.nativeBalance.price_per_sol; 75 | } 76 | 77 | for (const asset of data.result.items) { 78 | if (!asset.token_info) continue; 79 | 80 | // Handle WSOL separately 81 | if (asset.id === WSOL_MINT.toBase58()) { 82 | wsolBalance = 83 | Number(asset.token_info.balance || 0) / 84 | Math.pow(10, asset.token_info.decimals); 85 | // Only include WSOL if not combining with native SOL 86 | if (!combineNativeBalance) { 87 | fetchedAssets.push({ 88 | mint: WSOL_MINT, 89 | name: "Wrapped SOL", 90 | symbol: "WSOL", 91 | image: 92 | "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png", 93 | price: asset.token_info.price_info?.price_per_token, 94 | decimals: asset.token_info.decimals, 95 | userTokenAccount: { 96 | address: WSOL_MINT, 97 | amount: wsolBalance, 98 | }, 99 | }); 100 | } 101 | continue; 102 | } 103 | 104 | const tokenBalance = 105 | Number(asset.token_info.balance || 0) / 106 | Math.pow(10, asset.token_info.decimals); 107 | 108 | fetchedAssets.push({ 109 | mint: new PublicKey(asset.id), 110 | name: asset.content.metadata.name, 111 | symbol: asset.content.metadata.symbol, 112 | image: asset.content.files?.[0]?.uri, 113 | price: asset.token_info.price_info?.price_per_token, 114 | decimals: asset.token_info.decimals, 115 | userTokenAccount: { 116 | address: new PublicKey(asset.id), 117 | amount: tokenBalance, 118 | }, 119 | }); 120 | } 121 | 122 | // Always add native SOL, combine with WSOL if specified 123 | const totalSolBalance = combineNativeBalance 124 | ? nativeSolBalance + wsolBalance 125 | : nativeSolBalance; 126 | 127 | fetchedAssets.push({ 128 | mint: SOL_MINT, 129 | name: "Solana", 130 | symbol: "SOL", 131 | image: 132 | "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png", 133 | price: solPrice, 134 | decimals: 9, 135 | userTokenAccount: { 136 | address: SOL_MINT, 137 | amount: totalSolBalance, 138 | }, 139 | }); 140 | 141 | // Sort assets by USD value 142 | return fetchedAssets 143 | .sort((a, b) => { 144 | const aValue = (a.userTokenAccount?.amount || 0) * (a.price || 0); 145 | const bValue = (b.userTokenAccount?.amount || 0) * (b.price || 0); 146 | return bValue - aValue; 147 | }) 148 | .slice(0, limit); 149 | } catch (error) { 150 | console.error("Error fetching wallet assets:", error); 151 | return []; 152 | } 153 | }; 154 | 155 | export { fetchWalletAssets }; 156 | -------------------------------------------------------------------------------- /src/components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { type DialogProps } from "@radix-ui/react-dialog" 5 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons" 6 | import { Command as CommandPrimitive } from "cmdk" 7 | 8 | import { cn } from "@/lib/utils" 9 | import { Dialog, DialogContent } from "@/components/ui/dialog" 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )) 24 | Command.displayName = CommandPrimitive.displayName 25 | 26 | interface CommandDialogProps extends DialogProps {} 27 | 28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => { 29 | return ( 30 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | ) 38 | } 39 | 40 | const CommandInput = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 |
45 | 46 | 54 |
55 | )) 56 | 57 | CommandInput.displayName = CommandPrimitive.Input.displayName 58 | 59 | const CommandList = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, ...props }, ref) => ( 63 | 68 | )) 69 | 70 | CommandList.displayName = CommandPrimitive.List.displayName 71 | 72 | const CommandEmpty = React.forwardRef< 73 | React.ElementRef, 74 | React.ComponentPropsWithoutRef 75 | >((props, ref) => ( 76 | 81 | )) 82 | 83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 84 | 85 | const CommandGroup = React.forwardRef< 86 | React.ElementRef, 87 | React.ComponentPropsWithoutRef 88 | >(({ className, ...props }, ref) => ( 89 | 97 | )) 98 | 99 | CommandGroup.displayName = CommandPrimitive.Group.displayName 100 | 101 | const CommandSeparator = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 112 | 113 | const CommandItem = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 125 | )) 126 | 127 | CommandItem.displayName = CommandPrimitive.Item.displayName 128 | 129 | const CommandShortcut = ({ 130 | className, 131 | ...props 132 | }: React.HTMLAttributes) => { 133 | return ( 134 | 141 | ) 142 | } 143 | CommandShortcut.displayName = "CommandShortcut" 144 | 145 | export { 146 | Command, 147 | CommandDialog, 148 | CommandInput, 149 | CommandList, 150 | CommandEmpty, 151 | CommandGroup, 152 | CommandItem, 153 | CommandShortcut, 154 | CommandSeparator, 155 | } 156 | -------------------------------------------------------------------------------- /src/components/sol/txn-list.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Link from "next/link"; 5 | 6 | import { 7 | VersionedTransactionResponse, 8 | LAMPORTS_PER_SOL, 9 | } from "@solana/web3.js"; 10 | import { useConnection } from "@solana/wallet-adapter-react"; 11 | import { formatDistanceToNow } from "date-fns"; 12 | import { AlertCircleIcon, ExternalLinkIcon } from "lucide-react"; 13 | 14 | import { shortAddress, cn } from "@/lib/utils"; 15 | 16 | import { 17 | Table, 18 | TableBody, 19 | TableCell, 20 | TableHead, 21 | TableHeader, 22 | TableRow, 23 | } from "@/components/ui/table"; 24 | import { Skeleton } from "@/components/ui/skeleton"; 25 | 26 | type TxnListProps = { 27 | transactions: VersionedTransactionResponse[]; 28 | onClick?: (txn: VersionedTransactionResponse) => void; 29 | }; 30 | 31 | const TxnList = ({ transactions, onClick }: TxnListProps) => { 32 | const { connection } = useConnection(); 33 | const [currentSlot, setCurrentSlot] = React.useState(null); 34 | const [averageBlockTime, setAverageBlockTime] = React.useState(0.4); 35 | 36 | React.useEffect(() => { 37 | const init = async () => { 38 | try { 39 | const [slot, recentPerformanceSamples] = await Promise.all([ 40 | connection.getSlot(), 41 | connection.getRecentPerformanceSamples(30), 42 | ]); 43 | 44 | const totalSampleSeconds = recentPerformanceSamples.reduce( 45 | (acc, sample) => acc + sample.samplePeriodSecs, 46 | 0, 47 | ); 48 | const totalSamples = recentPerformanceSamples.reduce( 49 | (acc, sample) => acc + sample.numSlots, 50 | 0, 51 | ); 52 | const calculatedAverageBlockTime = totalSampleSeconds / totalSamples; 53 | 54 | setCurrentSlot(slot); 55 | setAverageBlockTime(calculatedAverageBlockTime); 56 | } catch (error) { 57 | console.error("Error fetching block time:", error); 58 | } 59 | }; 60 | 61 | if (!connection) return; 62 | init(); 63 | }, [connection]); 64 | 65 | const estimateTimestamp = (blockTime: number | null | undefined) => { 66 | if (blockTime === null || blockTime === undefined || currentSlot === null) { 67 | return "Unknown"; 68 | } 69 | const currentTime = Date.now() / 1000; 70 | const blockDifference = currentSlot - blockTime; 71 | const estimatedTimestamp = currentTime - blockDifference * averageBlockTime; 72 | return formatDistanceToNow(new Date(estimatedTimestamp * 1000), { 73 | addSuffix: true, 74 | }); 75 | }; 76 | 77 | return ( 78 | 79 | 80 | 81 | Signature 82 | Block 83 | Time 84 | By 85 | Fee 86 | 87 | 88 | 89 | {transactions.length === 0 ? ( 90 | <> 91 | {[...Array(5)].map((_, index) => ( 92 | 93 | {[...Array(5)].map((_, index) => ( 94 | 95 | 96 | 97 | ))} 98 | 99 | ))} 100 | 101 | ) : ( 102 | transactions.map((txn) => ( 103 | onClick && onClick(txn)} 110 | > 111 | 112 | e.stopPropagation()} 118 | > 119 | 120 | 121 | {shortAddress(txn.transaction.signatures[0])} 122 | 123 | {txn.meta?.err && ( 124 | 125 | )} 126 | 127 | 128 | {txn.blockTime} 129 | {estimateTimestamp(txn.slot)} 130 | 131 | e.stopPropagation()} 137 | > 138 | 139 | 140 | {shortAddress(txn.transaction.message.staticAccountKeys[0])} 141 | 142 | 143 | 144 | {(txn.meta?.fee || 0) / LAMPORTS_PER_SOL} 145 | 146 | )) 147 | )} 148 | 149 |
150 | ); 151 | }; 152 | 153 | export { TxnList }; 154 | -------------------------------------------------------------------------------- /src/components/sol/user-dropdown.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { PublicKey } from "@solana/web3.js"; 5 | import { useConnection, useWallet } from "@solana/wallet-adapter-react"; 6 | import { getPrimaryDomain } from "@bonfida/spl-name-service"; 7 | import { CopyIcon, CheckIcon } from "lucide-react"; 8 | import { CopyToClipboard } from "react-copy-to-clipboard"; 9 | 10 | import { formatNumber, formatUsd, shortAddress } from "@/lib/utils"; 11 | import { SolAsset } from "@/lib/types"; 12 | 13 | import { 14 | Popover, 15 | PopoverContent, 16 | PopoverTrigger, 17 | } from "@/components/ui/popover"; 18 | import { Button } from "@/components/ui/button"; 19 | import { Skeleton } from "@/components/ui/skeleton"; 20 | 21 | import { Avatar } from "@/components/sol/avatar"; 22 | import { TokenIcon } from "@/components/sol/token-icon"; 23 | 24 | type UserDropdownProps = { 25 | address: PublicKey | null; 26 | assets?: SolAsset[]; 27 | size?: number; 28 | }; 29 | 30 | const UserDropdown = ({ 31 | address, 32 | assets = [], 33 | size = 42, 34 | }: UserDropdownProps) => { 35 | const { connected, disconnect } = useWallet(); 36 | const { connection } = useConnection(); 37 | const [isOpen, setIsOpen] = React.useState(false); 38 | const [isCopied, setIsCopied] = React.useState(false); 39 | const [domain, setDomain] = React.useState(null); 40 | 41 | const totalBalance = React.useMemo(() => { 42 | return assets.reduce( 43 | (acc, asset) => 44 | acc + (asset.userTokenAccount?.amount || 0) * (asset.price || 0), 45 | 0, 46 | ); 47 | }, [assets]); 48 | 49 | const fetchDomain = React.useCallback(async () => { 50 | if (!connection || !address) return; 51 | try { 52 | const { reverse } = await getPrimaryDomain(connection, address); 53 | setDomain(`${reverse}.sol`); 54 | } catch (error) { 55 | setDomain(null); 56 | } 57 | }, [connection, address]); 58 | 59 | React.useEffect(() => { 60 | if (domain) return; 61 | fetchDomain(); 62 | }, [fetchDomain, domain]); 63 | 64 | if (!address) { 65 | return ( 66 | 70 | ); 71 | } 72 | 73 | return ( 74 | 75 | 76 | 77 | 78 | 79 |
80 |
81 |
Address
82 |
83 | { 86 | setIsCopied(true); 87 | setTimeout(() => { 88 | setIsCopied(false); 89 | }, 2000); 90 | }} 91 | > 92 | {isCopied ? ( 93 |
94 | Copied 95 |
96 | ) : ( 97 | 101 | )} 102 |
103 |
104 | {domain && ( 105 | <> 106 |
Domain
107 |
{domain}
108 | 109 | )} 110 |
Balance
111 |
{formatUsd(totalBalance)}
112 |
113 | {assets.length === 0 ? ( 114 |

Loading tokens...

115 | ) : ( 116 |
    117 | {assets.map((asset) => ( 118 |
  • 122 | 123 | {asset.symbol} 124 | 125 | {asset.userTokenAccount?.amount ? ( 126 | <> 127 | {formatNumber(asset.userTokenAccount.amount)} 128 | {asset.price && ( 129 | 130 | {formatUsd( 131 | asset.userTokenAccount.amount * asset.price, 132 | )} 133 | 134 | )} 135 | 136 | ) : ( 137 | <> 138 | 0 139 | 140 | $0.00 141 | 142 | 143 | )} 144 | 145 |
  • 146 | ))} 147 |
148 | )} 149 | 150 | {connected && ( 151 | 162 | )} 163 |
164 |
165 |
166 | ); 167 | }; 168 | 169 | export { UserDropdown }; 170 | -------------------------------------------------------------------------------- /src/app/docs/components/price-change/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import Link from "next/link"; 6 | 7 | import { PublicKey } from "@solana/web3.js"; 8 | import { IconInfoCircle } from "@tabler/icons-react"; 9 | 10 | 11 | 12 | import { fetchPriceHistoryBirdeye } from "@/lib/prices/birdeye"; 13 | 14 | import { DocsTabs, DocsVariant } from "@/components/web/docs-tabs"; 15 | import { DocsWrapper } from "@/components/web/docs-wrapper"; 16 | import { DocsH1, DocsH2 } from "@/components/web/docs-heading"; 17 | import { PropsTable } from "@/components/web/props-table"; 18 | import { Code } from "@/components/web/code"; 19 | import { DocsInstallTabs } from "@/components/web/docs-install-tabs"; 20 | 21 | import { Alert, AlertTitle } from "@/components/ui/alert"; 22 | 23 | import { PriceChange } from "@/components/sol/price-change"; 24 | 25 | export default function PriceChangePage() { 26 | const [chartData, setChartData] = React.useState< 27 | { timestamp: number; price: number }[] 28 | >([]); 29 | const [isFetching, setIsFetching] = React.useState(false); 30 | const [componentSource, setComponentSource] = React.useState(""); 31 | 32 | const fetchChartData = React.useCallback(async () => { 33 | if (isFetching) return; 34 | 35 | try { 36 | setIsFetching(true); 37 | const data = await fetchPriceHistoryBirdeye( 38 | new PublicKey("ED5nyyWEzpPPiWimP8vYm7sD7TD3LAt3Q3gRTWHzPJBY"), 39 | 1729497600, 40 | 1730073600, 41 | "1H", 42 | ); 43 | if (data) setChartData(data); 44 | } finally { 45 | setIsFetching(false); 46 | } 47 | }, [isFetching]); 48 | 49 | React.useEffect(() => { 50 | if (chartData.length === 0 && !isFetching) { 51 | fetchChartData(); 52 | } 53 | }, [fetchChartData, chartData.length, isFetching]); 54 | 55 | React.useEffect(() => { 56 | fetch("/generated/component-sources/price-change.tsx.txt") 57 | .then((res) => res.text()) 58 | .then(setComponentSource); 59 | }, []); 60 | 61 | const variants: DocsVariant[] = [ 62 | { 63 | label: "Default", 64 | value: "default", 65 | preview: ( 66 |
67 |

68 | Click to toggle between % and $ 69 |

70 | 71 |
72 | ), 73 | code: `import { PriceChange } from "@/components/sol/price-change" 74 | 75 | export function PriceChangeDemo() { 76 | const [chartData, setChartData] = React.useState< 77 | { timestamp: number; price: number }[] 78 | >([]); 79 | const [isFetching, setIsFetching] = React.useState(false); 80 | 81 | const fetchChartData = React.useCallback(async () => { 82 | if (isFetching) return; 83 | 84 | try { 85 | setIsFetching(true); 86 | const data = await fetchPriceHistoryBirdeye( 87 | new PublicKey("ED5nyyWEzpPPiWimP8vYm7sD7TD3LAt3Q3gRTWHzPJBY"), 88 | 1729497600, 89 | 1730073600, 90 | "1H", 91 | ); 92 | if (data) setChartData(data); 93 | } finally { 94 | setIsFetching(false); 95 | } 96 | }, [isFetching]); 97 | 98 | React.useEffect(() => { 99 | if (chartData.length === 0 && !isFetching) { 100 | fetchChartData(); 101 | } 102 | }, [fetchChartData, chartData.length, isFetching]); 103 | 104 | return ( 105 | 106 | ) 107 | }`, 108 | }, 109 | ]; 110 | 111 | return ( 112 | 113 |
114 | PriceChange 115 |

116 | The PriceChange component displays the change of a price over time. 117 |

118 | 119 |
120 | 121 | Installation 122 | 123 | 124 | 125 | 126 |

1. Install SolanaUI Price Change

127 |

128 | Copy the PriceChange component to 129 | src/components/sol/price-change.tsx. 130 |

131 | 132 | 133 |

2. Use PriceChange

134 |

135 | Import the PriceChange component and use it in your 136 | app. 137 |

138 | 139 | 140 | 141 | SolanaUI provides utilities to help with fetching historic price 142 | data. Learn more. 143 | 144 | 145 | `} /> 146 | 147 |
148 | 152 | Props 153 | 154 | 168 |
169 |
170 |
171 |
172 | ); 173 | } 174 | -------------------------------------------------------------------------------- /src/app/docs/components/txn-settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import Link from "next/link"; 6 | 7 | 8 | 9 | import { TxnSettings, useTxnSettings } from "@/components/sol/txn-settings"; 10 | import { DocsTabs, DocsVariant } from "@/components/web/docs-tabs"; 11 | import { PropsTable } from "@/components/web/props-table"; 12 | import { DocsWrapper } from "@/components/web/docs-wrapper"; 13 | import { DocsH1, DocsH2 } from "@/components/web/docs-heading"; 14 | import { Code } from "@/components/web/code"; 15 | import { DocsInstallTabs } from "@/components/web/docs-install-tabs"; 16 | 17 | export default function TokenCardPage() { 18 | const { settings } = useTxnSettings(); 19 | const [componentSource, setComponentSource] = React.useState(""); 20 | 21 | React.useEffect(() => { 22 | fetch("/generated/component-sources/txn-settings.tsx.txt") 23 | .then((res) => res.text()) 24 | .then(setComponentSource); 25 | }, []); 26 | 27 | const variants: DocsVariant[] = [ 28 | { 29 | label: "Default", 30 | value: "default", 31 | preview: ( 32 |
33 |
    34 | {settings.priority && ( 35 |
  • 36 | Transaction priority:{" "} 37 | {settings.priority} 38 |
  • 39 | )} 40 | {settings.priorityFeeCap && ( 41 |
  • 42 | Priority fee cap:{" "} 43 | {settings.priorityFeeCap} 44 |
  • 45 | )} 46 |
47 | 48 |
49 | ), 50 | code: `import { TxnSettings } from "@/components/sol/txn-settings" 51 | 52 | export function TxnSettingsDemo() { 53 | return ( 54 | 55 | ) 56 | }`, 57 | }, 58 | ]; 59 | 60 | return ( 61 | 62 |
63 | TxnSettings 64 |

65 | The TxnSettings component is a popover that allows users to set the 66 | transaction priority, fee cap, and other transaction settings. 67 |

68 | 69 |
70 | 71 | Installation 72 | 73 | 74 | 75 | 76 |

77 | 1. Install shadcn/ui popover, toggle group, input, and button 78 | components 79 |

80 |

81 | Use shadcn/ui CLI or manually install the shadcn/ui{" "} 82 | 87 | popover 88 | 89 | ,{" "} 90 | 95 | toggle group 96 | 97 | ,{" "} 98 | 103 | input 104 | {" "} 105 | and{" "} 106 | 111 | button 112 | {" "} 113 | components. 114 |

115 | 119 | 120 |

2. Install SolanaUI TxnSettings

121 |

122 | Copy the code below to{" "} 123 | src/components/sol/txn-settings.tsx. 124 |

125 | 126 | 127 |

128 | 3. Add TxnSettings provider to your layout 129 |

130 | 141 | 142 | {children} 143 | 144 |
145 | ) 146 | } 147 | `} 148 | /> 149 | 150 |

4. Use TxnSettings

151 |

152 | Import the TxnSettings component and use it in your 153 | app. 154 |

155 | `} /> 156 |
157 |
158 | 159 | Props 160 | 161 | 167 | 168 | `, 169 | }, 170 | ]} 171 | /> 172 |
173 |
174 | 175 | ); 176 | } 177 | -------------------------------------------------------------------------------- /src/app/docs/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | 5 | import { ThemeSelector } from "@/components/web/themes"; 6 | import { DocsWrapper } from "@/components/web/docs-wrapper"; 7 | import { DocsH1, DocsH2 } from "@/components/web/docs-heading"; 8 | import { Code } from "@/components/web/code"; 9 | 10 | export default function IntroductionPage() { 11 | return ( 12 | 13 |
14 | Getting Started 15 |

16 | SolanaUI is a collection of beautifully designed UI components and 17 | utility functions, built for Solana. It extends the powerful{" "} 18 | 23 | shadcn/ui 24 | {" "} 25 | library with Solana-specific components along with asset / price 26 | fetching utilites, making it easier to get started with Solana UI 27 | development. The project is fully open source and it is intended for 28 | components and utilities to be copy and pasted into your project 29 |

30 | 31 |

32 | Building Solana apps at scale is notoriously complex and often 33 | requires custom indexing and data storage / retrieval. SolanaUI is 34 | designed to be a starting point for your project and can be extended 35 | as needed. 36 |

37 |
38 |
39 | Installation 40 |

41 | To get started with SolanaUI, you'll need a React project with{" "} 42 | 47 | TailwindCSS 48 | 49 | ,{" "} 50 | 55 | shadcn/ui 56 | 57 | , and{" "} 58 | 63 | Solana web3.js 64 | 65 | . If you have an existing project then skip this step, otherwise you 66 | can follow the steps below. 67 |

68 | 69 | 86 |

87 | Set your RPC url in your .env.local file as{" "} 88 | NEXT_PUBLIC_RPC_URL. In production we recommend proxying 89 | your RPC requests via a server side route to avoid exposing your API 90 | key. 91 |

92 | 93 |
94 |
95 | Themes 96 |

97 | SolanaUI extends @shadcn/ui and therefore inherits the{" "} 98 | 103 | theming system 104 | 105 | . Copy and paste components into your project and they will adapt 106 | according to your theme. 107 |

108 |

Try changing the theme to see the components adapt.

109 |
110 | 114 |
115 |
116 |
117 | Fetching Data 118 |

119 | Fetching data on Solana is notoriously complex. If you are doing 120 | anything at scale you likely have your own indexing and data storage / 121 | retrieval systems. All SolanaUI components are designed to be data 122 | source agnostic. 123 |

124 |

125 | SolanaUI provides a few utilities to help you get started with 126 | fetching assets and prices from{" "} 127 | 132 | Birdeye 133 | 134 | ,{" "} 135 | 140 | Helius 141 | 142 | , and{" "} 143 | 148 | Metaplex UMI 149 | 150 | . The examples in our documentation all use these utilities.{" "} 151 | 152 | Read more about the asset / price fetching utilities. 153 | 154 |

155 |
156 |
157 | ); 158 | } 159 | -------------------------------------------------------------------------------- /src/app/docs/components/token-icon/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import Link from "next/link"; 6 | 7 | import { PublicKey } from "@solana/web3.js"; 8 | import { IconInfoCircle } from "@tabler/icons-react"; 9 | 10 | import { SolAsset } from "@/lib/types"; 11 | import { fetchAssets } from "@/lib/assets/birdeye/fetch"; 12 | 13 | import { DocsWrapper } from "@/components/web/docs-wrapper"; 14 | import { DocsTabs, DocsVariant } from "@/components/web/docs-tabs"; 15 | import { DocsH1, DocsH2 } from "@/components/web/docs-heading"; 16 | import { PropsTable } from "@/components/web/props-table"; 17 | import { Code } from "@/components/web/code"; 18 | import { DocsInstallTabs } from "@/components/web/docs-install-tabs"; 19 | 20 | import { Alert, AlertTitle } from "@/components/ui/alert"; 21 | 22 | import { TokenIcon } from "@/components/sol/token-icon"; 23 | 24 | export default function TokenIconPage() { 25 | const [assets, setAssets] = React.useState([]); 26 | const [isFetching, setIsFetching] = React.useState(false); 27 | const [componentSource, setComponentSource] = React.useState(""); 28 | 29 | const fetchData = React.useCallback(async () => { 30 | if (isFetching) return; 31 | 32 | try { 33 | setIsFetching(true); 34 | const fetchedAssets = await fetchAssets({ 35 | addresses: [ 36 | new PublicKey("So11111111111111111111111111111111111111112"), 37 | ], 38 | }); 39 | setAssets(fetchedAssets); 40 | } finally { 41 | setIsFetching(false); 42 | } 43 | }, [isFetching]); 44 | 45 | React.useEffect(() => { 46 | if (assets.length === 0 && !isFetching) { 47 | fetchData(); 48 | } 49 | }, [fetchData, assets.length, isFetching]); 50 | 51 | React.useEffect(() => { 52 | fetch("/generated/component-sources/token-icon.tsx.txt") 53 | .then((res) => res.text()) 54 | .then(setComponentSource); 55 | }, []); 56 | 57 | const variants: DocsVariant[] = [ 58 | { 59 | label: "Default", 60 | value: "default", 61 | preview: ( 62 |
63 | {assets.map((asset, index) => ( 64 | 65 | ))} 66 |
67 | ), 68 | code: `import React from "react"; 69 | 70 | import { useWallet } from "@solana/wallet-adapter-react"; 71 | import { PublicKey } from "@solana/web3.js"; 72 | 73 | import { fetchAssets } from "@/lib/assets" 74 | import { WSOL_MINT, USDC_MINT } from "@/lib/consts"; 75 | 76 | import { TokenIcon } from "@/components/sol/token-icon" 77 | 78 | export function TokenIconDemo() { 79 | const [assets, setAssets] = React.useState([]); 80 | const [isFetching, setIsFetching] = React.useState(false); 81 | 82 | const fetchData = React.useCallback(async () => { 83 | if (isFetching) return; 84 | 85 | try { 86 | setIsFetching(true); 87 | const fetchedAssets = await fetchAssets({ 88 | addresses: [ 89 | new PublicKey("So11111111111111111111111111111111111111112"), 90 | ], 91 | }); 92 | setAssets(fetchedAssets); 93 | } finally { 94 | setIsFetching(false); 95 | } 96 | }, [isFetching]); 97 | 98 | React.useEffect(() => { 99 | if (assets.length === 0 && !isFetching) { 100 | fetchData(); 101 | } 102 | }, [fetchData, assets.length, isFetching]); 103 | 104 | return ( 105 | {assets.map((asset, index) => ( 106 | 111 | ))} 112 | ) 113 | }`, 114 | }, 115 | ]; 116 | 117 | return ( 118 | 119 |
120 | TokenIcon 121 |

122 | The TokenIcon component is a component for rendering token icons with 123 | loading and fallback states. 124 |

125 | 126 |
127 | 128 | Installation 129 | 130 | 131 | 132 | 133 |

1. Install SolanaUI Token Icon

134 |

135 | Copy the code below to{" "} 136 | src/components/sol/token-icon.tsx. 137 |

138 | 139 | 140 |

2. Use TokenIcon

141 |

142 | Import the TokenIcon component and use it in your app. 143 |

144 | 145 | 146 | 147 | SolanaUI provides utilities to help with fetching assets.{" "} 148 | Learn more. 149 | 150 | 151 | `} 159 | /> 160 | 161 |
162 | 163 | Props 164 | 165 | 186 |
187 |
188 |
189 |
190 | ); 191 | } 192 | -------------------------------------------------------------------------------- /src/components/sol/wallet.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { PublicKey } from "@solana/web3.js"; 5 | import { useConnection } from "@solana/wallet-adapter-react"; 6 | import { getPrimaryDomain } from "@bonfida/spl-name-service"; 7 | import { SearchIcon } from "lucide-react"; 8 | 9 | import { SolAsset } from "@/lib/types"; 10 | import { formatUsd, formatNumber, shortAddress } from "@/lib/utils"; 11 | 12 | import { Button } from "@/components/ui/button"; 13 | import { Input } from "@/components/ui/input"; 14 | import { Skeleton } from "@/components/ui/skeleton"; 15 | import { 16 | Sheet, 17 | SheetContent, 18 | SheetDescription, 19 | SheetHeader, 20 | SheetTitle, 21 | SheetTrigger, 22 | } from "@/components/ui/sheet"; 23 | 24 | import { Avatar } from "@/components/sol/avatar"; 25 | import { TokenIcon } from "@/components/sol/token-icon"; 26 | 27 | type WalletProps = { 28 | address: PublicKey | null; 29 | assets?: SolAsset[]; 30 | trigger?: React.ReactNode; 31 | onAssetClick?: (asset: SolAsset) => void; 32 | }; 33 | 34 | const Wallet = ({ address, assets, trigger, onAssetClick }: WalletProps) => { 35 | const [search, setSearch] = React.useState(""); 36 | const { connection } = useConnection(); 37 | const [domain, setDomain] = React.useState(null); 38 | 39 | const totalBalanceUsd = React.useMemo( 40 | () => 41 | assets?.reduce( 42 | (acc, asset) => 43 | acc + (asset.userTokenAccount?.amount || 0) * (asset.price || 0), 44 | 0, 45 | ), 46 | [assets], 47 | ); 48 | 49 | const filteredAssets = React.useMemo(() => { 50 | return assets && assets.length > 0 51 | ? assets?.filter( 52 | (asset) => 53 | asset.symbol && 54 | asset.symbol.toLowerCase().includes(search.toLowerCase()), 55 | ) 56 | : []; 57 | }, [assets, search]); 58 | 59 | const fetchDomain = React.useCallback(async () => { 60 | if (!connection || !address) return; 61 | try { 62 | const { reverse } = await getPrimaryDomain(connection, address); 63 | setDomain(`${reverse}.sol`); 64 | } catch (error) { 65 | setDomain(null); 66 | } 67 | }, [connection, address]); 68 | 69 | React.useEffect(() => { 70 | if (domain) return; 71 | fetchDomain(); 72 | }, [fetchDomain, domain]); 73 | 74 | if (!address) { 75 | return ; 76 | } 77 | 78 | return ( 79 | 80 | 81 | {trigger || ( 82 | 86 | )} 87 | 88 | 89 | 90 | 91 |
92 | 93 | {domain ? ( 94 |
95 | {domain} 96 | {shortAddress(address)} 97 |
98 | ) : ( 99 | shortAddress(address) 100 | )} 101 |
102 |
103 | 104 | {shortAddress(address)} wallet 105 | 106 |
107 |
108 |
109 |
110 | {formatUsd(totalBalanceUsd || 0)} 111 |
112 |
Total Balance
113 |
114 |
115 | {filteredAssets && ( 116 |
117 |
{ 120 | e.preventDefault(); 121 | setSearch(search); 122 | }} 123 | > 124 | setSearch(e.target.value)} 129 | /> 130 | 138 |
139 |
140 | {filteredAssets.map((asset) => ( 141 | 160 | ))} 161 |
162 |
163 | )} 164 |
165 |
166 | ); 167 | }; 168 | 169 | export { Wallet }; 170 | --------------------------------------------------------------------------------