diff --git a/.github/workflows/ui_sdk_publish.yaml b/.github/workflows/ui_sdk_publish.yaml
new file mode 100644
index 00000000..7b6c0ff1
--- /dev/null
+++ b/.github/workflows/ui_sdk_publish.yaml
@@ -0,0 +1,119 @@
+name: Publish UI SDK
+
+on:
+ push:
+ tags:
+ - "ui/v*"
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version to publish (e.g., 1.0.0)"
+ required: true
+ type: string
+ tag:
+ description: "Tag to publish (e.g., latest, alpha, beta, etc.)"
+ required: false
+ type: string
+ default: ""
+
+permissions:
+ id-token: write
+ contents: read
+
+jobs:
+ publish:
+ name: Publish to npm
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Use Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: "20"
+ registry-url: "https://registry.npmjs.org"
+
+ - name: Set version
+ id: set_version
+ run: |
+ if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
+ VERSION="${{ github.event.inputs.version }}"
+ else
+ TAG_NAME=${GITHUB_REF#refs/tags/}
+ VERSION=${TAG_NAME#ui/v}
+ fi
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+
+ - name: Update package.json version
+ run: |
+ VERSION=${{ steps.set_version.outputs.version }}
+ cd ui
+ npm version "$VERSION" --no-git-tag-version --allow-same-version
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v2
+ with:
+ version: 10.6.3
+
+ - name: Install dependencies
+ run: |
+ cd ui
+ pnpm install
+
+ - name: Build the package
+ run: |
+ cd ui
+ pnpm run build
+
+ - name: Update npm
+ run: npm install -g npm@latest
+
+ - name: Check if version exists
+ id: check_version
+ run: |
+ cd ui
+ VERSION=${{ steps.set_version.outputs.version }}
+ echo "Checking if version $VERSION already exists..."
+
+ if npm view "@avalanche-sdk/ui@$VERSION" version >/dev/null 2>&1; then
+ echo "Version $VERSION already exists"
+ echo "exists=true" >> $GITHUB_OUTPUT
+ else
+ echo "Version $VERSION does not exist"
+ echo "exists=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Publish package
+ run: |
+ cd ui
+ VERSION=${{ steps.set_version.outputs.version }}
+ INPUT_TAG=${{ github.event.inputs.tag }}
+ EXISTS=${{ steps.check_version.outputs.exists }}
+
+ echo "Detected version: $VERSION"
+ echo "Input tag: $INPUT_TAG"
+ echo "Version exists: $EXISTS"
+
+ if [[ "$EXISTS" == "false" ]]; then
+ if [[ "$VERSION" == *"-"* ]]; then
+ # Prerelease: publish with auto-detected tag + add input tag if provided
+ PRERELEASE=${VERSION#*-}
+ TAG=${PRERELEASE%%.*}
+ echo "Prerelease detected; publishing under dist-tag: $TAG"
+ npm publish --tag "$TAG" --access public
+ else
+ # Stable: publish with latest tag
+ echo "Official release detected; publishing under 'latest'"
+ npm publish --access public
+ fi
+ fi
+ if [[ -n "$INPUT_TAG" ]]; then
+ echo "Adding input tag: $INPUT_TAG"
+ npm dist-tag add "@avalanche-sdk/ui@$VERSION" "$INPUT_TAG"
+ else
+ echo "No input tag provided. Skipping tag addition."
+ fi
+
+
diff --git a/ui/.gitignore b/ui/.gitignore
new file mode 100644
index 00000000..d42f73f9
--- /dev/null
+++ b/ui/.gitignore
@@ -0,0 +1,56 @@
+# Dependencies
+node_modules/
+.pnpm-store/
+.pnpm-debug.log*
+
+# Build outputs
+dist/
+esm/
+*.tsbuildinfo
+
+# Testing
+coverage/
+.nyc_output/
+*.lcov
+.vitest/
+
+# Logs
+*.log
+npm-debug.log*
+pnpm-debug.log*
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Environment
+.env
+.env.local
+.env.*.local
+
+# ESLint cache
+.eslintcache
+
+# Temporary files
+*.tmp
+*.temp
+.cache/
+temp/
+
+# Vite
+.vite/
+
+# TypeScript
+*.tsbuildinfo
+
+# Playground build
+playground/dist/
+playground/node_modules/
+
diff --git a/ui/package.json b/ui/package.json
new file mode 100644
index 00000000..c7ce7c0c
--- /dev/null
+++ b/ui/package.json
@@ -0,0 +1,138 @@
+{
+ "name": "@avalanche-sdk/ui",
+ "version": "0.1.0",
+ "description": "A comprehensive React component library and TypeScript utilities for building Avalanche blockchain applications. Includes ready-to-use UI components for wallet management, token transfers, staking, earning, and interchain operations.",
+ "type": "module",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/ava-labs/avalanche-sdk-typescript.git",
+ "directory": "ui"
+ },
+ "license": "BSD-3-Clause",
+ "keywords": [
+ "avalanche",
+ "avalanche-sdk",
+ "react",
+ "ui",
+ "components",
+ "wallet",
+ "blockchain",
+ "web3"
+ ],
+ "author": "0xstt",
+ "bugs": {
+ "url": "https://github.com/ava-labs/avalanche-sdk-typescript/issues"
+ },
+ "homepage": "https://github.com/ava-labs/avalanche-sdk-typescript/tree/main/ui#readme",
+ "publishConfig": {
+ "access": "public"
+ },
+ "scripts": {
+ "build": "pnpm clean && vite build",
+ "dev": "NODE_ENV=development vite build --watch",
+ "clean": "rm -rf dist esm",
+ "format": "prettier --write .",
+ "typecheck": "tsc --noEmit",
+ "test": "vitest run",
+ "test:coverage": "vitest run --coverage",
+ "test:watch": "vitest",
+ "test:ui": "vitest --ui",
+ "prepublishOnly": "pnpm run typecheck && pnpm run build"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19",
+ "react-dom": "^18 || ^19",
+ "viem": "^2.0.0"
+ },
+ "dependencies": {
+ "@avalanche-sdk/chainkit": "0.3.0-alpha.8",
+ "@avalanche-sdk/client": "^0.0.4-alpha.16",
+ "@avalanche-sdk/interchain": "^0.1.1-alpha.1",
+ "@floating-ui/react": "^0.27.13",
+ "@radix-ui/react-dialog": "^1.1.14",
+ "@radix-ui/react-dropdown-menu": "^2.1.6",
+ "@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-popover": "^1.1.14",
+ "@radix-ui/react-select": "^2.1.2",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-toast": "^1.2.14",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.468.0",
+ "tailwind-merge": "^3.2.0",
+ "usehooks-ts": "^3.1.1"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "6.4.7",
+ "@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/node": "^22.13.10",
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "@vitejs/plugin-react": "^4.3.4",
+ "@vitest/coverage-v8": "^3.0.5",
+ "@vitest/ui": "^3.0.5",
+ "autoprefixer": "^10.4.19",
+ "esbuild-fix-imports-plugin": "^1.0.19",
+ "esbuild-plugin-babel": "^0.2.3",
+ "glob": "^11.0.1",
+ "jsdom": "^24.1.0",
+ "postcss": "^8",
+ "postcss-import": "^16.1.0",
+ "postcss-load-config": "^6.0.1",
+ "react": "^18.2.43",
+ "react-dom": "^18.2.17",
+ "rollup-plugin-preserve-use-client": "^3.0.1",
+ "tailwindcss": "^3.4.6",
+ "tailwindcss-animate": "^1.0.7",
+ "tscpaths": "^0.0.9",
+ "tsup": "^8.3.5",
+ "viem": "^2.21.45",
+ "vite": "^5.4.20",
+ "vite-plugin-dts": "^4.5.3",
+ "vite-plugin-externalize-deps": "^0.9.0",
+ "vitest": "^3.0.5"
+ },
+ "files": [
+ "dist/**/*",
+ "src/**/*"
+ ],
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "module": "./dist/index.js",
+ "exports": {
+ "./package.json": "./package.json",
+ "./styles.css": "./dist/assets/style.css",
+ "./dist/assets/style.css": "./dist/assets/style.css",
+ "./theme": {
+ "types": "./dist/theme.d.ts",
+ "module": "./dist/theme.js",
+ "import": "./dist/theme.js",
+ "default": "./dist/theme.js"
+ },
+ ".": {
+ "types": "./dist/index.d.ts",
+ "module": "./dist/index.js",
+ "import": "./dist/index.js",
+ "default": "./dist/index.js"
+ },
+ "./wallet": {
+ "types": "./dist/wallet.d.ts",
+ "module": "./dist/wallet.js",
+ "import": "./dist/wallet.js",
+ "default": "./dist/wallet.js"
+ },
+ "./transfer": {
+ "types": "./dist/transfer.d.ts",
+ "module": "./dist/transfer.js",
+ "import": "./dist/transfer.js",
+ "default": "./dist/transfer.js"
+ }
+ },
+ "sideEffects": false,
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "packageManager": "pnpm@10.6.3"
+}
diff --git a/ui/playground/favicon.ico b/ui/playground/favicon.ico
new file mode 100644
index 00000000..ecde3034
Binary files /dev/null and b/ui/playground/favicon.ico differ
diff --git a/ui/playground/index.html b/ui/playground/index.html
new file mode 100644
index 00000000..ba9c0dff
--- /dev/null
+++ b/ui/playground/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Avalanche UI Kit Playground
+
+
+
+
+
+
diff --git a/ui/playground/package.json b/ui/playground/package.json
new file mode 100644
index 00000000..eb272eaa
--- /dev/null
+++ b/ui/playground/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "avalanche-ui-playground",
+ "version": "0.1.0",
+ "description": "Development playground and interactive demo for @avalanche-sdk/ui components.",
+ "private": true,
+ "type": "module",
+ "author": "0xstt",
+ "license": "BSD-3-Clause",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@avalanche-sdk/client": "^0.0.4-alpha.16",
+ "@avalanche-sdk/ui": "^0.1.0",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@types/react-syntax-highlighter": "^15.5.13",
+ "lucide-react": "^0.468.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-syntax-highlighter": "^16.1.0",
+ "viem": "^2.21.45"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "@vitejs/plugin-react": "^4.2.1",
+ "autoprefixer": "^10.4.19",
+ "buffer": "^6.0.3",
+ "postcss": "^8",
+ "process": "^0.11.10",
+ "tailwindcss": "^3.4.6",
+ "tailwindcss-animate": "^1.0.7",
+ "typescript": "^5.2.2",
+ "util": "^0.12.5",
+ "vite": "^5.0.8"
+ }
+}
diff --git a/ui/playground/postcss.config.js b/ui/playground/postcss.config.js
new file mode 100644
index 00000000..1d926516
--- /dev/null
+++ b/ui/playground/postcss.config.js
@@ -0,0 +1,7 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
+
diff --git a/ui/playground/src/App.css b/ui/playground/src/App.css
new file mode 100644
index 00000000..1a8e9076
--- /dev/null
+++ b/ui/playground/src/App.css
@@ -0,0 +1,131 @@
+.playground {
+ min-height: 100vh;
+ background: var(--background);
+ transition: background-color 0.3s ease;
+ color: var(--foreground);
+ position: relative;
+}
+
+.playground > div:last-child {
+ padding: 0 2rem 3rem 2rem;
+}
+
+.playground-header {
+ margin-bottom: 4rem;
+ position: sticky;
+ top: 0;
+ z-index: 50;
+ padding: 2rem 0;
+ border-bottom: 1px solid var(--border);
+ background: var(--background)/95;
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+}
+
+/* Remove sticky on mobile */
+@media (max-width: 1023px) {
+ .playground-header {
+ position: relative;
+ background: var(--background);
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ }
+
+ /* Ensure sidebar is hidden on mobile */
+ nav[class*="NavigationSidebar"],
+ .navigation-sidebar {
+ display: none !important;
+ }
+}
+
+.playground-main {
+ max-width: 1000px;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 4rem;
+}
+
+.component-section {
+ width: 100%;
+}
+
+/* Responsive spacing adjustments */
+@media (max-width: 1023px) {
+ .playground > div:last-child {
+ margin-left: 0 !important;
+ padding: 0 1rem 2rem 2rem !important;
+ }
+
+ .playground-header {
+ margin-bottom: 3rem;
+ }
+
+ .playground-main {
+ gap: 3rem;
+ }
+}
+
+.component-demo {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+/* Theme Switcher */
+.theme-switcher {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 0.5rem;
+ color: var(--foreground);
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-size: 0.875rem;
+ font-weight: 500;
+}
+
+.theme-switcher:hover {
+ background: var(--accent);
+ border-color: var(--avax-red);
+ transform: translateY(-1px);
+}
+
+.theme-switcher:active {
+ transform: translateY(0);
+}
+
+.theme-switcher svg {
+ flex-shrink: 0;
+}
+
+.theme-label {
+ white-space: nowrap;
+}
+
+@media (max-width: 640px) {
+ .theme-switcher {
+ position: static;
+ margin: 0 auto 1rem;
+ width: fit-content;
+ }
+}
+
+/* Mobile responsive */
+@media (max-width: 768px) {
+ .playground > div:last-child {
+ padding: 0 1rem 1rem 1rem !important;
+ margin-left: 0 !important;
+ }
+
+ .playground-main {
+ grid-template-columns: 1fr;
+ }
+
+ .playground-header h1 {
+ font-size: 2rem;
+ }
+}
diff --git a/ui/playground/src/App.tsx b/ui/playground/src/App.tsx
new file mode 100644
index 00000000..b761d9fd
--- /dev/null
+++ b/ui/playground/src/App.tsx
@@ -0,0 +1,1805 @@
+/**
+ * Avalanche SDK UI Playground
+ *
+ * To create a new Avalanche application, run:
+ * npm create avalanche
+ *
+ * Or visit: https://github.com/ava-labs/avalanche-sdk-typescript
+ */
+
+import React, { useMemo, useState, useEffect } from 'react';
+import {
+ AvalancheProvider,
+ CrossChainTransfer,
+ WalletProvider,
+ WalletBalance,
+ WalletConnect,
+ WalletDropdown,
+ WalletMessage,
+ WalletPortfolio,
+ WalletTransactions,
+ WalletActivity,
+ NetworkSelector,
+ useAvalanche,
+ Button,
+ Badge,
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+ Input,
+ ThemeProvider,
+ AvalancheLogo,
+ ChainLogo,
+ ChainRow,
+ ChainSelectDropdown,
+ TokenChip,
+ TokenImage,
+ TokenRow,
+ TokenSelectDropdown,
+ AddressInput,
+ AmountInput,
+ Earn,
+ ICTT,
+ Stake,
+ type ChainOption,
+ type Token
+} from '@avalanche-sdk/ui';
+import { avalanche, avalancheFuji } from '@avalanche-sdk/client/chains';
+import { dispatch, echo } from './chains';
+import type { ICTTToken } from '@avalanche-sdk/ui';
+import { ThemeSwitcher } from './components/ThemeSwitcher';
+import { InstallCommand } from './components/InstallCommand';
+import { ComponentWithCode } from './components/ComponentWithCode';
+import { ChainBalancesSectionContent } from './components/ChainBalancesSection';
+import { SingleChainTransferDemo } from './components/SingleChainTransferDemo';
+import { SimpleTransfer } from './components/SimpleTransfer';
+import { SimpleICTT1 } from './components/SimpleICTT1';
+import { SimpleICTT2 } from './components/SimpleICTT2';
+import { ChainListDemo } from './components/ChainListDemo';
+import { EarnDemo1 } from './components/EarnDemo1';
+import { EarnDemo2 } from './components/EarnDemo2';
+import { NavigationSidebar } from './components/NavigationSidebar';
+import { Footer } from './components/Footer';
+import './App.css';
+
+function App() {
+ // Set favicon
+ useEffect(() => {
+ const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
+ if (link) {
+ link.href = '/favicon.ico';
+ link.type = 'image/x-icon';
+ } else {
+ const newLink = document.createElement('link');
+ newLink.rel = 'icon';
+ newLink.type = 'image/x-icon';
+ newLink.href = '/favicon.ico';
+ document.getElementsByTagName('head')[0].appendChild(newLink);
+ }
+ }, []);
+
+ // User provides their own chain list - including custom Avalanche subnets
+ const availableChains = [
+ Object.assign(avalanche, {
+ iconUrl: "https://raw.githubusercontent.com/ava-labs/avalanche-starter-kit/refs/heads/main/web-apps/public/chains/logo/43113.png",
+ testnet: false
+ }),
+ Object.assign(avalancheFuji, {
+ blockchainId: '0x7fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d5',
+ iconUrl: "https://raw.githubusercontent.com/ava-labs/avalanche-starter-kit/refs/heads/main/web-apps/public/chains/logo/43113.png",
+ testnet: true,
+ interchainContracts: {
+ teleporterRegistry: '0xF86Cb19Ad8405AEFa7d09C778215D2Cb6eBfB228',
+ teleporterManager: '0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf',
+ }
+ }),
+ dispatch,
+ echo,
+ ];
+
+ const chainShowcaseOptions = useMemo(() => {
+ return availableChains.map((chain) => ({
+ id: chain.id.toString(),
+ name: chain.name,
+ description: chain.testnet ? 'Testnet network' : 'Mainnet network',
+ iconUrl: (chain as any)?.iconUrl,
+ badge: chain.testnet ? 'T' : undefined,
+ testnet: chain.testnet ?? false,
+ icon: (
+
+ ),
+ }));
+ }, [availableChains]);
+
+ const [selectedChainId, setSelectedChainId] = useState(() => chainShowcaseOptions[0]?.id?.toString() ?? '');
+
+ // User provides well-known tokens for ICTT
+ const wellKnownTokens: ICTTToken[] = [
+ {
+ address: '0xa216e8ff9d8ac1bc4c37daab5fbe89b7d9b7514e',
+ name: 'Mock Token',
+ symbol: 'EXMP',
+ decimals: 18,
+ chainId: '173750',
+ ictt: {
+ home: '0x1a7c48cf8382c4d066addc7b825ceec3a454a7ac',
+ mirrors: [
+ { chainId: '779672', address: '0xa4637506d64d806529fbedcea160f371cde07311' },
+ ]
+ }
+ }
+ ];
+
+ const [activeToken, setActiveToken] = useState(() => wellKnownTokens[0]);
+
+ // Form state for Form Elements demo
+ const [addressValue, setAddressValue] = useState('');
+ const [amountValue, setAmountValue] = useState('');
+
+ return (
+
+
+ {
+ console.log('Wallet connected:', address);
+ }}
+ onError={(error) => {
+ console.error('Wallet error:', error);
+ }}
+ >
+
+
+
+ {/* Header */}
+
+
+
+
+
+
+ UI Playground
+
+
+ Explore Avalanche SDK components and themes
+
+
+
+
+
+
+
+
+ {/* Install Command Section */}
+
+
+
+ Get Started
+
+
+ Create a new Avalanche application with the SDK
+
+
+
+
+
+ {/* Theme Showcase Section */}
+
+
+
Design System
+
+ Core UI components with theme-aware styling
+
+
+
+
+ {/* Button Variants */}
+
New}
+ code={`import { Button } from '@avalanche-sdk/ui';
+
+Primary
+Secondary
+Outline
+Ghost
+Link
+Small
+Large
+Disabled `}
+ language="tsx"
+ >
+
+
+
+ New
+ Button Variants
+
+ Theme-aware button styles
+
+
+ Primary
+ Secondary
+ Outline
+ Ghost
+ Link
+
+
+
+
+ {/* Badge Variants */}
+
+ Default
+ Secondary
+ Destructive
+ Outline
+ Success
+ Warning
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+ Badge Variants
+ Status and category indicators
+
+
+
+ Default
+ Secondary
+ Destructive
+ Outline
+ Success
+ Warning
+
+
+
+
+
+ {/* Form Elements */}
+
+
+
{
+ setAddress(value);
+ console.log('Validation:', validation);
+ }}
+ showValidation={true}
+ />
+ setAmount(e.target.value)}
+ showMax={true}
+ showUSD={false}
+ />
+
+ Submit
+
+
+
+ );
+}`}
+ language="tsx"
+ className="md:col-span-2 lg:col-span-3"
+ >
+
+
+ Form Elements
+ Inputs and form controls
+
+
+ {
+ setAddressValue(value);
+ console.log('Address validation:', validation);
+ }}
+ showValidation={true}
+ />
+ setAmountValue(e.target.value)}
+ showMax={true}
+ showUSD={false}
+ placeholder="0.0"
+ />
+
+ Submit Transaction
+
+
+
+
+
+ {/* Chain Components */}
+
New}
+ code={`import { AvalancheProvider, ChainLogo, ChainRow, ChainSelectDropdown } from '@avalanche-sdk/ui';
+import { avalanche, avalancheFuji } from '@avalanche-sdk/client/chains';
+
+function App() {
+ const availableChains = [avalanche, avalancheFuji];
+
+ return (
+
+ {/* Chain Logo */}
+
+
+ {/* Chain Row */}
+
+
+ {/* Chain Select Dropdown */}
+ {
+ console.log('Selected:', chain);
+ }}
+ />
+
+ );
+}`}
+ language="tsx"
+ className="md:col-span-2 lg:col-span-1"
+ >
+
+
+
+ New
+ Chain Components
+
+ Avalanche chain identification and selection
+
+
+ {/* Chain Logo */}
+
+
+ Chain Logo
+
+
+
+
+
+
With Badge
+
+ P-Chain logo with badge overlay
+
+
+
+
+
+
+
Without Badge
+
+ Clean logo without overlay
+
+
+
+
+
+
+
+
+ {/* Chain Row */}
+
+
+ Chain Row
+
+
+ {chainShowcaseOptions.slice(0, 3).map((chainOption) => (
+
+ ))}
+
+
+
+
+
+ {/* Chain Select Dropdown */}
+
+
+ Chain Select Dropdown
+
+
{
+ setSelectedChainId(value);
+ console.log('Selected chain:', chain);
+ }}
+ placeholder="Select a chain"
+ />
+
+
+
+
+
+ {/* Token Components */}
+
New}
+ code={`import { AvalancheProvider, TokenImage, TokenChip, TokenRow } from '@avalanche-sdk/ui';
+import type { Token } from '@avalanche-sdk/ui';
+import { avalanche, avalancheFuji } from '@avalanche-sdk/client/chains';
+
+function App() {
+ const availableChains = [avalanche, avalancheFuji];
+
+ const token: Token = {
+ address: '0x...',
+ symbol: 'USDC',
+ name: 'USD Coin',
+ decimals: 18,
+ chainId: 43114,
+ };
+
+ return (
+
+ {/* Token Image */}
+
+
+ {/* Token Chip */}
+ console.log('Selected:', t)}
+ />
+
+ {/* Token Row */}
+ console.log('Clicked:', t)}
+ />
+
+ );
+}`}
+ language="tsx"
+ className="md:col-span-2 lg:col-span-1"
+ >
+
+
+
+ New
+ Token Components
+
+ Token display and selection
+
+
+
+
+ Token Images
+
+
+ {wellKnownTokens.map((token, idx) => (
+
+ ))}
+ {wellKnownTokens[0] && (
+
+ )}
+
+
+
+
+
+
+
+ Token Chips
+
+
+ {wellKnownTokens.map((token, idx) => (
+ console.log('Selected:', token.symbol)}
+ />
+ ))}
+ {wellKnownTokens[0] && (
+
+ )}
+
+
+
+
+
+
+
+ Token Rows
+
+
+ {wellKnownTokens.map((token, idx) => (
+ console.log('Clicked:', t.symbol)}
+ />
+ ))}
+
+
+
+
+
+
+
+
+ {/* Wallet Components */}
+
+
+
+ Wallet Integration
+
+
+ Connect and manage wallet connections
+
+
+
+ {/* Wallet Connect */}
+
+
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+ Wallet Connect
+
+ Connect your wallet to interact with Avalanche
+
+
+
+
+
+
+
+
+ {/* Wallet Dropdown - With XP Addresses */}
+
showXPAddresses}
+ code={`import { AvalancheProvider, WalletProvider, WalletDropdown } from '@avalanche-sdk/ui';
+import { avalanche, avalancheFuji } from '@avalanche-sdk/client/chains';
+
+function App() {
+ const availableChains = [avalanche, avalancheFuji];
+
+ return (
+
+
+ {/* With X-Chain and P-Chain addresses */}
+
+
+ {/* C-Chain only (default) */}
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+
+ Wallet Dropdown
+ showXPAddresses
+
+
+ View wallet address with X-Chain and P-Chain addresses
+
+
+
+
+
+
+
+
+ {/* Wallet Dropdown - Without XP Addresses */}
+
+
+
+ Wallet Dropdown
+ default
+
+
+ View wallet address (C-Chain only)
+
+
+
+
+
+
+
+ {/* Wallet Message */}
+
+
+
+
+ Wallet Message
+
+ Display wallet connection status messages
+
+
+
+
+
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+ Wallet Message
+
+ Display wallet connection status messages
+
+
+
+
+
+
+
+
+ {/* Network Selector */}
+
+
+
+
+ Network Selector
+
+ Switch between available networks
+
+
+
+
+
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+ Network Selector
+
+ Switch between available networks
+
+
+
+
+
+
+
+
+ {/* Wallet Balance - C-Chain */}
+
+
+ {/* C-Chain balance */}
+
+
+ {/* P-Chain balance */}
+
+
+ {/* X-Chain balance */}
+
+
+ {/* With USD value */}
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+ Wallet Balance
+
+ C-Chain balance display
+
+
+
+
+
+
+
+
+ {/* Wallet Portfolio */}
+
Glacier}
+ code={`import { AvalancheProvider, WalletProvider, WalletPortfolio } from '@avalanche-sdk/ui';
+import { avalanche, avalancheFuji } from '@avalanche-sdk/client/chains';
+
+function App() {
+ const availableChains = [avalanche, avalancheFuji];
+
+ return (
+
+
+
+
+
+ );
+}`}
+ language="tsx"
+ className="md:col-span-2"
+ >
+
+
+
+ Wallet Portfolio
+ Glacier
+
+
+ View ERC-20 token balances for your connected wallet
+
+
+
+
+
+
+
+
+ {/* Wallet Transactions */}
+
Glacier}
+ code={`import { AvalancheProvider, WalletProvider, WalletTransactions, Card, CardContent, CardDescription, CardHeader, CardTitle } from '@avalanche-sdk/ui';
+import { avalanche, avalancheFuji } from '@avalanche-sdk/client/chains';
+
+function App() {
+ const availableChains = [avalanche, avalancheFuji];
+
+ return (
+
+
+
+
+ Transaction History
+
+ View transaction history with input/output tokens
+
+
+
+
+
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+
+ Transaction History
+ Glacier
+
+
+ View transaction history with input/output tokens
+
+
+
+
+
+
+
+
+ {/* Wallet Activity - Combined Portfolio & Transactions */}
+
Glacier}
+ code={`import { AvalancheProvider, WalletProvider, WalletActivity, Card, CardContent, CardDescription, CardHeader, CardTitle } from '@avalanche-sdk/ui';
+import { avalanche, avalancheFuji } from '@avalanche-sdk/client/chains';
+
+function App() {
+ const availableChains = [avalanche, avalancheFuji];
+
+ return (
+
+
+
+
+ Wallet Activity
+
+ Combined view with tabs for token balances and transaction history
+
+
+
+
+
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+
+ Wallet Activity
+ Glacier
+
+
+ Combined view with tabs for token balances and transaction history
+
+
+
+
+
+
+
+
+
+
+ {/* Balance Components */}
+
+
+ {/* Provider Configuration */}
+
+
+
+ Provider Configuration
+
+
+ Configure chain providers and network settings
+
+
+
+
+
+ Available Chains
+
+ Chains available from AvalancheProvider when constructed
+
+
+
+
+
+
+
Current Chain:
+
+ {currentChain.name} (ID: {currentChain.id})
+
+
+
+
+
All Available Chains:
+
+ {availableChains.map((chain) => (
+
+ {chain.name} (ID: {chain.id})
+
+ ))}
+
+
+
+
+
+ );
+}
+
+function App() {
+ const availableChains = [avalanche, avalancheFuji];
+
+ return (
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+
+
+ {/* Transfer Components */}
+
+
+
+ Transfer Components
+
+
+ Transfer AVAX and tokens across chains
+
+
+
+
+
+ {
+ console.log('Cross-chain transfer successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Cross-chain transfer error:', error);
+ }}
+ />
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+ Cross-Chain Transfer
+
+ Move AVAX between C-Chain, P-Chain, and X-Chain
+
+
+
+ {
+ console.log('Cross-chain transfer successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Cross-chain transfer error:', error);
+ }}
+ />
+
+
+
+
+
+
('C');
+
+ const chains = [
+ { id: 'C' as const, name: 'C-Chain', description: 'Contract Chain' },
+ { id: 'P' as const, name: 'P-Chain', description: 'Platform Chain' },
+ { id: 'X' as const, name: 'X-Chain', description: 'Exchange Chain' },
+ ];
+
+ return (
+
+
+
+ Single Chain Transfer
+ Send AVAX to another address on the same chain
+
+
+
+ {chains.map((chain) => (
+ setSelectedChain(chain.id)}
+ className="flex items-center gap-3 p-4"
+ >
+
+ {chain.name}
+
+ ))}
+
+
+
+ {
+ console.log(\`\${selectedChain}-Chain transfer successful:\`, result);
+ }}
+ onError={(error) => {
+ console.error(\`\${selectedChain}-Chain transfer error:\`, error);
+ }}
+ />
+
+
+ );
+}
+
+function App() {
+ const availableChains = [avalanche, avalancheFuji];
+
+ return (
+
+
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+
{
+ setToAddress(predefinedAddress);
+ setAmount(predefinedAmount);
+ }, [setToAddress, setAmount]);
+
+ return (
+
+
+ Request Payment
+ Send AVAX on C-Chain with one click
+
+
+
+ To: {predefinedAddress}
+
+
+ Amount: {predefinedAmount} AVAX
+
+
+
+
+ );
+}
+
+function SimpleTransfer() {
+ return (
+ {
+ console.log('Transfer successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Transfer error:', error);
+ }}
+ >
+
+
+ );
+}
+
+function App() {
+ const availableChains = [avalanche, avalancheFuji];
+
+ return (
+
+
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+
+
+
+
+ {/* ICM (Interchain Messaging) Components */}
+
+
+
+ Interchain Messaging
+
+
+ Transfer tokens across Avalanche subnets
+
+
+
+
+
+
+
+
+ Interchain Token Transfer (ICTT)
+ Bridge tokens between different blockchain networks
+
+
+
+ {
+ console.log('ICTT bridge successful:', result);
+ }}
+ onError={(error) => {
+ console.error('ICTT bridge error:', error);
+ }}
+ />
+
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+
+ Interchain Token Transfer (ICTT)
+ Bridge tokens between different blockchain networks
+
+
+
+ {
+ console.log('ICTT bridge successful:', result);
+ }}
+ onError={(error) => {
+ console.error('ICTT bridge error:', error);
+ }}
+ />
+
+
+
+
+
{
+ setFromChain(echo.id.toString());
+ setToChain(dispatch.id.toString());
+ }, [setFromChain, setToChain]);
+
+ React.useEffect(() => {
+ if (address) {
+ setRecipientAddress(address);
+ }
+ }, [address, setRecipientAddress]);
+
+ const fromChainData = React.useMemo(() =>
+ availableChains.find(chain => chain.id.toString() === fromChain),
+ [availableChains, fromChain]
+ );
+
+ const toChainData = React.useMemo(() =>
+ availableChains.find(chain => chain.id.toString() === toChain),
+ [availableChains, toChain]
+ );
+
+ return (
+
+
+
+ {fromChainData && (
+
+ )}
+
+ From
+ {fromChainData?.name || 'Echo L1'}
+
+
+
+
+
+
+
+
+
+ To
+ {toChainData?.name || 'Dispatch L1'}
+
+ {toChainData && (
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+function SimpleICTT1() {
+ return (
+ {
+ console.log('ICTT transfer successful:', result);
+ }}
+ onError={(error) => {
+ console.error('ICTT transfer error:', error);
+ }}
+ >
+
+
+ Quick Bridge
+ Transfer tokens from Echo L1 to Dispatch L1
+
+
+
+
+
+
+
+
+ );
+}
+
+function App() {
+ const availableChains = [avalanche, avalancheFuji, echo, dispatch];
+
+ return (
+
+
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+
{
+ setFromChain(echo.id.toString());
+ setToChain(dispatch.id.toString());
+ }, [setFromChain, setToChain]);
+
+ React.useEffect(() => {
+ if (address) {
+ setRecipientAddress(address);
+ }
+ }, [address, setRecipientAddress]);
+
+ React.useEffect(() => {
+ if (availableTokens && availableTokens.length > 0) {
+ setSelectedToken(availableTokens[0]);
+ setAmount('15');
+ }
+ }, [availableTokens, setSelectedToken, setAmount]);
+
+ const fromChainData = React.useMemo(() =>
+ availableChains.find(chain => chain.id.toString() === fromChain),
+ [availableChains, fromChain]
+ );
+
+ const toChainData = React.useMemo(() =>
+ availableChains.find(chain => chain.id.toString() === toChain),
+ [availableChains, toChain]
+ );
+
+ return (
+
+
+
+ {fromChainData && (
+
+ )}
+
+ From
+ {fromChainData?.name || 'Echo L1'}
+
+
+
+
+
+
+
+
+
+ To
+ {toChainData?.name || 'Dispatch L1'}
+
+ {toChainData && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+function SimpleICTT2() {
+ return (
+ {
+ console.log('ICTT transfer successful:', result);
+ }}
+ onError={(error) => {
+ console.error('ICTT transfer error:', error);
+ }}
+ >
+
+
+ Instant Bridge
+ One-click token bridge from Echo L1 to Dispatch L1
+
+
+
+
+
+
+
+
+ );
+}
+
+function App() {
+ const availableChains = [avalanche, avalancheFuji, echo, dispatch];
+
+ return (
+
+
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+
+
+
+
+ {/* Staking Components */}
+
+
+
+ Staking Components
+
+
+ Stake AVAX and manage validators
+
+
+
+
+ {
+ console.log('Stake successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Stake error:', error);
+ }}
+ networkConfig={{
+ minStakeAvax: 1,
+ minEndSeconds: 24 * 60 * 60, // 24 hours
+ defaultDays: 1,
+ }}
+ />
+
+
+ );
+}`}
+ language="tsx"
+ >
+ {
+ console.log('Stake successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Stake error:', error);
+ }}
+ networkConfig={{
+ minStakeAvax: 1,
+ minEndSeconds: 24 * 60 * 60, // 24 hours
+ defaultDays: 1,
+ }}
+ />
+
+
+
+ {/* Earn Components */}
+
+
+
+ Earn Components
+
+
+ Earn yield on your assets with AAVE and Benqi liquidity pools
+
+
+
+ {/* Earn Component - Full Featured */}
+
+
+ {
+ console.log('Earn action successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Earn error:', error);
+ }}
+ onStatusChange={(status) => {
+ console.log('Earn status:', status);
+ }}
+ />
+
+
+ );
+}`}
+ language="tsx"
+ >
+ {
+ console.log('Earn action successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Earn error:', error);
+ }}
+ onStatusChange={(status) => {
+ console.log('Earn status:', status);
+ }}
+ />
+
+
+ {/* Single Pool Demos */}
+
+
{
+ console.log('Single pool action successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Single pool error:', error);
+ }}
+ />
+ );
+}
+
+function App() {
+ const availableChains = [avalanche];
+
+ return (
+
+
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+
+
{
+ console.log('Single pool action successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Single pool error:', error);
+ }}
+ />
+ );
+}
+
+function App() {
+ const availableChains = [avalanche];
+
+ return (
+
+
+
+
+
+ );
+}`}
+ language="tsx"
+ >
+
+
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/ui/playground/src/chains/dispatch.ts b/ui/playground/src/chains/dispatch.ts
new file mode 100644
index 00000000..068a41a1
--- /dev/null
+++ b/ui/playground/src/chains/dispatch.ts
@@ -0,0 +1,30 @@
+import { defineChain } from "viem";
+import type { ChainConfig } from "@avalanche-sdk/ui";
+
+export const dispatch = defineChain({
+ id: 779672,
+ name: 'Dispatch L1',
+ network: 'dispatch',
+ nativeCurrency: {
+ decimals: 18,
+ name: 'DIS',
+ symbol: 'DIS',
+ },
+ rpcUrls: {
+ default: {
+ http: ['https://subnets.avax.network/dispatch/testnet/rpc']
+ },
+ },
+ blockExplorers: {
+ default: { name: 'Explorer', url: 'https://subnets-test.avax.network/dispatch' },
+ },
+ // Custom variables
+ iconUrl: "https://raw.githubusercontent.com/ava-labs/avalanche-starter-kit/refs/heads/main/web-apps/public/chains/logo/779672.png",
+ blockchainId: "0x9f3be606497285d0ffbb5ac9ba24aa60346a9b1812479ed66cb329f394a4b1c7",
+ icm_registry: "0xF86Cb19Ad8405AEFa7d09C778215D2Cb6eBfB228",
+ testnet: true,
+ interchainContracts: {
+ teleporterRegistry: "0xF86Cb19Ad8405AEFa7d09C778215D2Cb6eBfB228",
+ teleporterManager: "0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf",
+ }
+}) as ChainConfig;
diff --git a/ui/playground/src/chains/echo.ts b/ui/playground/src/chains/echo.ts
new file mode 100644
index 00000000..82a69d9d
--- /dev/null
+++ b/ui/playground/src/chains/echo.ts
@@ -0,0 +1,30 @@
+import { defineChain } from "viem";
+import type { ChainConfig } from "@avalanche-sdk/ui";
+
+export const echo = defineChain({
+ id: 173750,
+ name: 'Echo L1',
+ network: 'echo',
+ nativeCurrency: {
+ decimals: 18,
+ name: 'Ech',
+ symbol: 'ECH',
+ },
+ rpcUrls: {
+ default: {
+ http: ['https://subnets.avax.network/echo/testnet/rpc']
+ },
+ },
+ blockExplorers: {
+ default: { name: 'Explorer', url: 'https://subnets-test.avax.network/echo' },
+ },
+ // Custom variables
+ iconUrl: "https://raw.githubusercontent.com/ava-labs/avalanche-starter-kit/refs/heads/main/web-apps/public/chains/logo/173750.png",
+ blockchainId: "0x1278d1be4b987e847be3465940eb5066c4604a7fbd6e086900823597d81af4c1",
+ icm_registry: "0xF86Cb19Ad8405AEFa7d09C778215D2Cb6eBfB228",
+ testnet: true,
+ interchainContracts: {
+ teleporterRegistry: "0xF86Cb19Ad8405AEFa7d09C778215D2Cb6eBfB228",
+ teleporterManager: "0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf",
+ }
+}) as ChainConfig;
diff --git a/ui/playground/src/chains/index.ts b/ui/playground/src/chains/index.ts
new file mode 100644
index 00000000..33a120d9
--- /dev/null
+++ b/ui/playground/src/chains/index.ts
@@ -0,0 +1,2 @@
+export { dispatch } from './dispatch';
+export { echo } from './echo';
diff --git a/ui/playground/src/components/ChainBalancesSection.tsx b/ui/playground/src/components/ChainBalancesSection.tsx
new file mode 100644
index 00000000..24b48e18
--- /dev/null
+++ b/ui/playground/src/components/ChainBalancesSection.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { useAvalanche, WalletBalance, Card, CardHeader, CardTitle, CardContent, Badge } from '@avalanche-sdk/ui';
+
+export function ChainBalancesSectionContent() {
+ const { chain } = useAvalanche();
+ const isAvalancheChain = chain.id === 43113 || chain.id === 43114; // Mainnet or Fuji
+
+ return (
+
+
+
+ Chain Balances
+
+
+ View balances across Avalanche chains
+
+
+
+
+
+
+ {chain.name}
+
+
+
+
+
+
+ {isAvalancheChain && (
+ <>
+
+
+
+ P-Chain
+
+
+
+
+
+
+
+
+
+ X-Chain
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+}
+
diff --git a/ui/playground/src/components/ChainListDemo.tsx b/ui/playground/src/components/ChainListDemo.tsx
new file mode 100644
index 00000000..94a8c02f
--- /dev/null
+++ b/ui/playground/src/components/ChainListDemo.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { useAvalanche, Card, CardContent, CardDescription, CardHeader, CardTitle, Badge } from '@avalanche-sdk/ui';
+
+export function ChainListDemo() {
+ const { availableChains, chain: currentChain } = useAvalanche();
+
+ return (
+
+
+
+ Available Chains
+
+ Chains available from AvalancheProvider when constructed
+
+
+
+
+
+
+
Current Chain:
+
+ {currentChain.name} (ID: {currentChain.id})
+
+
+
+
+
All Available Chains:
+
+ {availableChains.map((chain) => (
+
+ {chain.name} (ID: {chain.id})
+
+ ))}
+
+
+
+
+
+ The availableChains list is provided by the user when constructing the AvalancheProvider.
+ It includes: {availableChains.map(chain => chain.name).join(', ')}.
+
+
+ If no chains are provided, it defaults to Avalanche Mainnet and Fuji Testnet.
+
+
+
+
+
+ );
+}
diff --git a/ui/playground/src/components/CodeModal.tsx b/ui/playground/src/components/CodeModal.tsx
new file mode 100644
index 00000000..2f638be9
--- /dev/null
+++ b/ui/playground/src/components/CodeModal.tsx
@@ -0,0 +1,137 @@
+import React, { useState } from 'react';
+import { Copy, Check, X } from 'lucide-react';
+import { cn } from '@avalanche-sdk/ui';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
+
+interface CodeModalProps {
+ code: string;
+ language?: 'typescript' | 'tsx' | 'bash' | 'javascript';
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export function CodeModal({
+ code,
+ language = 'typescript',
+ isOpen,
+ onClose
+}: CodeModalProps) {
+ const [copied, setCopied] = useState(false);
+
+ const copyToClipboard = async () => {
+ try {
+ await navigator.clipboard.writeText(code);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ console.error('Failed to copy:', err);
+ }
+ };
+
+ // Map language to syntax highlighter language
+ const getLanguage = () => {
+ switch (language) {
+ case 'tsx':
+ return 'tsx';
+ case 'typescript':
+ return 'typescript';
+ case 'bash':
+ return 'bash';
+ case 'javascript':
+ return 'javascript';
+ default:
+ return 'typescript';
+ }
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+
e.stopPropagation()}
+ >
+ {/* Header */}
+
+
+
+
+ {language === 'tsx' ? 'tsx' : language === 'typescript' ? 'ts' : language}
+
+
+
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ {/* Code Content */}
+
+
+ {code}
+
+
+
+ {/* Footer */}
+
+
+ Press ESC to close
+
+
+
+
+ );
+}
+
diff --git a/ui/playground/src/components/CodeSnippet.tsx b/ui/playground/src/components/CodeSnippet.tsx
new file mode 100644
index 00000000..7ec41b14
--- /dev/null
+++ b/ui/playground/src/components/CodeSnippet.tsx
@@ -0,0 +1,109 @@
+import React, { useState } from 'react';
+import { Copy, Check } from 'lucide-react';
+import { cn } from '@avalanche-sdk/ui';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
+
+interface CodeSnippetProps {
+ code: string;
+ language?: 'typescript' | 'tsx' | 'bash' | 'javascript';
+ className?: string;
+}
+
+export function CodeSnippet({
+ code,
+ language = 'typescript',
+ className
+}: CodeSnippetProps) {
+ const [copied, setCopied] = useState(false);
+
+ const copyToClipboard = async () => {
+ try {
+ await navigator.clipboard.writeText(code);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ console.error('Failed to copy:', err);
+ }
+ };
+
+ // Map language to syntax highlighter language
+ const getLanguage = () => {
+ switch (language) {
+ case 'tsx':
+ return 'tsx';
+ case 'typescript':
+ return 'typescript';
+ case 'bash':
+ return 'bash';
+ case 'javascript':
+ return 'javascript';
+ default:
+ return 'typescript';
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ {language === 'tsx' ? 'tsx' : language === 'typescript' ? 'ts' : language}
+
+
+
+ {code}
+
+
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+ {copied && (
+
+ Copied!
+
+ )}
+
+
+ );
+}
diff --git a/ui/playground/src/components/ComponentWithCode.tsx b/ui/playground/src/components/ComponentWithCode.tsx
new file mode 100644
index 00000000..0d0efce1
--- /dev/null
+++ b/ui/playground/src/components/ComponentWithCode.tsx
@@ -0,0 +1,60 @@
+import React, { useState, useEffect } from 'react';
+import { Code2 } from 'lucide-react';
+import { CodeModal } from './CodeModal';
+
+interface ComponentWithCodeProps {
+ title: string;
+ description?: string;
+ badge?: React.ReactNode;
+ code: string;
+ language?: 'typescript' | 'tsx' | 'bash' | 'javascript';
+ children: React.ReactNode;
+ className?: string;
+}
+
+export function ComponentWithCode({
+ title,
+ description,
+ badge,
+ code,
+ language = 'tsx',
+ children,
+ className,
+}: ComponentWithCodeProps) {
+ const [showCode, setShowCode] = useState(false);
+
+ // Close modal on ESC key
+ useEffect(() => {
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key === 'Escape' && showCode) {
+ setShowCode(false);
+ }
+ };
+ window.addEventListener('keydown', handleEscape);
+ return () => window.removeEventListener('keydown', handleEscape);
+ }, [showCode]);
+
+ return (
+ <>
+
+
+ {children}
+ setShowCode(true)}
+ className="group absolute -top-3 -right-3 flex items-center justify-center gap-2 px-4 py-2 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 transition-all duration-300 text-xs font-medium text-muted-foreground focus:outline-none focus:ring-0 overflow-hidden shadow-lg border border-border/50 hover:border-primary/50 z-10"
+ aria-label="Show code"
+ >
+
+
+
+
+ setShowCode(false)}
+ />
+ >
+ );
+}
+
diff --git a/ui/playground/src/components/EarnDemo.tsx b/ui/playground/src/components/EarnDemo.tsx
new file mode 100644
index 00000000..6296d0da
--- /dev/null
+++ b/ui/playground/src/components/EarnDemo.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { Earn, EarnSinglePoolCard } from '@avalanche-sdk/ui';
+import { avalanche } from '@avalanche-sdk/client/chains';
+import type { ChainConfig } from '@avalanche-sdk/ui';
+
+export function EarnDemo() {
+ const avalancheChain = avalanche as ChainConfig;
+
+ return (
+
+
+
Earn Component
+
+ Earn yield on your assets by depositing them into AAVE liquidity pools on Avalanche.
+ View available pools, deposit assets, withdraw, and claim rewards.
+
+
+
{
+ console.log('Earn action successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Earn error:', error);
+ }}
+ onStatusChange={(status) => {
+ console.log('Earn status:', status);
+ }}
+ />
+
+
+
+
Single Pool Card
+
+ Display a specific pool by providing the provider, chain, and pool address.
+
+
+
+
+ {
+ console.log('Single pool action successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Single pool error:', error);
+ }}
+ />
+
+
+
+ {
+ console.log('Single pool action successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Single pool error:', error);
+ }}
+ />
+
+
+
+
+
+ );
+}
+
diff --git a/ui/playground/src/components/EarnDemo1.tsx b/ui/playground/src/components/EarnDemo1.tsx
new file mode 100644
index 00000000..13c3a845
--- /dev/null
+++ b/ui/playground/src/components/EarnDemo1.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { EarnSinglePoolCard } from '@avalanche-sdk/ui';
+import { avalanche } from '@avalanche-sdk/client/chains';
+import type { ChainConfig } from '@avalanche-sdk/ui';
+
+export function EarnDemo1() {
+ const avalancheChain = avalanche as ChainConfig;
+
+ return (
+ {
+ console.log('Single pool action successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Single pool error:', error);
+ }}
+ />
+ );
+}
+
diff --git a/ui/playground/src/components/EarnDemo2.tsx b/ui/playground/src/components/EarnDemo2.tsx
new file mode 100644
index 00000000..4fdd5969
--- /dev/null
+++ b/ui/playground/src/components/EarnDemo2.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { EarnSinglePoolCard } from '@avalanche-sdk/ui';
+import { avalanche } from '@avalanche-sdk/client/chains';
+import type { ChainConfig } from '@avalanche-sdk/ui';
+
+export function EarnDemo2() {
+ const avalancheChain = avalanche as ChainConfig;
+
+ return (
+ {
+ console.log('Single pool action successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Single pool error:', error);
+ }}
+ />
+ );
+}
+
diff --git a/ui/playground/src/components/Footer.tsx b/ui/playground/src/components/Footer.tsx
new file mode 100644
index 00000000..39c00545
--- /dev/null
+++ b/ui/playground/src/components/Footer.tsx
@@ -0,0 +1,188 @@
+import React from 'react';
+import { Github, ExternalLink, BookOpen, Code, Zap, Shield } from 'lucide-react';
+import { AvalancheLogo } from '@avalanche-sdk/ui';
+
+export function Footer() {
+ const currentYear = new Date().getFullYear();
+
+ const footerLinks = {
+ resources: [
+ {
+ label: 'GitHub Repository',
+ href: 'https://github.com/ava-labs/avalanche-sdk-typescript',
+ icon: Github,
+ },
+ {
+ label: 'Documentation',
+ href: 'https://docs.avax.network',
+ icon: BookOpen,
+ },
+ {
+ label: 'Avalanche Network',
+ href: 'https://avax.network',
+ icon: ExternalLink,
+ },
+ ],
+ development: [
+ {
+ label: 'API Reference',
+ href: 'https://github.com/ava-labs/avalanche-sdk-typescript',
+ icon: Code,
+ },
+ {
+ label: 'Examples',
+ href: 'https://github.com/ava-labs/avalanche-sdk-typescript/tree/main/examples',
+ icon: Zap,
+ },
+ {
+ label: 'Security',
+ href: 'https://github.com/ava-labs/avalanche-sdk-typescript/security',
+ icon: Shield,
+ },
+ ],
+ };
+
+ return (
+
+ );
+}
diff --git a/ui/playground/src/components/InstallCommand.tsx b/ui/playground/src/components/InstallCommand.tsx
new file mode 100644
index 00000000..fd555974
--- /dev/null
+++ b/ui/playground/src/components/InstallCommand.tsx
@@ -0,0 +1,130 @@
+import React from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle, Badge } from '@avalanche-sdk/ui';
+import { Copy, Check, Rocket, Package } from 'lucide-react';
+import { useState } from 'react';
+import { cn } from '@avalanche-sdk/ui';
+
+interface CommandBlockProps {
+ title: string;
+ description: string;
+ command: string;
+ badge?: string;
+ icon: React.ComponentType<{ className?: string }>;
+}
+
+function CommandBlock({ title, description, command, badge, icon: Icon }: CommandBlockProps) {
+ const [copied, setCopied] = useState(false);
+
+ const copyToClipboard = async () => {
+ try {
+ await navigator.clipboard.writeText(command);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ console.error('Failed to copy:', err);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ {title}
+
+ {badge && (
+
+ {badge}
+
+ )}
+
+
+ {description}
+
+
+
+
+
+
+
+
+
+
+
+ Terminal
+
+ •
+ bash
+
+
+ npm
+ {' '}
+ {command.includes('create') ? (
+ <>
+ create
+ {' '}
+ avalanche
+ >
+ ) : (
+ <>
+ install
+ {' '}
+ @avalanche-sdk/ui
+ >
+ )}
+
+
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+ {copied && (
+
+ Copied!
+
+ )}
+
+
+
+ );
+}
+
+export function InstallCommand() {
+ return (
+
+
+
+
+ );
+}
+
diff --git a/ui/playground/src/components/NavigationSidebar.tsx b/ui/playground/src/components/NavigationSidebar.tsx
new file mode 100644
index 00000000..f67142a2
--- /dev/null
+++ b/ui/playground/src/components/NavigationSidebar.tsx
@@ -0,0 +1,163 @@
+import React, { useState, useEffect } from 'react';
+import {
+ ChevronRight,
+ Palette,
+ Wallet,
+ Coins,
+ Settings,
+ ArrowRightLeft,
+ MessageSquare,
+ TrendingUp,
+ ArrowUp,
+ Rocket,
+ DollarSign,
+ HandCoins
+} from 'lucide-react';
+
+interface NavigationItem {
+ id: string;
+ label: string;
+ icon: React.ComponentType<{ className?: string }>;
+}
+
+interface NavigationSidebarProps {
+ className?: string;
+}
+
+const navigationItems: NavigationItem[] = [
+ { id: 'install-command', label: 'Get Started', icon: Rocket },
+ { id: 'theme-showcase', label: 'Design System', icon: Palette },
+ { id: 'wallet-components', label: 'Wallet Integration', icon: Wallet },
+ { id: 'chain-balances', label: 'Chain Balances', icon: HandCoins },
+ { id: 'provider-config', label: 'Provider Config', icon: Settings },
+ { id: 'transfer-components', label: 'Transfers', icon: ArrowRightLeft },
+ { id: 'icm-components', label: 'Interchain Messaging', icon: MessageSquare },
+ { id: 'staking-components', label: 'Staking', icon: TrendingUp },
+ { id: 'earn-components', label: 'Earn (AAVE & Benqi)', icon: DollarSign },
+];
+
+export function NavigationSidebar({ className = '' }: NavigationSidebarProps) {
+ const [activeSection, setActiveSection] = useState('install-command');
+ const [showScrollTop, setShowScrollTop] = useState(false);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ const sections = navigationItems.map(item => document.getElementById(item.id));
+ const scrollPosition = window.scrollY + 200;
+
+ // Update active section
+ for (let i = sections.length - 1; i >= 0; i--) {
+ const section = sections[i];
+ if (section && section.offsetTop <= scrollPosition) {
+ setActiveSection(navigationItems[i].id);
+ break;
+ }
+ }
+
+ // Show/hide scroll to top button
+ setShowScrollTop(window.scrollY > 400);
+ };
+
+ window.addEventListener('scroll', handleScroll, { passive: true });
+ handleScroll();
+
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, []);
+
+ const scrollToSection = (sectionId: string) => {
+ const element = document.getElementById(sectionId);
+ if (element) {
+ const headerHeight = 180;
+ const elementPosition = element.offsetTop - headerHeight;
+
+ window.scrollTo({
+ top: elementPosition,
+ behavior: 'smooth'
+ });
+ }
+ };
+
+ const scrollToTop = () => {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth'
+ });
+ };
+
+ return (
+ <>
+
+
+ {/* Navigation Header */}
+
+
+ Quick Navigation
+
+
+ Jump to any section
+
+
+
+ {/* Navigation Items */}
+
+ {navigationItems.map((item) => {
+ const isActive = activeSection === item.id;
+ const Icon = item.icon;
+
+ return (
+
+ scrollToSection(item.id)}
+ >
+
+ {item.label}
+ {isActive && (
+
+ )}
+
+
+ );
+ })}
+
+
+ {/* Scroll to Top Button */}
+ {showScrollTop && (
+
+ )}
+
+
+
+ {/* Mobile Scroll to Top Button */}
+ {showScrollTop && (
+
+
+
+ )}
+ >
+ );
+}
diff --git a/ui/playground/src/components/SimpleICTT.tsx b/ui/playground/src/components/SimpleICTT.tsx
new file mode 100644
index 00000000..350a6bb7
--- /dev/null
+++ b/ui/playground/src/components/SimpleICTT.tsx
@@ -0,0 +1,200 @@
+import React from 'react';
+import { ICTTProvider, useICTTContext, ICTTTokenModeToggle, ICTTAmountInput, ICTTButtons, Card, CardContent, CardDescription, CardHeader, CardTitle, Badge } from '@avalanche-sdk/ui';
+import { useAvalanche } from '@avalanche-sdk/ui';
+import { useWalletContext } from '@avalanche-sdk/ui';
+import { echo } from '../chains/echo';
+import { dispatch } from '../chains/dispatch';
+
+// Internal component that uses the ICTT context
+function SimpleICTTContent() {
+ const { fromChain, toChain, setFromChain, setToChain, setRecipientAddress } = useICTTContext();
+ const { availableChains } = useAvalanche();
+ const { address } = useWalletContext();
+
+ // Set chains on mount
+ React.useEffect(() => {
+ // Set chain IDs as strings (ICTTChain type is string)
+ setFromChain(echo.id.toString());
+ setToChain(dispatch.id.toString());
+ }, [setFromChain, setToChain]);
+
+ // Auto-set recipient address to current wallet address
+ React.useEffect(() => {
+ if (address) {
+ setRecipientAddress(address);
+ }
+ }, [address, setRecipientAddress]);
+
+ // Get chain objects from IDs for display
+ const fromChainData = React.useMemo(() =>
+ availableChains.find(chain => chain.id.toString() === fromChain),
+ [availableChains, fromChain]
+ );
+
+ const toChainData = React.useMemo(() =>
+ availableChains.find(chain => chain.id.toString() === toChain),
+ [availableChains, toChain]
+ );
+
+ return (
+
+ {/* Display selected chains */}
+
+
+ {fromChainData?.iconUrl && (
+
+ )}
+
+ {fromChainData?.name || 'Echo L1'}
+
+
→
+ {toChainData?.iconUrl && (
+
+ )}
+
+ {toChainData?.name || 'Dispatch L1'}
+
+
+
+
+ {/* Token mode toggle - manual mode disabled */}
+
+
+
+
+ {/* Recipient address automatically set to current wallet address - input hidden */}
+
+
+
+ );
+}
+
+// Custom Simple ICTT Component using ICTTProvider
+export function SimpleICTT() {
+ return (
+ {
+ console.log('ICTT transfer successful:', result);
+ }}
+ onError={(error) => {
+ console.error('ICTT transfer error:', error);
+ }}
+ >
+
+
+ Quick Bridge
+ Transfer tokens from Echo L1 to Dispatch L1
+
+
+
+
+
+
+ );
+}
+
+// Quick variation without amount input and token already selected
+function SimpleICTTQuickContent() {
+ const { fromChain, toChain, setFromChain, setToChain, setRecipientAddress, setSelectedToken, setAmount, availableTokens } = useICTTContext();
+ const { availableChains } = useAvalanche();
+ const { address } = useWalletContext();
+
+ // Set chains on mount
+ React.useEffect(() => {
+ setFromChain(echo.id.toString());
+ setToChain(dispatch.id.toString());
+ }, [setFromChain, setToChain]);
+
+ // Auto-set recipient address to current wallet address
+ React.useEffect(() => {
+ if (address) {
+ setRecipientAddress(address);
+ }
+ }, [address, setRecipientAddress]);
+
+ // Auto-select first available token and set amount
+ React.useEffect(() => {
+ if (availableTokens && availableTokens.length > 0) {
+ setSelectedToken(availableTokens[0]);
+ setAmount('1'); // Set a default amount
+ }
+ }, [availableTokens, setSelectedToken, setAmount]);
+
+ // Get chain objects from IDs for display
+ const fromChainData = React.useMemo(() =>
+ availableChains.find(chain => chain.id.toString() === fromChain),
+ [availableChains, fromChain]
+ );
+
+ const toChainData = React.useMemo(() =>
+ availableChains.find(chain => chain.id.toString() === toChain),
+ [availableChains, toChain]
+ );
+
+ return (
+
+ {/* Display selected chains */}
+
+
+ {fromChainData?.iconUrl && (
+
+ )}
+
+ {fromChainData?.name || 'Echo L1'}
+
+
→
+ {toChainData?.iconUrl && (
+
+ )}
+
+ {toChainData?.name || 'Dispatch L1'}
+
+
+
+
+ {/* Token and amount are auto-set, only show buttons */}
+
+
+ );
+}
+
+// Quick ICTT Component with token already selected
+export function SimpleICTTQuick() {
+ return (
+ {
+ console.log('ICTT transfer successful:', result);
+ }}
+ onError={(error) => {
+ console.error('ICTT transfer error:', error);
+ }}
+ >
+
+
+ Instant Bridge
+ One-click token bridge from Echo L1 to Dispatch L1
+
+
+
+
+
+
+ );
+}
+
diff --git a/ui/playground/src/components/SimpleICTT1.tsx b/ui/playground/src/components/SimpleICTT1.tsx
new file mode 100644
index 00000000..0fd79c16
--- /dev/null
+++ b/ui/playground/src/components/SimpleICTT1.tsx
@@ -0,0 +1,118 @@
+import React from 'react';
+import { ICTTProvider, useICTTContext, ICTTTokenModeToggle, ICTTAmountInput, ICTTButtons, Card, CardContent, CardDescription, CardHeader, CardTitle, TokenChip, WalletConnectionOverlay, AvalancheChainOverlay, ChainLogo } from '@avalanche-sdk/ui';
+import { useAvalanche } from '@avalanche-sdk/ui';
+import { useWalletContext } from '@avalanche-sdk/ui';
+import { echo } from '../chains/echo';
+import { dispatch } from '../chains/dispatch';
+
+// Internal component that uses the ICTT context
+function SimpleICTT1Content() {
+ const { fromChain, toChain, setFromChain, setToChain, setRecipientAddress, selectedToken } = useICTTContext();
+ const { availableChains } = useAvalanche();
+ const { address } = useWalletContext();
+
+ // Set chains on mount
+ React.useEffect(() => {
+ setFromChain(echo.id.toString());
+ setToChain(dispatch.id.toString());
+ }, [setFromChain, setToChain]);
+
+ // Auto-set recipient address to current wallet address
+ React.useEffect(() => {
+ if (address) {
+ setRecipientAddress(address);
+ }
+ }, [address, setRecipientAddress]);
+
+ // Get chain objects from IDs for display
+ const fromChainData = React.useMemo(() =>
+ availableChains.find(chain => chain.id.toString() === fromChain),
+ [availableChains, fromChain]
+ );
+
+ const toChainData = React.useMemo(() =>
+ availableChains.find(chain => chain.id.toString() === toChain),
+ [availableChains, toChain]
+ );
+
+ return (
+
+ {/* Clean, minimal transfer preview */}
+
+ {/* From Chain */}
+
+ {fromChainData && (
+
+ )}
+
+ From
+ {fromChainData?.name || 'Echo L1'}
+
+
+
+ {/* Arrow */}
+
+
+
+
+ {/* To Chain */}
+
+
+ To
+ {toChainData?.name || 'Dispatch L1'}
+
+ {toChainData && (
+
+ )}
+
+
+
+ {/* Token mode toggle - manual mode disabled */}
+
+
+
+
+
+
+ );
+}
+
+// SimpleICTT1: Full featured with token chip display
+export function SimpleICTT1() {
+ return (
+ {
+ console.log('ICTT transfer successful:', result);
+ }}
+ onError={(error) => {
+ console.error('ICTT transfer error:', error);
+ }}
+ >
+
+
+ Quick Bridge
+ Transfer tokens from Echo L1 to Dispatch L1
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/ui/playground/src/components/SimpleICTT2.tsx b/ui/playground/src/components/SimpleICTT2.tsx
new file mode 100644
index 00000000..e1f90f91
--- /dev/null
+++ b/ui/playground/src/components/SimpleICTT2.tsx
@@ -0,0 +1,122 @@
+import React from 'react';
+import { ICTTProvider, useICTTContext, ICTTButtons, Card, CardContent, CardDescription, CardHeader, CardTitle, TokenChip, WalletConnectionOverlay, AvalancheChainOverlay, ChainLogo } from '@avalanche-sdk/ui';
+import { useAvalanche } from '@avalanche-sdk/ui';
+import { useWalletContext } from '@avalanche-sdk/ui';
+import { echo } from '../chains/echo';
+import { dispatch } from '../chains/dispatch';
+
+// Internal component that uses the ICTT context
+function SimpleICTT2Content() {
+ const { fromChain, toChain, setFromChain, setToChain, setRecipientAddress, setSelectedToken, setAmount, availableTokens, selectedToken, amount } = useICTTContext();
+ const { availableChains } = useAvalanche();
+ const { address } = useWalletContext();
+
+ // Set chains on mount
+ React.useEffect(() => {
+ setFromChain(echo.id.toString());
+ setToChain(dispatch.id.toString());
+ }, [setFromChain, setToChain]);
+
+ // Auto-set recipient address to current wallet address
+ React.useEffect(() => {
+ if (address) {
+ setRecipientAddress(address);
+ }
+ }, [address, setRecipientAddress]);
+
+ // Auto-select first available token and set amount
+ React.useEffect(() => {
+ if (availableTokens && availableTokens.length > 0) {
+ setSelectedToken(availableTokens[0]);
+ setAmount('15'); // Set a default amount
+ }
+ }, [availableTokens, setSelectedToken, setAmount]);
+
+ // Get chain objects from IDs for display
+ const fromChainData = React.useMemo(() =>
+ availableChains.find(chain => chain.id.toString() === fromChain),
+ [availableChains, fromChain]
+ );
+
+ const toChainData = React.useMemo(() =>
+ availableChains.find(chain => chain.id.toString() === toChain),
+ [availableChains, toChain]
+ );
+
+ return (
+
+ {/* Clean, minimal transfer preview */}
+
+ {/* From Chain */}
+
+ {fromChainData && (
+
+ )}
+
+ From
+ {fromChainData?.name || 'Echo L1'}
+
+
+
+ {/* Arrow */}
+
+
+
+
+ {/* To Chain */}
+
+
+ To
+ {toChainData?.name || 'Dispatch L1'}
+
+ {toChainData && (
+
+ )}
+
+
+
+ {/* Action buttons */}
+
+
+ );
+}
+
+// SimpleICTT2: Instant bridge with token pre-selected
+export function SimpleICTT2() {
+ return (
+ {
+ console.log('ICTT transfer successful:', result);
+ }}
+ onError={(error) => {
+ console.error('ICTT transfer error:', error);
+ }}
+ >
+
+
+ Instant Bridge
+ One-click token bridge from Echo L1 to Dispatch L1
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/ui/playground/src/components/SimpleTransfer.tsx b/ui/playground/src/components/SimpleTransfer.tsx
new file mode 100644
index 00000000..51d5a2d7
--- /dev/null
+++ b/ui/playground/src/components/SimpleTransfer.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { TransferProvider, useTransferContext, TransferButton, Card, CardContent, CardDescription, CardHeader, CardTitle } from '@avalanche-sdk/ui';
+
+// Internal component that uses the transfer context
+function SimpleTransferContent() {
+ const { setAmount, setToAddress } = useTransferContext();
+ const predefinedAddress = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"; // Example address
+ const predefinedAmount = "0.1";
+
+ // Set the predefined values when component mounts
+ React.useEffect(() => {
+ setToAddress(predefinedAddress);
+ setAmount(predefinedAmount);
+ }, [setToAddress, setAmount]);
+
+ return (
+
+
+ Request Payment
+ Send AVAX on C-Chain with one click
+
+
+
+ To: {predefinedAddress}
+
+
+ Amount: {predefinedAmount} AVAX
+
+
+
+
+ );
+}
+
+// Custom Simple Transfer Component using TransferProvider
+export function SimpleTransfer() {
+ return (
+ {
+ console.log('Transfer successful:', result);
+ }}
+ onError={(error) => {
+ console.error('Transfer error:', error);
+ }}
+ >
+
+
+ );
+}
diff --git a/ui/playground/src/components/SingleChainTransferDemo.tsx b/ui/playground/src/components/SingleChainTransferDemo.tsx
new file mode 100644
index 00000000..b284843e
--- /dev/null
+++ b/ui/playground/src/components/SingleChainTransferDemo.tsx
@@ -0,0 +1,53 @@
+import React, { useState } from 'react';
+import { Transfer, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, ChainLogo } from '@avalanche-sdk/ui';
+
+export function SingleChainTransferDemo() {
+ const [selectedChain, setSelectedChain] = useState<'C' | 'P' | 'X'>('C');
+
+ const chains = [
+ { id: 'C' as const, name: 'C-Chain', description: 'Contract Chain' },
+ { id: 'P' as const, name: 'P-Chain', description: 'Platform Chain' },
+ { id: 'X' as const, name: 'X-Chain', description: 'Exchange Chain' },
+ ];
+
+ return (
+
+
+
+ Single Chain Transfer
+ Send AVAX to another address on the same chain
+
+
+ {/* Chain Selection Buttons */}
+
+ {chains.map((chain) => (
+ setSelectedChain(chain.id)}
+ className="flex items-center gap-3 p-4"
+ >
+
+ {chain.name}
+
+ ))}
+
+
+
+ {
+ console.log(`${selectedChain}-Chain transfer successful:`, result);
+ }}
+ onError={(error) => {
+ console.error(`${selectedChain}-Chain transfer error:`, error);
+ }}
+ />
+
+
+ );
+}
diff --git a/ui/playground/src/components/ThemeSwitcher.tsx b/ui/playground/src/components/ThemeSwitcher.tsx
new file mode 100644
index 00000000..42213740
--- /dev/null
+++ b/ui/playground/src/components/ThemeSwitcher.tsx
@@ -0,0 +1,147 @@
+import React, { useState } from 'react';
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
+import { useTheme, Theme, AvalancheLogo } from '@avalanche-sdk/ui';
+import { cn, text, pressable } from '@avalanche-sdk/ui';
+
+export function ThemeSwitcher() {
+ const { theme, mode, setTheme, toggleMode } = useTheme();
+ const [open, setOpen] = useState(false);
+
+ const themes: { value: Theme; label: string; emoji: string }[] = [
+ { value: 'avalanche', label: 'Avalanche', emoji: '🏔️' },
+ { value: 'cyber', label: 'Cyber', emoji: '🤖' },
+ { value: 'matrix', label: 'Matrix', emoji: '🟢' },
+ { value: 'amber', label: 'Amber', emoji: '🟤' },
+ { value: 'amethyst', label: 'Amethyst', emoji: '💜' },
+ ];
+
+ const currentTheme = themes.find((t) => t.value === theme);
+
+ return (
+
+ {/* Theme Dropdown */}
+
+
+
+ {currentTheme?.value === 'avalanche' ? (
+
+ ) : (
+ {currentTheme?.emoji}
+ )}
+
+ {currentTheme?.label}
+
+
+
+
+
+
+
+
+
+ {/* Theme Selection */}
+
+
+ Theme
+
+
+ {themes.map((t) => (
+
+ setTheme(t.value)}
+ className={cn(
+ 'w-full flex items-center gap-2 px-2 py-1.5 text-left rounded-sm transition-colors',
+ theme === t.value
+ ? 'bg-primary text-primary-foreground'
+ : 'hover:bg-accent hover:text-accent-foreground',
+ )}
+ aria-label={`Switch to ${t.label} theme`}
+ >
+ {t.value === 'avalanche' ? (
+
+ ) : (
+ {t.emoji}
+ )}
+ {t.label}
+ {theme === t.value && (
+
+
+
+ )}
+
+
+ ))}
+
+
+
+
+
+
+ {/* Mode Toggle Button */}
+
+ {mode === 'light' ? (
+ // Moon icon for dark mode
+
+
+
+ ) : (
+ // Sun icon for light mode
+
+
+
+
+ )}
+
+ {mode === 'light' ? 'Dark' : 'Light'}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui/playground/src/index.css b/ui/playground/src/index.css
new file mode 100644
index 00000000..5056df98
--- /dev/null
+++ b/ui/playground/src/index.css
@@ -0,0 +1,44 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
+
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+#root {
+ width: 100%;
+}
diff --git a/ui/playground/src/main.tsx b/ui/playground/src/main.tsx
new file mode 100644
index 00000000..bee21b6f
--- /dev/null
+++ b/ui/playground/src/main.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App.tsx';
+import '@avalanche-sdk/ui/styles/index.css';
+import './index.css';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+);
diff --git a/ui/playground/tailwind.config.js b/ui/playground/tailwind.config.js
new file mode 100644
index 00000000..1a731258
--- /dev/null
+++ b/ui/playground/tailwind.config.js
@@ -0,0 +1,54 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ './index.html',
+ './src/**/*.{js,ts,jsx,tsx}',
+ '../src/**/*.{js,ts,jsx,tsx}',
+ ],
+ theme: {
+ extend: {
+ colors: {
+ background: 'var(--background)',
+ foreground: 'var(--foreground)',
+ card: {
+ DEFAULT: 'var(--card)',
+ foreground: 'var(--card-foreground)',
+ },
+ popover: {
+ DEFAULT: 'var(--popover)',
+ foreground: 'var(--popover-foreground)',
+ },
+ primary: {
+ DEFAULT: 'var(--primary)',
+ foreground: 'var(--primary-foreground)',
+ },
+ secondary: {
+ DEFAULT: 'var(--secondary)',
+ foreground: 'var(--secondary-foreground)',
+ },
+ muted: {
+ DEFAULT: 'var(--muted)',
+ foreground: 'var(--muted-foreground)',
+ },
+ accent: {
+ DEFAULT: 'var(--accent)',
+ foreground: 'var(--accent-foreground)',
+ },
+ destructive: {
+ DEFAULT: 'var(--destructive)',
+ foreground: 'var(--destructive-foreground)',
+ },
+ border: 'var(--border)',
+ input: 'var(--input)',
+ ring: 'var(--ring)',
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)',
+ },
+ },
+ },
+ plugins: [require('tailwindcss-animate')],
+};
+
diff --git a/ui/playground/vite.config.ts b/ui/playground/vite.config.ts
new file mode 100644
index 00000000..53a318ed
--- /dev/null
+++ b/ui/playground/vite.config.ts
@@ -0,0 +1,25 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { resolve } from 'path';
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, './src'),
+ process: 'process/browser',
+ buffer: 'buffer',
+ util: 'util'
+ },
+ },
+ css: {
+ postcss: './postcss.config.js',
+ },
+ define: {
+ global: 'globalThis',
+ 'process.env': {}
+ },
+ optimizeDeps: {
+ include: ['buffer', 'process']
+ }
+});
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
new file mode 100644
index 00000000..92b901f2
--- /dev/null
+++ b/ui/pnpm-lock.yaml
@@ -0,0 +1,6853 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@avalanche-sdk/chainkit':
+ specifier: 0.3.0-alpha.8
+ version: 0.3.0-alpha.8
+ '@avalanche-sdk/client':
+ specifier: ^0.0.4-alpha.16
+ version: 0.0.4-alpha.16(typescript@5.8.2)(zod@4.1.12)
+ '@avalanche-sdk/interchain':
+ specifier: ^0.1.1-alpha.1
+ version: 0.1.1-alpha.1(typescript@5.8.2)(viem@2.38.6(typescript@5.8.2)(zod@4.1.12))(zod@4.1.12)
+ '@floating-ui/react':
+ specifier: ^0.27.13
+ version: 0.27.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-dialog':
+ specifier: ^1.1.14
+ version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-dropdown-menu':
+ specifier: ^2.1.6
+ version: 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-label':
+ specifier: ^2.1.0
+ version: 2.1.8(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-popover':
+ specifier: ^1.1.14
+ version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-select':
+ specifier: ^2.1.2
+ version: 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot':
+ specifier: ^1.1.0
+ version: 1.2.3(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-tabs':
+ specifier: ^1.1.13
+ version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-toast':
+ specifier: ^1.2.14
+ version: 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ class-variance-authority:
+ specifier: ^0.7.1
+ version: 0.7.1
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ lucide-react:
+ specifier: ^0.468.0
+ version: 0.468.0(react@18.3.1)
+ tailwind-merge:
+ specifier: ^3.2.0
+ version: 3.3.1
+ usehooks-ts:
+ specifier: ^3.1.1
+ version: 3.1.1(react@18.3.1)
+ devDependencies:
+ '@testing-library/jest-dom':
+ specifier: 6.4.7
+ version: 6.4.7(vitest@3.2.4)
+ '@testing-library/react':
+ specifier: ^16.3.0
+ version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@testing-library/user-event':
+ specifier: ^14.6.1
+ version: 14.6.1(@testing-library/dom@10.4.1)
+ '@types/node':
+ specifier: ^22.13.10
+ version: 22.19.0
+ '@types/react':
+ specifier: ^18.2.43
+ version: 18.3.26
+ '@types/react-dom':
+ specifier: ^18.2.17
+ version: 18.3.7(@types/react@18.3.26)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.7.0(vite@5.4.21(@types/node@22.19.0))
+ '@vitest/coverage-v8':
+ specifier: ^3.0.5
+ version: 3.2.4(vitest@3.2.4)
+ '@vitest/ui':
+ specifier: ^3.0.5
+ version: 3.2.4(vitest@3.2.4)
+ autoprefixer:
+ specifier: ^10.4.19
+ version: 10.4.21(postcss@8.5.6)
+ esbuild-fix-imports-plugin:
+ specifier: ^1.0.19
+ version: 1.0.23
+ esbuild-plugin-babel:
+ specifier: ^0.2.3
+ version: 0.2.3(@babel/core@7.28.5)
+ glob:
+ specifier: ^11.0.1
+ version: 11.0.3
+ jsdom:
+ specifier: ^24.1.0
+ version: 24.1.3
+ postcss:
+ specifier: ^8
+ version: 8.5.6
+ postcss-import:
+ specifier: ^16.1.0
+ version: 16.1.1(postcss@8.5.6)
+ postcss-load-config:
+ specifier: ^6.0.1
+ version: 6.0.1(jiti@1.21.7)(postcss@8.5.6)
+ react:
+ specifier: ^18.2.43
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.2.17
+ version: 18.3.1(react@18.3.1)
+ rollup-plugin-preserve-use-client:
+ specifier: ^3.0.1
+ version: 3.0.1(rollup@4.52.5)
+ tailwindcss:
+ specifier: ^3.4.6
+ version: 3.4.18
+ tailwindcss-animate:
+ specifier: ^1.0.7
+ version: 1.0.7(tailwindcss@3.4.18)
+ tscpaths:
+ specifier: ^0.0.9
+ version: 0.0.9
+ tsup:
+ specifier: ^8.3.5
+ version: 8.5.0(@microsoft/api-extractor@7.54.0(@types/node@22.19.0))(jiti@1.21.7)(postcss@8.5.6)(typescript@5.8.2)
+ viem:
+ specifier: ^2.21.45
+ version: 2.38.6(typescript@5.8.2)(zod@4.1.12)
+ vite:
+ specifier: ^5.4.20
+ version: 5.4.21(@types/node@22.19.0)
+ vite-plugin-dts:
+ specifier: ^4.5.3
+ version: 4.5.4(@types/node@22.19.0)(rollup@4.52.5)(typescript@5.8.2)(vite@5.4.21(@types/node@22.19.0))
+ vite-plugin-externalize-deps:
+ specifier: ^0.9.0
+ version: 0.9.0(vite@5.4.21(@types/node@22.19.0))
+ vitest:
+ specifier: ^3.0.5
+ version: 3.2.4(@types/node@22.19.0)(@vitest/ui@3.2.4)(jsdom@24.1.3)
+
+ playground:
+ dependencies:
+ '@avalanche-sdk/client':
+ specifier: ^0.0.4-alpha.16
+ version: 0.0.4-alpha.16(typescript@5.8.2)(zod@4.1.12)
+ '@avalanche-sdk/ui':
+ specifier: workspace:*
+ version: link:..
+ '@radix-ui/react-dropdown-menu':
+ specifier: ^2.1.16
+ version: 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@types/react-syntax-highlighter':
+ specifier: ^15.5.13
+ version: 15.5.13
+ lucide-react:
+ specifier: ^0.468.0
+ version: 0.468.0(react@18.3.1)
+ react:
+ specifier: ^18.2.0
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.3.1(react@18.3.1)
+ react-syntax-highlighter:
+ specifier: ^16.1.0
+ version: 16.1.0(react@18.3.1)
+ viem:
+ specifier: ^2.21.45
+ version: 2.38.6(typescript@5.8.2)(zod@4.1.12)
+ devDependencies:
+ '@types/react':
+ specifier: ^18.2.43
+ version: 18.3.26
+ '@types/react-dom':
+ specifier: ^18.2.17
+ version: 18.3.7(@types/react@18.3.26)
+ '@vitejs/plugin-react':
+ specifier: ^4.2.1
+ version: 4.7.0(vite@5.4.21(@types/node@22.19.0))
+ autoprefixer:
+ specifier: ^10.4.19
+ version: 10.4.21(postcss@8.5.6)
+ buffer:
+ specifier: ^6.0.3
+ version: 6.0.3
+ postcss:
+ specifier: ^8
+ version: 8.5.6
+ process:
+ specifier: ^0.11.10
+ version: 0.11.10
+ tailwindcss:
+ specifier: ^3.4.6
+ version: 3.4.18
+ tailwindcss-animate:
+ specifier: ^1.0.7
+ version: 1.0.7(tailwindcss@3.4.18)
+ typescript:
+ specifier: ^5.2.2
+ version: 5.8.2
+ util:
+ specifier: ^0.12.5
+ version: 0.12.5
+ vite:
+ specifier: ^5.0.8
+ version: 5.4.21(@types/node@22.19.0)
+
+packages:
+
+ '@adobe/css-tools@4.4.4':
+ resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
+
+ '@adraffy/ens-normalize@1.11.1':
+ resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==}
+
+ '@alloc/quick-lru@5.2.0':
+ resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
+ engines: {node: '>=10'}
+
+ '@ampproject/remapping@2.3.0':
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+ engines: {node: '>=6.0.0'}
+
+ '@asamuzakjp/css-color@3.2.0':
+ resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
+
+ '@avalabs/avalanchejs@5.0.0':
+ resolution: {integrity: sha512-0hJK/Hdf8v+q05c8+5K6arFmzq7o1W4I05/Dmr+Es1XRi8canvTu1Y0RruYd6ea2rrvX3UhKrPs3BzLhCTHDrw==}
+ engines: {node: '>=20'}
+
+ '@avalabs/avalanchejs@5.1.0-alpha.1':
+ resolution: {integrity: sha512-nX8RDRrDvZv5SRGElD6FCYjHHeBR43iBHRoVtOmj5FQtHXNLjV2t8lLRD7/qoeV2LFIfG1b0pRk7bmEn305oZA==}
+ engines: {node: '>=20'}
+
+ '@avalanche-sdk/chainkit@0.3.0-alpha.8':
+ resolution: {integrity: sha512-7hJqAVoJ4g73M4CYWKictSCSrml7RY00sLYfjAWug0s6FNemKgPljq9sUvufO4iGJ03rjt7+c0dBgACTB55XqQ==}
+ hasBin: true
+ peerDependencies:
+ '@modelcontextprotocol/sdk': '>=1.5.0 <1.10.0'
+ peerDependenciesMeta:
+ '@modelcontextprotocol/sdk':
+ optional: true
+
+ '@avalanche-sdk/client@0.0.4-alpha.16':
+ resolution: {integrity: sha512-rpOwBURNaV62rZSyacuQfPhTsuHWhnNNcoqTWEBXrGqIkUXoIdG4YqGrjKVVvNk/c7uRC1NX7FbmpOv7fgibJw==}
+ engines: {node: '>=20', npm: '>=10'}
+
+ '@avalanche-sdk/interchain@0.1.1-alpha.1':
+ resolution: {integrity: sha512-wbzqg3ayJLAh051/K+oi3sRQRr3P4/4LqFbY2pTlfLqhxh+xFBTYmAr++Xv7RVzy7uvYA+0RD1X6ztioRY9lWw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ viem: ^2.33.1
+
+ '@babel/code-frame@7.27.1':
+ resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.28.5':
+ resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.28.5':
+ resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.28.5':
+ resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.27.2':
+ resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.27.1':
+ resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.3':
+ resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.27.1':
+ resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.28.4':
+ resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.28.5':
+ resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1':
+ resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1':
+ resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/runtime@7.28.4':
+ resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/template@7.27.2':
+ resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.28.5':
+ resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.28.5':
+ resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
+ engines: {node: '>=6.9.0'}
+
+ '@bcoe/v8-coverage@1.0.2':
+ resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
+ engines: {node: '>=18'}
+
+ '@csstools/color-helpers@5.1.0':
+ resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
+ engines: {node: '>=18'}
+
+ '@csstools/css-calc@2.1.4':
+ resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-color-parser@3.1.0':
+ resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5':
+ resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-tokenizer@3.0.4':
+ resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
+ engines: {node: '>=18'}
+
+ '@esbuild/aix-ppc64@0.21.5':
+ resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/aix-ppc64@0.25.12':
+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.21.5':
+ resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm64@0.25.12':
+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.21.5':
+ resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.12':
+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.21.5':
+ resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.12':
+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.21.5':
+ resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-arm64@0.25.12':
+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.21.5':
+ resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.12':
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.21.5':
+ resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.21.5':
+ resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.12':
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.21.5':
+ resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm64@0.25.12':
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.21.5':
+ resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.12':
+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.21.5':
+ resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.12':
+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.21.5':
+ resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.12':
+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.21.5':
+ resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.12':
+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.21.5':
+ resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.12':
+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.21.5':
+ resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.12':
+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.21.5':
+ resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.12':
+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.21.5':
+ resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.12':
+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.21.5':
+ resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.12':
+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.21.5':
+ resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.12':
+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.21.5':
+ resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/sunos-x64@0.25.12':
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.21.5':
+ resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-arm64@0.25.12':
+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.21.5':
+ resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.12':
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.21.5':
+ resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.12':
+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@ethereumjs/rlp@5.0.0':
+ resolution: {integrity: sha512-WuS1l7GJmB0n0HsXLozCoEFc9IwYgf3l0gCkKVYgR67puVF1O4OpEaN0hWmm1c+iHUHFCKt1hJrvy5toLg+6ag==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ '@floating-ui/core@1.7.3':
+ resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
+
+ '@floating-ui/dom@1.7.4':
+ resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
+
+ '@floating-ui/react-dom@2.1.6':
+ resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@floating-ui/react@0.27.16':
+ resolution: {integrity: sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==}
+ peerDependencies:
+ react: '>=17.0.0'
+ react-dom: '>=17.0.0'
+
+ '@floating-ui/utils@0.2.10':
+ resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+
+ '@isaacs/balanced-match@4.0.1':
+ resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
+ engines: {node: 20 || >=22}
+
+ '@isaacs/brace-expansion@5.0.0':
+ resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
+ engines: {node: 20 || >=22}
+
+ '@isaacs/cliui@8.0.2':
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+
+ '@istanbuljs/schema@0.1.3':
+ resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
+ engines: {node: '>=8'}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@microsoft/api-extractor-model@7.31.3':
+ resolution: {integrity: sha512-dv4quQI46p0U03TCEpasUf6JrJL3qjMN7JUAobsPElxBv4xayYYvWW9aPpfYV+Jx6hqUcVaLVOeV7+5hxsyoFQ==}
+
+ '@microsoft/api-extractor@7.54.0':
+ resolution: {integrity: sha512-t0SEcbVUPy4yAVykPafTNWktBg728X6p9t8qCuGDsYr1/lz2VQFihYDP2CnBFSArP5vwJPcvxktoKVSqH326cA==}
+ hasBin: true
+
+ '@microsoft/tsdoc-config@0.17.1':
+ resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==}
+
+ '@microsoft/tsdoc@0.15.1':
+ resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
+
+ '@mrmlnc/readdir-enhanced@2.2.1':
+ resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==}
+ engines: {node: '>=4'}
+
+ '@noble/ciphers@1.3.0':
+ resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
+ engines: {node: ^14.21.3 || >=16}
+
+ '@noble/curves@1.3.0':
+ resolution: {integrity: sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==}
+
+ '@noble/curves@1.9.1':
+ resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==}
+ engines: {node: ^14.21.3 || >=16}
+
+ '@noble/hashes@1.3.3':
+ resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==}
+ engines: {node: '>= 16'}
+
+ '@noble/hashes@1.8.0':
+ resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
+ engines: {node: ^14.21.3 || >=16}
+
+ '@noble/secp256k1@2.0.0':
+ resolution: {integrity: sha512-rUGBd95e2a45rlmFTqQJYEFA4/gdIARFfuTuTqLglz0PZ6AKyzyXsEZZq7UZn8hZsvaBgpCzKKBJizT2cJERXw==}
+
+ '@nodelib/fs.scandir@2.1.5':
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.stat@1.1.3':
+ resolution: {integrity: sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==}
+ engines: {node: '>= 6'}
+
+ '@nodelib/fs.stat@2.0.5':
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.walk@1.2.8':
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+
+ '@pkgjs/parseargs@0.11.0':
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+
+ '@polka/url@1.0.0-next.29':
+ resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+
+ '@radix-ui/number@1.1.1':
+ resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
+
+ '@radix-ui/primitive@1.1.3':
+ resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
+
+ '@radix-ui/react-arrow@1.1.7':
+ resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-collection@1.1.7':
+ resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-compose-refs@1.1.2':
+ resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-context@1.1.2':
+ resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-dialog@1.1.15':
+ resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-direction@1.1.1':
+ resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-dismissable-layer@1.1.11':
+ resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-dropdown-menu@2.1.16':
+ resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-focus-guards@1.1.3':
+ resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-focus-scope@1.1.7':
+ resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-id@1.1.1':
+ resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-label@2.1.8':
+ resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-menu@2.1.16':
+ resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-popover@1.1.15':
+ resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-popper@1.2.8':
+ resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-portal@1.1.9':
+ resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-presence@1.1.5':
+ resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-primitive@2.1.3':
+ resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-primitive@2.1.4':
+ resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-roving-focus@1.1.11':
+ resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-select@2.2.6':
+ resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-slot@1.2.3':
+ resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-slot@1.2.4':
+ resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-tabs@1.1.13':
+ resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-toast@1.2.15':
+ resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-use-callback-ref@1.1.1':
+ resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-controllable-state@1.2.2':
+ resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-effect-event@0.0.2':
+ resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-escape-keydown@1.1.1':
+ resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-layout-effect@1.1.1':
+ resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-previous@1.1.1':
+ resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-rect@1.1.1':
+ resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-size@1.1.1':
+ resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-visually-hidden@1.2.3':
+ resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/rect@1.1.1':
+ resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+
+ '@rolldown/pluginutils@1.0.0-beta.27':
+ resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
+
+ '@rollup/pluginutils@5.3.0':
+ resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+
+ '@rollup/rollup-android-arm-eabi@4.52.5':
+ resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.52.5':
+ resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.52.5':
+ resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.52.5':
+ resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.52.5':
+ resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.52.5':
+ resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.52.5':
+ resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.52.5':
+ resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.52.5':
+ resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.52.5':
+ resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-gnu@4.52.5':
+ resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.52.5':
+ resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.52.5':
+ resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.52.5':
+ resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.52.5':
+ resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.52.5':
+ resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.52.5':
+ resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-openharmony-arm64@4.52.5':
+ resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.52.5':
+ resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.52.5':
+ resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.52.5':
+ resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.52.5':
+ resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rushstack/node-core-library@5.18.0':
+ resolution: {integrity: sha512-XDebtBdw5S3SuZIt+Ra2NieT8kQ3D2Ow1HxhDQ/2soinswnOu9e7S69VSwTOLlQnx5mpWbONu+5JJjDxMAb6Fw==}
+ peerDependencies:
+ '@types/node': '*'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@rushstack/problem-matcher@0.1.1':
+ resolution: {integrity: sha512-Fm5XtS7+G8HLcJHCWpES5VmeMyjAKaWeyZU5qPzZC+22mPlJzAsOxymHiWIfuirtPckX3aptWws+K2d0BzniJA==}
+ peerDependencies:
+ '@types/node': '*'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@rushstack/rig-package@0.6.0':
+ resolution: {integrity: sha512-ZQmfzsLE2+Y91GF15c65L/slMRVhF6Hycq04D4TwtdGaUAbIXXg9c5pKA5KFU7M4QMaihoobp9JJYpYcaY3zOw==}
+
+ '@rushstack/terminal@0.19.3':
+ resolution: {integrity: sha512-0P8G18gK9STyO+CNBvkKPnWGMxESxecTYqOcikHOVIHXa9uAuTK+Fw8TJq2Gng1w7W6wTC9uPX6hGNvrMll2wA==}
+ peerDependencies:
+ '@types/node': '*'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@rushstack/ts-command-line@5.1.3':
+ resolution: {integrity: sha512-Kdv0k/BnnxIYFlMVC1IxrIS0oGQd4T4b7vKfx52Y2+wk2WZSDFIvedr7JrhenzSlm3ou5KwtoTGTGd5nbODRug==}
+
+ '@scure/base@1.1.5':
+ resolution: {integrity: sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==}
+
+ '@scure/base@1.2.6':
+ resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==}
+
+ '@scure/bip32@1.7.0':
+ resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==}
+
+ '@scure/bip39@1.6.0':
+ resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==}
+
+ '@testing-library/dom@10.4.1':
+ resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
+ engines: {node: '>=18'}
+
+ '@testing-library/jest-dom@6.4.7':
+ resolution: {integrity: sha512-GaKJ0nijoNf30dWSOOzQEBkWBRk4rG3C/efw8zKrimNuZpnS/6/AEwo0WvZHgJxG84cNCgxt+mtbe1fsvfLp2A==}
+ engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+ peerDependencies:
+ '@jest/globals': '>= 28'
+ '@types/bun': latest
+ '@types/jest': '>= 28'
+ jest: '>= 28'
+ vitest: '>= 0.32'
+ peerDependenciesMeta:
+ '@jest/globals':
+ optional: true
+ '@types/bun':
+ optional: true
+ '@types/jest':
+ optional: true
+ jest:
+ optional: true
+ vitest:
+ optional: true
+
+ '@testing-library/react@16.3.0':
+ resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@testing-library/dom': ^10.0.0
+ '@types/react': ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^18.0.0 || ^19.0.0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@testing-library/user-event@14.6.1':
+ resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+
+ '@types/argparse@1.0.38':
+ resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==}
+
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@types/glob@7.2.0':
+ resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
+
+ '@types/hast@3.0.4':
+ resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
+
+ '@types/minimatch@6.0.0':
+ resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
+ deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
+
+ '@types/node@22.19.0':
+ resolution: {integrity: sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==}
+
+ '@types/prismjs@1.26.5':
+ resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
+
+ '@types/prop-types@15.7.15':
+ resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
+
+ '@types/react-dom@18.3.7':
+ resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
+ peerDependencies:
+ '@types/react': ^18.0.0
+
+ '@types/react-syntax-highlighter@15.5.13':
+ resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==}
+
+ '@types/react@18.3.26':
+ resolution: {integrity: sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==}
+
+ '@types/unist@2.0.11':
+ resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
+
+ '@types/unist@3.0.3':
+ resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+
+ '@vitejs/plugin-react@4.7.0':
+ resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+
+ '@vitest/coverage-v8@3.2.4':
+ resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==}
+ peerDependencies:
+ '@vitest/browser': 3.2.4
+ vitest: 3.2.4
+ peerDependenciesMeta:
+ '@vitest/browser':
+ optional: true
+
+ '@vitest/expect@3.2.4':
+ resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
+
+ '@vitest/mocker@3.2.4':
+ resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@3.2.4':
+ resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
+
+ '@vitest/runner@3.2.4':
+ resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
+
+ '@vitest/snapshot@3.2.4':
+ resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
+
+ '@vitest/spy@3.2.4':
+ resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
+
+ '@vitest/ui@3.2.4':
+ resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==}
+ peerDependencies:
+ vitest: 3.2.4
+
+ '@vitest/utils@3.2.4':
+ resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
+
+ '@volar/language-core@2.4.23':
+ resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==}
+
+ '@volar/source-map@2.4.23':
+ resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==}
+
+ '@volar/typescript@2.4.23':
+ resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==}
+
+ '@vue/compiler-core@3.5.23':
+ resolution: {integrity: sha512-nW7THWj5HOp085ROk65LwaoxuzDsjIxr485F4iu63BoxsXoSqKqmsUUoP4A7Gl67DgIgi0zJ8JFgHfvny/74MA==}
+
+ '@vue/compiler-dom@3.5.23':
+ resolution: {integrity: sha512-AT8RMw0vEzzzO0JU5gY0F6iCzaWUIh/aaRVordzMBKXRpoTllTT4kocHDssByPsvodNCfump/Lkdow2mT/O5KQ==}
+
+ '@vue/compiler-vue2@2.7.16':
+ resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
+
+ '@vue/language-core@2.2.0':
+ resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@vue/shared@3.5.23':
+ resolution: {integrity: sha512-0YZ1DYuC5o/YJPf6pFdt2KYxVGDxkDbH/1NYJnVJWUkzr8ituBEmFVQRNX2gCaAsFEjEDnLkWpgqlZA7htgS/g==}
+
+ abitype@1.1.0:
+ resolution: {integrity: sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==}
+ peerDependencies:
+ typescript: '>=5.0.4'
+ zod: ^3.22.0 || ^4.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ zod:
+ optional: true
+
+ acorn@8.15.0:
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ agent-base@7.1.4:
+ resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
+ engines: {node: '>= 14'}
+
+ ajv-draft-04@1.0.0:
+ resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
+ peerDependencies:
+ ajv: ^8.5.0
+ peerDependenciesMeta:
+ ajv:
+ optional: true
+
+ ajv-formats@3.0.1:
+ resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
+ peerDependencies:
+ ajv: ^8.0.0
+ peerDependenciesMeta:
+ ajv:
+ optional: true
+
+ ajv@8.12.0:
+ resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
+
+ ajv@8.13.0:
+ resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==}
+
+ alien-signals@0.4.14:
+ resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ engines: {node: '>=12'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ ansi-styles@5.2.0:
+ resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+ engines: {node: '>=10'}
+
+ ansi-styles@6.2.3:
+ resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
+ engines: {node: '>=12'}
+
+ any-promise@1.3.0:
+ resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
+
+ anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+
+ arg@5.0.2:
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+
+ argparse@1.0.10:
+ resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
+
+ aria-hidden@1.2.6:
+ resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+ engines: {node: '>=10'}
+
+ aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
+ arr-diff@4.0.0:
+ resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==}
+ engines: {node: '>=0.10.0'}
+
+ arr-flatten@1.1.0:
+ resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==}
+ engines: {node: '>=0.10.0'}
+
+ arr-union@3.1.0:
+ resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==}
+ engines: {node: '>=0.10.0'}
+
+ array-union@1.0.2:
+ resolution: {integrity: sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==}
+ engines: {node: '>=0.10.0'}
+
+ array-uniq@1.0.3:
+ resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==}
+ engines: {node: '>=0.10.0'}
+
+ array-unique@0.3.2:
+ resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==}
+ engines: {node: '>=0.10.0'}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ assign-symbols@1.0.0:
+ resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==}
+ engines: {node: '>=0.10.0'}
+
+ ast-v8-to-istanbul@0.3.8:
+ resolution: {integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==}
+
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ atob@2.1.2:
+ resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
+ engines: {node: '>= 4.5.0'}
+ hasBin: true
+
+ autoprefixer@10.4.21:
+ resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
+ engines: {node: ^10 || ^12 || >=14}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
+
+ available-typed-arrays@1.0.7:
+ resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+ engines: {node: '>= 0.4'}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+ base@0.11.2:
+ resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==}
+ engines: {node: '>=0.10.0'}
+
+ baseline-browser-mapping@2.8.25:
+ resolution: {integrity: sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==}
+ hasBin: true
+
+ binary-extensions@2.3.0:
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+ engines: {node: '>=8'}
+
+ brace-expansion@1.1.12:
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+ brace-expansion@2.0.2:
+ resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+
+ braces@2.3.2:
+ resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==}
+ engines: {node: '>=0.10.0'}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browserslist@4.27.0:
+ resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ buffer@6.0.3:
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
+ bundle-require@5.1.0:
+ resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ peerDependencies:
+ esbuild: '>=0.18'
+
+ cac@6.7.14:
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+ engines: {node: '>=8'}
+
+ cache-base@1.0.1:
+ resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==}
+ engines: {node: '>=0.10.0'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bind@1.0.8:
+ resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
+ call-me-maybe@1.0.2:
+ resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==}
+
+ camelcase-css@2.0.1:
+ resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
+ engines: {node: '>= 6'}
+
+ caniuse-lite@1.0.30001754:
+ resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==}
+
+ chai@5.3.3:
+ resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
+ engines: {node: '>=18'}
+
+ chalk@3.0.0:
+ resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==}
+ engines: {node: '>=8'}
+
+ character-entities-legacy@3.0.0:
+ resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
+ character-entities@2.0.2:
+ resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
+
+ character-reference-invalid@2.0.1:
+ resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
+
+ check-error@2.1.1:
+ resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
+ engines: {node: '>= 16'}
+
+ chokidar@3.6.0:
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+ engines: {node: '>= 8.10.0'}
+
+ chokidar@4.0.3:
+ resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
+ engines: {node: '>= 14.16.0'}
+
+ class-utils@0.3.6:
+ resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
+ engines: {node: '>=0.10.0'}
+
+ class-variance-authority@0.7.1:
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ collection-visit@1.0.0:
+ resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==}
+ engines: {node: '>=0.10.0'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ comma-separated-tokens@2.0.3:
+ resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
+ commander@2.20.3:
+ resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+
+ commander@4.1.1:
+ resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
+ engines: {node: '>= 6'}
+
+ compare-versions@6.1.1:
+ resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
+
+ component-emitter@1.3.1:
+ resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ confbox@0.1.8:
+ resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
+
+ confbox@0.2.2:
+ resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
+
+ consola@3.4.2:
+ resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
+ engines: {node: ^14.18.0 || >=16.10.0}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ copy-descriptor@0.1.1:
+ resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==}
+ engines: {node: '>=0.10.0'}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ css.escape@1.5.1:
+ resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
+
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ cssstyle@4.6.0:
+ resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
+ engines: {node: '>=18'}
+
+ csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+ data-urls@5.0.0:
+ resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
+ engines: {node: '>=18'}
+
+ de-indent@1.0.2:
+ resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
+
+ debug@2.6.9:
+ resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
+ decode-named-character-reference@1.2.0:
+ resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
+
+ decode-uri-component@0.2.2:
+ resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
+ engines: {node: '>=0.10'}
+
+ deep-eql@5.0.2:
+ resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
+ engines: {node: '>=6'}
+
+ define-data-property@1.1.4:
+ resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+ engines: {node: '>= 0.4'}
+
+ define-property@0.2.5:
+ resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==}
+ engines: {node: '>=0.10.0'}
+
+ define-property@1.0.0:
+ resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==}
+ engines: {node: '>=0.10.0'}
+
+ define-property@2.0.2:
+ resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==}
+ engines: {node: '>=0.10.0'}
+
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ detect-node-es@1.1.0:
+ resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+
+ didyoumean@1.2.2:
+ resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
+
+ diff@8.0.2:
+ resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==}
+ engines: {node: '>=0.3.1'}
+
+ dir-glob@2.2.2:
+ resolution: {integrity: sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==}
+ engines: {node: '>=4'}
+
+ dlv@1.1.3:
+ resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
+
+ dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
+ dom-accessibility-api@0.6.3:
+ resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+
+ electron-to-chromium@1.5.248:
+ resolution: {integrity: sha512-zsur2yunphlyAO4gIubdJEXCK6KOVvtpiuDfCIqbM9FjcnMYiyn0ICa3hWfPr0nc41zcLWobgy1iL7VvoOyA2Q==}
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-module-lexer@1.7.0:
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
+ esbuild-fix-imports-plugin@1.0.23:
+ resolution: {integrity: sha512-zDn2Mq3OnW9qNm9FrHDeE0FePgIRIaH9EWpHOGsJZFPfyPSNqzvCK/x9EZJ5eHEEyXe8ZGdb5CjDpzqkyAqcCg==}
+
+ esbuild-plugin-babel@0.2.3:
+ resolution: {integrity: sha512-hGLL31n+GvBhkHUpPCt1sU4ynzOH7I1IUkKhera66jigi4mHFPL6dfJo44L6/1rfcZudXx+wGdf9VOifzDPqYQ==}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ esbuild@0.21.5:
+ resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
+ engines: {node: '>=12'}
+ hasBin: true
+
+ esbuild@0.25.12:
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ eventemitter3@5.0.1:
+ resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
+
+ expand-brackets@2.1.4:
+ resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==}
+ engines: {node: '>=0.10.0'}
+
+ expect-type@1.2.2:
+ resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
+ engines: {node: '>=12.0.0'}
+
+ exsolve@1.0.7:
+ resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
+
+ extend-shallow@2.0.1:
+ resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
+ engines: {node: '>=0.10.0'}
+
+ extend-shallow@3.0.2:
+ resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==}
+ engines: {node: '>=0.10.0'}
+
+ extglob@2.0.4:
+ resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==}
+ engines: {node: '>=0.10.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-glob@2.2.7:
+ resolution: {integrity: sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==}
+ engines: {node: '>=4.0.0'}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fastq@1.19.1:
+ resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+
+ fault@1.0.4:
+ resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fflate@0.8.2:
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+
+ fill-range@4.0.0:
+ resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==}
+ engines: {node: '>=0.10.0'}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ fix-dts-default-cjs-exports@1.0.1:
+ resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
+
+ flatted@3.3.3:
+ resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
+ for-each@0.3.5:
+ resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
+ engines: {node: '>= 0.4'}
+
+ for-in@1.0.2:
+ resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==}
+ engines: {node: '>=0.10.0'}
+
+ foreground-child@3.3.1:
+ resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
+ engines: {node: '>=14'}
+
+ form-data@4.0.4:
+ resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
+ engines: {node: '>= 6'}
+
+ format@0.2.2:
+ resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
+ engines: {node: '>=0.4.x'}
+
+ fraction.js@4.3.7:
+ resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+
+ fragment-cache@0.2.1:
+ resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
+ engines: {node: '>=0.10.0'}
+
+ fs-extra@11.3.2:
+ resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
+ engines: {node: '>=14.14'}
+
+ fs.realpath@1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ generator-function@2.0.1:
+ resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
+ engines: {node: '>= 0.4'}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-nonce@1.0.1:
+ resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+ engines: {node: '>=6'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ get-value@2.0.6:
+ resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==}
+ engines: {node: '>=0.10.0'}
+
+ glob-parent@3.1.0:
+ resolution: {integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==}
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ glob-to-regexp@0.3.0:
+ resolution: {integrity: sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==}
+
+ glob@10.4.5:
+ resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
+ hasBin: true
+
+ glob@11.0.3:
+ resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
+ engines: {node: 20 || >=22}
+ hasBin: true
+
+ glob@7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ deprecated: Glob versions prior to v9 are no longer supported
+
+ globby@9.2.0:
+ resolution: {integrity: sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==}
+ engines: {node: '>=6'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-property-descriptors@1.0.2:
+ resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
+ has-value@0.3.1:
+ resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==}
+ engines: {node: '>=0.10.0'}
+
+ has-value@1.0.0:
+ resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==}
+ engines: {node: '>=0.10.0'}
+
+ has-values@0.1.4:
+ resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==}
+ engines: {node: '>=0.10.0'}
+
+ has-values@1.0.0:
+ resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==}
+ engines: {node: '>=0.10.0'}
+
+ hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+
+ hast-util-parse-selector@4.0.0:
+ resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
+
+ hastscript@9.0.1:
+ resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
+
+ he@1.2.0:
+ resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
+ hasBin: true
+
+ highlight.js@10.7.3:
+ resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
+
+ highlightjs-vue@1.0.0:
+ resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==}
+
+ html-encoding-sniffer@4.0.0:
+ resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
+ engines: {node: '>=18'}
+
+ html-escaper@2.0.2:
+ resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+
+ http-proxy-agent@7.0.2:
+ resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
+ engines: {node: '>= 14'}
+
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
+ ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
+ ignore@4.0.6:
+ resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==}
+ engines: {node: '>= 4'}
+
+ import-lazy@4.0.0:
+ resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==}
+ engines: {node: '>=8'}
+
+ indent-string@4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+
+ inflight@1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ is-accessor-descriptor@1.0.1:
+ resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==}
+ engines: {node: '>= 0.10'}
+
+ is-alphabetical@2.0.1:
+ resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
+
+ is-alphanumerical@2.0.1:
+ resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
+
+ is-arguments@1.2.0:
+ resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==}
+ engines: {node: '>= 0.4'}
+
+ is-binary-path@2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+
+ is-buffer@1.1.6:
+ resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
+
+ is-callable@1.2.7:
+ resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
+ engines: {node: '>= 0.4'}
+
+ is-core-module@2.16.1:
+ resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
+ engines: {node: '>= 0.4'}
+
+ is-data-descriptor@1.0.1:
+ resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==}
+ engines: {node: '>= 0.4'}
+
+ is-decimal@2.0.1:
+ resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
+
+ is-descriptor@0.1.7:
+ resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==}
+ engines: {node: '>= 0.4'}
+
+ is-descriptor@1.0.3:
+ resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==}
+ engines: {node: '>= 0.4'}
+
+ is-extendable@0.1.1:
+ resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
+ engines: {node: '>=0.10.0'}
+
+ is-extendable@1.0.1:
+ resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==}
+ engines: {node: '>=0.10.0'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
+ is-generator-function@1.1.2:
+ resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
+ engines: {node: '>= 0.4'}
+
+ is-glob@3.1.0:
+ resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-hexadecimal@2.0.1:
+ resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
+
+ is-number@3.0.0:
+ resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==}
+ engines: {node: '>=0.10.0'}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ is-plain-object@2.0.4:
+ resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
+ engines: {node: '>=0.10.0'}
+
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
+ is-regex@1.2.1:
+ resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
+ engines: {node: '>= 0.4'}
+
+ is-typed-array@1.1.15:
+ resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
+ engines: {node: '>= 0.4'}
+
+ is-windows@1.0.2:
+ resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
+ engines: {node: '>=0.10.0'}
+
+ isarray@1.0.0:
+ resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ isobject@2.1.0:
+ resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==}
+ engines: {node: '>=0.10.0'}
+
+ isobject@3.0.1:
+ resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
+ engines: {node: '>=0.10.0'}
+
+ isows@1.0.7:
+ resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==}
+ peerDependencies:
+ ws: '*'
+
+ istanbul-lib-coverage@3.2.2:
+ resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
+ engines: {node: '>=8'}
+
+ istanbul-lib-report@3.0.1:
+ resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
+ engines: {node: '>=10'}
+
+ istanbul-lib-source-maps@5.0.6:
+ resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
+ engines: {node: '>=10'}
+
+ istanbul-reports@3.2.0:
+ resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
+ engines: {node: '>=8'}
+
+ jackspeak@3.4.3:
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
+
+ jackspeak@4.1.1:
+ resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
+ engines: {node: 20 || >=22}
+
+ jiti@1.21.7:
+ resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
+ hasBin: true
+
+ jju@1.4.0:
+ resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
+
+ joycon@3.1.1:
+ resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
+ engines: {node: '>=10'}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-tokens@9.0.1:
+ resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
+
+ jsdom@24.1.3:
+ resolution: {integrity: sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ canvas: ^2.11.2
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-canonicalize@1.2.0:
+ resolution: {integrity: sha512-TTdjBvqrqJKSADlEsY5rWbx8/1tOrVlTR/aSLU8N2VSInCTffP0p+byYB8Es+OmL4ZOeEftjUdvV+eJeSzJC/Q==}
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ jsonfile@6.2.0:
+ resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
+
+ kind-of@3.2.2:
+ resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==}
+ engines: {node: '>=0.10.0'}
+
+ kind-of@4.0.0:
+ resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==}
+ engines: {node: '>=0.10.0'}
+
+ kind-of@6.0.3:
+ resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
+ engines: {node: '>=0.10.0'}
+
+ kolorist@1.8.0:
+ resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
+
+ lilconfig@3.1.3:
+ resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
+ engines: {node: '>=14'}
+
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+ load-tsconfig@0.2.5:
+ resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ local-pkg@1.1.2:
+ resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
+ engines: {node: '>=14'}
+
+ lodash.debounce@4.0.8:
+ resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
+
+ lodash.sortby@4.7.0:
+ resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
+
+ lodash@4.17.21:
+ resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+
+ loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+
+ loupe@3.2.1:
+ resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
+
+ lowlight@1.20.0:
+ resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
+
+ lru-cache@10.4.3:
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+
+ lru-cache@11.2.2:
+ resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
+ engines: {node: 20 || >=22}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ lru-cache@6.0.0:
+ resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
+ engines: {node: '>=10'}
+
+ lucide-react@0.468.0:
+ resolution: {integrity: sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
+
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ magicast@0.3.5:
+ resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
+
+ make-dir@4.0.0:
+ resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
+ engines: {node: '>=10'}
+
+ map-cache@0.2.2:
+ resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==}
+ engines: {node: '>=0.10.0'}
+
+ map-visit@1.0.0:
+ resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==}
+ engines: {node: '>=0.10.0'}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ micro-eth-signer@0.7.2:
+ resolution: {integrity: sha512-uFH23nqPNdg2KZ9ZdvLG4GO3bTAOWRhwGTsecY4Et2IdQOJ26x6inu8lJ9oyslnYL/0o1vnETCGhMimMvO0SqQ==}
+
+ micro-packed@0.5.3:
+ resolution: {integrity: sha512-zWRoH+qUb/ZMp9gVZhexvRGCENDM5HEQF4sflqpdilUHWK2/zKR7/MT8GBctnTwbhNJwy1iuk5q6+TYP7/twYA==}
+
+ micromatch@3.1.10:
+ resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==}
+ engines: {node: '>=0.10.0'}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ min-indent@1.0.1:
+ resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+ engines: {node: '>=4'}
+
+ minimatch@10.0.3:
+ resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
+ engines: {node: 20 || >=22}
+
+ minimatch@10.1.1:
+ resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
+ engines: {node: 20 || >=22}
+
+ minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+ minimatch@9.0.5:
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minipass@7.1.2:
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ mixin-deep@1.3.2:
+ resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==}
+ engines: {node: '>=0.10.0'}
+
+ mlly@1.8.0:
+ resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
+
+ mrmime@2.0.1:
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+ engines: {node: '>=10'}
+
+ ms@2.0.0:
+ resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ muggle-string@0.4.1:
+ resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
+
+ mz@2.7.0:
+ resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ nanomatch@1.2.13:
+ resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
+ engines: {node: '>=0.10.0'}
+
+ node-releases@2.0.27:
+ resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
+
+ normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+
+ normalize-range@0.1.2:
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
+ engines: {node: '>=0.10.0'}
+
+ nwsapi@2.2.22:
+ resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==}
+
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
+ object-copy@0.1.0:
+ resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==}
+ engines: {node: '>=0.10.0'}
+
+ object-hash@3.0.0:
+ resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
+ engines: {node: '>= 6'}
+
+ object-visit@1.0.1:
+ resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==}
+ engines: {node: '>=0.10.0'}
+
+ object.pick@1.3.0:
+ resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==}
+ engines: {node: '>=0.10.0'}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+ ox@0.9.6:
+ resolution: {integrity: sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==}
+ peerDependencies:
+ typescript: '>=5.4.0'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ package-json-from-dist@1.0.1:
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+
+ parse-entities@4.0.2:
+ resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+
+ parse5@7.3.0:
+ resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+
+ pascalcase@0.1.1:
+ resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
+ engines: {node: '>=0.10.0'}
+
+ path-browserify@1.0.1:
+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+
+ path-dirname@1.0.2:
+ resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==}
+
+ path-is-absolute@1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
+ path-scurry@1.11.1:
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
+ engines: {node: '>=16 || 14 >=14.18'}
+
+ path-scurry@2.0.0:
+ resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
+ engines: {node: 20 || >=22}
+
+ path-type@3.0.0:
+ resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
+ engines: {node: '>=4'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ pathval@2.0.1:
+ resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
+ engines: {node: '>= 14.16'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
+ pify@2.3.0:
+ resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
+ engines: {node: '>=0.10.0'}
+
+ pify@3.0.0:
+ resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
+ engines: {node: '>=4'}
+
+ pify@4.0.1:
+ resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
+ engines: {node: '>=6'}
+
+ pirates@4.0.7:
+ resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
+ engines: {node: '>= 6'}
+
+ pkg-types@1.3.1:
+ resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
+
+ pkg-types@2.3.0:
+ resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
+
+ posix-character-classes@0.1.1:
+ resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==}
+ engines: {node: '>=0.10.0'}
+
+ possible-typed-array-names@1.1.0:
+ resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
+ engines: {node: '>= 0.4'}
+
+ postcss-import@15.1.0:
+ resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ postcss: ^8.0.0
+
+ postcss-import@16.1.1:
+ resolution: {integrity: sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ postcss: ^8.0.0
+
+ postcss-js@4.1.0:
+ resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==}
+ engines: {node: ^12 || ^14 || >= 16}
+ peerDependencies:
+ postcss: ^8.4.21
+
+ postcss-load-config@6.0.1:
+ resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ jiti: '>=1.21.0'
+ postcss: '>=8.0.9'
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+ postcss:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ postcss-nested@6.2.0:
+ resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.2.14
+
+ postcss-selector-parser@6.1.2:
+ resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
+ engines: {node: '>=4'}
+
+ postcss-value-parser@4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
+ prismjs@1.30.0:
+ resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
+ engines: {node: '>=6'}
+
+ process@0.11.10:
+ resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
+ engines: {node: '>= 0.6.0'}
+
+ property-information@7.1.0:
+ resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
+
+ psl@1.15.0:
+ resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ quansync@0.2.11:
+ resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
+
+ querystringify@2.2.0:
+ resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ react-dom@18.3.1:
+ resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
+ peerDependencies:
+ react: ^18.3.1
+
+ react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
+ react-refresh@0.17.0:
+ resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
+ engines: {node: '>=0.10.0'}
+
+ react-remove-scroll-bar@2.3.8:
+ resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-remove-scroll@2.7.1:
+ resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-style-singleton@2.2.3:
+ resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-syntax-highlighter@16.1.0:
+ resolution: {integrity: sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==}
+ engines: {node: '>= 16.20.2'}
+ peerDependencies:
+ react: '>= 0.14.0'
+
+ react@18.3.1:
+ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
+ engines: {node: '>=0.10.0'}
+
+ read-cache@1.0.0:
+ resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
+
+ readdirp@3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+
+ readdirp@4.1.2:
+ resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
+ engines: {node: '>= 14.18.0'}
+
+ redent@3.0.0:
+ resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
+ engines: {node: '>=8'}
+
+ refractor@5.0.0:
+ resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==}
+
+ regex-not@1.0.2:
+ resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==}
+ engines: {node: '>=0.10.0'}
+
+ repeat-element@1.1.4:
+ resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==}
+ engines: {node: '>=0.10.0'}
+
+ repeat-string@1.6.1:
+ resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
+ engines: {node: '>=0.10'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ requires-port@1.0.0:
+ resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
+
+ resolve-from@5.0.0:
+ resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
+ engines: {node: '>=8'}
+
+ resolve-url@0.2.1:
+ resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==}
+ deprecated: https://github.com/lydell/resolve-url#deprecated
+
+ resolve@1.22.11:
+ resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
+ ret@0.1.15:
+ resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==}
+ engines: {node: '>=0.12'}
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rollup-plugin-preserve-use-client@3.0.1:
+ resolution: {integrity: sha512-4WKtGnQsgeCzT/PnA82V4knXVTKxNrxJFcPVa1Kero2XaLs1yazGSCUwxv6NzVmeNeURqE+A5wLbI+zlPKVgMg==}
+ peerDependencies:
+ rollup: ^4.0.0
+
+ rollup@4.52.5:
+ resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ rrweb-cssom@0.7.1:
+ resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==}
+
+ rrweb-cssom@0.8.0:
+ resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ safe-regex-test@1.1.0:
+ resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
+ engines: {node: '>= 0.4'}
+
+ safe-regex@1.1.0:
+ resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
+ scheduler@0.23.2:
+ resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.5.4:
+ resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ semver@7.7.3:
+ resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ set-function-length@1.2.2:
+ resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+ engines: {node: '>= 0.4'}
+
+ set-value@2.0.1:
+ resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==}
+ engines: {node: '>=0.10.0'}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
+ sirv@3.0.2:
+ resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
+ engines: {node: '>=18'}
+
+ slash@2.0.0:
+ resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
+ engines: {node: '>=6'}
+
+ snapdragon-node@2.1.1:
+ resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==}
+ engines: {node: '>=0.10.0'}
+
+ snapdragon-util@3.0.1:
+ resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==}
+ engines: {node: '>=0.10.0'}
+
+ snapdragon@0.8.2:
+ resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==}
+ engines: {node: '>=0.10.0'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ source-map-resolve@0.5.3:
+ resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==}
+ deprecated: See https://github.com/lydell/source-map-resolve#deprecated
+
+ source-map-url@0.4.1:
+ resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==}
+ deprecated: See https://github.com/lydell/source-map-url#deprecated
+
+ source-map@0.5.7:
+ resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
+ engines: {node: '>=0.10.0'}
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ source-map@0.8.0-beta.0:
+ resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
+ engines: {node: '>= 8'}
+ deprecated: The work that was done in this beta branch won't be included in future versions
+
+ space-separated-tokens@2.0.2:
+ resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
+
+ split-string@3.1.0:
+ resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==}
+ engines: {node: '>=0.10.0'}
+
+ sprintf-js@1.0.3:
+ resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ static-extend@0.1.2:
+ resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
+ engines: {node: '>=0.10.0'}
+
+ std-env@3.10.0:
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+
+ string-argv@0.3.2:
+ resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
+ engines: {node: '>=0.6.19'}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-ansi@7.1.2:
+ resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
+ engines: {node: '>=12'}
+
+ strip-indent@3.0.0:
+ resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+ engines: {node: '>=8'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ strip-literal@3.1.0:
+ resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
+
+ sucrase@3.35.0:
+ resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ supports-color@8.1.1:
+ resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
+ engines: {node: '>=10'}
+
+ supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
+ tabbable@6.3.0:
+ resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==}
+
+ tailwind-merge@3.3.1:
+ resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
+
+ tailwindcss-animate@1.0.7:
+ resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
+ peerDependencies:
+ tailwindcss: '>=3.0.0 || insiders'
+
+ tailwindcss@3.4.18:
+ resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
+ test-exclude@7.0.1:
+ resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
+ engines: {node: '>=18'}
+
+ thenify-all@1.6.0:
+ resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
+ engines: {node: '>=0.8'}
+
+ thenify@3.3.1:
+ resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@0.3.2:
+ resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
+ tinypool@1.1.1:
+ resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+
+ tinyrainbow@2.0.0:
+ resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
+ engines: {node: '>=14.0.0'}
+
+ tinyspy@4.0.4:
+ resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
+ engines: {node: '>=14.0.0'}
+
+ to-object-path@0.3.0:
+ resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==}
+ engines: {node: '>=0.10.0'}
+
+ to-regex-range@2.1.1:
+ resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==}
+ engines: {node: '>=0.10.0'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ to-regex@3.0.2:
+ resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==}
+ engines: {node: '>=0.10.0'}
+
+ totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+
+ tough-cookie@4.1.4:
+ resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
+ engines: {node: '>=6'}
+
+ tr46@1.0.1:
+ resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
+
+ tr46@5.1.1:
+ resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
+ engines: {node: '>=18'}
+
+ tree-kill@1.2.2:
+ resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
+ hasBin: true
+
+ ts-interface-checker@0.1.13:
+ resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
+
+ tscpaths@0.0.9:
+ resolution: {integrity: sha512-tz4qimSJTCjYtHVsoY7pvxLcxhmhgmwzm7fyMEiL3/kPFFVyUuZOwuwcWwjkAsIrSUKJK22A7fNuJUwxzQ+H+w==}
+ hasBin: true
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ tsup@8.5.0:
+ resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+ peerDependencies:
+ '@microsoft/api-extractor': ^7.36.0
+ '@swc/core': ^1
+ postcss: ^8.4.12
+ typescript: '>=4.5.0'
+ peerDependenciesMeta:
+ '@microsoft/api-extractor':
+ optional: true
+ '@swc/core':
+ optional: true
+ postcss:
+ optional: true
+ typescript:
+ optional: true
+
+ typescript@5.8.2:
+ resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ ufo@1.6.1:
+ resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
+
+ undici-types@6.21.0:
+ resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+
+ union-value@1.0.1:
+ resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==}
+ engines: {node: '>=0.10.0'}
+
+ universalify@0.2.0:
+ resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
+ engines: {node: '>= 4.0.0'}
+
+ universalify@2.0.1:
+ resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+ engines: {node: '>= 10.0.0'}
+
+ unset-value@1.0.0:
+ resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==}
+ engines: {node: '>=0.10.0'}
+
+ update-browserslist-db@1.1.4:
+ resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ urix@0.1.0:
+ resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==}
+ deprecated: Please see https://github.com/lydell/urix#deprecated
+
+ url-parse@1.5.10:
+ resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
+
+ use-callback-ref@1.3.3:
+ resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-sidecar@1.1.3:
+ resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use@3.1.1:
+ resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}
+ engines: {node: '>=0.10.0'}
+
+ usehooks-ts@3.1.1:
+ resolution: {integrity: sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==}
+ engines: {node: '>=16.15.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ util@0.12.5:
+ resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==}
+
+ viem@2.38.6:
+ resolution: {integrity: sha512-aqO6P52LPXRjdnP6rl5Buab65sYa4cZ6Cpn+k4OLOzVJhGIK8onTVoKMFMT04YjDfyDICa/DZyV9HmvLDgcjkw==}
+ peerDependencies:
+ typescript: '>=5.0.4'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ vite-node@3.2.4:
+ resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+
+ vite-plugin-dts@4.5.4:
+ resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==}
+ peerDependencies:
+ typescript: '*'
+ vite: '*'
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
+ vite-plugin-externalize-deps@0.9.0:
+ resolution: {integrity: sha512-wg3qb5gCy2d1KpPKyD9wkXMcYJ84yjgziHrStq9/8R7chhUC73mhQz+tVtvhFiICQHsBn1pnkY4IBbPqF9JHNw==}
+ peerDependencies:
+ vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
+
+ vite@5.4.21:
+ resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || >=20.0.0
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+
+ vitest@3.2.4:
+ resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@types/debug': ^4.1.12
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ '@vitest/browser': 3.2.4
+ '@vitest/ui': 3.2.4
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@types/debug':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ vscode-uri@3.1.0:
+ resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
+
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
+ webidl-conversions@4.0.2:
+ resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
+
+ webidl-conversions@7.0.0:
+ resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
+ engines: {node: '>=12'}
+
+ whatwg-encoding@3.1.1:
+ resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
+ engines: {node: '>=18'}
+
+ whatwg-mimetype@4.0.0:
+ resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
+ engines: {node: '>=18'}
+
+ whatwg-url@14.2.0:
+ resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
+ engines: {node: '>=18'}
+
+ whatwg-url@7.1.0:
+ resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
+
+ which-typed-array@1.1.19:
+ resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
+ engines: {node: '>= 0.4'}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+ ws@8.18.3:
+ resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yallist@4.0.0:
+ resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
+
+ zod@4.1.12:
+ resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
+
+snapshots:
+
+ '@adobe/css-tools@4.4.4': {}
+
+ '@adraffy/ens-normalize@1.11.1': {}
+
+ '@alloc/quick-lru@5.2.0': {}
+
+ '@ampproject/remapping@2.3.0':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@asamuzakjp/css-color@3.2.0':
+ dependencies:
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+ lru-cache: 10.4.3
+
+ '@avalabs/avalanchejs@5.0.0':
+ dependencies:
+ '@noble/curves': 1.3.0
+ '@noble/hashes': 1.3.3
+ '@noble/secp256k1': 2.0.0
+ '@scure/base': 1.1.5
+ micro-eth-signer: 0.7.2
+
+ '@avalabs/avalanchejs@5.1.0-alpha.1':
+ dependencies:
+ '@noble/curves': 1.3.0
+ '@noble/hashes': 1.3.3
+ '@noble/secp256k1': 2.0.0
+ '@scure/base': 1.1.5
+ micro-eth-signer: 0.7.2
+
+ '@avalanche-sdk/chainkit@0.3.0-alpha.8':
+ dependencies:
+ json-canonicalize: 1.2.0
+ zod: 4.1.12
+
+ '@avalanche-sdk/client@0.0.4-alpha.16(typescript@5.8.2)(zod@4.1.12)':
+ dependencies:
+ '@avalabs/avalanchejs': 5.0.0
+ '@noble/hashes': 1.3.3
+ '@noble/secp256k1': 2.0.0
+ util: 0.12.5
+ viem: 2.38.6(typescript@5.8.2)(zod@4.1.12)
+ transitivePeerDependencies:
+ - bufferutil
+ - typescript
+ - utf-8-validate
+ - zod
+
+ '@avalanche-sdk/interchain@0.1.1-alpha.1(typescript@5.8.2)(viem@2.38.6(typescript@5.8.2)(zod@4.1.12))(zod@4.1.12)':
+ dependencies:
+ '@avalabs/avalanchejs': 5.1.0-alpha.1
+ '@avalanche-sdk/client': 0.0.4-alpha.16(typescript@5.8.2)(zod@4.1.12)
+ viem: 2.38.6(typescript@5.8.2)(zod@4.1.12)
+ transitivePeerDependencies:
+ - bufferutil
+ - typescript
+ - utf-8-validate
+ - zod
+
+ '@babel/code-frame@7.27.1':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.28.5': {}
+
+ '@babel/core@7.28.5':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/generator': 7.28.5
+ '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5)
+ '@babel/helpers': 7.28.4
+ '@babel/parser': 7.28.5
+ '@babel/template': 7.27.2
+ '@babel/traverse': 7.28.5
+ '@babel/types': 7.28.5
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.28.5':
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.27.2':
+ dependencies:
+ '@babel/compat-data': 7.28.5
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.27.0
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-module-imports@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.28.5
+ '@babel/types': 7.28.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/helper-module-imports': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.28.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.27.1': {}
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.28.4':
+ dependencies:
+ '@babel/template': 7.27.2
+ '@babel/types': 7.28.5
+
+ '@babel/parser@7.28.5':
+ dependencies:
+ '@babel/types': 7.28.5
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/runtime@7.28.4': {}
+
+ '@babel/template@7.27.2':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+
+ '@babel/traverse@7.28.5':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/generator': 7.28.5
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.28.5
+ '@babel/template': 7.27.2
+ '@babel/types': 7.28.5
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.28.5':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@bcoe/v8-coverage@1.0.2': {}
+
+ '@csstools/color-helpers@5.1.0': {}
+
+ '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/color-helpers': 5.1.0
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-tokenizer@3.0.4': {}
+
+ '@esbuild/aix-ppc64@0.21.5':
+ optional: true
+
+ '@esbuild/aix-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm@0.21.5':
+ optional: true
+
+ '@esbuild/android-arm@0.25.12':
+ optional: true
+
+ '@esbuild/android-x64@0.21.5':
+ optional: true
+
+ '@esbuild/android-x64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-x64@0.21.5':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.21.5':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm@0.21.5':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ia32@0.21.5':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/linux-loong64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.21.5':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-s390x@0.21.5':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.12':
+ optional: true
+
+ '@esbuild/linux-x64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.21.5':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.21.5':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/sunos-x64@0.21.5':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-ia32@0.21.5':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/win32-x64@0.21.5':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.12':
+ optional: true
+
+ '@ethereumjs/rlp@5.0.0': {}
+
+ '@floating-ui/core@1.7.3':
+ dependencies:
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/dom@1.7.4':
+ dependencies:
+ '@floating-ui/core': 1.7.3
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/react-dom@2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@floating-ui/dom': 1.7.4
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ '@floating-ui/react@0.27.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@floating-ui/utils': 0.2.10
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ tabbable: 6.3.0
+
+ '@floating-ui/utils@0.2.10': {}
+
+ '@isaacs/balanced-match@4.0.1': {}
+
+ '@isaacs/brace-expansion@5.0.0':
+ dependencies:
+ '@isaacs/balanced-match': 4.0.1
+
+ '@isaacs/cliui@8.0.2':
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: string-width@4.2.3
+ strip-ansi: 7.1.2
+ strip-ansi-cjs: strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: wrap-ansi@7.0.0
+
+ '@istanbuljs/schema@0.1.3': {}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@microsoft/api-extractor-model@7.31.3(@types/node@22.19.0)':
+ dependencies:
+ '@microsoft/tsdoc': 0.15.1
+ '@microsoft/tsdoc-config': 0.17.1
+ '@rushstack/node-core-library': 5.18.0(@types/node@22.19.0)
+ transitivePeerDependencies:
+ - '@types/node'
+
+ '@microsoft/api-extractor@7.54.0(@types/node@22.19.0)':
+ dependencies:
+ '@microsoft/api-extractor-model': 7.31.3(@types/node@22.19.0)
+ '@microsoft/tsdoc': 0.15.1
+ '@microsoft/tsdoc-config': 0.17.1
+ '@rushstack/node-core-library': 5.18.0(@types/node@22.19.0)
+ '@rushstack/rig-package': 0.6.0
+ '@rushstack/terminal': 0.19.3(@types/node@22.19.0)
+ '@rushstack/ts-command-line': 5.1.3(@types/node@22.19.0)
+ diff: 8.0.2
+ lodash: 4.17.21
+ minimatch: 10.0.3
+ resolve: 1.22.11
+ semver: 7.5.4
+ source-map: 0.6.1
+ typescript: 5.8.2
+ transitivePeerDependencies:
+ - '@types/node'
+
+ '@microsoft/tsdoc-config@0.17.1':
+ dependencies:
+ '@microsoft/tsdoc': 0.15.1
+ ajv: 8.12.0
+ jju: 1.4.0
+ resolve: 1.22.11
+
+ '@microsoft/tsdoc@0.15.1': {}
+
+ '@mrmlnc/readdir-enhanced@2.2.1':
+ dependencies:
+ call-me-maybe: 1.0.2
+ glob-to-regexp: 0.3.0
+
+ '@noble/ciphers@1.3.0': {}
+
+ '@noble/curves@1.3.0':
+ dependencies:
+ '@noble/hashes': 1.3.3
+
+ '@noble/curves@1.9.1':
+ dependencies:
+ '@noble/hashes': 1.8.0
+
+ '@noble/hashes@1.3.3': {}
+
+ '@noble/hashes@1.8.0': {}
+
+ '@noble/secp256k1@2.0.0': {}
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@1.1.3': {}
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.19.1
+
+ '@pkgjs/parseargs@0.11.0':
+ optional: true
+
+ '@polka/url@1.0.0-next.29': {}
+
+ '@radix-ui/number@1.1.1': {}
+
+ '@radix-ui/primitive@1.1.3': {}
+
+ '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-context@1.1.2(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.26)(react@18.3.1)
+ aria-hidden: 1.2.6
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-remove-scroll: 2.7.1(@types/react@18.3.26)(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-direction@1.1.1(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-id@1.1.1(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-label@2.1.8(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ aria-hidden: 1.2.6
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-remove-scroll: 2.7.1(@types/react@18.3.26)(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.26)(react@18.3.1)
+ aria-hidden: 1.2.6
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-remove-scroll: 2.7.1(@types/react@18.3.26)(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/rect': 1.1.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.4(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-select@2.2.6(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/number': 1.1.1
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ aria-hidden: 1.2.6
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-remove-scroll: 2.7.1(@types/react@18.3.26)(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-slot@1.2.3(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-slot@1.2.4(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-toast@1.2.15(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.26)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/rect': 1.1.1
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-use-size@1.1.1(@types/react@18.3.26)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.26)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@radix-ui/rect@1.1.1': {}
+
+ '@rolldown/pluginutils@1.0.0-beta.27': {}
+
+ '@rollup/pluginutils@5.3.0(rollup@4.52.5)':
+ dependencies:
+ '@types/estree': 1.0.8
+ estree-walker: 2.0.2
+ picomatch: 4.0.3
+ optionalDependencies:
+ rollup: 4.52.5
+
+ '@rollup/rollup-android-arm-eabi@4.52.5':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.52.5':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.52.5':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.52.5':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.52.5':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.52.5':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.52.5':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.52.5':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.52.5':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.52.5':
+ optional: true
+
+ '@rushstack/node-core-library@5.18.0(@types/node@22.19.0)':
+ dependencies:
+ ajv: 8.13.0
+ ajv-draft-04: 1.0.0(ajv@8.13.0)
+ ajv-formats: 3.0.1(ajv@8.13.0)
+ fs-extra: 11.3.2
+ import-lazy: 4.0.0
+ jju: 1.4.0
+ resolve: 1.22.11
+ semver: 7.5.4
+ optionalDependencies:
+ '@types/node': 22.19.0
+
+ '@rushstack/problem-matcher@0.1.1(@types/node@22.19.0)':
+ optionalDependencies:
+ '@types/node': 22.19.0
+
+ '@rushstack/rig-package@0.6.0':
+ dependencies:
+ resolve: 1.22.11
+ strip-json-comments: 3.1.1
+
+ '@rushstack/terminal@0.19.3(@types/node@22.19.0)':
+ dependencies:
+ '@rushstack/node-core-library': 5.18.0(@types/node@22.19.0)
+ '@rushstack/problem-matcher': 0.1.1(@types/node@22.19.0)
+ supports-color: 8.1.1
+ optionalDependencies:
+ '@types/node': 22.19.0
+
+ '@rushstack/ts-command-line@5.1.3(@types/node@22.19.0)':
+ dependencies:
+ '@rushstack/terminal': 0.19.3(@types/node@22.19.0)
+ '@types/argparse': 1.0.38
+ argparse: 1.0.10
+ string-argv: 0.3.2
+ transitivePeerDependencies:
+ - '@types/node'
+
+ '@scure/base@1.1.5': {}
+
+ '@scure/base@1.2.6': {}
+
+ '@scure/bip32@1.7.0':
+ dependencies:
+ '@noble/curves': 1.9.1
+ '@noble/hashes': 1.8.0
+ '@scure/base': 1.2.6
+
+ '@scure/bip39@1.6.0':
+ dependencies:
+ '@noble/hashes': 1.8.0
+ '@scure/base': 1.2.6
+
+ '@testing-library/dom@10.4.1':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/runtime': 7.28.4
+ '@types/aria-query': 5.0.4
+ aria-query: 5.3.0
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ picocolors: 1.1.1
+ pretty-format: 27.5.1
+
+ '@testing-library/jest-dom@6.4.7(vitest@3.2.4)':
+ dependencies:
+ '@adobe/css-tools': 4.4.4
+ '@babel/runtime': 7.28.4
+ aria-query: 5.3.2
+ chalk: 3.0.0
+ css.escape: 1.5.1
+ dom-accessibility-api: 0.6.3
+ lodash: 4.17.21
+ redent: 3.0.0
+ optionalDependencies:
+ vitest: 3.2.4(@types/node@22.19.0)(@vitest/ui@3.2.4)(jsdom@24.1.3)
+
+ '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@babel/runtime': 7.28.4
+ '@testing-library/dom': 10.4.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+ '@types/react-dom': 18.3.7(@types/react@18.3.26)
+
+ '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
+ dependencies:
+ '@testing-library/dom': 10.4.1
+
+ '@types/argparse@1.0.38': {}
+
+ '@types/aria-query@5.0.4': {}
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.28.5
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.28.5
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/estree@1.0.8': {}
+
+ '@types/glob@7.2.0':
+ dependencies:
+ '@types/minimatch': 6.0.0
+ '@types/node': 22.19.0
+
+ '@types/hast@3.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/minimatch@6.0.0':
+ dependencies:
+ minimatch: 10.1.1
+
+ '@types/node@22.19.0':
+ dependencies:
+ undici-types: 6.21.0
+
+ '@types/prismjs@1.26.5': {}
+
+ '@types/prop-types@15.7.15': {}
+
+ '@types/react-dom@18.3.7(@types/react@18.3.26)':
+ dependencies:
+ '@types/react': 18.3.26
+
+ '@types/react-syntax-highlighter@15.5.13':
+ dependencies:
+ '@types/react': 18.3.26
+
+ '@types/react@18.3.26':
+ dependencies:
+ '@types/prop-types': 15.7.15
+ csstype: 3.1.3
+
+ '@types/unist@2.0.11': {}
+
+ '@types/unist@3.0.3': {}
+
+ '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.0))':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5)
+ '@rolldown/pluginutils': 1.0.0-beta.27
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.17.0
+ vite: 5.4.21(@types/node@22.19.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vitest/coverage-v8@3.2.4(vitest@3.2.4)':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@bcoe/v8-coverage': 1.0.2
+ ast-v8-to-istanbul: 0.3.8
+ debug: 4.4.3
+ istanbul-lib-coverage: 3.2.2
+ istanbul-lib-report: 3.0.1
+ istanbul-lib-source-maps: 5.0.6
+ istanbul-reports: 3.2.0
+ magic-string: 0.30.21
+ magicast: 0.3.5
+ std-env: 3.10.0
+ test-exclude: 7.0.1
+ tinyrainbow: 2.0.0
+ vitest: 3.2.4(@types/node@22.19.0)(@vitest/ui@3.2.4)(jsdom@24.1.3)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vitest/expect@3.2.4':
+ dependencies:
+ '@types/chai': 5.2.3
+ '@vitest/spy': 3.2.4
+ '@vitest/utils': 3.2.4
+ chai: 5.3.3
+ tinyrainbow: 2.0.0
+
+ '@vitest/mocker@3.2.4(vite@5.4.21(@types/node@22.19.0))':
+ dependencies:
+ '@vitest/spy': 3.2.4
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 5.4.21(@types/node@22.19.0)
+
+ '@vitest/pretty-format@3.2.4':
+ dependencies:
+ tinyrainbow: 2.0.0
+
+ '@vitest/runner@3.2.4':
+ dependencies:
+ '@vitest/utils': 3.2.4
+ pathe: 2.0.3
+ strip-literal: 3.1.0
+
+ '@vitest/snapshot@3.2.4':
+ dependencies:
+ '@vitest/pretty-format': 3.2.4
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@3.2.4':
+ dependencies:
+ tinyspy: 4.0.4
+
+ '@vitest/ui@3.2.4(vitest@3.2.4)':
+ dependencies:
+ '@vitest/utils': 3.2.4
+ fflate: 0.8.2
+ flatted: 3.3.3
+ pathe: 2.0.3
+ sirv: 3.0.2
+ tinyglobby: 0.2.15
+ tinyrainbow: 2.0.0
+ vitest: 3.2.4(@types/node@22.19.0)(@vitest/ui@3.2.4)(jsdom@24.1.3)
+
+ '@vitest/utils@3.2.4':
+ dependencies:
+ '@vitest/pretty-format': 3.2.4
+ loupe: 3.2.1
+ tinyrainbow: 2.0.0
+
+ '@volar/language-core@2.4.23':
+ dependencies:
+ '@volar/source-map': 2.4.23
+
+ '@volar/source-map@2.4.23': {}
+
+ '@volar/typescript@2.4.23':
+ dependencies:
+ '@volar/language-core': 2.4.23
+ path-browserify: 1.0.1
+ vscode-uri: 3.1.0
+
+ '@vue/compiler-core@3.5.23':
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@vue/shared': 3.5.23
+ entities: 4.5.0
+ estree-walker: 2.0.2
+ source-map-js: 1.2.1
+
+ '@vue/compiler-dom@3.5.23':
+ dependencies:
+ '@vue/compiler-core': 3.5.23
+ '@vue/shared': 3.5.23
+
+ '@vue/compiler-vue2@2.7.16':
+ dependencies:
+ de-indent: 1.0.2
+ he: 1.2.0
+
+ '@vue/language-core@2.2.0(typescript@5.8.2)':
+ dependencies:
+ '@volar/language-core': 2.4.23
+ '@vue/compiler-dom': 3.5.23
+ '@vue/compiler-vue2': 2.7.16
+ '@vue/shared': 3.5.23
+ alien-signals: 0.4.14
+ minimatch: 9.0.5
+ muggle-string: 0.4.1
+ path-browserify: 1.0.1
+ optionalDependencies:
+ typescript: 5.8.2
+
+ '@vue/shared@3.5.23': {}
+
+ abitype@1.1.0(typescript@5.8.2)(zod@4.1.12):
+ optionalDependencies:
+ typescript: 5.8.2
+ zod: 4.1.12
+
+ acorn@8.15.0: {}
+
+ agent-base@7.1.4: {}
+
+ ajv-draft-04@1.0.0(ajv@8.13.0):
+ optionalDependencies:
+ ajv: 8.13.0
+
+ ajv-formats@3.0.1(ajv@8.13.0):
+ optionalDependencies:
+ ajv: 8.13.0
+
+ ajv@8.12.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+ uri-js: 4.4.1
+
+ ajv@8.13.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+ uri-js: 4.4.1
+
+ alien-signals@0.4.14: {}
+
+ ansi-regex@5.0.1: {}
+
+ ansi-regex@6.2.2: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ ansi-styles@5.2.0: {}
+
+ ansi-styles@6.2.3: {}
+
+ any-promise@1.3.0: {}
+
+ anymatch@3.1.3:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+
+ arg@5.0.2: {}
+
+ argparse@1.0.10:
+ dependencies:
+ sprintf-js: 1.0.3
+
+ aria-hidden@1.2.6:
+ dependencies:
+ tslib: 2.8.1
+
+ aria-query@5.3.0:
+ dependencies:
+ dequal: 2.0.3
+
+ aria-query@5.3.2: {}
+
+ arr-diff@4.0.0: {}
+
+ arr-flatten@1.1.0: {}
+
+ arr-union@3.1.0: {}
+
+ array-union@1.0.2:
+ dependencies:
+ array-uniq: 1.0.3
+
+ array-uniq@1.0.3: {}
+
+ array-unique@0.3.2: {}
+
+ assertion-error@2.0.1: {}
+
+ assign-symbols@1.0.0: {}
+
+ ast-v8-to-istanbul@0.3.8:
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ estree-walker: 3.0.3
+ js-tokens: 9.0.1
+
+ asynckit@0.4.0: {}
+
+ atob@2.1.2: {}
+
+ autoprefixer@10.4.21(postcss@8.5.6):
+ dependencies:
+ browserslist: 4.27.0
+ caniuse-lite: 1.0.30001754
+ fraction.js: 4.3.7
+ normalize-range: 0.1.2
+ picocolors: 1.1.1
+ postcss: 8.5.6
+ postcss-value-parser: 4.2.0
+
+ available-typed-arrays@1.0.7:
+ dependencies:
+ possible-typed-array-names: 1.1.0
+
+ balanced-match@1.0.2: {}
+
+ base64-js@1.5.1: {}
+
+ base@0.11.2:
+ dependencies:
+ cache-base: 1.0.1
+ class-utils: 0.3.6
+ component-emitter: 1.3.1
+ define-property: 1.0.0
+ isobject: 3.0.1
+ mixin-deep: 1.3.2
+ pascalcase: 0.1.1
+
+ baseline-browser-mapping@2.8.25: {}
+
+ binary-extensions@2.3.0: {}
+
+ brace-expansion@1.1.12:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@2.0.2:
+ dependencies:
+ balanced-match: 1.0.2
+
+ braces@2.3.2:
+ dependencies:
+ arr-flatten: 1.1.0
+ array-unique: 0.3.2
+ extend-shallow: 2.0.1
+ fill-range: 4.0.0
+ isobject: 3.0.1
+ repeat-element: 1.1.4
+ snapdragon: 0.8.2
+ snapdragon-node: 2.1.1
+ split-string: 3.1.0
+ to-regex: 3.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ browserslist@4.27.0:
+ dependencies:
+ baseline-browser-mapping: 2.8.25
+ caniuse-lite: 1.0.30001754
+ electron-to-chromium: 1.5.248
+ node-releases: 2.0.27
+ update-browserslist-db: 1.1.4(browserslist@4.27.0)
+
+ buffer@6.0.3:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
+ bundle-require@5.1.0(esbuild@0.25.12):
+ dependencies:
+ esbuild: 0.25.12
+ load-tsconfig: 0.2.5
+
+ cac@6.7.14: {}
+
+ cache-base@1.0.1:
+ dependencies:
+ collection-visit: 1.0.0
+ component-emitter: 1.3.1
+ get-value: 2.0.6
+ has-value: 1.0.0
+ isobject: 3.0.1
+ set-value: 2.0.1
+ to-object-path: 0.3.0
+ union-value: 1.0.1
+ unset-value: 1.0.0
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bind@1.0.8:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ get-intrinsic: 1.3.0
+ set-function-length: 1.2.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
+ call-me-maybe@1.0.2: {}
+
+ camelcase-css@2.0.1: {}
+
+ caniuse-lite@1.0.30001754: {}
+
+ chai@5.3.3:
+ dependencies:
+ assertion-error: 2.0.1
+ check-error: 2.1.1
+ deep-eql: 5.0.2
+ loupe: 3.2.1
+ pathval: 2.0.1
+
+ chalk@3.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ character-entities-legacy@3.0.0: {}
+
+ character-entities@2.0.2: {}
+
+ character-reference-invalid@2.0.1: {}
+
+ check-error@2.1.1: {}
+
+ chokidar@3.6.0:
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.3
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ chokidar@4.0.3:
+ dependencies:
+ readdirp: 4.1.2
+
+ class-utils@0.3.6:
+ dependencies:
+ arr-union: 3.1.0
+ define-property: 0.2.5
+ isobject: 3.0.1
+ static-extend: 0.1.2
+
+ class-variance-authority@0.7.1:
+ dependencies:
+ clsx: 2.1.1
+
+ clsx@2.1.1: {}
+
+ collection-visit@1.0.0:
+ dependencies:
+ map-visit: 1.0.0
+ object-visit: 1.0.1
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ comma-separated-tokens@2.0.3: {}
+
+ commander@2.20.3: {}
+
+ commander@4.1.1: {}
+
+ compare-versions@6.1.1: {}
+
+ component-emitter@1.3.1: {}
+
+ concat-map@0.0.1: {}
+
+ confbox@0.1.8: {}
+
+ confbox@0.2.2: {}
+
+ consola@3.4.2: {}
+
+ convert-source-map@2.0.0: {}
+
+ copy-descriptor@0.1.1: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ css.escape@1.5.1: {}
+
+ cssesc@3.0.0: {}
+
+ cssstyle@4.6.0:
+ dependencies:
+ '@asamuzakjp/css-color': 3.2.0
+ rrweb-cssom: 0.8.0
+
+ csstype@3.1.3: {}
+
+ data-urls@5.0.0:
+ dependencies:
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.2.0
+
+ de-indent@1.0.2: {}
+
+ debug@2.6.9:
+ dependencies:
+ ms: 2.0.0
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ decimal.js@10.6.0: {}
+
+ decode-named-character-reference@1.2.0:
+ dependencies:
+ character-entities: 2.0.2
+
+ decode-uri-component@0.2.2: {}
+
+ deep-eql@5.0.2: {}
+
+ define-data-property@1.1.4:
+ dependencies:
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ define-property@0.2.5:
+ dependencies:
+ is-descriptor: 0.1.7
+
+ define-property@1.0.0:
+ dependencies:
+ is-descriptor: 1.0.3
+
+ define-property@2.0.2:
+ dependencies:
+ is-descriptor: 1.0.3
+ isobject: 3.0.1
+
+ delayed-stream@1.0.0: {}
+
+ dequal@2.0.3: {}
+
+ detect-node-es@1.1.0: {}
+
+ didyoumean@1.2.2: {}
+
+ diff@8.0.2: {}
+
+ dir-glob@2.2.2:
+ dependencies:
+ path-type: 3.0.0
+
+ dlv@1.1.3: {}
+
+ dom-accessibility-api@0.5.16: {}
+
+ dom-accessibility-api@0.6.3: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ eastasianwidth@0.2.0: {}
+
+ electron-to-chromium@1.5.248: {}
+
+ emoji-regex@8.0.0: {}
+
+ emoji-regex@9.2.2: {}
+
+ entities@4.5.0: {}
+
+ entities@6.0.1: {}
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-module-lexer@1.7.0: {}
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ esbuild-fix-imports-plugin@1.0.23: {}
+
+ esbuild-plugin-babel@0.2.3(@babel/core@7.28.5):
+ dependencies:
+ '@babel/core': 7.28.5
+
+ esbuild@0.21.5:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.21.5
+ '@esbuild/android-arm': 0.21.5
+ '@esbuild/android-arm64': 0.21.5
+ '@esbuild/android-x64': 0.21.5
+ '@esbuild/darwin-arm64': 0.21.5
+ '@esbuild/darwin-x64': 0.21.5
+ '@esbuild/freebsd-arm64': 0.21.5
+ '@esbuild/freebsd-x64': 0.21.5
+ '@esbuild/linux-arm': 0.21.5
+ '@esbuild/linux-arm64': 0.21.5
+ '@esbuild/linux-ia32': 0.21.5
+ '@esbuild/linux-loong64': 0.21.5
+ '@esbuild/linux-mips64el': 0.21.5
+ '@esbuild/linux-ppc64': 0.21.5
+ '@esbuild/linux-riscv64': 0.21.5
+ '@esbuild/linux-s390x': 0.21.5
+ '@esbuild/linux-x64': 0.21.5
+ '@esbuild/netbsd-x64': 0.21.5
+ '@esbuild/openbsd-x64': 0.21.5
+ '@esbuild/sunos-x64': 0.21.5
+ '@esbuild/win32-arm64': 0.21.5
+ '@esbuild/win32-ia32': 0.21.5
+ '@esbuild/win32-x64': 0.21.5
+
+ esbuild@0.25.12:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.12
+ '@esbuild/android-arm': 0.25.12
+ '@esbuild/android-arm64': 0.25.12
+ '@esbuild/android-x64': 0.25.12
+ '@esbuild/darwin-arm64': 0.25.12
+ '@esbuild/darwin-x64': 0.25.12
+ '@esbuild/freebsd-arm64': 0.25.12
+ '@esbuild/freebsd-x64': 0.25.12
+ '@esbuild/linux-arm': 0.25.12
+ '@esbuild/linux-arm64': 0.25.12
+ '@esbuild/linux-ia32': 0.25.12
+ '@esbuild/linux-loong64': 0.25.12
+ '@esbuild/linux-mips64el': 0.25.12
+ '@esbuild/linux-ppc64': 0.25.12
+ '@esbuild/linux-riscv64': 0.25.12
+ '@esbuild/linux-s390x': 0.25.12
+ '@esbuild/linux-x64': 0.25.12
+ '@esbuild/netbsd-arm64': 0.25.12
+ '@esbuild/netbsd-x64': 0.25.12
+ '@esbuild/openbsd-arm64': 0.25.12
+ '@esbuild/openbsd-x64': 0.25.12
+ '@esbuild/openharmony-arm64': 0.25.12
+ '@esbuild/sunos-x64': 0.25.12
+ '@esbuild/win32-arm64': 0.25.12
+ '@esbuild/win32-ia32': 0.25.12
+ '@esbuild/win32-x64': 0.25.12
+
+ escalade@3.2.0: {}
+
+ estree-walker@2.0.2: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
+ eventemitter3@5.0.1: {}
+
+ expand-brackets@2.1.4:
+ dependencies:
+ debug: 2.6.9
+ define-property: 0.2.5
+ extend-shallow: 2.0.1
+ posix-character-classes: 0.1.1
+ regex-not: 1.0.2
+ snapdragon: 0.8.2
+ to-regex: 3.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ expect-type@1.2.2: {}
+
+ exsolve@1.0.7: {}
+
+ extend-shallow@2.0.1:
+ dependencies:
+ is-extendable: 0.1.1
+
+ extend-shallow@3.0.2:
+ dependencies:
+ assign-symbols: 1.0.0
+ is-extendable: 1.0.1
+
+ extglob@2.0.4:
+ dependencies:
+ array-unique: 0.3.2
+ define-property: 1.0.0
+ expand-brackets: 2.1.4
+ extend-shallow: 2.0.1
+ fragment-cache: 0.2.1
+ regex-not: 1.0.2
+ snapdragon: 0.8.2
+ to-regex: 3.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-glob@2.2.7:
+ dependencies:
+ '@mrmlnc/readdir-enhanced': 2.2.1
+ '@nodelib/fs.stat': 1.1.3
+ glob-parent: 3.1.0
+ is-glob: 4.0.3
+ merge2: 1.4.1
+ micromatch: 3.1.10
+ transitivePeerDependencies:
+ - supports-color
+
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fastq@1.19.1:
+ dependencies:
+ reusify: 1.1.0
+
+ fault@1.0.4:
+ dependencies:
+ format: 0.2.2
+
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
+ fflate@0.8.2: {}
+
+ fill-range@4.0.0:
+ dependencies:
+ extend-shallow: 2.0.1
+ is-number: 3.0.0
+ repeat-string: 1.6.1
+ to-regex-range: 2.1.1
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ fix-dts-default-cjs-exports@1.0.1:
+ dependencies:
+ magic-string: 0.30.21
+ mlly: 1.8.0
+ rollup: 4.52.5
+
+ flatted@3.3.3: {}
+
+ for-each@0.3.5:
+ dependencies:
+ is-callable: 1.2.7
+
+ for-in@1.0.2: {}
+
+ foreground-child@3.3.1:
+ dependencies:
+ cross-spawn: 7.0.6
+ signal-exit: 4.1.0
+
+ form-data@4.0.4:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.2
+ mime-types: 2.1.35
+
+ format@0.2.2: {}
+
+ fraction.js@4.3.7: {}
+
+ fragment-cache@0.2.1:
+ dependencies:
+ map-cache: 0.2.2
+
+ fs-extra@11.3.2:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.2.0
+ universalify: 2.0.1
+
+ fs.realpath@1.0.0: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ generator-function@2.0.1: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
+ get-nonce@1.0.1: {}
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ get-value@2.0.6: {}
+
+ glob-parent@3.1.0:
+ dependencies:
+ is-glob: 3.1.0
+ path-dirname: 1.0.2
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-to-regexp@0.3.0: {}
+
+ glob@10.4.5:
+ dependencies:
+ foreground-child: 3.3.1
+ jackspeak: 3.4.3
+ minimatch: 9.0.5
+ minipass: 7.1.2
+ package-json-from-dist: 1.0.1
+ path-scurry: 1.11.1
+
+ glob@11.0.3:
+ dependencies:
+ foreground-child: 3.3.1
+ jackspeak: 4.1.1
+ minimatch: 10.1.1
+ minipass: 7.1.2
+ package-json-from-dist: 1.0.1
+ path-scurry: 2.0.0
+
+ glob@7.2.3:
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.2
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+
+ globby@9.2.0:
+ dependencies:
+ '@types/glob': 7.2.0
+ array-union: 1.0.2
+ dir-glob: 2.2.2
+ fast-glob: 2.2.7
+ glob: 7.2.3
+ ignore: 4.0.6
+ pify: 4.0.1
+ slash: 2.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ gopd@1.2.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ has-flag@4.0.0: {}
+
+ has-property-descriptors@1.0.2:
+ dependencies:
+ es-define-property: 1.0.1
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ has-value@0.3.1:
+ dependencies:
+ get-value: 2.0.6
+ has-values: 0.1.4
+ isobject: 2.1.0
+
+ has-value@1.0.0:
+ dependencies:
+ get-value: 2.0.6
+ has-values: 1.0.0
+ isobject: 3.0.1
+
+ has-values@0.1.4: {}
+
+ has-values@1.0.0:
+ dependencies:
+ is-number: 3.0.0
+ kind-of: 4.0.0
+
+ hasown@2.0.2:
+ dependencies:
+ function-bind: 1.1.2
+
+ hast-util-parse-selector@4.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ hastscript@9.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ comma-separated-tokens: 2.0.3
+ hast-util-parse-selector: 4.0.0
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+
+ he@1.2.0: {}
+
+ highlight.js@10.7.3: {}
+
+ highlightjs-vue@1.0.0: {}
+
+ html-encoding-sniffer@4.0.0:
+ dependencies:
+ whatwg-encoding: 3.1.1
+
+ html-escaper@2.0.2: {}
+
+ http-proxy-agent@7.0.2:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ ieee754@1.2.1: {}
+
+ ignore@4.0.6: {}
+
+ import-lazy@4.0.0: {}
+
+ indent-string@4.0.0: {}
+
+ inflight@1.0.6:
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+
+ inherits@2.0.4: {}
+
+ is-accessor-descriptor@1.0.1:
+ dependencies:
+ hasown: 2.0.2
+
+ is-alphabetical@2.0.1: {}
+
+ is-alphanumerical@2.0.1:
+ dependencies:
+ is-alphabetical: 2.0.1
+ is-decimal: 2.0.1
+
+ is-arguments@1.2.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-binary-path@2.1.0:
+ dependencies:
+ binary-extensions: 2.3.0
+
+ is-buffer@1.1.6: {}
+
+ is-callable@1.2.7: {}
+
+ is-core-module@2.16.1:
+ dependencies:
+ hasown: 2.0.2
+
+ is-data-descriptor@1.0.1:
+ dependencies:
+ hasown: 2.0.2
+
+ is-decimal@2.0.1: {}
+
+ is-descriptor@0.1.7:
+ dependencies:
+ is-accessor-descriptor: 1.0.1
+ is-data-descriptor: 1.0.1
+
+ is-descriptor@1.0.3:
+ dependencies:
+ is-accessor-descriptor: 1.0.1
+ is-data-descriptor: 1.0.1
+
+ is-extendable@0.1.1: {}
+
+ is-extendable@1.0.1:
+ dependencies:
+ is-plain-object: 2.0.4
+
+ is-extglob@2.1.1: {}
+
+ is-fullwidth-code-point@3.0.0: {}
+
+ is-generator-function@1.1.2:
+ dependencies:
+ call-bound: 1.0.4
+ generator-function: 2.0.1
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
+ is-glob@3.1.0:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-hexadecimal@2.0.1: {}
+
+ is-number@3.0.0:
+ dependencies:
+ kind-of: 3.2.2
+
+ is-number@7.0.0: {}
+
+ is-plain-object@2.0.4:
+ dependencies:
+ isobject: 3.0.1
+
+ is-potential-custom-element-name@1.0.1: {}
+
+ is-regex@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ is-typed-array@1.1.15:
+ dependencies:
+ which-typed-array: 1.1.19
+
+ is-windows@1.0.2: {}
+
+ isarray@1.0.0: {}
+
+ isexe@2.0.0: {}
+
+ isobject@2.1.0:
+ dependencies:
+ isarray: 1.0.0
+
+ isobject@3.0.1: {}
+
+ isows@1.0.7(ws@8.18.3):
+ dependencies:
+ ws: 8.18.3
+
+ istanbul-lib-coverage@3.2.2: {}
+
+ istanbul-lib-report@3.0.1:
+ dependencies:
+ istanbul-lib-coverage: 3.2.2
+ make-dir: 4.0.0
+ supports-color: 7.2.0
+
+ istanbul-lib-source-maps@5.0.6:
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ debug: 4.4.3
+ istanbul-lib-coverage: 3.2.2
+ transitivePeerDependencies:
+ - supports-color
+
+ istanbul-reports@3.2.0:
+ dependencies:
+ html-escaper: 2.0.2
+ istanbul-lib-report: 3.0.1
+
+ jackspeak@3.4.3:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+
+ jackspeak@4.1.1:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+
+ jiti@1.21.7: {}
+
+ jju@1.4.0: {}
+
+ joycon@3.1.1: {}
+
+ js-tokens@4.0.0: {}
+
+ js-tokens@9.0.1: {}
+
+ jsdom@24.1.3:
+ dependencies:
+ cssstyle: 4.6.0
+ data-urls: 5.0.0
+ decimal.js: 10.6.0
+ form-data: 4.0.4
+ html-encoding-sniffer: 4.0.0
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ is-potential-custom-element-name: 1.0.1
+ nwsapi: 2.2.22
+ parse5: 7.3.0
+ rrweb-cssom: 0.7.1
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 4.1.4
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 7.0.0
+ whatwg-encoding: 3.1.1
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.2.0
+ ws: 8.18.3
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ jsesc@3.1.0: {}
+
+ json-canonicalize@1.2.0: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ json5@2.2.3: {}
+
+ jsonfile@6.2.0:
+ dependencies:
+ universalify: 2.0.1
+ optionalDependencies:
+ graceful-fs: 4.2.11
+
+ kind-of@3.2.2:
+ dependencies:
+ is-buffer: 1.1.6
+
+ kind-of@4.0.0:
+ dependencies:
+ is-buffer: 1.1.6
+
+ kind-of@6.0.3: {}
+
+ kolorist@1.8.0: {}
+
+ lilconfig@3.1.3: {}
+
+ lines-and-columns@1.2.4: {}
+
+ load-tsconfig@0.2.5: {}
+
+ local-pkg@1.1.2:
+ dependencies:
+ mlly: 1.8.0
+ pkg-types: 2.3.0
+ quansync: 0.2.11
+
+ lodash.debounce@4.0.8: {}
+
+ lodash.sortby@4.7.0: {}
+
+ lodash@4.17.21: {}
+
+ loose-envify@1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
+
+ loupe@3.2.1: {}
+
+ lowlight@1.20.0:
+ dependencies:
+ fault: 1.0.4
+ highlight.js: 10.7.3
+
+ lru-cache@10.4.3: {}
+
+ lru-cache@11.2.2: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ lru-cache@6.0.0:
+ dependencies:
+ yallist: 4.0.0
+
+ lucide-react@0.468.0(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+
+ lz-string@1.5.0: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ magicast@0.3.5:
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+ source-map-js: 1.2.1
+
+ make-dir@4.0.0:
+ dependencies:
+ semver: 7.7.3
+
+ map-cache@0.2.2: {}
+
+ map-visit@1.0.0:
+ dependencies:
+ object-visit: 1.0.1
+
+ math-intrinsics@1.1.0: {}
+
+ merge2@1.4.1: {}
+
+ micro-eth-signer@0.7.2:
+ dependencies:
+ '@ethereumjs/rlp': 5.0.0
+ '@noble/curves': 1.3.0
+ '@noble/hashes': 1.3.3
+ '@scure/base': 1.1.5
+ micro-packed: 0.5.3
+
+ micro-packed@0.5.3:
+ dependencies:
+ '@scure/base': 1.1.5
+
+ micromatch@3.1.10:
+ dependencies:
+ arr-diff: 4.0.0
+ array-unique: 0.3.2
+ braces: 2.3.2
+ define-property: 2.0.2
+ extend-shallow: 3.0.2
+ extglob: 2.0.4
+ fragment-cache: 0.2.1
+ kind-of: 6.0.3
+ nanomatch: 1.2.13
+ object.pick: 1.3.0
+ regex-not: 1.0.2
+ snapdragon: 0.8.2
+ to-regex: 3.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ min-indent@1.0.1: {}
+
+ minimatch@10.0.3:
+ dependencies:
+ '@isaacs/brace-expansion': 5.0.0
+
+ minimatch@10.1.1:
+ dependencies:
+ '@isaacs/brace-expansion': 5.0.0
+
+ minimatch@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.12
+
+ minimatch@9.0.5:
+ dependencies:
+ brace-expansion: 2.0.2
+
+ minipass@7.1.2: {}
+
+ mixin-deep@1.3.2:
+ dependencies:
+ for-in: 1.0.2
+ is-extendable: 1.0.1
+
+ mlly@1.8.0:
+ dependencies:
+ acorn: 8.15.0
+ pathe: 2.0.3
+ pkg-types: 1.3.1
+ ufo: 1.6.1
+
+ mrmime@2.0.1: {}
+
+ ms@2.0.0: {}
+
+ ms@2.1.3: {}
+
+ muggle-string@0.4.1: {}
+
+ mz@2.7.0:
+ dependencies:
+ any-promise: 1.3.0
+ object-assign: 4.1.1
+ thenify-all: 1.6.0
+
+ nanoid@3.3.11: {}
+
+ nanomatch@1.2.13:
+ dependencies:
+ arr-diff: 4.0.0
+ array-unique: 0.3.2
+ define-property: 2.0.2
+ extend-shallow: 3.0.2
+ fragment-cache: 0.2.1
+ is-windows: 1.0.2
+ kind-of: 6.0.3
+ object.pick: 1.3.0
+ regex-not: 1.0.2
+ snapdragon: 0.8.2
+ to-regex: 3.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ node-releases@2.0.27: {}
+
+ normalize-path@3.0.0: {}
+
+ normalize-range@0.1.2: {}
+
+ nwsapi@2.2.22: {}
+
+ object-assign@4.1.1: {}
+
+ object-copy@0.1.0:
+ dependencies:
+ copy-descriptor: 0.1.1
+ define-property: 0.2.5
+ kind-of: 3.2.2
+
+ object-hash@3.0.0: {}
+
+ object-visit@1.0.1:
+ dependencies:
+ isobject: 3.0.1
+
+ object.pick@1.3.0:
+ dependencies:
+ isobject: 3.0.1
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
+ ox@0.9.6(typescript@5.8.2)(zod@4.1.12):
+ dependencies:
+ '@adraffy/ens-normalize': 1.11.1
+ '@noble/ciphers': 1.3.0
+ '@noble/curves': 1.9.1
+ '@noble/hashes': 1.8.0
+ '@scure/bip32': 1.7.0
+ '@scure/bip39': 1.6.0
+ abitype: 1.1.0(typescript@5.8.2)(zod@4.1.12)
+ eventemitter3: 5.0.1
+ optionalDependencies:
+ typescript: 5.8.2
+ transitivePeerDependencies:
+ - zod
+
+ package-json-from-dist@1.0.1: {}
+
+ parse-entities@4.0.2:
+ dependencies:
+ '@types/unist': 2.0.11
+ character-entities-legacy: 3.0.0
+ character-reference-invalid: 2.0.1
+ decode-named-character-reference: 1.2.0
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+ is-hexadecimal: 2.0.1
+
+ parse5@7.3.0:
+ dependencies:
+ entities: 6.0.1
+
+ pascalcase@0.1.1: {}
+
+ path-browserify@1.0.1: {}
+
+ path-dirname@1.0.2: {}
+
+ path-is-absolute@1.0.1: {}
+
+ path-key@3.1.1: {}
+
+ path-parse@1.0.7: {}
+
+ path-scurry@1.11.1:
+ dependencies:
+ lru-cache: 10.4.3
+ minipass: 7.1.2
+
+ path-scurry@2.0.0:
+ dependencies:
+ lru-cache: 11.2.2
+ minipass: 7.1.2
+
+ path-type@3.0.0:
+ dependencies:
+ pify: 3.0.0
+
+ pathe@2.0.3: {}
+
+ pathval@2.0.1: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1: {}
+
+ picomatch@4.0.3: {}
+
+ pify@2.3.0: {}
+
+ pify@3.0.0: {}
+
+ pify@4.0.1: {}
+
+ pirates@4.0.7: {}
+
+ pkg-types@1.3.1:
+ dependencies:
+ confbox: 0.1.8
+ mlly: 1.8.0
+ pathe: 2.0.3
+
+ pkg-types@2.3.0:
+ dependencies:
+ confbox: 0.2.2
+ exsolve: 1.0.7
+ pathe: 2.0.3
+
+ posix-character-classes@0.1.1: {}
+
+ possible-typed-array-names@1.1.0: {}
+
+ postcss-import@15.1.0(postcss@8.5.6):
+ dependencies:
+ postcss: 8.5.6
+ postcss-value-parser: 4.2.0
+ read-cache: 1.0.0
+ resolve: 1.22.11
+
+ postcss-import@16.1.1(postcss@8.5.6):
+ dependencies:
+ postcss: 8.5.6
+ postcss-value-parser: 4.2.0
+ read-cache: 1.0.0
+ resolve: 1.22.11
+
+ postcss-js@4.1.0(postcss@8.5.6):
+ dependencies:
+ camelcase-css: 2.0.1
+ postcss: 8.5.6
+
+ postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6):
+ dependencies:
+ lilconfig: 3.1.3
+ optionalDependencies:
+ jiti: 1.21.7
+ postcss: 8.5.6
+
+ postcss-nested@6.2.0(postcss@8.5.6):
+ dependencies:
+ postcss: 8.5.6
+ postcss-selector-parser: 6.1.2
+
+ postcss-selector-parser@6.1.2:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss-value-parser@4.2.0: {}
+
+ postcss@8.5.6:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ pretty-format@27.5.1:
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+
+ prismjs@1.30.0: {}
+
+ process@0.11.10: {}
+
+ property-information@7.1.0: {}
+
+ psl@1.15.0:
+ dependencies:
+ punycode: 2.3.1
+
+ punycode@2.3.1: {}
+
+ quansync@0.2.11: {}
+
+ querystringify@2.2.0: {}
+
+ queue-microtask@1.2.3: {}
+
+ react-dom@18.3.1(react@18.3.1):
+ dependencies:
+ loose-envify: 1.4.0
+ react: 18.3.1
+ scheduler: 0.23.2
+
+ react-is@17.0.2: {}
+
+ react-refresh@0.17.0: {}
+
+ react-remove-scroll-bar@2.3.8(@types/react@18.3.26)(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-style-singleton: 2.2.3(@types/react@18.3.26)(react@18.3.1)
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ react-remove-scroll@2.7.1(@types/react@18.3.26)(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-remove-scroll-bar: 2.3.8(@types/react@18.3.26)(react@18.3.1)
+ react-style-singleton: 2.2.3(@types/react@18.3.26)(react@18.3.1)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@18.3.26)(react@18.3.1)
+ use-sidecar: 1.1.3(@types/react@18.3.26)(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ react-style-singleton@2.2.3(@types/react@18.3.26)(react@18.3.1):
+ dependencies:
+ get-nonce: 1.0.1
+ react: 18.3.1
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ react-syntax-highlighter@16.1.0(react@18.3.1):
+ dependencies:
+ '@babel/runtime': 7.28.4
+ highlight.js: 10.7.3
+ highlightjs-vue: 1.0.0
+ lowlight: 1.20.0
+ prismjs: 1.30.0
+ react: 18.3.1
+ refractor: 5.0.0
+
+ react@18.3.1:
+ dependencies:
+ loose-envify: 1.4.0
+
+ read-cache@1.0.0:
+ dependencies:
+ pify: 2.3.0
+
+ readdirp@3.6.0:
+ dependencies:
+ picomatch: 2.3.1
+
+ readdirp@4.1.2: {}
+
+ redent@3.0.0:
+ dependencies:
+ indent-string: 4.0.0
+ strip-indent: 3.0.0
+
+ refractor@5.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/prismjs': 1.26.5
+ hastscript: 9.0.1
+ parse-entities: 4.0.2
+
+ regex-not@1.0.2:
+ dependencies:
+ extend-shallow: 3.0.2
+ safe-regex: 1.1.0
+
+ repeat-element@1.1.4: {}
+
+ repeat-string@1.6.1: {}
+
+ require-from-string@2.0.2: {}
+
+ requires-port@1.0.0: {}
+
+ resolve-from@5.0.0: {}
+
+ resolve-url@0.2.1: {}
+
+ resolve@1.22.11:
+ dependencies:
+ is-core-module: 2.16.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
+ ret@0.1.15: {}
+
+ reusify@1.1.0: {}
+
+ rollup-plugin-preserve-use-client@3.0.1(rollup@4.52.5):
+ dependencies:
+ rollup: 4.52.5
+
+ rollup@4.52.5:
+ dependencies:
+ '@types/estree': 1.0.8
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.52.5
+ '@rollup/rollup-android-arm64': 4.52.5
+ '@rollup/rollup-darwin-arm64': 4.52.5
+ '@rollup/rollup-darwin-x64': 4.52.5
+ '@rollup/rollup-freebsd-arm64': 4.52.5
+ '@rollup/rollup-freebsd-x64': 4.52.5
+ '@rollup/rollup-linux-arm-gnueabihf': 4.52.5
+ '@rollup/rollup-linux-arm-musleabihf': 4.52.5
+ '@rollup/rollup-linux-arm64-gnu': 4.52.5
+ '@rollup/rollup-linux-arm64-musl': 4.52.5
+ '@rollup/rollup-linux-loong64-gnu': 4.52.5
+ '@rollup/rollup-linux-ppc64-gnu': 4.52.5
+ '@rollup/rollup-linux-riscv64-gnu': 4.52.5
+ '@rollup/rollup-linux-riscv64-musl': 4.52.5
+ '@rollup/rollup-linux-s390x-gnu': 4.52.5
+ '@rollup/rollup-linux-x64-gnu': 4.52.5
+ '@rollup/rollup-linux-x64-musl': 4.52.5
+ '@rollup/rollup-openharmony-arm64': 4.52.5
+ '@rollup/rollup-win32-arm64-msvc': 4.52.5
+ '@rollup/rollup-win32-ia32-msvc': 4.52.5
+ '@rollup/rollup-win32-x64-gnu': 4.52.5
+ '@rollup/rollup-win32-x64-msvc': 4.52.5
+ fsevents: 2.3.3
+
+ rrweb-cssom@0.7.1: {}
+
+ rrweb-cssom@0.8.0: {}
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ safe-regex-test@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-regex: 1.2.1
+
+ safe-regex@1.1.0:
+ dependencies:
+ ret: 0.1.15
+
+ safer-buffer@2.1.2: {}
+
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
+ scheduler@0.23.2:
+ dependencies:
+ loose-envify: 1.4.0
+
+ semver@6.3.1: {}
+
+ semver@7.5.4:
+ dependencies:
+ lru-cache: 6.0.0
+
+ semver@7.7.3: {}
+
+ set-function-length@1.2.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+
+ set-value@2.0.1:
+ dependencies:
+ extend-shallow: 2.0.1
+ is-extendable: 0.1.1
+ is-plain-object: 2.0.4
+ split-string: 3.1.0
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ siginfo@2.0.0: {}
+
+ signal-exit@4.1.0: {}
+
+ sirv@3.0.2:
+ dependencies:
+ '@polka/url': 1.0.0-next.29
+ mrmime: 2.0.1
+ totalist: 3.0.1
+
+ slash@2.0.0: {}
+
+ snapdragon-node@2.1.1:
+ dependencies:
+ define-property: 1.0.0
+ isobject: 3.0.1
+ snapdragon-util: 3.0.1
+
+ snapdragon-util@3.0.1:
+ dependencies:
+ kind-of: 3.2.2
+
+ snapdragon@0.8.2:
+ dependencies:
+ base: 0.11.2
+ debug: 2.6.9
+ define-property: 0.2.5
+ extend-shallow: 2.0.1
+ map-cache: 0.2.2
+ source-map: 0.5.7
+ source-map-resolve: 0.5.3
+ use: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ source-map-js@1.2.1: {}
+
+ source-map-resolve@0.5.3:
+ dependencies:
+ atob: 2.1.2
+ decode-uri-component: 0.2.2
+ resolve-url: 0.2.1
+ source-map-url: 0.4.1
+ urix: 0.1.0
+
+ source-map-url@0.4.1: {}
+
+ source-map@0.5.7: {}
+
+ source-map@0.6.1: {}
+
+ source-map@0.8.0-beta.0:
+ dependencies:
+ whatwg-url: 7.1.0
+
+ space-separated-tokens@2.0.2: {}
+
+ split-string@3.1.0:
+ dependencies:
+ extend-shallow: 3.0.2
+
+ sprintf-js@1.0.3: {}
+
+ stackback@0.0.2: {}
+
+ static-extend@0.1.2:
+ dependencies:
+ define-property: 0.2.5
+ object-copy: 0.1.0
+
+ std-env@3.10.0: {}
+
+ string-argv@0.3.2: {}
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string-width@5.1.2:
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.1.2
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-ansi@7.1.2:
+ dependencies:
+ ansi-regex: 6.2.2
+
+ strip-indent@3.0.0:
+ dependencies:
+ min-indent: 1.0.1
+
+ strip-json-comments@3.1.1: {}
+
+ strip-literal@3.1.0:
+ dependencies:
+ js-tokens: 9.0.1
+
+ sucrase@3.35.0:
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ commander: 4.1.1
+ glob: 10.4.5
+ lines-and-columns: 1.2.4
+ mz: 2.7.0
+ pirates: 4.0.7
+ ts-interface-checker: 0.1.13
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ supports-color@8.1.1:
+ dependencies:
+ has-flag: 4.0.0
+
+ supports-preserve-symlinks-flag@1.0.0: {}
+
+ symbol-tree@3.2.4: {}
+
+ tabbable@6.3.0: {}
+
+ tailwind-merge@3.3.1: {}
+
+ tailwindcss-animate@1.0.7(tailwindcss@3.4.18):
+ dependencies:
+ tailwindcss: 3.4.18
+
+ tailwindcss@3.4.18:
+ dependencies:
+ '@alloc/quick-lru': 5.2.0
+ arg: 5.0.2
+ chokidar: 3.6.0
+ didyoumean: 1.2.2
+ dlv: 1.1.3
+ fast-glob: 3.3.3
+ glob-parent: 6.0.2
+ is-glob: 4.0.3
+ jiti: 1.21.7
+ lilconfig: 3.1.3
+ micromatch: 4.0.8
+ normalize-path: 3.0.0
+ object-hash: 3.0.0
+ picocolors: 1.1.1
+ postcss: 8.5.6
+ postcss-import: 15.1.0(postcss@8.5.6)
+ postcss-js: 4.1.0(postcss@8.5.6)
+ postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)
+ postcss-nested: 6.2.0(postcss@8.5.6)
+ postcss-selector-parser: 6.1.2
+ resolve: 1.22.11
+ sucrase: 3.35.0
+ transitivePeerDependencies:
+ - tsx
+ - yaml
+
+ test-exclude@7.0.1:
+ dependencies:
+ '@istanbuljs/schema': 0.1.3
+ glob: 10.4.5
+ minimatch: 9.0.5
+
+ thenify-all@1.6.0:
+ dependencies:
+ thenify: 3.3.1
+
+ thenify@3.3.1:
+ dependencies:
+ any-promise: 1.3.0
+
+ tinybench@2.9.0: {}
+
+ tinyexec@0.3.2: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
+ tinypool@1.1.1: {}
+
+ tinyrainbow@2.0.0: {}
+
+ tinyspy@4.0.4: {}
+
+ to-object-path@0.3.0:
+ dependencies:
+ kind-of: 3.2.2
+
+ to-regex-range@2.1.1:
+ dependencies:
+ is-number: 3.0.0
+ repeat-string: 1.6.1
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ to-regex@3.0.2:
+ dependencies:
+ define-property: 2.0.2
+ extend-shallow: 3.0.2
+ regex-not: 1.0.2
+ safe-regex: 1.1.0
+
+ totalist@3.0.1: {}
+
+ tough-cookie@4.1.4:
+ dependencies:
+ psl: 1.15.0
+ punycode: 2.3.1
+ universalify: 0.2.0
+ url-parse: 1.5.10
+
+ tr46@1.0.1:
+ dependencies:
+ punycode: 2.3.1
+
+ tr46@5.1.1:
+ dependencies:
+ punycode: 2.3.1
+
+ tree-kill@1.2.2: {}
+
+ ts-interface-checker@0.1.13: {}
+
+ tscpaths@0.0.9:
+ dependencies:
+ commander: 2.20.3
+ globby: 9.2.0
+ transitivePeerDependencies:
+ - supports-color
+
+ tslib@2.8.1: {}
+
+ tsup@8.5.0(@microsoft/api-extractor@7.54.0(@types/node@22.19.0))(jiti@1.21.7)(postcss@8.5.6)(typescript@5.8.2):
+ dependencies:
+ bundle-require: 5.1.0(esbuild@0.25.12)
+ cac: 6.7.14
+ chokidar: 4.0.3
+ consola: 3.4.2
+ debug: 4.4.3
+ esbuild: 0.25.12
+ fix-dts-default-cjs-exports: 1.0.1
+ joycon: 3.1.1
+ picocolors: 1.1.1
+ postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)
+ resolve-from: 5.0.0
+ rollup: 4.52.5
+ source-map: 0.8.0-beta.0
+ sucrase: 3.35.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.15
+ tree-kill: 1.2.2
+ optionalDependencies:
+ '@microsoft/api-extractor': 7.54.0(@types/node@22.19.0)
+ postcss: 8.5.6
+ typescript: 5.8.2
+ transitivePeerDependencies:
+ - jiti
+ - supports-color
+ - tsx
+ - yaml
+
+ typescript@5.8.2: {}
+
+ ufo@1.6.1: {}
+
+ undici-types@6.21.0: {}
+
+ union-value@1.0.1:
+ dependencies:
+ arr-union: 3.1.0
+ get-value: 2.0.6
+ is-extendable: 0.1.1
+ set-value: 2.0.1
+
+ universalify@0.2.0: {}
+
+ universalify@2.0.1: {}
+
+ unset-value@1.0.0:
+ dependencies:
+ has-value: 0.3.1
+ isobject: 3.0.1
+
+ update-browserslist-db@1.1.4(browserslist@4.27.0):
+ dependencies:
+ browserslist: 4.27.0
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ urix@0.1.0: {}
+
+ url-parse@1.5.10:
+ dependencies:
+ querystringify: 2.2.0
+ requires-port: 1.0.0
+
+ use-callback-ref@1.3.3(@types/react@18.3.26)(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ use-sidecar@1.1.3(@types/react@18.3.26)(react@18.3.1):
+ dependencies:
+ detect-node-es: 1.1.0
+ react: 18.3.1
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 18.3.26
+
+ use@3.1.1: {}
+
+ usehooks-ts@3.1.1(react@18.3.1):
+ dependencies:
+ lodash.debounce: 4.0.8
+ react: 18.3.1
+
+ util-deprecate@1.0.2: {}
+
+ util@0.12.5:
+ dependencies:
+ inherits: 2.0.4
+ is-arguments: 1.2.0
+ is-generator-function: 1.1.2
+ is-typed-array: 1.1.15
+ which-typed-array: 1.1.19
+
+ viem@2.38.6(typescript@5.8.2)(zod@4.1.12):
+ dependencies:
+ '@noble/curves': 1.9.1
+ '@noble/hashes': 1.8.0
+ '@scure/bip32': 1.7.0
+ '@scure/bip39': 1.6.0
+ abitype: 1.1.0(typescript@5.8.2)(zod@4.1.12)
+ isows: 1.0.7(ws@8.18.3)
+ ox: 0.9.6(typescript@5.8.2)(zod@4.1.12)
+ ws: 8.18.3
+ optionalDependencies:
+ typescript: 5.8.2
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+ - zod
+
+ vite-node@3.2.4(@types/node@22.19.0):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.4.3
+ es-module-lexer: 1.7.0
+ pathe: 2.0.3
+ vite: 5.4.21(@types/node@22.19.0)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
+ vite-plugin-dts@4.5.4(@types/node@22.19.0)(rollup@4.52.5)(typescript@5.8.2)(vite@5.4.21(@types/node@22.19.0)):
+ dependencies:
+ '@microsoft/api-extractor': 7.54.0(@types/node@22.19.0)
+ '@rollup/pluginutils': 5.3.0(rollup@4.52.5)
+ '@volar/typescript': 2.4.23
+ '@vue/language-core': 2.2.0(typescript@5.8.2)
+ compare-versions: 6.1.1
+ debug: 4.4.3
+ kolorist: 1.8.0
+ local-pkg: 1.1.2
+ magic-string: 0.30.21
+ typescript: 5.8.2
+ optionalDependencies:
+ vite: 5.4.21(@types/node@22.19.0)
+ transitivePeerDependencies:
+ - '@types/node'
+ - rollup
+ - supports-color
+
+ vite-plugin-externalize-deps@0.9.0(vite@5.4.21(@types/node@22.19.0)):
+ dependencies:
+ vite: 5.4.21(@types/node@22.19.0)
+
+ vite@5.4.21(@types/node@22.19.0):
+ dependencies:
+ esbuild: 0.21.5
+ postcss: 8.5.6
+ rollup: 4.52.5
+ optionalDependencies:
+ '@types/node': 22.19.0
+ fsevents: 2.3.3
+
+ vitest@3.2.4(@types/node@22.19.0)(@vitest/ui@3.2.4)(jsdom@24.1.3):
+ dependencies:
+ '@types/chai': 5.2.3
+ '@vitest/expect': 3.2.4
+ '@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@22.19.0))
+ '@vitest/pretty-format': 3.2.4
+ '@vitest/runner': 3.2.4
+ '@vitest/snapshot': 3.2.4
+ '@vitest/spy': 3.2.4
+ '@vitest/utils': 3.2.4
+ chai: 5.3.3
+ debug: 4.4.3
+ expect-type: 1.2.2
+ magic-string: 0.30.21
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.15
+ tinypool: 1.1.1
+ tinyrainbow: 2.0.0
+ vite: 5.4.21(@types/node@22.19.0)
+ vite-node: 3.2.4(@types/node@22.19.0)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 22.19.0
+ '@vitest/ui': 3.2.4(vitest@3.2.4)
+ jsdom: 24.1.3
+ transitivePeerDependencies:
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
+ vscode-uri@3.1.0: {}
+
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
+ webidl-conversions@4.0.2: {}
+
+ webidl-conversions@7.0.0: {}
+
+ whatwg-encoding@3.1.1:
+ dependencies:
+ iconv-lite: 0.6.3
+
+ whatwg-mimetype@4.0.0: {}
+
+ whatwg-url@14.2.0:
+ dependencies:
+ tr46: 5.1.1
+ webidl-conversions: 7.0.0
+
+ whatwg-url@7.1.0:
+ dependencies:
+ lodash.sortby: 4.7.0
+ tr46: 1.0.1
+ webidl-conversions: 4.0.2
+
+ which-typed-array@1.1.19:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ for-each: 0.3.5
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@8.1.0:
+ dependencies:
+ ansi-styles: 6.2.3
+ string-width: 5.1.2
+ strip-ansi: 7.1.2
+
+ wrappy@1.0.2: {}
+
+ ws@8.18.3: {}
+
+ xml-name-validator@5.0.0: {}
+
+ xmlchars@2.2.0: {}
+
+ yallist@3.1.1: {}
+
+ yallist@4.0.0: {}
+
+ zod@4.1.12: {}
diff --git a/ui/pnpm-workspace.yaml b/ui/pnpm-workspace.yaml
new file mode 100644
index 00000000..130a8e35
--- /dev/null
+++ b/ui/pnpm-workspace.yaml
@@ -0,0 +1,5 @@
+packages:
+ - '.'
+ - './playground'
+
+
diff --git a/ui/postcss.config.js b/ui/postcss.config.js
new file mode 100644
index 00000000..2aa7205d
--- /dev/null
+++ b/ui/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/ui/src/3rd-party/earn/aave/aaveProvider.ts b/ui/src/3rd-party/earn/aave/aaveProvider.ts
new file mode 100644
index 00000000..44b5bb42
--- /dev/null
+++ b/ui/src/3rd-party/earn/aave/aaveProvider.ts
@@ -0,0 +1,153 @@
+import type { ChainConfig } from '../../../types/chainConfig';
+import type { WalletClient } from 'viem';
+import type { Avalanche } from '@avalanche-sdk/chainkit';
+import type { EarnPool } from '../../../earn/types';
+import type { EarnProviderBase } from '../../../earn/providers/base';
+import { createPublicClient } from 'viem';
+import { fetchAavePools } from './contracts';
+import { checkTokenApproval, approveTokenForAavePool } from './approve';
+import { depositToAavePool } from './deposit';
+import { withdrawFromAavePool } from './withdraw';
+import { claimAaveRewards } from './claim';
+
+/**
+ * AAVE Earn Provider Implementation
+ */
+export class AaveProvider implements EarnProviderBase {
+ readonly providerId = 'aave';
+
+ async fetchPools(
+ chain: ChainConfig,
+ chainkit: Avalanche,
+ userAddress?: `0x${string}`
+ ): Promise {
+ return fetchAavePools(chain, chainkit, userAddress);
+ }
+
+ async checkApproval(params: {
+ publicClient: ReturnType;
+ pool: EarnPool;
+ amount: string;
+ owner: `0x${string}`;
+ }): Promise<{ needsApproval: boolean; currentAllowance: bigint; requiredAmount: bigint }> {
+ const { publicClient, pool, amount, owner } = params;
+
+ const assetAddress = (pool.token.address as `0x${string}`) || '0x0000000000000000000000000000000000000000' as `0x${string}`;
+
+ if (!assetAddress || assetAddress === '0x0000000000000000000000000000000000000000') {
+ return {
+ needsApproval: false,
+ currentAllowance: BigInt(0),
+ requiredAmount: BigInt(0),
+ };
+ }
+
+ // Get pool address from PoolAddressesProvider
+ const { POOL_ADDRESSES_PROVIDER } = await import('./contracts');
+ const { POOL_ADDRESSES_PROVIDER_ABI } = await import('./abis');
+
+ const poolAddress = await publicClient.readContract({
+ address: POOL_ADDRESSES_PROVIDER as `0x${string}`,
+ abi: POOL_ADDRESSES_PROVIDER_ABI,
+ functionName: 'getPool',
+ }) as `0x${string}`;
+
+ return checkTokenApproval({
+ publicClient,
+ asset: assetAddress,
+ amount,
+ decimals: pool.token.decimals,
+ owner,
+ spender: poolAddress,
+ });
+ }
+
+ async approveToken(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ pool: EarnPool;
+ amount: string;
+ }): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, pool, amount } = params;
+
+ const assetAddress = (pool.token.address as `0x${string}`) || '0x0000000000000000000000000000000000000000' as `0x${string}`;
+
+ return approveTokenForAavePool({
+ walletClient,
+ chain,
+ asset: assetAddress,
+ amount,
+ decimals: pool.token.decimals,
+ });
+ }
+
+ async deposit(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ pool: EarnPool;
+ amount: string;
+ }): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, pool, amount } = params;
+
+ if (!walletClient.account) {
+ throw new Error('Wallet not connected');
+ }
+
+ const assetAddress = (pool.token.address as `0x${string}`) || '0x0000000000000000000000000000000000000000' as `0x${string}`;
+
+ return depositToAavePool({
+ walletClient,
+ chain,
+ asset: assetAddress,
+ amount,
+ decimals: pool.token.decimals,
+ onBehalfOf: walletClient.account.address as `0x${string}`,
+ });
+ }
+
+ async withdraw(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ pool: EarnPool;
+ amount: string;
+ }): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, pool, amount } = params;
+
+ if (!walletClient.account) {
+ throw new Error('Wallet not connected');
+ }
+
+ const assetAddress = (pool.token.address as `0x${string}`) || '0x0000000000000000000000000000000000000000' as `0x${string}`;
+
+ return withdrawFromAavePool({
+ walletClient,
+ chain,
+ asset: assetAddress,
+ amount,
+ decimals: pool.token.decimals,
+ to: walletClient.account.address as `0x${string}`,
+ });
+ }
+
+ async claimRewards(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ pool: EarnPool;
+ }): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, pool } = params;
+
+ if (!walletClient.account) {
+ throw new Error('Wallet not connected');
+ }
+
+ const assetAddress = (pool.token.address as `0x${string}`) || '0x0000000000000000000000000000000000000000' as `0x${string}`;
+
+ return claimAaveRewards({
+ walletClient,
+ chain,
+ assets: [assetAddress],
+ to: walletClient.account.address as `0x${string}`,
+ });
+ }
+}
+
diff --git a/ui/src/3rd-party/earn/aave/abis.ts b/ui/src/3rd-party/earn/aave/abis.ts
new file mode 100644
index 00000000..7e36f2fa
--- /dev/null
+++ b/ui/src/3rd-party/earn/aave/abis.ts
@@ -0,0 +1,138 @@
+import { parseAbi } from 'viem';
+
+/**
+ * AAVE PoolDataProvider ABI
+ * Used for fetching pool data and reserves information
+ */
+export const POOL_DATA_PROVIDER_ABI = [
+ {
+ inputs: [],
+ name: 'getAllReservesTokens',
+ outputs: [
+ {
+ components: [
+ { internalType: 'string', name: 'symbol', type: 'string' },
+ { internalType: 'address', name: 'tokenAddress', type: 'address' },
+ ],
+ internalType: 'struct IPoolDataProvider.TokenData[]',
+ name: '',
+ type: 'tuple[]',
+ },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'asset', type: 'address' }],
+ name: 'getReserveData',
+ outputs: [
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ { internalType: 'uint256', name: 'accruedToTreasuryScaled', type: 'uint256' },
+ { internalType: 'uint256', name: 'totalAToken', type: 'uint256' },
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ { internalType: 'uint256', name: 'totalVariableDebt', type: 'uint256' },
+ { internalType: 'uint256', name: 'liquidityRate', type: 'uint256' },
+ { internalType: 'uint256', name: 'variableBorrowRate', type: 'uint256' },
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ { internalType: 'uint256', name: 'liquidityIndex', type: 'uint256' },
+ { internalType: 'uint256', name: 'variableBorrowIndex', type: 'uint256' },
+ { internalType: 'uint40', name: 'lastUpdateTimestamp', type: 'uint40' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'asset', type: 'address' }],
+ name: 'getReserveConfigurationData',
+ outputs: [
+ { internalType: 'uint256', name: 'decimals', type: 'uint256' },
+ { internalType: 'uint256', name: 'ltv', type: 'uint256' },
+ { internalType: 'uint256', name: 'liquidationThreshold', type: 'uint256' },
+ { internalType: 'uint256', name: 'liquidationBonus', type: 'uint256' },
+ { internalType: 'uint256', name: 'reserveFactor', type: 'uint256' },
+ { internalType: 'bool', name: 'usageAsCollateralEnabled', type: 'bool' },
+ { internalType: 'bool', name: 'borrowingEnabled', type: 'bool' },
+ { internalType: 'bool', name: 'stableBorrowRateEnabled', type: 'bool' },
+ { internalType: 'bool', name: 'isActive', type: 'bool' },
+ { internalType: 'bool', name: 'isFrozen', type: 'bool' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'asset', type: 'address' }],
+ name: 'getATokenTotalSupply',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'asset', type: 'address' },
+ { internalType: 'address', name: 'user', type: 'address' },
+ ],
+ name: 'getUserReserveData',
+ outputs: [
+ { internalType: 'uint256', name: 'currentATokenBalance', type: 'uint256' },
+ { internalType: 'uint256', name: 'currentStableDebt', type: 'uint256' },
+ { internalType: 'uint256', name: 'currentVariableDebt', type: 'uint256' },
+ { internalType: 'uint256', name: 'principalStableDebt', type: 'uint256' },
+ { internalType: 'uint256', name: 'scaledVariableDebt', type: 'uint256' },
+ { internalType: 'uint256', name: 'stableBorrowRate', type: 'uint256' },
+ { internalType: 'uint256', name: 'liquidityRate', type: 'uint256' },
+ { internalType: 'uint40', name: 'stableRateLastUpdated', type: 'uint40' },
+ { internalType: 'bool', name: 'usageAsCollateralEnabled', type: 'bool' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'asset', type: 'address' }],
+ name: 'getReserveTokensAddresses',
+ outputs: [
+ { internalType: 'address', name: 'aTokenAddress', type: 'address' },
+ { internalType: 'address', name: 'stableDebtTokenAddress', type: 'address' },
+ { internalType: 'address', name: 'variableDebtTokenAddress', type: 'address' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+] as const;
+
+/**
+ * AAVE PoolAddressesProvider ABI (basic)
+ * Used for getting the Pool contract address
+ */
+export const POOL_ADDRESSES_PROVIDER_ABI = parseAbi([
+ 'function getPool() view returns (address)',
+]);
+
+/**
+ * AAVE PoolAddressesProvider ABI (full)
+ * Used for getting addresses by ID (e.g., RewardsController)
+ */
+export const POOL_ADDRESSES_PROVIDER_FULL_ABI = parseAbi([
+ 'function getPool() view returns (address)',
+ 'function getAddress(bytes32 id) view returns (address)',
+]);
+
+/**
+ * AAVE Pool ABI
+ * Used for deposit and withdraw operations
+ */
+export const POOL_ABI = parseAbi([
+ 'function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)',
+ 'function supplyWithPermit(address asset, uint256 amount, address onBehalfOf, uint16 referralCode, uint256 deadline, uint8 v, bytes32 r, bytes32 s)',
+ 'function withdraw(address asset, uint256 amount, address to) returns (uint256)',
+]);
+
+/**
+ * AAVE RewardsController ABI
+ * Used for claiming rewards
+ */
+export const REWARDS_CONTROLLER_ABI = parseAbi([
+ 'function claimRewards(address[] calldata assets, uint256 amount, address to, address reward) returns (uint256)',
+ 'function getRewardsList() view returns (address[])',
+]);
+
diff --git a/ui/src/3rd-party/earn/aave/approve.ts b/ui/src/3rd-party/earn/aave/approve.ts
new file mode 100644
index 00000000..fe6878df
--- /dev/null
+++ b/ui/src/3rd-party/earn/aave/approve.ts
@@ -0,0 +1,94 @@
+import { createPublicClient, http, parseUnits } from 'viem';
+import { waitForTransactionReceipt } from 'viem/actions';
+import type { ChainConfig } from '../../../types/chainConfig';
+import type { WalletClient } from 'viem';
+import { ERC20_APPROVAL_ABI } from '../../../utils/erc20';
+import { POOL_ADDRESSES_PROVIDER } from './contracts';
+import { POOL_ADDRESSES_PROVIDER_ABI } from './abis';
+
+/**
+ * Check if token needs approval
+ */
+export async function checkTokenApproval(params: {
+ publicClient: ReturnType;
+ asset: `0x${string}`;
+ amount: string;
+ decimals: number;
+ owner: `0x${string}`;
+ spender: `0x${string}`;
+}): Promise<{ needsApproval: boolean; currentAllowance: bigint; requiredAmount: bigint }> {
+ const { publicClient, asset, amount, decimals, owner, spender } = params;
+
+ const requiredAmount = parseUnits(amount, decimals);
+ const currentAllowance = await publicClient.readContract({
+ address: asset,
+ abi: ERC20_APPROVAL_ABI,
+ functionName: 'allowance',
+ args: [owner, spender],
+ }) as bigint;
+
+ return {
+ needsApproval: currentAllowance < requiredAmount,
+ currentAllowance,
+ requiredAmount,
+ };
+}
+
+/**
+ * Approve token spending for AAVE pool
+ */
+export async function approveTokenForAavePool(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ asset: `0x${string}`;
+ amount: string;
+ decimals: number;
+}): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, asset, amount, decimals } = params;
+
+ if (!walletClient.account) {
+ throw new Error('Wallet not connected');
+ }
+
+ const publicClient = createPublicClient({
+ chain,
+ transport: http(),
+ });
+
+ // Get Pool contract address
+ const poolAddress = await publicClient.readContract({
+ address: POOL_ADDRESSES_PROVIDER as `0x${string}`,
+ abi: POOL_ADDRESSES_PROVIDER_ABI,
+ functionName: 'getPool',
+ }) as `0x${string}`;
+
+ // Parse amount to wei/base units
+ const amountInWei = parseUnits(amount, decimals);
+
+ // Check if native AVAX (empty address) - AAVE requires wrapped tokens
+ if (!asset || asset === '0x0000000000000000000000000000000000000000') {
+ throw new Error('Native AVAX approvals not supported. Please use WAVAX (Wrapped AVAX) instead.');
+ }
+
+ // Approve the pool to spend tokens
+ const approveHash = await walletClient.writeContract({
+ address: asset,
+ abi: ERC20_APPROVAL_ABI,
+ functionName: 'approve',
+ args: [poolAddress, amountInWei],
+ chain,
+ account: walletClient.account,
+ });
+
+ // Wait for approval transaction
+ const receipt = await waitForTransactionReceipt(publicClient, {
+ hash: approveHash,
+ });
+
+ if (receipt.status === 'reverted') {
+ throw new Error('Approval transaction reverted');
+ }
+
+ return { txHash: approveHash };
+}
+
diff --git a/ui/src/3rd-party/earn/aave/claim.ts b/ui/src/3rd-party/earn/aave/claim.ts
new file mode 100644
index 00000000..e73e84a8
--- /dev/null
+++ b/ui/src/3rd-party/earn/aave/claim.ts
@@ -0,0 +1,81 @@
+import { createPublicClient, http } from 'viem';
+import { waitForTransactionReceipt } from 'viem/actions';
+import type { ChainConfig } from '../../../types/chainConfig';
+import type { WalletClient } from 'viem';
+import { POOL_ADDRESSES_PROVIDER } from './contracts';
+import {
+ POOL_ADDRESSES_PROVIDER_FULL_ABI,
+ REWARDS_CONTROLLER_ABI,
+} from './abis';
+
+/**
+ * Claim rewards from AAVE pool
+ */
+export async function claimAaveRewards(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ assets: `0x${string}`[];
+ to?: `0x${string}`;
+}): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, assets, to } = params;
+
+ if (!walletClient.account) {
+ throw new Error('Wallet not connected');
+ }
+
+ const publicClient = createPublicClient({
+ chain,
+ transport: http(),
+ });
+
+ // Get RewardsController address
+ // AAVE v3 uses a RewardsController contract
+ // The address is stored in PoolAddressesProvider with id "REWARDS_CONTROLLER"
+ let rewardsControllerAddress: `0x${string}`;
+
+ try {
+ // Try to get RewardsController address from PoolAddressesProvider
+ const REWARDS_CONTROLLER_ID = '0x703c2c8634bed68d98c029c18f310e7f7ec0e5d6342c590190b3cb8b3ba54532'; // keccak256("REWARDS_CONTROLLER")
+ rewardsControllerAddress = await publicClient.readContract({
+ address: POOL_ADDRESSES_PROVIDER as `0x${string}`,
+ abi: POOL_ADDRESSES_PROVIDER_FULL_ABI,
+ functionName: 'getAddress',
+ args: [REWARDS_CONTROLLER_ID as `0x${string}`],
+ }) as `0x${string}`;
+ } catch (error) {
+ // If RewardsController is not found, try to get it from the pool
+ // For now, we'll use a fallback approach
+ throw new Error('RewardsController not found. Rewards claiming may not be available on this chain.');
+ }
+
+ // Use provided 'to' address or default to wallet address
+ const toAddress = to || walletClient.account.address as `0x${string}`;
+
+ // Get rewards list to determine which reward token to claim
+ // For simplicity, we'll claim all available rewards (amount = type(uint256).max)
+ const maxAmount = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
+
+ // Claim rewards for all provided assets
+ // For AAVE, reward token is typically the native token or a specific reward token
+ // We'll use address(0) to claim all rewards
+ const txHash = await walletClient.writeContract({
+ address: rewardsControllerAddress,
+ abi: REWARDS_CONTROLLER_ABI,
+ functionName: 'claimRewards',
+ args: [assets, maxAmount, toAddress, '0x0000000000000000000000000000000000000000' as `0x${string}`],
+ chain,
+ account: walletClient.account,
+ });
+
+ // Wait for transaction receipt
+ const receipt = await waitForTransactionReceipt(publicClient, {
+ hash: txHash,
+ });
+
+ if (receipt.status === 'reverted') {
+ throw new Error('Transaction reverted');
+ }
+
+ return { txHash };
+}
+
diff --git a/ui/src/3rd-party/earn/aave/contracts.ts b/ui/src/3rd-party/earn/aave/contracts.ts
new file mode 100644
index 00000000..d4f6b6e4
--- /dev/null
+++ b/ui/src/3rd-party/earn/aave/contracts.ts
@@ -0,0 +1,223 @@
+import { createPublicClient, http, formatUnits } from 'viem';
+import type { Avalanche } from '@avalanche-sdk/chainkit';
+import { ERC20_METADATA_ABI } from '../../../utils/erc20';
+import type { ChainConfig } from '../../../types/chainConfig';
+import type { EarnPool } from '../../../earn/types';
+import { POOL_DATA_PROVIDER_ABI } from './abis';
+
+// Contract addresses
+export const POOL_ADDRESSES_PROVIDER = '0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb' as const;
+export const POOL_DATA_PROVIDER = '0x243Aa95cAC2a25651eda86e80bEe66114413c43b' as const;
+
+/**
+ * Get token info from ERC20 contract
+ */
+async function getTokenInfo(
+ publicClient: ReturnType,
+ tokenAddress: `0x${string}`
+): Promise<{ name: string; symbol: string; decimals: number }> {
+ try {
+ const [name, symbol, decimals] = await Promise.all([
+ publicClient.readContract({
+ address: tokenAddress,
+ abi: ERC20_METADATA_ABI,
+ functionName: 'name',
+ }),
+ publicClient.readContract({
+ address: tokenAddress,
+ abi: ERC20_METADATA_ABI,
+ functionName: 'symbol',
+ }),
+ publicClient.readContract({
+ address: tokenAddress,
+ abi: ERC20_METADATA_ABI,
+ functionName: 'decimals',
+ }),
+ ]);
+
+ return {
+ name: name as string,
+ symbol: symbol as string,
+ decimals: Number(decimals),
+ };
+ } catch (error) {
+ console.error(`Error fetching token info for ${tokenAddress}:`, error);
+ // Return defaults if contract call fails
+ return {
+ name: 'Unknown Token',
+ symbol: 'UNKNOWN',
+ decimals: 18,
+ };
+ }
+}
+
+/**
+ * Fetch AAVE pools from contracts
+ */
+export async function fetchAavePools(
+ chain: ChainConfig,
+ chainkit: Avalanche,
+ userAddress?: `0x${string}`
+): Promise {
+ const publicClient = createPublicClient({
+ chain,
+ transport: http(),
+ });
+
+ // Get PoolDataProvider address (use the provided address)
+ const poolDataProviderAddress = POOL_DATA_PROVIDER as `0x${string}`;
+
+ // Get all reserves tokens
+ const reservesTokens = await publicClient.readContract({
+ address: poolDataProviderAddress,
+ abi: POOL_DATA_PROVIDER_ABI,
+ functionName: 'getAllReservesTokens',
+ }) as Array<{ symbol: string; tokenAddress: `0x${string}` }>;
+
+ // Fetch data for each reserve
+ const pools: EarnPool[] = await Promise.all(
+ reservesTokens.map(async (reserve) => {
+ const tokenAddress = reserve.tokenAddress;
+
+ try {
+ // Fetch reserve configuration, token info, aToken supply, and contract metadata in parallel
+ const [reserveConfig, tokenInfo, aTokenSupply, contractMetadataResult] = await Promise.all([
+ publicClient.readContract({
+ address: poolDataProviderAddress,
+ abi: POOL_DATA_PROVIDER_ABI,
+ functionName: 'getReserveConfigurationData',
+ args: [tokenAddress],
+ }),
+ getTokenInfo(publicClient, tokenAddress),
+ publicClient.readContract({
+ address: poolDataProviderAddress,
+ abi: POOL_DATA_PROVIDER_ABI,
+ functionName: 'getATokenTotalSupply',
+ args: [tokenAddress],
+ }),
+ chainkit.data.evm.contracts.getMetadata({
+ address: tokenAddress,
+ chainId: chain.id.toString(),
+ }).catch(() => null), // Gracefully handle errors
+ ]);
+
+ // Parse reserve configuration
+ const configResult = reserveConfig as [
+ bigint, // decimals
+ bigint,
+ bigint,
+ bigint,
+ bigint,
+ boolean,
+ boolean,
+ boolean,
+ boolean, // isActive
+ boolean,
+ ];
+
+ const isActive = configResult[8];
+
+ // Format total supply
+ const totalAToken = aTokenSupply as bigint;
+ const totalSupply = formatUnits(totalAToken, tokenInfo.decimals);
+
+ // Use contract metadata if available, otherwise use token info
+ let finalTokenInfo = tokenInfo;
+ let tokenImage: string | null = null;
+ if (contractMetadataResult) {
+ try {
+ const metadata = await contractMetadataResult;
+ if (metadata && typeof metadata === 'object' && 'contractMetadata' in metadata) {
+ const contractMeta = (metadata as any).contractMetadata;
+ if (contractMeta && typeof contractMeta === 'object') {
+ finalTokenInfo = {
+ name: contractMeta.name || tokenInfo.name,
+ symbol: contractMeta.symbol || tokenInfo.symbol,
+ decimals: tokenInfo.decimals,
+ };
+ }
+ }
+ // Extract logoAsset imageUri if available
+ if (metadata && typeof metadata === 'object' && 'logoAsset' in metadata) {
+ const logoAsset = (metadata as any).logoAsset;
+ if (logoAsset && typeof logoAsset === 'object' && 'imageUri' in logoAsset) {
+ tokenImage = logoAsset.imageUri || null;
+ }
+ }
+ } catch (error) {
+ // Use tokenInfo if metadata fetch fails
+ console.error('Error parsing contract metadata:', error);
+ }
+ }
+
+ // Fetch user data if address provided
+ let userDeposited: string | undefined;
+ let userRewards: string | undefined;
+
+ if (userAddress) {
+ try {
+ const userReserveData = await publicClient.readContract({
+ address: poolDataProviderAddress,
+ abi: POOL_DATA_PROVIDER_ABI,
+ functionName: 'getUserReserveData',
+ args: [tokenAddress, userAddress],
+ }) as readonly [
+ bigint, // currentATokenBalance
+ bigint,
+ bigint,
+ bigint,
+ bigint,
+ bigint,
+ bigint,
+ number, // stableRateLastUpdated (uint40)
+ boolean,
+ ];
+
+ const userBalance = userReserveData[0];
+ if (userBalance > 0n) {
+ userDeposited = formatUnits(userBalance, tokenInfo.decimals);
+ }
+ } catch (error) {
+ console.error(`Error fetching user data for ${tokenAddress}:`, error);
+ }
+ }
+
+ // Get aToken address for pool address
+ const reserveTokensAddresses = await publicClient.readContract({
+ address: poolDataProviderAddress,
+ abi: POOL_DATA_PROVIDER_ABI,
+ functionName: 'getReserveTokensAddresses',
+ args: [tokenAddress],
+ }) as readonly [`0x${string}`, `0x${string}`, `0x${string}`];
+ const aTokenAddress = reserveTokensAddresses[0];
+
+ return {
+ id: `aave-${finalTokenInfo.symbol.toLowerCase()}-${chain.id}`,
+ name: finalTokenInfo.symbol,
+ token: {
+ address: tokenAddress,
+ chainId: chain.id,
+ decimals: finalTokenInfo.decimals,
+ symbol: finalTokenInfo.symbol,
+ name: finalTokenInfo.name,
+ image: tokenImage,
+ },
+ totalSupply,
+ status: isActive ? 'active' : 'inactive',
+ provider: 'aave',
+ poolAddress: aTokenAddress,
+ userDeposited,
+ userRewards,
+ };
+ } catch (error) {
+ console.error(`Error processing reserve ${tokenAddress}:`, error);
+ // Return null for failed reserves, filter them out later
+ return null as unknown as EarnPool;
+ }
+ })
+ );
+
+ // Filter out failed reserves
+ return pools.filter((pool): pool is EarnPool => pool !== null);
+}
+
diff --git a/ui/src/3rd-party/earn/aave/deposit.ts b/ui/src/3rd-party/earn/aave/deposit.ts
new file mode 100644
index 00000000..808a505d
--- /dev/null
+++ b/ui/src/3rd-party/earn/aave/deposit.ts
@@ -0,0 +1,68 @@
+import { createPublicClient, http, parseUnits } from 'viem';
+import { waitForTransactionReceipt } from 'viem/actions';
+import type { ChainConfig } from '../../../types/chainConfig';
+import type { WalletClient } from 'viem';
+import { POOL_ADDRESSES_PROVIDER } from './contracts';
+import { POOL_ADDRESSES_PROVIDER_ABI, POOL_ABI } from './abis';
+
+/**
+ * Deposit tokens to AAVE pool
+ */
+export async function depositToAavePool(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ asset: `0x${string}`;
+ amount: string;
+ decimals: number;
+ onBehalfOf: `0x${string}`;
+ referralCode?: number;
+}): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, asset, amount, decimals, onBehalfOf, referralCode = 0 } = params;
+
+ if (!walletClient.account) {
+ throw new Error('Wallet not connected');
+ }
+
+ const publicClient = createPublicClient({
+ chain,
+ transport: http(),
+ });
+
+ // Get Pool contract address
+ const poolAddress = await publicClient.readContract({
+ address: POOL_ADDRESSES_PROVIDER as `0x${string}`,
+ abi: POOL_ADDRESSES_PROVIDER_ABI,
+ functionName: 'getPool',
+ }) as `0x${string}`;
+
+ // Parse amount to wei/base units
+ const amountInWei = parseUnits(amount, decimals);
+
+ // Check if native AVAX (empty address) - AAVE requires wrapped tokens
+ // For native AVAX, user should deposit WAVAX instead
+ if (!asset || asset === '0x0000000000000000000000000000000000000000') {
+ throw new Error('Native AVAX deposits not supported. Please use WAVAX (Wrapped AVAX) instead.');
+ }
+
+ // Deposit to pool
+ const txHash = await walletClient.writeContract({
+ address: poolAddress,
+ abi: POOL_ABI,
+ functionName: 'supply',
+ args: [asset, amountInWei, onBehalfOf, referralCode],
+ chain,
+ account: walletClient.account,
+ });
+
+ // Wait for transaction receipt
+ const receipt = await waitForTransactionReceipt(publicClient, {
+ hash: txHash,
+ });
+
+ if (receipt.status === 'reverted') {
+ throw new Error('Transaction reverted');
+ }
+
+ return { txHash };
+}
+
diff --git a/ui/src/3rd-party/earn/aave/index.ts b/ui/src/3rd-party/earn/aave/index.ts
new file mode 100644
index 00000000..2445e6da
--- /dev/null
+++ b/ui/src/3rd-party/earn/aave/index.ts
@@ -0,0 +1,8 @@
+export { AaveProvider } from './aaveProvider';
+export * from './abis';
+export * from './contracts';
+export * from './deposit';
+export * from './withdraw';
+export * from './claim';
+export * from './approve';
+
diff --git a/ui/src/3rd-party/earn/aave/withdraw.ts b/ui/src/3rd-party/earn/aave/withdraw.ts
new file mode 100644
index 00000000..a0296994
--- /dev/null
+++ b/ui/src/3rd-party/earn/aave/withdraw.ts
@@ -0,0 +1,69 @@
+import { createPublicClient, http, parseUnits } from 'viem';
+import { waitForTransactionReceipt } from 'viem/actions';
+import type { ChainConfig } from '../../../types/chainConfig';
+import type { WalletClient } from 'viem';
+import { POOL_ADDRESSES_PROVIDER } from './contracts';
+import { POOL_ADDRESSES_PROVIDER_ABI, POOL_ABI } from './abis';
+
+/**
+ * Withdraw tokens from AAVE pool
+ */
+export async function withdrawFromAavePool(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ asset: `0x${string}`;
+ amount: string;
+ decimals: number;
+ to?: `0x${string}`;
+}): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, asset, amount, decimals, to } = params;
+
+ if (!walletClient.account) {
+ throw new Error('Wallet not connected');
+ }
+
+ const publicClient = createPublicClient({
+ chain,
+ transport: http(),
+ });
+
+ // Get Pool contract address
+ const poolAddress = await publicClient.readContract({
+ address: POOL_ADDRESSES_PROVIDER as `0x${string}`,
+ abi: POOL_ADDRESSES_PROVIDER_ABI,
+ functionName: 'getPool',
+ }) as `0x${string}`;
+
+ // Parse amount to wei/base units
+ const amountInWei = parseUnits(amount, decimals);
+
+ // Use provided 'to' address or default to wallet address
+ const toAddress = to || walletClient.account.address as `0x${string}`;
+
+ // Check if native AVAX (empty address) - AAVE uses wrapped tokens
+ if (!asset || asset === '0x0000000000000000000000000000000000000000') {
+ throw new Error('Native AVAX withdrawals not supported. Please use WAVAX (Wrapped AVAX) instead.');
+ }
+
+ // Withdraw from pool
+ const txHash = await walletClient.writeContract({
+ address: poolAddress,
+ abi: POOL_ABI,
+ functionName: 'withdraw',
+ args: [asset, amountInWei, toAddress],
+ chain,
+ account: walletClient.account,
+ });
+
+ // Wait for transaction receipt
+ const receipt = await waitForTransactionReceipt(publicClient, {
+ hash: txHash,
+ });
+
+ if (receipt.status === 'reverted') {
+ throw new Error('Transaction reverted');
+ }
+
+ return { txHash };
+}
+
diff --git a/ui/src/3rd-party/earn/benqi/abis.ts b/ui/src/3rd-party/earn/benqi/abis.ts
new file mode 100644
index 00000000..77cfd3af
--- /dev/null
+++ b/ui/src/3rd-party/earn/benqi/abis.ts
@@ -0,0 +1,170 @@
+import { parseAbi } from 'viem';
+
+/**
+ * Benqi Unitroller (Comptroller) ABI
+ * Used for fetching markets and claiming rewards
+ */
+export const UNITROLLER_ABI = [
+ {
+ constant: true,
+ inputs: [],
+ name: 'getAllMarkets',
+ outputs: [
+ {
+ internalType: 'contract QiToken[]',
+ name: '',
+ type: 'address[]',
+ },
+ ],
+ payable: false,
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ constant: true,
+ inputs: [{ internalType: 'address', name: '', type: 'address' }],
+ name: 'markets',
+ outputs: [
+ { internalType: 'bool', name: 'isListed', type: 'bool' },
+ { internalType: 'uint256', name: 'collateralFactorMantissa', type: 'uint256' },
+ { internalType: 'enum ComptrollerV1Storage.Version', name: 'version', type: 'uint8' },
+ ],
+ payable: false,
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ constant: true,
+ inputs: [{ internalType: 'address', name: '', type: 'address' }],
+ name: 'mintGuardianPaused',
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ payable: false,
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ constant: false,
+ inputs: [
+ { internalType: 'uint8', name: 'rewardType', type: 'uint8' },
+ { internalType: 'address payable', name: 'holder', type: 'address' },
+ { internalType: 'contract QiToken[]', name: 'qiTokens', type: 'address[]' },
+ ],
+ name: 'claimReward',
+ outputs: [],
+ payable: false,
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ constant: false,
+ inputs: [
+ { internalType: 'uint8', name: 'rewardType', type: 'uint8' },
+ { internalType: 'address payable', name: 'holder', type: 'address' },
+ ],
+ name: 'claimReward',
+ outputs: [],
+ payable: false,
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+] as const;
+
+/**
+ * Benqi QiToken ABI (full)
+ * Used for reading token information and balances
+ */
+export const QI_TOKEN_ABI = [
+ {
+ constant: true,
+ inputs: [],
+ name: 'symbol',
+ outputs: [{ internalType: 'string', name: '', type: 'string' }],
+ payable: false,
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ constant: true,
+ inputs: [],
+ name: 'name',
+ outputs: [{ internalType: 'string', name: '', type: 'string' }],
+ payable: false,
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ constant: true,
+ inputs: [],
+ name: 'decimals',
+ outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
+ payable: false,
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ constant: true,
+ inputs: [],
+ name: 'totalSupply',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ payable: false,
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ constant: true,
+ inputs: [],
+ name: 'underlying',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ payable: false,
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ constant: true,
+ inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
+ name: 'balanceOf',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ payable: false,
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ constant: true,
+ inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
+ name: 'balanceOfUnderlying',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ payable: false,
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ constant: true,
+ inputs: [],
+ name: 'exchangeRateStored',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ payable: false,
+ stateMutability: 'view',
+ type: 'function',
+ },
+] as const;
+
+/**
+ * Benqi QiToken ABI for mint (deposit)
+ * Used for depositing tokens
+ */
+export const QI_TOKEN_MINT_ABI = parseAbi([
+ 'function mint(uint256 mintAmount) returns (uint256)',
+ 'function mint() payable',
+ 'function underlying() view returns (address)',
+]);
+
+/**
+ * Benqi QiToken ABI for redeem (withdraw)
+ * Used for withdrawing tokens
+ */
+export const QI_TOKEN_REDEEM_ABI = parseAbi([
+ 'function redeem(uint256 redeemTokens) returns (uint256)',
+ 'function redeemUnderlying(uint256 redeemAmount) returns (uint256)',
+ 'function underlying() view returns (address)',
+]);
+
diff --git a/ui/src/3rd-party/earn/benqi/approve.ts b/ui/src/3rd-party/earn/benqi/approve.ts
new file mode 100644
index 00000000..2d0af5c3
--- /dev/null
+++ b/ui/src/3rd-party/earn/benqi/approve.ts
@@ -0,0 +1,116 @@
+import { createPublicClient, http, parseUnits } from 'viem';
+import { waitForTransactionReceipt } from 'viem/actions';
+import type { ChainConfig } from '../../../types/chainConfig';
+import type { WalletClient } from 'viem';
+import { ERC20_APPROVAL_ABI } from '../../../utils/erc20';
+import { QI_TOKEN_MINT_ABI } from './abis';
+
+/**
+ * Check if token needs approval for Benqi
+ */
+export async function checkTokenApprovalForBenqi(params: {
+ publicClient: ReturnType;
+ qiTokenAddress: `0x${string}`;
+ amount: string;
+ decimals: number;
+ owner: `0x${string}`;
+}): Promise<{ needsApproval: boolean; currentAllowance: bigint; requiredAmount: bigint }> {
+ const { publicClient, qiTokenAddress, amount, decimals, owner } = params;
+
+ // Get underlying token address
+ const underlyingAddress = await publicClient.readContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_MINT_ABI,
+ functionName: 'underlying',
+ }).catch(() => null as `0x${string}` | null) as `0x${string}` | null;
+
+ // Native AVAX doesn't need approval
+ const isNative = !underlyingAddress ||
+ underlyingAddress === '0x0000000000000000000000000000000000000000' ||
+ underlyingAddress === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
+
+ if (isNative) {
+ return {
+ needsApproval: false,
+ currentAllowance: BigInt(0),
+ requiredAmount: BigInt(0),
+ };
+ }
+
+ const requiredAmount = parseUnits(amount, decimals);
+ const currentAllowance = await publicClient.readContract({
+ address: underlyingAddress,
+ abi: ERC20_APPROVAL_ABI,
+ functionName: 'allowance',
+ args: [owner, qiTokenAddress],
+ }) as bigint;
+
+ return {
+ needsApproval: currentAllowance < requiredAmount,
+ currentAllowance,
+ requiredAmount,
+ };
+}
+
+/**
+ * Approve token spending for Benqi pool
+ */
+export async function approveTokenForBenqiPool(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ qiTokenAddress: `0x${string}`;
+ amount: string;
+ decimals: number;
+}): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, qiTokenAddress, amount, decimals } = params;
+
+ if (!walletClient.account) {
+ throw new Error('Wallet not connected');
+ }
+
+ const publicClient = createPublicClient({
+ chain,
+ transport: http(),
+ });
+
+ // Get underlying token address
+ const underlyingAddress = await publicClient.readContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_MINT_ABI,
+ functionName: 'underlying',
+ }).catch(() => null as `0x${string}` | null) as `0x${string}` | null;
+
+ // Native AVAX doesn't need approval
+ const isNative = !underlyingAddress ||
+ underlyingAddress === '0x0000000000000000000000000000000000000000' ||
+ underlyingAddress === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
+
+ if (isNative) {
+ throw new Error('Native AVAX does not require approval');
+ }
+
+ // Parse amount to wei/base units
+ const amountInWei = parseUnits(amount, decimals);
+
+ // Approve the qiToken to spend tokens
+ const approveHash = await walletClient.writeContract({
+ address: underlyingAddress,
+ abi: ERC20_APPROVAL_ABI,
+ functionName: 'approve',
+ args: [qiTokenAddress, amountInWei],
+ chain,
+ account: walletClient.account,
+ });
+
+ // Wait for approval transaction
+ const receipt = await waitForTransactionReceipt(publicClient, {
+ hash: approveHash,
+ });
+
+ if (receipt.status === 'reverted') {
+ throw new Error('Approval transaction reverted');
+ }
+
+ return { txHash: approveHash };
+}
+
diff --git a/ui/src/3rd-party/earn/benqi/benqiProvider.ts b/ui/src/3rd-party/earn/benqi/benqiProvider.ts
new file mode 100644
index 00000000..07fbaf36
--- /dev/null
+++ b/ui/src/3rd-party/earn/benqi/benqiProvider.ts
@@ -0,0 +1,110 @@
+import type { ChainConfig } from '../../../types/chainConfig';
+import type { WalletClient } from 'viem';
+import type { Avalanche } from '@avalanche-sdk/chainkit';
+import type { EarnPool } from '../../../earn/types';
+import type { EarnProviderBase } from '../../../earn/providers/base';
+import { createPublicClient } from 'viem';
+import { fetchBenqiPools } from './contracts';
+import { checkTokenApprovalForBenqi, approveTokenForBenqiPool } from './approve';
+import { depositToBenqiPool } from './deposit';
+import { withdrawFromBenqiPool } from './withdraw';
+import { claimBenqiRewards } from './claim';
+
+/**
+ * Benqi Earn Provider Implementation
+ */
+export class BenqiProvider implements EarnProviderBase {
+ readonly providerId = 'benqi';
+
+ async fetchPools(
+ chain: ChainConfig,
+ chainkit: Avalanche,
+ userAddress?: `0x${string}`
+ ): Promise {
+ return fetchBenqiPools(chain, chainkit, userAddress);
+ }
+
+ async checkApproval(params: {
+ publicClient: ReturnType;
+ pool: EarnPool;
+ amount: string;
+ owner: `0x${string}`;
+ }): Promise<{ needsApproval: boolean; currentAllowance: bigint; requiredAmount: bigint }> {
+ const { publicClient, pool, amount, owner } = params;
+
+ return checkTokenApprovalForBenqi({
+ publicClient,
+ qiTokenAddress: pool.poolAddress as `0x${string}`,
+ amount,
+ decimals: pool.token.decimals,
+ owner,
+ });
+ }
+
+ async approveToken(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ pool: EarnPool;
+ amount: string;
+ }): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, pool, amount } = params;
+
+ return approveTokenForBenqiPool({
+ walletClient,
+ chain,
+ qiTokenAddress: pool.poolAddress as `0x${string}`,
+ amount,
+ decimals: pool.token.decimals,
+ });
+ }
+
+ async deposit(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ pool: EarnPool;
+ amount: string;
+ }): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, pool, amount } = params;
+
+ return depositToBenqiPool({
+ walletClient,
+ chain,
+ qiTokenAddress: pool.poolAddress as `0x${string}`,
+ amount,
+ decimals: pool.token.decimals,
+ });
+ }
+
+ async withdraw(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ pool: EarnPool;
+ amount: string;
+ }): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, pool, amount } = params;
+
+ return withdrawFromBenqiPool({
+ walletClient,
+ chain,
+ qiTokenAddress: pool.poolAddress as `0x${string}`,
+ amount,
+ decimals: pool.token.decimals,
+ });
+ }
+
+ async claimRewards(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ pool: EarnPool;
+ }): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, pool } = params;
+
+ return claimBenqiRewards({
+ walletClient,
+ chain,
+ qiTokenAddresses: [pool.poolAddress as `0x${string}`],
+ rewardType: 0, // 0 for QI token rewards
+ });
+ }
+}
+
diff --git a/ui/src/3rd-party/earn/benqi/claim.ts b/ui/src/3rd-party/earn/benqi/claim.ts
new file mode 100644
index 00000000..fbfe4388
--- /dev/null
+++ b/ui/src/3rd-party/earn/benqi/claim.ts
@@ -0,0 +1,69 @@
+import { createPublicClient, http } from 'viem';
+import { waitForTransactionReceipt } from 'viem/actions';
+import type { ChainConfig } from '../../../types/chainConfig';
+import type { WalletClient } from 'viem';
+import { BENQI_UNITROLLER } from './contracts';
+import { UNITROLLER_ABI } from './abis';
+
+/**
+ * Claim rewards from Benqi pool
+ *
+ * @param rewardType - 0 for QI token rewards, 1 for AVAX rewards (if available)
+ */
+export async function claimBenqiRewards(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ qiTokenAddresses?: `0x${string}`[];
+ rewardType?: number;
+}): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, qiTokenAddresses, rewardType = 0 } = params;
+
+ if (!walletClient.account) {
+ throw new Error('Wallet not connected');
+ }
+
+ const publicClient = createPublicClient({
+ chain,
+ transport: http(),
+ });
+
+ // Use provided qiToken addresses or claim for all markets
+ // If qiTokenAddresses is provided, claim for specific markets
+ // Otherwise, claim for all markets (pass empty array or omit parameter)
+
+ let txHash: `0x${string}`;
+
+ if (qiTokenAddresses && qiTokenAddresses.length > 0) {
+ // Claim rewards for specific qiTokens
+ txHash = await walletClient.writeContract({
+ address: BENQI_UNITROLLER as `0x${string}`,
+ abi: UNITROLLER_ABI,
+ functionName: 'claimReward',
+ args: [rewardType as number, walletClient.account.address as `0x${string}`, qiTokenAddresses],
+ chain,
+ account: walletClient.account,
+ });
+ } else {
+ // Claim rewards for all markets
+ txHash = await walletClient.writeContract({
+ address: BENQI_UNITROLLER as `0x${string}`,
+ abi: UNITROLLER_ABI,
+ functionName: 'claimReward',
+ args: [rewardType as number, walletClient.account.address as `0x${string}`],
+ chain,
+ account: walletClient.account,
+ });
+ }
+
+ // Wait for transaction receipt
+ const receipt = await waitForTransactionReceipt(publicClient, {
+ hash: txHash,
+ });
+
+ if (receipt.status === 'reverted') {
+ throw new Error('Transaction reverted');
+ }
+
+ return { txHash };
+}
+
diff --git a/ui/src/3rd-party/earn/benqi/contracts.ts b/ui/src/3rd-party/earn/benqi/contracts.ts
new file mode 100644
index 00000000..7db3811c
--- /dev/null
+++ b/ui/src/3rd-party/earn/benqi/contracts.ts
@@ -0,0 +1,244 @@
+import { createPublicClient, http, formatUnits } from 'viem';
+import type { Avalanche } from '@avalanche-sdk/chainkit';
+import { ERC20_METADATA_ABI } from '../../../utils/erc20';
+import type { ChainConfig } from '../../../types/chainConfig';
+import type { EarnPool } from '../../../earn/types';
+import { UNITROLLER_ABI, QI_TOKEN_ABI } from './abis';
+
+// Contract addresses
+export const BENQI_UNITROLLER = '0x486Af39519B4Dc9a7fCcd318217352830E8AD9b4' as const;
+
+
+/**
+ * Get token info from ERC20 contract
+ */
+async function getTokenInfo(
+ publicClient: ReturnType,
+ tokenAddress: `0x${string}`
+): Promise<{ name: string; symbol: string; decimals: number }> {
+ try {
+ const [name, symbol, decimals] = await Promise.all([
+ publicClient.readContract({
+ address: tokenAddress,
+ abi: ERC20_METADATA_ABI,
+ functionName: 'symbol',
+ }),
+ publicClient.readContract({
+ address: tokenAddress,
+ abi: ERC20_METADATA_ABI,
+ functionName: 'name',
+ }),
+ publicClient.readContract({
+ address: tokenAddress,
+ abi: ERC20_METADATA_ABI,
+ functionName: 'decimals',
+ }),
+ ]);
+
+ return {
+ name: name as string,
+ symbol: symbol as string,
+ decimals: decimals as number,
+ };
+ } catch (error) {
+ console.error(`Error fetching token info for ${tokenAddress}:`, error);
+ throw error;
+ }
+}
+
+/**
+ * Fetch Benqi pools from contracts
+ */
+export async function fetchBenqiPools(
+ chain: ChainConfig,
+ chainkit: Avalanche,
+ userAddress?: `0x${string}`
+): Promise {
+ const publicClient = createPublicClient({
+ chain,
+ transport: http(),
+ });
+
+ const unitrollerAddress = BENQI_UNITROLLER as `0x${string}`;
+
+ // Get all markets from Unitroller
+ const markets = await publicClient.readContract({
+ address: unitrollerAddress,
+ abi: UNITROLLER_ABI,
+ functionName: 'getAllMarkets',
+ }) as `0x${string}`[];
+
+ // Fetch data for each market
+ const pools: EarnPool[] = await Promise.all(
+ markets.map(async (qiTokenAddress) => {
+ try {
+ // Check if market is listed and not paused
+ const [marketInfo, mintPaused] = await Promise.all([
+ publicClient.readContract({
+ address: unitrollerAddress,
+ abi: UNITROLLER_ABI,
+ functionName: 'markets',
+ args: [qiTokenAddress],
+ }),
+ publicClient.readContract({
+ address: unitrollerAddress,
+ abi: UNITROLLER_ABI,
+ functionName: 'mintGuardianPaused',
+ args: [qiTokenAddress],
+ }).catch(() => false), // If function doesn't exist, assume not paused
+ ]);
+
+ const isListed = (marketInfo as readonly [boolean, bigint, number])[0];
+ const isMintPaused = mintPaused as boolean;
+
+ // Filter out unlisted or paused markets
+ if (!isListed || isMintPaused) {
+ return null as unknown as EarnPool;
+ }
+
+ // Get QiToken info
+ const [qiTokenSymbol, qiTokenName, qiTokenDecimals, underlyingAddress, totalSupply, exchangeRate] = await Promise.all([
+ publicClient.readContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_ABI,
+ functionName: 'symbol',
+ }),
+ publicClient.readContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_ABI,
+ functionName: 'name',
+ }),
+ publicClient.readContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_ABI,
+ functionName: 'decimals',
+ }),
+ publicClient.readContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_ABI,
+ functionName: 'underlying',
+ }).catch(() => null as `0x${string}` | null),
+ publicClient.readContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_ABI,
+ functionName: 'totalSupply',
+ }),
+ publicClient.readContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_ABI,
+ functionName: 'exchangeRateStored',
+ }),
+ ]);
+
+ // Check if underlying exists - if not, treat as native AVAX
+ const isNative = !underlyingAddress ||
+ underlyingAddress === '0x0000000000000000000000000000000000000000' ||
+ underlyingAddress === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; // Common native token address
+
+ // For native tokens, use AVAX info
+ let tokenInfo: { name: string; symbol: string; decimals: number };
+ let underlyingTokenAddress: `0x${string}`;
+
+ if (isNative) {
+ // Native AVAX token
+ tokenInfo = {
+ name: 'Avalanche',
+ symbol: 'AVAX',
+ decimals: 18,
+ };
+ underlyingTokenAddress = '0x0000000000000000000000000000000000000000' as `0x${string}`;
+ } else {
+ // ERC20 token - get underlying token info
+ underlyingTokenAddress = underlyingAddress;
+ tokenInfo = await getTokenInfo(publicClient, underlyingTokenAddress).catch(() => ({
+ name: qiTokenName as string,
+ symbol: qiTokenSymbol as string,
+ decimals: qiTokenDecimals as number,
+ }));
+ }
+
+ // Try to get contract metadata from Glacier (only for ERC20 tokens)
+ let tokenImage: string | null = null;
+ if (!isNative) {
+ try {
+ const contractMetadata = await chainkit.data.evm.contracts.getMetadata({
+ address: underlyingTokenAddress,
+ chainId: chain.id.toString(),
+ });
+
+ if (contractMetadata && typeof contractMetadata === 'object' && 'contractMetadata' in contractMetadata) {
+ const contractMeta = (contractMetadata as any).contractMetadata;
+ if (contractMeta && typeof contractMeta === 'object') {
+ tokenInfo = {
+ name: contractMeta.name || tokenInfo.name,
+ symbol: contractMeta.symbol || tokenInfo.symbol,
+ decimals: tokenInfo.decimals,
+ };
+ }
+ }
+
+ // Extract logoAsset imageUri if available
+ if (contractMetadata && typeof contractMetadata === 'object' && 'logoAsset' in contractMetadata) {
+ const logoAsset = (contractMetadata as any).logoAsset;
+ if (logoAsset && typeof logoAsset === 'object' && 'imageUri' in logoAsset) {
+ tokenImage = logoAsset.imageUri || null;
+ }
+ }
+ } catch (error) {
+ console.error('Error fetching contract metadata:', error);
+ }
+ }
+
+ // Calculate total supply in underlying tokens
+ // totalSupply (qiToken) * exchangeRate / 1e18 = underlying amount
+ const exchangeRateValue = exchangeRate as bigint;
+ const totalSupplyValue = totalSupply as bigint;
+ const totalUnderlying = (totalSupplyValue * exchangeRateValue) / BigInt(10 ** 18);
+ const totalSupplyFormatted = formatUnits(totalUnderlying, tokenInfo.decimals);
+
+ // Fetch user data if address provided
+ let userDeposited: string | undefined;
+ if (userAddress) {
+ try {
+ const userBalanceUnderlying = await publicClient.readContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_ABI,
+ functionName: 'balanceOfUnderlying',
+ args: [userAddress],
+ }) as bigint;
+
+ if (userBalanceUnderlying > 0n) {
+ userDeposited = formatUnits(userBalanceUnderlying, tokenInfo.decimals);
+ }
+ } catch (error) {
+ console.error(`Error fetching user data for ${qiTokenAddress}:`, error);
+ }
+ }
+
+ return {
+ id: `benqi-${tokenInfo.symbol.toLowerCase()}-${chain.id}`,
+ name: tokenInfo.symbol,
+ token: {
+ address: underlyingTokenAddress as `0x${string}`,
+ chainId: chain.id,
+ decimals: tokenInfo.decimals,
+ symbol: tokenInfo.symbol,
+ name: tokenInfo.name,
+ image: tokenImage,
+ },
+ totalSupply: totalSupplyFormatted,
+ status: 'active',
+ provider: 'benqi',
+ poolAddress: qiTokenAddress,
+ userDeposited,
+ };
+ } catch (error) {
+ console.error(`Error processing market ${qiTokenAddress}:`, error);
+ return null as unknown as EarnPool;
+ }
+ })
+ );
+
+ return pools.filter((pool): pool is EarnPool => pool !== null);
+}
+
diff --git a/ui/src/3rd-party/earn/benqi/deposit.ts b/ui/src/3rd-party/earn/benqi/deposit.ts
new file mode 100644
index 00000000..16dc252b
--- /dev/null
+++ b/ui/src/3rd-party/earn/benqi/deposit.ts
@@ -0,0 +1,113 @@
+import { createPublicClient, http, parseUnits } from 'viem';
+import { waitForTransactionReceipt } from 'viem/actions';
+import type { ChainConfig } from '../../../types/chainConfig';
+import type { WalletClient } from 'viem';
+import { ERC20_APPROVAL_ABI } from '../../../utils/erc20';
+import { QI_TOKEN_MINT_ABI } from './abis';
+
+/**
+ * Deposit tokens to Benqi pool
+ */
+export async function depositToBenqiPool(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ qiTokenAddress: `0x${string}`;
+ amount: string;
+ decimals: number;
+}): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, qiTokenAddress, amount, decimals } = params;
+
+ if (!walletClient.account) {
+ throw new Error('Wallet not connected');
+ }
+
+ const publicClient = createPublicClient({
+ chain,
+ transport: http(),
+ });
+
+ // Get underlying token address
+ const underlyingAddress = await publicClient.readContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_MINT_ABI,
+ functionName: 'underlying',
+ }).catch(() => null as `0x${string}` | null) as `0x${string}` | null;
+
+ // Parse amount to wei/base units
+ const amountInWei = parseUnits(amount, decimals);
+
+ // Check if native AVAX (no underlying or empty address) - Benqi supports native AVAX via payable mint
+ const isNative = !underlyingAddress ||
+ underlyingAddress === '0x0000000000000000000000000000000000000000' ||
+ underlyingAddress === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
+
+ if (!isNative) {
+ // ERC20 token - check and approve if needed
+ const currentAllowance = await publicClient.readContract({
+ address: underlyingAddress,
+ abi: ERC20_APPROVAL_ABI,
+ functionName: 'allowance',
+ args: [walletClient.account.address, qiTokenAddress],
+ }) as bigint;
+
+ if (currentAllowance < amountInWei) {
+ // Approve the qiToken to spend tokens
+ const approveHash = await walletClient.writeContract({
+ address: underlyingAddress,
+ abi: ERC20_APPROVAL_ABI,
+ functionName: 'approve',
+ args: [qiTokenAddress, amountInWei],
+ chain,
+ account: walletClient.account,
+ });
+
+ // Wait for approval transaction
+ await waitForTransactionReceipt(publicClient, {
+ hash: approveHash,
+ });
+ }
+
+ // Deposit ERC20 tokens using mint(uint256)
+ const txHash = await walletClient.writeContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_MINT_ABI,
+ functionName: 'mint',
+ args: [amountInWei],
+ chain,
+ account: walletClient.account,
+ });
+
+ // Wait for transaction receipt
+ const receipt = await waitForTransactionReceipt(publicClient, {
+ hash: txHash,
+ });
+
+ if (receipt.status === 'reverted') {
+ throw new Error('Transaction reverted');
+ }
+
+ return { txHash };
+ }
+
+ // Deposit native AVAX using payable mint()
+ const txHash = await walletClient.writeContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_MINT_ABI,
+ functionName: 'mint',
+ chain,
+ account: walletClient.account,
+ value: amountInWei,
+ });
+
+ // Wait for transaction receipt
+ const receipt = await waitForTransactionReceipt(publicClient, {
+ hash: txHash,
+ });
+
+ if (receipt.status === 'reverted') {
+ throw new Error('Transaction reverted');
+ }
+
+ return { txHash };
+}
+
diff --git a/ui/src/3rd-party/earn/benqi/index.ts b/ui/src/3rd-party/earn/benqi/index.ts
new file mode 100644
index 00000000..3984f70b
--- /dev/null
+++ b/ui/src/3rd-party/earn/benqi/index.ts
@@ -0,0 +1,8 @@
+export { BenqiProvider } from './benqiProvider';
+export * from './abis';
+export * from './contracts';
+export * from './deposit';
+export * from './withdraw';
+export * from './claim';
+export * from './approve';
+
diff --git a/ui/src/3rd-party/earn/benqi/withdraw.ts b/ui/src/3rd-party/earn/benqi/withdraw.ts
new file mode 100644
index 00000000..b47d38aa
--- /dev/null
+++ b/ui/src/3rd-party/earn/benqi/withdraw.ts
@@ -0,0 +1,53 @@
+import { createPublicClient, http, parseUnits } from 'viem';
+import { waitForTransactionReceipt } from 'viem/actions';
+import type { ChainConfig } from '../../../types/chainConfig';
+import type { WalletClient } from 'viem';
+import { QI_TOKEN_REDEEM_ABI } from './abis';
+
+/**
+ * Withdraw tokens from Benqi pool
+ */
+export async function withdrawFromBenqiPool(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ qiTokenAddress: `0x${string}`;
+ amount: string;
+ decimals: number;
+}): Promise<{ txHash: `0x${string}` }> {
+ const { walletClient, chain, qiTokenAddress, amount, decimals } = params;
+
+ if (!walletClient.account) {
+ throw new Error('Wallet not connected');
+ }
+
+ const publicClient = createPublicClient({
+ chain,
+ transport: http(),
+ });
+
+ // Parse amount to wei/base units
+ const amountInWei = parseUnits(amount, decimals);
+
+ // Use redeemUnderlying to withdraw the exact amount of underlying tokens
+ // This is more user-friendly than calculating the exact qiToken amount
+ const txHash = await walletClient.writeContract({
+ address: qiTokenAddress,
+ abi: QI_TOKEN_REDEEM_ABI,
+ functionName: 'redeemUnderlying',
+ args: [amountInWei],
+ chain,
+ account: walletClient.account,
+ });
+
+ // Wait for transaction receipt
+ const receipt = await waitForTransactionReceipt(publicClient, {
+ hash: txHash,
+ });
+
+ if (receipt.status === 'reverted') {
+ throw new Error('Transaction reverted');
+ }
+
+ return { txHash };
+}
+
diff --git a/ui/src/AvalancheProvider.tsx b/ui/src/AvalancheProvider.tsx
new file mode 100644
index 00000000..028da679
--- /dev/null
+++ b/ui/src/AvalancheProvider.tsx
@@ -0,0 +1,430 @@
+'use client';
+import { createContext, useContext, useLayoutEffect, useMemo, useState, useEffect, useCallback, type ReactNode } from 'react';
+import { useSessionStorage } from 'usehooks-ts';
+import { createAvalancheWalletClient } from '@avalanche-sdk/client';
+import type { Chain } from '@avalanche-sdk/client/chains';
+import { avalanche, avalancheFuji } from '@avalanche-sdk/client/chains';
+import { Avalanche } from '@avalanche-sdk/chainkit';
+import type { ICTTToken } from './ictt/types';
+import type { ChainConfig } from './types/chainConfig';
+
+export type AvalancheConfig = {
+ /** The name of the application */
+ name?: string;
+ /** The logo URL for the application */
+ logo?: string;
+ /** The theme mode */
+ mode?: 'auto' | 'light' | 'dark';
+ /** The theme variant */
+ theme?: 'default' | 'custom';
+ /** RPC URL override */
+ rpcUrl?: string;
+ /** API key for enhanced features */
+ apiKey?: string;
+};
+
+export type AvalancheProviderProps = {
+ /** The blockchain network chain */
+ chain: ChainConfig;
+ /** Available chains for network switching */
+ chains?: ChainConfig[];
+ /** Well-known tokens for ICTT */
+ wellKnownTokens?: ICTTToken[];
+ /** Configuration options */
+ config?: AvalancheConfig;
+ /** Child components */
+ children: ReactNode;
+ /** Session ID for analytics */
+ sessionId?: string;
+};
+
+export type AvalancheContextType = {
+ /** Current blockchain chain */
+ chain: Chain;
+ /** Application configuration */
+ config: Required;
+ /** Session identifier */
+ sessionId: string;
+ /** ChainKit SDK client for Glacier API */
+ chainkit: Avalanche;
+ /** Wallet client for transactions (null if no wallet) */
+ walletClient: ReturnType | null;
+ /** Current wallet address */
+ walletAddress: string | null;
+ /** Current chain ID from wallet */
+ walletChainId: number | null;
+ /** Whether wallet is connected */
+ isWalletConnected: boolean;
+ /** Available chains for network switching */
+ availableChains: ChainConfig[];
+ /** Well-known tokens for ICTT */
+ wellKnownTokens: ICTTToken[];
+ /** Connect wallet function */
+ connectWallet: () => Promise;
+ /** Disconnect wallet function */
+ disconnectWallet: () => void;
+ /** Switch chain function */
+ switchChain: (targetChain: Chain) => Promise;
+};
+
+const AvalancheContext = createContext(null);
+
+function generateSessionId(): string {
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+}
+
+function useTheme(mode: AvalancheConfig['mode'] = 'auto') {
+ return useMemo(() => {
+ if (mode === 'light') return 'light';
+ if (mode === 'dark') return 'dark';
+
+ // Auto mode - default to dark theme to match Builder Console
+ if (typeof window !== 'undefined') {
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
+ }
+
+ return 'dark'; // Default to dark theme
+ }, [mode]);
+}
+
+/**
+ * Provides the Avalanche context to the app.
+ * This is the main provider that should wrap your entire application.
+ */
+export function AvalancheProvider({
+ chain: initialChain,
+ chains,
+ wellKnownTokens = [],
+ config = {},
+ children,
+ sessionId: providedSessionId,
+}: AvalancheProviderProps) {
+ const [sessionId] = useSessionStorage(
+ 'session-id',
+ providedSessionId || generateSessionId(),
+ );
+
+ // Wallet state
+ const [walletAddress, setWalletAddress] = useState(null);
+ const [walletChainId, setWalletChainId] = useState(null);
+ const [currentChain, setCurrentChain] = useState(null);
+ const [walletClient, setWalletClient] = useState | null>(null);
+ const [isInitializing, setIsInitializing] = useState(true);
+
+ const theme = useTheme(config.mode);
+
+ useLayoutEffect(() => {
+ document.documentElement.setAttribute('data-theme', theme);
+ // Also set the class for compatibility
+ if (theme === 'dark') {
+ document.documentElement.classList.add('dark');
+ document.documentElement.classList.remove('light');
+ } else {
+ document.documentElement.classList.add('light');
+ document.documentElement.classList.remove('dark');
+ }
+ }, [theme]);
+
+ // Create ChainKit SDK client (only when chain is available)
+ const chainkit = useMemo(() => {
+ if (!currentChain) return null;
+ return new Avalanche({
+ chainId: currentChain.id.toString(),
+ ...(config.apiKey && { apiKey: config.apiKey }),
+ });
+ }, [currentChain, config.apiKey]);
+
+ // Create wallet client when address is available
+ const createWalletClient = useCallback(async (address: string, targetChain?: Chain) => {
+ if (typeof window === 'undefined') return null;
+
+ const provider = (window as any).avalanche;
+ if (!provider) return null;
+
+ const chainToUse = targetChain || currentChain || initialChain;
+
+ try {
+ const client = createAvalancheWalletClient({
+ chain: chainToUse,
+ transport: { type: 'custom', provider },
+ account: address as `0x${string}`,
+ } as any);
+ return client;
+ } catch (error) {
+ console.error('Failed to create wallet client:', error);
+ return null;
+ }
+ }, [currentChain, initialChain]);
+
+ // Handle account changes
+ const handleAccountsChanged = useCallback(async (accounts: string[]) => {
+ if (!accounts || accounts.length === 0) {
+ // Wallet disconnected
+ setWalletAddress(null);
+ setWalletClient(null);
+ return;
+ }
+
+ const account = accounts[0];
+ setWalletAddress(account);
+
+ // Create new wallet client with current account
+ const chainToUse = currentChain || initialChain;
+ const client = await createWalletClient(account, chainToUse);
+ setWalletClient(client);
+ }, [createWalletClient, currentChain, initialChain]);
+
+ // Handle chain changes
+ const handleChainChanged = useCallback(async (chainId: string | number) => {
+ const numericId = typeof chainId === 'string' ? parseInt(chainId, 16) : chainId;
+ setWalletChainId(numericId);
+
+ // Update current chain based on chain ID - check all available chains
+ const newChain = chains?.find(chain => chain.id === numericId) ||
+ (numericId === avalanche.id ? avalanche : avalancheFuji);
+ setCurrentChain(newChain);
+ setIsInitializing(false);
+
+ // Recreate wallet client with new chain if wallet is connected
+ if (walletAddress) {
+ const client = await createWalletClient(walletAddress, newChain);
+ setWalletClient(client);
+ }
+ }, [walletAddress, createWalletClient, chains]);
+
+ // Connect wallet function
+ const connectWallet = useCallback(async () => {
+ if (typeof window === 'undefined') {
+ throw new Error('Wallet connection is only available in browser environment');
+ }
+
+ const provider = (window as any).avalanche;
+ if (!provider) {
+ throw new Error('Avalanche wallet not found. Please install Core wallet.');
+ }
+
+ try {
+ // Request account access
+ const accounts = await provider.request({ method: 'eth_requestAccounts' });
+ if (accounts && accounts.length > 0) {
+ await handleAccountsChanged(accounts);
+
+ // Get current chain ID
+ const chainId = await provider.request({ method: 'eth_chainId' });
+ if (chainId) {
+ await handleChainChanged(chainId);
+ }
+ }
+ } catch (error: any) {
+ throw new Error(`Failed to connect wallet: ${error.message}`);
+ }
+ }, [handleAccountsChanged, handleChainChanged]);
+
+ // Disconnect wallet function
+ const disconnectWallet = useCallback(() => {
+ setWalletAddress(null);
+ setWalletClient(null);
+ setWalletChainId(null);
+ }, []);
+
+ // Switch chain function
+ const switchChain = useCallback(async (targetChain: Chain) => {
+ if (typeof window === 'undefined') {
+ throw new Error('Chain switching is only available in browser environment');
+ }
+
+ const provider = (window as any).avalanche;
+ if (!provider) {
+ throw new Error('Avalanche wallet not found. Please install Core wallet.');
+ }
+
+ // Find matching ChainConfig from availableChains if available
+ // ChainConfig extends Chain, so this works for both types
+ const chainToUse: Chain = chains?.find(c => c.id === targetChain.id) || targetChain;
+
+ try {
+ // Request to switch to the target chain (only need id for switching)
+ await provider.request({
+ method: 'wallet_switchEthereumChain',
+ params: [{ chainId: `0x${targetChain.id.toString(16)}` }],
+ });
+
+ // Update current chain
+ setCurrentChain(chainToUse);
+ setWalletChainId(targetChain.id);
+
+ // Recreate wallet client with new chain
+ if (walletAddress) {
+ const client = await createWalletClient(walletAddress, chainToUse);
+ setWalletClient(client);
+ }
+ } catch (switchError: any) {
+ // If the chain hasn't been added to the wallet, add it
+ if (switchError.code === 4902) {
+ try {
+ await provider.request({
+ method: 'wallet_addEthereumChain',
+ params: [ chainToUse ],
+ });
+
+ // Update current chain after adding
+ setCurrentChain(chainToUse);
+ setWalletChainId(chainToUse.id);
+
+ // Recreate wallet client with new chain
+ if (walletAddress) {
+ const client = await createWalletClient(walletAddress, chainToUse);
+ setWalletClient(client);
+ }
+ } catch (addError: any) {
+ throw new Error(`Failed to add chain: ${addError.message}`);
+ }
+ } else {
+ throw new Error(`Failed to switch chain: ${switchError.message}`);
+ }
+ }
+ }, [walletAddress, createWalletClient, chains]);
+
+ // Set up wallet event listeners and initialize chain
+ useEffect(() => {
+ if (typeof window === 'undefined') {
+ // No wallet available, use initial chain
+ setCurrentChain(initialChain);
+ setIsInitializing(false);
+ return;
+ }
+
+ const provider = (window as any).avalanche;
+ if (!provider || !provider.on) {
+ // No wallet provider, use initial chain
+ setCurrentChain(initialChain);
+ setIsInitializing(false);
+ return;
+ }
+
+ // Add event listeners
+ provider.on('accountsChanged', handleAccountsChanged);
+ provider.on('chainChanged', handleChainChanged);
+
+ // Check if already connected and get actual chain
+ const checkConnection = async () => {
+ try {
+ // Always get the chain ID first, even if wallet is not connected
+ const chainId = await provider.request({ method: 'eth_chainId' });
+ if (chainId) {
+ await handleChainChanged(chainId);
+ } else {
+ // No chain ID available, use initial chain
+ setCurrentChain(initialChain);
+ }
+
+ // Then check accounts
+ const accounts = await provider.request({ method: 'eth_accounts' });
+ if (accounts && accounts.length > 0) {
+ await handleAccountsChanged(accounts);
+ }
+ } catch (error) {
+ console.error('Failed to check wallet connection:', error);
+ // On error, fall back to initial chain
+ setCurrentChain(initialChain);
+ } finally {
+ setIsInitializing(false);
+ }
+ };
+
+ checkConnection();
+
+ // Cleanup event listeners
+ return () => {
+ if (provider.removeListener) {
+ provider.removeListener('accountsChanged', handleAccountsChanged);
+ provider.removeListener('chainChanged', handleChainChanged);
+ }
+ };
+ }, [handleAccountsChanged, handleChainChanged, initialChain]);
+
+ const contextValue = useMemo(() => {
+ const avalancheConfig: Required = {
+ name: config.name || 'Avalanche App',
+ logo: config.logo || '',
+ mode: config.mode || 'auto',
+ theme: config.theme || 'default',
+ rpcUrl: config.rpcUrl || '',
+ apiKey: config.apiKey || '',
+ };
+
+ // Don't return context until we have the actual chain (unless no wallet available)
+ if (isInitializing && typeof window !== 'undefined' && (window as any).avalanche) {
+ // Still initializing, return null chain to prevent wrong data
+ return null;
+ }
+
+ // Use currentChain if available, otherwise fall back to initialChain
+ const activeChain = currentChain || initialChain;
+
+ return {
+ chain: activeChain,
+ config: avalancheConfig,
+ sessionId,
+ chainkit: chainkit || new Avalanche({
+ chainId: activeChain.id.toString(),
+ ...(config.apiKey && { apiKey: config.apiKey }),
+ }),
+ walletClient,
+ walletAddress,
+ walletChainId,
+ isWalletConnected: !!walletAddress,
+ availableChains: chains as ChainConfig[] || [],
+ wellKnownTokens,
+ connectWallet,
+ disconnectWallet,
+ switchChain,
+ };
+ }, [
+ currentChain,
+ initialChain,
+ isInitializing,
+ chains,
+ wellKnownTokens,
+ config,
+ sessionId,
+ chainkit,
+ walletClient,
+ walletAddress,
+ walletChainId,
+ connectWallet,
+ disconnectWallet,
+ switchChain,
+ ]);
+
+ // Don't render children until we have the chain (unless no wallet available)
+ if (contextValue === null) {
+ return null; // or a loading spinner
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to access the Avalanche context.
+ * Must be used within an AvalancheProvider.
+ */
+export function useAvalanche() {
+ const context = useContext(AvalancheContext);
+ if (!context) {
+ throw new Error('useAvalanche must be used within an AvalancheProvider');
+ }
+ return context;
+}
+
+/**
+ * Hook to get available chains for network switching.
+ */
+export function useAvailableChains() {
+ const { availableChains } = useAvalanche();
+ return availableChains;
+}
diff --git a/ui/src/assets/avalanche-logo.svg b/ui/src/assets/avalanche-logo.svg
new file mode 100644
index 00000000..ec0b6742
--- /dev/null
+++ b/ui/src/assets/avalanche-logo.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/chain/components/ChainLogo.tsx b/ui/src/chain/components/ChainLogo.tsx
new file mode 100644
index 00000000..446dcf8a
--- /dev/null
+++ b/ui/src/chain/components/ChainLogo.tsx
@@ -0,0 +1,89 @@
+'use client';
+
+import { useState } from 'react';
+import { cn } from '../../styles/theme';
+import { Badge } from '../../components/ui/badge';
+import type { ChainLogoProps } from '../types';
+import { normalizeChain } from '../utils/normalizeChain';
+import { getChainColor, getChainGradient } from '../utils/getChainColor';
+
+export function ChainLogo({
+ chain,
+ size = 40,
+ className,
+ showLabel = true,
+ labelClassName,
+ badge,
+}: ChainLogoProps) {
+ const normalized = normalizeChain(chain);
+ const [fallback, setFallback] = useState(false);
+
+ const numericSize =
+ typeof size === 'number'
+ ? size
+ : Number.parseInt((size as string) || '40', 10) || 40;
+ const diameter = typeof size === 'number' ? `${size}px` : size;
+ const badgeSize = Math.max(14, Math.round(numericSize * 0.28));
+ const logoLabel = normalized.label ?? normalized.name?.charAt(0)?.toUpperCase() ?? '?';
+ const displayBadge = badge ?? normalized.badge ?? null;
+ const gradientBackground = getChainGradient(normalized.id);
+ const solidBackground = getChainColor(normalized.id);
+
+ const shouldShowImage = normalized.iconUrl && !fallback;
+
+ return (
+
+
+ {shouldShowImage ? (
+
setFallback(true)}
+ />
+ ) : (
+ showLabel && (
+
+ {logoLabel}
+
+ )
+ )}
+
+
+ {displayBadge ? (
+
+ {displayBadge}
+
+ ) : null}
+
+ );
+}
diff --git a/ui/src/chain/components/ChainRow.tsx b/ui/src/chain/components/ChainRow.tsx
new file mode 100644
index 00000000..754ca467
--- /dev/null
+++ b/ui/src/chain/components/ChainRow.tsx
@@ -0,0 +1,59 @@
+'use client';
+
+import { cn } from '../../styles/theme';
+import type { ChainRowProps } from '../types';
+import { ChainLogo } from './ChainLogo';
+
+export function ChainRow({
+ chain,
+ className,
+ iconSize = 32,
+ showDescription = true,
+ descriptionOverride,
+ disabled = false,
+}: ChainRowProps) {
+ const icon = (() => {
+ if (chain.icon) {
+ return chain.icon;
+ }
+ return (
+
+ );
+ })();
+
+ const fallbackDescription = chain.description ?? (chain.testnet ? 'Testnet' : undefined);
+ const resolvedDescription = descriptionOverride ?? fallbackDescription;
+
+ return (
+
+
+ {icon}
+
+
+
+ {chain.name}
+
+ {showDescription && resolvedDescription ? (
+
+ {resolvedDescription}
+
+ ) : null}
+
+
+ );
+}
diff --git a/ui/src/chain/components/ChainSelectDropdown.tsx b/ui/src/chain/components/ChainSelectDropdown.tsx
new file mode 100644
index 00000000..155eb707
--- /dev/null
+++ b/ui/src/chain/components/ChainSelectDropdown.tsx
@@ -0,0 +1,129 @@
+'use client';
+
+import { useMemo } from 'react';
+import { Select, SelectContent, SelectItem, SelectTrigger } from '../../components/ui/select';
+import { cn, text } from '../../styles/theme';
+import type { ChainOption, ChainSelectDropdownProps } from '../types';
+import { ChainRow } from './ChainRow';
+
+const toValue = (id: ChainOption['id']): string => id.toString();
+
+export function ChainSelectDropdown({
+ options,
+ value,
+ onValueChange,
+ onSelect,
+ placeholder = 'Select chain',
+ disabled,
+ disabledOptions,
+ label,
+ className,
+ triggerClassName,
+ contentClassName,
+ emptyStateLabel = 'Select Chain',
+}: ChainSelectDropdownProps) {
+ const disabledSet = useMemo(() => {
+ if (!disabledOptions?.length) {
+ return new Set();
+ }
+ const values = disabledOptions
+ .filter((option): option is ChainOption['id'] => option !== undefined && option !== null)
+ .map((option) => option.toString());
+ return new Set(values);
+ }, [disabledOptions]);
+
+ const selectedValue = value !== undefined ? value.toString() : undefined;
+ const selectedChain = selectedValue
+ ? options.find((option) => toValue(option.id) === selectedValue)
+ : undefined;
+
+ const handleValueChange = (selected: string) => {
+ const chain = options.find((option) => toValue(option.id) === selected);
+ if (!chain) {
+ return;
+ }
+ onValueChange?.(selected, chain);
+ onSelect?.(chain);
+ };
+
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+
+
+ {selectedChain ? (
+
+ ) : (
+ {placeholder}
+ )}
+
+
+
+ {options.length ? (
+
+ {emptyStateLabel && (
+
+
+ {emptyStateLabel}
+
+
+ )}
+
+ {options.map((option) => {
+ const valueString = toValue(option.id);
+ const isDisabledBySet = disabledSet.has(valueString);
+ const isOptionDisabled = option.disabled || isDisabledBySet;
+ const disabledReason = option.disabledReason ?? (isDisabledBySet ? 'Same as source' : undefined);
+ const descriptionOverride = disabledReason
+ ? `${option.description ?? ''}${option.description ? ' • ' : ''}${disabledReason}`
+ : undefined;
+
+ return (
+
+
+
+ );
+ })}
+
+
+ ) : (
+
+ No chains available
+
+ )}
+
+
+
+ );
+}
diff --git a/ui/src/chain/index.ts b/ui/src/chain/index.ts
new file mode 100644
index 00000000..c152def6
--- /dev/null
+++ b/ui/src/chain/index.ts
@@ -0,0 +1,19 @@
+// Chain Module
+// Components
+export { ChainLogo } from './components/ChainLogo';
+export { ChainRow } from './components/ChainRow';
+export { ChainSelectDropdown } from './components/ChainSelectDropdown';
+
+// Utils
+export { getChainColor, getChainGradient } from './utils/getChainColor';
+export { normalizeChain } from './utils/normalizeChain';
+
+// Types
+export type {
+ Chain,
+ ChainIdentifier,
+ ChainLogoProps,
+ ChainOption,
+ ChainRowProps,
+ ChainSelectDropdownProps,
+} from './types';
diff --git a/ui/src/chain/types.ts b/ui/src/chain/types.ts
new file mode 100644
index 00000000..3e17a7f6
--- /dev/null
+++ b/ui/src/chain/types.ts
@@ -0,0 +1,62 @@
+import type { ReactNode } from 'react';
+
+export type ChainIdentifier = string | number;
+
+export type Chain = {
+ id: ChainIdentifier;
+ name: string;
+ /** Optional short label used for initials or fallbacks */
+ label?: string;
+ /** Optional url pointing to a chain icon */
+ iconUrl?: string | null;
+ /** Optional accent color (CSS color string) */
+ color?: string | null;
+ /** Optional description displayed in dropdowns */
+ description?: string | null;
+ /** Optional badge text displayed on top of the logo */
+ badge?: string | null;
+ /** Flag indicating whether the chain is a testnet */
+ testnet?: boolean;
+};
+
+export type ChainOption = Chain & {
+ /** Optional custom icon to render for this chain */
+ icon?: ReactNode;
+ /** Disable the option */
+ disabled?: boolean;
+ /** Optional reason shown when the option is disabled */
+ disabledReason?: string;
+};
+
+export type ChainLogoProps = {
+ chain: Chain | ChainIdentifier;
+ size?: number | string;
+ className?: string;
+ showLabel?: boolean;
+ labelClassName?: string;
+ badge?: string | null;
+};
+
+export type ChainRowProps = {
+ chain: ChainOption;
+ className?: string;
+ iconSize?: number;
+ showDescription?: boolean;
+ descriptionOverride?: string | null;
+ disabled?: boolean;
+};
+
+export type ChainSelectDropdownProps = {
+ options: ChainOption[];
+ value?: ChainIdentifier;
+ onValueChange?: (value: string, chain: ChainOption) => void;
+ onSelect?: (chain: ChainOption) => void;
+ placeholder?: string;
+ disabled?: boolean;
+ disabledOptions?: ChainIdentifier[];
+ label?: string;
+ className?: string;
+ triggerClassName?: string;
+ contentClassName?: string;
+ emptyStateLabel?: string;
+};
diff --git a/ui/src/chain/utils/getChainColor.ts b/ui/src/chain/utils/getChainColor.ts
new file mode 100644
index 00000000..5515b0a7
--- /dev/null
+++ b/ui/src/chain/utils/getChainColor.ts
@@ -0,0 +1,30 @@
+import type { ChainIdentifier } from '../types';
+
+const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
+
+const hashString = (value: string): number => {
+ let hash = 0;
+ for (let i = 0; i < value.length; i += 1) {
+ hash = (hash << 5) - hash + value.charCodeAt(i);
+ hash |= 0; // Convert to 32bit integer
+ }
+ return Math.abs(hash);
+};
+
+/**
+ * Generates an HSL color string for the chain icon fallback.
+ */
+export function getChainColor(id: ChainIdentifier): string {
+ const seed = hashString(id.toString());
+ const hue = seed % 360;
+ const saturation = 60 + (seed % 20); // 60-79%
+ const lightness = 45 + (seed % 10); // 45-54%
+ return `hsl(${hue}, ${clamp(saturation, 40, 80)}%, ${clamp(lightness, 40, 65)}%)`;
+}
+
+export function getChainGradient(id: ChainIdentifier): string {
+ const seed = hashString(`${id}-gradient`);
+ const hue = seed % 360;
+ const nextHue = (hue + 20) % 360;
+ return `linear-gradient(135deg, hsl(${hue}, 70%, 52%), hsl(${nextHue}, 70%, 58%))`;
+}
diff --git a/ui/src/chain/utils/normalizeChain.ts b/ui/src/chain/utils/normalizeChain.ts
new file mode 100644
index 00000000..3f36ceee
--- /dev/null
+++ b/ui/src/chain/utils/normalizeChain.ts
@@ -0,0 +1,30 @@
+import type { Chain, ChainIdentifier } from '../types';
+
+const computeLabel = (option: { name?: string | null; id: ChainIdentifier }): string => {
+ const name = option.name ?? option.id?.toString() ?? '';
+ if (!name) {
+ return '';
+ }
+ if (name.length <= 3) {
+ return name.toUpperCase();
+ }
+ return name.charAt(0).toUpperCase();
+};
+
+export function normalizeChain(input: Chain | ChainIdentifier): Chain {
+ if (typeof input === 'string' || typeof input === 'number') {
+ const id = input;
+ const name = input.toString();
+ const label = computeLabel({ id, name });
+ return {
+ id,
+ name,
+ label,
+ };
+ }
+
+ return {
+ ...input,
+ label: input.label ?? computeLabel({ id: input.id, name: input.name }),
+ };
+}
diff --git a/ui/src/components/ui/address-input.tsx b/ui/src/components/ui/address-input.tsx
new file mode 100644
index 00000000..255b87e8
--- /dev/null
+++ b/ui/src/components/ui/address-input.tsx
@@ -0,0 +1,156 @@
+import * as React from "react"
+import { cn, text } from "../../styles/theme"
+import { Input } from "./input"
+import { Label } from "./label"
+import { validateAddress, detectChainType, type ChainType, type AddressValidationResult } from "../../utils/addressValidation"
+
+export interface AddressInputProps extends Omit, 'onChange'> {
+ label?: string
+ chainType: ChainType
+ value: string
+ onChange: (value: string, validation: AddressValidationResult) => void
+ showValidation?: boolean
+ containerClassName?: string
+}
+
+const AddressInput = React.forwardRef(
+ ({
+ className,
+ label = "Address",
+ chainType,
+ value,
+ onChange,
+ showValidation = true,
+ containerClassName,
+ disabled,
+ ...props
+ }, ref) => {
+ const [validation, setValidation] = React.useState({ isValid: true });
+ const [isTouched, setIsTouched] = React.useState(false);
+
+ // Validate address whenever value or chainType changes
+ React.useEffect(() => {
+ if (value) {
+ const result = validateAddress(value, chainType);
+ setValidation(result);
+ } else {
+ setValidation({ isValid: true });
+ }
+ }, [value, chainType]);
+
+ const handleChange = React.useCallback((e: React.ChangeEvent) => {
+ const newValue = e.target.value;
+ const result = validateAddress(newValue, chainType);
+ setValidation(result);
+ onChange(newValue, result);
+ }, [chainType, onChange]);
+
+ const handleBlur = React.useCallback((e: React.FocusEvent) => {
+ setIsTouched(true);
+ props.onBlur?.(e);
+ }, [props]);
+
+ const getPlaceholder = () => {
+ switch (chainType) {
+ case 'C':
+ return '0x...';
+ case 'P':
+ return 'P-...';
+ case 'X':
+ return 'X-...';
+ default:
+ return 'Enter address';
+ }
+ };
+
+ const getChainName = () => {
+ switch (chainType) {
+ case 'C': return 'C-Chain';
+ case 'P': return 'P-Chain';
+ case 'X': return 'X-Chain';
+ default: return 'Chain';
+ }
+ };
+
+ const shouldShowError = showValidation && isTouched && !validation.isValid && value;
+ const shouldShowSuggestion = shouldShowError && validation.suggestion;
+
+ // Detect if user is entering wrong chain address
+ const detectedChain = detectChainType(value);
+ const isWrongChain = detectedChain && detectedChain !== chainType;
+
+ return (
+
+
+ {label}
+
+ ({getChainName()})
+
+
+
+
+
+
+ {/* Validation indicator */}
+ {showValidation && value && (
+
+ {validation.isValid ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ {/* Error message */}
+ {shouldShowError && (
+
+
+ {validation.error}
+
+ {shouldShowSuggestion && (
+
+ 💡 {validation.suggestion}
+
+ )}
+
+ )}
+
+ {/* Wrong chain warning */}
+ {isWrongChain && !shouldShowError && (
+
+
⚠️
+
+
+ Wrong chain detected
+
+
+ You entered a {detectedChain}-Chain address, but this field expects a {getChainName()} address.
+
+
+
+ )}
+
+ )
+ }
+)
+AddressInput.displayName = "AddressInput"
+
+export { AddressInput }
+
diff --git a/ui/src/components/ui/alert.tsx b/ui/src/components/ui/alert.tsx
new file mode 100644
index 00000000..6d2036a8
--- /dev/null
+++ b/ui/src/components/ui/alert.tsx
@@ -0,0 +1,58 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { cn } from "../../styles/theme"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/ui/src/components/ui/amount-input.tsx b/ui/src/components/ui/amount-input.tsx
new file mode 100644
index 00000000..aacc8adf
--- /dev/null
+++ b/ui/src/components/ui/amount-input.tsx
@@ -0,0 +1,135 @@
+import * as React from "react"
+import { cn, text } from "../../styles/theme"
+import { Input } from "./input"
+import { Button } from "./button"
+import { Label } from "./label"
+
+export interface AmountInputProps extends React.InputHTMLAttributes {
+ label?: string
+ symbol?: string
+ showMax?: boolean
+ showUSD?: boolean
+ showBalance?: boolean
+ usdRate?: number
+ maxValue?: string
+ onMaxClick?: () => void
+ containerClassName?: string
+}
+
+const AmountInput = React.forwardRef(
+ ({
+ className,
+ label = "Amount",
+ symbol = "AVAX",
+ showMax = true,
+ showUSD = false,
+ showBalance = false,
+ usdRate = 0,
+ maxValue,
+ onMaxClick,
+ containerClassName,
+ value,
+ onChange,
+ disabled,
+ ...props
+ }, ref) => {
+ const handleAmountChange = React.useCallback((e: React.ChangeEvent) => {
+ const inputValue = e.target.value;
+ // Allow only numbers and decimal points
+ if (/^\d*\.?\d*$/.test(inputValue)) {
+ onChange?.(e);
+ }
+ }, [onChange]);
+
+ const handleMaxClick = React.useCallback(() => {
+ if (onMaxClick) {
+ onMaxClick();
+ } else if (maxValue && onChange) {
+ // Create synthetic event for maxValue
+ const syntheticEvent = {
+ target: { value: maxValue },
+ currentTarget: { value: maxValue },
+ } as React.ChangeEvent;
+ onChange(syntheticEvent);
+ }
+ }, [onMaxClick, maxValue, onChange]);
+
+ const numericValue = React.useMemo(() => {
+ const val = typeof value === 'string' ? value : String(value || '');
+ return parseFloat(val) || 0;
+ }, [value]);
+
+ const usdValue = React.useMemo(() => {
+ if (!showUSD || !usdRate || numericValue === 0) return null;
+ return (numericValue * usdRate).toFixed(2);
+ }, [showUSD, usdRate, numericValue]);
+
+ const formatBalance = React.useCallback((balance: string) => {
+ const num = parseFloat(balance);
+ if (num === 0) return '0';
+ if (num < 0.0001) return '<0.0001';
+ if (num < 1) return num.toFixed(4);
+ if (num < 1000) return num.toFixed(2);
+ return num.toLocaleString(undefined, { maximumFractionDigits: 2 });
+ }, []);
+
+ const formattedBalance = React.useMemo(() => {
+ if (!showBalance || !maxValue) return null;
+ return formatBalance(maxValue);
+ }, [showBalance, maxValue, formatBalance]);
+
+ return (
+
+
+
+ {label}
+
+ {formattedBalance && (
+
+ Max: {formattedBalance}
+
+ )}
+
+
+
+
+
+ {showMax && (
+
+ MAX
+
+ )}
+
+
+ {usdValue && (
+
+ ≈ ${usdValue} USD
+
+ )}
+
+ )
+ }
+)
+AmountInput.displayName = "AmountInput"
+
+export { AmountInput }
diff --git a/ui/src/components/ui/avalanche-chain-overlay.tsx b/ui/src/components/ui/avalanche-chain-overlay.tsx
new file mode 100644
index 00000000..16051d91
--- /dev/null
+++ b/ui/src/components/ui/avalanche-chain-overlay.tsx
@@ -0,0 +1,113 @@
+import * as React from "react"
+import { AlertCircle } from "lucide-react"
+import { cn, text } from "../../styles/theme"
+import { Button } from "./button"
+import { useAvalanche } from "../../AvalancheProvider"
+import { avalanche, avalancheFuji } from "@avalanche-sdk/client/chains"
+
+export interface AvalancheChainOverlayProps {
+ children: React.ReactNode
+ className?: string
+ showOverlay?: boolean
+ /** If true (default), only allows Mainnet. If false, allows both Mainnet and Fuji. */
+ onlyMainnet?: boolean
+}
+
+const AvalancheChainOverlay = React.forwardRef(
+ ({ children, className, showOverlay: forceShowOverlay, onlyMainnet, ...props }, ref) => {
+ const { chain: currentChain, switchChain, availableChains } = useAvalanche()
+
+ // Check if current chain is Avalanche Mainnet or Fuji (if allowed)
+ const isAvalancheMainnet = currentChain.id === avalanche.id
+ const isAvalancheFuji = currentChain.id === avalancheFuji.id
+ const isAllowedChain = isAvalancheMainnet || (!onlyMainnet && isAvalancheFuji)
+
+ // Show overlay if not on allowed Avalanche chain or if explicitly forced
+ const shouldShowOverlay = forceShowOverlay !== undefined ? forceShowOverlay : !isAllowedChain
+
+ const handleSwitchToFuji = async () => {
+ try {
+ const fujiChain = availableChains.find(c => c.id === avalancheFuji.id)
+ if (fujiChain && switchChain) {
+ await switchChain(fujiChain)
+ }
+ } catch (error) {
+ console.error('Failed to switch to Fuji:', error)
+ }
+ }
+
+ const handleSwitchToMainnet = async () => {
+ try {
+ const mainnetChain = availableChains.find(c => c.id === avalanche.id)
+ if (mainnetChain && switchChain) {
+ await switchChain(mainnetChain)
+ }
+ } catch (error) {
+ console.error('Failed to switch to Mainnet:', error)
+ }
+ }
+
+ // If not on Avalanche chain, show the overlay
+ if (shouldShowOverlay) {
+ return (
+
+
+ {/* Alert Icon */}
+
+
+ {/* Title */}
+
+
+ Wrong Network
+
+
+ This feature is only available on Avalanche C-Chain{onlyMainnet ? ' (Mainnet)' : ' (Mainnet or Fuji Testnet)'}. You're currently on{" "}
+ {currentChain.name} .
+
+
+
+ {/* Switch Network Buttons */}
+
+ {!onlyMainnet && (
+
+ Switch to Fuji Testnet
+
+ )}
+
+ Switch to Mainnet
+
+
+
+ {/* Help Text */}
+
+ Please switch to Avalanche C-Chain {onlyMainnet ? '(Mainnet)' : '(Mainnet or Fuji Testnet)'} to use this feature.
+
+
+
+ )
+ }
+
+ // If on Avalanche chain, show the children normally
+ return (
+
+ {children}
+
+ )
+ }
+)
+AvalancheChainOverlay.displayName = "AvalancheChainOverlay"
+
+export { AvalancheChainOverlay }
+
diff --git a/ui/src/components/ui/avalanche-logo.tsx b/ui/src/components/ui/avalanche-logo.tsx
new file mode 100644
index 00000000..43e5eca1
--- /dev/null
+++ b/ui/src/components/ui/avalanche-logo.tsx
@@ -0,0 +1,47 @@
+import * as React from "react"
+import { cn } from "../../styles/theme"
+
+export interface AvalancheLogoProps extends React.SVGProps {
+ size?: number | string
+}
+
+const AvalancheLogo = React.forwardRef(
+ ({ className, size = 24, ...props }, ref) => {
+ return (
+
+
+
+
+
+ )
+ }
+)
+AvalancheLogo.displayName = "AvalancheLogo"
+
+export { AvalancheLogo }
+
diff --git a/ui/src/components/ui/badge.tsx b/ui/src/components/ui/badge.tsx
new file mode 100644
index 00000000..8a4b13fc
--- /dev/null
+++ b/ui/src/components/ui/badge.tsx
@@ -0,0 +1,43 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "../../styles/theme"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ success:
+ "border-transparent bg-green-500 text-primary-foreground hover:bg-green-500/80",
+ warning:
+ "border-transparent bg-yellow-500 text-primary-foreground hover:bg-yellow-500/80",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+const Badge = React.forwardRef(
+ ({ className, variant, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Badge.displayName = "Badge"
+
+export { Badge, badgeVariants }
diff --git a/ui/src/components/ui/button.tsx b/ui/src/components/ui/button.tsx
new file mode 100644
index 00000000..3750f0fa
--- /dev/null
+++ b/ui/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 "../../styles/theme"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ 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/ui/src/components/ui/card.tsx b/ui/src/components/ui/card.tsx
new file mode 100644
index 00000000..b3dbd49e
--- /dev/null
+++ b/ui/src/components/ui/card.tsx
@@ -0,0 +1,76 @@
+import * as React from "react"
+
+import { cn } from "../../styles/theme"
+
+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/ui/src/components/ui/direction-toggle.tsx b/ui/src/components/ui/direction-toggle.tsx
new file mode 100644
index 00000000..9b59d1d0
--- /dev/null
+++ b/ui/src/components/ui/direction-toggle.tsx
@@ -0,0 +1,48 @@
+'use client';
+import { cn, pressable } from '../../styles/theme';
+
+export interface DirectionToggleProps {
+ className?: string;
+ disabled?: boolean;
+ onClick: () => void;
+ 'data-testid'?: string;
+}
+
+export function DirectionToggle({
+ className,
+ disabled = false,
+ onClick,
+ 'data-testid': testId,
+}: DirectionToggleProps) {
+ return (
+
+ );
+}
diff --git a/ui/src/components/ui/index.ts b/ui/src/components/ui/index.ts
new file mode 100644
index 00000000..ef572215
--- /dev/null
+++ b/ui/src/components/ui/index.ts
@@ -0,0 +1,25 @@
+export { AddressInput } from './address-input';
+export { Alert, AlertTitle, AlertDescription } from './alert';
+export { AmountInput } from './amount-input';
+export { AvalancheLogo } from './avalanche-logo';
+export { AvalancheChainOverlay } from './avalanche-chain-overlay';
+export { Badge, badgeVariants } from './badge';
+export { Button, buttonVariants } from './button';
+export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './card';
+export { DirectionToggle } from './direction-toggle';
+export { Input } from './input';
+export { Label } from './label';
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+} from './select';
+export { WalletConnectionOverlay } from './wallet-connection-overlay';
+export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
diff --git a/ui/src/components/ui/input.tsx b/ui/src/components/ui/input.tsx
new file mode 100644
index 00000000..bd26f968
--- /dev/null
+++ b/ui/src/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react"
+
+import { cn } from "../../styles/theme"
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/ui/src/components/ui/label.tsx b/ui/src/components/ui/label.tsx
new file mode 100644
index 00000000..8837ad9d
--- /dev/null
+++ b/ui/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 "../../styles/theme"
+
+const labelVariants = cva(
+ "text-sm font-medium text-foreground 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/ui/src/components/ui/select.tsx b/ui/src/components/ui/select.tsx
new file mode 100644
index 00000000..27dc22c1
--- /dev/null
+++ b/ui/src/components/ui/select.tsx
@@ -0,0 +1,159 @@
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { ChevronDown, ChevronUp, Check } from "lucide-react"
+
+import { cn } from "../../styles/theme"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {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/ui/src/components/ui/tabs.tsx b/ui/src/components/ui/tabs.tsx
new file mode 100644
index 00000000..09f4bdd4
--- /dev/null
+++ b/ui/src/components/ui/tabs.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "../../styles/theme"
+
+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/ui/src/components/ui/wallet-connection-overlay.tsx b/ui/src/components/ui/wallet-connection-overlay.tsx
new file mode 100644
index 00000000..e45354af
--- /dev/null
+++ b/ui/src/components/ui/wallet-connection-overlay.tsx
@@ -0,0 +1,76 @@
+import * as React from "react"
+import { Lock } from "lucide-react"
+import { cn, text } from "../../styles/theme"
+import { Button } from "./button"
+import { useAvalanche } from "../../AvalancheProvider"
+
+export interface WalletConnectionOverlayProps {
+ children: React.ReactNode
+ className?: string
+ showOverlay?: boolean
+}
+
+const WalletConnectionOverlay = React.forwardRef(
+ ({ children, className, showOverlay: forceShowOverlay, ...props }, ref) => {
+ const { isWalletConnected, connectWallet } = useAvalanche()
+
+ // Show overlay if wallet is not connected or if explicitly forced
+ const shouldShowOverlay = forceShowOverlay || !isWalletConnected
+
+ const handleConnectWallet = async () => {
+ try {
+ await connectWallet()
+ } catch (error) {
+ console.error('Failed to connect wallet:', error)
+ }
+ }
+
+ // If wallet is not connected, show the connection state inline
+ if (shouldShowOverlay) {
+ return (
+
+
+ {/* Lock Icon */}
+
+
+
+
+ {/* Title */}
+
+
+ Wallet Not Connected
+
+
+ Connect your wallet to start making transfers on the Avalanche network.
+
+
+
+ {/* Connect Button */}
+
+ Connect Wallet
+
+
+ {/* Help Text */}
+
+ Make sure you have Core Wallet or another compatible wallet installed.
+
+
+
+ )
+ }
+
+ // If wallet is connected, show the children normally
+ return (
+
+ {children}
+
+ )
+ }
+)
+WalletConnectionOverlay.displayName = "WalletConnectionOverlay"
+
+export { WalletConnectionOverlay }
diff --git a/ui/src/earn/components/Earn.tsx b/ui/src/earn/components/Earn.tsx
new file mode 100644
index 00000000..dd9bf6a2
--- /dev/null
+++ b/ui/src/earn/components/Earn.tsx
@@ -0,0 +1,193 @@
+'use client';
+import React from 'react';
+import { cn } from '../../styles/theme';
+import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
+import { WalletConnectionOverlay } from '../../components/ui/wallet-connection-overlay';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
+import { EarnProvider } from './EarnProvider';
+import { EarnProviderSelector } from './EarnProviderSelector';
+import { EarnPoolsList } from './EarnPoolsList';
+import { EarnDeposit } from './EarnDeposit';
+import { EarnWithdraw } from './EarnWithdraw';
+import { EarnClaimRewards } from './EarnClaimRewards';
+import { TokenImage } from '../../token/components/TokenImage';
+import { Badge } from '../../components/ui/badge';
+import { ExternalLink, ArrowDownCircle, ArrowUpCircle, Gift } from 'lucide-react';
+import { useEarnContext } from './EarnProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+import { AvalancheChainOverlay } from '../../components/ui/avalanche-chain-overlay';
+import { avalanche } from '@avalanche-sdk/client/chains';
+import { openExplorer } from '../../utils/explorer';
+import type { EarnProviderProps } from '../types';
+import type { ChainConfig } from '../../types/chainConfig';
+
+type EarnProps = {
+ children?: React.ReactNode;
+ className?: string;
+ title?: string;
+} & Omit;
+
+function EarnContent() {
+ const { action, setAction, selectedPool, setSelectedPool, chainId } = useEarnContext();
+ const { availableChains, walletChainId } = useAvalanche();
+
+ const handleExternalLink = () => {
+ const chain = availableChains.find((c: ChainConfig) => c.id.toString() === chainId);
+ if (selectedPool?.poolAddress) {
+ openExplorer(chain, { type: 'address', value: selectedPool.poolAddress });
+ }
+ };
+
+ // Check if on Avalanche Mainnet
+ const isOnMainnet = walletChainId === avalanche.id;
+
+ return (
+
+
+
+
+ {selectedPool ? (
+
+ {/* Expanded Pool View */}
+
+
+
+
+
{
+ setSelectedPool(null);
+ setAction(null);
+ }}
+ className="text-muted-foreground hover:text-foreground transition-colors p-1 rounded-md hover:bg-accent"
+ title="Back to pools"
+ >
+
+
+
+
+
+
+
+
{selectedPool.name} Pool
+
+
+ {selectedPool.provider.toUpperCase()}
+
+ {selectedPool.status === 'active' && (
+
+ Active
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Pool Info */}
+
+
+
Total Supply
+
+ {parseFloat(selectedPool.totalSupply).toLocaleString(undefined, { maximumFractionDigits: 2 })} {selectedPool.token.symbol}
+
+
+ {selectedPool.userDeposited && (
+
+
Your Deposit
+
{selectedPool.userDeposited} {selectedPool.token.symbol}
+
+ )}
+ {selectedPool.userRewards && parseFloat(selectedPool.userRewards) > 0 && (
+
+
Pending Rewards
+
{selectedPool.userRewards} {selectedPool.rewardToken?.symbol || 'REWARDS'}
+
+ )}
+
+
+ {/* Action Tabs */}
+
+
setAction(value as 'deposit' | 'withdraw' | 'claim')}>
+
+
+
+ Deposit
+
+
+
+ Withdraw
+
+
+
+ Claim
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+export function Earn({
+ children,
+ className,
+ title = "Earn",
+ ...providerProps
+}: EarnProps) {
+ return (
+
+
+
+
+ {title}
+
+
+
+
+ {children || }
+
+
+
+
+ );
+}
+
diff --git a/ui/src/earn/components/EarnClaimRewards.tsx b/ui/src/earn/components/EarnClaimRewards.tsx
new file mode 100644
index 00000000..5b99d3cc
--- /dev/null
+++ b/ui/src/earn/components/EarnClaimRewards.tsx
@@ -0,0 +1,110 @@
+'use client';
+import { cn } from '../../styles/theme';
+import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
+import { Button } from '../../components/ui/button';
+import { Alert, AlertDescription } from '../../components/ui/alert';
+import { LoaderCircle, Gift, RefreshCw } from 'lucide-react';
+import { useEarnContext } from './EarnProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+
+export interface EarnClaimRewardsProps {
+ className?: string;
+}
+
+export function EarnClaimRewards({ className }: EarnClaimRewardsProps) {
+ const {
+ selectedPool,
+ isClaiming,
+ claimRewards,
+ isOnCorrectChain,
+ isSwitchingChain,
+ switchToChain,
+ chainId,
+ error,
+ } = useEarnContext();
+
+ const { availableChains } = useAvalanche();
+
+ const getChainName = () => {
+ const chain = availableChains.find(c => c.id.toString() === chainId);
+ return chain?.name || `Chain ${chainId}`;
+ };
+
+ if (!selectedPool) {
+ return (
+
+
+ Please select a pool to claim rewards from
+
+
+ );
+ }
+
+ const hasRewards = selectedPool.userRewards && parseFloat(selectedPool.userRewards) > 0;
+
+ if (!hasRewards) {
+ return (
+
+
+ No rewards available to claim
+
+
+ );
+ }
+
+ return (
+
+
+ Claim Rewards
+
+
+
+
Available Rewards
+
+ {selectedPool.userRewards} {selectedPool.rewardToken?.symbol || 'REWARDS'}
+
+
+ From {selectedPool.name} pool
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {!isOnCorrectChain && (
+
+ {isSwitchingChain ? (
+
+ ) : (
+
+ )}
+ Switch to {getChainName()}
+
+ )}
+
+
+ {isClaiming ? (
+
+ ) : (
+
+ )}
+ {isClaiming ? 'Claiming...' : 'Claim Rewards'}
+
+
+
+ );
+}
+
diff --git a/ui/src/earn/components/EarnDeposit.tsx b/ui/src/earn/components/EarnDeposit.tsx
new file mode 100644
index 00000000..4d4788a4
--- /dev/null
+++ b/ui/src/earn/components/EarnDeposit.tsx
@@ -0,0 +1,199 @@
+'use client';
+import { useState, useEffect } from 'react';
+import { cn } from '../../styles/theme';
+import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
+import { Button } from '../../components/ui/button';
+import { Alert, AlertDescription } from '../../components/ui/alert';
+import { AmountInput } from '../../components/ui/amount-input';
+import { LoaderCircle, ArrowDownCircle, RefreshCw, CheckCircle2 } from 'lucide-react';
+import { useEarnContext } from './EarnProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+
+export interface EarnDepositProps {
+ className?: string;
+}
+
+export function EarnDeposit({ className }: EarnDepositProps) {
+ const {
+ selectedPool,
+ depositAmount,
+ setDepositAmount,
+ isDepositing,
+ deposit,
+ needsApproval,
+ isApproving,
+ approveToken,
+ isValidForDeposit,
+ isOnCorrectChain,
+ isSwitchingChain,
+ switchToChain,
+ chainId,
+ error,
+ } = useEarnContext();
+
+ const { walletAddress, isWalletConnected, availableChains } = useAvalanche();
+ const [tokenBalance, setTokenBalance] = useState(null);
+
+ // Fetch token balance
+ useEffect(() => {
+ const fetchTokenBalance = async () => {
+ if (!selectedPool || !walletAddress || !isWalletConnected) {
+ setTokenBalance(null);
+ return;
+ }
+
+ try {
+ const { createPublicClient, http, formatUnits } = await import('viem');
+ const { ERC20_BALANCE_ABI } = await import('../../utils/erc20');
+
+ const chainData = availableChains.find(c => c.id.toString() === chainId);
+ if (!chainData) {
+ throw new Error(`Chain ${chainId} not found`);
+ }
+
+ const publicClient = createPublicClient({
+ chain: chainData,
+ transport: http(),
+ });
+
+ const tokenAddress = selectedPool.token.address || '0x0000000000000000000000000000000000000000';
+
+ if (!tokenAddress || tokenAddress === '0x0000000000000000000000000000000000000000') {
+ // Native token (AVAX)
+ const balance = await publicClient.getBalance({
+ address: walletAddress as `0x${string}`,
+ });
+ const formattedBalance = formatUnits(balance, 18);
+ setTokenBalance(parseFloat(formattedBalance).toLocaleString(undefined, {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 6,
+ }));
+ } else {
+ const [balance, decimals] = await Promise.all([
+ publicClient.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_BALANCE_ABI,
+ functionName: 'balanceOf',
+ args: [walletAddress as `0x${string}`],
+ }),
+ publicClient.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_BALANCE_ABI,
+ functionName: 'decimals',
+ }),
+ ]);
+
+ const formattedBalance = formatUnits(balance as bigint, decimals as number);
+ setTokenBalance(parseFloat(formattedBalance).toLocaleString(undefined, {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 6,
+ }));
+ }
+ } catch (error) {
+ console.error('Failed to fetch token balance:', error);
+ setTokenBalance('0.00');
+ }
+ };
+
+ fetchTokenBalance();
+ }, [selectedPool, walletAddress, isWalletConnected, chainId, availableChains]);
+
+ const handleMaxClick = () => {
+ if (tokenBalance && tokenBalance !== '0.00') {
+ const numericBalance = tokenBalance.replace(/,/g, '');
+ setDepositAmount(numericBalance);
+ }
+ };
+
+ const getChainName = () => {
+ const chain = availableChains.find(c => c.id.toString() === chainId);
+ return chain?.name || `Chain ${chainId}`;
+ };
+
+ if (!selectedPool) {
+ return (
+
+
+ Please select a pool to deposit
+
+
+ );
+ }
+
+ return (
+
+
+ Deposit to {selectedPool.name}
+
+
+
+
setDepositAmount(e.target.value)}
+ symbol={selectedPool.token.symbol}
+ placeholder="0.00"
+ disabled={!isOnCorrectChain || !isWalletConnected}
+ showMax={!!tokenBalance && tokenBalance !== '0.00' && isWalletConnected && isOnCorrectChain}
+ maxValue={tokenBalance?.replace(/,/g, '') || '0'}
+ onMaxClick={handleMaxClick}
+ showBalance={!!tokenBalance && isWalletConnected}
+ />
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {!isOnCorrectChain && (
+
+ {isSwitchingChain ? (
+
+ ) : (
+
+ )}
+ Switch to {getChainName()}
+
+ )}
+
+ {needsApproval && (
+
+ {isApproving ? (
+
+ ) : (
+
+ )}
+ {isApproving ? 'Approving...' : `Approve ${selectedPool.token.symbol}`}
+
+ )}
+
+
+ {isDepositing ? (
+
+ ) : (
+
+ )}
+ {isDepositing ? 'Depositing...' : `Deposit ${selectedPool.token.symbol}`}
+
+
+
+ );
+}
+
diff --git a/ui/src/earn/components/EarnPoolCard.tsx b/ui/src/earn/components/EarnPoolCard.tsx
new file mode 100644
index 00000000..1dc298f1
--- /dev/null
+++ b/ui/src/earn/components/EarnPoolCard.tsx
@@ -0,0 +1,102 @@
+'use client';
+import React from 'react';
+import { ExternalLink } from 'lucide-react';
+import { cn } from '../../styles/theme';
+import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
+import { Badge } from '../../components/ui/badge';
+import { TokenImage } from '../../token/components/TokenImage';
+import { useEarnContext } from './EarnProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+import { openExplorer } from '../../utils/explorer';
+import type { EarnPool } from '../types';
+
+export interface EarnPoolCardProps {
+ pool: EarnPool;
+ className?: string;
+ onClick?: (pool: EarnPool) => void;
+}
+
+export function EarnPoolCard({ pool, className, onClick }: EarnPoolCardProps) {
+ const { setSelectedPool, chainId } = useEarnContext();
+ const { availableChains } = useAvalanche();
+
+ const handleClick = () => {
+ setSelectedPool(pool);
+ onClick?.(pool);
+ };
+
+
+ const handleExternalLink = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const chain = availableChains.find(c => c.id.toString() === chainId);
+ openExplorer(chain, { type: 'address', value: pool.poolAddress });
+ };
+
+ const formatNumber = (value: string) => {
+ const num = parseFloat(value);
+ if (num === 0) return '0';
+ if (num < 0.0001) return '<0.0001';
+ if (num < 1) return num.toFixed(4);
+ if (num < 1000) return num.toFixed(2);
+ return num.toLocaleString(undefined, { maximumFractionDigits: 2 });
+ };
+
+ return (
+
+
+
+
+
+
+
{pool.name}
+
+
+ {pool.provider.toUpperCase()}
+
+ {pool.status === 'active' && (
+
+ Active
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
Total Supply
+
{formatNumber(pool.totalSupply)} {pool.token.symbol}
+
+
+ {pool.userDeposited && (
+
+
Your Deposit
+
{pool.userDeposited} {pool.token.symbol}
+
+ )}
+
+ {pool.userRewards && parseFloat(pool.userRewards) > 0 && (
+
+
Pending Rewards
+
{pool.userRewards} {pool.rewardToken?.symbol || 'REWARDS'}
+
+ )}
+
+
+
+ );
+}
+
diff --git a/ui/src/earn/components/EarnPoolListItem.tsx b/ui/src/earn/components/EarnPoolListItem.tsx
new file mode 100644
index 00000000..a0a71d91
--- /dev/null
+++ b/ui/src/earn/components/EarnPoolListItem.tsx
@@ -0,0 +1,89 @@
+'use client';
+import React from 'react';
+import { ExternalLink } from 'lucide-react';
+import { cn } from '../../styles/theme';
+import { Badge } from '../../components/ui/badge';
+import { TokenImage } from '../../token/components/TokenImage';
+import { useEarnContext } from './EarnProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+import { openExplorer } from '../../utils/explorer';
+import type { EarnPool } from '../types';
+
+export interface EarnPoolListItemProps {
+ pool: EarnPool;
+ className?: string;
+ onClick?: (pool: EarnPool) => void;
+}
+
+export function EarnPoolListItem({ pool, className, onClick }: EarnPoolListItemProps) {
+ const { setSelectedPool, chainId } = useEarnContext();
+ const { availableChains } = useAvalanche();
+
+ const handleClick = () => {
+ setSelectedPool(pool);
+ onClick?.(pool);
+ };
+
+
+ const handleExternalLink = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const chain = availableChains.find(c => c.id.toString() === chainId);
+ openExplorer(chain, { type: 'address', value: pool.poolAddress });
+ };
+
+ const formatNumber = (value: string) => {
+ const num = parseFloat(value);
+ if (num === 0) return '0';
+ if (num < 0.0001) return '<0.0001';
+ if (num < 1) return num.toFixed(4);
+ if (num < 1000) return num.toFixed(2);
+ return num.toLocaleString(undefined, { maximumFractionDigits: 2 });
+ };
+
+ return (
+
+
+
+
+
+
{pool.name}
+
+ {pool.provider.toUpperCase()}
+
+ {pool.status === 'active' && (
+
+ Active
+
+ )}
+
+
+ Total Supply: {formatNumber(pool.totalSupply)} {pool.token.symbol}
+ {pool.userDeposited && (
+ Your Deposit: {pool.userDeposited} {pool.token.symbol}
+ )}
+ {pool.userRewards && parseFloat(pool.userRewards) > 0 && (
+ Rewards: {pool.userRewards} {pool.rewardToken?.symbol || 'REWARDS'}
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/ui/src/earn/components/EarnPoolsList.tsx b/ui/src/earn/components/EarnPoolsList.tsx
new file mode 100644
index 00000000..00fad4ac
--- /dev/null
+++ b/ui/src/earn/components/EarnPoolsList.tsx
@@ -0,0 +1,129 @@
+'use client';
+import { LayoutGrid, List, RefreshCw, Loader2 } from 'lucide-react';
+import { cn } from '../../styles/theme';
+import { Button } from '../../components/ui/button';
+import { Tabs, TabsList, TabsTrigger } from '../../components/ui/tabs';
+import { useEarnContext } from './EarnProvider';
+import { EarnPoolCard } from './EarnPoolCard';
+import { EarnPoolListItem } from './EarnPoolListItem';
+
+export interface EarnPoolsListProps {
+ className?: string;
+ onPoolClick?: (pool: any) => void;
+}
+
+export function EarnPoolsList({ className, onPoolClick }: EarnPoolsListProps) {
+ const { pools, isLoadingPools, viewMode, setViewMode, refreshPools } = useEarnContext();
+
+ return (
+
+ {/* Header with view toggle */}
+
+
Earn Pools
+
+ setViewMode(value as 'card' | 'list')}>
+
+
+
+ Card
+
+
+
+ List
+
+
+
+
+ {isLoadingPools ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Loading state - initial load */}
+ {isLoadingPools && pools.length === 0 && (
+
+ )}
+
+ {/* Loading overlay - when refreshing existing pools */}
+ {isLoadingPools && pools.length > 0 && (
+
+
+
+
+
Refreshing pools...
+
+
+ {viewMode === 'card' ? (
+
+ {pools.map((pool) => (
+
+ ))}
+
+ ) : (
+
+ {pools.map((pool) => (
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Empty state */}
+ {!isLoadingPools && pools.length === 0 && (
+
+ )}
+
+ {/* Pools grid/list */}
+ {!isLoadingPools && pools.length > 0 && (
+ <>
+ {viewMode === 'card' ? (
+
+ {pools.map((pool) => (
+
+ ))}
+
+ ) : (
+
+ {pools.map((pool) => (
+
+ ))}
+
+ )}
+ >
+ )}
+
+ );
+}
+
diff --git a/ui/src/earn/components/EarnProvider.tsx b/ui/src/earn/components/EarnProvider.tsx
new file mode 100644
index 00000000..0e5c2e0c
--- /dev/null
+++ b/ui/src/earn/components/EarnProvider.tsx
@@ -0,0 +1,585 @@
+'use client';
+import { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';
+import { useAvalanche } from '../../AvalancheProvider';
+import { getProvider } from '../providers/registry';
+import { createPublicClient, http } from 'viem';
+import type {
+ EarnProviderType,
+ EarnPool,
+ EarnAction,
+ EarnStatus,
+ EarnProviderProps,
+ EarnContextType
+} from '../types';
+
+const EarnContext = createContext(undefined);
+
+export function EarnProvider({
+ children,
+ initialProvider = 'aave',
+ initialChainId,
+ onStatusChange,
+ onSuccess,
+ onError,
+}: EarnProviderProps) {
+ const { availableChains, walletClient, walletChainId, switchChain, chainkit } = useAvalanche();
+
+ // Get default chain ID
+ const defaultChainId = initialChainId || availableChains[0]?.id.toString() || '43114';
+
+ // Provider selection
+ const [provider, setProvider] = useState(initialProvider);
+
+ // Chain selection
+ const [chainId, setChainId] = useState(defaultChainId);
+
+ // Pool selection
+ const [selectedPool, setSelectedPool] = useState(null);
+ const [pools, setPools] = useState([]);
+ const [isLoadingPools, setIsLoadingPools] = useState(false);
+
+ // View mode
+ const [viewMode, setViewMode] = useState<'card' | 'list'>('card');
+
+ // Action state
+ const [action, setAction] = useState(null);
+
+ // Deposit state
+ const [depositAmount, setDepositAmount] = useState('');
+ const [isDepositing, setIsDepositing] = useState(false);
+
+ // Approval state
+ const [needsApproval, setNeedsApproval] = useState(false);
+ const [isApproving, setIsApproving] = useState(false);
+
+ // Withdraw state
+ const [withdrawAmount, setWithdrawAmount] = useState('');
+ const [isWithdrawing, setIsWithdrawing] = useState(false);
+
+ // Claim rewards state
+ const [isClaiming, setIsClaiming] = useState(false);
+
+ // Status and errors
+ const [status, setStatus] = useState('idle');
+ const [error, setError] = useState(null);
+
+ // Chain switching state
+ const [isOnCorrectChain, setIsOnCorrectChain] = useState(true);
+ const [isSwitchingChain, setIsSwitchingChain] = useState(false);
+
+ // Load pools based on provider and chain
+ const loadPools = useCallback(async () => {
+ setIsLoadingPools(true);
+ setError(null);
+
+ try {
+ const chainData = availableChains.find(chain => chain.id.toString() === chainId);
+
+ if (!chainData) {
+ throw new Error(`Chain ${chainId} not found`);
+ }
+
+ const earnProvider = getProvider(provider);
+ const userAddress = walletClient?.account?.address as `0x${string}` | undefined;
+ const fetchedPools = await earnProvider.fetchPools(chainData, chainkit, userAddress);
+ setPools(fetchedPools);
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Failed to load pools';
+ setError(errorMessage);
+ console.error('Error loading pools:', err);
+ onError?.(new Error(errorMessage));
+ } finally {
+ setIsLoadingPools(false);
+ }
+ }, [provider, chainId, availableChains, walletClient, chainkit, onError]);
+
+ // Refresh pools
+ const refreshPools = useCallback(async () => {
+ await loadPools();
+ }, [loadPools]);
+
+ // Load pools when provider or chain changes
+ useEffect(() => {
+ loadPools();
+ }, [loadPools]);
+
+ // Reload pools when wallet address changes to update user deposits
+ useEffect(() => {
+ if (walletClient?.account?.address) {
+ loadPools();
+ }
+ }, [walletClient?.account?.address, loadPools]);
+
+ // Check if user is on correct chain
+ useEffect(() => {
+ const chainData = availableChains.find(chain => chain.id.toString() === chainId);
+
+ if (!chainData || !walletChainId) {
+ setIsOnCorrectChain(false);
+ return;
+ }
+
+ setIsOnCorrectChain(walletChainId === chainData.id);
+ }, [chainId, availableChains, walletChainId]);
+
+ // Switch to chain
+ const switchToChain = useCallback(async () => {
+ const chainData = availableChains.find(chain => chain.id.toString() === chainId);
+
+ if (!chainData) {
+ setError('Chain not found');
+ return;
+ }
+
+ if (!switchChain) {
+ setError('Chain switching not available');
+ return;
+ }
+
+ setIsSwitchingChain(true);
+ setError(null);
+
+ try {
+ await switchChain(chainData);
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Failed to switch chain';
+ setError(errorMessage);
+ onError?.(new Error(errorMessage));
+ } finally {
+ setIsSwitchingChain(false);
+ }
+ }, [chainId, availableChains, switchChain, onError]);
+
+ // Check approval status
+ const checkApproval = useCallback(async () => {
+ if (!selectedPool || !depositAmount || !walletClient?.account) {
+ setNeedsApproval(false);
+ return;
+ }
+
+ const chainData = availableChains.find(chain => chain.id.toString() === chainId);
+ if (!chainData) {
+ setNeedsApproval(false);
+ return;
+ }
+
+ try {
+ const publicClient = createPublicClient({
+ chain: chainData,
+ transport: http(),
+ });
+
+ const earnProvider = getProvider(selectedPool.provider);
+ const approvalCheck = await earnProvider.checkApproval({
+ publicClient,
+ pool: selectedPool,
+ amount: depositAmount,
+ owner: walletClient.account.address as `0x${string}`,
+ });
+
+ setNeedsApproval(approvalCheck.needsApproval);
+ } catch (err) {
+ console.error('Error checking approval:', err);
+ setNeedsApproval(false);
+ }
+ }, [selectedPool, depositAmount, walletClient, chainId, availableChains]);
+
+ // Approve token
+ const approveToken = useCallback(async () => {
+ if (!selectedPool || !depositAmount) {
+ setError('Please select a pool and enter an amount');
+ return;
+ }
+
+ if (!walletClient?.account) {
+ setError('Please connect your wallet');
+ return;
+ }
+
+ const chainData = availableChains.find(chain => chain.id.toString() === chainId);
+ if (!chainData) {
+ setError('Chain not found');
+ return;
+ }
+
+ setIsApproving(true);
+ setError(null);
+
+ try {
+ const earnProvider = getProvider(selectedPool.provider);
+ const result = await earnProvider.approveToken({
+ walletClient,
+ chain: chainData,
+ pool: selectedPool,
+ amount: depositAmount,
+ });
+
+ // Recheck approval status
+ await checkApproval();
+
+ onSuccess?.({
+ pool: selectedPool,
+ amount: depositAmount,
+ action: 'approve' as const,
+ txHash: result.txHash,
+ });
+ } catch (err) {
+ let errorMessage = err instanceof Error ? err.message : 'Approval failed';
+
+ // Truncate long error messages
+ if (errorMessage.length > 200) {
+ errorMessage = errorMessage.substring(0, 200) + '...';
+ }
+
+ // Simplify common error messages
+ if (errorMessage.includes('User rejected')) {
+ errorMessage = 'Transaction was rejected. Please try again.';
+ } else if (errorMessage.includes('User denied')) {
+ errorMessage = 'Transaction was denied. Please try again.';
+ }
+
+ setError(errorMessage);
+ onError?.(new Error(errorMessage));
+ } finally {
+ setIsApproving(false);
+ }
+ }, [selectedPool, depositAmount, walletClient, chainId, availableChains, checkApproval, onSuccess, onError]);
+
+ // Deposit
+ const deposit = useCallback(async () => {
+ if (!selectedPool || !depositAmount) {
+ setError('Please select a pool and enter an amount');
+ return;
+ }
+
+ if (!walletClient?.account) {
+ setError('Please connect your wallet');
+ return;
+ }
+
+ if (needsApproval) {
+ setError('Please approve the token first');
+ return;
+ }
+
+ const chainData = availableChains.find(chain => chain.id.toString() === chainId);
+ if (!chainData) {
+ setError('Chain not found');
+ return;
+ }
+
+ setIsDepositing(true);
+ setStatus('loading');
+ setError(null);
+
+ try {
+ const earnProvider = getProvider(selectedPool.provider);
+ const depositResult = await earnProvider.deposit({
+ walletClient,
+ chain: chainData,
+ pool: selectedPool,
+ amount: depositAmount,
+ });
+
+ const result = {
+ pool: selectedPool,
+ amount: depositAmount,
+ action: 'deposit' as EarnAction,
+ txHash: depositResult.txHash,
+ };
+
+ setStatus('success');
+ setDepositAmount('');
+
+ // Refresh pools to update user deposits
+ await loadPools();
+
+ onSuccess?.(result);
+ } catch (err) {
+ let errorMessage = err instanceof Error ? err.message : 'Deposit failed';
+
+ // Truncate long error messages
+ if (errorMessage.length > 200) {
+ errorMessage = errorMessage.substring(0, 200) + '...';
+ }
+
+ // Simplify common error messages
+ if (errorMessage.includes('User rejected')) {
+ errorMessage = 'Transaction was rejected. Please try again.';
+ } else if (errorMessage.includes('User denied')) {
+ errorMessage = 'Transaction was denied. Please try again.';
+ }
+
+ setError(errorMessage);
+ setStatus('error');
+ onError?.(new Error(errorMessage));
+ } finally {
+ setIsDepositing(false);
+ }
+ }, [selectedPool, depositAmount, walletClient, chainId, availableChains, needsApproval, loadPools, onSuccess, onError]);
+
+ // Check approval when deposit amount or pool changes
+ useEffect(() => {
+ if (selectedPool && depositAmount && walletClient?.account) {
+ checkApproval();
+ } else {
+ setNeedsApproval(false);
+ }
+ }, [selectedPool, depositAmount, walletClient?.account, checkApproval]);
+
+ // Withdraw
+ const withdraw = useCallback(async () => {
+ if (!selectedPool || !withdrawAmount) {
+ setError('Please select a pool and enter an amount');
+ return;
+ }
+
+ if (!walletClient?.account) {
+ setError('Please connect your wallet');
+ return;
+ }
+
+ const chainData = availableChains.find(chain => chain.id.toString() === chainId);
+ if (!chainData) {
+ setError('Chain not found');
+ return;
+ }
+
+ setIsWithdrawing(true);
+ setStatus('loading');
+ setError(null);
+
+ try {
+ const earnProvider = getProvider(selectedPool.provider);
+ const withdrawResult = await earnProvider.withdraw({
+ walletClient,
+ chain: chainData,
+ pool: selectedPool,
+ amount: withdrawAmount,
+ });
+
+ const result = {
+ pool: selectedPool,
+ amount: withdrawAmount,
+ action: 'withdraw' as EarnAction,
+ txHash: withdrawResult.txHash,
+ };
+
+ setStatus('success');
+ setWithdrawAmount('');
+
+ // Refresh pools to update user deposits
+ await loadPools();
+
+ onSuccess?.(result);
+ } catch (err) {
+ let errorMessage = err instanceof Error ? err.message : 'Withdraw failed';
+
+ // Truncate long error messages
+ if (errorMessage.length > 200) {
+ errorMessage = errorMessage.substring(0, 200) + '...';
+ }
+
+ // Simplify common error messages
+ if (errorMessage.includes('User rejected')) {
+ errorMessage = 'Transaction was rejected. Please try again.';
+ } else if (errorMessage.includes('User denied')) {
+ errorMessage = 'Transaction was denied. Please try again.';
+ }
+
+ setError(errorMessage);
+ setStatus('error');
+ onError?.(new Error(errorMessage));
+ } finally {
+ setIsWithdrawing(false);
+ }
+ }, [selectedPool, withdrawAmount, walletClient, chainId, availableChains, loadPools, onSuccess, onError]);
+
+ // Claim rewards
+ const claimRewards = useCallback(async () => {
+ if (!selectedPool) {
+ setError('Please select a pool');
+ return;
+ }
+
+ if (!walletClient?.account) {
+ setError('Please connect your wallet');
+ return;
+ }
+
+ const chainData = availableChains.find(chain => chain.id.toString() === chainId);
+ if (!chainData) {
+ setError('Chain not found');
+ return;
+ }
+
+ setIsClaiming(true);
+ setStatus('loading');
+ setError(null);
+
+ try {
+ const earnProvider = getProvider(selectedPool.provider);
+ const claimResult = await earnProvider.claimRewards({
+ walletClient,
+ chain: chainData,
+ pool: selectedPool,
+ });
+
+ const result = {
+ pool: selectedPool,
+ action: 'claim' as EarnAction,
+ txHash: claimResult.txHash,
+ };
+
+ setStatus('success');
+
+ // Refresh pools to update user rewards
+ await loadPools();
+
+ onSuccess?.(result);
+ } catch (err) {
+ let errorMessage = err instanceof Error ? err.message : 'Claim rewards failed';
+
+ // Truncate long error messages
+ if (errorMessage.length > 200) {
+ errorMessage = errorMessage.substring(0, 200) + '...';
+ }
+
+ // Simplify common error messages
+ if (errorMessage.includes('User rejected')) {
+ errorMessage = 'Transaction was rejected. Please try again.';
+ } else if (errorMessage.includes('User denied')) {
+ errorMessage = 'Transaction was denied. Please try again.';
+ }
+
+ setError(errorMessage);
+ setStatus('error');
+ onError?.(new Error(errorMessage));
+ } finally {
+ setIsClaiming(false);
+ }
+ }, [selectedPool, walletClient, chainId, availableChains, loadPools, onSuccess, onError]);
+
+ // Validation
+ const { isValidForDeposit, isValidForWithdraw, validationErrors } = useMemo(() => {
+ const errors: string[] = [];
+
+ if (!selectedPool) {
+ errors.push('Please select a pool');
+ }
+
+ if (!isOnCorrectChain) {
+ errors.push('Please switch to the correct chain');
+ }
+
+ if (!walletClient) {
+ errors.push('Please connect your wallet');
+ }
+
+ // Deposit validation
+ const depositErrors = [...errors];
+ if (!depositAmount || parseFloat(depositAmount) <= 0) {
+ depositErrors.push('Please enter a valid deposit amount');
+ }
+
+ // Withdraw validation
+ const withdrawErrors = [...errors];
+ if (!withdrawAmount || parseFloat(withdrawAmount) <= 0) {
+ withdrawErrors.push('Please enter a valid withdraw amount');
+ }
+ if (selectedPool && selectedPool.userDeposited) {
+ const withdrawAmountNum = parseFloat(withdrawAmount);
+ const userDepositedNum = parseFloat(selectedPool.userDeposited);
+ if (withdrawAmountNum > userDepositedNum) {
+ withdrawErrors.push('Withdraw amount exceeds your deposited amount');
+ }
+ }
+
+ return {
+ isValidForDeposit: depositErrors.length === 0 && isOnCorrectChain && !!walletClient,
+ isValidForWithdraw: withdrawErrors.length === 0 && isOnCorrectChain && !!walletClient,
+ validationErrors: withdrawErrors,
+ };
+ }, [selectedPool, depositAmount, withdrawAmount, isOnCorrectChain, walletClient]);
+
+ // Status change callback
+ useEffect(() => {
+ onStatusChange?.(status);
+ }, [status, onStatusChange]);
+
+ const contextValue: EarnContextType = {
+ // Provider selection
+ provider,
+ setProvider,
+
+ // Chain selection
+ chainId,
+ setChainId,
+
+ // Pool selection
+ selectedPool,
+ setSelectedPool,
+ pools,
+ isLoadingPools,
+ refreshPools,
+
+ // View mode
+ viewMode,
+ setViewMode,
+
+ // Action state
+ action,
+ setAction,
+
+ // Deposit state
+ depositAmount,
+ setDepositAmount,
+ isDepositing,
+ deposit,
+
+ // Approval state
+ needsApproval,
+ isApproving,
+ approveToken,
+ checkApproval,
+
+ // Withdraw state
+ withdrawAmount,
+ setWithdrawAmount,
+ isWithdrawing,
+ withdraw,
+
+ // Claim rewards state
+ isClaiming,
+ claimRewards,
+
+ // Status and errors
+ status,
+ error,
+
+ // Validation
+ isValidForDeposit,
+ isValidForWithdraw,
+ validationErrors,
+
+ // Chain switching
+ isOnCorrectChain,
+ isSwitchingChain,
+ switchToChain,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useEarnContext() {
+ const context = useContext(EarnContext);
+ if (context === undefined) {
+ throw new Error('useEarnContext must be used within an EarnProvider');
+ }
+ return context;
+}
+
diff --git a/ui/src/earn/components/EarnProviderSelector.tsx b/ui/src/earn/components/EarnProviderSelector.tsx
new file mode 100644
index 00000000..8e7917ce
--- /dev/null
+++ b/ui/src/earn/components/EarnProviderSelector.tsx
@@ -0,0 +1,29 @@
+'use client';
+import { cn } from '../../styles/theme';
+import { Tabs, TabsList, TabsTrigger } from '../../components/ui/tabs';
+import { useEarnContext } from './EarnProvider';
+
+export interface EarnProviderSelectorProps {
+ className?: string;
+}
+
+export function EarnProviderSelector({ className }: EarnProviderSelectorProps) {
+ const { provider, setProvider } = useEarnContext();
+
+ return (
+
+ Provider
+ setProvider(value as 'aave' | 'benqi')}>
+
+
+ AAVE
+
+
+ Benqi
+
+
+
+
+ );
+}
+
diff --git a/ui/src/earn/components/EarnSinglePoolCard.tsx b/ui/src/earn/components/EarnSinglePoolCard.tsx
new file mode 100644
index 00000000..b6afee8b
--- /dev/null
+++ b/ui/src/earn/components/EarnSinglePoolCard.tsx
@@ -0,0 +1,218 @@
+'use client';
+import React from 'react';
+import { ExternalLink, ArrowDownCircle, ArrowUpCircle, Gift, Loader2 } from 'lucide-react';
+import { cn } from '../../styles/theme';
+import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
+import { Badge } from '../../components/ui/badge';
+import { TokenImage } from '../../token/components/TokenImage';
+import { EarnProvider } from './EarnProvider';
+import { EarnDeposit } from './EarnDeposit';
+import { EarnWithdraw } from './EarnWithdraw';
+import { EarnClaimRewards } from './EarnClaimRewards';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
+import { WalletConnectionOverlay } from '../../components/ui/wallet-connection-overlay';
+import { AvalancheChainOverlay } from '../../components/ui/avalanche-chain-overlay';
+import { useEarnContext } from './EarnProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+import { avalanche } from '@avalanche-sdk/client/chains';
+import { openExplorer } from '../../utils/explorer';
+import type { EarnProviderType } from '../types';
+import type { ChainConfig } from '../../types/chainConfig';
+
+export interface EarnSinglePoolCardProps {
+ /** Provider name (aave, benqi) */
+ provider: EarnProviderType;
+ /** Chain configuration */
+ chain: ChainConfig;
+ /** Pool contract address (aToken address) */
+ poolAddress: string;
+ /** Optional className */
+ className?: string;
+ /** Optional title */
+ title?: string;
+ /** Callback on success */
+ onSuccess?: (result: any) => void;
+ /** Callback on error */
+ onError?: (error: Error) => void;
+}
+
+function EarnSinglePoolCardContent({
+ provider,
+ chain,
+ poolAddress
+}: Omit) {
+ const { action, setAction, selectedPool, setSelectedPool, pools, isLoadingPools } = useEarnContext();
+ const { walletChainId } = useAvalanche();
+
+ // Check if on Avalanche Mainnet
+ const isOnMainnet = walletChainId === avalanche.id;
+
+ const handleExternalLink = () => {
+ openExplorer(chain, { type: 'address', value: poolAddress });
+ };
+
+ // Find and select the pool by address when pools are loaded
+ React.useEffect(() => {
+ if (pools.length > 0 && !selectedPool) {
+ const pool = pools.find(p => p.poolAddress.toLowerCase() === poolAddress.toLowerCase());
+ if (pool) {
+ setSelectedPool(pool);
+ setAction('deposit');
+ }
+ }
+ }, [pools, poolAddress, selectedPool, setSelectedPool, setAction]);
+
+ if (isLoadingPools) {
+ return (
+
+
+
Loading pool data...
+
+ );
+ }
+
+ if (!selectedPool || selectedPool.poolAddress.toLowerCase() !== poolAddress.toLowerCase()) {
+ return (
+
+
Pool not found
+
Address: {poolAddress}
+
+ );
+ }
+
+ return (
+
+
+ {/* Pool Header */}
+
+
+
+
+
{selectedPool.name} Pool
+
+
+ {provider.toUpperCase()}
+
+
+ {chain.name}
+
+ {selectedPool.status === 'active' && (
+
+ Active
+
+ )}
+
+
+
+
+
+
+
+
+ {/* Pool Info */}
+
+
+
Total Supply
+
+ {parseFloat(selectedPool.totalSupply).toLocaleString(undefined, { maximumFractionDigits: 2 })} {selectedPool.token.symbol}
+
+
+ {selectedPool.userDeposited && (
+
+
Your Deposit
+
{selectedPool.userDeposited} {selectedPool.token.symbol}
+
+ )}
+ {selectedPool.userRewards && parseFloat(selectedPool.userRewards) > 0 && (
+
+
Pending Rewards
+
{selectedPool.userRewards} {selectedPool.rewardToken?.symbol || 'REWARDS'}
+
+ )}
+
+
+ {/* Action Tabs */}
+
+
setAction(value as 'deposit' | 'withdraw' | 'claim')}>
+
+
+
+ Deposit
+
+
+
+ Withdraw
+
+
+
+ Claim
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function EarnSinglePoolCard({
+ provider,
+ chain,
+ poolAddress,
+ className,
+ title = "Earn",
+ onSuccess,
+ onError,
+}: EarnSinglePoolCardProps) {
+ return (
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/ui/src/earn/components/EarnWithdraw.tsx b/ui/src/earn/components/EarnWithdraw.tsx
new file mode 100644
index 00000000..1aca9679
--- /dev/null
+++ b/ui/src/earn/components/EarnWithdraw.tsx
@@ -0,0 +1,128 @@
+'use client';
+import { cn } from '../../styles/theme';
+import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
+import { Button } from '../../components/ui/button';
+import { Alert, AlertDescription } from '../../components/ui/alert';
+import { AmountInput } from '../../components/ui/amount-input';
+import { LoaderCircle, ArrowUpCircle, RefreshCw } from 'lucide-react';
+import { useEarnContext } from './EarnProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+
+export interface EarnWithdrawProps {
+ className?: string;
+}
+
+export function EarnWithdraw({ className }: EarnWithdrawProps) {
+ const {
+ selectedPool,
+ withdrawAmount,
+ setWithdrawAmount,
+ isWithdrawing,
+ withdraw,
+ isValidForWithdraw,
+ isOnCorrectChain,
+ isSwitchingChain,
+ switchToChain,
+ chainId,
+ error,
+ } = useEarnContext();
+
+ const { availableChains } = useAvalanche();
+
+ const handleMaxClick = () => {
+ if (selectedPool?.userDeposited) {
+ setWithdrawAmount(selectedPool.userDeposited);
+ }
+ };
+
+ const getChainName = () => {
+ const chain = availableChains.find(c => c.id.toString() === chainId);
+ return chain?.name || `Chain ${chainId}`;
+ };
+
+ if (!selectedPool) {
+ return (
+
+
+ Please select a pool to withdraw from
+
+
+ );
+ }
+
+ if (!selectedPool.userDeposited || parseFloat(selectedPool.userDeposited) <= 0) {
+ return (
+
+
+ You have no deposits in this pool
+
+
+ );
+ }
+
+ return (
+
+
+ Withdraw from {selectedPool.name}
+
+
+
+
setWithdrawAmount(e.target.value)}
+ symbol={selectedPool.token.symbol}
+ placeholder="0.00"
+ disabled={!isOnCorrectChain}
+ showMax={!!selectedPool.userDeposited && parseFloat(selectedPool.userDeposited) > 0}
+ maxValue={selectedPool.userDeposited}
+ onMaxClick={handleMaxClick}
+ showBalance={!!selectedPool.userDeposited}
+ />
+ {selectedPool.userDeposited && (
+
+ Available: {selectedPool.userDeposited} {selectedPool.token.symbol}
+
+ )}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {!isOnCorrectChain && (
+
+ {isSwitchingChain ? (
+
+ ) : (
+
+ )}
+ Switch to {getChainName()}
+
+ )}
+
+
+ {isWithdrawing ? (
+
+ ) : (
+
+ )}
+ {isWithdrawing ? 'Withdrawing...' : `Withdraw ${selectedPool.token.symbol}`}
+
+
+
+ );
+}
+
diff --git a/ui/src/earn/components/index.ts b/ui/src/earn/components/index.ts
new file mode 100644
index 00000000..aac1e57d
--- /dev/null
+++ b/ui/src/earn/components/index.ts
@@ -0,0 +1,12 @@
+export { Earn } from './Earn';
+export { EarnProvider, useEarnContext } from './EarnProvider';
+export { EarnPoolsList } from './EarnPoolsList';
+export { EarnPoolCard } from './EarnPoolCard';
+export { EarnPoolListItem } from './EarnPoolListItem';
+export { EarnDeposit } from './EarnDeposit';
+export { EarnWithdraw } from './EarnWithdraw';
+export { EarnClaimRewards } from './EarnClaimRewards';
+export { EarnProviderSelector } from './EarnProviderSelector';
+export { EarnSinglePoolCard } from './EarnSinglePoolCard';
+export type { EarnSinglePoolCardProps } from './EarnSinglePoolCard';
+
diff --git a/ui/src/earn/hooks/index.ts b/ui/src/earn/hooks/index.ts
new file mode 100644
index 00000000..a5e99d05
--- /dev/null
+++ b/ui/src/earn/hooks/index.ts
@@ -0,0 +1,2 @@
+export { useEarnContext } from '../components/EarnProvider';
+
diff --git a/ui/src/earn/index.ts b/ui/src/earn/index.ts
new file mode 100644
index 00000000..e299b0f7
--- /dev/null
+++ b/ui/src/earn/index.ts
@@ -0,0 +1,5 @@
+// Earn Module
+export * from './components';
+export * from './hooks';
+export * from './types';
+
diff --git a/ui/src/earn/providers/base.ts b/ui/src/earn/providers/base.ts
new file mode 100644
index 00000000..274ccc6b
--- /dev/null
+++ b/ui/src/earn/providers/base.ts
@@ -0,0 +1,74 @@
+import type { ChainConfig } from '../../types/chainConfig';
+import type { WalletClient } from 'viem';
+import type { Avalanche } from '@avalanche-sdk/chainkit';
+import type { EarnPool } from '../types';
+
+/**
+ * Abstract base interface for earn providers
+ * All earn providers (AAVE, Benqi, etc.) must implement this interface
+ */
+export interface EarnProviderBase {
+ /**
+ * Provider identifier (e.g., 'aave', 'benqi')
+ */
+ readonly providerId: string;
+
+ /**
+ * Fetch pools from the provider
+ */
+ fetchPools(
+ chain: ChainConfig,
+ chainkit: Avalanche,
+ userAddress?: `0x${string}`
+ ): Promise;
+
+ /**
+ * Check if token approval is needed before deposit
+ */
+ checkApproval(params: {
+ publicClient: ReturnType;
+ pool: EarnPool;
+ amount: string;
+ owner: `0x${string}`;
+ }): Promise<{ needsApproval: boolean; currentAllowance: bigint; requiredAmount: bigint }>;
+
+ /**
+ * Approve token spending
+ */
+ approveToken(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ pool: EarnPool;
+ amount: string;
+ }): Promise<{ txHash: `0x${string}` }>;
+
+ /**
+ * Deposit tokens to pool
+ */
+ deposit(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ pool: EarnPool;
+ amount: string;
+ }): Promise<{ txHash: `0x${string}` }>;
+
+ /**
+ * Withdraw tokens from pool
+ */
+ withdraw(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ pool: EarnPool;
+ amount: string;
+ }): Promise<{ txHash: `0x${string}` }>;
+
+ /**
+ * Claim rewards from pool
+ */
+ claimRewards(params: {
+ walletClient: WalletClient;
+ chain: ChainConfig;
+ pool: EarnPool;
+ }): Promise<{ txHash: `0x${string}` }>;
+}
+
diff --git a/ui/src/earn/providers/index.ts b/ui/src/earn/providers/index.ts
new file mode 100644
index 00000000..35ea8023
--- /dev/null
+++ b/ui/src/earn/providers/index.ts
@@ -0,0 +1,3 @@
+export type { EarnProviderBase } from './base';
+export { getProvider, registerProvider, getProviderIds } from './registry';
+
diff --git a/ui/src/earn/providers/registry.ts b/ui/src/earn/providers/registry.ts
new file mode 100644
index 00000000..53b66143
--- /dev/null
+++ b/ui/src/earn/providers/registry.ts
@@ -0,0 +1,38 @@
+import type { EarnProviderBase } from './base';
+import type { EarnProviderType } from '../types';
+import { AaveProvider } from '../../3rd-party/earn/aave';
+import { BenqiProvider } from '../../3rd-party/earn/benqi';
+
+/**
+ * Provider registry - maps provider IDs to provider instances
+ */
+const providers: Record = {
+ aave: new AaveProvider(),
+ benqi: new BenqiProvider(),
+};
+
+/**
+ * Get a provider instance by ID
+ */
+export function getProvider(providerId: EarnProviderType): EarnProviderBase {
+ const provider = providers[providerId];
+ if (!provider) {
+ throw new Error(`Provider "${providerId}" not found`);
+ }
+ return provider;
+}
+
+/**
+ * Register a new provider
+ */
+export function registerProvider(providerId: string, provider: EarnProviderBase): void {
+ (providers as any)[providerId] = provider;
+}
+
+/**
+ * Get all registered provider IDs
+ */
+export function getProviderIds(): EarnProviderType[] {
+ return Object.keys(providers) as EarnProviderType[];
+}
+
diff --git a/ui/src/earn/types.ts b/ui/src/earn/types.ts
new file mode 100644
index 00000000..a58cfbbb
--- /dev/null
+++ b/ui/src/earn/types.ts
@@ -0,0 +1,131 @@
+import type { Token } from '../token/types';
+
+/**
+ * Earn provider type (AAVE, Benqi, etc.)
+ */
+export type EarnProviderType = 'aave' | 'benqi';
+
+/**
+ * Pool status
+ */
+export type PoolStatus = 'active' | 'inactive' | 'deprecated';
+
+/**
+ * Earn pool data structure
+ */
+export interface EarnPool {
+ /** Unique pool identifier */
+ id: string;
+ /** Pool name */
+ name: string;
+ /** Underlying token */
+ token: Token;
+ /** Total supply of tokens in the pool */
+ totalSupply: string;
+ /** Pool status */
+ status: PoolStatus;
+ /** Provider (AAVE, Benqi, etc.) */
+ provider: EarnProviderType;
+ /** User's deposited amount */
+ userDeposited?: string;
+ /** User's pending rewards */
+ userRewards?: string;
+ /** Pool contract address */
+ poolAddress: string;
+ /** Reward token */
+ rewardToken?: Token;
+ /** Additional metadata */
+ metadata?: Record;
+}
+
+/**
+ * Earn action type
+ */
+export type EarnAction = 'deposit' | 'withdraw' | 'claim';
+
+/**
+ * Earn status
+ */
+export type EarnStatus = 'idle' | 'loading' | 'success' | 'error';
+
+/**
+ * Earn provider props
+ */
+export interface EarnProviderProps {
+ children: React.ReactNode;
+ /** Initial provider selection */
+ initialProvider?: EarnProviderType;
+ /** Initial chain ID */
+ initialChainId?: string;
+ /** Callback when status changes */
+ onStatusChange?: (status: EarnStatus) => void;
+ /** Callback on successful action */
+ onSuccess?: (result: any) => void;
+ /** Callback on error */
+ onError?: (error: Error) => void;
+}
+
+/**
+ * Earn context type
+ */
+export interface EarnContextType {
+ // Provider selection
+ provider: EarnProviderType;
+ setProvider: (provider: EarnProviderType) => void;
+
+ // Chain selection
+ chainId: string;
+ setChainId: (chainId: string) => void;
+
+ // Pool selection
+ selectedPool: EarnPool | null;
+ setSelectedPool: (pool: EarnPool | null) => void;
+ pools: EarnPool[];
+ isLoadingPools: boolean;
+ refreshPools: () => Promise;
+
+ // View mode
+ viewMode: 'card' | 'list';
+ setViewMode: (mode: 'card' | 'list') => void;
+
+ // Action state
+ action: EarnAction | null;
+ setAction: (action: EarnAction | null) => void;
+
+ // Deposit state
+ depositAmount: string;
+ setDepositAmount: (amount: string) => void;
+ isDepositing: boolean;
+ deposit: () => Promise;
+
+ // Approval state
+ needsApproval: boolean;
+ isApproving: boolean;
+ approveToken: () => Promise;
+ checkApproval: () => Promise;
+
+ // Withdraw state
+ withdrawAmount: string;
+ setWithdrawAmount: (amount: string) => void;
+ isWithdrawing: boolean;
+ withdraw: () => Promise;
+
+ // Claim rewards state
+ isClaiming: boolean;
+ claimRewards: () => Promise;
+
+ // Status and errors
+ status: EarnStatus;
+ error: string | null;
+
+ // Validation
+ isValidForDeposit: boolean;
+ isValidForWithdraw: boolean;
+ validationErrors: string[];
+
+ // Chain switching
+ isOnCorrectChain: boolean;
+ isSwitchingChain: boolean;
+ switchToChain: () => Promise;
+}
+
diff --git a/ui/src/glacier/wallet/useErc20Balances.ts b/ui/src/glacier/wallet/useErc20Balances.ts
new file mode 100644
index 00000000..3964404a
--- /dev/null
+++ b/ui/src/glacier/wallet/useErc20Balances.ts
@@ -0,0 +1,155 @@
+import { useState, useEffect, useCallback, useMemo } from 'react';
+import { useAvalanche } from '../../AvalancheProvider';
+import { useWalletContext } from '../../wallet/hooks/useWalletContext';
+import type { Erc20TokenBalance } from '@avalanche-sdk/chainkit/models/components';
+import { formatUnits } from 'viem';
+
+export type UseErc20BalancesOptions = {
+ /** Wallet address to fetch balances for (defaults to connected wallet address) */
+ address?: string;
+ /** Block number to fetch balances at */
+ blockNumber?: number;
+ /** Specific contract addresses to fetch balances for */
+ contractAddresses?: string[];
+ /** Whether to filter out zero balances (default: true) */
+ filterZeroBalances?: boolean;
+ /** Whether to sort by USD value (default: true) */
+ sortByValue?: boolean;
+ /** Whether to auto-fetch on mount and when dependencies change (default: true) */
+ autoFetch?: boolean;
+};
+
+export type UseErc20BalancesReturn = {
+ /** Array of ERC-20 token balances */
+ balances: Erc20TokenBalance[];
+ /** Whether balances are currently being fetched */
+ loading: boolean;
+ /** Error message if fetch failed */
+ error: string | null;
+ /** Manually trigger a refresh of balances */
+ refresh: () => Promise;
+};
+
+/**
+ * Hook to fetch ERC-20 token balances for a wallet address using ChainKit SDK
+ *
+ * @example
+ * ```tsx
+ * const { balances, loading, error, refresh } = useErc20Balances({
+ * blockNumber: 12345678,
+ * contractAddresses: ['0x...'],
+ * });
+ * ```
+ */
+export function useErc20Balances(
+ options: UseErc20BalancesOptions = {}
+): UseErc20BalancesReturn {
+ const {
+ address: providedAddress,
+ blockNumber,
+ contractAddresses,
+ filterZeroBalances = true,
+ sortByValue = true,
+ autoFetch = true,
+ } = options;
+
+ const { status, address: walletAddress } = useWalletContext();
+ const { chain, chainkit } = useAvalanche();
+ const [balances, setBalances] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Use provided address or fall back to connected wallet address
+ const address = providedAddress || walletAddress;
+ const isConnected = status === 'connected' && !!address;
+
+ // Memoize contract addresses string for stable dependency comparison
+ const contractAddressesStr = useMemo(
+ () => contractAddresses?.join(',') || '',
+ [contractAddresses]
+ );
+
+ const fetchBalances = useCallback(async () => {
+ if (!address || !isConnected || !chainkit) {
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ const result = await chainkit.data.evm.address.balances.listErc20({
+ address: address,
+ chainId: chain.id.toString(),
+ blockNumber: blockNumber?.toString(),
+ contractAddresses: contractAddressesStr || undefined,
+ currency: 'usd',
+ filterSpamTokens: true,
+ pageSize: 100,
+ });
+
+ const allBalances: Erc20TokenBalance[] = [];
+
+ // Iterate through pages (result is already unwrapped by SDK)
+ for await (const page of result) {
+ // Page is ListErc20BalancesResponse after unwrapResultIterator
+ // The SDK returns operations.ListErc20BalancesResponse which wraps components.ListErc20BalancesResponse
+ const balances = page.result?.erc20TokenBalances || [];
+ allBalances.push(...balances);
+ }
+
+ // Filter out zero balances and sort by USD value if available
+ let processedBalances = allBalances;
+
+ if (filterZeroBalances) {
+ processedBalances = processedBalances.filter((b) => {
+ const formattedAmount = formatUnits(BigInt(b.balance), b.decimals);
+ return parseFloat(formattedAmount) > 0;
+ });
+ }
+
+ if (sortByValue) {
+ processedBalances = processedBalances.sort((a, b) => {
+ const aValue = a.balanceValue?.value || 0;
+ const bValue = b.balanceValue?.value || 0;
+ return bValue - aValue;
+ });
+ }
+
+ setBalances(processedBalances);
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Failed to fetch token balances';
+ setError(errorMessage);
+ console.error('Error fetching ERC-20 balances:', err);
+ } finally {
+ setLoading(false);
+ }
+ }, [
+ address,
+ isConnected,
+ chainkit,
+ chain.id,
+ blockNumber,
+ contractAddressesStr,
+ filterZeroBalances,
+ sortByValue,
+ ]);
+
+ useEffect(() => {
+ if (autoFetch) {
+ if (address && isConnected) {
+ fetchBalances();
+ } else {
+ setBalances([]);
+ }
+ }
+ }, [address, isConnected, autoFetch, fetchBalances, chain.id]);
+
+ return {
+ balances,
+ loading,
+ error,
+ refresh: fetchBalances,
+ };
+}
+
diff --git a/ui/src/glacier/wallet/useNativeBalance.ts b/ui/src/glacier/wallet/useNativeBalance.ts
new file mode 100644
index 00000000..8657d796
--- /dev/null
+++ b/ui/src/glacier/wallet/useNativeBalance.ts
@@ -0,0 +1,104 @@
+import { useState, useEffect, useCallback } from 'react';
+import { useAvalanche } from '../../AvalancheProvider';
+import { useWalletContext } from '../../wallet/hooks/useWalletContext';
+import type { NativeTokenBalance } from '@avalanche-sdk/chainkit/models/components';
+
+export type UseNativeBalanceOptions = {
+ /** Wallet address to fetch balance for (defaults to connected wallet address) */
+ address?: string;
+ /** Block number to fetch balance at */
+ blockNumber?: number;
+ /** Whether to auto-fetch on mount and when dependencies change (default: true) */
+ autoFetch?: boolean;
+};
+
+export type UseNativeBalanceReturn = {
+ /** Native token balance data */
+ balance: NativeTokenBalance | null;
+ /** Whether balance is currently being fetched */
+ loading: boolean;
+ /** Error message if fetch failed */
+ error: string | null;
+ /** Manually trigger a refresh of balance */
+ refresh: () => Promise;
+};
+
+/**
+ * Hook to fetch native token balance for a wallet address using ChainKit SDK
+ *
+ * @example
+ * ```tsx
+ * const { balance, loading, error, refresh } = useNativeBalance({
+ * blockNumber: 12345678,
+ * });
+ * ```
+ */
+export function useNativeBalance(
+ options: UseNativeBalanceOptions = {}
+): UseNativeBalanceReturn {
+ const {
+ address: providedAddress,
+ blockNumber,
+ autoFetch = true,
+ } = options;
+
+ const { status, address: walletAddress } = useWalletContext();
+ const { chain, chainkit } = useAvalanche();
+ const [balance, setBalance] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Use provided address or fall back to connected wallet address
+ const address = providedAddress || walletAddress;
+ const isConnected = status === 'connected' && !!address;
+
+ const fetchBalance = useCallback(async () => {
+ if (!address || !isConnected || !chainkit) {
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ const result = await chainkit.data.evm.address.balances.getNative({
+ address: address,
+ chainId: chain.id.toString(),
+ blockNumber: blockNumber?.toString(),
+ currency: 'usd',
+ });
+
+ setBalance(result.nativeTokenBalance);
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Failed to fetch native balance';
+ setError(errorMessage);
+ console.error('Error fetching native balance:', err);
+ } finally {
+ setLoading(false);
+ }
+ }, [
+ address,
+ isConnected,
+ chainkit,
+ chain.id,
+ blockNumber,
+ ]);
+
+ useEffect(() => {
+ if (autoFetch) {
+ if (address && isConnected) {
+ fetchBalance();
+ } else {
+ setBalance(null);
+ }
+ }
+ }, [address, isConnected, autoFetch, fetchBalance, chain.id]);
+
+ return {
+ balance,
+ loading,
+ error,
+ refresh: fetchBalance,
+ };
+}
+
diff --git a/ui/src/glacier/wallet/useTransactions.ts b/ui/src/glacier/wallet/useTransactions.ts
new file mode 100644
index 00000000..0ddf35d2
--- /dev/null
+++ b/ui/src/glacier/wallet/useTransactions.ts
@@ -0,0 +1,139 @@
+import { useState, useEffect, useCallback } from 'react';
+import { useAvalanche } from '../../AvalancheProvider';
+import { useWalletContext } from '../../wallet/hooks/useWalletContext';
+import type { TransactionDetails } from '@avalanche-sdk/chainkit/models/components';
+
+export type UseTransactionsOptions = {
+ /** Wallet address to fetch transactions for (defaults to connected wallet address) */
+ address?: string;
+ /** Start block number for filtering */
+ startBlock?: number;
+ /** End block number for filtering */
+ endBlock?: number;
+ /** Sort order: 'asc' or 'desc' (default: 'desc') */
+ sortOrder?: 'asc' | 'desc';
+ /** Maximum number of transactions to fetch (default: 50) */
+ maxItems?: number;
+ /** Whether to auto-fetch on mount and when dependencies change (default: true) */
+ autoFetch?: boolean;
+};
+
+export type UseTransactionsReturn = {
+ /** Array of transaction details */
+ transactions: TransactionDetails[];
+ /** Whether transactions are currently being fetched */
+ loading: boolean;
+ /** Error message if fetch failed */
+ error: string | null;
+ /** Manually trigger a refresh of transactions */
+ refresh: () => Promise;
+};
+
+/**
+ * Hook to fetch transaction history for a wallet address using ChainKit SDK
+ *
+ * @example
+ * ```tsx
+ * const { transactions, loading, error, refresh } = useTransactions({
+ * startBlock: 12345678,
+ * endBlock: 12345900,
+ * sortOrder: 'desc',
+ * });
+ * ```
+ */
+export function useTransactions(
+ options: UseTransactionsOptions = {}
+): UseTransactionsReturn {
+ const {
+ address: providedAddress,
+ startBlock,
+ endBlock,
+ sortOrder = 'desc',
+ maxItems = 50,
+ autoFetch = true,
+ } = options;
+
+ const { status, address: walletAddress } = useWalletContext();
+ const { chain, chainkit } = useAvalanche();
+ const [transactions, setTransactions] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Use provided address or fall back to connected wallet address
+ const address = providedAddress || walletAddress;
+ const isConnected = status === 'connected' && !!address;
+
+ const fetchTransactions = useCallback(async () => {
+ if (!address || !isConnected || !chainkit) {
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ const result = await chainkit.data.evm.address.transactions.list({
+ address: address,
+ chainId: chain.id.toString(),
+ startBlock: startBlock,
+ endBlock: endBlock,
+ sortOrder: sortOrder,
+ pageSize: 100,
+ });
+
+ const allTransactions: TransactionDetails[] = [];
+
+ // Iterate through pages (result is already unwrapped by SDK)
+ for await (const page of result) {
+ // Page is ListTransactionsResponse after unwrapResultIterator
+ const pageTransactions = page.result?.transactions || [];
+ allTransactions.push(...pageTransactions);
+
+ // Stop if we've reached maxItems
+ if (maxItems && allTransactions.length >= maxItems) {
+ break;
+ }
+ }
+
+ // Limit to maxItems if specified
+ const limitedTransactions = maxItems
+ ? allTransactions.slice(0, maxItems)
+ : allTransactions;
+
+ setTransactions(limitedTransactions);
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Failed to fetch transactions';
+ setError(errorMessage);
+ console.error('Error fetching transactions:', err);
+ } finally {
+ setLoading(false);
+ }
+ }, [
+ address,
+ isConnected,
+ chainkit,
+ chain.id,
+ startBlock,
+ endBlock,
+ sortOrder,
+ maxItems,
+ ]);
+
+ useEffect(() => {
+ if (autoFetch) {
+ if (address && isConnected) {
+ fetchTransactions();
+ } else {
+ setTransactions([]);
+ }
+ }
+ }, [address, isConnected, autoFetch, fetchTransactions, chain.id]);
+
+ return {
+ transactions,
+ loading,
+ error,
+ refresh: fetchTransactions,
+ };
+}
+
diff --git a/ui/src/hooks/index.ts b/ui/src/hooks/index.ts
new file mode 100644
index 00000000..780b7913
--- /dev/null
+++ b/ui/src/hooks/index.ts
@@ -0,0 +1 @@
+export { useSwitchChain } from './useSwitchChain';
diff --git a/ui/src/hooks/useSwitchChain.ts b/ui/src/hooks/useSwitchChain.ts
new file mode 100644
index 00000000..619d9bb7
--- /dev/null
+++ b/ui/src/hooks/useSwitchChain.ts
@@ -0,0 +1,52 @@
+'use client';
+import { useCallback, useState } from 'react';
+import { useAvalanche } from '../AvalancheProvider';
+import type { Chain } from '@avalanche-sdk/client/chains';
+
+export interface SwitchChainOptions {
+ onSuccess?: (chain: Chain) => void;
+ onError?: (error: Error) => void;
+}
+
+export function useSwitchChain(options: SwitchChainOptions = {}) {
+ const { chain: currentChain, switchChain: switchChainFromProvider } = useAvalanche();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const switchChain = useCallback(async (targetChain: Chain) => {
+ if (!targetChain) {
+ const error = new Error('Target chain is required');
+ setError(error);
+ options.onError?.(error);
+ return;
+ }
+
+ if (currentChain.id === targetChain.id) {
+ // Already on the target chain
+ options.onSuccess?.(targetChain);
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ // Use the centralized switch chain function from AvalancheProvider
+ await switchChainFromProvider(targetChain);
+ options.onSuccess?.(targetChain);
+ } catch (switchError: any) {
+ const error = new Error(switchError.message || 'Failed to switch chain');
+ setError(error);
+ options.onError?.(error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [currentChain, switchChainFromProvider, options]);
+
+ return {
+ switchChain,
+ isLoading,
+ error,
+ currentChain,
+ };
+}
diff --git a/ui/src/ictt/components/ICTT.tsx b/ui/src/ictt/components/ICTT.tsx
new file mode 100644
index 00000000..0dc5a1da
--- /dev/null
+++ b/ui/src/ictt/components/ICTT.tsx
@@ -0,0 +1,67 @@
+'use client';
+import React from 'react';
+import { cn } from '../../styles/theme';
+import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
+import { WalletConnectionOverlay } from '../../components/ui/wallet-connection-overlay';
+import { ICTTProvider } from './ICTTProvider';
+import { ICTTChainSelector } from './ICTTChainSelector';
+import { ICTTToggleButton } from './ICTTToggleButton';
+import { ICTTTokenModeToggle } from './ICTTTokenModeToggle';
+import { ICTTAmountInput } from './ICTTAmountInput';
+import { ICTTAddressInput } from './ICTTAddressInput';
+import { ICTTButtons } from './ICTTButtons';
+import type { ICTTProviderProps } from '../types';
+
+type ICTTProps = {
+ children?: React.ReactNode;
+ className?: string;
+ title?: string;
+ allowManualMode?: boolean;
+} & Omit;
+
+function ICTTContent({ allowManualMode }: { allowManualMode?: boolean }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function ICTT({
+ children,
+ className,
+ title = "Interchain Token Transfer",
+ allowManualMode,
+ ...providerProps
+}: ICTTProps) {
+ return (
+
+
+
+
+ {title}
+
+
+
+
+ {children || }
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui/src/ictt/components/ICTTAddressInput.tsx b/ui/src/ictt/components/ICTTAddressInput.tsx
new file mode 100644
index 00000000..17d23de8
--- /dev/null
+++ b/ui/src/ictt/components/ICTTAddressInput.tsx
@@ -0,0 +1,98 @@
+'use client';
+import React from 'react';
+import { cn, text } from '../../styles/theme';
+import { Input } from '../../components/ui/input';
+import { Button } from '../../components/ui/button';
+import { Label } from '../../components/ui/label';
+import { useICTTContext } from './ICTTProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+import { validateAddress } from '../../utils/addressValidation';
+
+export interface ICTTAddressInputProps {
+ className?: string;
+}
+
+export function ICTTAddressInput({ className }: ICTTAddressInputProps) {
+ const {
+ recipientAddress,
+ setRecipientAddress,
+ toChain
+ } = useICTTContext();
+
+ const { walletAddress, isWalletConnected, availableChains } = useAvalanche();
+
+ const getChainName = (chainId: string): string => {
+ const chain = availableChains.find(c => c.id.toString() === chainId);
+ return chain?.name || 'Chain';
+ };
+
+ const handleAddressChange = (e: React.ChangeEvent) => {
+ setRecipientAddress(e.target.value);
+ };
+
+ const handleUseMyAddress = () => {
+ if (walletAddress) {
+ setRecipientAddress(walletAddress);
+ }
+ };
+
+ // Validate the current address
+ const validation = validateAddress(recipientAddress, 'C');
+ const shouldShowError = recipientAddress && !validation.isValid;
+
+ return (
+
+
+ Recipient Address
+
+ ({getChainName(toChain)})
+
+
+
+
+
+
+ {/* Use My Address Button */}
+ {isWalletConnected && walletAddress && (
+
+ Use My Address
+
+ )}
+
+ {/* Validation indicator - only show red circle for invalid addresses */}
+ {recipientAddress && !validation.isValid && (
+
+ )}
+
+
+ {/* Error message */}
+ {shouldShowError && (
+
+ {validation.error}
+
+ )}
+
+ {!isWalletConnected && (
+
+ Connect wallet to use your address
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/ui/src/ictt/components/ICTTAmountInput.tsx b/ui/src/ictt/components/ICTTAmountInput.tsx
new file mode 100644
index 00000000..551b1697
--- /dev/null
+++ b/ui/src/ictt/components/ICTTAmountInput.tsx
@@ -0,0 +1,109 @@
+'use client';
+import { useState, useEffect } from 'react';
+import { cn } from '../../styles/theme';
+import { AmountInput } from '../../components/ui/amount-input';
+import { useICTTContext } from './ICTTProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+
+export interface ICTTAmountInputProps {
+ className?: string;
+}
+
+export function ICTTAmountInput({ className }: ICTTAmountInputProps) {
+ const {
+ amount,
+ setAmount,
+ selectedToken,
+ fromChain,
+ areTokensValid
+ } = useICTTContext();
+
+ const { walletAddress, isWalletConnected, availableChains } = useAvalanche();
+ const [tokenBalance, setTokenBalance] = useState(null);
+
+ // Fetch token balance when wallet is connected and token is selected
+ useEffect(() => {
+ const fetchTokenBalance = async () => {
+ if (!selectedToken || !walletAddress || !isWalletConnected) {
+ setTokenBalance(null);
+ return;
+ }
+
+ try {
+ // Import viem for RPC calls
+ const { createPublicClient, http, formatUnits } = await import('viem');
+
+ // Find the chain data from availableChains
+ const chainData = availableChains.find(c => c.id.toString() === fromChain);
+ if (!chainData) {
+ throw new Error(`Chain ${fromChain} not found in available chains`);
+ }
+
+ // Import ERC20 ABI for balance fetching
+ const { ERC20_BALANCE_ABI } = await import('../../utils/erc20');
+
+ const publicClient = createPublicClient({
+ chain: chainData,
+ transport: http(),
+ });
+
+ // Fetch balance and decimals
+ const [balance, decimals] = await Promise.all([
+ publicClient.readContract({
+ address: selectedToken.address as `0x${string}`,
+ abi: ERC20_BALANCE_ABI,
+ functionName: 'balanceOf',
+ args: [walletAddress as `0x${string}`],
+ }),
+ publicClient.readContract({
+ address: selectedToken.address as `0x${string}`,
+ abi: ERC20_BALANCE_ABI,
+ functionName: 'decimals',
+ }),
+ ]);
+
+ // Format balance using token decimals
+ const formattedBalance = formatUnits(balance as bigint, decimals as number);
+
+ // Format for display (add commas for thousands)
+ const numericBalance = parseFloat(formattedBalance);
+ const displayBalance = numericBalance.toLocaleString(undefined, {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 6,
+ });
+
+ setTokenBalance(displayBalance);
+ } catch (error) {
+ console.error('Failed to fetch token balance:', error);
+ setTokenBalance('0.00');
+ }
+ };
+
+ fetchTokenBalance();
+ }, [selectedToken, walletAddress, isWalletConnected, fromChain]);
+
+ const handleMaxClick = () => {
+ if (tokenBalance && tokenBalance !== '0.00') {
+ // Remove commas for input value
+ const numericBalance = tokenBalance.replace(/,/g, '');
+ setAmount(numericBalance);
+ }
+ };
+
+ return (
+
+
setAmount(e.target.value)}
+ symbol={selectedToken?.symbol || 'TOKEN'}
+ placeholder="0.00"
+ disabled={!areTokensValid}
+ showMax={!!tokenBalance && tokenBalance !== '0.00' && isWalletConnected && areTokensValid}
+ maxValue={tokenBalance?.replace(/,/g, '') || '0'}
+ onMaxClick={handleMaxClick}
+ showBalance={!!tokenBalance && isWalletConnected}
+ />
+
+ );
+}
diff --git a/ui/src/ictt/components/ICTTButtons.tsx b/ui/src/ictt/components/ICTTButtons.tsx
new file mode 100644
index 00000000..b906d168
--- /dev/null
+++ b/ui/src/ictt/components/ICTTButtons.tsx
@@ -0,0 +1,135 @@
+'use client';
+import { cn } from '../../styles/theme';
+import { Button } from '../../components/ui/button';
+import { LoaderCircle, Send, CheckCircle, RefreshCw } from 'lucide-react';
+import { useICTTContext } from './ICTTProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+
+export interface ICTTButtonsProps {
+ className?: string;
+}
+
+export function ICTTButtons({ className }: ICTTButtonsProps) {
+ const {
+ approveToken,
+ sendToken,
+ isApproving,
+ isSending,
+ isApproved,
+ isCheckingAllowance,
+ isValidForApproval,
+ isValidForSending,
+ validationErrors,
+ isOnCorrectChain,
+ isSwitchingChain,
+ switchToSourceChain,
+ fromChain,
+ selectedToken,
+ amount,
+ } = useICTTContext();
+
+ const { availableChains } = useAvalanche();
+
+ const getChainName = (chainId: string) => {
+ const chain = availableChains.find(c => c.id.toString() === chainId);
+ return chain?.name || `Chain ${chainId}`;
+ };
+
+ const getSwitchChainButtonText = () => {
+ if (isSwitchingChain) return 'Switching Chain...';
+ return `Switch to ${getChainName(fromChain)}`;
+ };
+
+ const getApproveButtonText = () => {
+ if (isApproving) return 'Approving...';
+ if (isCheckingAllowance) return 'Checking Allowance...';
+ if (isApproved) return 'Approved';
+ if (!isValidForApproval && validationErrors.length > 0) {
+ return validationErrors[0];
+ }
+
+ // Show amount and token symbol if available
+ if (selectedToken && amount) {
+ return `Approve ${amount} ${selectedToken.symbol}`;
+ }
+
+ return 'Approve Token';
+ };
+
+ const getSendButtonText = () => {
+ if (isSending) return 'Sending...';
+ if (!isApproved) return 'Approve First';
+ if (!isValidForSending && validationErrors.length > 0) {
+ return validationErrors[0];
+ }
+
+ // Show amount and token symbol if available
+ if (selectedToken && amount) {
+ return `Send ${amount} ${selectedToken.symbol}`;
+ }
+
+ return 'Send Token';
+ };
+
+ return (
+
+ {/* Switch Chain Button - only show when not on correct chain */}
+ {!isOnCorrectChain && (
+
+ {isSwitchingChain ? (
+
+ ) : (
+
+ )}
+ {getSwitchChainButtonText()}
+
+ )}
+
+ {/* Approve Button */}
+
+ {isApproving || isCheckingAllowance ? (
+
+ ) : isApproved ? (
+
+ ) : (
+
+ )}
+ {getApproveButtonText()}
+
+
+ {/* Send Button */}
+
+ {isSending ? (
+
+ ) : (
+
+ )}
+ {getSendButtonText()}
+
+
+ {/* Chain Status Info */}
+ {!isOnCorrectChain && (
+
+ Please switch to {getChainName(fromChain)} to continue
+
+ )}
+
+ );
+}
diff --git a/ui/src/ictt/components/ICTTChainSelector.tsx b/ui/src/ictt/components/ICTTChainSelector.tsx
new file mode 100644
index 00000000..39017b9a
--- /dev/null
+++ b/ui/src/ictt/components/ICTTChainSelector.tsx
@@ -0,0 +1,78 @@
+'use client';
+import { useMemo } from 'react';
+import { ChainSelectDropdown, ChainLogo, type ChainOption } from '../../chain';
+import { useICTTContext } from './ICTTProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+import type { Chain as SDKChain } from '@avalanche-sdk/client/chains';
+
+function mapSDKChainToICTTChain(sdkChain: SDKChain): ChainOption {
+ // Use chain ID as the ICTT chain identifier
+ const chainId = sdkChain.id.toString();
+
+ // Generate description based on testnet flag
+ const description = sdkChain.testnet ? 'Testnet' : 'Mainnet';
+
+ // Generate color based on chain ID (simple hash-based color)
+ const colors = [
+ 'bg-blue-500', 'bg-green-500', 'bg-purple-500', 'bg-orange-500',
+ 'bg-red-500', 'bg-teal-500', 'bg-pink-500', 'bg-indigo-500'
+ ];
+ const colorIndex = sdkChain.id % colors.length;
+
+ // Create chain data with iconUrl for ChainLogo
+ const chainData = {
+ id: chainId,
+ name: sdkChain.name,
+ iconUrl: (sdkChain as any).iconUrl, // SDK Chain type might not have iconUrl in types but could exist at runtime
+ testnet: sdkChain.testnet || false
+ };
+
+ return {
+ id: chainId,
+ name: sdkChain.name,
+ description,
+ color: colors[colorIndex],
+ icon:
+ };
+}
+
+export interface ICTTChainSelectorProps {
+ className?: string;
+ type: 'from' | 'to';
+}
+
+export function ICTTChainSelector({ className, type }: ICTTChainSelectorProps) {
+ const {
+ fromChain,
+ toChain,
+ setFromChain,
+ setToChain,
+ } = useICTTContext();
+
+ const { availableChains } = useAvalanche();
+
+ // Convert all SDK chains to ICTT-compatible chains
+ const icttChains = useMemo(() => {
+ return availableChains.map(mapSDKChainToICTTChain);
+ }, [availableChains]);
+
+ const currentChain = type === 'from' ? fromChain : toChain;
+ const setChain = type === 'from' ? setFromChain : setToChain;
+ const otherChain = type === 'from' ? toChain : fromChain;
+
+ const handleChainChange = (chainId: string) => {
+ setChain(chainId);
+ };
+
+ return (
+ handleChainChange(chainId)}
+ label={type === 'from' ? 'Source Chain' : 'Destination Chain'}
+ disabledOptions={[otherChain]}
+ className={className}
+ data-testid={`ictt-chain-selector-${type}`}
+ />
+ );
+}
\ No newline at end of file
diff --git a/ui/src/ictt/components/ICTTHomeTokenAddressInput.tsx b/ui/src/ictt/components/ICTTHomeTokenAddressInput.tsx
new file mode 100644
index 00000000..c9e08ba0
--- /dev/null
+++ b/ui/src/ictt/components/ICTTHomeTokenAddressInput.tsx
@@ -0,0 +1,336 @@
+'use client';
+import { useState, useCallback, useEffect } from 'react';
+import { cn } from '../../styles/theme';
+import { TokenImage } from '../../token';
+import { XCircle, Loader2 } from 'lucide-react';
+import { useICTTContext } from './ICTTProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+import { isAddress, formatUnits, createPublicClient, http } from 'viem';
+import { ERC20_ABI } from '../../utils/erc20';
+
+interface TokenInfo {
+ name: string;
+ symbol: string;
+ decimals: number;
+ address: string;
+}
+
+interface TokenValidationState {
+ isValidating: boolean;
+ isValid: boolean;
+ error?: string;
+ tokenInfo?: TokenInfo;
+}
+
+export interface ICTTHomeTokenAddressInputProps {
+ className?: string;
+ label?: string;
+}
+
+export function ICTTHomeTokenAddressInput({
+ className,
+ label = "ICTT Home Contract Address"
+}: ICTTHomeTokenAddressInputProps) {
+ const { fromChain, setSelectedToken, setTokenHomeContract } = useICTTContext();
+ const { availableChains, walletAddress, isWalletConnected } = useAvalanche();
+ const [homeContractAddress, setHomeContractAddress] = useState('');
+ const [tokenValidation, setTokenValidation] = useState({
+ isValidating: false,
+ isValid: false,
+ });
+ const [tokenBalance, setTokenBalance] = useState(null);
+
+ // Get chain config and create public client
+ const getPublicClient = useCallback((chainId: string) => {
+ const chainData = availableChains.find(c => c.id.toString() === chainId);
+
+ if (!chainData) {
+ throw new Error(`Chain ${chainId} not found in availableChains`);
+ }
+
+ const rpcUrl = chainData.rpcUrls?.default?.http?.[0];
+ if (!rpcUrl) {
+ throw new Error(`No RPC URL found for chain ${chainId}. Please ensure the chain is properly configured in AvalancheProvider.`);
+ }
+
+ return createPublicClient({
+ chain: chainData as any, // ChainConfig should be compatible with viem Chain
+ transport: http(rpcUrl),
+ });
+ }, [availableChains]);
+
+
+ // Get token address from ICTT home contract
+ const getTokenAddressFromHome = useCallback(async (homeContract: string): Promise => {
+ // Call getTokenAddress() function on the home contract
+ // This is a custom function on ICTT home contracts, not part of ERC20
+ // So we use raw RPC call instead of viem's readContract
+ const rpcUrl = availableChains.find(c => c.id.toString() === fromChain)?.rpcUrls?.default?.http?.[0];
+ if (!rpcUrl) {
+ throw new Error(`No RPC URL found for chain ${fromChain}`);
+ }
+
+ const response = await fetch(rpcUrl, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_call',
+ params: [
+ {
+ to: homeContract,
+ data: '0x10fe9ae8', // getTokenAddress() function selector
+ },
+ 'latest'
+ ],
+ id: 1,
+ }),
+ });
+
+ const result = await response.json();
+
+ if (result.error || !result.result || result.result === '0x') {
+ throw new Error('Failed to get token address from home contract');
+ }
+
+ // Decode the address from the result (last 20 bytes)
+ const tokenAddress = '0x' + result.result.slice(-40);
+
+ if (!isAddress(tokenAddress)) {
+ throw new Error('Invalid token address returned from home contract');
+ }
+
+ return tokenAddress;
+ }, [fromChain, availableChains]);
+
+ // Validate token contract using ERC20 interface
+ const validateTokenContract = useCallback(async (tokenAddress: string) => {
+ const publicClient = getPublicClient(fromChain);
+
+ // Read token metadata using ERC20 ABI
+ const [name, symbol, decimals] = await Promise.all([
+ publicClient.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: 'name',
+ }) as Promise,
+ publicClient.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: 'symbol',
+ }) as Promise,
+ publicClient.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: 'decimals',
+ }) as Promise,
+ ]);
+
+ if (!name || !symbol || isNaN(decimals)) {
+ throw new Error('Invalid token contract data');
+ }
+
+ // Read balance if wallet is connected
+ if (walletAddress && isWalletConnected) {
+ try {
+ const balance = await publicClient.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: 'balanceOf',
+ args: [walletAddress as `0x${string}`],
+ }) as bigint;
+
+ const formattedBalance = formatUnits(balance, decimals);
+ const numericBalance = parseFloat(formattedBalance);
+ const displayBalance = numericBalance.toLocaleString(undefined, {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 6,
+ });
+ setTokenBalance(displayBalance);
+ } catch {
+ setTokenBalance('0');
+ }
+ } else {
+ setTokenBalance(null);
+ }
+
+ return {
+ name,
+ symbol,
+ decimals,
+ address: tokenAddress,
+ };
+ }, [fromChain, getPublicClient, walletAddress, isWalletConnected]);
+
+ // Validate ICTT home contract and get token info
+ const validateHomeContract = useCallback(async (homeContractAddress: string) => {
+ if (!homeContractAddress || !isAddress(homeContractAddress)) {
+ setTokenValidation({
+ isValidating: false,
+ isValid: false,
+ error: 'Invalid home contract address format',
+ });
+ return;
+ }
+
+ setTokenValidation({ isValidating: true, isValid: false });
+ setTokenBalance(null);
+
+ try {
+ // Step 1: Get token address from home contract
+ const tokenAddress = await getTokenAddressFromHome(homeContractAddress);
+
+ // Step 2: Validate the token contract
+ const tokenInfo = await validateTokenContract(tokenAddress);
+
+ setTokenValidation({
+ isValidating: false,
+ isValid: true,
+ tokenInfo,
+ });
+
+ // Update the selected token and home contract in the context
+ // For manual mode, we just need a simple token structure (not ICTT token)
+ setSelectedToken({
+ address: tokenInfo.address,
+ name: tokenInfo.name,
+ symbol: tokenInfo.symbol,
+ decimals: tokenInfo.decimals,
+ chainId: fromChain,
+ // No ictt field - this is a regular ERC20 token
+ });
+
+ // Set the home contract address in the context
+ setTokenHomeContract(homeContractAddress);
+
+ } catch (error) {
+ setTokenValidation({
+ isValidating: false,
+ isValid: false,
+ error: error instanceof Error ? error.message : 'Failed to validate home contract',
+ });
+ setSelectedToken(null);
+ setTokenHomeContract(null);
+ }
+ }, [getTokenAddressFromHome, validateTokenContract, setSelectedToken, setTokenHomeContract, fromChain]);
+
+ // Handle address change with debouncing
+ useEffect(() => {
+ if (!homeContractAddress) {
+ setTokenValidation({ isValidating: false, isValid: false });
+ setSelectedToken(null);
+ setTokenHomeContract(null);
+ return;
+ }
+
+ const timeoutId = setTimeout(() => {
+ validateHomeContract(homeContractAddress);
+ }, 500); // 500ms debounce
+
+ return () => clearTimeout(timeoutId);
+ }, [homeContractAddress, validateHomeContract, setSelectedToken, setTokenHomeContract]);
+
+ return (
+
+ {/* Label */}
+
+ {label}
+
+
+ {/* Input Container with Token Display */}
+
+
+ {/* Home Contract Address Input */}
+
+ setHomeContractAddress(e.target.value)}
+ placeholder="0x... (ICTT Home Contract)"
+ className={cn(
+ "flex h-12 w-full border border-input bg-background px-3 py-2 text-base",
+ "file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground",
+ "focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 font-mono",
+ "rounded-l-md border-r-0", // Left side rounded, no right border
+ tokenValidation.isValid && "border-green-500",
+ tokenValidation.error && "border-destructive"
+ )}
+ />
+
+
+ {/* Token Display - looks like part of input */}
+
+ {/* Left Divider */}
+
+ {tokenValidation.isValid && tokenValidation.tokenInfo ? (
+
+
+
+
+ {tokenValidation.tokenInfo.name}
+
+
+ {tokenValidation.tokenInfo.symbol}
+
+
+
+ ) : homeContractAddress && tokenValidation.isValidating ? (
+
+
+ Validating...
+
+ ) : null}
+
+
+
+ {/* Validation Status Below */}
+ {homeContractAddress && (
+
+ {tokenValidation.isValidating ? (
+ <>
+
+ Validating ICTT home contract...
+ >
+ ) : tokenValidation.error ? (
+ <>
+
+ {tokenValidation.error}
+ >
+ ) : null}
+
+ )}
+
+ {/* Token Balance */}
+ {tokenValidation.isValid && tokenValidation.tokenInfo && (
+
+ {isWalletConnected ? (
+ tokenBalance !== null ? (
+ <>Balance: {tokenBalance} {tokenValidation.tokenInfo.symbol}>
+ ) : (
+ 'Connect wallet to see balance'
+ )
+ ) : (
+ 'Connect wallet to see balance'
+ )}
+
+ )}
+
+
+
+ );
+}
diff --git a/ui/src/ictt/components/ICTTProvider.tsx b/ui/src/ictt/components/ICTTProvider.tsx
new file mode 100644
index 00000000..a4c18d71
--- /dev/null
+++ b/ui/src/ictt/components/ICTTProvider.tsx
@@ -0,0 +1,470 @@
+'use client';
+import { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';
+import { useAvalanche } from '../../AvalancheProvider';
+import { createICTTClient } from '@avalanche-sdk/interchain/ictt';
+
+import type {
+ ICTTToken,
+ ICTTTokenMirror,
+ ICTTStatus,
+ ICTTProviderProps,
+ ICTTContextType
+} from '../types';
+
+const ICTTContext = createContext(undefined);
+
+export function ICTTProvider({
+ children,
+ initialFromChain,
+ initialToChain,
+ onStatusChange,
+ onSuccess,
+ onError,
+}: ICTTProviderProps) {
+ const { availableChains, wellKnownTokens, walletClient, walletChainId, switchChain } = useAvalanche();
+
+ // Get default chains from available chains
+ const defaultFromChain = initialFromChain || availableChains[0]?.id.toString() || '1';
+ const defaultToChain = initialToChain || availableChains[1]?.id.toString() || availableChains[0]?.id.toString() || '43113';
+
+ // Chain selection
+ const [fromChain, setFromChain] = useState(defaultFromChain);
+ const [toChain, setToChain] = useState(defaultToChain);
+
+ // Token selection
+ const [selectedToken, setSelectedToken] = useState(null);
+
+ // Auto-set home and remote contracts when a well-known token is selected
+ useEffect(() => {
+ if (selectedToken && isICTTToken(selectedToken) && selectedToken.ictt) {
+ // Set the home contract from the selected token
+ setTokenHomeContract(selectedToken.ictt.home);
+
+ // Find the remote contract for the destination chain
+ const remoteMirror = selectedToken.ictt.mirrors.find((mirror: ICTTTokenMirror) => mirror.chainId === toChain);
+ if (remoteMirror) {
+ setTokenRemoteContract(remoteMirror.address);
+ } else {
+ // If no mirror found for the destination chain, clear the remote contract
+ setTokenRemoteContract(null);
+ }
+ } else {
+ // If no token selected or token doesn't have ICTT data, clear contracts
+ // (but only if they weren't manually set via the home/remote inputs)
+ // We'll keep them as they might be manually entered
+ }
+ }, [selectedToken, toChain]);
+
+ // Contract addresses for Home mode
+ const [tokenHomeContract, setTokenHomeContract] = useState(null);
+ const [tokenRemoteContract, setTokenRemoteContract] = useState(null);
+
+ // Transfer details
+ const [amount, setAmount] = useState('');
+ const [recipientAddress, setRecipientAddress] = useState('');
+
+ // Chain switching state
+ const [isOnCorrectChain, setIsOnCorrectChain] = useState(true);
+ const [isSwitchingChain, setIsSwitchingChain] = useState(false);
+
+ // Allowance and approval state
+ const [allowance, setAllowance] = useState(null);
+ const [isApproved, setIsApproved] = useState(false);
+ const [isCheckingAllowance, setIsCheckingAllowance] = useState(false);
+
+ // Status
+ const [status, setStatus] = useState('idle');
+ const [isApproving, setIsApproving] = useState(false);
+ const [isSending, setIsSending] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Swap chains
+ const swapChains = useCallback(() => {
+ const tempChain = fromChain;
+ setFromChain(toChain);
+ setToChain(tempChain);
+ }, [fromChain, toChain]);
+
+ // Switch to source chain
+ const switchToSourceChain = useCallback(async () => {
+ const sourceChainData = availableChains.find(chain => chain.id.toString() === fromChain);
+
+ if (!sourceChainData) {
+ setError('Source chain not found');
+ console.error('Source chain not found:', fromChain, 'Available chains:', availableChains);
+ return;
+ }
+
+ if (!switchChain) {
+ setError('Chain switching not available');
+ console.error('switchChain function not available');
+ return;
+ }
+
+ setIsSwitchingChain(true);
+ setError(null);
+
+ try {
+ console.log('Switching to chain:', sourceChainData);
+ await switchChain(sourceChainData);
+ console.log('Chain switch successful');
+ } catch (error) {
+ console.error('Chain switch failed:', error);
+ setError(error instanceof Error ? error.message : 'Failed to switch chain');
+ } finally {
+ setIsSwitchingChain(false);
+ }
+ }, [fromChain, availableChains, switchChain]);
+
+ // Check token allowance
+ const checkAllowance = useCallback(async () => {
+ if (!selectedToken || !amount || !tokenHomeContract || !walletClient) {
+ setAllowance(null);
+ setIsApproved(false);
+ return;
+ }
+
+ setIsCheckingAllowance(true);
+ setError(null);
+
+ try {
+ // Find the source chain
+ const sourceChainData = availableChains.find(chain => chain.id.toString() === fromChain);
+ if (!sourceChainData) {
+ throw new Error('Source chain not found');
+ }
+
+ // Create public client for reading allowance
+ const { createPublicClient, http } = await import('viem');
+ const { ERC20_APPROVAL_ABI } = await import('../../utils/erc20');
+
+ const publicClient = createPublicClient({
+ chain: sourceChainData,
+ transport: http(),
+ });
+
+ // Check allowance using ERC20 allowance function
+ const allowanceResult = await publicClient.readContract({
+ address: selectedToken.address as `0x${string}`,
+ abi: ERC20_APPROVAL_ABI,
+ functionName: 'allowance',
+ args: [walletClient.account?.address as `0x${string}`, tokenHomeContract as `0x${string}`],
+ });
+
+ const allowanceAmount = allowanceResult.toString();
+ const requiredAmount = (parseFloat(amount) * Math.pow(10, selectedToken.decimals)).toString();
+
+ setAllowance(allowanceAmount);
+ setIsApproved(BigInt(allowanceAmount) >= BigInt(requiredAmount));
+
+ } catch (error) {
+ console.error('Failed to check allowance:', error);
+ setAllowance(null);
+ setIsApproved(false);
+ setError(error instanceof Error ? error.message : 'Failed to check allowance');
+ } finally {
+ setIsCheckingAllowance(false);
+ }
+ }, [selectedToken, amount, tokenHomeContract, walletClient, fromChain, availableChains]);
+
+ // Approve token
+ const approveToken = useCallback(async () => {
+ if (!selectedToken || !amount || !tokenHomeContract) {
+ setError('Please fill in all required fields for approval');
+ return;
+ }
+
+ setIsApproving(true);
+ setStatus('loading');
+ setError(null);
+
+ try {
+ // Find the source chain
+ const sourceChainData = availableChains.find(chain => chain.id.toString() === fromChain);
+ if (!sourceChainData) {
+ throw new Error('Source chain not found');
+ }
+
+ if (!walletClient) {
+ throw new Error('Wallet not connected');
+ }
+
+ // Parse amount to base units (approve double the amount for safety)
+ const amountInBaseUnit = parseFloat(amount);
+
+ const icttClient = createICTTClient(sourceChainData);
+
+ console.log('📝 Approving token...', {
+ sourceChain: sourceChainData.name,
+ tokenHomeContract: tokenHomeContract,
+ tokenAddress: selectedToken.address,
+ amountInBaseUnit,
+ });
+
+ const approveResult = await icttClient.approveToken({
+ walletClient,
+ sourceChain: sourceChainData,
+ tokenHomeContract: tokenHomeContract as `0x${string}`,
+ tokenAddress: selectedToken.address as `0x${string}`,
+ amountInBaseUnit,
+ });
+
+ console.log('✅ Token approved:', approveResult.txHash);
+
+ // Check allowance after approval
+ await checkAllowance();
+
+ setStatus('idle');
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Approval failed';
+ setError(errorMessage);
+ setStatus('error');
+ onError?.(new Error(errorMessage));
+ } finally {
+ setIsApproving(false);
+ }
+ }, [selectedToken, amount, tokenHomeContract, fromChain, availableChains, walletClient, onError, checkAllowance]);
+
+ // Send token
+ const sendToken = useCallback(async () => {
+ if (!selectedToken || !amount || !recipientAddress || !tokenHomeContract || !tokenRemoteContract) {
+ setError('Please fill in all required fields for sending');
+ return;
+ }
+
+ setIsSending(true);
+ setStatus('loading');
+ setError(null);
+
+ try {
+ // Find the source and destination chains
+ const sourceChainData = availableChains.find(chain => chain.id.toString() === fromChain);
+ const destinationChainData = availableChains.find(chain => chain.id.toString() === toChain);
+
+ if (!sourceChainData || !destinationChainData) {
+ throw new Error('Source or destination chain not found');
+ }
+
+ if (!walletClient) {
+ throw new Error('Wallet not connected');
+ }
+
+ // Parse amount to base units
+ const amountInBaseUnit = parseFloat(amount);
+
+ const icttClient = createICTTClient(sourceChainData, destinationChainData);
+
+ console.log('📝 Sending token...', {
+ sourceChain: sourceChainData.name,
+ destinationChain: destinationChainData.name,
+ tokenHomeContract: tokenHomeContract,
+ tokenRemoteContract: tokenRemoteContract,
+ recipient: recipientAddress,
+ amountInBaseUnit,
+ });
+
+ const sendResult = await icttClient.sendToken({
+ walletClient,
+ sourceChain: sourceChainData,
+ destinationChain: destinationChainData,
+ tokenHomeContract: tokenHomeContract as `0x${string}`,
+ tokenRemoteContract: tokenRemoteContract as `0x${string}`,
+ recipient: recipientAddress as `0x${string}`,
+ amountInBaseUnit,
+ });
+
+ // TODO: remote > home transfer??
+
+ console.log('✅ Token sent:', sendResult.txHash);
+
+ const result = {
+ fromChain,
+ toChain,
+ token: selectedToken,
+ amount,
+ recipientAddress,
+ txHash: sendResult.txHash,
+ };
+
+ setStatus('success');
+ onSuccess?.(result);
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Transfer failed';
+ setError(errorMessage);
+ setStatus('error');
+ onError?.(new Error(errorMessage));
+ } finally {
+ setIsSending(false);
+ }
+ }, [selectedToken, amount, recipientAddress, fromChain, toChain, availableChains, walletClient, onSuccess, onError, tokenHomeContract, tokenRemoteContract]);
+
+ // Validation
+ const { isValidForApproval, isValidForSending, validationErrors } = useMemo(() => {
+ const errors: string[] = [];
+
+ if (!selectedToken) {
+ errors.push('Please select a token');
+ }
+
+ if (!amount || parseFloat(amount) <= 0) {
+ errors.push('Please enter a valid amount');
+ }
+
+ if (!tokenHomeContract) {
+ errors.push('Please provide token home contract');
+ }
+
+ if (fromChain === toChain) {
+ errors.push('Source and destination chains must be different');
+ }
+
+ // Additional validation for sending
+ const sendingErrors = [...errors];
+
+ if (!recipientAddress) {
+ sendingErrors.push('Please enter recipient address');
+ }
+
+ if (!tokenRemoteContract) {
+ sendingErrors.push('Please provide token remote contract');
+ }
+
+ return {
+ isValidForApproval: errors.length === 0 && isOnCorrectChain,
+ isValidForSending: sendingErrors.length === 0 && isApproved && isOnCorrectChain,
+ validationErrors: sendingErrors,
+ };
+ }, [selectedToken, amount, tokenHomeContract, tokenRemoteContract, recipientAddress, fromChain, toChain, isApproved, isOnCorrectChain]);
+
+ // Check if user is on correct chain
+ useEffect(() => {
+ const sourceChainData = availableChains.find(chain => chain.id.toString() === fromChain);
+
+ console.log('Chain check:', {
+ fromChain,
+ sourceChainData,
+ walletChainId,
+ availableChains: availableChains.map(c => ({ id: c.id, name: c.name }))
+ });
+
+ if (!sourceChainData || !walletChainId) {
+ console.log('Setting isOnCorrectChain to false - missing data');
+ setIsOnCorrectChain(false);
+ return;
+ }
+
+ const isCorrect = walletChainId === sourceChainData.id;
+ console.log('Chain comparison:', { walletChainId, sourceChainId: sourceChainData.id, isCorrect });
+ setIsOnCorrectChain(isCorrect);
+ }, [fromChain, availableChains, walletChainId]);
+
+ // Check allowance when relevant parameters change
+ useEffect(() => {
+ if (selectedToken && amount && tokenHomeContract && walletClient && isOnCorrectChain) {
+ checkAllowance();
+ } else {
+ // Reset allowance when not on correct chain
+ setAllowance(null);
+ setIsApproved(false);
+ }
+ }, [selectedToken, amount, tokenHomeContract, walletClient, isOnCorrectChain, checkAllowance]);
+
+ // Type guard to check if token is an ICTT token
+ const isICTTToken = (token: any): token is ICTTToken => {
+ return token && typeof token === 'object' && 'ictt' in token && token.ictt && token.ictt.home;
+ };
+
+ // Token validation logic
+ const areTokensValid = useMemo(() => {
+ if (!selectedToken) {
+ return false;
+ }
+
+ // For selector mode: selectedToken must be a well-known ICTT token with mirrors
+ if (isICTTToken(selectedToken) && selectedToken.ictt) {
+ // Check if there's a mirror for the destination chain
+ const hasRemoteMirror = selectedToken.ictt.mirrors.some(mirror => mirror.chainId === toChain);
+ return hasRemoteMirror;
+ }
+
+ // For manual mode: selectedToken is a regular ERC20 token and both contracts must be valid
+ if (!isICTTToken(selectedToken) && tokenHomeContract && tokenRemoteContract) {
+ return true;
+ }
+
+ // No tokens are valid yet
+ return false;
+ }, [selectedToken, tokenHomeContract, tokenRemoteContract, toChain]);
+
+ // Status change callback
+ useEffect(() => {
+ onStatusChange?.(status);
+ }, [status, onStatusChange]);
+
+ const contextValue: ICTTContextType = {
+ // Chain selection
+ fromChain,
+ toChain,
+ setFromChain,
+ setToChain,
+ swapChains,
+
+ // Token selection
+ selectedToken,
+ setSelectedToken,
+ availableTokens: wellKnownTokens,
+
+ // Contract addresses for Home mode
+ tokenHomeContract,
+ setTokenHomeContract,
+ tokenRemoteContract,
+ setTokenRemoteContract,
+
+ // Transfer details
+ amount,
+ setAmount,
+ recipientAddress,
+ setRecipientAddress,
+
+ // Chain switching
+ isOnCorrectChain,
+ isSwitchingChain,
+ switchToSourceChain,
+
+ // Allowance and approval
+ allowance,
+ isApproved,
+ isCheckingAllowance,
+ checkAllowance,
+
+ // Status and actions
+ status,
+ isApproving,
+ isSending,
+ error,
+ approveToken,
+ sendToken,
+
+ // Token validation
+ areTokensValid,
+
+ // Validation
+ isValidForApproval,
+ isValidForSending,
+ validationErrors,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useICTTContext() {
+ const context = useContext(ICTTContext);
+ if (context === undefined) {
+ throw new Error('useICTTContext must be used within an ICTTProvider');
+ }
+ return context;
+}
\ No newline at end of file
diff --git a/ui/src/ictt/components/ICTTRemoteTokenAddressInput.tsx b/ui/src/ictt/components/ICTTRemoteTokenAddressInput.tsx
new file mode 100644
index 00000000..ce2674b3
--- /dev/null
+++ b/ui/src/ictt/components/ICTTRemoteTokenAddressInput.tsx
@@ -0,0 +1,332 @@
+'use client';
+import { useState, useCallback, useEffect } from 'react';
+import { cn } from '../../styles/theme';
+import { TokenImage } from '../../token';
+import { XCircle, Loader2 } from 'lucide-react';
+import { useICTTContext } from './ICTTProvider';
+import { useAvalanche } from '../../AvalancheProvider';
+import { isAddress, formatUnits, createPublicClient, http } from 'viem';
+import { ERC20_ABI } from '../../utils/erc20';
+
+interface TokenInfo {
+ name: string;
+ symbol: string;
+ decimals: number;
+ address: string;
+}
+
+interface TokenValidationState {
+ isValidating: boolean;
+ isValid: boolean;
+ error?: string;
+ tokenInfo?: TokenInfo;
+ homeContractAddress?: string;
+}
+
+export interface ICTTRemoteTokenAddressInputProps {
+ className?: string;
+ label?: string;
+ onRemoteContractChange?: (remoteContract: string | null) => void;
+ onHomeContractChange?: (homeContract: string | null) => void;
+}
+
+export function ICTTRemoteTokenAddressInput({
+ className,
+ label = "ICTT Remote Contract Address",
+ onRemoteContractChange,
+ onHomeContractChange
+}: ICTTRemoteTokenAddressInputProps) {
+ const { toChain } = useICTTContext();
+ const { availableChains, walletAddress, isWalletConnected } = useAvalanche();
+ const [remoteContractAddress, setRemoteContractAddress] = useState('');
+ const [tokenValidation, setTokenValidation] = useState({
+ isValidating: false,
+ isValid: false,
+ });
+ const [tokenBalance, setTokenBalance] = useState(null);
+
+ // Get chain config and create public client
+ const getPublicClient = useCallback((chainId: string) => {
+ const chainData = availableChains.find(c => c.id.toString() === chainId);
+
+ if (!chainData) {
+ throw new Error(`Chain ${chainId} not found in availableChains`);
+ }
+
+ const rpcUrl = chainData.rpcUrls?.default?.http?.[0];
+ if (!rpcUrl) {
+ throw new Error(`No RPC URL found for chain ${chainId}. Please ensure the chain is properly configured in AvalancheProvider.`);
+ }
+
+ return createPublicClient({
+ chain: chainData as any, // ChainConfig should be compatible with viem Chain
+ transport: http(rpcUrl),
+ });
+ }, [availableChains]);
+
+ // Get token home address from ICTT remote contract
+ const getTokenHomeAddressFromRemote = useCallback(async (remoteContract: string): Promise => {
+ // Call getTokenHomeAddress() function on the remote contract
+ // This is a custom function on ICTT remote contracts, not part of ERC20
+ // So we use raw RPC call instead of viem's readContract
+ const rpcUrl = availableChains.find(c => c.id.toString() === toChain)?.rpcUrls?.default?.http?.[0];
+ if (!rpcUrl) {
+ throw new Error(`No RPC URL found for chain ${toChain}`);
+ }
+
+ const response = await fetch(rpcUrl, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_call',
+ params: [
+ {
+ to: remoteContract,
+ data: '0xc3cd6927', // getTokenHomeAddress() function selector
+ },
+ 'latest'
+ ],
+ id: 1,
+ }),
+ });
+
+ const result = await response.json();
+
+ if (result.error || !result.result || result.result === '0x') {
+ throw new Error('Failed to get token home address from remote contract');
+ }
+
+ // Decode the address from the result (last 20 bytes)
+ const homeAddress = '0x' + result.result.slice(-40);
+
+ if (!isAddress(homeAddress)) {
+ throw new Error('Invalid token home address returned from remote contract');
+ }
+
+ return homeAddress;
+ }, [toChain, availableChains]);
+
+
+ // Validate token contract using ERC20 interface
+ const validateTokenContract = useCallback(async (tokenAddress: string, chainId: string) => {
+ const publicClient = getPublicClient(chainId);
+
+ // Read token metadata using ERC20 ABI
+ const [name, symbol, decimals] = await Promise.all([
+ publicClient.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: 'name',
+ }) as Promise,
+ publicClient.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: 'symbol',
+ }) as Promise,
+ publicClient.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: 'decimals',
+ }) as Promise,
+ ]);
+
+ if (!name || !symbol || isNaN(decimals)) {
+ throw new Error('Invalid token contract data');
+ }
+
+ // Read balance if wallet is connected
+ if (walletAddress && isWalletConnected) {
+ try {
+ const balance = await publicClient.readContract({
+ address: tokenAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ functionName: 'balanceOf',
+ args: [walletAddress as `0x${string}`],
+ }) as bigint;
+
+ const formattedBalance = formatUnits(balance, decimals);
+ const numericBalance = parseFloat(formattedBalance);
+ const displayBalance = numericBalance.toLocaleString(undefined, {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 6,
+ });
+ setTokenBalance(displayBalance);
+ } catch {
+ setTokenBalance('0');
+ }
+ } else {
+ setTokenBalance(null);
+ }
+
+ return {
+ name,
+ symbol,
+ decimals,
+ address: tokenAddress,
+ };
+ }, [getPublicClient, walletAddress, isWalletConnected]);
+
+ // Validate ICTT remote contract and get token info
+ const validateRemoteContract = useCallback(async (remoteContractAddress: string) => {
+ if (!remoteContractAddress || !isAddress(remoteContractAddress)) {
+ setTokenValidation({
+ isValidating: false,
+ isValid: false,
+ error: 'Invalid remote contract address format',
+ });
+ return;
+ }
+
+ setTokenValidation({ isValidating: true, isValid: false });
+ setTokenBalance(null);
+
+ try {
+ // Step 1: Get token home address from remote contract for validation
+ const homeContractAddress = await getTokenHomeAddressFromRemote(remoteContractAddress);
+
+ // Step 2: Get token metadata directly from the remote contract address (not from home contract)
+ const tokenInfo = await validateTokenContract(remoteContractAddress, toChain);
+
+ setTokenValidation({
+ isValidating: false,
+ isValid: true,
+ tokenInfo,
+ homeContractAddress,
+ });
+
+ // Notify parent components about the contract addresses
+ onRemoteContractChange?.(remoteContractAddress);
+ onHomeContractChange?.(homeContractAddress);
+
+ } catch (error) {
+ setTokenValidation({
+ isValidating: false,
+ isValid: false,
+ error: error instanceof Error ? error.message : 'Failed to validate remote contract',
+ });
+ onRemoteContractChange?.(null);
+ onHomeContractChange?.(null);
+ }
+ }, [getTokenHomeAddressFromRemote, validateTokenContract, toChain, onRemoteContractChange, onHomeContractChange]);
+
+ // Handle address change with debouncing
+ useEffect(() => {
+ if (!remoteContractAddress) {
+ setTokenValidation({ isValidating: false, isValid: false });
+ onRemoteContractChange?.(null);
+ onHomeContractChange?.(null);
+ return;
+ }
+
+ const timeoutId = setTimeout(() => {
+ validateRemoteContract(remoteContractAddress);
+ }, 500); // 500ms debounce
+
+ return () => clearTimeout(timeoutId);
+ }, [remoteContractAddress, validateRemoteContract, onRemoteContractChange, onHomeContractChange]);
+
+ return (
+
+ {/* Label */}
+
+ {label}
+
+
+ {/* Input Container with Token Display */}
+
+
+ {/* Remote Contract Address Input */}
+
+ setRemoteContractAddress(e.target.value)}
+ placeholder="0x... (ICTT Remote Contract)"
+ className={cn(
+ "flex h-12 w-full border border-input bg-background px-3 py-2 text-base",
+ "file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground",
+ "focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 font-mono",
+ "rounded-l-md border-r-0", // Left side rounded, no right border
+ tokenValidation.isValid && "border-green-500",
+ tokenValidation.error && "border-destructive"
+ )}
+ />
+
+
+ {/* Token Display - looks like part of input */}
+
+ {/* Left Divider */}
+
+ {tokenValidation.isValid && tokenValidation.tokenInfo ? (
+
+
+
+
+ {tokenValidation.tokenInfo.name}
+
+
+ {tokenValidation.tokenInfo.symbol}
+
+
+
+ ) : remoteContractAddress && tokenValidation.isValidating ? (
+
+
+ Validating...
+
+ ) : null}
+
+
+
+ {/* Validation Status Below */}
+ {remoteContractAddress && (
+
+ {tokenValidation.isValidating ? (
+ <>
+
+ Validating ICTT remote contract...
+ >
+ ) : tokenValidation.error ? (
+ <>
+
+ {tokenValidation.error}
+ >
+ ) : null}
+
+ )}
+
+ {/* Token Balance */}
+ {tokenValidation.isValid && tokenValidation.tokenInfo && (
+
+ {isWalletConnected ? (
+ tokenBalance !== null ? (
+ <>Balance: {tokenBalance} {tokenValidation.tokenInfo.symbol}>
+ ) : (
+ 'Connect wallet to see balance'
+ )
+ ) : (
+ 'Connect wallet to see balance'
+ )}
+
+ )}
+
+
+
+ );
+}
diff --git a/ui/src/ictt/components/ICTTToggleButton.tsx b/ui/src/ictt/components/ICTTToggleButton.tsx
new file mode 100644
index 00000000..02f73e93
--- /dev/null
+++ b/ui/src/ictt/components/ICTTToggleButton.tsx
@@ -0,0 +1,26 @@
+'use client';
+import { useICTTContext } from './ICTTProvider';
+import { DirectionToggle } from '../../components/ui/direction-toggle';
+
+export interface ICTTToggleButtonProps {
+ className?: string;
+ disabled?: boolean;
+}
+
+export function ICTTToggleButton({
+ className,
+ disabled = false,
+}: ICTTToggleButtonProps) {
+ const { status, swapChains } = useICTTContext();
+
+ const isDisabled = disabled || status === 'loading';
+
+ return (
+
+ );
+}
diff --git a/ui/src/ictt/components/ICTTTokenModeToggle.tsx b/ui/src/ictt/components/ICTTTokenModeToggle.tsx
new file mode 100644
index 00000000..40abbec7
--- /dev/null
+++ b/ui/src/ictt/components/ICTTTokenModeToggle.tsx
@@ -0,0 +1,67 @@
+'use client';
+import { useState } from 'react';
+import { cn } from '../../styles/theme';
+import { Button } from '../../components/ui/button';
+import { List, Home } from 'lucide-react';
+import { ICTTTokenSelector } from './ICTTTokenSelector';
+import { ICTTHomeTokenAddressInput } from './ICTTHomeTokenAddressInput';
+import { ICTTRemoteTokenAddressInput } from './ICTTRemoteTokenAddressInput';
+import { useICTTContext } from './ICTTProvider';
+
+export type TokenInputMode = 'selector' | 'manual';
+
+export interface ICTTTokenModeToggleProps {
+ className?: string;
+ defaultMode?: TokenInputMode;
+ allowManualMode?: boolean;
+}
+
+export function ICTTTokenModeToggle({
+ className,
+ defaultMode = 'selector',
+ allowManualMode = false
+}: ICTTTokenModeToggleProps) {
+ const [mode, setMode] = useState(defaultMode);
+ const { setTokenRemoteContract } = useICTTContext();
+
+ return (
+
+ {/* Mode Toggle Buttons - only show if manual mode is allowed */}
+ {allowManualMode ? (
+
+ setMode('selector')}
+ className="flex-1 h-8"
+ >
+
+ Select
+
+ setMode('manual')}
+ className="flex-1 h-8"
+ >
+
+ Manual
+
+
+ ) : null}
+
+ {/* Token Input Component */}
+ {mode === 'selector' || !allowManualMode ? (
+
+ ) : (
+
+
+
+
+ )}
+
+ );
+}
diff --git a/ui/src/ictt/components/ICTTTokenSelector.tsx b/ui/src/ictt/components/ICTTTokenSelector.tsx
new file mode 100644
index 00000000..2fe20771
--- /dev/null
+++ b/ui/src/ictt/components/ICTTTokenSelector.tsx
@@ -0,0 +1,80 @@
+'use client';
+import { useMemo } from 'react';
+import { cn } from '../../styles/theme';
+import { TokenSelectDropdown } from '../../token';
+import type { Token } from '../../token/types';
+import { useICTTContext } from './ICTTProvider';
+import type { ICTTToken } from '../types';
+
+export interface ICTTTokenSelectorProps {
+ label?: string;
+ className?: string;
+}
+
+// Convert ICTTToken to Token for the TokenSelectDropdown component
+function convertICTTTokenToToken(icttToken: ICTTToken): Token {
+ return {
+ symbol: icttToken.symbol,
+ name: icttToken.name,
+ address: icttToken.address,
+ decimals: icttToken.decimals,
+ image: icttToken.logoUrl || null,
+ chainId: parseInt(icttToken.chainId),
+ };
+}
+
+export function ICTTTokenSelector({
+ label = "Select Token",
+ className
+}: ICTTTokenSelectorProps) {
+ const {
+ selectedToken,
+ setSelectedToken,
+ availableTokens,
+ fromChain,
+ toChain
+ } = useICTTContext();
+
+ // Filter tokens that are on the source chain and have a mirror on the destination chain
+ const filteredTokens = useMemo(() => {
+ return availableTokens.filter(token => {
+ // Check if token is on the source chain
+ const isOnSourceChain = token.chainId === fromChain;
+
+ // Check if token has a mirror on the destination chain
+ const hasMirrorOnDestination = token.ictt && token.ictt.mirrors.some(
+ mirror => mirror.chainId === toChain
+ );
+
+ return isOnSourceChain && hasMirrorOnDestination;
+ });
+ }, [availableTokens, fromChain, toChain]);
+
+ // Convert filtered ICTTToken[] to Token[] for TokenSelectDropdown
+ const convertedTokens = useMemo(() => {
+ return filteredTokens.map(convertICTTTokenToToken);
+ }, [filteredTokens]);
+
+ const handleTokenChange = (token: Token) => {
+ // Find the original ICTTToken that matches the selected Token from filtered tokens
+ const originalToken = filteredTokens.find(t => t.symbol === token.symbol && t.address === token.address);
+ setSelectedToken(originalToken || null);
+ };
+
+ const selectedConvertedToken = selectedToken ? convertICTTTokenToToken(selectedToken) : undefined;
+
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui/src/ictt/components/index.ts b/ui/src/ictt/components/index.ts
new file mode 100644
index 00000000..485c34d2
--- /dev/null
+++ b/ui/src/ictt/components/index.ts
@@ -0,0 +1,11 @@
+export { ICTT } from './ICTT';
+export { ICTTProvider, useICTTContext } from './ICTTProvider';
+export { ICTTChainSelector } from './ICTTChainSelector';
+export { ICTTToggleButton } from './ICTTToggleButton';
+export { ICTTTokenSelector } from './ICTTTokenSelector';
+export { ICTTHomeTokenAddressInput } from './ICTTHomeTokenAddressInput';
+export { ICTTRemoteTokenAddressInput } from './ICTTRemoteTokenAddressInput';
+export { ICTTTokenModeToggle } from './ICTTTokenModeToggle';
+export { ICTTAmountInput } from './ICTTAmountInput';
+export { ICTTAddressInput } from './ICTTAddressInput';
+export { ICTTButtons } from './ICTTButtons';
diff --git a/ui/src/ictt/hooks/index.ts b/ui/src/ictt/hooks/index.ts
new file mode 100644
index 00000000..73c8a719
--- /dev/null
+++ b/ui/src/ictt/hooks/index.ts
@@ -0,0 +1 @@
+export { useICTTContext } from './useICTTContext';
diff --git a/ui/src/ictt/hooks/useICTTContext.ts b/ui/src/ictt/hooks/useICTTContext.ts
new file mode 100644
index 00000000..c65ff14b
--- /dev/null
+++ b/ui/src/ictt/hooks/useICTTContext.ts
@@ -0,0 +1 @@
+export { useICTTContext } from '../components/ICTTProvider';
diff --git a/ui/src/ictt/index.ts b/ui/src/ictt/index.ts
new file mode 100644
index 00000000..6910ad7c
--- /dev/null
+++ b/ui/src/ictt/index.ts
@@ -0,0 +1,4 @@
+// ICTT (Interchain Token Transfer) Module
+export * from './components';
+export * from './hooks';
+export * from './types';
diff --git a/ui/src/ictt/types.ts b/ui/src/ictt/types.ts
new file mode 100644
index 00000000..2d4c9f93
--- /dev/null
+++ b/ui/src/ictt/types.ts
@@ -0,0 +1,93 @@
+import type { Token } from '../token/types';
+
+export interface ICTTTokenMirror {
+ chainId: string;
+ address: string;
+}
+
+/**
+ * ICTT Token extends the base Token type with cross-chain functionality
+ */
+export interface ICTTToken extends Token {
+ /** Chain ID as string for ICTT compatibility */
+ chainId: string;
+ /** ICTT-specific home and mirror configuration (optional for manual mode) */
+ ictt?: {
+ home: string;
+ mirrors: ICTTTokenMirror[];
+ };
+ /** Optional logo URL (maps to Token.image) */
+ logoUrl?: string;
+}
+
+export interface ICTTTransferParams {
+ fromChain: string;
+ toChain: string;
+ token: ICTTToken;
+ amount: string;
+ recipientAddress: string;
+}
+
+export type ICTTStatus = 'idle' | 'loading' | 'success' | 'error';
+
+export interface ICTTProviderProps {
+ children: React.ReactNode;
+ initialFromChain?: string;
+ initialToChain?: string;
+ onStatusChange?: (status: ICTTStatus) => void;
+ onSuccess?: (result: any) => void;
+ onError?: (error: Error) => void;
+}
+
+export interface ICTTContextType {
+ // Chain selection
+ fromChain: string;
+ toChain: string;
+ setFromChain: (chain: string) => void;
+ setToChain: (chain: string) => void;
+ swapChains: () => void;
+
+ // Token selection
+ selectedToken: ICTTToken | null;
+ setSelectedToken: (token: ICTTToken | null) => void;
+ availableTokens: ICTTToken[];
+
+ // Contract addresses for Home mode
+ tokenHomeContract: string | null;
+ setTokenHomeContract: (contract: string | null) => void;
+ tokenRemoteContract: string | null;
+ setTokenRemoteContract: (contract: string | null) => void;
+
+ // Transfer details
+ amount: string;
+ setAmount: (amount: string) => void;
+ recipientAddress: string;
+ setRecipientAddress: (address: string) => void;
+
+ // Chain switching
+ isOnCorrectChain: boolean;
+ isSwitchingChain: boolean;
+ switchToSourceChain: () => Promise;
+
+ // Allowance and approval
+ allowance: string | null;
+ isApproved: boolean;
+ isCheckingAllowance: boolean;
+ checkAllowance: () => Promise;
+
+ // Status and actions
+ status: ICTTStatus;
+ isApproving: boolean;
+ isSending: boolean;
+ error: string | null;
+ approveToken: () => Promise;
+ sendToken: () => Promise;
+
+ // Token validation
+ areTokensValid: boolean;
+
+ // Validation
+ isValidForApproval: boolean;
+ isValidForSending: boolean;
+ validationErrors: string[];
+}
diff --git a/ui/src/index.ts b/ui/src/index.ts
new file mode 100644
index 00000000..08c97435
--- /dev/null
+++ b/ui/src/index.ts
@@ -0,0 +1,50 @@
+// 🏔️❄️🏔️ Avalanche UI Kit
+// Import styles
+import './styles/index.css';
+
+// Main Provider
+export { AvalancheProvider, useAvalanche, useAvailableChains } from './AvalancheProvider';
+
+// Theme Provider
+export { ThemeProvider, useTheme } from './theme';
+export type { Theme, Mode } from './theme';
+
+// Wallet Module
+export * from './wallet';
+
+// Transfer Module
+export * from './transfer';
+
+// ICTT Module
+export * from './ictt';
+export type { ICTTToken, ICTTTokenMirror } from './ictt/types';
+
+// Stake Module
+export * from './stake';
+
+// Earn Module
+export * from './earn';
+export type { EarnPool, EarnProviderType, EarnStatus, EarnAction } from './earn/types';
+
+// Chain Module
+export * from './chain';
+
+// Token Module
+export * from './token';
+
+// Hooks
+export * from './hooks';
+
+// Utils
+export * from './utils';
+
+// shadcn/ui Components
+export * from './components/ui';
+
+// Theme
+export { cn, text, pressable, border } from './styles/theme';
+
+// Types
+export type { AvalancheConfig, AvalancheProviderProps, AvalancheContextType } from './AvalancheProvider';
+export type { ChainConfig } from './types/chainConfig';
+export { isChainConfig } from './types/chainConfig';
diff --git a/ui/src/stake/components/Stake.tsx b/ui/src/stake/components/Stake.tsx
new file mode 100644
index 00000000..ebc487fc
--- /dev/null
+++ b/ui/src/stake/components/Stake.tsx
@@ -0,0 +1,64 @@
+'use client';
+import React from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
+import { WalletConnectionOverlay } from '../../components/ui/wallet-connection-overlay';
+import { AvalancheChainOverlay } from '../../components/ui/avalanche-chain-overlay';
+import { cn } from '../../styles/theme';
+import { StakeProvider } from './StakeProvider';
+import { StakeValidatorInput } from './StakeValidatorInput';
+import { StakeAmountInput } from './StakeAmountInput';
+import { StakeDurationInput } from './StakeDurationInput';
+import { StakeButton } from './StakeButton';
+import { StakeMessage } from './StakeMessage';
+import type { StakeProviderProps } from '../types';
+
+type StakeProps = {
+ children?: React.ReactNode;
+ className?: string;
+ title?: string;
+ description?: string;
+} & Omit;
+
+function StakeContent() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export function Stake({
+ children,
+ className,
+ title = "Stake on Primary Network",
+ description = "Stake AVAX as a validator on Avalanche's Primary Network to secure the network and earn rewards",
+ ...providerProps
+}: StakeProps) {
+ return (
+
+
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+
+
+ {children || }
+
+
+
+
+
+ );
+}
diff --git a/ui/src/stake/components/StakeAmountInput.tsx b/ui/src/stake/components/StakeAmountInput.tsx
new file mode 100644
index 00000000..6007c39c
--- /dev/null
+++ b/ui/src/stake/components/StakeAmountInput.tsx
@@ -0,0 +1,216 @@
+'use client';
+import { useState, useEffect } from 'react';
+import { AmountInput } from '../../components/ui/amount-input';
+import { Label } from '../../components/ui/label';
+import { cn } from '../../styles/theme';
+import { useStakeContext } from './StakeProvider';
+import { useWalletContext } from '../../wallet/hooks/useWalletContext';
+import type { StakeAmountInputProps } from '../types';
+
+export function StakeAmountInput({
+ className,
+ label = "Stake Configuration",
+ disabled = false,
+}: StakeAmountInputProps) {
+ const {
+ stakeAmount,
+ setStakeAmount,
+ delegatorRewardPercentage,
+ setDelegatorRewardPercentage,
+ networkConfig,
+ isTestnet,
+ } = useStakeContext();
+
+ const { status, balances, currentChain } = useWalletContext();
+ const [stakeError, setStakeError] = useState(null);
+ const [feeError, setFeeError] = useState(null);
+
+ // Determine minimum stake and network name based on connected chain
+ const getMinStakeAndNetwork = () => {
+ const chainId = currentChain?.id;
+
+ if (chainId === 43113) {
+ // Fuji Testnet
+ return { minStake: 1, networkName: 'Fuji' };
+ } else if (chainId === 43114) {
+ // Avalanche Mainnet
+ return { minStake: 2000, networkName: 'Mainnet' };
+ } else {
+ // Fallback to config values
+ return {
+ minStake: networkConfig.minStakeAvax,
+ networkName: isTestnet ? 'Fuji' : 'Mainnet'
+ };
+ }
+ };
+
+ const { minStake, networkName } = getMinStakeAndNetwork();
+
+ // Get P-Chain balance for staking
+ const pChainBalance = balances.pChain;
+ const isWalletConnected = status === 'connected';
+
+ // Handle MAX button click - use P-Chain balance minus small buffer for transaction fees
+ const handleMaxClick = () => {
+ if (pChainBalance && pChainBalance.avax) {
+ const balance = parseFloat(pChainBalance.avax);
+ // Leave 0.01 AVAX for transaction fees
+ const maxAmount = Math.max(0, balance - 0.01);
+ setStakeAmount(maxAmount.toString());
+ }
+ };
+
+ // Real-time validation for stake amount
+ useEffect(() => {
+ if (!stakeAmount) {
+ setStakeError(null);
+ return;
+ }
+
+ const amount = Number(stakeAmount);
+ if (isNaN(amount)) {
+ setStakeError('Please enter a valid number');
+ return;
+ }
+
+ if (amount <= 0) {
+ setStakeError('Stake amount must be greater than 0');
+ return;
+ }
+
+ if (amount < minStake) {
+ setStakeError(`Minimum stake is ${minStake.toLocaleString()} AVAX on ${networkName}`);
+ return;
+ }
+
+ // Check wallet balance if connected
+ if (isWalletConnected && pChainBalance && pChainBalance.avax) {
+ const availableBalance = parseFloat(pChainBalance.avax);
+ if (amount > availableBalance) {
+ setStakeError(`Insufficient balance. Available: ${availableBalance.toFixed(4)} AVAX`);
+ return;
+ }
+
+ // Warn if trying to stake almost all balance (less than 0.01 AVAX left for fees)
+ if (amount > availableBalance - 0.01) {
+ setStakeError('Leave some AVAX for transaction fees. Try using the MAX button.');
+ return;
+ }
+ }
+
+ // Check for reasonable maximum (e.g., 1 million AVAX)
+ if (amount > 1000000) {
+ setStakeError('Stake amount seems unusually high. Please verify.');
+ return;
+ }
+
+ // Check for too many decimal places
+ const decimalPlaces = (stakeAmount.split('.')[1] || '').length;
+ if (decimalPlaces > 9) {
+ setStakeError('Maximum 9 decimal places allowed');
+ return;
+ }
+
+ setStakeError(null);
+ }, [stakeAmount, minStake, networkName, isWalletConnected, pChainBalance, currentChain]);
+
+ // Real-time validation for delegator reward percentage
+ useEffect(() => {
+ if (!delegatorRewardPercentage) {
+ setFeeError(null);
+ return;
+ }
+
+ const percentage = Number(delegatorRewardPercentage);
+ if (isNaN(percentage)) {
+ setFeeError('Please enter a valid percentage');
+ return;
+ }
+
+ if (percentage < 2) {
+ setFeeError('Minimum delegator fee is 2%');
+ return;
+ }
+
+ if (percentage > 100) {
+ setFeeError('Maximum delegator fee is 100%');
+ return;
+ }
+
+ // Check for too many decimal places
+ const decimalPlaces = (delegatorRewardPercentage.split('.')[1] || '').length;
+ if (decimalPlaces > 2) {
+ setFeeError('Maximum 2 decimal places allowed for percentage');
+ return;
+ }
+
+ setFeeError(null);
+ }, [delegatorRewardPercentage]);
+
+ return (
+
+
{label}
+
+
+
+
setStakeAmount(e.target.value)}
+ symbol="AVAX"
+ placeholder="0.00"
+ disabled={disabled}
+ min={minStake}
+ step={0.001}
+ showMax={isWalletConnected}
+ showBalance={isWalletConnected}
+ maxValue={pChainBalance?.avax}
+ onMaxClick={handleMaxClick}
+ className={stakeError ? 'border-destructive focus:border-destructive' : ''}
+ />
+ {stakeError ? (
+
+ {stakeError}
+
+ ) : (
+
+ Minimum: {minStake.toLocaleString()} AVAX ({networkName})
+
+ )}
+
+
+
+
setDelegatorRewardPercentage(e.target.value)}
+ symbol="%"
+ placeholder="2.0"
+ disabled={disabled}
+ min={2}
+ max={100}
+ step={0.1}
+ className={feeError ? 'border-destructive focus:border-destructive' : ''}
+ />
+ {feeError ? (
+
+ {feeError}
+
+ ) : (
+
+ Your fee from delegators (2-100%)
+
+ )}
+
+
+
+
+
+ • Stake will be locked for the entire duration
+ • Maintain >80% uptime to receive rewards
+ • Transaction fees apply
+
+
+
+ );
+}
diff --git a/ui/src/stake/components/StakeButton.tsx b/ui/src/stake/components/StakeButton.tsx
new file mode 100644
index 00000000..56fc77b0
--- /dev/null
+++ b/ui/src/stake/components/StakeButton.tsx
@@ -0,0 +1,40 @@
+'use client';
+import { Button } from '../../components/ui/button';
+import { Loader2 } from 'lucide-react';
+import { cn } from '../../styles/theme';
+import { useStakeContext } from './StakeProvider';
+import { useWalletContext } from '../../wallet/hooks/useWalletContext';
+import type { StakeButtonProps } from '../types';
+
+export function StakeButton({
+ className,
+ children,
+ disabled = false,
+ loadingText = "Processing transaction...",
+}: StakeButtonProps) {
+ const { status, submitStake, isTestnet, isFormValid } = useStakeContext();
+ const { address } = useWalletContext();
+
+ const isLoading = status === 'preparing' || status === 'pending';
+ const isDisabled = disabled || !address || isLoading || !isFormValid;
+ const networkName = isTestnet ? 'Fuji' : 'Mainnet';
+
+ const getButtonText = () => {
+ if (isLoading) return loadingText;
+ if (children) return children;
+ return `Stake ${networkName} Validator`;
+ };
+
+ return (
+
+ {isLoading && }
+ {getButtonText()}
+
+ );
+}
diff --git a/ui/src/stake/components/StakeDurationInput.tsx b/ui/src/stake/components/StakeDurationInput.tsx
new file mode 100644
index 00000000..9665e138
--- /dev/null
+++ b/ui/src/stake/components/StakeDurationInput.tsx
@@ -0,0 +1,136 @@
+'use client';
+import { useState, useEffect } from 'react';
+import { Input } from '../../components/ui/input';
+import { Label } from '../../components/ui/label';
+import { Button } from '../../components/ui/button';
+import { cn } from '../../styles/theme';
+import { useStakeContext } from './StakeProvider';
+import type { StakeDurationInputProps } from '../types';
+
+export function StakeDurationInput({
+ className,
+ label = "Staking Duration",
+ disabled = false,
+ showPresets = true,
+}: StakeDurationInputProps) {
+ const {
+ endTime,
+ setEndTime,
+ setEndInDays,
+ networkConfig,
+ isTestnet,
+ } = useStakeContext();
+
+ const [durationError, setDurationError] = useState(null);
+
+ const isDateButtonActive = (days: number) => {
+ if (!endTime) return false;
+ const targetDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
+ const selectedDate = new Date(endTime);
+ return Math.abs(targetDate.getTime() - selectedDate.getTime()) < 24 * 60 * 60 * 1000;
+ };
+
+ // Real-time validation for end time
+ useEffect(() => {
+ if (!endTime) {
+ setDurationError(null);
+ return;
+ }
+
+ const selectedDate = new Date(endTime);
+ const now = new Date();
+
+ // Check if the date is valid
+ if (isNaN(selectedDate.getTime())) {
+ setDurationError('Please enter a valid date and time');
+ return;
+ }
+
+ // Check if the date is in the past
+ if (selectedDate <= now) {
+ setDurationError('End time must be in the future');
+ return;
+ }
+
+ const duration = Math.floor(selectedDate.getTime() / 1000) - Math.floor(now.getTime() / 1000);
+
+ // Check minimum duration
+ if (duration < networkConfig.minEndSeconds) {
+ const minDuration = isTestnet ? '24 hours' : '2 weeks';
+ setDurationError(`Must be at least ${minDuration} from now`);
+ return;
+ }
+
+ // Check maximum duration (1 year)
+ const maxDuration = 365 * 24 * 60 * 60;
+ if (duration > maxDuration) {
+ setDurationError('Must be within 1 year from now');
+ return;
+ }
+
+ // Check if it's too close to minimum (warn about buffer time)
+ const bufferTime = 5 * 60; // 5 minutes buffer
+ if (duration < networkConfig.minEndSeconds + bufferTime) {
+ setDurationError(`Consider adding a few minutes buffer to ensure the transaction is processed in time`);
+ return;
+ }
+
+ // Maximum is already enforced above at 1 year, no need for additional check
+
+ setDurationError(null);
+ }, [endTime, networkConfig.minEndSeconds, isTestnet]);
+
+ return (
+
+
{label}
+
+ {showPresets && (
+
+ {networkConfig.presets.map((preset) => (
+ setEndInDays(preset.days)}
+ variant={isDateButtonActive(preset.days) ? "default" : "outline"}
+ size="sm"
+ disabled={disabled}
+ className="text-sm"
+ >
+ {preset.label}
+
+ ))}
+
+ )}
+
+
+
Custom End Date
+
+ setEndTime(e.target.value)}
+ type="datetime-local"
+ disabled={disabled}
+ min={new Date().toISOString().slice(0, 16)}
+ max={new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 16)}
+ className={cn(
+ "pr-10 [&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:right-3 [&::-webkit-calendar-picker-indicator]:top-1/2 [&::-webkit-calendar-picker-indicator]:-translate-y-1/2 [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-100 [&::-webkit-calendar-picker-indicator]:brightness-0 dark:[&::-webkit-calendar-picker-indicator]:brightness-100",
+ durationError ? 'border-destructive focus:border-destructive' : ''
+ )}
+ style={{
+ colorScheme: 'light'
+ }}
+ />
+
+ {durationError ? (
+
+ {durationError}
+
+ ) : (
+
+ Min: {isTestnet ? '24 hours' : '2 weeks'} • Max: 1 year
+
+ )}
+
+
+ );
+}
diff --git a/ui/src/stake/components/StakeMessage.tsx b/ui/src/stake/components/StakeMessage.tsx
new file mode 100644
index 00000000..77555837
--- /dev/null
+++ b/ui/src/stake/components/StakeMessage.tsx
@@ -0,0 +1,70 @@
+'use client';
+import { Alert, AlertDescription } from '../../components/ui/alert';
+import { CheckCircle2, XCircle } from 'lucide-react';
+import { cn } from '../../styles/theme';
+import { useStakeContext } from './StakeProvider';
+import type { StakeMessageProps } from '../types';
+
+export function StakeMessage({ className }: StakeMessageProps) {
+ const { status, error, result, isTestnet } = useStakeContext();
+
+ if (status === 'idle' || status === 'preparing' || status === 'pending') {
+ return null;
+ }
+
+ if (status === 'error' && error) {
+ return (
+
+
+
+ Stake Failed: {error.message}
+
+
+ );
+ }
+
+ if (status === 'success' && result) {
+ const explorerUrl = isTestnet
+ ? `https://subnets-test.avax.network/p-chain/tx/${result.txHash}`
+ : `https://subnets.avax.network/p-chain/tx/${result.txHash}`;
+
+ return (
+
+
+
+
+
+ Stake Successful!
+
+
+
+
+ Stake Amount: {result.stakeAmount} AVAX
+
+
+ Node ID: {' '}
+ {result.nodeId}
+
+
+ End Time: {' '}
+ {new Date(result.endTime * 1000).toLocaleString()}
+
+
+
+
+
+ );
+ }
+
+ return null;
+}
diff --git a/ui/src/stake/components/StakeProvider.tsx b/ui/src/stake/components/StakeProvider.tsx
new file mode 100644
index 00000000..35f2b21c
--- /dev/null
+++ b/ui/src/stake/components/StakeProvider.tsx
@@ -0,0 +1,391 @@
+'use client';
+import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';
+import { useWalletContext } from '../../wallet/hooks/useWalletContext';
+import { useAvalanche } from '../../AvalancheProvider';
+import type {
+ StakeContextType,
+ StakeProviderProps,
+ StakeStatus,
+ StakeError,
+ StakeResult,
+ ValidatorCredentials,
+ NetworkConfig,
+} from '../types';
+
+// Network-specific constants
+const DEFAULT_NETWORK_CONFIG = {
+ fuji: {
+ minStakeAvax: 1,
+ minEndSeconds: 24 * 60 * 60, // 24 hours
+ defaultDays: 1,
+ presets: [
+ { label: '1 day', days: 1 },
+ { label: '1 week', days: 7 },
+ { label: '2 weeks', days: 14 }
+ ]
+ },
+ mainnet: {
+ minStakeAvax: 2000,
+ minEndSeconds: 14 * 24 * 60 * 60, // 14 days
+ defaultDays: 14,
+ presets: [
+ { label: '2 weeks', days: 14 },
+ { label: '1 month', days: 30 },
+ { label: '3 months', days: 90 }
+ ]
+ }
+};
+
+const MAX_END_SECONDS = 365 * 24 * 60 * 60; // 1 year
+const DEFAULT_DELEGATOR_REWARD_PERCENTAGE = "2";
+const BUFFER_MINUTES = 5;
+
+const StakeContext = createContext(null);
+
+export function useStakeContext(): StakeContextType {
+ const context = useContext(StakeContext);
+ if (!context) {
+ throw new Error('useStakeContext must be used within a StakeProvider');
+ }
+ return context;
+}
+
+export function StakeProvider({
+ children,
+ onSuccess,
+ onError,
+ networkConfig: customNetworkConfig,
+}: StakeProviderProps) {
+ const { pAddress: pChainAddress } = useWalletContext();
+ const { walletClient } = useAvalanche();
+
+ // State
+ const [status, setStatus] = useState('idle');
+ const [validator, setValidator] = useState(null);
+ const [stakeAmount, setStakeAmount] = useState('');
+ const [endTime, setEndTime] = useState('');
+ const [delegatorRewardPercentage, setDelegatorRewardPercentage] = useState(DEFAULT_DELEGATOR_REWARD_PERCENTAGE);
+ const [error, setError] = useState();
+ const [result, setResult] = useState();
+
+ // Determine network configuration from actual chain
+ const { chain } = useAvalanche();
+ const isTestnet = useMemo(() => {
+ // Check if chain has testnet property
+ return chain.testnet ?? false;
+ }, [chain]);
+
+ const networkConfig = useMemo((): NetworkConfig => {
+ const baseConfig = isTestnet ? DEFAULT_NETWORK_CONFIG.fuji : DEFAULT_NETWORK_CONFIG.mainnet;
+ return { ...baseConfig, ...customNetworkConfig };
+ }, [isTestnet, customNetworkConfig]);
+
+ // Initialize defaults
+ const initializeDefaults = useCallback(() => {
+ if (!stakeAmount) {
+ setStakeAmount(String(networkConfig.minStakeAvax));
+ }
+
+ if (!endTime) {
+ const d = new Date();
+ d.setDate(d.getDate() + networkConfig.defaultDays);
+ d.setMinutes(d.getMinutes() + BUFFER_MINUTES);
+ const iso = new Date(d.getTime() - d.getTimezoneOffset() * 60000)
+ .toISOString()
+ .slice(0, 16);
+ setEndTime(iso);
+ }
+ }, [stakeAmount, endTime, networkConfig]);
+
+ // Initialize defaults on mount
+ React.useEffect(() => {
+ initializeDefaults();
+ }, [initializeDefaults]);
+
+ const setEndInDays = useCallback((days: number) => {
+ const d = new Date();
+ d.setDate(d.getDate() + days);
+ d.setMinutes(d.getMinutes() + BUFFER_MINUTES);
+ const iso = new Date(d.getTime() - d.getTimezoneOffset() * 60000)
+ .toISOString()
+ .slice(0, 16);
+ setEndTime(iso);
+ }, []);
+
+ const validateForm = useCallback((): string | null => {
+ // Wallet validation
+ if (!pChainAddress) {
+ return 'Connect wallet to get your P-Chain address';
+ }
+
+ // Validator validation
+ if (!validator) {
+ return 'Please provide validator credentials';
+ }
+
+ if (!validator.nodeID?.startsWith('NodeID-')) {
+ return 'Invalid NodeID format - must start with "NodeID-"';
+ }
+
+ // More thorough NodeID validation
+ if (validator.nodeID.length < 15 || validator.nodeID.length > 60) {
+ return 'Invalid NodeID format - incorrect length';
+ }
+
+ // BLS Public Key validation
+ if (!validator.nodePOP.publicKey?.startsWith('0x')) {
+ return 'Invalid BLS Public Key format - must start with "0x"';
+ }
+
+ if (validator.nodePOP.publicKey.length !== 98) {
+ return 'Invalid BLS Public Key format - must be 98 characters (0x + 96 hex chars)';
+ }
+
+ // Check if public key is valid hex
+ const publicKeyHex = validator.nodePOP.publicKey.slice(2);
+ if (!/^[0-9a-fA-F]+$/.test(publicKeyHex)) {
+ return 'Invalid BLS Public Key - contains non-hexadecimal characters';
+ }
+
+ if (publicKeyHex === '0'.repeat(96)) {
+ return 'Invalid BLS Public Key - cannot be all zeros';
+ }
+
+ // BLS Proof of Possession validation
+ if (!validator.nodePOP.proofOfPossession?.startsWith('0x')) {
+ return 'Invalid BLS Proof of Possession format - must start with "0x"';
+ }
+
+ if (validator.nodePOP.proofOfPossession.length !== 194) {
+ return 'Invalid BLS Proof of Possession format - must be 194 characters (0x + 192 hex chars)';
+ }
+
+ // Check if proof of possession is valid hex
+ const proofHex = validator.nodePOP.proofOfPossession.slice(2);
+ if (!/^[0-9a-fA-F]+$/.test(proofHex)) {
+ return 'Invalid BLS Proof of Possession - contains non-hexadecimal characters';
+ }
+
+ if (proofHex === '0'.repeat(192)) {
+ return 'Invalid BLS Proof of Possession - cannot be all zeros';
+ }
+
+ // Stake amount validation
+ if (!stakeAmount || stakeAmount.trim() === '') {
+ return 'Stake amount is required';
+ }
+
+ const stakeNum = Number(stakeAmount);
+ if (!Number.isFinite(stakeNum)) {
+ return 'Please enter a valid stake amount';
+ }
+
+ if (stakeNum <= 0) {
+ return 'Stake amount must be greater than 0';
+ }
+
+ if (stakeNum < networkConfig.minStakeAvax) {
+ const networkName = isTestnet ? 'Fuji' : 'Mainnet';
+ return `Minimum stake is ${networkConfig.minStakeAvax.toLocaleString()} AVAX on ${networkName}`;
+ }
+
+ // Check for reasonable maximum
+ if (stakeNum > 1000000) {
+ return 'Stake amount seems unusually high. Please verify the amount.';
+ }
+
+ // Check decimal places
+ const decimalPlaces = (stakeAmount.split('.')[1] || '').length;
+ if (decimalPlaces > 9) {
+ return 'Maximum 9 decimal places allowed for stake amount';
+ }
+
+ // End time validation
+ if (!endTime) {
+ return 'End time is required';
+ }
+
+ const selectedDate = new Date(endTime);
+ if (isNaN(selectedDate.getTime())) {
+ return 'Please enter a valid end date and time';
+ }
+
+ const endUnix = Math.floor(selectedDate.getTime() / 1000);
+ const nowUnix = Math.floor(Date.now() / 1000);
+ const duration = endUnix - nowUnix;
+
+ if (duration <= 0) {
+ return 'End time must be in the future';
+ }
+
+ if (duration < networkConfig.minEndSeconds) {
+ const minDuration = isTestnet ? '24 hours' : '2 weeks';
+ return `End time must be at least ${minDuration} from now`;
+ }
+
+ if (duration > MAX_END_SECONDS) {
+ return 'End time must be within 1 year';
+ }
+
+ // Delegator reward percentage validation
+ if (!delegatorRewardPercentage || delegatorRewardPercentage.trim() === '') {
+ return 'Delegator reward percentage is required';
+ }
+
+ const drp = Number(delegatorRewardPercentage);
+ if (!Number.isFinite(drp)) {
+ return 'Please enter a valid delegator reward percentage';
+ }
+
+ if (drp < 2) {
+ return 'Minimum delegator reward percentage is 2%';
+ }
+
+ if (drp > 100) {
+ return 'Maximum delegator reward percentage is 100%';
+ }
+
+ // Check decimal places for percentage
+ const percentageDecimalPlaces = (delegatorRewardPercentage.split('.')[1] || '').length;
+ if (percentageDecimalPlaces > 2) {
+ return 'Maximum 2 decimal places allowed for delegator reward percentage';
+ }
+
+ return null;
+ }, [pChainAddress, validator, stakeAmount, endTime, delegatorRewardPercentage, networkConfig, isTestnet]);
+
+ // Compute form validity reactively
+ const isFormValid = useMemo(() => {
+ return validateForm() === null;
+ }, [validateForm]);
+
+ const submitStake = useCallback(async () => {
+ setError(undefined);
+ setResult(undefined);
+
+ const validationError = validateForm();
+ if (validationError) {
+ const error = { message: validationError };
+ setError(error);
+ onError?.(error);
+ return;
+ }
+
+ if (!walletClient) {
+ const error = { message: "Wallet client not found" };
+ setError(error);
+ onError?.(error);
+ return;
+ }
+
+ try {
+ setStatus('preparing');
+
+ const endUnix = Math.floor(new Date(endTime).getTime() / 1000);
+
+ const { tx } = await walletClient.pChain.prepareAddPermissionlessValidatorTxn({
+ nodeId: validator!.nodeID,
+ stakeInAvax: Number(stakeAmount),
+ end: endUnix,
+ rewardAddresses: [pChainAddress!],
+ delegatorRewardAddresses: [pChainAddress!],
+ delegatorRewardPercentage: Number(delegatorRewardPercentage),
+ threshold: 1,
+ locktime: 0,
+ publicKey: validator!.nodePOP.publicKey,
+ signature: validator!.nodePOP.proofOfPossession,
+ });
+
+ setStatus('pending');
+
+ const { txHash } = await walletClient.sendXPTransaction({
+ tx: tx,
+ chainAlias: 'P',
+ });
+
+ const stakeResult: StakeResult = {
+ txHash,
+ nodeId: validator!.nodeID,
+ stakeAmount,
+ endTime: endUnix,
+ };
+
+ setResult(stakeResult);
+ setStatus('success');
+ onSuccess?.(stakeResult);
+ } catch (e: any) {
+ console.error(e);
+ const error = { message: e.message };
+ setError(error);
+ setStatus('error');
+ onError?.(error);
+ }
+ }, [
+ validateForm,
+ walletClient,
+ endTime,
+ validator,
+ stakeAmount,
+ pChainAddress,
+ delegatorRewardPercentage,
+ onSuccess,
+ onError,
+ ]);
+
+ const clearError = useCallback(() => {
+ setError(undefined);
+ }, []);
+
+ const reset = useCallback(() => {
+ setStatus('idle');
+ setValidator(null);
+ setStakeAmount('');
+ setEndTime('');
+ setDelegatorRewardPercentage(DEFAULT_DELEGATOR_REWARD_PERCENTAGE);
+ setError(undefined);
+ setResult(undefined);
+ initializeDefaults();
+ }, [initializeDefaults]);
+
+ const contextValue = useMemo((): StakeContextType => ({
+ status,
+ validator,
+ stakeAmount,
+ endTime,
+ delegatorRewardPercentage,
+ error,
+ result,
+ networkConfig,
+ isTestnet,
+ isFormValid,
+ setValidator,
+ setStakeAmount,
+ setEndTime,
+ setDelegatorRewardPercentage,
+ setEndInDays,
+ submitStake,
+ clearError,
+ reset,
+ }), [
+ status,
+ validator,
+ stakeAmount,
+ endTime,
+ delegatorRewardPercentage,
+ error,
+ result,
+ networkConfig,
+ isTestnet,
+ isFormValid,
+ setEndInDays,
+ submitStake,
+ clearError,
+ reset,
+ ]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/ui/src/stake/components/StakeValidatorInput.tsx b/ui/src/stake/components/StakeValidatorInput.tsx
new file mode 100644
index 00000000..be2ad494
--- /dev/null
+++ b/ui/src/stake/components/StakeValidatorInput.tsx
@@ -0,0 +1,309 @@
+'use client';
+import { useState, useEffect } from 'react';
+import { Button } from '../../components/ui/button';
+import { Input } from '../../components/ui/input';
+import { Label } from '../../components/ui/label';
+import { cn } from '../../styles/theme';
+import { useStakeContext } from './StakeProvider';
+import type { StakeValidatorInputProps, ValidatorCredentials } from '../types';
+
+export function StakeValidatorInput({
+ className,
+ label = "Node Credentials",
+ disabled = false,
+}: StakeValidatorInputProps) {
+ const { setValidator } = useStakeContext();
+ const [nodeEndpoint, setNodeEndpoint] = useState('127.0.0.1:9650');
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [fetchedFromRPC, setFetchedFromRPC] = useState(false);
+
+ // Manual input states
+ const [manualNodeID, setManualNodeID] = useState('');
+ const [manualPublicKey, setManualPublicKey] = useState('');
+ const [manualProofOfPossession, setManualProofOfPossession] = useState('');
+
+ // Validation states for real-time border colors
+ const [nodeIDError, setNodeIDError] = useState(null);
+ const [publicKeyError, setPublicKeyError] = useState(null);
+ const [proofError, setProofError] = useState(null);
+
+ const fetchNodeInfo = async () => {
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const response = await fetch(`http://${nodeEndpoint}/ext/info`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'info.getNodeID',
+ id: 1,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ if (data.error) {
+ throw new Error(data.error.message);
+ }
+
+ // Validate the response structure
+ if (!data.result?.nodeID) {
+ throw new Error('Invalid response: missing nodeID');
+ }
+
+ if (!data.result?.nodePOP?.publicKey || !data.result?.nodePOP?.proofOfPossession) {
+ throw new Error('Invalid response: missing BLS credentials (nodePOP)');
+ }
+
+ const credentials: ValidatorCredentials = {
+ nodeID: data.result.nodeID,
+ nodePOP: {
+ publicKey: data.result.nodePOP.publicKey,
+ proofOfPossession: data.result.nodePOP.proofOfPossession,
+ },
+ };
+
+ // Populate the manual input fields with fetched data
+ setManualNodeID(credentials.nodeID);
+ setManualPublicKey(credentials.nodePOP.publicKey);
+ setManualProofOfPossession(credentials.nodePOP.proofOfPossession);
+ setFetchedFromRPC(true);
+ setValidator(credentials);
+ } catch (e: any) {
+ setError(e.message || 'Failed to fetch node information');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const clearValidator = () => {
+ setValidator(null);
+ setError(null);
+ setFetchedFromRPC(false);
+ // Clear manual inputs
+ setManualNodeID('');
+ setManualPublicKey('');
+ setManualProofOfPossession('');
+ };
+
+ const validateHexString = (value: string, expectedLength: number): boolean => {
+ if (!value.startsWith('0x')) return false;
+ const hexPart = value.slice(2);
+ if (hexPart.length !== expectedLength - 2) return false;
+ return /^[0-9a-fA-F]+$/.test(hexPart);
+ };
+
+ // Real-time validation functions
+ const validateNodeID = (value: string): string | null => {
+ if (!value.trim()) return null; // Empty is not an error, just not complete
+
+ if (!value.startsWith('NodeID-')) {
+ return 'Node ID must start with "NodeID-"';
+ }
+
+ if (value.length < 15 || value.length > 60) {
+ return 'Node ID format appears invalid. Expected format: NodeID-[base58string]';
+ }
+
+ const nodeIdPart = value.slice(7);
+ if (!/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/.test(nodeIdPart)) {
+ return 'Node ID contains invalid characters. Must use base58 encoding.';
+ }
+
+ return null;
+ };
+
+ const validatePublicKey = (value: string): string | null => {
+ if (!value.trim()) return null;
+
+ if (!validateHexString(value, 98)) {
+ return 'BLS Public Key must be a 98-character hex string starting with "0x"';
+ }
+
+ const publicKeyHex = value.slice(2);
+ if (publicKeyHex === '0'.repeat(96)) {
+ return 'BLS Public Key cannot be all zeros';
+ }
+
+ return null;
+ };
+
+ const validateProofOfPossession = (value: string): string | null => {
+ if (!value.trim()) return null;
+
+ if (!validateHexString(value, 194)) {
+ return 'Proof of Possession must be a 194-character hex string starting with "0x"';
+ }
+
+ const proofHex = value.slice(2);
+ if (proofHex === '0'.repeat(192)) {
+ return 'Proof of Possession cannot be all zeros';
+ }
+
+ return null;
+ };
+
+ // Real-time validation effects
+ useEffect(() => {
+ setNodeIDError(validateNodeID(manualNodeID));
+ }, [manualNodeID]);
+
+ useEffect(() => {
+ setPublicKeyError(validatePublicKey(manualPublicKey));
+ }, [manualPublicKey]);
+
+ useEffect(() => {
+ setProofError(validateProofOfPossession(manualProofOfPossession));
+ }, [manualProofOfPossession]);
+
+ // Auto-submit when all fields are valid and not empty
+ useEffect(() => {
+ if (manualNodeID && manualPublicKey && manualProofOfPossession &&
+ !nodeIDError && !publicKeyError && !proofError && !fetchedFromRPC) {
+ const credentials: ValidatorCredentials = {
+ nodeID: manualNodeID.trim(),
+ nodePOP: {
+ publicKey: manualPublicKey.trim(),
+ proofOfPossession: manualProofOfPossession.trim(),
+ },
+ };
+ setValidator(credentials);
+ } else if (!manualNodeID && !manualPublicKey && !manualProofOfPossession) {
+ // Clear validator if all fields are empty
+ setValidator(null);
+ }
+ }, [manualNodeID, manualPublicKey, manualProofOfPossession, nodeIDError, publicKeyError, proofError, fetchedFromRPC, setValidator]);
+
+ return (
+
+
+
{label}
+
+ {/* RPC Fetch Section - always show */}
+
+
+ setNodeEndpoint(e.target.value)}
+ placeholder="127.0.0.1:9650"
+ disabled={disabled || isLoading}
+ className="flex-1 h-10 min-h-[2.5rem] max-h-[2.5rem]"
+ />
+
+ {isLoading ? 'Fetching...' : 'Fetch from URL'}
+
+
+ Clear
+
+
+
+
+ Note: This queries your node's info.getNodeID API endpoint
+ to retrieve the NodeID and BLS credentials. Ensure your AvalancheGo node is running
+ and accessible at the specified endpoint.
+
+
+
+ {/* Manual Input Fields - always show */}
+
+
+
Node ID
+
setManualNodeID(e.target.value)}
+ placeholder="NodeID-..."
+ disabled={disabled || fetchedFromRPC}
+ className={cn("font-mono", nodeIDError ? "border-destructive" : "")}
+ />
+ {nodeIDError ? (
+
+ {nodeIDError}
+
+ ) : (
+
+ Must start with "NodeID-"
+
+ )}
+
+
+
+
BLS Public Key
+
setManualPublicKey(e.target.value)}
+ placeholder="0x..."
+ disabled={disabled || fetchedFromRPC}
+ className={cn("font-mono", publicKeyError ? "border-destructive" : "")}
+ />
+ {publicKeyError ? (
+
+ {publicKeyError}
+
+ ) : (
+
+ BLS public key: 98-character hex string starting with "0x" (96 hex chars + 0x prefix)
+
+ )}
+
+
+
+
Proof of Possession
+
setManualProofOfPossession(e.target.value)}
+ placeholder="0x..."
+ disabled={disabled || fetchedFromRPC}
+ className={cn("font-mono", proofError ? "border-destructive" : "")}
+ />
+ {proofError ? (
+
+ {proofError}
+
+ ) : (
+
+ BLS proof of possession: 194-character hex string starting with "0x" (192 hex chars + 0x prefix)
+
+ )}
+
+
+ {/* Show fetch source info */}
+ {fetchedFromRPC && (
+
+ Fetched from RPC: {nodeEndpoint}
+
+ )}
+
+
+ {/* Error Display */}
+ {error && (
+
+ {error}
+
+ )}
+
+
+ );
+}
diff --git a/ui/src/stake/hooks/useStakeContext.ts b/ui/src/stake/hooks/useStakeContext.ts
new file mode 100644
index 00000000..73a0bd3f
--- /dev/null
+++ b/ui/src/stake/hooks/useStakeContext.ts
@@ -0,0 +1 @@
+export { useStakeContext } from '../components/StakeProvider';
diff --git a/ui/src/stake/index.ts b/ui/src/stake/index.ts
new file mode 100644
index 00000000..e09990ea
--- /dev/null
+++ b/ui/src/stake/index.ts
@@ -0,0 +1,27 @@
+// Components
+export { Stake } from './components/Stake';
+export { StakeProvider } from './components/StakeProvider';
+export { StakeValidatorInput } from './components/StakeValidatorInput';
+export { StakeAmountInput } from './components/StakeAmountInput';
+export { StakeDurationInput } from './components/StakeDurationInput';
+export { StakeButton } from './components/StakeButton';
+export { StakeMessage } from './components/StakeMessage';
+
+// Hooks
+export { useStakeContext } from './hooks/useStakeContext';
+
+// Types
+export type {
+ StakeStatus,
+ StakeError,
+ StakeResult,
+ ValidatorCredentials,
+ NetworkConfig,
+ StakeContextType,
+ StakeProviderProps,
+ StakeValidatorInputProps,
+ StakeAmountInputProps,
+ StakeDurationInputProps,
+ StakeButtonProps,
+ StakeMessageProps,
+} from './types';
diff --git a/ui/src/stake/types.ts b/ui/src/stake/types.ts
new file mode 100644
index 00000000..0f88a3fe
--- /dev/null
+++ b/ui/src/stake/types.ts
@@ -0,0 +1,118 @@
+import type { ReactNode } from 'react';
+
+export type StakeStatus = 'idle' | 'preparing' | 'pending' | 'success' | 'error';
+
+export type StakeError = {
+ message: string;
+ code?: string | number;
+};
+
+export type StakeResult = {
+ txHash: string;
+ nodeId: string;
+ stakeAmount: string;
+ endTime: number;
+};
+
+export type ValidatorCredentials = {
+ nodeID: string;
+ nodePOP: {
+ publicKey: string;
+ proofOfPossession: string;
+ };
+};
+
+export type NetworkConfig = {
+ minStakeAvax: number;
+ minEndSeconds: number;
+ defaultDays: number;
+ presets: Array<{ label: string; days: number }>;
+};
+
+export type StakeContextType = {
+ /** Current stake status */
+ status: StakeStatus;
+ /** Validator credentials */
+ validator: ValidatorCredentials | null;
+ /** Stake amount in AVAX */
+ stakeAmount: string;
+ /** End time as ISO string */
+ endTime: string;
+ /** Delegator reward percentage */
+ delegatorRewardPercentage: string;
+ /** Current error if any */
+ error?: StakeError;
+ /** Last successful stake result */
+ result?: StakeResult;
+ /** Network configuration */
+ networkConfig: NetworkConfig;
+ /** Whether on testnet */
+ isTestnet: boolean;
+ /** Whether all form inputs are valid */
+ isFormValid: boolean;
+ /** Set validator credentials */
+ setValidator: (validator: ValidatorCredentials | null) => void;
+ /** Set stake amount */
+ setStakeAmount: (amount: string) => void;
+ /** Set end time */
+ setEndTime: (endTime: string) => void;
+ /** Set delegator reward percentage */
+ setDelegatorRewardPercentage: (percentage: string) => void;
+ /** Set end time in days from now */
+ setEndInDays: (days: number) => void;
+ /** Submit stake transaction */
+ submitStake: () => Promise;
+ /** Clear error */
+ clearError: () => void;
+ /** Reset form */
+ reset: () => void;
+};
+
+export type StakeProviderProps = {
+ children: ReactNode;
+ /** Callback when stake is successful */
+ onSuccess?: (result: StakeResult) => void;
+ /** Callback when stake fails */
+ onError?: (error: StakeError) => void;
+ /** Custom network configuration */
+ networkConfig?: Partial;
+};
+
+export type StakeValidatorInputProps = {
+ className?: string;
+ label?: string;
+ disabled?: boolean;
+ /** Default input mode */
+ defaultMode?: 'fetch' | 'manual';
+ /** Whether to allow manual entry mode */
+ allowManualEntry?: boolean;
+};
+
+export type StakeAmountInputProps = {
+ className?: string;
+ label?: string;
+ disabled?: boolean;
+};
+
+export type StakeDurationInputProps = {
+ className?: string;
+ label?: string;
+ disabled?: boolean;
+ showPresets?: boolean;
+};
+
+export type StakeButtonProps = {
+ className?: string;
+ children?: ReactNode;
+ disabled?: boolean;
+ loadingText?: string;
+};
+
+export type StakeMessageProps = {
+ className?: string;
+};
+
+export type StakeToastProps = {
+ className?: string;
+ position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
+};
diff --git a/ui/src/styles/index.css b/ui/src/styles/index.css
new file mode 100644
index 00000000..1d2f6323
--- /dev/null
+++ b/ui/src/styles/index.css
@@ -0,0 +1,242 @@
+@import 'tailwindcss/base';
+@import 'tailwindcss/components';
+@import 'tailwindcss/utilities';
+
+/* Multi-Theme UI Kit CSS Variables */
+:root {
+ /* Core theme variables */
+ --radius: 0.5rem;
+}
+
+/* Avalanche Theme - Light */
+:root,
+[data-theme='avalanche'] {
+ /* Light theme colors (OKLCH) */
+ --background: oklch(96.96% 0 0);
+ --foreground: oklch(0.129 0.042 264.695);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.129 0.042 264.695);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.129 0.042 264.695);
+ --primary: #e84142;
+ --primary-foreground: oklch(0.984 0.003 247.858);
+ --secondary: oklch(0.968 0.007 247.896);
+ --secondary-foreground: oklch(0.208 0.042 265.755);
+ --muted: oklch(0.968 0.007 247.896);
+ --muted-foreground: oklch(0.554 0.046 257.417);
+ --accent: oklch(0.929 0.013 255.508);
+ --accent-foreground: oklch(0.129 0.042 264.695);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.929 0.013 255.508);
+ --input: oklch(0.929 0.013 255.508);
+ --ring: oklch(0.704 0.04 256.788);
+}
+
+/* Avalanche Theme - Dark */
+.dark[data-theme='avalanche'],
+.dark:root,
+[data-theme='avalanche'].dark {
+ --background: oklch(0 0 0);
+ --foreground: oklch(0.98 0 0);
+ --card: oklch(0 0 0);
+ --card-foreground: oklch(0.98 0 0);
+ --popover: oklch(0 0 0);
+ --popover-foreground: oklch(0.98 0 0);
+ --primary: #e84142;
+ --primary-foreground: oklch(0.98 0 0);
+ --secondary: oklch(0.12 0 0);
+ --secondary-foreground: oklch(0.9 0 0);
+ --muted: oklch(0.16 0 0);
+ --muted-foreground: oklch(0.7 0 0);
+ --accent: oklch(0.14 0 0);
+ --accent-foreground: oklch(0.92 0 0);
+ --destructive: oklch(0.6 0.02 30);
+ --border: oklch(0.4 0 0);
+ --input: oklch(0.2 0 0);
+ --ring: oklch(0.7 0 0);
+}
+
+/* Cyber Theme - Light */
+[data-theme='cyber'] {
+ --background: oklch(98% 0.005 240);
+ --foreground: oklch(0.15 0.02 240);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.15 0.02 240);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.15 0.02 240);
+ --primary: #00ffff;
+ --primary-foreground: oklch(0.1 0.02 240);
+ --secondary: oklch(0.95 0.01 240);
+ --secondary-foreground: oklch(0.2 0.02 240);
+ --muted: oklch(0.95 0.01 240);
+ --muted-foreground: oklch(0.5 0.02 240);
+ --accent: oklch(0.9 0.02 240);
+ --accent-foreground: oklch(0.15 0.02 240);
+ --destructive: #ff0080;
+ --border: oklch(0.85 0.02 240);
+ --input: oklch(0.85 0.02 240);
+ --ring: #00ffff;
+}
+
+/* Cyber Theme - Dark */
+[data-theme='cyber'].dark {
+ --background: oklch(0.05 0.02 240);
+ --foreground: #00ffff;
+ --card: oklch(0.08 0.02 240);
+ --card-foreground: #00ffff;
+ --popover: oklch(0.08 0.02 240);
+ --popover-foreground: #00ffff;
+ --primary: #00ffff;
+ --primary-foreground: oklch(0.05 0.02 240);
+ --secondary: oklch(0.15 0.02 240);
+ --secondary-foreground: #00ffff;
+ --muted: oklch(0.12 0.02 240);
+ --muted-foreground: oklch(0.7 0.02 240);
+ --accent: oklch(0.2 0.02 240);
+ --accent-foreground: #00ffff;
+ --destructive: #ff0080;
+ --border: oklch(0.4 0.02 240);
+ --input: oklch(0.25 0.02 240);
+ --ring: #00ffff;
+}
+
+/* Matrix Theme - Light */
+[data-theme='matrix'] {
+ --background: oklch(98% 0.005 142);
+ --foreground: oklch(0.1 0.03 142);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.1 0.03 142);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.1 0.03 142);
+ --primary: #00ff41;
+ --primary-foreground: oklch(0.05 0.03 142);
+ --secondary: oklch(0.95 0.01 142);
+ --secondary-foreground: oklch(0.15 0.03 142);
+ --muted: oklch(0.95 0.01 142);
+ --muted-foreground: oklch(0.5 0.02 142);
+ --accent: oklch(0.9 0.02 142);
+ --accent-foreground: oklch(0.1 0.03 142);
+ --destructive: #ff0041;
+ --border: oklch(0.85 0.02 142);
+ --input: oklch(0.85 0.02 142);
+ --ring: #00ff41;
+}
+
+/* Matrix Theme - Dark */
+[data-theme='matrix'].dark {
+ --background: oklch(0.02 0.01 142);
+ --foreground: #00ff41;
+ --card: oklch(0.04 0.01 142);
+ --card-foreground: #00ff41;
+ --popover: oklch(0.04 0.01 142);
+ --popover-foreground: #00ff41;
+ --primary: #00ff41;
+ --primary-foreground: oklch(0.02 0.01 142);
+ --secondary: oklch(0.08 0.02 142);
+ --secondary-foreground: #00ff41;
+ --muted: oklch(0.06 0.01 142);
+ --muted-foreground: oklch(0.6 0.02 142);
+ --accent: oklch(0.1 0.02 142);
+ --accent-foreground: #00ff41;
+ --destructive: #ff0041;
+ --border: oklch(0.2 0.02 142);
+ --input: oklch(0.12 0.02 142);
+ --ring: #00ff41;
+}
+
+/* Amber Theme - Light */
+[data-theme='amber'] {
+ --background: oklch(97% 0.015 65);
+ --foreground: oklch(0.25 0.05 45);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.25 0.05 45);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.25 0.05 45);
+ --primary: #d97706;
+ --primary-foreground: oklch(0.98 0.01 65);
+ --secondary: oklch(0.95 0.02 50);
+ --secondary-foreground: oklch(0.3 0.05 45);
+ --muted: oklch(0.94 0.015 60);
+ --muted-foreground: oklch(0.55 0.04 50);
+ --accent: oklch(0.92 0.02 55);
+ --accent-foreground: oklch(0.25 0.05 45);
+ --destructive: #dc2626;
+ --border: oklch(0.88 0.02 55);
+ --input: oklch(0.88 0.02 55);
+ --ring: #d97706;
+}
+
+/* Amber Theme - Dark */
+[data-theme='amber'].dark {
+ --background: oklch(0.12 0.02 35);
+ --foreground: oklch(0.92 0.02 65);
+ --card: oklch(0.15 0.02 40);
+ --card-foreground: oklch(0.92 0.02 65);
+ --popover: oklch(0.15 0.02 40);
+ --popover-foreground: oklch(0.92 0.02 65);
+ --primary: #f59e0b;
+ --primary-foreground: oklch(0.12 0.02 35);
+ --secondary: oklch(0.2 0.03 45);
+ --secondary-foreground: oklch(0.9 0.02 60);
+ --muted: oklch(0.18 0.02 40);
+ --muted-foreground: oklch(0.65 0.03 55);
+ --accent: oklch(0.22 0.03 50);
+ --accent-foreground: oklch(0.9 0.02 60);
+ --destructive: #ef4444;
+ --border: oklch(0.3 0.03 45);
+ --input: oklch(0.25 0.03 45);
+ --ring: #f59e0b;
+}
+
+/* Amethyst Theme - Light */
+[data-theme='amethyst'] {
+ --background: oklch(98% 0.01 300);
+ --foreground: oklch(0.2 0.04 280);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.2 0.04 280);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.2 0.04 280);
+ --primary: #9333ea;
+ --primary-foreground: oklch(0.98 0.01 300);
+ --secondary: oklch(0.95 0.015 290);
+ --secondary-foreground: oklch(0.25 0.04 280);
+ --muted: oklch(0.94 0.01 295);
+ --muted-foreground: oklch(0.5 0.03 285);
+ --accent: oklch(0.92 0.02 290);
+ --accent-foreground: oklch(0.2 0.04 280);
+ --destructive: #dc2626;
+ --border: oklch(0.88 0.02 295);
+ --input: oklch(0.88 0.02 295);
+ --ring: #9333ea;
+}
+
+/* Amethyst Theme - Dark */
+[data-theme='amethyst'].dark {
+ --background: oklch(0.1 0.02 280);
+ --foreground: oklch(0.95 0.02 300);
+ --card: oklch(0.12 0.02 285);
+ --card-foreground: oklch(0.95 0.02 300);
+ --popover: oklch(0.12 0.02 285);
+ --popover-foreground: oklch(0.95 0.02 300);
+ --primary: #a855f7;
+ --primary-foreground: oklch(0.1 0.02 280);
+ --secondary: oklch(0.18 0.03 290);
+ --secondary-foreground: oklch(0.92 0.02 300);
+ --muted: oklch(0.15 0.02 285);
+ --muted-foreground: oklch(0.65 0.03 295);
+ --accent: oklch(0.2 0.03 290);
+ --accent-foreground: oklch(0.92 0.02 300);
+ --destructive: #ef4444;
+ --border: oklch(0.3 0.03 290);
+ --input: oklch(0.25 0.03 290);
+ --ring: #a855f7;
+}
+
+
+/* Base styles */
+body {
+ color: var(--foreground);
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+
diff --git a/ui/src/styles/theme.ts b/ui/src/styles/theme.ts
new file mode 100644
index 00000000..7de6e83c
--- /dev/null
+++ b/ui/src/styles/theme.ts
@@ -0,0 +1,31 @@
+import { type ClassValue, clsx } from 'clsx';
+
+export function cn(...inputs: ClassValue[]) {
+ return clsx(inputs);
+}
+
+export const text = {
+ base: 'font-sans text-foreground',
+ body: 'font-sans font-normal text-base text-foreground',
+ caption: 'font-sans font-semibold text-xs text-foreground',
+ headline: 'font-sans font-semibold text-foreground',
+ label1: 'font-sans font-semibold text-sm text-foreground',
+ label2: 'font-sans text-sm text-foreground',
+ legal: 'font-sans text-xs text-foreground',
+ title1: 'font-sans font-semibold text-2xl text-foreground',
+ title3: 'font-sans font-semibold text-xl text-foreground',
+} as const;
+
+export const pressable = {
+ default: 'cursor-pointer bg-background hover:bg-accent active:bg-accent focus:bg-accent text-foreground',
+ alternate: 'cursor-pointer bg-muted hover:bg-accent active:bg-accent focus:bg-accent text-foreground',
+ inverse: 'cursor-pointer bg-foreground hover:bg-foreground/90 active:bg-foreground/90 focus:bg-foreground/90 text-background',
+ primary: 'cursor-pointer bg-primary hover:bg-primary/90 active:bg-primary/90 focus:bg-primary/90 text-primary-foreground',
+ secondary: 'cursor-pointer bg-secondary hover:bg-secondary/80 active:bg-secondary/80 focus:bg-secondary/80 text-secondary-foreground',
+ avalancheBranding: 'cursor-pointer bg-primary hover:bg-primary/90 active:bg-primary/90 text-primary-foreground',
+ disabled: 'opacity-50 pointer-events-none',
+} as const;
+
+export const border = {
+ lineDefault: 'border border-border',
+} as const;
\ No newline at end of file
diff --git a/ui/src/theme.tsx b/ui/src/theme.tsx
new file mode 100644
index 00000000..5bb45d79
--- /dev/null
+++ b/ui/src/theme.tsx
@@ -0,0 +1,86 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+
+export type Theme = 'avalanche' | 'cyber' | 'matrix' | 'amber' | 'amethyst';
+export type Mode = 'light' | 'dark';
+
+interface ThemeContextType {
+ theme: Theme;
+ mode: Mode;
+ setTheme: (theme: Theme) => void;
+ setMode: (mode: Mode) => void;
+ toggleMode: () => void;
+}
+
+const ThemeContext = createContext(undefined);
+
+export function useTheme() {
+ const context = useContext(ThemeContext);
+ if (context === undefined) {
+ throw new Error('useTheme must be used within a ThemeProvider');
+ }
+ return context;
+}
+
+interface ThemeProviderProps {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ defaultMode?: Mode;
+ storageKey?: string;
+}
+
+export function ThemeProvider({
+ children,
+ defaultTheme = 'avalanche',
+ defaultMode = 'light',
+ storageKey = 'ui-theme',
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(defaultTheme);
+ const [mode, setMode] = useState(defaultMode);
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+
+ // Remove previous theme and mode classes
+ root.classList.remove('light', 'dark');
+ root.removeAttribute('data-theme');
+
+ // Apply new theme and mode
+ root.setAttribute('data-theme', theme);
+ root.classList.add(mode);
+
+ // Store in localStorage
+ localStorage.setItem(storageKey, JSON.stringify({ theme, mode }));
+ }, [theme, mode, storageKey]);
+
+ useEffect(() => {
+ // Load from localStorage on mount
+ try {
+ const stored = localStorage.getItem(storageKey);
+ if (stored) {
+ const { theme: storedTheme, mode: storedMode } = JSON.parse(stored);
+ if (storedTheme) setTheme(storedTheme);
+ if (storedMode) setMode(storedMode);
+ }
+ } catch (error) {
+ console.warn('Failed to load theme from localStorage:', error);
+ }
+ }, [storageKey]);
+
+ const toggleMode = () => {
+ setMode(mode === 'light' ? 'dark' : 'light');
+ };
+
+ const value = {
+ theme,
+ mode,
+ setTheme,
+ setMode,
+ toggleMode,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/ui/src/token/components/TokenChip.tsx b/ui/src/token/components/TokenChip.tsx
new file mode 100644
index 00000000..20edf921
--- /dev/null
+++ b/ui/src/token/components/TokenChip.tsx
@@ -0,0 +1,50 @@
+'use client';
+
+import { Button } from '../../components/ui/button';
+import { Badge } from '../../components/ui/badge';
+import { cn, text } from '../../styles/theme';
+import type { TokenChipProps } from '../types';
+import { TokenImage } from './TokenImage';
+
+/**
+ * Small button that displays a given token symbol and image.
+ */
+export function TokenChip({
+ token,
+ onClick,
+ className,
+ isPressable = true,
+}: TokenChipProps) {
+ if (!isPressable) {
+ // Non-interactive badge variant
+ return (
+
+
+ {token.symbol}
+
+ );
+ }
+
+ return (
+ onClick?.(token)}
+ >
+
+ {token.symbol}
+
+ );
+}
+
diff --git a/ui/src/token/components/TokenImage.tsx b/ui/src/token/components/TokenImage.tsx
new file mode 100644
index 00000000..d16590e8
--- /dev/null
+++ b/ui/src/token/components/TokenImage.tsx
@@ -0,0 +1,65 @@
+'use client';
+
+import { useMemo, useState } from 'react';
+import { cn } from '../../styles/theme';
+import type { TokenImageProps } from '../types';
+import { getTokenImageColor } from '../utils/getTokenImageColor';
+
+/**
+ * TokenImage component displays a token's logo image
+ */
+export function TokenImage({ className, size = 24, token }: TokenImageProps) {
+ const { image, name, symbol } = token;
+ const [imageError, setImageError] = useState(false);
+
+ const styles = useMemo(() => {
+ return {
+ container: {
+ width: `${size}px`,
+ height: `${size}px`,
+ minWidth: `${size}px`,
+ minHeight: `${size}px`,
+ },
+ };
+ }, [size]);
+
+ // Show gradient fallback if no image or image failed to load
+ if (!image || imageError) {
+ return (
+
+
+ {symbol.charAt(0).toUpperCase()}
+
+
+ );
+ }
+
+ return (
+
+
setImageError(true)}
+ />
+
+ );
+}
+
diff --git a/ui/src/token/components/TokenRow.tsx b/ui/src/token/components/TokenRow.tsx
new file mode 100644
index 00000000..65a08c4a
--- /dev/null
+++ b/ui/src/token/components/TokenRow.tsx
@@ -0,0 +1,121 @@
+'use client';
+
+import { memo } from 'react';
+import { Button } from '../../components/ui/button';
+import { Badge } from '../../components/ui/badge';
+import { cn, pressable, text } from '../../styles/theme';
+import type { TokenRowProps } from '../types';
+import { formatAmount } from '../utils/formatAmount';
+import { TokenImage } from './TokenImage';
+import { AlertTriangle, ShieldCheck, Coins } from 'lucide-react';
+
+/**
+ * TokenRow component for displaying a token in a list format
+ */
+export const TokenRow = memo(function TokenRow({
+ className,
+ token,
+ amount,
+ value,
+ reputation,
+ isNative = false,
+ variant = 'ghost',
+ onClick,
+ hideImage,
+ hideSymbol,
+ as,
+}: TokenRowProps) {
+ const Component = as ?? Button;
+
+ // When using a div (as="div"), we need to apply variant styles manually
+ const variantStyles = variant === 'outline'
+ ? 'border border-input bg-background hover:bg-accent hover:text-accent-foreground rounded-md'
+ : variant === 'default'
+ ? 'bg-primary text-primary-foreground hover:bg-primary/90 rounded-md'
+ : 'hover:bg-accent hover:text-accent-foreground rounded-md'; // Ghost variant: no background, but has hover effects
+
+ // For ghost variant with div, apply cursor-pointer and hover effects, but not bg-background
+ const pressableStyles = as && variant === 'ghost'
+ ? 'cursor-pointer'
+ : as
+ ? pressable.default
+ : undefined;
+
+ return (
+ onClick?.(token)}
+ >
+
+ {!hideImage && }
+
+
+
+ {token.name.trim()}
+
+ {isNative && (
+
+
+
+ )}
+ {reputation === 'Malicious' && (
+
+
+
+ )}
+ {reputation === 'Benign' && (
+
+
+
+ )}
+
+ {!hideSymbol && (
+
+ {token.symbol}
+
+ )}
+
+
+ {(amount !== undefined || value !== undefined) && (
+
+ {amount !== undefined && (
+
+ {formatAmount(amount, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: Number(amount) < 1 ? 5 : 2,
+ })}
+
+ )}
+ {value !== undefined && (
+
+ {typeof value === 'number'
+ ? `$${value.toFixed(2)}`
+ : value}
+
+ )}
+
+ )}
+
+ );
+});
+
diff --git a/ui/src/token/components/TokenSelectButton.tsx b/ui/src/token/components/TokenSelectButton.tsx
new file mode 100644
index 00000000..cfd03f9f
--- /dev/null
+++ b/ui/src/token/components/TokenSelectButton.tsx
@@ -0,0 +1,48 @@
+'use client';
+
+import { forwardRef } from 'react';
+import { ChevronDown } from 'lucide-react';
+import { Button } from '../../components/ui/button';
+import { cn, text } from '../../styles/theme';
+import type { TokenSelectButtonProps } from '../types';
+import { TokenImage } from './TokenImage';
+
+/**
+ * TokenSelectButton - trigger button for token selection
+ */
+export const TokenSelectButton = forwardRef(
+ ({ className, isOpen, onClick, token }, ref) => {
+ return (
+
+ {token ? (
+ <>
+
+ {token.symbol}
+ >
+ ) : (
+
+ Select token
+
+ )}
+
+
+ );
+ }
+);
+
+TokenSelectButton.displayName = 'TokenSelectButton';
+
diff --git a/ui/src/token/components/TokenSelectDropdown.tsx b/ui/src/token/components/TokenSelectDropdown.tsx
new file mode 100644
index 00000000..c86e760e
--- /dev/null
+++ b/ui/src/token/components/TokenSelectDropdown.tsx
@@ -0,0 +1,98 @@
+'use client';
+
+import { useCallback } from 'react';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+} from '../../components/ui/select';
+import { cn, text } from '../../styles/theme';
+import type { TokenSelectDropdownProps } from '../types';
+import { TokenImage } from './TokenImage';
+
+/**
+ * TokenSelectDropdown - dropdown for selecting tokens
+ */
+export function TokenSelectDropdown({
+ options,
+ setToken,
+ token,
+ className,
+}: TokenSelectDropdownProps) {
+ const handleValueChange = useCallback((value: string) => {
+ const selectedToken = options.find((t) => {
+ const tokenValue = t.address || t.symbol;
+ return tokenValue === value;
+ });
+ if (selectedToken) {
+ setToken(selectedToken);
+ }
+ }, [options, setToken]);
+
+ const hasTokens = options.length > 0;
+ const currentValue = token ? (token.address || token.symbol) : '';
+
+ return (
+
+
+
+ {token ? (
+ <>
+
+ {token.symbol}
+ >
+ ) : (
+
+ {hasTokens ? 'Select token' : 'No tokens available'}
+
+ )}
+
+
+
+ {hasTokens ? (
+
+
+
+ Select Token
+
+
+
+ {options.map((tokenOption) => {
+ const optionValue = tokenOption.address || tokenOption.symbol;
+ return (
+
+
+
+
+
+ {tokenOption.name}
+
+
+ {tokenOption.symbol}
+
+
+
+
+ );
+ })}
+
+
+ ) : (
+
+ No tokens available
+
+ )}
+
+
+ );
+}
+
diff --git a/ui/src/token/index.ts b/ui/src/token/index.ts
new file mode 100644
index 00000000..dba4ca95
--- /dev/null
+++ b/ui/src/token/index.ts
@@ -0,0 +1,25 @@
+// Components
+export { TokenChip } from './components/TokenChip';
+export { TokenImage } from './components/TokenImage';
+export { TokenRow } from './components/TokenRow';
+export { TokenSelectButton } from './components/TokenSelectButton';
+export { TokenSelectDropdown } from './components/TokenSelectDropdown';
+
+// Utils
+export { formatAmount } from './utils/formatAmount';
+export { getTokenImageColor } from './utils/getTokenImageColor';
+
+// Types
+export type {
+ FormatAmountOptions,
+ FormatAmountResponse,
+ Token,
+ TokenChipProps,
+ TokenImageProps,
+ TokenRowProps,
+ TokenSearchProps,
+ TokenSelectButtonProps,
+ TokenSelectDropdownProps,
+} from './types';
+
+
diff --git a/ui/src/token/types.ts b/ui/src/token/types.ts
new file mode 100644
index 00000000..31c9d750
--- /dev/null
+++ b/ui/src/token/types.ts
@@ -0,0 +1,121 @@
+// 🏔️❄️🏔️
+
+/**
+ * Note: exported as public Type
+ */
+export type FormatAmountOptions = {
+ /** User locale (default: browser locale) */
+ locale?: string;
+ /** Minimum fraction digits for number decimals */
+ minimumFractionDigits?: number;
+ /** Maximum fraction digits for number decimals */
+ maximumFractionDigits?: number;
+};
+
+/**
+ * Note: exported as public Type
+ * See Number.prototype.toLocaleString for more info
+ */
+export type FormatAmountResponse = string;
+
+/**
+ * Note: exported as public Type
+ */
+export type Token = {
+ /** The address of the token contract, this value will be empty for native AVAX */
+ address: string;
+ /** The chain id of the token contract */
+ chainId: number | string;
+ /** The number of token decimals */
+ decimals: number;
+ /** A string url of the token logo */
+ image?: string | null;
+ /** Token name */
+ name: string;
+ /** A ticker symbol or shorthand, up to 11 characters */
+ symbol: string;
+};
+
+/**
+ * Note: exported as public Type
+ */
+export type TokenChipProps = {
+ /** Rendered token */
+ token: Token;
+ onClick?: (token: Token) => void;
+ className?: string;
+ isPressable?: boolean;
+};
+
+/**
+ * Note: exported as public Type
+ */
+export type TokenImageProps = {
+ /** Optional additional CSS class to apply to the component */
+ className?: string;
+ /** size of the image in px (default: 24) */
+ size?: number;
+ token: Token;
+};
+
+/**
+ * Note: exported as public Type
+ */
+export type TokenRowProps = {
+ /** Token amount */
+ amount?: string;
+ /** USD value to display below the amount */
+ value?: string | number;
+ /** Token reputation: 'Benign', 'Malicious', or null */
+ reputation?: 'Benign' | 'Malicious' | null;
+ /** Whether this is a native token */
+ isNative?: boolean;
+ /** Visual variant style */
+ variant?: 'default' | 'outline' | 'ghost';
+ className?: string;
+ hideImage?: boolean;
+ hideSymbol?: boolean;
+ /** Component on click handler */
+ onClick?: (token: Token) => void;
+ /** Rendered token */
+ token: Token;
+ as?: React.ElementType;
+};
+
+/**
+ * Note: exported as public Type
+ */
+export type TokenSearchProps = {
+ className?: string;
+ /** Debounce delay in milliseconds */
+ delayMs?: number;
+ /** Search callback function */
+ onChange: (value: string) => void;
+};
+
+/**
+ * Note: exported as public Type
+ */
+export type TokenSelectButtonProps = {
+ className?: string;
+ /** Determines carot icon direction */
+ isOpen: boolean;
+ /** Button on click handler */
+ onClick: () => void;
+ /** Selected token */
+ token?: Token;
+};
+
+/**
+ * Note: exported as public Type
+ */
+export type TokenSelectDropdownProps = {
+ /** List of tokens */
+ options: Token[];
+ /** Token setter */
+ setToken: (token: Token) => void;
+ /** Selected token */
+ token?: Token;
+ className?: string;
+};
+
diff --git a/ui/src/token/utils/formatAmount.ts b/ui/src/token/utils/formatAmount.ts
new file mode 100644
index 00000000..c8caf818
--- /dev/null
+++ b/ui/src/token/utils/formatAmount.ts
@@ -0,0 +1,22 @@
+import type { FormatAmountOptions, FormatAmountResponse } from '../types';
+
+/**
+ * Formats a numeric string into a localized string representation with optional
+ * control over the number of decimal places.
+ */
+export function formatAmount(
+ amount: string | undefined,
+ options: FormatAmountOptions = {},
+): FormatAmountResponse {
+ if (amount === undefined) {
+ return '';
+ }
+
+ const { locale, minimumFractionDigits, maximumFractionDigits } = options;
+
+ return Number(amount).toLocaleString(locale, {
+ minimumFractionDigits,
+ maximumFractionDigits,
+ });
+}
+
diff --git a/ui/src/token/utils/getTokenImageColor.ts b/ui/src/token/utils/getTokenImageColor.ts
new file mode 100644
index 00000000..cb138052
--- /dev/null
+++ b/ui/src/token/utils/getTokenImageColor.ts
@@ -0,0 +1,24 @@
+/**
+ * Generates a consistent color based on the token name
+ */
+export function getTokenImageColor(name: string): string {
+ const colors = [
+ 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+ 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
+ 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
+ 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
+ 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
+ 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)',
+ 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)',
+ 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)',
+ 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
+ 'linear-gradient(135deg, #ff6e7f 0%, #bfe9ff 100%)',
+ ];
+
+ // Simple hash function for consistent color based on token name
+ const hash = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
+ const colorIndex = hash % colors.length;
+
+ return colors[colorIndex];
+}
+
diff --git a/ui/src/transfer/components/CrossChainTransfer.tsx b/ui/src/transfer/components/CrossChainTransfer.tsx
new file mode 100644
index 00000000..7ebfd46d
--- /dev/null
+++ b/ui/src/transfer/components/CrossChainTransfer.tsx
@@ -0,0 +1,123 @@
+'use client';
+import { cn, text } from '../../styles/theme';
+import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
+import { Input } from '../../components/ui/input';
+import { Label } from '../../components/ui/label';
+import { WalletConnectionOverlay } from '../../components/ui/wallet-connection-overlay';
+import { AvalancheChainOverlay } from '../../components/ui/avalanche-chain-overlay';
+import { TransferProvider } from './TransferProvider';
+import { TransferAmountInput } from './TransferAmountInput';
+import { TransferChainSelector } from './TransferChainSelector';
+import { TransferButton } from './TransferButton';
+import { TransferMessage } from './TransferMessage';
+import { TransferToast } from './TransferToast';
+import { TransferToggleButton } from './TransferToggleButton';
+import { useTransferContext } from '../hooks/useTransferContext';
+import type { TransferProviderProps } from '../types';
+
+type CrossChainTransferProps = {
+ children?: React.ReactNode;
+ className?: string;
+ title?: string;
+} & Omit;
+
+function TransferAddressInput() {
+ const { toAddress, toChain } = useTransferContext();
+
+ const isDisabled = true; // Always disabled - auto-derived
+
+ const getPlaceholder = () => {
+ if (toChain === 'C') return 'Auto-derived C-Chain address';
+ return `Auto-derived ${toChain}-Chain address`;
+ };
+
+ return (
+
+ );
+}
+
+function CrossChainTransferContent() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Destination Address
+
+ (Auto-derived)
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function CrossChainTransfer({
+ children,
+ className,
+ title = 'Cross-Chain Transfer',
+ initialFromChain,
+ initialToChain,
+ onStatusChange,
+ onError,
+ onSuccess,
+}: CrossChainTransferProps) {
+ return (
+
+
+
+
+
+
+ {title}
+
+
+
+ {children || }
+
+
+
+
+
+ );
+}
diff --git a/ui/src/transfer/components/Transfer.tsx b/ui/src/transfer/components/Transfer.tsx
new file mode 100644
index 00000000..3dc98bbb
--- /dev/null
+++ b/ui/src/transfer/components/Transfer.tsx
@@ -0,0 +1,97 @@
+'use client';
+import { useState } from 'react';
+import { cn } from '../../styles/theme';
+import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
+import { AddressInput } from '../../components/ui/address-input';
+import { WalletConnectionOverlay } from '../../components/ui/wallet-connection-overlay';
+import { TransferProvider } from './TransferProvider';
+import { TransferAmountInput } from './TransferAmountInput';
+import { TransferButton } from './TransferButton';
+import { TransferMessage } from './TransferMessage';
+import { TransferToast } from './TransferToast';
+import { useTransferContext } from '../hooks/useTransferContext';
+import type { TransferProviderProps } from '../types';
+import type { AddressValidationResult, ChainType } from '../../utils/addressValidation';
+
+type TransferProps = {
+ children?: React.ReactNode;
+ className?: string;
+ title?: string;
+} & Omit;
+
+function TransferAddressInput() {
+ const { setToAddress, fromChain } = useTransferContext();
+ const [address, setAddress] = useState('');
+
+ const handleAddressChange = (value: string, _validation: AddressValidationResult) => {
+ setAddress(value);
+ setToAddress(value);
+ // Note: _validation.isValid can be used for form validation in the future
+ };
+
+ // Convert TransferChain to ChainType for validation
+ const chainType: ChainType = fromChain as ChainType;
+
+ return (
+
+ );
+}
+
+function SingleChainTransferContent() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function Transfer({
+ children,
+ className,
+ title = 'Transfer',
+ initialFromChain = 'C', // Default to C-Chain for single-chain transfers
+ initialToChain = 'C', // Same chain for single-chain transfers
+ onStatusChange,
+ onError,
+ onSuccess,
+}: TransferProps) {
+ return (
+
+
+
+
+
+ {title}
+
+
+
+ {children || }
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui/src/transfer/components/TransferAmountInput.tsx b/ui/src/transfer/components/TransferAmountInput.tsx
new file mode 100644
index 00000000..d05ae9ff
--- /dev/null
+++ b/ui/src/transfer/components/TransferAmountInput.tsx
@@ -0,0 +1,65 @@
+'use client';
+import { useCallback, useMemo } from 'react';
+import { useTransferContext } from '../hooks/useTransferContext';
+import { useWalletContext } from '../../wallet/hooks/useWalletContext';
+import { cn } from '../../styles/theme';
+import { AmountInput } from '../../components/ui/amount-input';
+import type { TransferAmountInputProps } from '../types';
+
+export function TransferAmountInput({
+ className,
+ label = 'Amount',
+ placeholder = '0.0',
+ showMax = true,
+ disabled = false,
+}: TransferAmountInputProps) {
+ const { amount, setAmount, status, fromChain } = useTransferContext();
+ const { balances } = useWalletContext();
+
+ const isDisabled = disabled || status === 'preparing' || status === 'pending';
+
+ const handleAmountChange = useCallback((e: React.ChangeEvent) => {
+ setAmount(e.target.value);
+ }, [setAmount]);
+
+ // Get the available balance for the selected chain
+ const availableBalance = useMemo(() => {
+ if (fromChain === 'P') return balances.pChain.avax;
+ if (fromChain === 'X') return balances.xChain.avax;
+ return balances.cChain.avax;
+ }, [fromChain, balances]);
+
+ const handleMaxClick = useCallback(() => {
+ // Use 99% of available balance to account for gas fees
+ const maxAmount = (parseFloat(availableBalance) * 0.99).toFixed(6);
+ setAmount(maxAmount);
+ }, [availableBalance, setAmount]);
+
+ // Get USD rate from any chain balance (they should all have the same rate)
+ const usdRate = useMemo(() => {
+ const balance = balances.cChain;
+ if (balance.usd && balance.avax) {
+ return parseFloat(balance.usd) / parseFloat(balance.avax);
+ }
+ return 0;
+ }, [balances.cChain]);
+
+ return (
+
+ );
+}
diff --git a/ui/src/transfer/components/TransferButton.tsx b/ui/src/transfer/components/TransferButton.tsx
new file mode 100644
index 00000000..68dbc665
--- /dev/null
+++ b/ui/src/transfer/components/TransferButton.tsx
@@ -0,0 +1,44 @@
+'use client';
+import { useTransferContext } from '../hooks/useTransferContext';
+import { cn } from '../../styles/theme';
+import { Button } from '../../components/ui/button';
+import { Loader2, Send } from 'lucide-react';
+import type { TransferButtonProps } from '../types';
+
+export function TransferButton({
+ className,
+ text: buttonText = 'Transfer',
+ disabled = false,
+}: TransferButtonProps) {
+ const { status, amount, toAddress, executeTransfer } = useTransferContext();
+
+ const isLoading = status === 'preparing' || status === 'pending';
+ const isDisabled = disabled || isLoading || !amount || !toAddress;
+
+ const getButtonText = () => {
+ if (status === 'preparing') return 'Preparing...';
+ if (status === 'pending') return 'Confirming...';
+ return buttonText;
+ };
+
+ const getButtonIcon = () => {
+ if (isLoading) {
+ return ;
+ }
+
+ return ;
+ };
+
+ return (
+
+ {getButtonIcon()}
+ {getButtonText()}
+
+ );
+}
diff --git a/ui/src/transfer/components/TransferChainSelector.tsx b/ui/src/transfer/components/TransferChainSelector.tsx
new file mode 100644
index 00000000..f1eb8d22
--- /dev/null
+++ b/ui/src/transfer/components/TransferChainSelector.tsx
@@ -0,0 +1,87 @@
+'use client';
+import { useTransferContext } from '../hooks/useTransferContext';
+import { useAvailableChains } from '../../AvalancheProvider';
+import { ChainSelectDropdown, ChainLogo, type ChainOption } from '../../chain';
+import type { TransferChainSelectorProps, TransferChain } from '../types';
+
+export function TransferChainSelector({
+ className,
+ type,
+ disabled = false,
+}: TransferChainSelectorProps) {
+ const {
+ fromChain,
+ toChain,
+ setFromChain,
+ setToChain,
+ availableChains,
+ status
+ } = useTransferContext();
+
+ const avalancheChains = useAvailableChains();
+
+ const currentChain = type === 'from' ? fromChain : toChain;
+ const setChain = type === 'from' ? setFromChain : setToChain;
+ const isDisabled = disabled || status === 'preparing' || status === 'pending';
+ const otherChain = type === 'from' ? toChain : fromChain;
+
+ // Convert TransferChain to Chain format for ChainSelector
+ const chains: ChainOption[] = availableChains.map((chain: TransferChain) => {
+ // Find the Avalanche chain from available chains (usually the first mainnet or testnet chain)
+ const avalancheChain = avalancheChains.find(c => !c.testnet) || avalancheChains[0];
+
+ // X-P-C specific information to overlay on the Avalanche chain
+ const chainSpecificData = {
+ P: {
+ name: 'P-Chain',
+ description: 'Platform Chain for staking & validators',
+ badge: 'P'
+ },
+ C: {
+ name: 'C-Chain',
+ description: 'EVM-compatible chain for smart contracts',
+ badge: 'C'
+ },
+ X: {
+ name: 'X-Chain',
+ description: 'Exchange Chain for asset transfers',
+ badge: 'X'
+ },
+ };
+
+ const chainInfo = chainSpecificData[chain];
+
+ // Create chain data combining Avalanche chain info with X-P-C specifics
+ const chainData = {
+ id: avalancheChain?.id?.toString() || chain,
+ name: avalancheChain?.name || 'Avalanche',
+ iconUrl: (avalancheChain as any)?.iconUrl,
+ testnet: avalancheChain?.testnet || false
+ };
+
+ return {
+ id: chain,
+ name: chainInfo.name,
+ description: chainInfo.description,
+ color: '', // Not needed since we're using custom icons
+ icon: ,
+ };
+ });
+
+ const handleChainChange = (chainId: string) => {
+ setChain(chainId as TransferChain);
+ };
+
+ return (
+ handleChainChange(chainId)}
+ label={type === 'from' ? 'Source Chain' : 'Destination Chain'}
+ disabled={isDisabled}
+ disabledOptions={[otherChain]}
+ className={className}
+ data-testid={`transfer-chain-selector-${type}`}
+ />
+ );
+}
diff --git a/ui/src/transfer/components/TransferMessage.tsx b/ui/src/transfer/components/TransferMessage.tsx
new file mode 100644
index 00000000..028980c4
--- /dev/null
+++ b/ui/src/transfer/components/TransferMessage.tsx
@@ -0,0 +1,103 @@
+'use client';
+import { useTransferContext } from '../hooks/useTransferContext';
+import { cn, text } from '../../styles/theme';
+import type { TransferMessageProps } from '../types';
+
+export function TransferMessage({
+ className,
+ showDetails = false,
+}: TransferMessageProps) {
+ const { status, error, result, clearError } = useTransferContext();
+
+ if (status === 'idle' || status === 'preparing' || status === 'pending') {
+ return null;
+ }
+
+ if (status === 'error' && error) {
+ return (
+
+
+
+
+
+
+
+ Transfer Failed
+
+
+
+ {error.message}
+
+
+ {showDetails && error.code && (
+
+ Error Code: {error.code}
+
+ )}
+
+
+
+
+
+
+
+
+ );
+ }
+
+ if (status === 'success' && result) {
+ return (
+
+
+
+
+
+
+
+ Transfer Successful
+
+
+
+ {result.amount} AVAX transferred from {result.fromChain}-Chain to {result.toChain}-Chain
+
+
+ {showDetails && result.txHashes.length > 0 && (
+
+
Transaction Hash{result.txHashes.length > 1 ? 'es' : ''}:
+ {result.txHashes.map((hash, index) => (
+
+ {hash}
+
+ ))}
+
+ )}
+
+
+ );
+ }
+
+ return null;
+}
diff --git a/ui/src/transfer/components/TransferProvider.tsx b/ui/src/transfer/components/TransferProvider.tsx
new file mode 100644
index 00000000..53d00491
--- /dev/null
+++ b/ui/src/transfer/components/TransferProvider.tsx
@@ -0,0 +1,278 @@
+'use client';
+import { createContext, useCallback, useState, useMemo, useEffect } from 'react';
+import { publicKeyToXPAddress } from '@avalanche-sdk/client/accounts';
+import type { Address } from '@avalanche-sdk/client';
+import { useWalletContext } from '../../wallet/hooks/useWalletContext';
+import type {
+ TransferContextType,
+ TransferProviderProps,
+ TransferStatus,
+ TransferChain,
+ TransferError,
+ TransferResult,
+} from '../types';
+
+export const TransferContext = createContext(null);
+
+export function TransferProvider({
+ children,
+ initialFromChain = 'P',
+ initialToChain = 'C',
+ onStatusChange,
+ onError,
+ onSuccess,
+}: TransferProviderProps) {
+ const { address: walletAddress, currentChain, walletClient } = useWalletContext();
+
+ const [status, setStatus] = useState('idle');
+ const [fromChain, setFromChain] = useState(initialFromChain);
+ const [toChain, setToChain] = useState(initialToChain);
+ const [amount, setAmount] = useState('');
+ const [toAddress, setToAddress] = useState('');
+ const [error, setError] = useState();
+ const [result, setResult] = useState();
+
+ // TODO: Add X-Chain support when low-level export/import methods are implemented
+ // Currently P ↔ C, P → P, and C → C transfers are supported by the SDK's high-level send() method
+ const availableChains: TransferChain[] = useMemo(() => ['P', 'C'], []);
+
+ // Derive destination address based on the selected chain
+ const deriveDestinationAddress = useCallback(async (targetChain: TransferChain): Promise => {
+ if (!walletClient || !walletAddress) return '';
+
+ try {
+ if (targetChain === 'C') {
+ // For C-Chain, use the wallet's C-Chain address (EVM format)
+ return walletAddress;
+ } else {
+ // For P-Chain and X-Chain, derive from public key
+ const { xp } = await walletClient.getAccountPubKey();
+ const hrp = currentChain.name?.includes('Fuji') ? 'fuji' : 'avax';
+ const xpBech32 = publicKeyToXPAddress(xp, hrp);
+ return `${targetChain}-${xpBech32}`;
+ }
+ } catch (error) {
+ console.error('Error deriving destination address:', error);
+ return '';
+ }
+ }, [walletClient, walletAddress, currentChain]);
+
+ // Auto-update destination address when toChain changes
+ useEffect(() => {
+ if (walletAddress && walletClient) {
+ deriveDestinationAddress(toChain).then(setToAddress);
+ }
+ }, [toChain, walletAddress, walletClient, deriveDestinationAddress]);
+
+ const updateStatus = useCallback((newStatus: TransferStatus) => {
+ setStatus(newStatus);
+ onStatusChange?.(newStatus);
+ }, [onStatusChange]);
+
+ const handleError = useCallback((err: Error | TransferError) => {
+ const transferError: TransferError = {
+ message: err.message || 'An unknown error occurred',
+ code: 'code' in err ? err.code : undefined,
+ };
+ setError(transferError);
+ updateStatus('error');
+ onError?.(transferError);
+ }, [onError, updateStatus]);
+
+ const executeTransfer = useCallback(async () => {
+ if (!walletClient || !walletAddress) {
+ handleError(new Error('Wallet not connected'));
+ return;
+ }
+
+ if (!amount || !toAddress) {
+ handleError(new Error('Please fill in all required fields'));
+ return;
+ }
+
+ const value = Number(amount);
+ if (!value || value <= 0) {
+ handleError(new Error('Please enter a valid amount'));
+ return;
+ }
+
+ try {
+ updateStatus('preparing');
+ setError(undefined);
+
+ let transferResult: any;
+
+ if (fromChain === 'P' && toChain === 'C') {
+ // P → C transfer
+ transferResult = await walletClient.send({
+ to: toAddress as Address,
+ amount: parseFloat(amount), // Convert to number for client
+ sourceChain: "P",
+ destinationChain: "C",
+ });
+ } else if (fromChain === 'C' && toChain === 'P') {
+ // C → P transfer
+ transferResult = await walletClient.send({
+ to: toAddress,
+ amount: parseFloat(amount), // Convert to number for client
+ destinationChain: "P",
+ });
+ } else if (fromChain === 'C' && toChain === 'C') {
+ // C → C transfer (same chain transfer)
+ // For C→C, don't specify sourceChain/destinationChain - they default to C
+ console.log('C → C transfer - walletClient:', {
+ toAddress,
+ amount,
+ parsedAmount: parseFloat(amount),
+ walletClientType: typeof walletClient,
+ hasWalletClient: !!walletClient,
+ });
+
+ // Note: C→C transfers using custom provider (Core Wallet) may have limitations
+ // The SDK's transferCtoCChain uses viem actions (estimateGas, getGasPrice, getBalance)
+ // which might not be fully supported by all custom providers
+ try {
+ transferResult = await walletClient.send({
+ to: toAddress as Address,
+ amount: parseFloat(amount), // Convert to number for client
+ });
+ } catch (ctocError: any) {
+ // If the error is about an unrecognized method, provide more context
+ if (ctocError.message?.includes('Unrecognized method') || ctocError.code === -32603) {
+ throw new Error(
+ `C-Chain to C-Chain transfers may not be fully supported with the current wallet provider. ` +
+ `Error: ${ctocError.message || 'Unknown error'}. ` +
+ `This might be a limitation of the Core Wallet's custom provider implementation.`
+ );
+ }
+ throw ctocError;
+ }
+ } else if (fromChain === 'P' && toChain === 'P') {
+ // P → P transfer (same chain transfer)
+ transferResult = await walletClient.send({
+ to: toAddress,
+ amount: parseFloat(amount), // Convert to number for client
+ sourceChain: "P",
+ destinationChain: "P",
+ });
+ } else if (
+ (fromChain === 'P' && toChain === 'X') ||
+ (fromChain === 'X' && toChain === 'P') ||
+ (fromChain === 'C' && toChain === 'X') ||
+ (fromChain === 'X' && toChain === 'C')
+ ) {
+ // TODO: X-Chain transfers are not supported by the high-level send() method
+ // The SDK supports P ↔ C, P → P, and C → C transfers via the send() method.
+ // X-Chain transfers require low-level export/import transactions:
+ // 1. Export from source chain (e.g., walletClient.cChain.prepareExportTxn())
+ // 2. Import to destination chain (e.g., walletClient.xChain.prepareImportTxn())
+ // See examples in: client/examples/prepare-primary-network-txns/
+ throw new Error(
+ `X-Chain transfers are not yet supported. Supported routes: P ↔ C only.`
+ );
+ } else {
+ throw new Error(`Unsupported transfer direction: ${fromChain} → ${toChain}`);
+ }
+
+ updateStatus('pending');
+
+ // Simulate transaction confirmation
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ const successResult: TransferResult = {
+ txHashes: transferResult.txHashes || ['0x...'],
+ fromChain,
+ toChain,
+ amount,
+ };
+
+ setResult(successResult);
+ updateStatus('success');
+ onSuccess?.(successResult);
+
+ } catch (err: any) {
+ console.error('Transfer error details:', {
+ error: err,
+ errorMessage: err?.message,
+ errorCode: err?.code,
+ errorDetails: err?.details,
+ errorStack: err?.stack,
+ fromChain,
+ toChain,
+ amount,
+ toAddress,
+ });
+ handleError(err);
+ }
+ }, [
+ walletClient,
+ walletAddress,
+ amount,
+ toAddress,
+ fromChain,
+ toChain,
+ handleError,
+ updateStatus,
+ onSuccess,
+ ]);
+
+ const toggleChains = useCallback(() => {
+ const newFromChain = toChain;
+ const newToChain = fromChain;
+ setFromChain(newFromChain);
+ setToChain(newToChain);
+ }, [fromChain, toChain]);
+
+ const clearError = useCallback(() => {
+ setError(undefined);
+ if (status === 'error') {
+ updateStatus('idle');
+ }
+ }, [status, updateStatus]);
+
+ const reset = useCallback(() => {
+ setAmount('');
+ setToAddress('');
+ setError(undefined);
+ setResult(undefined);
+ updateStatus('idle');
+ }, [updateStatus]);
+
+ const contextValue = useMemo((): TransferContextType => ({
+ status,
+ fromChain,
+ toChain,
+ amount,
+ toAddress,
+ error,
+ result,
+ availableChains,
+ setFromChain,
+ setToChain,
+ setAmount,
+ setToAddress,
+ executeTransfer,
+ toggleChains,
+ clearError,
+ reset,
+ }), [
+ status,
+ fromChain,
+ toChain,
+ amount,
+ toAddress,
+ error,
+ result,
+ availableChains,
+ executeTransfer,
+ toggleChains,
+ clearError,
+ reset,
+ ]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/ui/src/transfer/components/TransferToast.tsx b/ui/src/transfer/components/TransferToast.tsx
new file mode 100644
index 00000000..c6dab830
--- /dev/null
+++ b/ui/src/transfer/components/TransferToast.tsx
@@ -0,0 +1,89 @@
+'use client';
+import { useEffect, useState } from 'react';
+import * as Toast from '@radix-ui/react-toast';
+import { useTransferContext } from '../hooks/useTransferContext';
+import { cn, text } from '../../styles/theme';
+import type { TransferToastProps } from '../types';
+
+export function TransferToast({
+ className,
+ duration = 5000,
+}: TransferToastProps) {
+ const { status, result, error } = useTransferContext();
+ const [open, setOpen] = useState(false);
+
+ useEffect(() => {
+ if (status === 'success' || status === 'error') {
+ setOpen(true);
+ }
+ }, [status]);
+
+ const isSuccess = status === 'success' && result;
+ const isError = status === 'error' && error;
+
+ if (!isSuccess && !isError) {
+ return null;
+ }
+
+ return (
+
+
+
+ {isSuccess && (
+
+
+
+ )}
+
+ {isError && (
+
+
+
+ )}
+
+
+
+ {isSuccess ? 'Transfer Successful' : 'Transfer Failed'}
+
+
+
+ {isSuccess && result
+ ? `${result.amount} AVAX transferred from ${result.fromChain}-Chain to ${result.toChain}-Chain`
+ : error?.message
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ui/src/transfer/components/TransferToggleButton.tsx b/ui/src/transfer/components/TransferToggleButton.tsx
new file mode 100644
index 00000000..1ea70b88
--- /dev/null
+++ b/ui/src/transfer/components/TransferToggleButton.tsx
@@ -0,0 +1,22 @@
+'use client';
+import { useTransferContext } from '../hooks/useTransferContext';
+import { DirectionToggle } from '../../components/ui/direction-toggle';
+import type { TransferToggleButtonProps } from '../types';
+
+export function TransferToggleButton({
+ className,
+ disabled = false,
+}: TransferToggleButtonProps) {
+ const { status, toggleChains } = useTransferContext();
+
+ const isDisabled = disabled || status === 'preparing' || status === 'pending';
+
+ return (
+
+ );
+}
diff --git a/ui/src/transfer/hooks/useTransferContext.ts b/ui/src/transfer/hooks/useTransferContext.ts
new file mode 100644
index 00000000..41de5ae6
--- /dev/null
+++ b/ui/src/transfer/hooks/useTransferContext.ts
@@ -0,0 +1,14 @@
+import { useContext } from 'react';
+import { TransferContext } from '../components/TransferProvider';
+
+/**
+ * Hook to access the transfer context.
+ * Must be used within a TransferProvider.
+ */
+export function useTransferContext() {
+ const context = useContext(TransferContext);
+ if (!context) {
+ throw new Error('useTransferContext must be used within a TransferProvider');
+ }
+ return context;
+}
diff --git a/ui/src/transfer/index.ts b/ui/src/transfer/index.ts
new file mode 100644
index 00000000..c31141f9
--- /dev/null
+++ b/ui/src/transfer/index.ts
@@ -0,0 +1,30 @@
+// 🏔️⚡🏔️
+// Components
+export { Transfer } from './components/Transfer';
+export { CrossChainTransfer } from './components/CrossChainTransfer';
+export { TransferProvider } from './components/TransferProvider';
+export { TransferAmountInput } from './components/TransferAmountInput';
+export { TransferChainSelector } from './components/TransferChainSelector';
+export { TransferButton } from './components/TransferButton';
+export { TransferMessage } from './components/TransferMessage';
+export { TransferToast } from './components/TransferToast';
+export { TransferToggleButton } from './components/TransferToggleButton';
+
+// Hooks
+export { useTransferContext } from './hooks/useTransferContext';
+
+// Types
+export type {
+ TransferStatus,
+ TransferChain,
+ TransferError,
+ TransferResult,
+ TransferContextType,
+ TransferProviderProps,
+ TransferAmountInputProps,
+ TransferChainSelectorProps,
+ TransferButtonProps,
+ TransferMessageProps,
+ TransferToastProps,
+ TransferToggleButtonProps,
+} from './types';
diff --git a/ui/src/transfer/types.ts b/ui/src/transfer/types.ts
new file mode 100644
index 00000000..b55b9999
--- /dev/null
+++ b/ui/src/transfer/types.ts
@@ -0,0 +1,118 @@
+import type { ReactNode } from 'react';
+
+export type TransferStatus = 'idle' | 'preparing' | 'pending' | 'success' | 'error';
+
+export type TransferChain = 'P' | 'C' | 'X';
+
+export type TransferError = {
+ message: string;
+ code?: string | number;
+};
+
+export type TransferResult = {
+ txHashes: string[];
+ fromChain: TransferChain;
+ toChain: TransferChain;
+ amount: string;
+};
+
+export type TransferContextType = {
+ /** Current transfer status */
+ status: TransferStatus;
+ /** Source chain */
+ fromChain: TransferChain;
+ /** Destination chain */
+ toChain: TransferChain;
+ /** Transfer amount */
+ amount: string;
+ /** Destination address */
+ toAddress: string;
+ /** Current error if any */
+ error?: TransferError;
+ /** Last successful transfer result */
+ result?: TransferResult;
+ /** Available chains for transfer */
+ availableChains: TransferChain[];
+ /** Set the source chain */
+ setFromChain: (chain: TransferChain) => void;
+ /** Set the destination chain */
+ setToChain: (chain: TransferChain) => void;
+ /** Set the transfer amount */
+ setAmount: (amount: string) => void;
+ /** Set the destination address */
+ setToAddress: (address: string) => void;
+ /** Execute the transfer */
+ executeTransfer: () => Promise;
+ /** Toggle source and destination chains */
+ toggleChains: () => void;
+ /** Clear any errors */
+ clearError: () => void;
+ /** Reset the transfer form */
+ reset: () => void;
+};
+
+export type TransferProviderProps = {
+ children: ReactNode;
+ /** Initial source chain */
+ initialFromChain?: TransferChain;
+ /** Initial destination chain */
+ initialToChain?: TransferChain;
+ /** Callback when transfer status changes */
+ onStatusChange?: (status: TransferStatus) => void;
+ /** Callback when an error occurs */
+ onError?: (error: TransferError) => void;
+ /** Callback when transfer succeeds */
+ onSuccess?: (result: TransferResult) => void;
+};
+
+export type TransferAmountInputProps = {
+ /** Custom class name */
+ className?: string;
+ /** Input label */
+ label?: string;
+ /** Placeholder text */
+ placeholder?: string;
+ /** Show max button */
+ showMax?: boolean;
+ /** Disabled state */
+ disabled?: boolean;
+};
+
+export type TransferChainSelectorProps = {
+ /** Custom class name */
+ className?: string;
+ /** Which chain selector (from or to) */
+ type: 'from' | 'to';
+ /** Disabled state */
+ disabled?: boolean;
+};
+
+export type TransferButtonProps = {
+ /** Custom class name */
+ className?: string;
+ /** Custom button text */
+ text?: string;
+ /** Disabled state */
+ disabled?: boolean;
+};
+
+export type TransferMessageProps = {
+ /** Custom class name */
+ className?: string;
+ /** Show detailed error messages */
+ showDetails?: boolean;
+};
+
+export type TransferToastProps = {
+ /** Custom class name */
+ className?: string;
+ /** Auto-hide duration in ms */
+ duration?: number;
+};
+
+export type TransferToggleButtonProps = {
+ /** Custom class name */
+ className?: string;
+ /** Disabled state */
+ disabled?: boolean;
+};
diff --git a/ui/src/types/chainConfig.ts b/ui/src/types/chainConfig.ts
new file mode 100644
index 00000000..5fc97e44
--- /dev/null
+++ b/ui/src/types/chainConfig.ts
@@ -0,0 +1,18 @@
+import { Address } from "viem";
+import type { Chain } from "@avalanche-sdk/client/chains";
+
+export interface ChainConfig extends Chain {
+ blockchainId: string;
+ iconUrl?: string;
+ interchainContracts: {
+ teleporterRegistry: Address;
+ teleporterManager: Address;
+ };
+}
+
+/**
+ * Type guard to check if a Chain is a ChainConfig
+ */
+export function isChainConfig(chain: Chain | ChainConfig): chain is ChainConfig {
+ return 'blockchainId' in chain;
+}
diff --git a/ui/src/utils/addressValidation.ts b/ui/src/utils/addressValidation.ts
new file mode 100644
index 00000000..f68ca77d
--- /dev/null
+++ b/ui/src/utils/addressValidation.ts
@@ -0,0 +1,206 @@
+import { isAddress } from 'viem';
+
+export type ChainType = 'P' | 'C' | 'X';
+
+export interface AddressValidationResult {
+ isValid: boolean;
+ error?: string;
+ suggestion?: string;
+}
+
+/**
+ * Validates an address for a specific chain type
+ */
+export function validateAddress(address: string, chainType: ChainType): AddressValidationResult {
+ if (!address || address.trim() === '') {
+ return {
+ isValid: false,
+ error: 'Address is required'
+ };
+ }
+
+ const trimmedAddress = address.trim();
+
+ switch (chainType) {
+ case 'C':
+ return validateCChainAddress(trimmedAddress);
+ case 'P':
+ return validatePChainAddress(trimmedAddress);
+ case 'X':
+ return validateXChainAddress(trimmedAddress);
+ default:
+ return {
+ isValid: false,
+ error: 'Invalid chain type'
+ };
+ }
+}
+
+/**
+ * Validates C-Chain (EVM) addresses
+ */
+function validateCChainAddress(address: string): AddressValidationResult {
+ // Check if it's a valid EVM address (0x format)
+ if (isAddress(address)) {
+ return { isValid: true };
+ }
+
+ // Check if user provided P-Chain or X-Chain address by mistake
+ if (address.startsWith('P-')) {
+ return {
+ isValid: false,
+ error: 'Invalid address format for C-Chain',
+ suggestion: 'You provided a P-Chain address. C-Chain addresses start with "0x" and are 42 characters long.'
+ };
+ }
+
+ if (address.startsWith('X-')) {
+ return {
+ isValid: false,
+ error: 'Invalid address format for C-Chain',
+ suggestion: 'You provided an X-Chain address. C-Chain addresses start with "0x" and are 42 characters long.'
+ };
+ }
+
+ // Check if it looks like it should be a hex address but missing 0x
+ if (/^[a-fA-F0-9]{40}$/.test(address)) {
+ return {
+ isValid: false,
+ error: 'Invalid address format',
+ suggestion: 'Did you mean "0x' + address + '"? C-Chain addresses must start with "0x".'
+ };
+ }
+
+ return {
+ isValid: false,
+ error: 'Invalid C-Chain address format. Expected format: 0x followed by 40 hexadecimal characters.'
+ };
+}
+
+/**
+ * Validates P-Chain addresses
+ */
+function validatePChainAddress(address: string): AddressValidationResult {
+ // P-Chain addresses should start with P- and be bech32 encoded
+ if (!address.startsWith('P-')) {
+ // Check if user provided C-Chain address by mistake
+ if (isAddress(address)) {
+ return {
+ isValid: false,
+ error: 'Invalid address format for P-Chain',
+ suggestion: 'You provided a C-Chain address. P-Chain addresses start with "P-" followed by bech32 encoded data.'
+ };
+ }
+
+ // Check if user provided X-Chain address by mistake
+ if (address.startsWith('X-')) {
+ return {
+ isValid: false,
+ error: 'Invalid address format for P-Chain',
+ suggestion: 'You provided an X-Chain address. P-Chain addresses start with "P-" not "X-".'
+ };
+ }
+
+ return {
+ isValid: false,
+ error: 'P-Chain addresses must start with "P-"'
+ };
+ }
+
+ // Basic validation for bech32 format after P-
+ const bech32Part = address.slice(2); // Remove "P-"
+
+ // Bech32 addresses should be at least 39 characters and contain only valid bech32 characters
+ if (bech32Part.length < 39) {
+ return {
+ isValid: false,
+ error: 'P-Chain address is too short'
+ };
+ }
+
+ // Check for valid bech32 characters (a-z, 0-9, excluding 1, b, i, o)
+ if (!/^[ac-hj-np-z02-9]+$/.test(bech32Part)) {
+ return {
+ isValid: false,
+ error: 'P-Chain address contains invalid characters'
+ };
+ }
+
+ return { isValid: true };
+}
+
+/**
+ * Validates X-Chain addresses
+ */
+function validateXChainAddress(address: string): AddressValidationResult {
+ // X-Chain addresses should start with X- and be bech32 encoded
+ if (!address.startsWith('X-')) {
+ // Check if user provided C-Chain address by mistake
+ if (isAddress(address)) {
+ return {
+ isValid: false,
+ error: 'Invalid address format for X-Chain',
+ suggestion: 'You provided a C-Chain address. X-Chain addresses start with "X-" followed by bech32 encoded data.'
+ };
+ }
+
+ // Check if user provided P-Chain address by mistake
+ if (address.startsWith('P-')) {
+ return {
+ isValid: false,
+ error: 'Invalid address format for X-Chain',
+ suggestion: 'You provided a P-Chain address. X-Chain addresses start with "X-" not "P-".'
+ };
+ }
+
+ return {
+ isValid: false,
+ error: 'X-Chain addresses must start with "X-"'
+ };
+ }
+
+ // Basic validation for bech32 format after X-
+ const bech32Part = address.slice(2); // Remove "X-"
+
+ // Bech32 addresses should be at least 39 characters and contain only valid bech32 characters
+ if (bech32Part.length < 39) {
+ return {
+ isValid: false,
+ error: 'X-Chain address is too short'
+ };
+ }
+
+ // Check for valid bech32 characters (a-z, 0-9, excluding 1, b, i, o)
+ if (!/^[ac-hj-np-z02-9]+$/.test(bech32Part)) {
+ return {
+ isValid: false,
+ error: 'X-Chain address contains invalid characters'
+ };
+ }
+
+ return { isValid: true };
+}
+
+/**
+ * Detects the likely chain type from an address
+ */
+export function detectChainType(address: string): ChainType | null {
+ if (!address) return null;
+
+ const trimmedAddress = address.trim();
+
+ if (isAddress(trimmedAddress)) {
+ return 'C';
+ }
+
+ if (trimmedAddress.startsWith('P-')) {
+ return 'P';
+ }
+
+ if (trimmedAddress.startsWith('X-')) {
+ return 'X';
+ }
+
+ return null;
+}
+
diff --git a/ui/src/utils/erc20.ts b/ui/src/utils/erc20.ts
new file mode 100644
index 00000000..ed909c76
--- /dev/null
+++ b/ui/src/utils/erc20.ts
@@ -0,0 +1,51 @@
+import { parseAbi } from 'viem';
+
+/**
+ * Standard ERC20 ABI with common functions
+ * Includes all commonly used ERC20 functions for token interactions
+ */
+export const ERC20_ABI = parseAbi([
+ // Read functions
+ 'function name() view returns (string)',
+ 'function symbol() view returns (string)',
+ 'function decimals() view returns (uint8)',
+ 'function totalSupply() view returns (uint256)',
+ 'function balanceOf(address owner) view returns (uint256)',
+ 'function allowance(address owner, address spender) view returns (uint256)',
+
+ // Write functions
+ 'function transfer(address to, uint256 amount) returns (bool)',
+ 'function transferFrom(address from, address to, uint256 amount) returns (bool)',
+ 'function approve(address spender, uint256 amount) returns (bool)',
+
+ // Events
+ 'event Transfer(address indexed from, address indexed to, uint256 value)',
+ 'event Approval(address indexed owner, address indexed spender, uint256 value)',
+]);
+
+/**
+ * ERC20 ABI subset for reading token metadata (name, symbol, decimals)
+ */
+export const ERC20_METADATA_ABI = parseAbi([
+ 'function name() view returns (string)',
+ 'function symbol() view returns (string)',
+ 'function decimals() view returns (uint8)',
+]);
+
+/**
+ * ERC20 ABI subset for balance operations
+ */
+export const ERC20_BALANCE_ABI = parseAbi([
+ 'function balanceOf(address owner) view returns (uint256)',
+ 'function decimals() view returns (uint8)',
+]);
+
+/**
+ * ERC20 ABI subset for approval operations
+ */
+export const ERC20_APPROVAL_ABI = parseAbi([
+ 'function approve(address spender, uint256 amount) returns (bool)',
+ 'function allowance(address owner, address spender) view returns (uint256)',
+]);
+
+
diff --git a/ui/src/utils/explorer.ts b/ui/src/utils/explorer.ts
new file mode 100644
index 00000000..7a7923c7
--- /dev/null
+++ b/ui/src/utils/explorer.ts
@@ -0,0 +1,79 @@
+import type { Chain } from '@avalanche-sdk/client/chains';
+
+/**
+ * Types of explorer resources
+ */
+export type ExplorerResourceType = 'address' | 'tx' | 'transaction' | 'token' | 'block';
+
+/**
+ * Explorer URL parameters
+ */
+export interface ExplorerUrlParams {
+ /** Resource type (address, tx, token, etc.) */
+ type: ExplorerResourceType;
+ /** Resource identifier (address, tx hash, token address, block number) */
+ value: string;
+ /** Additional query parameters */
+ params?: Record;
+}
+
+/**
+ * Get the explorer URL for a given chain
+ * Falls back to Snowtrace (Avalanche Mainnet) if not available
+ */
+export function getExplorerUrl(chain?: Chain | null): string {
+ return chain?.blockExplorers?.default?.url || 'https://snowtrace.io';
+}
+
+/**
+ * Build explorer URL with pattern-based approach
+ * Supports different resource types and additional parameters
+ */
+export function buildExplorerUrl(
+ chain: Chain | null | undefined,
+ params: ExplorerUrlParams
+): string {
+ const baseUrl = getExplorerUrl(chain);
+ const { type, value, params: queryParams } = params;
+
+ // Map resource types to URL patterns
+ const typeMap: Record = {
+ address: 'address',
+ tx: 'tx',
+ transaction: 'tx',
+ token: 'token',
+ block: 'block',
+ };
+
+ const pathSegment = typeMap[type] || type;
+ let url = `${baseUrl}/${pathSegment}/${value}`;
+
+ // Add query parameters if provided
+ if (queryParams && Object.keys(queryParams).length > 0) {
+ const searchParams = new URLSearchParams();
+ Object.entries(queryParams).forEach(([key, val]) => {
+ searchParams.append(key, String(val));
+ });
+ url += `?${searchParams.toString()}`;
+ }
+
+ return url;
+}
+
+/**
+ * Open an explorer URL in a new window
+ */
+export function openExplorerUrl(url: string): void {
+ window.open(url, '_blank', 'noopener,noreferrer');
+}
+
+/**
+ * Open an explorer resource in a new window (pattern-based)
+ */
+export function openExplorer(
+ chain: Chain | null | undefined,
+ params: ExplorerUrlParams
+): void {
+ openExplorerUrl(buildExplorerUrl(chain, params));
+}
+
diff --git a/ui/src/utils/formatAddress.ts b/ui/src/utils/formatAddress.ts
new file mode 100644
index 00000000..4b23c63e
--- /dev/null
+++ b/ui/src/utils/formatAddress.ts
@@ -0,0 +1,17 @@
+/**
+ * Abbreviate an Ethereum address to show first 6 and last 4 characters
+ *
+ * @example
+ * ```ts
+ * abbreviateAddress('0x1234567890abcdef1234567890abcdef12345678')
+ * Returns: "0x1234...5678"
+ * ```
+ *
+ * @param address - Full Ethereum address string
+ * @returns Abbreviated address string (e.g., "0x1234...5678") or "N/A" if address is empty
+ */
+export function abbreviateAddress(address: string | null | undefined): string {
+ if (!address) return 'N/A';
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
+}
+
diff --git a/ui/src/utils/formatRelativeTime.ts b/ui/src/utils/formatRelativeTime.ts
new file mode 100644
index 00000000..9aebd20f
--- /dev/null
+++ b/ui/src/utils/formatRelativeTime.ts
@@ -0,0 +1,41 @@
+/**
+ * Format a Unix timestamp (in seconds) as a relative time string
+ *
+ * @example
+ * ```ts
+ * formatRelativeTime(1704067200) // "2 days ago"
+ * formatRelativeTime(Date.now() / 1000) // "just now"
+ * ```
+ *
+ * @param timestamp - Unix timestamp in seconds
+ * @returns Formatted relative time string (e.g., "2 days ago", "3 hours ago")
+ */
+export function formatRelativeTime(timestamp: number): string {
+ const now = Date.now();
+ const txTime = timestamp * 1000;
+ const diffMs = now - txTime;
+ const diffSeconds = Math.floor(diffMs / 1000);
+ const diffMinutes = Math.floor(diffSeconds / 60);
+ const diffHours = Math.floor(diffMinutes / 60);
+ const diffDays = Math.floor(diffHours / 24);
+ const diffWeeks = Math.floor(diffDays / 7);
+ const diffMonths = Math.floor(diffDays / 30);
+ const diffYears = Math.floor(diffDays / 365);
+
+ if (diffSeconds < 60) {
+ return 'just now';
+ } else if (diffMinutes < 60) {
+ return `${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''} ago`;
+ } else if (diffHours < 24) {
+ return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
+ } else if (diffDays < 7) {
+ return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
+ } else if (diffWeeks < 4) {
+ return `${diffWeeks} week${diffWeeks !== 1 ? 's' : ''} ago`;
+ } else if (diffMonths < 12) {
+ return `${diffMonths} month${diffMonths !== 1 ? 's' : ''} ago`;
+ } else {
+ return `${diffYears} year${diffYears !== 1 ? 's' : ''} ago`;
+ }
+}
+
diff --git a/ui/src/utils/index.ts b/ui/src/utils/index.ts
new file mode 100644
index 00000000..2a5d1348
--- /dev/null
+++ b/ui/src/utils/index.ts
@@ -0,0 +1,12 @@
+export {
+ validateAddress,
+ detectChainType,
+ type ChainType,
+ type AddressValidationResult
+} from './addressValidation';
+
+export { formatRelativeTime } from './formatRelativeTime';
+export { abbreviateAddress } from './formatAddress';
+export * from './explorer';
+export * from './erc20';
+
diff --git a/ui/src/wallet/components/NetworkSelector.tsx b/ui/src/wallet/components/NetworkSelector.tsx
new file mode 100644
index 00000000..0847739b
--- /dev/null
+++ b/ui/src/wallet/components/NetworkSelector.tsx
@@ -0,0 +1,67 @@
+'use client';
+import { useMemo } from 'react';
+import { useAvalanche, useAvailableChains } from '../../AvalancheProvider';
+import { useSwitchChain } from '../../hooks/useSwitchChain';
+import { ChainSelectDropdown, ChainLogo, type ChainOption } from '../../chain';
+import type { NetworkSelectorProps } from '../types';
+
+export function NetworkSelector({
+ className,
+ showTestnets = true,
+ networks,
+}: NetworkSelectorProps) {
+ const { chain: currentChain } = useAvalanche();
+ const availableChains = useAvailableChains();
+ const { switchChain, isLoading } = useSwitchChain({
+ onSuccess: (chain) => {
+ console.log('Successfully switched to:', chain.name);
+ },
+ onError: (error) => {
+ console.error('Failed to switch chain:', error);
+ },
+ });
+
+ const chains = networks || availableChains;
+ const filteredChains = showTestnets
+ ? chains
+ : chains.filter(chain => !chain.testnet);
+
+ // Transform Avalanche SDK chains to ChainSelector format
+ const chainSelectorChains: ChainOption[] = useMemo(() => {
+ return filteredChains.map((chain) => {
+ return {
+ id: chain.id.toString(),
+ name: chain.name,
+ description: chain.testnet ? 'Testnet' : 'Mainnet',
+ color: chain.testnet ? 'bg-yellow-500' : 'bg-primary',
+ icon: (
+
+ )
+ };
+ });
+ }, [filteredChains]);
+
+ const handleChainChange = async (chainId: string) => {
+ const selectedChain = filteredChains.find(chain => chain.id.toString() === chainId);
+ if (selectedChain && selectedChain.id !== currentChain.id) {
+ await switchChain(selectedChain);
+ }
+ };
+
+ return (
+ handleChainChange(chainId)}
+ placeholder="Select Network"
+ disabled={isLoading}
+ className={className}
+ label="Network"
+ />
+ );
+}
diff --git a/ui/src/wallet/components/WalletActivity.tsx b/ui/src/wallet/components/WalletActivity.tsx
new file mode 100644
index 00000000..37050d97
--- /dev/null
+++ b/ui/src/wallet/components/WalletActivity.tsx
@@ -0,0 +1,82 @@
+'use client';
+import { useState, useRef } from 'react';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
+import { WalletPortfolio, type WalletPortfolioRef } from './WalletPortfolio';
+import { WalletTransactions, type WalletTransactionsRef } from './WalletTransactions';
+import { Button } from '../../components/ui/button';
+import { RefreshCw, Loader2 } from 'lucide-react';
+import type { WalletActivityProps } from '../types';
+
+export function WalletActivity({
+ className,
+ portfolioProps,
+ transactionsProps,
+ defaultTab = 'portfolio',
+ showRefresh = true,
+ itemsPerPage,
+}: WalletActivityProps) {
+ const [activeTab, setActiveTab] = useState(defaultTab);
+ const [refreshing, setRefreshing] = useState(false);
+ const portfolioRef = useRef(null);
+ const transactionsRef = useRef(null);
+
+ const handleRefresh = async () => {
+ setRefreshing(true);
+ try {
+ await Promise.all([
+ portfolioRef.current?.refresh(),
+ transactionsRef.current?.refresh(),
+ ]);
+ } finally {
+ setRefreshing(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Portfolio
+ Transactions
+
+
+ {showRefresh && (
+
+ {refreshing ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/ui/src/wallet/components/WalletBalance.tsx b/ui/src/wallet/components/WalletBalance.tsx
new file mode 100644
index 00000000..65698986
--- /dev/null
+++ b/ui/src/wallet/components/WalletBalance.tsx
@@ -0,0 +1,75 @@
+'use client';
+import { useMemo } from 'react';
+import { useWalletContext } from '../hooks/useWalletContext';
+import { useAvalanche } from '../../AvalancheProvider';
+import { cn, text } from '../../styles/theme';
+import { Button } from '../../components/ui/button';
+import { RefreshCw, Loader2 } from 'lucide-react';
+import type { WalletBalanceProps } from '../types';
+
+export function WalletBalance({
+ className,
+ symbol,
+ decimals = 4,
+ chainType = 'cChain', // Default to C-Chain
+}: WalletBalanceProps) {
+ const { status, balances, refreshBalances } = useWalletContext();
+ const { chain } = useAvalanche();
+
+ // Get the balance for the specified chain
+ const chainBalance = useMemo(() => {
+ return balances[chainType];
+ }, [balances, chainType]);
+
+ const { avax, usd, loading } = chainBalance;
+
+ // Format balance to specified decimals
+ const formattedBalance = useMemo(() => {
+ return parseFloat(avax).toFixed(decimals);
+ }, [avax, decimals]);
+
+ // Get chain name label
+ const chainLabel = useMemo(() => {
+ if (chainType === 'pChain') return 'P-Chain';
+ if (chainType === 'xChain') return 'X-Chain';
+ // For C-Chain, show the actual chain name
+ return chain.name;
+ }, [chainType, chain]);
+
+ // Get the native currency symbol from the chain
+ const currencySymbol = useMemo(() => {
+ // Use provided symbol if available, otherwise use chain's native currency
+ if (symbol) return symbol;
+ return chain.nativeCurrency?.symbol || 'AVAX';
+ }, [symbol, chain]);
+
+ if (status !== 'connected') {
+ return null;
+ }
+
+ return (
+
+
+
+ {chainLabel} Balance
+
+ {loading && }
+
+
+
+
+
+
+
+ {formattedBalance} {currencySymbol}
+
+
+
+ );
+}
diff --git a/ui/src/wallet/components/WalletConnect.tsx b/ui/src/wallet/components/WalletConnect.tsx
new file mode 100644
index 00000000..dc2f47b8
--- /dev/null
+++ b/ui/src/wallet/components/WalletConnect.tsx
@@ -0,0 +1,63 @@
+'use client';
+import { useWalletContext } from '../hooks/useWalletContext';
+import { cn } from '../../styles/theme';
+import { Button } from '../../components/ui/button';
+import { Loader2, Check, Lock } from 'lucide-react';
+import type { WalletConnectProps } from '../types';
+
+export function WalletConnect({
+ className,
+ connectText = 'Connect Wallet',
+ connectedText = 'Connected',
+ showLoading = true,
+}: WalletConnectProps) {
+ const { status, connect } = useWalletContext();
+
+ const isConnecting = status === 'connecting';
+ const isConnected = status === 'connected';
+ const isDisabled = isConnecting; // Only disable while connecting, not when connected
+
+ const handleClick = () => {
+ if (!isDisabled && !isConnected) {
+ connect();
+ }
+ };
+
+ const getButtonText = () => {
+ if (isConnecting && showLoading) return 'Connecting...';
+ if (isConnected) return connectedText;
+ return connectText;
+ };
+
+ const getButtonIcon = () => {
+ if (isConnecting && showLoading) {
+ return ;
+ }
+
+ if (isConnected) {
+ return ;
+ }
+
+ return ;
+ };
+
+ return (
+
+
+ {getButtonIcon()}
+ {getButtonText()}
+
+
+ );
+}
diff --git a/ui/src/wallet/components/WalletDropdown.tsx b/ui/src/wallet/components/WalletDropdown.tsx
new file mode 100644
index 00000000..2622dab3
--- /dev/null
+++ b/ui/src/wallet/components/WalletDropdown.tsx
@@ -0,0 +1,197 @@
+'use client';
+import { useState } from 'react';
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
+import { useWalletContext } from '../hooks/useWalletContext';
+import { useAvalanche } from '../../AvalancheProvider';
+import { cn, text, pressable } from '../../styles/theme';
+import { Copy, ExternalLink, Loader2 } from 'lucide-react';
+import type { WalletDropdownProps } from '../types';
+
+export function WalletDropdown({
+ className,
+ showBalance = true,
+ showNetwork = true,
+ showXPAddresses = false,
+}: WalletDropdownProps) {
+ const { status, address, disconnect, xAddress, pAddress, balances } = useWalletContext();
+ const { chain: currentChain } = useAvalanche();
+ const [open, setOpen] = useState(false);
+
+ if (status !== 'connected' || !address) {
+ return null;
+ }
+
+ // Utility function to abbreviate addresses
+ const abbreviateAddress = (addr: string) => {
+ return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
+ };
+
+ // Utility function to copy address
+ const copyAddress = async (addr: string) => {
+ try {
+ await navigator.clipboard.writeText(addr);
+ // You could add a toast notification here
+ } catch (err) {
+ console.error('Failed to copy address:', err);
+ }
+ };
+
+ // Utility function to open address in explorer
+ const openInExplorer = (addr: string, chainType: 'C' | 'X' | 'P' = 'C') => {
+ // This would need to be customized based on the explorer URLs for each chain
+ const baseUrl = chainType === 'C'
+ ? 'https://snowtrace.io/address/'
+ : `https://explorer.avax.network/address/`;
+ window.open(`${baseUrl}${addr}`, '_blank');
+ };
+
+ // Component for address row with copy and external link buttons
+ const AddressRow = ({
+ label,
+ addr,
+ chainType = 'C'
+ }: {
+ label: string;
+ addr: string;
+ chainType?: 'C' | 'X' | 'P';
+ }) => (
+
+
+
+ {label}
+
+
+ {abbreviateAddress(addr)}
+
+
+
+ copyAddress(addr)}
+ className={cn(
+ 'p-1 rounded hover:bg-accent transition-colors',
+ 'text-muted-foreground hover:text-foreground'
+ )}
+ title="Copy address"
+ >
+
+
+ openInExplorer(addr, chainType)}
+ className={cn(
+ 'p-1 rounded hover:bg-accent transition-colors',
+ 'text-muted-foreground hover:text-foreground'
+ )}
+ title="View in explorer"
+ >
+
+
+
+
+ );
+
+ return (
+
+
+
+
+
+ {address.slice(2, 4).toUpperCase()}
+
+
+
+ {abbreviateAddress(address)}
+
+
+
+
+
+
+
+
+
+ {/* Account Info */}
+
+
+ {showXPAddresses && xAddress && (
+
+ )}
+ {showXPAddresses && pAddress && (
+
+ )}
+
+
+ {/* Balance Info */}
+ {showBalance && (
+
+
+ Balance
+
+
+
+ {parseFloat(balances.cChain.avax).toFixed(4)} {currentChain.nativeCurrency?.symbol || 'AVAX'}
+
+ {balances.cChain.loading && (
+
+ )}
+
+
+ )}
+
+ {/* Network Info */}
+ {showNetwork && (
+
+
+ Network
+
+
+
+
{currentChain.name}
+
+
+ )}
+
+ {/* Actions */}
+
+
+
+
+
+
+ Disconnect
+
+
+
+
+
+ );
+}
diff --git a/ui/src/wallet/components/WalletMessage.tsx b/ui/src/wallet/components/WalletMessage.tsx
new file mode 100644
index 00000000..d1ea23b8
--- /dev/null
+++ b/ui/src/wallet/components/WalletMessage.tsx
@@ -0,0 +1,66 @@
+'use client';
+import { useWalletContext } from '../hooks/useWalletContext';
+import { cn, text } from '../../styles/theme';
+import type { WalletMessageProps } from '../types';
+
+export function WalletMessage({
+ className,
+ showDetails = false,
+}: WalletMessageProps) {
+ const { status, error, clearError } = useWalletContext();
+
+ if (status !== 'error' || !error) {
+ return null;
+ }
+
+ const getErrorIcon = () => (
+
+
+
+ );
+
+ return (
+
+ {getErrorIcon()}
+
+
+
+ Wallet Error
+
+
+
+ {error.message}
+
+
+ {showDetails && error.code && (
+
+ Error Code: {error.code}
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ui/src/wallet/components/WalletPortfolio.tsx b/ui/src/wallet/components/WalletPortfolio.tsx
new file mode 100644
index 00000000..e2c5e538
--- /dev/null
+++ b/ui/src/wallet/components/WalletPortfolio.tsx
@@ -0,0 +1,344 @@
+'use client';
+import { useState, useEffect, useMemo, forwardRef, useImperativeHandle } from 'react';
+import { useWalletContext } from '../hooks/useWalletContext';
+import { useErc20Balances } from '../../glacier/wallet/useErc20Balances';
+import { useNativeBalance } from '../../glacier/wallet/useNativeBalance';
+import { useAvalanche } from '../../AvalancheProvider';
+import { cn, text } from '../../styles/theme';
+import { Button } from '../../components/ui/button';
+import { RefreshCw, Loader2, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react';
+import type { WalletPortfolioProps } from '../types';
+import { formatUnits } from 'viem';
+import { TokenRow } from '../../token/components/TokenRow';
+import type { Token } from '../../token/types';
+import { buildExplorerUrl, openExplorer } from '../../utils/explorer';
+
+const DEFAULT_ITEMS_PER_PAGE = 10;
+
+export interface WalletPortfolioRef {
+ refresh: () => Promise;
+}
+
+export const WalletPortfolio = forwardRef(function WalletPortfolio({
+ className,
+ showUSD = true,
+ showRefresh = true,
+ maxItems,
+ itemsPerPage = DEFAULT_ITEMS_PER_PAGE,
+ contractAddresses,
+ blockNumber,
+}, ref) {
+ const { status, address } = useWalletContext();
+ const { chain } = useAvalanche();
+ const [currentPage, setCurrentPage] = useState(1);
+
+ const { balance: nativeBalance, loading: nativeLoading, refresh: refreshNative } = useNativeBalance({
+ blockNumber,
+ autoFetch: true,
+ });
+
+ const { balances, loading, error, refresh: fetchBalances } = useErc20Balances({
+ blockNumber,
+ contractAddresses,
+ filterZeroBalances: true,
+ sortByValue: true,
+ autoFetch: true,
+ });
+
+ // Expose refresh method via ref
+ useImperativeHandle(ref, () => ({
+ refresh: async () => {
+ await Promise.all([fetchBalances(), refreshNative()]);
+ },
+ }), [fetchBalances, refreshNative]);
+
+ // Convert native balance to Token type
+ const nativeToken = useMemo(() => {
+ if (!nativeBalance) return null;
+ return {
+ address: '', // Native token has no address
+ chainId: chain.id,
+ decimals: nativeBalance.decimals,
+ image: nativeBalance.logoUri || null,
+ name: nativeBalance.name,
+ symbol: nativeBalance.symbol,
+ };
+ }, [nativeBalance, chain.id]);
+
+ // Convert Erc20TokenBalance to Token type
+ const tokens = useMemo(() => {
+ return balances.map((balance) => ({
+ address: balance.address,
+ chainId: chain.id,
+ decimals: balance.decimals,
+ image: balance.logoUri || null,
+ name: balance.name,
+ symbol: balance.symbol,
+ }));
+ }, [balances, chain.id]);
+
+ // Pagination logic - include native token in count
+ const hasNativeToken = !!nativeBalance;
+ const totalItems = useMemo(() => {
+ const erc20Count = maxItems ? Math.min(balances.length, maxItems) : balances.length;
+ return erc20Count + (hasNativeToken ? 1 : 0);
+ }, [balances.length, maxItems, hasNativeToken]);
+
+ const totalPages = useMemo(() => {
+ return Math.ceil(totalItems / itemsPerPage);
+ }, [totalItems, itemsPerPage]);
+
+ // Determine if native token should be shown on current page
+ const showNativeTokenOnPage = useMemo(() => {
+ if (!hasNativeToken) return false;
+ // Native token is always on page 1 (index 0)
+ return currentPage === 1;
+ }, [hasNativeToken, currentPage]);
+
+ // Calculate how many ERC-20 tokens to show on current page
+ const displayedBalances = useMemo(() => {
+ const balancesToShow = maxItems ? balances.slice(0, maxItems) : balances;
+
+ if (showNativeTokenOnPage) {
+ // If showing native token, reduce ERC-20 tokens by 1
+ const erc20ItemsPerPage = itemsPerPage - 1;
+ const startIndex = (currentPage - 1) * erc20ItemsPerPage;
+ const endIndex = startIndex + erc20ItemsPerPage;
+ return balancesToShow.slice(startIndex, endIndex);
+ } else {
+ // If not showing native token, adjust start index to account for native token
+ const erc20ItemsPerPage = itemsPerPage - 1; // First page had one less ERC-20 token
+ const startIndex = erc20ItemsPerPage + (currentPage - 2) * itemsPerPage;
+ const endIndex = startIndex + itemsPerPage;
+ return balancesToShow.slice(startIndex, endIndex);
+ }
+ }, [balances, currentPage, maxItems, itemsPerPage, showNativeTokenOnPage]);
+
+ const displayedTokens = useMemo(() => {
+ const tokensToShow = maxItems ? tokens.slice(0, maxItems) : tokens;
+ // Match the displayed balances indices
+ if (displayedBalances.length === 0) return [];
+ const balancesToShow = maxItems ? balances.slice(0, maxItems) : balances;
+ const startIndex = balancesToShow.findIndex(b => b.address === displayedBalances[0].address);
+ if (startIndex === -1) return [];
+ const endIndex = startIndex + displayedBalances.length;
+ return tokensToShow.slice(startIndex, endIndex);
+ }, [tokens, displayedBalances, balances, maxItems]);
+
+ const totalUSDValue = useMemo(() => {
+ const erc20Value = balances.reduce((sum, balance) => {
+ return sum + (balance.balanceValue?.value || 0);
+ }, 0);
+ const nativeValue = nativeBalance?.balanceValue?.value || 0;
+ return erc20Value + nativeValue;
+ }, [balances, nativeBalance]);
+
+ // Reset to page 1 when balances change
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [balances.length, hasNativeToken]);
+
+ if (status !== 'connected' || !address) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ Token Portfolio
+
+ {showUSD && totalUSDValue > 0 && (
+
+ Total Value: ${totalUSDValue.toFixed(2)} USD
+
+ )}
+
+ {showRefresh && (
+
{
+ fetchBalances();
+ refreshNative();
+ }}
+ variant="outline"
+ size="icon"
+ className="h-8 w-8"
+ disabled={loading || nativeLoading}
+ title="Refresh balances"
+ >
+ {(loading || nativeLoading) ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ {error && (
+
+ )}
+
+ {(loading || nativeLoading) && balances.length === 0 && !nativeBalance ? (
+
+
+
+ ) : displayedBalances.length === 0 && !nativeBalance ? (
+
+
+ No token balances found
+
+
+ ) : (
+ <>
+ {/* Table Header */}
+
+
+
+ Token
+
+
+ Actions
+
+
+
+
+ {/* Table Body */}
+
+ {/* Native Token Balance - Shown on first page only */}
+ {showNativeTokenOnPage && nativeToken && nativeBalance && (
+
+
+
{
+ openExplorer(chain, { type: 'address', value: address });
+ }}
+ className="flex-1 min-w-0 -mx-2"
+ as="div"
+ />
+
+
+ {displayedBalances.length > 0 && (
+
+ )}
+
+ )}
+
+ {/* ERC-20 Token Balances */}
+ {displayedBalances.map((balance, index) => {
+ const formattedAmount = formatUnits(BigInt(balance.balance), balance.decimals);
+ const usdValue = balance.balanceValue?.value || 0;
+ const token = displayedTokens[index];
+ const isLast = index === displayedBalances.length - 1;
+ const isFirst = index === 0;
+
+ return (
+
+
+ {/* Token Info with Amount and Value */}
+
0 ? usdValue : undefined}
+ reputation={balance.tokenReputation}
+ variant="ghost"
+ hideSymbol={false}
+ onClick={(t) => {
+ openExplorer(chain, { type: 'token', value: t.address });
+ }}
+ className="flex-1 min-w-0 -mx-2"
+ as="div"
+ />
+
+ {/* Actions Column */}
+
+
+ {!isLast && (
+
+ )}
+
+ );
+ })}
+
+
+ {/* Pagination Controls */}
+ {totalPages > 1 && (
+
+
+ Showing {((currentPage - 1) * itemsPerPage) + 1} - {Math.min(currentPage * itemsPerPage, totalItems)} of {totalItems} token{totalItems !== 1 ? 's' : ''}
+
+
+
setCurrentPage((prev) => Math.max(1, prev - 1))}
+ disabled={currentPage === 1}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+ {currentPage} / {totalPages}
+
+
setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
+ disabled={currentPage === totalPages}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
+ )}
+ >
+ )}
+
+ );
+});
+
diff --git a/ui/src/wallet/components/WalletProvider.tsx b/ui/src/wallet/components/WalletProvider.tsx
new file mode 100644
index 00000000..fd552650
--- /dev/null
+++ b/ui/src/wallet/components/WalletProvider.tsx
@@ -0,0 +1,327 @@
+'use client';
+import { createContext, useCallback, useEffect, useState, useMemo } from 'react';
+import { avalanche, avalancheFuji } from '@avalanche-sdk/client/chains';
+import { formatUnits } from 'viem';
+import { publicKeyToXPAddress } from '@avalanche-sdk/client/accounts';
+import { getBalance as getCChainBalance } from '@avalanche-sdk/client/methods';
+import { getBalance as getPChainBalance } from '@avalanche-sdk/client/methods/pChain';
+import { getBalance as getXChainBalance } from '@avalanche-sdk/client/methods/xChain';
+import type { Address, Chain } from '@avalanche-sdk/client';
+import { useAvalanche } from '../../AvalancheProvider';
+import type { WalletContextType, WalletProviderProps, WalletStatus, WalletError, ChainBalances, WalletBalance } from '../types';
+
+export const WalletContext = createContext(null);
+
+export function WalletProvider({
+ children,
+ initialChain,
+ onStatusChange,
+ onError,
+ onConnect,
+ onDisconnect,
+}: WalletProviderProps) {
+ const {
+ chain: defaultChain,
+ walletClient,
+ walletAddress,
+ isWalletConnected,
+ connectWallet,
+ disconnectWallet
+ } = useAvalanche();
+ const [status, setStatus] = useState('disconnected');
+ const [error, setError] = useState();
+ const [currentChain, setCurrentChain] = useState(initialChain || defaultChain);
+
+ // Sync wallet address from AvalancheProvider
+ const address = walletAddress as Address | undefined;
+
+ // X and P chain addresses
+ const [xAddress, setXAddress] = useState();
+ const [pAddress, setPAddress] = useState();
+
+ // Initialize empty balances
+ const [balances, setBalances] = useState(() => ({
+ pChain: { avax: '0', wei: 0n, loading: false },
+ cChain: { avax: '0', wei: 0n, loading: false },
+ xChain: { avax: '0', wei: 0n, loading: false },
+ }));
+
+ const availableChains = useMemo(() => [avalanche, avalancheFuji], []);
+
+ const updateStatus = useCallback((newStatus: WalletStatus) => {
+ setStatus(newStatus);
+ onStatusChange?.(newStatus);
+ }, [onStatusChange]);
+
+ const handleError = useCallback((err: Error | WalletError) => {
+ const walletError: WalletError = {
+ message: err.message || 'An unknown error occurred',
+ code: 'code' in err ? err.code : undefined,
+ };
+ setError(walletError);
+ updateStatus('error');
+ onError?.(walletError);
+ }, [onError, updateStatus]);
+
+ // Function to compute X and P chain addresses
+ const updateXPAddresses = useCallback(async () => {
+ if (!walletClient || !address) {
+ setXAddress(undefined);
+ setPAddress(undefined);
+ return;
+ }
+
+ try {
+ // Get the public key from the connected wallet
+ const { xp } = await walletClient.getAccountPubKey();
+
+ // Determine the correct HRP (Human Readable Part) based on network
+ const hrp = currentChain.testnet ? 'fuji' : 'avax';
+
+ // Derive the bech32 XP address from the public key
+ const xpBech32 = publicKeyToXPAddress(xp, hrp);
+
+ // Set both X and P addresses (they use the same bech32 address with different prefixes)
+ setXAddress(`X-${xpBech32}`);
+ setPAddress(`P-${xpBech32}`);
+ } catch (error) {
+ console.error('Failed to compute X/P addresses:', error);
+ setXAddress(undefined);
+ setPAddress(undefined);
+ }
+ }, [walletClient, address, currentChain]);
+
+ // Fetch balance for a specific chain
+ const fetchChainBalance = useCallback(async (
+ chainType: 'pChain' | 'cChain' | 'xChain',
+ walletAddress: Address
+ ): Promise => {
+ try {
+ let balanceWei: bigint = 0n;
+
+ // Fetch balance based on chain type
+ if (chainType === 'cChain') {
+ // For C-Chain, get ETH balance (AVAX on C-Chain) using walletClient
+ if (!walletClient) {
+ balanceWei = 0n;
+ } else {
+ // Use getCChainBalance with walletClient
+ balanceWei = await getCChainBalance(walletClient, {
+ address: walletAddress,
+ });
+ }
+ } else if (chainType === 'pChain') {
+ // For P-Chain, use existing pAddress with walletClient's pChain
+ try {
+ if (!walletClient || !pAddress) {
+ balanceWei = 0n;
+ } else {
+ // Use getPChainBalance with walletClient's pChain client
+ // Type assertion needed because wallet client is compatible for read operations
+ const balance = await getPChainBalance(walletClient.pChainClient, {
+ addresses: [pAddress],
+ });
+
+ balanceWei = balance.balance;
+ }
+ } catch (error) {
+ console.error('P-Chain balance fetch error:', error);
+ balanceWei = 0n;
+ }
+ } else if (chainType === 'xChain') {
+ // For X-Chain, use existing xAddress with walletClient's xChain
+ try {
+ if (!walletClient || !xAddress) {
+ balanceWei = 0n;
+ } else {
+ // Determine AVAX asset ID based on network
+ const avaxAssetId = currentChain.testnet
+ ? 'U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK' // Fuji AVAX
+ : 'FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z'; // Mainnet AVAX
+
+ // Use getXChainBalance with walletClient's xChain client
+ // Type assertion needed because wallet client is compatible for read operations
+ // Note: X-Chain getBalance uses 'address' (singular string), not 'addresses'
+ const balance = await getXChainBalance(walletClient.xChainClient, {
+ address: xAddress,
+ assetID: avaxAssetId,
+ });
+
+ balanceWei = balance.balance;
+ }
+ } catch (error) {
+ console.error('X-Chain balance fetch error:', error);
+ balanceWei = 0n;
+ }
+ }
+
+ // Convert to native token - different chains use different units
+ let nativeBalance: string;
+ if (chainType === 'cChain') {
+ // C-Chain uses wei (18 decimals)
+ nativeBalance = formatUnits(balanceWei, 18);
+ } else {
+ // P-Chain and X-Chain use nanoAVAX (9 decimals)
+ nativeBalance = formatUnits(balanceWei, 9);
+ }
+
+ return {
+ avax: parseFloat(nativeBalance).toFixed(4),
+ wei: balanceWei,
+ loading: false,
+ lastUpdated: new Date(),
+ };
+ } catch (error) {
+ console.error(`Failed to fetch ${chainType} balance:`, error);
+ return {
+ avax: '0',
+ wei: 0n,
+ loading: false,
+ lastUpdated: new Date(),
+ };
+ }
+ }, [walletClient, pAddress, xAddress, currentChain]);
+
+ // Refresh all balances
+ const refreshBalances = useCallback(async () => {
+ if (!address || status !== 'connected') return;
+
+ // Set loading state for all chains
+ setBalances(prev => ({
+ pChain: { ...prev.pChain, loading: true },
+ cChain: { ...prev.cChain, loading: true },
+ xChain: { ...prev.xChain, loading: true },
+ }));
+
+ try {
+ // Fetch all balances in parallel
+ const [pChainBalance, cChainBalance, xChainBalance] = await Promise.all([
+ fetchChainBalance('pChain', address),
+ fetchChainBalance('cChain', address),
+ fetchChainBalance('xChain', address),
+ ]);
+
+ setBalances({
+ pChain: pChainBalance,
+ cChain: cChainBalance,
+ xChain: xChainBalance,
+ });
+ } catch (error) {
+ console.error('Failed to refresh balances:', error);
+ // Reset loading states on error
+ setBalances(prev => ({
+ pChain: { ...prev.pChain, loading: false },
+ cChain: { ...prev.cChain, loading: false },
+ xChain: { ...prev.xChain, loading: false },
+ }));
+ }
+ }, [address, status, fetchChainBalance]);
+
+ const connect = useCallback(async () => {
+ try {
+ updateStatus('connecting');
+ setError(undefined);
+
+ // Use centralized connect function from AvalancheProvider
+ await connectWallet();
+
+ updateStatus('connected');
+ onConnect?.(address!);
+
+ // Fetch balances after successful connection
+ setTimeout(() => refreshBalances(), 100);
+ } catch (err: any) {
+ handleError(err);
+ }
+ }, [connectWallet, handleError, updateStatus, onConnect, refreshBalances, address]);
+
+ const disconnect = useCallback(() => {
+ setError(undefined);
+ updateStatus('disconnected');
+
+ // Use centralized disconnect function from AvalancheProvider
+ disconnectWallet();
+
+ // Reset balances on disconnect
+ setBalances({
+ pChain: { avax: '0', wei: 0n, loading: false },
+ cChain: { avax: '0', wei: 0n, loading: false },
+ xChain: { avax: '0', wei: 0n, loading: false },
+ });
+ onDisconnect?.();
+ }, [disconnectWallet, updateStatus, onDisconnect]);
+
+ const switchChain = useCallback(async (newChain: Chain) => {
+ try {
+ setCurrentChain(newChain);
+ // If connected, we might need to reconnect with the new chain
+ if (status === 'connected') {
+ // For now, just update the chain. In a real implementation,
+ // you might want to request the wallet to switch networks
+ setError(undefined);
+ }
+ } catch (err: any) {
+ handleError(err);
+ }
+ }, [status, handleError]);
+
+ const clearError = useCallback(() => {
+ setError(undefined);
+ if (status === 'error') {
+ updateStatus('disconnected');
+ }
+ }, [status, updateStatus]);
+
+ // Sync wallet connection status with AvalancheProvider
+ useEffect(() => {
+ if (isWalletConnected && address) {
+ updateStatus('connected');
+ // Fetch balances for connected wallet
+ setTimeout(() => refreshBalances(), 100);
+ // Update X and P chain addresses
+ updateXPAddresses();
+ } else {
+ updateStatus('disconnected');
+ setXAddress(undefined);
+ setPAddress(undefined);
+ }
+ }, [isWalletConnected, address, updateStatus, refreshBalances, updateXPAddresses]);
+
+ const contextValue = useMemo((): WalletContextType => ({
+ status,
+ address,
+ xAddress,
+ pAddress,
+ error,
+ availableChains,
+ currentChain,
+ walletClient,
+ balances,
+ connect,
+ disconnect,
+ switchChain,
+ refreshBalances,
+ clearError,
+ }), [
+ status,
+ address,
+ xAddress,
+ pAddress,
+ error,
+ availableChains,
+ currentChain,
+ walletClient,
+ balances,
+ connect,
+ disconnect,
+ switchChain,
+ refreshBalances,
+ clearError,
+ ]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/ui/src/wallet/components/WalletTransactions.tsx b/ui/src/wallet/components/WalletTransactions.tsx
new file mode 100644
index 00000000..b64bbd50
--- /dev/null
+++ b/ui/src/wallet/components/WalletTransactions.tsx
@@ -0,0 +1,401 @@
+'use client';
+import { useState, useEffect, useMemo, forwardRef, useImperativeHandle } from 'react';
+import { useWalletContext } from '../hooks/useWalletContext';
+import { useTransactions } from '../../glacier/wallet/useTransactions';
+import { useAvalanche } from '../../AvalancheProvider';
+import { cn, text } from '../../styles/theme';
+import { Button } from '../../components/ui/button';
+import { RefreshCw, Loader2, ExternalLink, ChevronLeft, ChevronRight, ArrowUpRight, ArrowDownLeft } from 'lucide-react';
+import type { WalletTransactionsProps } from '../types';
+import { formatUnits } from 'viem';
+import { TokenChip } from '../../token/components/TokenChip';
+import type { Token } from '../../token/types';
+import type { TransactionDetails, Erc20TransferDetails } from '@avalanche-sdk/chainkit/models/components';
+import { formatRelativeTime, abbreviateAddress, buildExplorerUrl } from '../../utils';
+
+const DEFAULT_ITEMS_PER_PAGE = 10;
+
+export interface WalletTransactionsRef {
+ refresh: () => Promise;
+}
+
+export const WalletTransactions = forwardRef(function WalletTransactions({
+ className,
+ showRefresh = true,
+ maxItems,
+ itemsPerPage = DEFAULT_ITEMS_PER_PAGE,
+ startBlock,
+ endBlock,
+ sortOrder = 'desc',
+}, ref) {
+ const { status, address } = useWalletContext();
+ const { chain } = useAvalanche();
+ const [currentPage, setCurrentPage] = useState(1);
+
+ const { transactions, loading, error, refresh: fetchTransactions } = useTransactions({
+ startBlock,
+ endBlock,
+ sortOrder,
+ maxItems,
+ autoFetch: true,
+ });
+
+ // Expose refresh method via ref
+ useImperativeHandle(ref, () => ({
+ refresh: fetchTransactions,
+ }), [fetchTransactions]);
+
+ // Pagination logic
+ const totalPages = useMemo(() => {
+ const total = maxItems ? Math.min(transactions.length, maxItems) : transactions.length;
+ return Math.ceil(total / itemsPerPage);
+ }, [transactions.length, maxItems, itemsPerPage]);
+
+ const displayedTransactions = useMemo(() => {
+ const startIndex = (currentPage - 1) * itemsPerPage;
+ const endIndex = startIndex + itemsPerPage;
+ const transactionsToShow = maxItems ? transactions.slice(0, maxItems) : transactions;
+ return transactionsToShow.slice(startIndex, endIndex);
+ }, [transactions, currentPage, maxItems, itemsPerPage]);
+
+
+ // Reset to page 1 when transactions change
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [transactions.length]);
+
+ // Helper function to determine if address is sender or receiver
+ const getTransactionDirection = (tx: TransactionDetails, address: string) => {
+ const nativeTx = tx.nativeTransaction;
+ const addressLower = address.toLowerCase();
+ const isSender = nativeTx.from?.address?.toLowerCase() === addressLower;
+ const isReceiver = nativeTx.to?.address?.toLowerCase() === addressLower;
+
+ if (isSender && isReceiver) return 'self';
+ if (isSender) return 'out';
+ if (isReceiver) return 'in';
+ return 'unknown';
+ };
+
+ // Helper function to extract input/output tokens with amounts from transaction
+ const getTransactionTokens = (tx: TransactionDetails, address: string): {
+ input: Array<{ token: Token; amount: string }>;
+ output: Array<{ token: Token; amount: string }>
+ } => {
+ const input: Array<{ token: Token; amount: string }> = [];
+ const output: Array<{ token: Token; amount: string }> = [];
+ const addressLower = address.toLowerCase();
+
+ // Process ERC-20 transfers
+ if (tx.erc20Transfers) {
+ tx.erc20Transfers.forEach((transfer: Erc20TransferDetails) => {
+ const fromLower = transfer.from?.address?.toLowerCase();
+ const toLower = transfer.to?.address?.toLowerCase();
+
+ if (transfer.erc20Token) {
+ const token: Token = {
+ address: transfer.erc20Token.address,
+ chainId: chain.id,
+ decimals: transfer.erc20Token.decimals,
+ image: transfer.erc20Token.logoUri || null,
+ name: transfer.erc20Token.name,
+ symbol: transfer.erc20Token.symbol,
+ };
+
+ const amount = formatUnits(BigInt(transfer.value || '0'), transfer.erc20Token.decimals);
+
+ if (fromLower === addressLower) {
+ // Token sent out
+ input.push({ token, amount });
+ } else if (toLower === addressLower) {
+ // Token received
+ output.push({ token, amount });
+ }
+ }
+ });
+ }
+
+ // Process native transaction
+ const nativeTx = tx.nativeTransaction;
+ const nativeValue = nativeTx.value ? BigInt(nativeTx.value) : BigInt(0);
+ if (nativeValue > 0) {
+ // Get chain iconUrl if available
+ const chainIconUrl = (chain as any)?.iconUrl || null;
+
+ const nativeToken: Token = {
+ address: '',
+ chainId: chain.id,
+ decimals: 18,
+ image: chainIconUrl,
+ name: chain.nativeCurrency?.name || 'Native',
+ symbol: chain.nativeCurrency?.symbol || 'AVAX',
+ };
+
+ const fromLower = nativeTx.from?.address?.toLowerCase();
+ const toLower = nativeTx.to?.address?.toLowerCase();
+ const amount = formatUnits(nativeValue, 18);
+
+ if (fromLower === addressLower && toLower !== addressLower) {
+ input.push({ token: nativeToken, amount });
+ } else if (toLower === addressLower && fromLower !== addressLower) {
+ output.push({ token: nativeToken, amount });
+ }
+ }
+
+ return { input, output };
+ };
+
+
+
+ if (status !== 'connected' || !address) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ Transaction History
+
+
+ {transactions.length} transaction{transactions.length !== 1 ? 's' : ''}
+
+
+ {showRefresh && (
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ {error && (
+
+ )}
+
+ {loading && transactions.length === 0 ? (
+
+
+
+ ) : displayedTransactions.length === 0 ? (
+
+
+ No transactions found
+
+
+ ) : (
+ <>
+ {/* Table Header */}
+
+
+
+ Type
+
+
+ Transaction
+
+
+ Actions
+
+
+
+
+ {/* Table Body */}
+
+ {displayedTransactions.map((tx, index) => {
+ const direction = getTransactionDirection(tx, address);
+ const { input, output } = getTransactionTokens(tx, address);
+ const nativeTx = tx.nativeTransaction;
+ const isLast = index === displayedTransactions.length - 1;
+ const isFirst = index === 0;
+
+ return (
+
+
+ {/* Direction Icon */}
+
+ {direction === 'out' ? (
+
+ ) : direction === 'in' ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Transaction Info */}
+
+ {/* Tokens with Amounts */}
+
+ {input.length > 0 && (
+ <>
+ {input.map((item, idx) => (
+
+
+
+ {parseFloat(item.amount).toLocaleString(undefined, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 6,
+ })}
+
+
+ ))}
+
→
+ >
+ )}
+ {output.length > 0 ? (
+ output.map((item, idx) => (
+
+
+
+ {parseFloat(item.amount).toLocaleString(undefined, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 6,
+ })}
+
+
+ ))
+ ) : (
+
+ {abbreviateAddress(nativeTx.to?.address || '')}
+
+ )}
+
+
+ {/* Transaction Details */}
+
+
+
+ {nativeTx.blockNumber}
+
+
+ •
+
+
+ {abbreviateAddress(nativeTx.txHash)}
+
+
+ •
+
+
+ {formatRelativeTime(nativeTx.blockTimestamp)}
+
+ {nativeTx.gasUsed && (
+ <>
+
+ •
+
+
+ Gas: {parseInt(nativeTx.gasUsed).toLocaleString()}
+
+ >
+ )}
+ {nativeTx.gasUsed && nativeTx.gasPrice && (() => {
+ const gasFeeWei = BigInt(nativeTx.gasUsed) * BigInt(nativeTx.gasPrice);
+ const gasFeeFormatted = formatUnits(gasFeeWei, 18);
+ const gasFeeNumber = parseFloat(gasFeeFormatted);
+ return (
+ <>
+
+ •
+
+
+ Fee: {gasFeeNumber.toLocaleString(undefined, {
+ minimumFractionDigits: 6,
+ maximumFractionDigits: 6,
+ })} {chain.nativeCurrency?.symbol || 'AVAX'}
+
+ >
+ );
+ })()}
+
+
+
+
+ {/* Actions Column */}
+
+
+ {!isLast && (
+
+ )}
+
+ );
+ })}
+
+
+ {/* Pagination Controls */}
+ {totalPages > 1 && (
+
+
+ Showing {((currentPage - 1) * itemsPerPage) + 1} - {Math.min(currentPage * itemsPerPage, maxItems ? Math.min(transactions.length, maxItems) : transactions.length)} of {maxItems ? Math.min(transactions.length, maxItems) : transactions.length} transactions
+
+
+
setCurrentPage((prev) => Math.max(1, prev - 1))}
+ disabled={currentPage === 1}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+ {currentPage} / {totalPages}
+
+
setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
+ disabled={currentPage === totalPages}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
+ )}
+ >
+ )}
+
+ );
+});
+
diff --git a/ui/src/wallet/hooks/useWalletContext.ts b/ui/src/wallet/hooks/useWalletContext.ts
new file mode 100644
index 00000000..8c8954db
--- /dev/null
+++ b/ui/src/wallet/hooks/useWalletContext.ts
@@ -0,0 +1,14 @@
+import { useContext } from 'react';
+import { WalletContext } from '../components/WalletProvider';
+
+/**
+ * Hook to access the wallet context.
+ * Must be used within a WalletProvider.
+ */
+export function useWalletContext() {
+ const context = useContext(WalletContext);
+ if (!context) {
+ throw new Error('useWalletContext must be used within a WalletProvider');
+ }
+ return context;
+}
diff --git a/ui/src/wallet/index.ts b/ui/src/wallet/index.ts
new file mode 100644
index 00000000..be11aadf
--- /dev/null
+++ b/ui/src/wallet/index.ts
@@ -0,0 +1,38 @@
+// 🏔️❄️🏔️
+// Components
+export { WalletProvider } from './components/WalletProvider';
+export { WalletConnect } from './components/WalletConnect';
+export { WalletDropdown } from './components/WalletDropdown';
+export { WalletBalance } from './components/WalletBalance';
+export { WalletPortfolio } from './components/WalletPortfolio';
+export type { WalletPortfolioRef } from './components/WalletPortfolio';
+export { WalletTransactions } from './components/WalletTransactions';
+export type { WalletTransactionsRef } from './components/WalletTransactions';
+export { WalletActivity } from './components/WalletActivity';
+export { NetworkSelector } from './components/NetworkSelector';
+export { WalletMessage } from './components/WalletMessage';
+
+// Hooks
+export { useWalletContext } from './hooks/useWalletContext';
+export { useErc20Balances } from '../glacier/wallet/useErc20Balances';
+export type { UseErc20BalancesOptions, UseErc20BalancesReturn } from '../glacier/wallet/useErc20Balances';
+export { useNativeBalance } from '../glacier/wallet/useNativeBalance';
+export type { UseNativeBalanceOptions, UseNativeBalanceReturn } from '../glacier/wallet/useNativeBalance';
+export { useTransactions } from '../glacier/wallet/useTransactions';
+export type { UseTransactionsOptions, UseTransactionsReturn } from '../glacier/wallet/useTransactions';
+
+// Types
+export type {
+ WalletStatus,
+ WalletError,
+ WalletContextType,
+ WalletProviderProps,
+ WalletConnectProps,
+ WalletDropdownProps,
+ WalletBalanceProps,
+ WalletPortfolioProps,
+ WalletTransactionsProps,
+ WalletActivityProps,
+ NetworkSelectorProps,
+ WalletMessageProps,
+} from './types';
diff --git a/ui/src/wallet/types.ts b/ui/src/wallet/types.ts
new file mode 100644
index 00000000..e58b1808
--- /dev/null
+++ b/ui/src/wallet/types.ts
@@ -0,0 +1,177 @@
+import type { ReactNode } from 'react';
+import type { Chain } from '@avalanche-sdk/client/chains';
+import type { Address } from '@avalanche-sdk/client';
+
+export type WalletStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
+
+export type WalletError = {
+ message: string;
+ code?: string | number;
+};
+
+export type WalletBalance = {
+ /** Balance in AVAX (formatted string) */
+ avax: string;
+ /** Balance in wei (bigint) */
+ wei: bigint;
+ /** USD value (formatted string) */
+ usd?: string;
+ /** Loading state */
+ loading: boolean;
+ /** Last updated timestamp */
+ lastUpdated?: Date;
+};
+
+export type ChainBalances = {
+ /** P-Chain balance */
+ pChain: WalletBalance;
+ /** C-Chain balance */
+ cChain: WalletBalance;
+ /** X-Chain balance */
+ xChain: WalletBalance;
+};
+
+export type WalletContextType = {
+ /** Current wallet connection status */
+ status: WalletStatus;
+ /** Connected wallet address */
+ address?: Address;
+ /** X-Chain address (derived from wallet) */
+ xAddress?: string;
+ /** P-Chain address (derived from wallet) */
+ pAddress?: string;
+ /** Current error if any */
+ error?: WalletError;
+ /** Available chains for network switching */
+ availableChains: Chain[];
+ /** Currently selected chain */
+ currentChain: Chain;
+ /** Wallet client for transactions (null if no wallet) */
+ walletClient: ReturnType | null;
+ /** Balances across all chains */
+ balances: ChainBalances;
+ /** Connect to wallet */
+ connect: () => Promise;
+ /** Disconnect from wallet */
+ disconnect: () => void;
+ /** Switch to a different chain */
+ switchChain: (chain: Chain) => Promise;
+ /** Refresh balances */
+ refreshBalances: () => Promise;
+ /** Clear any errors */
+ clearError: () => void;
+};
+
+export type WalletProviderProps = {
+ children: ReactNode;
+ /** Initial chain to connect to */
+ initialChain?: Chain;
+ /** Callback when connection status changes */
+ onStatusChange?: (status: WalletStatus) => void;
+ /** Callback when an error occurs */
+ onError?: (error: WalletError) => void;
+ /** Callback when successfully connected */
+ onConnect?: (address: Address) => void;
+ /** Callback when disconnected */
+ onDisconnect?: () => void;
+};
+
+export type WalletConnectProps = {
+ /** Custom class name */
+ className?: string;
+ /** Custom text for the connect button */
+ connectText?: string;
+ /** Custom text for the connected state */
+ connectedText?: string;
+ /** Show loading state */
+ showLoading?: boolean;
+};
+
+export type WalletDropdownProps = {
+ /** Custom class name */
+ className?: string;
+ /** Show balance in dropdown */
+ showBalance?: boolean;
+ /** Show network info in dropdown */
+ showNetwork?: boolean;
+ /** Show X-Chain and P-Chain addresses */
+ showXPAddresses?: boolean;
+};
+
+export type WalletBalanceProps = {
+ /** Custom class name */
+ className?: string;
+ /** Token symbol to display (defaults to AVAX) */
+ symbol?: string;
+ /** Show USD value */
+ showUSD?: boolean;
+ /** Number of decimal places to show */
+ decimals?: number;
+ /** Which chain balance to display */
+ chainType?: 'pChain' | 'cChain' | 'xChain';
+};
+
+export type NetworkSelectorProps = {
+ /** Custom class name */
+ className?: string;
+ /** Show testnet networks */
+ showTestnets?: boolean;
+ /** Custom network options */
+ networks?: Chain[];
+};
+
+export type WalletMessageProps = {
+ /** Custom class name */
+ className?: string;
+ /** Show detailed error messages */
+ showDetails?: boolean;
+};
+
+export type WalletPortfolioProps = {
+ /** Custom class name */
+ className?: string;
+ /** Show USD values */
+ showUSD?: boolean;
+ /** Show refresh button */
+ showRefresh?: boolean;
+ /** Maximum number of tokens to display */
+ maxItems?: number;
+ /** Number of items per page for pagination */
+ itemsPerPage?: number;
+ /** Specific contract addresses to fetch balances for */
+ contractAddresses?: string[];
+ /** Block number to fetch balances at */
+ blockNumber?: number;
+};
+
+export type WalletTransactionsProps = {
+ /** Custom class name */
+ className?: string;
+ /** Show refresh button */
+ showRefresh?: boolean;
+ /** Maximum number of transactions to display */
+ maxItems?: number;
+ /** Number of items per page for pagination */
+ itemsPerPage?: number;
+ /** Start block number for filtering */
+ startBlock?: number;
+ /** End block number for filtering */
+ endBlock?: number;
+ /** Sort order: 'asc' or 'desc' (default: 'desc') */
+ sortOrder?: 'asc' | 'desc';
+};
+
+export type WalletActivityProps = {
+ /** Custom class name */
+ className?: string;
+ /** Portfolio props */
+ portfolioProps?: Omit;
+ /** Transactions props */
+ transactionsProps?: Omit;
+ /** Default active tab */
+ defaultTab?: 'portfolio' | 'transactions';
+ /** Show refresh button */
+ showRefresh?: boolean;
+ /** Number of items per page for pagination */
+ itemsPerPage?: number;
+};
diff --git a/ui/tailwind.config.js b/ui/tailwind.config.js
new file mode 100644
index 00000000..546a9e66
--- /dev/null
+++ b/ui/tailwind.config.js
@@ -0,0 +1,49 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./src/**/*.{js,ts,jsx,tsx}'],
+ theme: {
+ extend: {
+ colors: {
+ background: 'var(--background)',
+ foreground: 'var(--foreground)',
+ card: {
+ DEFAULT: 'var(--card)',
+ foreground: 'var(--card-foreground)',
+ },
+ popover: {
+ DEFAULT: 'var(--popover)',
+ foreground: 'var(--popover-foreground)',
+ },
+ primary: {
+ DEFAULT: 'var(--primary)',
+ foreground: 'var(--primary-foreground)',
+ },
+ secondary: {
+ DEFAULT: 'var(--secondary)',
+ foreground: 'var(--secondary-foreground)',
+ },
+ muted: {
+ DEFAULT: 'var(--muted)',
+ foreground: 'var(--muted-foreground)',
+ },
+ accent: {
+ DEFAULT: 'var(--accent)',
+ foreground: 'var(--accent-foreground)',
+ },
+ destructive: {
+ DEFAULT: 'var(--destructive)',
+ foreground: 'var(--destructive-foreground)',
+ },
+ border: 'var(--border)',
+ input: 'var(--input)',
+ ring: 'var(--ring)',
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)',
+ },
+ },
+ },
+ plugins: [require('tailwindcss-animate')],
+};
\ No newline at end of file
diff --git a/ui/tsconfig.json b/ui/tsconfig.json
new file mode 100644
index 00000000..e58ea221
--- /dev/null
+++ b/ui/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "dist"
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist", "**/*.test.*", "**/*.stories.*"]
+}
diff --git a/ui/vite.config.ts b/ui/vite.config.ts
new file mode 100644
index 00000000..c4d286c5
--- /dev/null
+++ b/ui/vite.config.ts
@@ -0,0 +1,50 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import dts from 'vite-plugin-dts';
+import { externalizeDeps } from 'vite-plugin-externalize-deps';
+import { resolve } from 'path';
+
+export default defineConfig({
+ plugins: [
+ react(),
+ externalizeDeps(),
+ dts({
+ insertTypesEntry: true,
+ exclude: ['**/*.test.*', '**/*.stories.*', 'vitest.config.ts'],
+ }),
+ ],
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, './src'),
+ },
+ },
+ build: {
+ lib: {
+ entry: {
+ index: resolve(__dirname, 'src/index.ts'),
+ wallet: resolve(__dirname, 'src/wallet/index.ts'),
+ transfer: resolve(__dirname, 'src/transfer/index.ts'),
+ theme: resolve(__dirname, 'src/styles/theme.ts'),
+ },
+ formats: ['es'],
+ },
+ rollupOptions: {
+ external: [
+ 'react',
+ 'react-dom',
+ '@avalanche-sdk/client',
+ 'viem'
+ ],
+ output: {
+ preserveModules: true,
+ preserveModulesRoot: 'src',
+ entryFileNames: '[name].js',
+ assetFileNames: 'assets/[name].[ext]',
+ },
+ },
+ cssCodeSplit: false,
+ },
+ css: {
+ postcss: './postcss.config.js',
+ },
+});
diff --git a/ui/vitest.config.ts b/ui/vitest.config.ts
new file mode 100644
index 00000000..96138af4
--- /dev/null
+++ b/ui/vitest.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vitest/config';
+import react from '@vitejs/plugin-react';
+import { resolve } from 'path';
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: ['./vitest.setup.ts'],
+ },
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, './src'),
+ },
+ },
+});
diff --git a/ui/vitest.setup.ts b/ui/vitest.setup.ts
new file mode 100644
index 00000000..26080e5d
--- /dev/null
+++ b/ui/vitest.setup.ts
@@ -0,0 +1,23 @@
+import '@testing-library/jest-dom';
+import { beforeAll, afterEach, afterAll } from 'vitest';
+import { cleanup } from '@testing-library/react';
+
+// Cleanup after each test case
+afterEach(() => {
+ cleanup();
+});
+
+// Mock window.matchMedia
+Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: (query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: () => {},
+ removeListener: () => {},
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ dispatchEvent: () => {},
+ }),
+});