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
+
{
+ setTheme(theme === 'light' ? 'dark' : 'light')
+ }}>
+ {theme === 'light' ? : }
+
+
+
+ {
+ setAddress(address)
+ }}>
+
+
+
+
+ {account && account.addresses.map((address, index) => (
+
+ {address}
+
+ ))}
+
+
+ {
+ navigator.clipboard.writeText(address!)
+
+ toast.success('Address copied into clipboard.')
+ }}>
+
+
+
+
+ {!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:
+ {
+ setTicker(ticker)
+ }}>
+
+
+
+
+ {Object.entries(tokens).map(([, token], index) => (
+
+ {token.tick}
+
+ ))}
+
+
+
+
+ {
+ if (!commit) {
+ const commitment = JSON.parse(await invoke('transact', [[[ commitAddress!, '0.2' ]]]))
+ setCommit(commitment.id)
+
+ toast.success('Committed token mint request succesfully!', {
+ action: {
+ label: 'Copy',
+ onClick: () => navigator.clipboard.writeText(commitment.id)
+ }
+ })
+ } else {
+ const reveal = JSON.parse(await invoke('transact', [[], "1", [{
+ address: commitAddress!,
+ outpoint: commit,
+ index: 0,
+ signer: address!,
+ script: script
+ }]]))
+
+ setCommit(undefined)
+
+ toast.success('Revealed and completed token mint request succesfully!', {
+ action: {
+ label: 'Copy',
+ onClick: () => navigator.clipboard.writeText(reveal.id)
+ }
+ })
+ }
+ }}>
+ {!commit ? 'Commit' : 'Reveal'}
+
+ {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
+ {
+ currentTarget.disabled = true
+
+ refresh().then(() => {
+ currentTarget.disabled = false
+ })
+ }}>
+
+
+
+ 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}
+ />
+
+
+
+ {
+ if (!commit) {
+ const commitment = JSON.parse(await invoke('transact', [[[ commitAddress!, '0.2' ]]]))
+ setCommit(commitment.id)
+
+ toast.success('Committed token transfer request succesfully!', {
+ action: {
+ label: 'Copy',
+ onClick: () => navigator.clipboard.writeText(commitment.id)
+ }
+ })
+ } else {
+ const reveal = JSON.parse(await invoke('transact', [[], "0.01", [{
+ address: commitAddress!,
+ outpoint: commit,
+ index: 0,
+ signer: address!,
+ script: script
+ }]]))
+
+ setRecipient('')
+ setAmount('')
+ setCommit(undefined)
+
+ toast.success('Revealed and completed token transfer request succesfully!', {
+ action: {
+ label: 'Copy',
+ onClick: () => navigator.clipboard.writeText(reveal.id)
+ }
+ })
+ }
+ }}>
+ {!commit ? 'Commit' : 'Reveal'}
+
+
+
+
+ )
+}
+
+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}
+ connect(provider.id)}>
+ Connect
+
+
+ ))
+ )}
+
+
+
+ >
+ )
+}
+
+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")
+ }
+ }
+})