From 983440a55831574c5c77eedfb26d577aee78f949 Mon Sep 17 00:00:00 2001 From: 0xstt Date: Tue, 11 Nov 2025 12:14:03 +0300 Subject: [PATCH] feat: add @avalanche-sdk/ui package - initial commit --- .github/workflows/ui_sdk_publish.yaml | 119 + ui/.gitignore | 56 + ui/package.json | 138 + ui/playground/favicon.ico | Bin 0 -> 3758 bytes ui/playground/index.html | 13 + ui/playground/package.json | 39 + ui/playground/postcss.config.js | 7 + ui/playground/src/App.css | 131 + ui/playground/src/App.tsx | 1805 +++++ ui/playground/src/chains/dispatch.ts | 30 + ui/playground/src/chains/echo.ts | 30 + ui/playground/src/chains/index.ts | 2 + .../src/components/ChainBalancesSection.tsx | 57 + .../src/components/ChainListDemo.tsx | 54 + ui/playground/src/components/CodeModal.tsx | 137 + ui/playground/src/components/CodeSnippet.tsx | 109 + .../src/components/ComponentWithCode.tsx | 60 + ui/playground/src/components/EarnDemo.tsx | 75 + ui/playground/src/components/EarnDemo1.tsx | 24 + ui/playground/src/components/EarnDemo2.tsx | 24 + ui/playground/src/components/Footer.tsx | 188 + .../src/components/InstallCommand.tsx | 130 + .../src/components/NavigationSidebar.tsx | 163 + ui/playground/src/components/SimpleICTT.tsx | 200 + ui/playground/src/components/SimpleICTT1.tsx | 118 + ui/playground/src/components/SimpleICTT2.tsx | 122 + .../src/components/SimpleTransfer.tsx | 51 + .../components/SingleChainTransferDemo.tsx | 53 + .../src/components/ThemeSwitcher.tsx | 147 + ui/playground/src/index.css | 44 + ui/playground/src/main.tsx | 11 + ui/playground/tailwind.config.js | 54 + ui/playground/vite.config.ts | 25 + ui/pnpm-lock.yaml | 6853 +++++++++++++++++ ui/pnpm-workspace.yaml | 5 + ui/postcss.config.js | 6 + ui/src/3rd-party/earn/aave/aaveProvider.ts | 153 + ui/src/3rd-party/earn/aave/abis.ts | 138 + ui/src/3rd-party/earn/aave/approve.ts | 94 + ui/src/3rd-party/earn/aave/claim.ts | 81 + ui/src/3rd-party/earn/aave/contracts.ts | 223 + ui/src/3rd-party/earn/aave/deposit.ts | 68 + ui/src/3rd-party/earn/aave/index.ts | 8 + ui/src/3rd-party/earn/aave/withdraw.ts | 69 + ui/src/3rd-party/earn/benqi/abis.ts | 170 + ui/src/3rd-party/earn/benqi/approve.ts | 116 + ui/src/3rd-party/earn/benqi/benqiProvider.ts | 110 + ui/src/3rd-party/earn/benqi/claim.ts | 69 + ui/src/3rd-party/earn/benqi/contracts.ts | 244 + ui/src/3rd-party/earn/benqi/deposit.ts | 113 + ui/src/3rd-party/earn/benqi/index.ts | 8 + ui/src/3rd-party/earn/benqi/withdraw.ts | 53 + ui/src/AvalancheProvider.tsx | 430 ++ ui/src/assets/avalanche-logo.svg | 6 + ui/src/chain/components/ChainLogo.tsx | 89 + ui/src/chain/components/ChainRow.tsx | 59 + .../chain/components/ChainSelectDropdown.tsx | 129 + ui/src/chain/index.ts | 19 + ui/src/chain/types.ts | 62 + ui/src/chain/utils/getChainColor.ts | 30 + ui/src/chain/utils/normalizeChain.ts | 30 + ui/src/components/ui/address-input.tsx | 156 + ui/src/components/ui/alert.tsx | 58 + ui/src/components/ui/amount-input.tsx | 135 + .../components/ui/avalanche-chain-overlay.tsx | 113 + ui/src/components/ui/avalanche-logo.tsx | 47 + ui/src/components/ui/badge.tsx | 43 + ui/src/components/ui/button.tsx | 57 + ui/src/components/ui/card.tsx | 76 + ui/src/components/ui/direction-toggle.tsx | 48 + ui/src/components/ui/index.ts | 25 + ui/src/components/ui/input.tsx | 25 + ui/src/components/ui/label.tsx | 24 + ui/src/components/ui/select.tsx | 159 + ui/src/components/ui/tabs.tsx | 56 + .../ui/wallet-connection-overlay.tsx | 76 + ui/src/earn/components/Earn.tsx | 193 + ui/src/earn/components/EarnClaimRewards.tsx | 110 + ui/src/earn/components/EarnDeposit.tsx | 199 + ui/src/earn/components/EarnPoolCard.tsx | 102 + ui/src/earn/components/EarnPoolListItem.tsx | 89 + ui/src/earn/components/EarnPoolsList.tsx | 129 + ui/src/earn/components/EarnProvider.tsx | 585 ++ .../earn/components/EarnProviderSelector.tsx | 29 + ui/src/earn/components/EarnSinglePoolCard.tsx | 218 + ui/src/earn/components/EarnWithdraw.tsx | 128 + ui/src/earn/components/index.ts | 12 + ui/src/earn/hooks/index.ts | 2 + ui/src/earn/index.ts | 5 + ui/src/earn/providers/base.ts | 74 + ui/src/earn/providers/index.ts | 3 + ui/src/earn/providers/registry.ts | 38 + ui/src/earn/types.ts | 131 + ui/src/glacier/wallet/useErc20Balances.ts | 155 + ui/src/glacier/wallet/useNativeBalance.ts | 104 + ui/src/glacier/wallet/useTransactions.ts | 139 + ui/src/hooks/index.ts | 1 + ui/src/hooks/useSwitchChain.ts | 52 + ui/src/ictt/components/ICTT.tsx | 67 + ui/src/ictt/components/ICTTAddressInput.tsx | 98 + ui/src/ictt/components/ICTTAmountInput.tsx | 109 + ui/src/ictt/components/ICTTButtons.tsx | 135 + ui/src/ictt/components/ICTTChainSelector.tsx | 78 + .../components/ICTTHomeTokenAddressInput.tsx | 336 + ui/src/ictt/components/ICTTProvider.tsx | 470 ++ .../ICTTRemoteTokenAddressInput.tsx | 332 + ui/src/ictt/components/ICTTToggleButton.tsx | 26 + .../ictt/components/ICTTTokenModeToggle.tsx | 67 + ui/src/ictt/components/ICTTTokenSelector.tsx | 80 + ui/src/ictt/components/index.ts | 11 + ui/src/ictt/hooks/index.ts | 1 + ui/src/ictt/hooks/useICTTContext.ts | 1 + ui/src/ictt/index.ts | 4 + ui/src/ictt/types.ts | 93 + ui/src/index.ts | 50 + ui/src/stake/components/Stake.tsx | 64 + ui/src/stake/components/StakeAmountInput.tsx | 216 + ui/src/stake/components/StakeButton.tsx | 40 + .../stake/components/StakeDurationInput.tsx | 136 + ui/src/stake/components/StakeMessage.tsx | 70 + ui/src/stake/components/StakeProvider.tsx | 391 + .../stake/components/StakeValidatorInput.tsx | 309 + ui/src/stake/hooks/useStakeContext.ts | 1 + ui/src/stake/index.ts | 27 + ui/src/stake/types.ts | 118 + ui/src/styles/index.css | 242 + ui/src/styles/theme.ts | 31 + ui/src/theme.tsx | 86 + ui/src/token/components/TokenChip.tsx | 50 + ui/src/token/components/TokenImage.tsx | 65 + ui/src/token/components/TokenRow.tsx | 121 + ui/src/token/components/TokenSelectButton.tsx | 48 + .../token/components/TokenSelectDropdown.tsx | 98 + ui/src/token/index.ts | 25 + ui/src/token/types.ts | 121 + ui/src/token/utils/formatAmount.ts | 22 + ui/src/token/utils/getTokenImageColor.ts | 24 + .../components/CrossChainTransfer.tsx | 123 + ui/src/transfer/components/Transfer.tsx | 97 + .../components/TransferAmountInput.tsx | 65 + ui/src/transfer/components/TransferButton.tsx | 44 + .../components/TransferChainSelector.tsx | 87 + .../transfer/components/TransferMessage.tsx | 103 + .../transfer/components/TransferProvider.tsx | 278 + ui/src/transfer/components/TransferToast.tsx | 89 + .../components/TransferToggleButton.tsx | 22 + ui/src/transfer/hooks/useTransferContext.ts | 14 + ui/src/transfer/index.ts | 30 + ui/src/transfer/types.ts | 118 + ui/src/types/chainConfig.ts | 18 + ui/src/utils/addressValidation.ts | 206 + ui/src/utils/erc20.ts | 51 + ui/src/utils/explorer.ts | 79 + ui/src/utils/formatAddress.ts | 17 + ui/src/utils/formatRelativeTime.ts | 41 + ui/src/utils/index.ts | 12 + ui/src/wallet/components/NetworkSelector.tsx | 67 + ui/src/wallet/components/WalletActivity.tsx | 82 + ui/src/wallet/components/WalletBalance.tsx | 75 + ui/src/wallet/components/WalletConnect.tsx | 63 + ui/src/wallet/components/WalletDropdown.tsx | 197 + ui/src/wallet/components/WalletMessage.tsx | 66 + ui/src/wallet/components/WalletPortfolio.tsx | 344 + ui/src/wallet/components/WalletProvider.tsx | 327 + .../wallet/components/WalletTransactions.tsx | 401 + ui/src/wallet/hooks/useWalletContext.ts | 14 + ui/src/wallet/index.ts | 38 + ui/src/wallet/types.ts | 177 + ui/tailwind.config.js | 49 + ui/tsconfig.json | 28 + ui/vite.config.ts | 50 + ui/vitest.config.ts | 17 + ui/vitest.setup.ts | 23 + 173 files changed, 25022 insertions(+) create mode 100644 .github/workflows/ui_sdk_publish.yaml create mode 100644 ui/.gitignore create mode 100644 ui/package.json create mode 100644 ui/playground/favicon.ico create mode 100644 ui/playground/index.html create mode 100644 ui/playground/package.json create mode 100644 ui/playground/postcss.config.js create mode 100644 ui/playground/src/App.css create mode 100644 ui/playground/src/App.tsx create mode 100644 ui/playground/src/chains/dispatch.ts create mode 100644 ui/playground/src/chains/echo.ts create mode 100644 ui/playground/src/chains/index.ts create mode 100644 ui/playground/src/components/ChainBalancesSection.tsx create mode 100644 ui/playground/src/components/ChainListDemo.tsx create mode 100644 ui/playground/src/components/CodeModal.tsx create mode 100644 ui/playground/src/components/CodeSnippet.tsx create mode 100644 ui/playground/src/components/ComponentWithCode.tsx create mode 100644 ui/playground/src/components/EarnDemo.tsx create mode 100644 ui/playground/src/components/EarnDemo1.tsx create mode 100644 ui/playground/src/components/EarnDemo2.tsx create mode 100644 ui/playground/src/components/Footer.tsx create mode 100644 ui/playground/src/components/InstallCommand.tsx create mode 100644 ui/playground/src/components/NavigationSidebar.tsx create mode 100644 ui/playground/src/components/SimpleICTT.tsx create mode 100644 ui/playground/src/components/SimpleICTT1.tsx create mode 100644 ui/playground/src/components/SimpleICTT2.tsx create mode 100644 ui/playground/src/components/SimpleTransfer.tsx create mode 100644 ui/playground/src/components/SingleChainTransferDemo.tsx create mode 100644 ui/playground/src/components/ThemeSwitcher.tsx create mode 100644 ui/playground/src/index.css create mode 100644 ui/playground/src/main.tsx create mode 100644 ui/playground/tailwind.config.js create mode 100644 ui/playground/vite.config.ts create mode 100644 ui/pnpm-lock.yaml create mode 100644 ui/pnpm-workspace.yaml create mode 100644 ui/postcss.config.js create mode 100644 ui/src/3rd-party/earn/aave/aaveProvider.ts create mode 100644 ui/src/3rd-party/earn/aave/abis.ts create mode 100644 ui/src/3rd-party/earn/aave/approve.ts create mode 100644 ui/src/3rd-party/earn/aave/claim.ts create mode 100644 ui/src/3rd-party/earn/aave/contracts.ts create mode 100644 ui/src/3rd-party/earn/aave/deposit.ts create mode 100644 ui/src/3rd-party/earn/aave/index.ts create mode 100644 ui/src/3rd-party/earn/aave/withdraw.ts create mode 100644 ui/src/3rd-party/earn/benqi/abis.ts create mode 100644 ui/src/3rd-party/earn/benqi/approve.ts create mode 100644 ui/src/3rd-party/earn/benqi/benqiProvider.ts create mode 100644 ui/src/3rd-party/earn/benqi/claim.ts create mode 100644 ui/src/3rd-party/earn/benqi/contracts.ts create mode 100644 ui/src/3rd-party/earn/benqi/deposit.ts create mode 100644 ui/src/3rd-party/earn/benqi/index.ts create mode 100644 ui/src/3rd-party/earn/benqi/withdraw.ts create mode 100644 ui/src/AvalancheProvider.tsx create mode 100644 ui/src/assets/avalanche-logo.svg create mode 100644 ui/src/chain/components/ChainLogo.tsx create mode 100644 ui/src/chain/components/ChainRow.tsx create mode 100644 ui/src/chain/components/ChainSelectDropdown.tsx create mode 100644 ui/src/chain/index.ts create mode 100644 ui/src/chain/types.ts create mode 100644 ui/src/chain/utils/getChainColor.ts create mode 100644 ui/src/chain/utils/normalizeChain.ts create mode 100644 ui/src/components/ui/address-input.tsx create mode 100644 ui/src/components/ui/alert.tsx create mode 100644 ui/src/components/ui/amount-input.tsx create mode 100644 ui/src/components/ui/avalanche-chain-overlay.tsx create mode 100644 ui/src/components/ui/avalanche-logo.tsx create mode 100644 ui/src/components/ui/badge.tsx create mode 100644 ui/src/components/ui/button.tsx create mode 100644 ui/src/components/ui/card.tsx create mode 100644 ui/src/components/ui/direction-toggle.tsx create mode 100644 ui/src/components/ui/index.ts create mode 100644 ui/src/components/ui/input.tsx create mode 100644 ui/src/components/ui/label.tsx create mode 100644 ui/src/components/ui/select.tsx create mode 100644 ui/src/components/ui/tabs.tsx create mode 100644 ui/src/components/ui/wallet-connection-overlay.tsx create mode 100644 ui/src/earn/components/Earn.tsx create mode 100644 ui/src/earn/components/EarnClaimRewards.tsx create mode 100644 ui/src/earn/components/EarnDeposit.tsx create mode 100644 ui/src/earn/components/EarnPoolCard.tsx create mode 100644 ui/src/earn/components/EarnPoolListItem.tsx create mode 100644 ui/src/earn/components/EarnPoolsList.tsx create mode 100644 ui/src/earn/components/EarnProvider.tsx create mode 100644 ui/src/earn/components/EarnProviderSelector.tsx create mode 100644 ui/src/earn/components/EarnSinglePoolCard.tsx create mode 100644 ui/src/earn/components/EarnWithdraw.tsx create mode 100644 ui/src/earn/components/index.ts create mode 100644 ui/src/earn/hooks/index.ts create mode 100644 ui/src/earn/index.ts create mode 100644 ui/src/earn/providers/base.ts create mode 100644 ui/src/earn/providers/index.ts create mode 100644 ui/src/earn/providers/registry.ts create mode 100644 ui/src/earn/types.ts create mode 100644 ui/src/glacier/wallet/useErc20Balances.ts create mode 100644 ui/src/glacier/wallet/useNativeBalance.ts create mode 100644 ui/src/glacier/wallet/useTransactions.ts create mode 100644 ui/src/hooks/index.ts create mode 100644 ui/src/hooks/useSwitchChain.ts create mode 100644 ui/src/ictt/components/ICTT.tsx create mode 100644 ui/src/ictt/components/ICTTAddressInput.tsx create mode 100644 ui/src/ictt/components/ICTTAmountInput.tsx create mode 100644 ui/src/ictt/components/ICTTButtons.tsx create mode 100644 ui/src/ictt/components/ICTTChainSelector.tsx create mode 100644 ui/src/ictt/components/ICTTHomeTokenAddressInput.tsx create mode 100644 ui/src/ictt/components/ICTTProvider.tsx create mode 100644 ui/src/ictt/components/ICTTRemoteTokenAddressInput.tsx create mode 100644 ui/src/ictt/components/ICTTToggleButton.tsx create mode 100644 ui/src/ictt/components/ICTTTokenModeToggle.tsx create mode 100644 ui/src/ictt/components/ICTTTokenSelector.tsx create mode 100644 ui/src/ictt/components/index.ts create mode 100644 ui/src/ictt/hooks/index.ts create mode 100644 ui/src/ictt/hooks/useICTTContext.ts create mode 100644 ui/src/ictt/index.ts create mode 100644 ui/src/ictt/types.ts create mode 100644 ui/src/index.ts create mode 100644 ui/src/stake/components/Stake.tsx create mode 100644 ui/src/stake/components/StakeAmountInput.tsx create mode 100644 ui/src/stake/components/StakeButton.tsx create mode 100644 ui/src/stake/components/StakeDurationInput.tsx create mode 100644 ui/src/stake/components/StakeMessage.tsx create mode 100644 ui/src/stake/components/StakeProvider.tsx create mode 100644 ui/src/stake/components/StakeValidatorInput.tsx create mode 100644 ui/src/stake/hooks/useStakeContext.ts create mode 100644 ui/src/stake/index.ts create mode 100644 ui/src/stake/types.ts create mode 100644 ui/src/styles/index.css create mode 100644 ui/src/styles/theme.ts create mode 100644 ui/src/theme.tsx create mode 100644 ui/src/token/components/TokenChip.tsx create mode 100644 ui/src/token/components/TokenImage.tsx create mode 100644 ui/src/token/components/TokenRow.tsx create mode 100644 ui/src/token/components/TokenSelectButton.tsx create mode 100644 ui/src/token/components/TokenSelectDropdown.tsx create mode 100644 ui/src/token/index.ts create mode 100644 ui/src/token/types.ts create mode 100644 ui/src/token/utils/formatAmount.ts create mode 100644 ui/src/token/utils/getTokenImageColor.ts create mode 100644 ui/src/transfer/components/CrossChainTransfer.tsx create mode 100644 ui/src/transfer/components/Transfer.tsx create mode 100644 ui/src/transfer/components/TransferAmountInput.tsx create mode 100644 ui/src/transfer/components/TransferButton.tsx create mode 100644 ui/src/transfer/components/TransferChainSelector.tsx create mode 100644 ui/src/transfer/components/TransferMessage.tsx create mode 100644 ui/src/transfer/components/TransferProvider.tsx create mode 100644 ui/src/transfer/components/TransferToast.tsx create mode 100644 ui/src/transfer/components/TransferToggleButton.tsx create mode 100644 ui/src/transfer/hooks/useTransferContext.ts create mode 100644 ui/src/transfer/index.ts create mode 100644 ui/src/transfer/types.ts create mode 100644 ui/src/types/chainConfig.ts create mode 100644 ui/src/utils/addressValidation.ts create mode 100644 ui/src/utils/erc20.ts create mode 100644 ui/src/utils/explorer.ts create mode 100644 ui/src/utils/formatAddress.ts create mode 100644 ui/src/utils/formatRelativeTime.ts create mode 100644 ui/src/utils/index.ts create mode 100644 ui/src/wallet/components/NetworkSelector.tsx create mode 100644 ui/src/wallet/components/WalletActivity.tsx create mode 100644 ui/src/wallet/components/WalletBalance.tsx create mode 100644 ui/src/wallet/components/WalletConnect.tsx create mode 100644 ui/src/wallet/components/WalletDropdown.tsx create mode 100644 ui/src/wallet/components/WalletMessage.tsx create mode 100644 ui/src/wallet/components/WalletPortfolio.tsx create mode 100644 ui/src/wallet/components/WalletProvider.tsx create mode 100644 ui/src/wallet/components/WalletTransactions.tsx create mode 100644 ui/src/wallet/hooks/useWalletContext.ts create mode 100644 ui/src/wallet/index.ts create mode 100644 ui/src/wallet/types.ts create mode 100644 ui/tailwind.config.js create mode 100644 ui/tsconfig.json create mode 100644 ui/vite.config.ts create mode 100644 ui/vitest.config.ts create mode 100644 ui/vitest.setup.ts 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 0000000000000000000000000000000000000000..ecde3034d08753780290ba3ac3eb78e11622bb32 GIT binary patch literal 3758 zcmeHJYiL_#7=GJM)7skpSY2J|=*2c^ljd@AE=|@ZC-~KnP>>c8 z#UU!la4P723=2ARGMsopvN?wn2XozI9o1#U+0NQ7c1v$da(uqiovmq3@+;jB@e6Ox zIq&y<@AJL4=NzIk{92lb{nOrBq76jU32A}2AWW|g%>6&~BOp6Q$zwZR7viVA5#ee` zPeT0PUa$SkdOvr8T-KwiAzS;~6cf|rcl_QM7Lq}|eVQ)J&&Xb2__*J44pCWINn3gp zg8e7@rSx3Cl%3a?%v?lB9hB|o8i1$Tuy?#m8|uaKFn{Ge>{-&^w`?yarRRb1B*Uty zskwQEs;VlAh$#y4lQFEB?Uxj#uw=6E^SNEy6q3w`$lkG+c0H&Ne0v_)*~0r21#!9> zbWUuIiVIX-U40Xu-Sk?2CaLR717fwWm<=`;6Boz)+<0fud0DmLLo8(b=tkH*QEYES zxVEpy{H?)bJC%d2KCB~xUled&5@O=grDAsArxFon-|A}HUE%KjfjHAgw9TdFZ%0hZ zAb0uki;36A;X4Qai0;UTXBx@;HJdBLIbLBHdUiA&>`eun6CTwb_Ihn+sJHv4YWVo6 zw&vC~tAvHwA%@|$EY~f&*BgHBBFT=iHpKGmt^N7_usL9Tve|n4E*4Kq+iQf{B>3n- z{H9j!jf#sW{f>)G0q##d$Q&KlF-$xBESA$x_e+`dN;|9XQBSFmb87n*#NN`jhg_EL z$c&sIH*ae3Lp~Nm{LIdWdKGeX?9ag}^dQ^Mg9}FLqr%*g)tIZFvF3ZP?OeUjzTDG6 z=Om#vjqrJtt&Iw)FZ!&fYQ6T+LU4I{OY0t$4}#9AhvDP3)tI@@nKb-+=CQCsyy{hRGdGIx~pZ}xmLq%IY##TK3E5Se`jSsBJWAO2O>BZwcb0hR`9A_#&Lc}!is)O zXfZ(FeF)yU6W@<(ip`Ak#)G7cFn*tTk;Q<0s|2_|_%QzuH8niC=J*VNeQ$YeXB&}+ z_YwD}+BdyW&fYt>YnWPIq6g5AF7uHJ&Um0q?ELC>LI3A)lp7;qh#L|WLl9ic;)Wz8 z>LASvG|-%;0Xa=BIZJ(mS!x + + + + + + 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'; + + + + + + + + +`} + language="tsx" + > + + + + New + Button Variants + + Theme-aware button styles + + + + + + + + + + + + {/* 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} + /> + +
+ + ); +}`} + 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" + /> + + + +
+ + {/* 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) => ( + + ))} +
+
+ + { + 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} + +
+
+ + +
+
+ + {/* 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! +
+ )} +
+
+ ); +} 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(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 ( +
+
+ {/* Main Footer Content */} +
+ {/* Brand Section */} +
+
+ + Avalanche SDK +
+

+ TypeScript SDK for building on Avalanche. Explore components, + integrate wallets, and interact with the blockchain. +

+
+ + + +
+
+ + {/* Resources */} +
+

+ Resources +

+
    + {footerLinks.resources.map((link) => { + const Icon = link.icon; + return ( +
  • + + + {link.label} + + +
  • + ); + })} +
+
+ + {/* Development */} +
+

+ Development +

+
    + {footerLinks.development.map((link) => { + const Icon = link.icon; + return ( +
  • + + + {link.label} + + +
  • + ); + })} +
+
+ + {/* Info */} +
+

+ Built With +

+
    +
  • +
    + React & TypeScript +
  • +
  • +
    + Tailwind CSS +
  • +
  • +
    + Vite +
  • +
  • +
    + Open Source +
  • +
+
+
+ + {/* Bottom Bar */} +
+
+

+ © {currentYear} Avalanche SDK TypeScript. All rights reserved. +

+ +
+
+
+
+ ); +} 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! +
+ )} +
+
+
+ ); +} + +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 ( + <> + + + {/* 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} + )} + + {fromChainData?.name || 'Echo L1'} + + + {toChainData?.iconUrl && ( + {toChainData.name} + )} + + {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} + )} + + {fromChainData?.name || 'Echo L1'} + + + {toChainData?.iconUrl && ( + {toChainData.name} + )} + + {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) => ( + + ))} +
+
+ + { + 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 */} + + + + + + + + {/* Theme Selection */} +
+
+ Theme +
+
+ {themes.map((t) => ( + + + + ))} +
+
+
+
+
+ + {/* Mode Toggle Button */} + +
+ ); +} \ 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 ? ( + {normalized.name} 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 && ( + + )} + + +
+ ); +} 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 ( +
+ + +
+ + + {/* 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 ( +
+
+ + {formattedBalance && ( + + Max: {formattedBalance} + + )} +
+ +
+ + + {showMax && ( + + )} +
+ + {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 && ( + + )} + +
+ + {/* 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 */} + + + {/* 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 */} + + +
+
+ +
+ +
+ {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 && ( + + )} + + +
+
+ ); +} + 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 && ( + + )} + + {needsApproval && ( + + )} + + +
+
+ ); +} + 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 + + + + +
+
+ + {/* Loading state - initial load */} + {isLoadingPools && pools.length === 0 && ( +
+ +

Loading pools...

+
+ )} + + {/* 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 && ( +
+

No pools available

+
+ )} + + {/* 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 ( +
+ + 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 && ( + + )} + + +
+
+ ); +} + 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 ( +
+ + +
+ + + {/* Use My Address Button */} + {isWalletConnected && walletAddress && ( + + )} + + {/* 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 && ( + + )} + + {/* Approve Button */} + + + {/* Send Button */} + + + {/* 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 */} + + + {/* 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 */} + + + {/* 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 ? ( +
+ + +
+ ) : 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 && ( + + )} + +
+ ); +} \ 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 ( +
+ + +
+
+ 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 ( + + ); +} 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 ( +
+ + + {showPresets && ( +
+ {networkConfig.presets.map((preset) => ( + + ))} +
+ )} + +
+ +
+ 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 ( +
+
+ + + {/* 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]" + /> + + +
+ +
+ 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 */} +
+
+ + setManualNodeID(e.target.value)} + placeholder="NodeID-..." + disabled={disabled || fetchedFromRPC} + className={cn("font-mono", nodeIDError ? "border-destructive" : "")} + /> + {nodeIDError ? ( +
+ {nodeIDError} +
+ ) : ( +
+ Must start with "NodeID-" +
+ )} +
+ +
+ + 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) +
+ )} +
+ +
+ + 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 ( + + ); +} + 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 ( +
+ {`${name} 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 ( + + ); + } +); + +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 ( + + ); +} + 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 ( +
+ +
+ + Auto + +
+
+ ); +} + +function CrossChainTransferContent() { + return ( +
+ + +
+ + + + + +
+ + + +
+ + +
+ + + + +
+ ); +} + +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 ( + + ); +} 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 && ( + + )} +
+ + + + + + + + +
+ ); +} + 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 ( + + ); +} 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)} +
+
+
+ + +
+
+ ); + + return ( + + + + + + + + {/* 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 */} + + + + + + + + ); +} 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 && ( + + )} +
+ + {error && ( +
+

{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' : ''} +
+
+ +
+ {currentPage} / {totalPages} +
+ +
+
+ )} + + )} +
+ ); +}); + 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 && ( + + )} +
+ + {error && ( +
+

{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 +
+
+ +
+ {currentPage} / {totalPages} +
+ +
+
+ )} + + )} +
+ ); +}); + 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: () => {}, + }), +});