diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ca39c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..0dd8924 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Kaspian-KRC20 + +## Development + +Ensure you have Bun. If not, please install the latest version of Bun to proceed with the development process. + +Install the required Bun modules using the command ``bun install`` and get WASM binaries from [here](https://kaspa.aspectron.org/nightly/downloads/) or by building it yourself from rusty-kaspa. Once obtained, place the WASM binaries into the ``./wasm`` folder. + +### Testing + +To begin testing, execute ``bun run dev`` to run the development server. Then, utilize the contents of the dist folder as an unpacked extension in your browser for testing purposes. + +### Building + +To build it as an unpacked extension, execute ``bun run build``. It will be built into dist folder. diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..d0d6344 Binary files /dev/null and b/bun.lockb differ diff --git a/components.json b/components.json new file mode 100644 index 0000000..d901a23 --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/style.scss", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/components/utils" + } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..b905407 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Kaspian | KRC20 + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..ca4d899 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "kaspian", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "kasplexbuilder": "github:KaffinPX/KasplexBuilder", + "kprovider": "github:KaffinPX/KProvider", + "lucide-react": "^0.436.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "sonner": "^1.5.0", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@types/node": "^22.3.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.15.0", + "@typescript-eslint/parser": "^7.15.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "postcss": "^8.4.41", + "tailwindcss": "^3.4.10", + "typescript": "^5.2.2", + "vite": "^5.3.4" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..91886d1 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,83 @@ +import { useKaspian } from 'KProvider' +import Connect from './pages/Connection' +import Account from './pages/Account' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { useEffect } from 'react' +import useAccount from './hooks/useAccount' +import { Button } from './components/ui/button' +import { CopyIcon, Moon, Sun } from 'lucide-react' +import { useTheme } from './hooks/useTheme' +import useIndexer from './hooks/useIndexer' +import { toast } from 'sonner' + +function App() { + const { account } = useKaspian() + const { setNetworkId, networkId } = useIndexer() + const { address, setAddress } = useAccount() + const { theme, setTheme } = useTheme() + + useEffect(() => { + if (account) { + if (networkId !== account.networkId) { + setNetworkId(account.networkId) + setAddress(account.addresses[0]) + } + if (!address) setAddress(account.addresses[0]) + } + }, [ account ]) + + return ( + <> +
+
+

Kaspian | KRC20

+ +
+
+ + +
+
+ {!account && ( + + )} + {account && ( + + )} +

+ As with all early-stage products there are risks associated with using the protocol and users assume the full responsibility for these risks. You should not deposit any money you are not comfortable losing. +

+ + ) +} + +export default App diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..14e8b76 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/components/utils" + +const buttonVariants = cva( + "inline-flex 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", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..a43acef --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/components/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..c6381f9 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/components/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..b409319 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/components/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..b120333 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/components/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..8798fbd --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/components/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverAnchor = PopoverPrimitive.Anchor + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..c167d8f --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,162 @@ +import * as React from "react" +import { + CaretSortIcon, + CheckIcon, + ChevronDownIcon, + ChevronUpIcon, +} from "@radix-ui/react-icons" +import * as SelectPrimitive from "@radix-ui/react-select" + +import { cn } from "@/components/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..2468d8c --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/components/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..1128edf --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,29 @@ +import { useTheme } from "next-themes" +import { Toaster as Sonner } from "sonner" + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..437a47d --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/components/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/src/components/utils.ts b/src/components/utils.ts new file mode 100644 index 0000000..d084cca --- /dev/null +++ b/src/components/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/contexts/Account.tsx b/src/contexts/Account.tsx new file mode 100644 index 0000000..369c857 --- /dev/null +++ b/src/contexts/Account.tsx @@ -0,0 +1,46 @@ +import useIndexer from "@/hooks/useIndexer" +import type { Balance } from "KasplexBuilder/src/indexer/protocol" +import { createContext, ReactNode, useCallback, useEffect, useState } from "react" + +type Balances = {[ ticker: string ]: Balance} + +export const AccountContext = createContext<{ + address: string | undefined + setAddress: (address: string) => void + balances: Balances + refresh: () => Promise +} | undefined>(undefined) + +export function AccountProvider ({ children }: { + children: ReactNode +}) { + const { indexer, tokens } = useIndexer() + const [ address, setAddress ] = useState() + const [ balances, setBalances ] = useState({}) // TBD: use useOptimistic for balances + + const refresh = useCallback(async () => { + if (!address || indexer.current.url === '') return console.log('rtrned', address, indexer.current.url) + + const balances = (await indexer.current.getKRC20Balances({ address })).result + + setBalances(balances.reduce((acc: { [ticker: string]: Balance }, balance) => { + acc[balance.tick] = balance + return acc + }, {})) + }, [ address ]) + + useEffect(() => { + refresh() + }, [ address, refresh, tokens ]) + + return ( + + {children} + + ) +} diff --git a/src/contexts/Indexer.tsx b/src/contexts/Indexer.tsx new file mode 100644 index 0000000..83f0721 --- /dev/null +++ b/src/contexts/Indexer.tsx @@ -0,0 +1,56 @@ +import { Indexer } from "KasplexBuilder" +import { createContext, ReactNode, useCallback, useEffect, useRef, useState } from "react" +import type { Token } from "KasplexBuilder/src/indexer/protocol" + +type Tokens = {[ ticker: string ]: Token} +export const IndexerContext = createContext<{ + tokens: Tokens + networkId: string | undefined + setNetworkId: (address: string) => void + indexer: React.MutableRefObject +} | undefined>(undefined) + +export function IndexerProvider ({ children }: { + children: ReactNode +}) { + const indexer = useRef(new Indexer('')) + const [ networkId, setNetworkId ] = useState() + const [ tokens, setTokens ] = useState({}) + + const refresh = useCallback(async () => { + if (indexer.current.url === '') return + + let tokens: Token[] = [] + let cursor = undefined + + while (true) { + const response = await indexer.current.getKRC20TokenList({ next: cursor }) + + tokens.push(...response.result) + + if (response.result.length < 50) break + cursor = response.next + } + + setTokens(tokens.reduce((acc: {[ ticker: string ]: Token}, token) => { + acc[token.tick] = token + return acc + }, {})) + }, []) + + useEffect(() => { + if (networkId === 'testnet-10') { + indexer.current.url = 'https://tn10api.kasplex.org' + } else { + indexer.current.url = '' + } + + refresh() + }, [ networkId ]) + + return ( + + {children} + + ) +} diff --git a/src/contexts/Theme.tsx b/src/contexts/Theme.tsx new file mode 100644 index 0000000..a6745ec --- /dev/null +++ b/src/contexts/Theme.tsx @@ -0,0 +1,63 @@ +import { createContext, useEffect, useState } from "react" + +type Theme = "dark" | "light" | "system" + +type ThemeProviderProps = { + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string +} + +type ThemeProviderState = { + theme: Theme + setTheme: (theme: Theme) => void +} + +const initialState: ThemeProviderState = { + theme: "system", + setTheme: () => null +} + +export const ThemeContext = createContext(initialState) + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "vite-ui-theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme + ) + + useEffect(() => { + const root = window.document.documentElement + + root.classList.remove("light", "dark") + + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light" + + setTheme(systemTheme) + root.classList.add(systemTheme) + return + } + + root.classList.add(theme) + }, [ theme ]) + + return ( + { + localStorage.setItem(storageKey, theme) + setTheme(theme) + } + }}> + {children} + + ) +} \ No newline at end of file diff --git a/src/hooks/useAccount.ts b/src/hooks/useAccount.ts new file mode 100644 index 0000000..43c50c0 --- /dev/null +++ b/src/hooks/useAccount.ts @@ -0,0 +1,10 @@ +import { useContext } from "react" +import { AccountContext } from "../contexts/Account" + +export default function useAccount () { + const context = useContext(AccountContext) + + if (!context) throw new Error("Missing Account context") + + return context +} diff --git a/src/hooks/useIndexer.ts b/src/hooks/useIndexer.ts new file mode 100644 index 0000000..2bb59a8 --- /dev/null +++ b/src/hooks/useIndexer.ts @@ -0,0 +1,10 @@ +import { useContext } from "react" +import { IndexerContext } from "../contexts/Indexer" + +export default function useIndexer () { + const context = useContext(IndexerContext) + + if (!context) throw new Error("Missing Indexer context") + + return context +} diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 0000000..0307183 --- /dev/null +++ b/src/hooks/useTheme.ts @@ -0,0 +1,11 @@ +import { useContext } from "react" +import { ThemeContext } from "@/contexts/Theme" + +export const useTheme = () => { + const context = useContext(ThemeContext) + + if (context === undefined) + throw new Error("useTheme must be used within a ThemeProvider") + + return context +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..15f4681 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,26 @@ +import ReactDOM from 'react-dom/client' +import App from './App' +import { KaspianProvider } from 'KProvider' +import "./style.css" +import { AccountProvider } from './contexts/Account' +import { ThemeProvider } from './contexts/Theme' +import { Toaster } from "@/components/ui/sonner" + +import * as kaspa from "@/../wasm" +import wasmBinary from "../wasm/kaspa_bg.wasm?url" +import { IndexerProvider } from './contexts/Indexer' + +kaspa.default(wasmBinary) + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + + + +) diff --git a/src/pages/Account.tsx b/src/pages/Account.tsx new file mode 100644 index 0000000..2ea3525 --- /dev/null +++ b/src/pages/Account.tsx @@ -0,0 +1,23 @@ +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import Tokens from "./Account/Tokens" +import Mintage from "./Account/Mintage" + +function Account () { + return ( + + + Tokens + Mintage + Deployment + + + + + + + + + ) +} + +export default Account diff --git a/src/pages/Account/Mintage.tsx b/src/pages/Account/Mintage.tsx new file mode 100644 index 0000000..c69ce9d --- /dev/null +++ b/src/pages/Account/Mintage.tsx @@ -0,0 +1,106 @@ +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import { CoinsIcon } from "lucide-react" +import useAccount from "@/hooks/useAccount" +import { useEffect, useState } from "react" +import { Address, ScriptBuilder, XOnlyPublicKey, addressFromScriptPublicKey } from '@/../wasm' +import { Inscription } from 'KasplexBuilder' +import { useKaspian } from 'KProvider' +import { Label } from "@/components/ui/label" +import useIndexer from "@/hooks/useIndexer" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { toast } from "sonner" + +function Mintage () { + const { address } = useAccount() + const { tokens, networkId } = useIndexer() + const { invoke } = useKaspian() + + const [ ticker, setTicker ] = useState('') + const [ script, setScript ] = useState() + const [ commitAddress, setCommitAddress ] = useState() + const [ commit, setCommit ] = useState() + + useEffect(() => { + if (!ticker) return setCommitAddress(undefined) + + const script = new ScriptBuilder() + const inscription = new Inscription('mint', { + tick: ticker + }) + + inscription.write(script, XOnlyPublicKey.fromAddress(new Address(address!)).toString()) + + setScript(script.toString()) + setCommitAddress(addressFromScriptPublicKey(script.createPayToScriptHashScript(), networkId!)!.toString()) + }, [ address, ticker ]) + + return ( + + + + Mintage + + Mint your own KRC-20 tokens by creating transactions with specific fees. + + + + + + + + {ticker && ( +

+ {(+tokens[ticker].lim / 10 ** +tokens[ticker].dec)} {ticker} to be minted. +

+ )} +
+
+ ) +} + +export default Mintage diff --git a/src/pages/Account/Tokens.tsx b/src/pages/Account/Tokens.tsx new file mode 100644 index 0000000..2a76e3f --- /dev/null +++ b/src/pages/Account/Tokens.tsx @@ -0,0 +1,47 @@ +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { RefreshCcwIcon } from "lucide-react" +import Transfer from "./Tokens/Transact" +import useAccount from "@/hooks/useAccount" + +function Tokens () { + const { balances, refresh } = useAccount() + + return ( + + + + Tokens + + + Manage and track your KRC-20 tokens effortlessly in this section. + + + {Object.entries(balances).length === 0 ? ( +

+ No tokens found. +

+ ) : ( + Object.entries(balances).map(([ticker, { balance, dec }], index) => ( +
+
+ {+balance / 10 ** +dec} {ticker} + +
+
+ )) + )} +
+
+ ) +} + +export default Tokens diff --git a/src/pages/Account/Tokens/Transact.tsx b/src/pages/Account/Tokens/Transact.tsx new file mode 100644 index 0000000..cc6986f --- /dev/null +++ b/src/pages/Account/Tokens/Transact.tsx @@ -0,0 +1,115 @@ +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { useEffect, useState } from 'react' +import { Input } from '@/components/ui/input' +import { useKaspian } from 'KProvider' +import { Address, ScriptBuilder, XOnlyPublicKey, addressFromScriptPublicKey } from '@/../wasm' +import { Inscription } from 'KasplexBuilder' +import useAccount from '@/hooks/useAccount' +import { CoinsIcon, SendIcon } from 'lucide-react' +import useIndexer from '@/hooks/useIndexer' +import { toast } from 'sonner' + +function Transfer ({ ticker }: { + ticker: string +}) { + const { address, balances } = useAccount() + const { networkId } = useIndexer() + const { invoke } = useKaspian() + + const [ recipient, setRecipient ] = useState('') + const [ amount, setAmount ] = useState('') + const [ script, setScript ] = useState() + const [ commitAddress, setCommitAddress ] = useState() + const [ commit, setCommit ] = useState() + + useEffect(() => { + if (!address || recipient === '' || amount === '') return setCommitAddress(undefined) + if (!Address.validate(recipient)) return + + const script = new ScriptBuilder() + const inscription = new Inscription('transfer', { + tick: ticker, + amt: BigInt(Number(amount) * 1e8).toString(), + to: recipient.toString() + }) + + inscription.write(script, XOnlyPublicKey.fromAddress(new Address(address!)).toString()) + + setScript(script.toString()) + setCommitAddress(addressFromScriptPublicKey(script.createPayToScriptHashScript(), networkId!)!.toString()) + }, [ address, recipient, amount ]) + + return ( + + + + + + + Transfer + + {+balances[ticker].balance / 10 ** +balances[ticker].dec} {ticker} + Available + +
+ { setRecipient(e.target.value) }} + disabled={!!commit} + /> + { setAmount(e.target.value) }} + disabled={!!commit} + /> +
+
+ + + +
+
+ ) +} + +export default Transfer diff --git a/src/pages/Connection.tsx b/src/pages/Connection.tsx new file mode 100644 index 0000000..ef7bd73 --- /dev/null +++ b/src/pages/Connection.tsx @@ -0,0 +1,44 @@ +import { useKaspian } from 'KProvider' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" + +function Connection () { + const { providers, connect } = useKaspian() + + return ( + <> + + + Connect + Connect to a Kaspian-compatible wallet to use DApp. + + +
+ {providers.length === 0 ? ( +

+ No wallets found. +

+ ) : ( + providers.map((provider) => ( +
+ {provider.name} + +
+ )) + )} +
+
+
+ + ) +} + +export default Connection diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..955c798 --- /dev/null +++ b/src/style.css @@ -0,0 +1,74 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +#root { + display: flex; + flex-direction: column; + align-items: center; +} + +@layer base { + :root { + --background: 220 15% 95%; /* Light Blue-Gray */ + --foreground: 220 15% 20%; /* Dark Blue-Gray */ + --card: 220 15% 90%; /* Light Blue-Gray */ + --card-foreground: 220 15% 30%; /* Medium Blue-Gray */ + --popover: 220 15% 95%; /* Light Blue-Gray */ + --popover-foreground: 220 15% 20%; /* Dark Blue-Gray */ + --primary: 200 60% 75%; /* Soft Pastel Blue */ + --primary-foreground: 0 0% 100%; /* Pure White */ + --secondary: 180 40% 70%; /* Soft Cyan */ + --secondary-foreground: 220 15% 20%; /* Dark Blue-Gray */ + --muted: 200 20% 80%; /* Muted Blue */ + --muted-foreground: 220 15% 40%; /* Medium Blue-Gray */ + --accent: 340 70% 65%; /* Softer Vivid Pink */ + --accent-foreground: 0 0% 100%; /* Pure White */ + --destructive: 0 70% 50%; /* Bright Red */ + --destructive-foreground: 0 0% 100%; /* Pure White */ + --border: 220 20% 60%; /* Medium Blue-Gray */ + --input: 220 20% 90%; /* Light Blue-Gray */ + --ring: 200 60% 75%; /* Soft Pastel Blue */ + --radius: 1rem; /* Slightly larger border radius */ + } + .dark { + --background: 220 5% 5%; /* Very Dark Gray */ + --foreground: 220 15% 90%; /* Light Blue-Gray */ + --card: 220 5% 10%; /* Very Dark Gray */ + --card-foreground: 220 15% 90%; /* Light Blue-Gray */ + --popover: 220 5% 15%; /* Dark Gray */ + --popover-foreground: 220 15% 90%; /* Light Blue-Gray */ + --primary: 200 60% 40%; /* Darker Pastel Blue */ + --primary-foreground: 0 0% 100%; /* Pure White */ + --secondary: 180 40% 40%; /* Soft Cyan */ + --secondary-foreground: 0 0% 100%; /* Pure White */ + --muted: 200 20% 30%; /* Muted Blue */ + --muted-foreground: 220 15% 60%; /* Medium Blue-Gray */ + --accent: 340 70% 55%; /* Softer Vivid Pink */ + --accent-foreground: 0 0% 100%; /* Pure White */ + --destructive: 0 70% 50%; /* Bright Red */ + --destructive-foreground: 0 0% 100%; /* Pure White */ + --border: 0 0% 20%; /* Dark Gray */ + --input: 220 20% 30%; /* Very Dark Blue-Gray */ + --ring: 200 60% 40%; /* Darker Pastel Blue */ + --radius: 1rem; /* Slightly larger border radius */ + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +.scrollbar-hidden::-webkit-scrollbar { + display: none; +} + +.scrollbar-hidden { + -ms-overflow-style: none; + scrollbar-width: none; +} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..f9305e8 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,71 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ["class"], + content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px" + } + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))" + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))" + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))" + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))" + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))" + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))" + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))" + } + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)" + }, + keyframes: { + "accordion-down": { + from: { height: 0 }, + to: { height: "var(--radix-accordion-content-height)" } + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: 0 } + } + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out" + } + } + }, + plugins: [require("tailwindcss-animate")] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a1e1c17 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ "DOM", "DOM.Iterable", "ESNext" ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": [ "src/*" ] + } + }, + "include": [ "src" ], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..d4a2cbc --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "target": "ESNext" + }, + "include": [ "vite.config.ts" ] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..6289621 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ react() ], + resolve: { + alias: { + "@": path.resolve(__dirname, "src") + } + } +})