diff --git a/.env.example b/.env.example index 4122728f..a799108a 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,13 @@ # IMPORTANT: Never commit actual secrets to version control! # ============================================================================= +# ----------------------------------------------------------------------------- +# Authentication +# ----------------------------------------------------------------------------- +# Secret used to sign and verify auth-token cookies. Use a random value with +# at least 32 characters and keep it server-side only. +AUTH_SECRET= + # ----------------------------------------------------------------------------- # Application General Settings # ----------------------------------------------------------------------------- @@ -25,6 +32,15 @@ ANALYZE=false # Enable CSP enforcement (set to "true" after report-only rollout) CSP_ENFORCE=false +# ----------------------------------------------------------------------------- +# Security / CSRF Protection +# ----------------------------------------------------------------------------- + +# HMAC signing secret for CSRF tokens. REQUIRED: CSRF token minting and +# verification fail closed when this is unset. Generate a strong random value, +# e.g.: openssl rand -hex 32 +CSRF_SECRET= + # ----------------------------------------------------------------------------- # API Configurations # ----------------------------------------------------------------------------- @@ -35,6 +51,12 @@ NEXT_PUBLIC_PROPERTY_API_URL=https://api.propchain.example.com # Analytics API endpoint (optional) NEXT_PUBLIC_ANALYTICS_API_URL=https://analytics.propchain.example.com +# Server-side endpoint that receives POST /api/errors reports (required to persist reports) +ERROR_REPORTING_ENDPOINT=https://analytics.propchain.example.com/errors + +# Optional bearer token for the error-reporting destination +# ERROR_REPORTING_API_KEY=your-error-reporting-api-key + # ----------------------------------------------------------------------------- # Redis Cache Configuration # ----------------------------------------------------------------------------- @@ -74,6 +96,10 @@ POLYGON_MAINNET_RPC_URL=https://polygon-rpc.com # Binance Smart Chain Mainnet RPC URL BSC_MAINNET_RPC_URL=https://bsc-dataseed.binance.org +# Batch purchase contract address (deployed batch-purchase contract). +# Checkout fails closed with a configuration error when this is unset. +NEXT_PUBLIC_BATCH_PURCHASE_ADDRESS= + # ----------------------------------------------------------------------------- # Wallet Connect Configuration # ----------------------------------------------------------------------------- @@ -126,9 +152,34 @@ NEXT_PUBLIC_SENTRY_DSN= # Mock data mode (bypasses real API calls) NEXT_PUBLIC_USE_MOCK_DATA=false +# Enable demo/ simulated transaction mode for development (bypasses real blockchain reads) +NEXT_PUBLIC_DEMO_TX=false + # Skip authentication for development NEXT_PUBLIC_SKIP_AUTH=false +# Mock wallet connector (DEV ONLY โ€” never set in production builds). +# Even when set to "true", the mock connector is ignored when NODE_ENV is +# "production", so production builds always use real injected connectors. +# The mock signs with a randomly generated per-session key (no committed +# private key). +NEXT_PUBLIC_MOCK_WALLET=false + +# ----------------------------------------------------------------------------- +# Phishing Manifest (CDN) Configuration +# ----------------------------------------------------------------------------- +# The phishing denylist is fetched from a CDN and verified against a signature +# before it is applied. The manifest is NEVER fetched or applied unless +# NEXT_PUBLIC_MANIFEST_SIGNING_KEY is set (the check fails closed). Without it, +# only the bundled fallback list is used and a warning is logged. + +# Signer address (Ethereum address) that signed the phishing manifest. +# Required for CDN manifest verification; the manifest is rejected when unset. +NEXT_PUBLIC_MANIFEST_SIGNING_KEY= + +# CDN URL of the signed phishing manifest (optional, uses default if not set) +# NEXT_PUBLIC_PHISHING_MANIFEST_URL=https://cdn.propchain.io/security/phishing-manifest.json + # ----------------------------------------------------------------------------- # Rate Limiting Configuration # ----------------------------------------------------------------------------- @@ -142,6 +193,28 @@ RATE_LIMIT_MAX_REQUESTS=100 # Maximum requests per wallet address per time window (default: 50) RATE_LIMIT_MAX_REQUESTS_PER_WALLET=50 +# ----------------------------------------------------------------------------- +# Upstash Redis Configuration (for distributed rate limiting) +# ----------------------------------------------------------------------------- +# Get your Upstash Redis instance at https://upstash.com +# Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN for distributed +# rate limiting across serverless/edge deployments. When not set, the +# rate limiter falls back to in-memory storage (per-instance only). + +UPSTASH_REDIS_REST_URL= +UPSTASH_REDIS_REST_TOKEN= + +# ----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- +# Chainalysis Security API Configuration +# ----------------------------------------------------------------------------- +# API key for Chainalysis address/transaction risk checks. +# IMPORTANT: This is read server-side only. Never expose it to the browser. +# CHAINALYSIS_API_KEY=your_chainalysis_api_key + +# Chainalysis API base URL (optional, uses default if not set) +# CHAINALYSIS_API_URL=https://api.chainalysis.com/api/v2 + # ----------------------------------------------------------------------------- # Secret Management (Production) # ----------------------------------------------------------------------------- diff --git a/.env.schema b/.env.schema new file mode 100644 index 00000000..bb8b6a05 --- /dev/null +++ b/.env.schema @@ -0,0 +1,3 @@ +# Authentication +# Generated via: openssl rand -hex 32 +AUTH_SECRET= diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..561ef62f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,37 @@ +# CODEOWNERS โ€” Domain-based Ownership for PropChain + +# Web3 / Wallet / Smart Contract Integration +src/utils/wallet* @propchain/web3-team +src/middleware.ts @propchain/web3-team +src/lib/walletConnectors/ @propchain/web3-team +src/lib/walletConnectors.ts @propchain/web3-team +src/config/wagmi.ts @propchain/web3-team +src/config/wallets.ts @propchain/web3-team +src/config/chains.ts @propchain/web3-team +src/hooks/useWalletConnector.ts @propchain/web3-team +src/lib/viem-client.ts @propchain/web3-team +src/features/web3-error/ @propchain/web3-team + +# Observability / Logging / Monitoring +src/utils/logger.ts @propchain/observability-team +src/utils/structuredLogger.ts @propchain/observability-team +src/utils/errorHandling.ts @propchain/observability-team +src/utils/errorReporting.ts @propchain/observability-team +src/utils/errorFactory.ts @propchain/observability-team +src/utils/errorMonitoringService.ts @propchain/observability-team +src/utils/earlyErrorSuppression.ts @propchain/observability-team +src/features/error-fallback/ @propchain/observability-team +src/features/recovery/ @propchain/observability-team +src/lib/rateLimit.ts @propchain/observability-team +src/lib/redis.ts @propchain/observability-team +src/lib/redisCache.ts @propchain/observability-team +src/lib/initRedisCache.ts @propchain/observability-team + +# Accessibility +src/features/transaction-a11y/ @propchain/accessibility-team + +# Smart Contracts / ABIs +src/config/abis.ts @propchain/smart-contract-team +src/config/referralContracts.ts @propchain/smart-contract-team +_fixtures/PropertyNFT.abi.json @propchain/smart-contract-team +_fixtures/ @propchain/smart-contract-team diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..0e2fe73f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,49 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "UTC" + open-pull-requests-limit: 10 + groups: + web3: + patterns: + - "viem" + - "wagmi" + - "@wagmi/*" + - "ethers" + - "@safe-global/*" + - "@walletconnect/*" + - "@coinbase/wallet-sdk" + - "@metamask/sdk" + ui: + patterns: + - "@radix-ui/*" + - "lucide-react" + - "framer-motion" + - "class-variance-authority" + - "tailwind-merge" + - "clsx" + - "cmdk" + - "vaul" + - "sonner" + infra: + patterns: + - "next" + - "react" + - "react-dom" + - "@types/react" + - "@types/react-dom" + - "typescript" + - "eslint" + - "@typescript-eslint/*" + - "eslint-config-next" + - "zustand" + labels: + - "dependencies" + - "Stellar Wave" + reviewers: + - "oladev2026-tech" diff --git a/.github/prompts/autonomy-version.json b/.github/prompts/autonomy-version.json new file mode 100644 index 00000000..39afc297 --- /dev/null +++ b/.github/prompts/autonomy-version.json @@ -0,0 +1,3 @@ +{ + "version": "2025.11" +} diff --git a/.github/prompts/autonomy.manifest.json b/.github/prompts/autonomy.manifest.json new file mode 100644 index 00000000..25f7f880 --- /dev/null +++ b/.github/prompts/autonomy.manifest.json @@ -0,0 +1,8 @@ +{ + "version": "2025.11", + "consent": { + "phrase": "", + "expiresMinutes": 0 + }, + "actions": [] +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..5557ddff --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-and-typecheck: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Lint + continue-on-error: true + run: npm run lint + + - name: Typecheck + continue-on-error: true + run: npm run typecheck + + test: + runs-on: ubuntu-latest + services: + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Wait for Redis + run: | + for i in $(seq 1 30); do + if redis-cli -h localhost -p 6379 ping | grep -q PONG; then + echo "Redis is ready!" + exit 0 + fi + echo "Waiting for Redis... ($i/30)" + sleep 2 + done + echo "Redis failed to start" + exit 1 + + - name: Run tests + env: + REDIS_HOST: localhost + REDIS_PORT: 6379 + run: npm run test:ci + + build: + runs-on: ubuntu-latest + needs: [lint-and-typecheck, test] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Build + run: npx next build diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 00000000..7369b0ff --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,77 @@ +name: Preview Environment + +on: + pull_request: + types: [opened, synchronize, reopened] + pull_request_target: + types: [closed] + +concurrency: + group: preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + deploy-preview: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + outputs: + preview_url: ${{ steps.deploy.outputs.preview_url }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Install Vercel CLI + run: npm install -g vercel + + - name: Deploy to Vercel Preview + id: deploy + run: | + DEPLOY_URL=$(vercel deploy --token=${{ secrets.VERCEL_TOKEN }} \ + --env NEXT_PUBLIC_PREVIEW_FORK_URL=http://preview-fork:8545 \ + --yes 2>&1 | tail -1) + echo "preview_url=$DEPLOY_URL" >> "$GITHUB_OUTPUT" + + - name: Comment PR with Preview URL + uses: actions/github-script@v7 + with: + script: | + const previewUrl = '${{ steps.deploy.outputs.preview_url }}'; + const body = `## ๐Ÿš€ Preview Environment Ready\n\n- **Frontend Preview:** ${previewUrl}\n- **Hardhat Fork:** \`http://preview-fork:8545\`\n\nThis environment will be destroyed when the PR is closed.`; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body, + }); + + destroy-preview: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - name: Install Vercel CLI + run: npm install -g vercel + + - name: Remove Vercel Preview Deployment + run: | + vercel remove --token=${{ secrets.VERCEL_TOKEN }} --yes --safe propchain-preview-pr-${{ github.event.pull_request.number }} + continue-on-error: true + + - name: Comment PR with teardown notice + uses: actions/github-script@v7 + with: + script: | + const body = `## ๐Ÿ”’ Preview Environment Destroyed\n\nThe preview environment for this PR has been torn down.`; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body, + }); diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..9062c4d6 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +gitleaks protect --staged --no-banner \ No newline at end of file diff --git a/.kilo/kilo.jsonc b/.kilo/kilo.jsonc new file mode 100644 index 00000000..d3e1b2d9 --- /dev/null +++ b/.kilo/kilo.jsonc @@ -0,0 +1,3 @@ +{ + "snapshot": false +} \ No newline at end of file diff --git a/AuthGuard.tsx b/AuthGuard.tsx index 50d5d1ff..9f5f6dcd 100644 --- a/AuthGuard.tsx +++ b/AuthGuard.tsx @@ -1,22 +1,40 @@ 'use client'; -import { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useRouter, usePathname } from 'next/navigation'; import { useAuth } from '@/hooks/useAuth'; import { Loader2 } from 'lucide-react'; export function AuthGuard({ children }: { children: React.ReactNode }) { - const { isAuthenticated, isLoading } = useAuth(); + const { isAuthenticated, isLoading, sessionExpiresAt, WARN_BEFORE_MS } = useAuth(); const router = useRouter(); const pathname = usePathname(); + const [showTimeoutWarning, setShowTimeoutWarning] = useState(false); useEffect(() => { if (!isLoading && !isAuthenticated) { - // Fallback client-side redirect router.push(`/?callbackUrl=${encodeURIComponent(pathname)}`); } }, [isLoading, isAuthenticated, router, pathname]); + // Schedule a warning announcement 5 minutes before session expiry + useEffect(() => { + if (!sessionExpiresAt) return; + + const warnAt = sessionExpiresAt - WARN_BEFORE_MS; + const delay = warnAt - Date.now(); + + if (delay <= 0) return; + + const warningTimer = setTimeout(() => setShowTimeoutWarning(true), delay); + const expireTimer = setTimeout(() => setShowTimeoutWarning(false), sessionExpiresAt - Date.now()); + + return () => { + clearTimeout(warningTimer); + clearTimeout(expireTimer); + }; + }, [sessionExpiresAt, WARN_BEFORE_MS]); + if (isLoading) { return (
@@ -29,5 +47,20 @@ export function AuthGuard({ children }: { children: React.ReactNode }) { return null; } - return <>{children}; -} \ No newline at end of file + return ( + <> + {/* aria-live region is always mounted so SR picks up dynamic content changes */} +
+ {showTimeoutWarning + ? 'Your session will expire in 5 minutes. Please save your work.' + : ''} +
+ {children} + + ); +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ed4fea9..fdcd4586 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,7 @@ Before you start contributing, make sure you have: ### Development Setup 1. **Fork the Repository** + ```bash # Fork the repository on GitHub, then clone your fork git clone https://github.com/YOUR_USERNAME/PropChain-FrontEnd.git @@ -24,11 +25,13 @@ Before you start contributing, make sure you have: ``` 2. **Add Upstream Remote** + ```bash git remote add upstream https://github.com/MettaChain/PropChain-FrontEnd.git ``` 3. **Install Dependencies** + ```bash npm install # or @@ -38,6 +41,7 @@ Before you start contributing, make sure you have: ``` 4. **Set Up Environment Variables** + ```bash cp .env.example .env # Edit .env with your configuration @@ -45,16 +49,16 @@ Before you start contributing, make sure you have: Key variables to configure: - | Variable | Description | Example | - |---|---|---| - | `NEXT_PUBLIC_API_URL` | Backend REST API base URL | `http://localhost:3001` | - | `NEXT_PUBLIC_WS_URL` | WebSocket server URL | `ws://localhost:3001` | - | `NEXT_PUBLIC_BLOCKCHAIN_NETWORK` | Target network name | `sepolia` | - | `NEXT_PUBLIC_RPC_URL` | Ethereum JSON-RPC endpoint | `https://sepolia.infura.io/v3/YOUR_KEY` | - | `NEXT_PUBLIC_CHAIN_ID` | EVM chain ID (decimal) | `11155111` | - | `NEXT_PUBLIC_ENABLE_TESTNET` | Enable testnet features | `true` | - | `NEXT_PUBLIC_GOOGLE_ANALYTICS_ID` | GA4 measurement ID | `G-XXXXXXXXXX` | - | `NEXT_PUBLIC_SENTRY_DSN` | Sentry error tracking DSN | `https://...@sentry.io/...` | + | Variable | Description | Example | + | --------------------------------- | -------------------------- | --------------------------------------- | + | `NEXT_PUBLIC_API_URL` | Backend REST API base URL | `http://localhost:3001` | + | `NEXT_PUBLIC_WS_URL` | WebSocket server URL | `ws://localhost:3001` | + | `NEXT_PUBLIC_BLOCKCHAIN_NETWORK` | Target network name | `sepolia` | + | `NEXT_PUBLIC_RPC_URL` | Ethereum JSON-RPC endpoint | `https://sepolia.infura.io/v3/YOUR_KEY` | + | `NEXT_PUBLIC_CHAIN_ID` | EVM chain ID (decimal) | `11155111` | + | `NEXT_PUBLIC_ENABLE_TESTNET` | Enable testnet features | `true` | + | `NEXT_PUBLIC_GOOGLE_ANALYTICS_ID` | GA4 measurement ID | `G-XXXXXXXXXX` | + | `NEXT_PUBLIC_SENTRY_DSN` | Sentry error tracking DSN | `https://...@sentry.io/...` | > **Never commit `.env` to version control.** It is already listed in `.gitignore`. @@ -80,6 +84,7 @@ Before you start contributing, make sure you have: ### Submitting Pull Requests 1. **Create a Branch** + ```bash git checkout -b feature/your-feature-name # or @@ -93,27 +98,30 @@ Before you start contributing, make sure you have: - Update documentation if needed 3. **Test Your Changes** + ```bash # Run tests npm test - + # Run type checking npm run type-check - + # Run linting npm run lint - + # Build the project npm run build ``` 4. **Commit Your Changes** + ```bash git add . git commit -m "feat: add your feature description" ``` 5. **Push to Your Fork** + ```bash git push origin feature/your-feature-name ``` @@ -129,6 +137,7 @@ Before you start contributing, make sure you have: We welcome contributions in the following areas: ### ๐Ÿ  Core Features + - Property browsing and search functionality - Wallet connection and Web3 integration - Smart contract interactions @@ -136,6 +145,7 @@ We welcome contributions in the following areas: - Transaction history ### ๐ŸŽจ UI/UX Improvements + - Component library enhancements - Responsive design fixes - Accessibility improvements @@ -143,6 +153,7 @@ We welcome contributions in the following areas: - Design system updates ### ๐Ÿ”ง Technical Improvements + - Code refactoring and optimization - Testing coverage improvements - Documentation updates @@ -150,6 +161,7 @@ We welcome contributions in the following areas: - Security improvements ### ๐Ÿ“š Documentation + - API documentation - Component documentation - Tutorial creation @@ -166,25 +178,35 @@ We use the following tools to maintain code quality: - **Prettier**: Code formatting - **TypeScript**: Type safety - **Husky**: Git hooks for pre-commit checks +- **gitleaks**: Pre-commit hook to prevent committing secrets **Auto-format before committing**: + ```bash npm run lint -- --fix # auto-fix ESLint issues ``` +**Secret Detection**: +We use `gitleaks` to prevent committing secrets to the repository. The pre-commit hook will automatically scan staged files for sensitive information like API keys and private keys. If a secret is detected, the commit will be blocked. + +If you need to commit a file that contains a value that is being flagged as a secret, but you are sure it is a false positive, you can add it to the `.secrets.baseline` file. + **Prettier config** (`.prettierrc` or `prettier.config.js`): + - Single quotes for strings - 2-space indentation - Trailing commas in multi-line structures - 100-character line length limit **TypeScript rules**: + - Prefer `interface` over `type` for object shapes - Always type function return values explicitly for exported functions - Avoid `any` โ€” use `unknown` and narrow with type guards instead - Use `const` assertions (`as const`) for literal tuples and objects **React-specific rules**: + - One component per file - Prefer named exports over default exports for components - Use `React.FC` or explicit return type annotations @@ -211,11 +233,7 @@ export const PropertyCard: React.FC = ({ property, onPurchase, }) => { - return ( -
- {/* Component content */} -
- ); + return
{/* Component content */}
; }; export default PropertyCard; @@ -234,6 +252,7 @@ We follow [Conventional Commits](https://www.conventionalcommits.org/) specifica - `chore:` Build process or dependency changes Examples: + ``` feat: add property search functionality fix: resolve wallet connection issue @@ -254,22 +273,22 @@ test: add unit tests for property service ```tsx // Example unit test -import { render, screen } from '@testing-library/react'; -import { PropertyCard } from './PropertyCard'; +import { render, screen } from "@testing-library/react"; +import { PropertyCard } from "./PropertyCard"; -describe('PropertyCard', () => { +describe("PropertyCard", () => { const mockProperty = { - id: '1', - name: 'Test Property', - price: '100000', + id: "1", + name: "Test Property", + price: "100000", // ... other properties }; - it('renders property information correctly', () => { + it("renders property information correctly", () => { render(); - - expect(screen.getByText('Test Property')).toBeInTheDocument(); - expect(screen.getByText('$100,000')).toBeInTheDocument(); + + expect(screen.getByText("Test Property")).toBeInTheDocument(); + expect(screen.getByText("$100,000")).toBeInTheDocument(); }); }); ``` @@ -316,11 +335,13 @@ npm run build **Coverage requirements**: PRs must not decrease overall coverage below the current threshold. Check the current threshold in `jest.config.js` under `coverageThreshold`. **Running a single test file**: + ```bash npx jest src/hooks/useTransaction.test.ts ``` **Debugging a failing test**: + ```bash npx jest --verbose --no-coverage src/path/to/test.ts ``` @@ -346,6 +367,102 @@ We use a consistent design system based on: ## ๐ŸŒ Web3 Development +### Adding a New Wallet Connector + +PropChain uses lazy-loaded wallet connector modules to keep the initial bundle small (~178 KB savings). To add support for a new wallet (e.g. Trust Wallet, Rainbow, Phantom), follow this step-by-step guide. + +#### Step 1: Create the connector module + +Create `src/lib/walletConnectors/.ts`. Every connector must export two functions: + +```typescript +// src/lib/walletConnectors/.ts + +export interface WalletNameConnectorResult { + address: string; // 0x-prefixed hex address + chainId: number; // decimal chain ID +} + +/** Connect to the wallet. Must throw descriptive user-facing errors. */ +export const connectWalletNameWallet = + async (): Promise => { + // 1. DETECTION โ€” check if the wallet is available + // 2. CONNECTION โ€” request accounts via the provider + // 3. VALIDATION โ€” verify the returned address and chainId + // 4. RETURN the result + }; + +/** Synchronous check: is the wallet installed and ready? */ +export const isWalletNameAvailable = (): boolean => { + // Return true only if the provider object is present and the wallet flag is set +}; +``` + +#### Step 2: Detection + +Before attempting connection, verify the wallet provider is present: + +- **Injected wallets** (MetaMask, Coinbase, Rainbow): check `window.ethereum` and the wallet-specific flag (`isMetaMask`, `isCoinbaseWallet`, `isRainbow`). +- **Mobile / deep-link wallets** (Trust Wallet, Phantom): check `window.ethereum` or the wallet's own injected namespace. +- **QR-code wallets** (WalletConnect): check that the required environment variable (`NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID`) is configured. + +Throw a descriptive error if the wallet is not found: + +```typescript +if (!window.ethereum?.isWalletName) { + throw new Error( + "WalletName is not installed. Please install the WalletName extension or app to continue.", + ); +} +``` + +#### Step 3: Validation + +Always validate the response from the wallet before returning: + +```typescript +const accounts = await provider.request({ method: "eth_requestAccounts" }); + +// Validate the response shape +if (!Array.isArray(accounts) || accounts.length === 0) { + throw new Error("No accounts returned from WalletName"); +} + +const address = accounts[0]; +if (typeof address !== "string" || !address.startsWith("0x")) { + throw new Error("Invalid account address received from WalletName"); +} + +const chainIdHex = await provider.request({ method: "eth_chainId" }); +if (typeof chainIdHex !== "string") { + throw new Error("Invalid chain ID received from WalletName"); +} +``` + +#### Step 4: Error messages + +All errors thrown by the connector MUST be user-friendly. Handle these standard cases: + +| Error code | Meaning | User-facing message | +| ---------------- | ------------------------- | -------------------------------------------------------------------- | +| `4001` | User rejected the request | `"You rejected the connection request. Please try again."` | +| `-32002` | Request already pending | `"Connection request is already pending. Please check your wallet."` | +| Provider missing | Wallet not installed | `" is not installed. Please install the extension."` | +| Network error | RPC timeout or offline | `"Network error. Please check your connection and try again."` | + +Use `getErrorCode(error)` from `@/utils/typeGuards` to safely extract numeric error codes. + +#### Step 5: Register the connector + +1. **Add to `useWalletConnector.ts`**: Import the new connector and add a case for it in the `connectWallet` function. +2. **Add to `WalletModal.tsx`**: Add a button/option for the new wallet in the wallet selection UI. +3. **Add tests**: Create a test file under `src/lib/walletConnectors/__tests__/` that mocks the provider and covers connection, rejection, and missing-provider scenarios. +4. **Update this guide**: Add the new connector to the table in `src/lib/walletConnectors/README.md`. + +#### Template connector + +A minimal starting point is available at `src/lib/walletConnectors/README.md#adding-new-wallets`. + ### Wallet Integration When working with Web3 features: @@ -486,10 +603,10 @@ PropChain Frontend uses [Release Please](https://github.com/googleapis/release-p ### Version Bumping Rules -| Commit type | Version bump | -|---|---| -| `fix:` | Patch (`1.0.x`) | -| `feat:` | Minor (`1.x.0`) | +| Commit type | Version bump | +| ------------------------------------- | --------------- | +| `fix:` | Patch (`1.0.x`) | +| `feat:` | Minor (`1.x.0`) | | `feat!:` or `BREAKING CHANGE:` footer | Major (`x.0.0`) | ### Hotfix Process diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..e77e27b8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM node:20-slim AS base +RUN corepack enable && corepack prepare pnpm@latest --activate +WORKDIR /app + +FROM base AS deps +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile --prod=false + +FROM base AS builder +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production +RUN pnpm next build + +FROM node:20-slim AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" +CMD ["node", "server.js"] diff --git a/README.md b/README.md index b6862708..1e001fda 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,18 @@ Built with modern web technologies and Web3 integration, this frontend serves as ## ๐Ÿš€ Features ### Core Capabilities + - **๐Ÿ  Property Discovery**: Browse and search tokenized real estate properties with advanced filtering - **๐Ÿ’ฐ Wallet Integration**: Connect MetaMask, WalletConnect, and other Web3 wallets seamlessly +- **โš™๏ธ Optimized Developer Diagnostics**: Memoized error test scenarios for faster interactive debugging and smoother rendering +- **๐ŸŽฏ Route-Level Error Handling**: Full-page route fallback UI with retry and home navigation using `RouteErrorBoundary` - **๐Ÿ”— Smart Contract Interaction**: Execute property purchases, transfers, and management through intuitive UI - **๐Ÿ“Š Real-Time Data**: Live property valuations, market trends, and portfolio analytics - **๐Ÿ” Web3 Authentication**: Secure wallet-based authentication with multi-network support - **๏ฟฝ Responsive Design**: Mobile-first design that works perfectly on all devices ### Advanced Features + - **๐ŸŒ Multi-Chain Support**: Switch between Ethereum, Polygon, and BSC networks - **๐Ÿ“ˆ Portfolio Dashboard**: Track your real estate NFT investments and performance - **๐Ÿ” Advanced Search**: Filter by location, price range, property type, and ROI metrics @@ -26,6 +30,7 @@ Built with modern web technologies and Web3 integration, this frontend serves as ## ๐Ÿ‘ฅ Target Audience This frontend is designed for: + - **Real Estate Investors** looking to diversify into blockchain property assets - **Crypto Enthusiasts** seeking tangible real-world asset investments - **Property Developers** wanting to tokenize their real estate projects @@ -35,7 +40,9 @@ This frontend is designed for: ## ๐Ÿ› ๏ธ Quick Start ### Prerequisites + Ensure you have the following installed: + - **Node.js** v18+ (LTS recommended) - **npm**, **yarn**, or **pnpm** package manager - **Git** version control @@ -72,13 +79,19 @@ The application will be available at `http://localhost:3000`. ## ๐Ÿš€ Development & Deployment ### Development Environment + ```bash npm run dev # Start development server with hot reload npm run lint # Run ESLint for code quality checks npm run type-check # Run TypeScript type checking ``` +### Mocking + +For details on local mocking, see the [mocking documentation](./docs/mocking.md). + ### Production Build + ```bash npm run build # Build optimized production bundle npm run start # Start production server @@ -86,6 +99,7 @@ npm run analyze # Analyze bundle size with webpack-bundle-analyzer ``` ### Testing Suite + ```bash npm test # Run unit tests npm run test:watch # Run tests in watch mode @@ -96,12 +110,14 @@ npm run test:e2e # Run end-to-end tests ## ๐ŸŒ Network Configuration ### Supported Blockchains + - **Ethereum** (Mainnet, Sepolia Testnet) -- **Polygon** (Mainnet, Mumbai Testnet) +- **Polygon** (Mainnet, Mumbai Testnet) - **Binance Smart Chain** (Mainnet, Testnet) - **Local Development** (Hardhat Network) ### Environment Configuration + ```env # API Configuration NEXT_PUBLIC_API_URL=http://localhost:3001 @@ -124,12 +140,15 @@ NEXT_PUBLIC_SENTRY_DSN=your_sentry_dsn ## ๐Ÿ“š Documentation & Resources ### Project Documentation + - **[๐Ÿ“– Component Library](./docs/components.md)** - Reusable UI components and usage examples - **[๐Ÿ”— Web3 Integration](./docs/web3.md)** - Wallet connection and blockchain interaction guides - **[๐Ÿš€ Deployment Guide](./docs/deployment.md)** - Production deployment best practices - **[๐Ÿ—๏ธ Architecture](./docs/architecture.md)** - Frontend architecture and state management +- **[๐Ÿ”ง Mocking](./docs/mocking.md)** - Local mocking and development overrides ### Repository Structure + ``` PropChain-FrontEnd/ โ”œโ”€โ”€ ๐Ÿ“ src/ @@ -148,12 +167,14 @@ PropChain-FrontEnd/ ``` ### Contributing + - **[๐Ÿค Contributing Guide](./CONTRIBUTING.md)** - How to contribute effectively - **[๐Ÿ“‹ Code of Conduct](./CODE_OF_CONDUCT.md)** - Community guidelines and standards - **[๐Ÿ› Issue Templates](./.github/ISSUE_TEMPLATE/)** - Standardized issue reporting - **[๐Ÿ’ก Feature Requests](./.github/ISSUE_TEMPLATE/feature_request.md)** - Feature proposal template ### Additional Resources + - **[๐Ÿ”Œ Backend API](https://github.com/MettaChain/PropChain-BackEnd)** - Server-side NestJS application - **[๐ŸŽจ Design System](./docs/design-system.md)** - UI/UX guidelines and design tokens - **[๐Ÿ“Š Performance Metrics](./docs/performance.md)** - Optimization guides and benchmarks @@ -162,18 +183,21 @@ PropChain-FrontEnd/ ## ๐Ÿ› ๏ธ Technology Stack ### Frontend Framework + - **โš›๏ธ Framework**: Next.js 15 with App Router - Modern React framework - **๐ŸŽจ UI Library**: React 19 - Latest React with concurrent features - **๐ŸŽญ Styling**: Tailwind CSS 4 - Utility-first CSS framework - **๏ฟฝ Components**: Headless UI + custom components - Accessible UI primitives ### State Management & Data + - **๐Ÿ”„ State**: Zustand - Lightweight state management - **๐ŸŒ Data Fetching**: TanStack Query (React Query) - Server state management - **๐Ÿ”— Web3**: ethers.js + wagmi - Modern Ethereum React hooks - **๐Ÿ“ Forms**: React Hook Form + Zod - Type-safe form handling ### Development & Tooling + - **๏ฟฝ Language**: TypeScript 5 - Type-safe JavaScript - **๐Ÿงช Testing**: Jest + Testing Library + Playwright - Comprehensive testing - **๏ฟฝ Bundling**: Next.js built-in webpack - Optimized bundling @@ -181,6 +205,7 @@ PropChain-FrontEnd/ - **๐Ÿณ Containerization**: Docker - Consistent development environment ### UI/UX & Performance + - **๐ŸŽจ Design**: Tailwind CSS + custom design system - Consistent styling - **๏ฟฝ Analytics**: Google Analytics + Vercel Analytics - User insights - **๏ฟฝ SEO**: Next.js SEO optimizations - Search engine friendly @@ -188,6 +213,73 @@ PropChain-FrontEnd/ --- +## ๐Ÿ“ Logging + +All application code MUST log through the **canonical logger** at +`@/utils/logger`: + +```ts +import { logger } from "@/utils/logger"; + +logger.debug("โ€ฆ"); +logger.info("โ€ฆ"); +logger.warn("โ€ฆ"); +logger.error("โ€ฆ", errorObject); +``` + +### Why a single import path? + +- One canonical implementation owns redaction, correlation IDs, JSON output, + environment-aware levels, and singleton config (`configureLogger`). +- The legacy `@/utils/structuredLogger` module is kept as a thin + backwards-compat wrapper (re-exports + a domain-specific `StructuredLogger` + class with batching/remote delivery). It is marked **`@deprecated`** and an + ESLint `no-restricted-imports` rule blocks new imports outside the wrapper + itself. New code MUST NOT import from it. +- Direct `console.*` calls are blocked by ESLint for everything except + `src/utils/earlyErrorSuppression.ts`, which intentionally operates on the + raw global `console` because it runs **before** `logger` is initialised to + silence noisy browser-extension errors. + +### Backwards compatibility + +`@/utils/structuredLogger` re-exports `logger`, `createLogger`, `LogLevel`, +etc. from the canonical module so existing call sites continue to work +without changes. The wrapper itself (`StructuredLogger`, `logNetworkRequest`, +`logWeb3Activity`, `logTransaction`) is preserved for callers that rely on +its batching/remote-send semantics. + +--- + +## ๐Ÿ“Š Build stats plugin + +`next.config.ts` includes a small `BuildStatsPlugin` that writes a JSON +snapshot of webpack output to `.next/build-stats.json` for local inspection. + +To keep production builds lean and quiet, the plugin is gated by **two** +conditions: + +| Condition | Value | +| ---------------------- | --------------------------------------------- | +| `process.env.ANALYZE` | MUST be set to `'true'` | +| `process.env.NODE_ENV` | MUST NOT be `production` | +| Server-side build? | Plugin is client-only โ€” skipped on `isServer` | + +In other words: + +```bash +# Quiet (default for `next build` in production) +npm run build + +# Opt-in to the JSON build-stats snapshot โ€” local dev only +ANALYZE=true npm run dev # or: ANALYZE=true next build +``` + +Production CI MUST NOT pass `ANALYZE=true`; if it does the plugin is still +disabled by the `NODE_ENV === 'production'` guard. + +--- + ## ๐Ÿ“„ License This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for complete details. @@ -195,14 +287,17 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## ๐Ÿค Support & Community ### Get Help + - **๐Ÿ› Report Issues**: [GitHub Issues](https://github.com/MettaChain/PropChain-FrontEnd/issues) - **๐Ÿ“ง Email Support**: frontend@propchain.io - **๐Ÿ“– Documentation**: [docs.propchain.io](https://docs.propchain.io) ### Contributing -We welcome contributions! Please read our [Contributing Guide](./CONTRIBUTING.md) to get started. + +We welcome contributions! Please read our [Contributing Guide](./CONTRIBUTING.md) to get started. **Quick contribution steps:** + 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add amazing feature'`) @@ -218,3 +313,20 @@ We welcome contributions! Please read our [Contributing Guide](./CONTRIBUTING.md Made with โค๏ธ by the PropChain Team
+ +## ๐Ÿ› ๏ธ Local Development Guardrails (Git Hooks) + +To maximize code reliability and streamline PR review cycles, this project uses **Husky** to enforce local quality validation checks prior to remote integration. + +### Active Git Hook Safeguards + +- **Pre-Commit Hook:** Triggered automatically upon running `git commit`. Performs light syntax linting on modified files. +- **Pre-Push Hook:** Triggered automatically when executing `git push`. This gate forces an application-wide compile verification check (`tsc --noEmit`) and runs all matching unit tests. If compilation faults are surfaced or unit assertions fail, the push is safely aborted locally, keeping broken code off the remote origin branch. + +### Bypassing in Emergencies + +If you must explicitly push an intermediate draft up to a private backup branch without running validations, you can bypass Husky checks by appending the `--no-verify` flag: + +```bash +git push origin feature/my-branch --no-verify +``` diff --git a/__mocks__/viem.js b/__mocks__/viem.js index 4461d3ac..d55bba2b 100644 --- a/__mocks__/viem.js +++ b/__mocks__/viem.js @@ -1,3 +1,62 @@ +const formatUnitsValue = (value, decimals) => String(Number(value) / Math.pow(10, decimals)); +const parseUnitsValue = (value, decimals) => BigInt(Math.floor(Number(value) * Math.pow(10, decimals))); +const isAddressValue = (addr) => /^0x[a-fA-F0-9]{40}$/.test(addr); + +const mockReceipt = { + status: 'success', + blockNumber: BigInt(18000000), + transactionHash: '0x0000000000000000000000000000000000000000000000000000000000000000', + blockHash: '0x0000000000000000000000000000000000000000000000000000000000000000', + contractAddress: null, + cumulativeGasUsed: BigInt(100000), + gasUsed: BigInt(50000), + logs: [], + logsBloom: '0x0000000000000000000000000000000000000000000000000000000000000000', + from: '0x0000000000000000000000000000000000000000', + to: '0x0000000000000000000000000000000000000000', + effectiveGasPrice: BigInt(20000000000), + type: 'eip1559', +}; + +const mockClient = { + getTransactionReceipt: jest.fn().mockRejectedValue(new Error('receipt not found')), + waitForTransactionReceipt: jest.fn().mockRejectedValue(new Error('timeout')), +}; + module.exports = { + createPublicClient: jest.fn((config = {}) => ({ + ...config, + getBalance: jest.fn(), + getBlockNumber: jest.fn(), + readContract: jest.fn(), + waitForTransactionReceipt: jest.fn(), + })), + fallback: jest.fn((transports) => ({ type: 'fallback', transports })), + formatEther: jest.fn((value) => formatUnitsValue(value, 18)), + formatUnits: jest.fn(formatUnitsValue), + getAddress: jest.fn((value) => { + if (!isAddressValue(value)) { + throw new Error('Invalid address'); + } + + return value; + }), + http: jest.fn((url) => ({ type: 'http', url })), + isAddress: jest.fn(isAddressValue), + isHex: jest.fn( + (value) => typeof value === 'string' && /^0x([a-fA-F0-9]{2})*$/.test(value), + ), + parseEther: jest.fn((value) => parseUnitsValue(value, 18)), + parseUnits: jest.fn(parseUnitsValue), recoverMessageAddress: jest.fn(() => Promise.resolve('0x123')), -}; \ No newline at end of file + createPublicClient: jest.fn(() => mockClient), + http: jest.fn(() => 'http://mock-transport'), + fallback: jest.fn((transports) => transports[0]), + isAddress: jest.fn((addr) => /^0x[a-fA-F0-9]{40}$/.test(addr)), + getAddress: jest.fn((addr) => addr), + isHex: jest.fn(() => true), + formatEther: jest.fn((wei) => Number(wei) / 1e18), + parseEther: jest.fn((eth) => BigInt(Math.floor(Number(eth) * 1e18))), + parseUnits: jest.fn((val, decimals) => BigInt(Number(val) * Math.pow(10, decimals))), + defineChain: jest.fn((chain) => chain), +}; diff --git a/__mocks__/viem/accounts.js b/__mocks__/viem/accounts.js new file mode 100644 index 00000000..90db097d --- /dev/null +++ b/__mocks__/viem/accounts.js @@ -0,0 +1,7 @@ +module.exports = { + generatePrivateKey: jest.fn(() => '0x0000000000000000000000000000000000000000000000000000000000000001'), + privateKeyToAccount: jest.fn((key) => ({ + address: '0x0000000000000000000000000000000000000000', + privateKey: key, + })), +}; diff --git a/__mocks__/viem/chains.js b/__mocks__/viem/chains.js index 4c39de3e..8d36f560 100644 --- a/__mocks__/viem/chains.js +++ b/__mocks__/viem/chains.js @@ -1,5 +1,8 @@ module.exports = { mainnet: { id: 1, name: 'Ethereum' }, + sepolia: { id: 11155111, name: 'Sepolia' }, polygon: { id: 137, name: 'Polygon' }, + polygonMumbai: { id: 80001, name: 'Polygon Mumbai' }, bsc: { id: 56, name: 'BSC' }, + bscTestnet: { id: 97, name: 'BSC Testnet' }, }; \ No newline at end of file diff --git a/__mocks__/viem/ens.js b/__mocks__/viem/ens.js new file mode 100644 index 00000000..5d33b805 --- /dev/null +++ b/__mocks__/viem/ens.js @@ -0,0 +1,3 @@ +module.exports = { + normalize: jest.fn((name) => name), +}; diff --git a/__mocks__/wagmi/connectors.js b/__mocks__/wagmi/connectors.js new file mode 100644 index 00000000..12a9c67e --- /dev/null +++ b/__mocks__/wagmi/connectors.js @@ -0,0 +1,3 @@ +module.exports = { + injected: jest.fn(() => ({ id: 'injected' })), +}; diff --git a/__tests__/middleware.test.ts b/__tests__/middleware.test.ts new file mode 100644 index 00000000..d5ad9566 --- /dev/null +++ b/__tests__/middleware.test.ts @@ -0,0 +1,146 @@ +import { TextEncoder } from "util"; +import { + signTestToken, + verifyTestToken, +} from "../tests/helpers/middlewareJwtTestHelper"; + +// Mock the subset of Next's request/response APIs used by middleware. +jest.mock("next/server", () => { + class MockNextRequest { + nextUrl: URL; + url: string; + cookies = { + values: new Map(), + get: (name: string) => { + const value = this.cookies.values.get(name); + return value === undefined ? undefined : { value }; + }, + set: (name: string, value: string) => { + this.cookies.values.set(name, value); + }, + }; + + constructor(url: string) { + this.url = url; + this.nextUrl = new URL(url); + } + } + + return { + NextRequest: MockNextRequest, + NextResponse: { + next: jest.fn(() => ({ type: "next" })), + redirect: jest.fn((url: URL) => ({ + type: "redirect", + url, + cookies: { + delete: jest.fn(), + }, + })), + }, + }; +}); + +jest.mock("jose", () => ({ + jwtVerify: jest.fn((token: string, secret: Uint8Array) => + verifyTestToken(token, secret), + ), +})); + +describe("middleware", () => { + const secretKey = "test-secret-with-at-least-32-characters"; + const legacySecret = + "default-fallback-secret-for-dev-only-do-not-use-in-prod"; + let originalEnv: NodeJS.ProcessEnv; + let NextRequest: new (url: string) => { + nextUrl: URL; + url: string; + cookies: { + set: (name: string, value: string) => void; + get: (name: string) => { value: string } | undefined; + }; + }; + let middleware: typeof import("../middleware").middleware; + + beforeAll(async () => { + ( + globalThis as typeof globalThis & { TextEncoder: typeof TextEncoder } + ).TextEncoder = TextEncoder; + ({ NextRequest } = await import("next/server")); + ({ middleware } = await import("../middleware")); + }); + + beforeEach(() => { + originalEnv = process.env; + process.env = { ...originalEnv, AUTH_SECRET: secretKey }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + const createRequest = (pathname: string, tokenValue?: string) => { + const req = new NextRequest(`http://localhost${pathname}`); + if (tokenValue !== undefined) { + req.cookies.set("auth-token", tokenValue); + } + return req; + }; + + it("allows public routes without token", async () => { + const req = createRequest("/public"); + const res = await middleware(req); + expect((res as any).type).toBe("next"); + }); + + it("redirects protected routes without token", async () => { + const req = createRequest("/dashboard"); + const res = await middleware(req); + expect((res as any).type).toBe("redirect"); + expect((res as any).url.pathname).toBe("/"); + }); + + it("allows protected routes with valid token", async () => { + const token = signTestToken(secretKey, 3600); + const req = createRequest("/dashboard", token); + const res = await middleware(req); + expect((res as any).type).toBe("next"); + }); + + it("redirects protected routes with expired token", async () => { + const token = signTestToken(secretKey, -3600); + const req = createRequest("/dashboard", token); + const res = await middleware(req); + expect((res as any).type).toBe("redirect"); + }); + + it("redirects protected routes with tampered token", async () => { + const token = signTestToken(secretKey, 3600); + const req = createRequest("/dashboard", `${token}tampered`); + const res = await middleware(req); + expect((res as any).type).toBe("redirect"); + }); + + it("rejects a token signed with the removed fallback secret", async () => { + const token = signTestToken(legacySecret, 3600); + const req = createRequest("/dashboard", token); + const res = await middleware(req); + expect((res as any).type).toBe("redirect"); + }); + + it("fails closed when AUTH_SECRET is missing", async () => { + delete process.env.AUTH_SECRET; + const token = signTestToken(secretKey, 3600); + const req = createRequest("/dashboard", token); + const res = await middleware(req); + expect((res as any).type).toBe("redirect"); + }); + + it("fails closed when AUTH_SECRET is empty", async () => { + process.env.AUTH_SECRET = " "; + const token = signTestToken(secretKey, 3600); + const req = createRequest("/dashboard", token); + const res = await middleware(req); + expect((res as any).type).toBe("redirect"); + }); +}); diff --git a/_fixtures/PropertyNFT.abi.json b/_fixtures/PropertyNFT.abi.json new file mode 100644 index 00000000..35e19050 --- /dev/null +++ b/_fixtures/PropertyNFT.abi.json @@ -0,0 +1,431 @@ +{ + "contractName": "PropertyNFT", + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenOfOwnerByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "string", + "name": "uri", + "type": "string" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ] +} diff --git a/_fixtures/README.md b/_fixtures/README.md new file mode 100644 index 00000000..8ec2117d --- /dev/null +++ b/_fixtures/README.md @@ -0,0 +1,74 @@ +# Local Web3 Development Fixtures + +This directory contains fixture files and configuration for local Web3 development using Foundry's Anvil. + +## Files + +### `PropertyNFT.abi.json` +The ABI for the PropertyNFT contract. This is a standard ERC721 contract with a `mint` function for creating new property tokens. + +### `sample-properties.json` +Sample property data for seeding the local development environment. Includes 5 example properties with mock data. + +## Usage + +### Start Anvil +```bash +docker-compose -f docker-compose.web3.yml up +``` + +This starts an Anvil instance on `http://localhost:8545` with 10 pre-funded accounts. + +### Seed Local Environment +```bash +npm run seed:local +``` + +This script: +1. Deploys the PropertyNFT contract to Anvil +2. Seeds sample properties into the contract +3. Outputs deployment information for use in the frontend + +### Anvil Accounts +Default mnemonic: `test test test test test test test test test test test junk` + +Pre-funded accounts: +- Account 0: `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` +- Account 1: `0x70997970C51812e339D9B73b0245ad39437D9142` +- Account 2: `0x3C44CdDdB6a900c8d7C64f46199c5e3c2c52bBd8` +- ... and 7 more accounts with 100 ETH each + +### Set Environment Variables +Create a `.env.local` file: + +```env +# Local Foundry RPC +LOCAL_RPC_URL=http://localhost:8545 + +# Keep your testnet RPC URLs if using them +ETHEREUM_MAINNET_RPC_URL=https://sepolia.infura.io/v3/YOUR_KEY +POLYGON_MAINNET_RPC_URL=https://polygon-mumbai.g.alchemy.com/v2/YOUR_KEY +``` + +## Example: Connecting to Local Chain + +With `LOCAL_RPC_URL` set, your wagmi config will automatically: +1. Add the Foundry chain (chainId: 31337) to supported chains +2. Allow wallet connection to the local Anvil instance +3. Enable contract interactions with locally deployed contracts + +## Resetting Anvil State + +Stop and restart the container: +```bash +docker-compose -f docker-compose.web3.yml down +docker-compose -f docker-compose.web3.yml up +``` + +This clears all state and restarts with fresh accounts. + +## Further Reading + +- [Foundry Docs](https://book.getfoundry.sh/) +- [Anvil Docs](https://book.getfoundry.sh/anvil/) +- [wagmi Documentation](https://wagmi.sh/) diff --git a/_fixtures/sample-properties.json b/_fixtures/sample-properties.json new file mode 100644 index 00000000..ffbc7e26 --- /dev/null +++ b/_fixtures/sample-properties.json @@ -0,0 +1,79 @@ +{ + "properties": [ + { + "id": "1", + "title": "Modern Downtown Apartment", + "location": "San Francisco, CA", + "price": 850000, + "tokenId": "1", + "description": "Beautiful 2-bedroom apartment in downtown San Francisco with stunning city views and modern amenities.", + "imageUrl": "https://images.unsplash.com/photo-1540932764986-b8887cceff1f?w=400", + "bedrooms": 2, + "bathrooms": 2, + "area": 1200, + "roi": 5.2, + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "contractAddress": "0x5FbDB2315678afccb333f8a9c34a6af00d2d16451" + }, + { + "id": "2", + "title": "Luxury Waterfront Penthouse", + "location": "Miami, FL", + "price": 2500000, + "tokenId": "2", + "description": "Exclusive penthouse with oceanfront views, private pool, and premium amenities in Miami Beach.", + "imageUrl": "https://images.unsplash.com/photo-1512917774080-9264f475eabf?w=400", + "bedrooms": 4, + "bathrooms": 4, + "area": 5000, + "roi": 6.8, + "owner": "0x70997970C51812e339D9B73b0245ad39437D9142", + "contractAddress": "0x5FbDB2315678afccb333f8a9c34a6af00d2d16451" + }, + { + "id": "3", + "title": "Historic Brooklyn Townhouse", + "location": "Brooklyn, NY", + "price": 1200000, + "tokenId": "3", + "description": "Beautifully restored historic brownstone in Brooklyn Heights with original hardwood floors and period details.", + "imageUrl": "https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=400", + "bedrooms": 3, + "bathrooms": 2, + "area": 2000, + "roi": 4.5, + "owner": "0x3C44CdDdB6a900c8d7C64f46199c5e3c2c52bBd8", + "contractAddress": "0x5FbDB2315678afccb333f8a9c34a6af00d2d16451" + }, + { + "id": "4", + "title": "Tech Hub Office Space", + "location": "Austin, TX", + "price": 750000, + "tokenId": "4", + "description": "Prime commercial real estate in the heart of Austin's tech district. Perfect for startups and tech companies.", + "imageUrl": "https://images.unsplash.com/photo-1552321554-5fefe8c9ef14?w=400", + "bedrooms": 0, + "bathrooms": 2, + "area": 3500, + "roi": 7.2, + "owner": "0x1234567890123456789012345678901234567890", + "contractAddress": "0x5FbDB2315678afccb333f8a9c34a6af00d2d16451" + }, + { + "id": "5", + "title": "Mountain View Villa", + "location": "Denver, CO", + "price": 950000, + "tokenId": "5", + "description": "Stunning villa with panoramic mountain views, outdoor living spaces, and eco-friendly features.", + "imageUrl": "https://images.unsplash.com/photo-1571508601101-440d5a02f147?w=400", + "bedrooms": 4, + "bathrooms": 3, + "area": 3200, + "roi": 5.9, + "owner": "0x0123456789012345678901234567890123456789", + "contractAddress": "0x5FbDB2315678afccb333f8a9c34a6af00d2d16451" + } + ] +} diff --git a/docker-compose.web3.yml b/docker-compose.web3.yml new file mode 100644 index 00000000..196b28da --- /dev/null +++ b/docker-compose.web3.yml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + anvil: + image: ghcr.io/foundry-rs/foundry:latest + container_name: propchain-anvil-local + ports: + - '8545:8545' + command: > + anvil + --host 0.0.0.0 + --port 8545 + --accounts 10 + --balance 100 + --mnemonic "test test test test test test test test test test test junk" + --block-time 1 + --gas-limit 30000000 + --state-interval 1 + environment: + - RUST_LOG=info + volumes: + - ./anvil_data:/data + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:8545'] + interval: 5s + timeout: 3s + retries: 5 + start_period: 10s + networks: + - propchain + +volumes: + anvil_data: + driver: local + +networks: + propchain: + driver: bridge diff --git a/docs/CACHE_TTL_RATIONALE.md b/docs/CACHE_TTL_RATIONALE.md new file mode 100644 index 00000000..16276631 --- /dev/null +++ b/docs/CACHE_TTL_RATIONALE.md @@ -0,0 +1,77 @@ +# Cache Manager TTL Rationale + +This document explains the rationale behind the default TTL (Time-To-Live) values used in `src/lib/cacheManager.ts`. + +## Overview + +The cache manager uses TTL values to determine how long cached data should remain valid. Different types of data have different freshness requirements, which is why we use different TTL values for different cache categories. + +## Default TTL Values + +| Cache Category | Default TTL | Rationale | +|----------------|-------------|-----------| +| Property Data | 5 minutes | Property data changes infrequently but needs to stay reasonably fresh for pricing updates | +| Transaction History | 1 minute | Transaction data should be relatively fresh to show recent activity | +| User Profile | 10 minutes | User profile data rarely changes and can be cached longer | +| Token Balances | 30 seconds | Token balances change frequently with transactions and need near-real-time accuracy | +| Gas Estimates | 15 seconds | Gas prices fluctuate rapidly and should be updated frequently | +| Network Status | 30 seconds | Network status can change quickly and affects transaction decisions | +| Static Config | 1 hour | Configuration data rarely changes and can be cached for extended periods | +| Exchange Rates | 2 minutes | Exchange rates fluctuate but not as rapidly as gas prices | + +## TTL Selection Criteria + +When choosing a TTL value, consider: + +1. **Data Volatility**: How often does this data change? + - High volatility (gas prices, balances) โ†’ Short TTL (15-30 seconds) + - Medium volatility (transactions, exchange rates) โ†’ Medium TTL (1-2 minutes) + - Low volatility (profiles, config) โ†’ Long TTL (5-60 minutes) + +2. **Freshness Requirements**: How critical is it that users see the latest data? + - Critical (token balances) โ†’ Short TTL + - Important (transactions) โ†’ Medium TTL + - Nice-to-have (config) โ†’ Long TTL + +3. **Performance Impact**: What's the cost of stale data? + - High cost (wrong balance shown) โ†’ Short TTL + - Medium cost (slightly outdated transactions) โ†’ Medium TTL + - Low cost (old config) โ†’ Long TTL + +4. **API Rate Limits**: Are there external API rate limits to consider? + - Aggressive caching reduces API calls + - Balance freshness needs vs. rate limits + +## Cache Invalidation + +In addition to TTL-based expiration, the cache manager supports: + +- **Manual invalidation**: Force refresh specific cache entries +- **Version-based invalidation**: Invalidate all caches when data schema changes +- **Event-based invalidation**: Invalidate related caches when actions occur + +## Best Practices + +1. **Use the shortest TTL that's acceptable** for the use case +2. **Consider cascading invalidation** when related data changes +3. **Monitor cache hit rates** to tune TTL values +4. **Document any custom TTL values** you introduce +5. **Test with TTL=0** to ensure your code works without caching + +## Configuration + +TTL values can be overridden per-cache entry: + +```typescript +cacheManager.set('key', data, { ttl: 60 }); // Custom TTL in seconds +``` + +Or via the cache configuration: + +```typescript +const cacheConfig: CacheConfig = { + ttl: 300, // 5 minutes default + maxEntries: 1000, + version: '1.0.0', +}; +``` diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md deleted file mode 100644 index 0fd8b856..00000000 --- a/docs/ERROR_HANDLING.md +++ /dev/null @@ -1,470 +0,0 @@ -# Error Handling Implementation Guide - -## Overview - -PropChain now implements a comprehensive error handling strategy with contextual error boundaries, recovery mechanisms, and analytics integration. This system provides users with clear error messages and actionable recovery options while enabling developers to diagnose and fix issues efficiently. - -## Architecture - -### Core Components - -1. **Error Types & Categories** (`src/types/errors.ts`) - - Structured error classification system - - Severity levels and recovery actions - - Comprehensive error metadata - -2. **Error Factory** (`src/utils/errorFactory.ts`) - - Centralized error creation - - User-friendly message generation - - Context-aware error categorization - -3. **Error Reporting Service** (`src/utils/errorReporting.ts`) - - Analytics integration - - Recovery mechanism orchestration - - Error metrics and tracking - -4. **Contextual Error Boundaries** (`src/components/error/`) - - Domain-specific error handling - - Tailored recovery strategies - - Graceful degradation support - -## Error Categories - -### Web3 Errors -- **Category**: `web3` -- **Severity**: High -- **Common Causes**: Wallet connection failures, transaction errors, network issues -- **Recovery Actions**: Reconnect wallet, switch network, reload page -- **Boundary**: `Web3ErrorBoundary` - -### Network Errors -- **Category**: `network` -- **Severity**: Medium -- **Common Causes**: Connection timeouts, API failures, offline status -- **Recovery Actions**: Retry with exponential backoff, refresh data, reload page -- **Boundary**: `NetworkErrorBoundary` - -### AR/VR Errors -- **Category**: `ar` -- **Severity**: Medium -- **Common Causes**: Camera permission denied, device incompatibility, WebXR errors -- **Recovery Actions**: Grant permissions, check device compatibility, ignore -- **Boundary**: `ARErrorBoundary` - -### UI Errors -- **Category**: `ui` -- **Severity**: Low -- **Common Causes**: Component failures, rendering errors, state issues -- **Recovery Actions**: Refresh component, retry operation, go home -- **Boundary**: `UIErrorBoundary` - -### Validation Errors -- **Category**: `validation` -- **Severity**: Low -- **Common Causes**: Invalid input, form validation failures -- **Recovery Actions**: Retry with corrected input, show validation hints -- **Boundary**: `UIErrorBoundary` - -### Permission Errors -- **Category**: `permission` -- **Severity**: Medium -- **Common Causes**: Camera/location denied, notification access -- **Recovery Actions**: Request permission, show instructions, ignore -- **Boundary**: `UIErrorBoundary` - -### Resource Errors -- **Category**: `resource` -- **Severity**: Medium -- **Common Causes**: Missing assets, API endpoints unavailable -- **Recovery Actions**: Refresh resource, retry request, use fallback -- **Boundary**: `UIErrorBoundary` - -## Error Severity Levels - -### Critical -- **Impact**: Application completely unusable -- **Action Required**: Immediate attention and reload -- **Examples**: Authentication failures, system crashes - -### High -- **Impact**: Major features unavailable -- **Action Required**: User intervention needed -- **Examples**: Wallet disconnection, network failures - -### Medium -- **Impact**: Some features degraded -- **Action Required**: Recovery options available -- **Examples**: AR errors, permission issues - -### Low -- **Impact**: Minor issues, workarounds available -- **Action Required**: Optional retry -- **Examples**: Validation errors, UI glitches - -## Recovery Strategies - -### Automatic Recovery -```typescript -// The system automatically attempts recovery based on error type -const recovered = await errorReporting.attemptRecovery(error); -``` - -### Recovery Actions - -#### Retry -- **Use Case**: Temporary failures, network timeouts -- **Implementation**: Exponential backoff with maximum attempts -- **User Experience**: Shows retry progress and countdown - -#### Refresh -- **Use Case**: Data stale, component state issues -- **Implementation**: Re-fetch data, re-render component -- **User Experience**: Maintains current page state - -#### Reconnect -- **Use Case**: Wallet disconnection, session expiry -- **Implementation**: Clear session, restart connection flow -- **User Experience**: Seamless reconnection - -#### Reload -- **Use Case**: Critical errors, unrecoverable states -- **Implementation**: Full page refresh -- **User Experience**: Clean slate recovery - -#### Grant Permission -- **Use Case**: Camera/location access denied -- **Implementation**: Trigger browser permission dialog -- **User Experience**: Clear permission request - -#### Switch Network -- **Use Case**: Unsupported network, wrong chain -- **Implementation**: Prompt for network switch -- **User Experience**: Guided network selection - -## Usage Guide - -### Basic Error Boundary Usage - -```tsx -import { ErrorBoundaryPresets } from '@/components/error/EnhancedErrorBoundary'; - -function MyComponent() { - return ( - - - - ); -} -``` - -### Advanced Error Boundary Usage - -```tsx -import { EnhancedErrorBoundary } from '@/components/error/EnhancedErrorBoundary'; -import { ErrorCategory } from '@/types/errors'; - -function MyComponent() { - const handleError = (error: AppError) => { - console.log('Error caught:', error); - // Send to custom analytics - }; - - return ( - , - hideOnError: false, - }} - > - - - ); -} -``` - -### Error Creation - -```typescript -import { ErrorFactory } from '@/utils/errorFactory'; - -// Create specific error types -const web3Error = ErrorFactory.createWeb3Error( - 'Wallet connection failed', - 'Unable to connect to wallet. Please check your wallet extension.', - { - context: { walletType: 'MetaMask' }, - recoveryAction: ErrorRecoveryAction.RECONNECT, - } -); - -const networkError = ErrorFactory.createNetworkError( - 'API timeout', - 'Network request timed out. Please check your connection.', - { - context: { endpoint: '/api/properties', timeout: 30000 }, - recoveryAction: ErrorRecoveryAction.RETRY, - } -); -``` - -### Error Reporting - -```typescript -import { errorReporting } from '@/utils/errorReporting'; - -// Manual error reporting -const error = ErrorFactory.createUIError(...); -errorReporting.reportError(error); - -// Get error metrics -const metrics = errorReporting.getMetrics(); -console.log('Total errors:', metrics.totalErrors); -console.log('Recovery success rate:', metrics.recoverySuccessRate); -``` - -## Graceful Degradation - -### Implementation - -```tsx - -

Feature Limited

-

This feature is partially unavailable. You can continue using other features.

- - ), - hideOnError: false, - }} -> - -
-``` - -### Use Cases - -- **AR Features**: Fallback to 2D property viewer -- **Real-time Data**: Show cached data with refresh option -- **Advanced Charts**: Display simplified version -- **File Upload**: Show basic upload form - -## Error Analytics - -### Metrics Tracked - -1. **Error Volume**: Total errors by category and severity -2. **Recovery Success Rate**: Percentage of successful recoveries -3. **Top Errors**: Most frequent error occurrences -4. **Error Patterns**: Temporal and contextual patterns - -### Dashboard Integration - -```typescript -// Error metrics can be displayed in admin dashboards -const ErrorMetrics = () => { - const metrics = errorReporting.getMetrics(); - - return ( -
-

Error Analytics

-

Total Errors: {metrics.totalErrors}

-

Recovery Rate: {(metrics.recoverySuccessRate * 100).toFixed(1)}%

- -

Errors by Category

- {Object.entries(metrics.errorsByCategory).map(([category, count]) => ( -
- {category}: {count} -
- ))} -
- ); -}; -``` - -## Testing - -### Error Test Suite - -Visit `/error-test` to access the comprehensive error testing interface: - -1. **Individual Error Tests**: Test specific error types -2. **Boundary Demonstrations**: See each error boundary in action -3. **Recovery Testing**: Verify recovery mechanisms work -4. **Graceful Degradation**: Test fallback components - -### Manual Testing - -```typescript -// Test error boundaries programmatically -const triggerError = (type: string) => { - switch (type) { - case 'web3': - throw ErrorFactory.createWeb3Error(...); - case 'network': - throw ErrorFactory.createNetworkError(...); - case 'ar': - throw ErrorFactory.createARError(...); - // ... other error types - } -}; -``` - -### Automated Testing - -```typescript -// Jest tests for error boundaries -describe('Error Boundaries', () => { - it('should catch Web3 errors', () => { - const error = ErrorFactory.createWeb3Error('Test', 'Test message'); - expect(error.category).toBe(ErrorCategory.WEB3); - expect(error.severity).toBe(ErrorSeverity.HIGH); - }); - - it('should provide recovery options', () => { - const error = ErrorFactory.createNetworkError('Test', 'Test message'); - expect(error.recoveryAction).toBe(ErrorRecoveryAction.RETRY); - expect(error.isRecoverable).toBe(true); - }); -}); -``` - -## Best Practices - -### For Developers - -1. **Use Specific Boundaries**: Choose the right boundary for each domain -2. **Provide Context**: Include relevant information in error context -3. **Enable Recovery**: Allow users to recover from errors when possible -4. **Test Errors**: Verify error handling works as expected -5. **Monitor Metrics**: Track error rates and recovery success - -### Error Message Guidelines - -1. **Be User-Friendly**: Avoid technical jargon -2. **Be Specific**: Explain what went wrong -3. **Be Actionable**: Tell users what to do next -4. **Be Consistent**: Use similar language across error types -5. **Be Localized**: Support multiple languages - -### Recovery Strategy Guidelines - -1. **Prioritize User Experience**: Minimize disruption -2. **Provide Options**: Offer multiple recovery paths -3. **Show Progress**: Indicate recovery attempts -4. **Limit Attempts**: Prevent infinite retry loops -5. **Fallback Gracefully**: Degrade features when needed - -## Configuration - -### Environment Variables - -```bash -# Enable debug mode for detailed error logging -NEXT_PUBLIC_ERROR_DEBUG=true - -# Configure error reporting endpoint -NEXT_PUBLIC_ERROR_ENDPOINT=https://api.propchain.com/errors - -# Set maximum retry attempts (default: 3) -NEXT_PUBLIC_MAX_RETRIES=3 - -# Enable graceful degradation (default: true) -NEXT_PUBLIC_GRACEFUL_DEGRADATION=true -``` - -### Error Reporting Configuration - -```typescript -// Custom error reporting integration -errorReporting.configure({ - endpoint: '/api/errors', - apiKey: process.env.ERROR_API_KEY, - batchSize: 10, - flushInterval: 30000, - includeStackTrace: process.env.NODE_ENV === 'development', -}); -``` - -## Troubleshooting - -### Common Issues - -1. **Errors Not Caught**: Ensure components are wrapped in appropriate boundaries -2. **Recovery Fails**: Check error context and recovery action configuration -3. **Metrics Not Reporting**: Verify analytics endpoint and network connectivity -4. **Fallback Not Showing**: Check graceful degradation configuration - -### Debug Tools - -1. **Error Test Suite**: Use `/error-test` for comprehensive testing -2. **Browser Console**: Check for detailed error logs in development -3. **Network Tab**: Verify error reporting requests are sent -4. **React DevTools**: Inspect component state during errors - -## Performance Considerations - -### Bundle Impact - -- **Error Boundaries**: ~15KB gzipped -- **Error Factory**: ~8KB gzipped -- **Error Reporting**: ~12KB gzipped -- **Total Overhead**: ~35KB gzipped - -### Runtime Performance - -- **Error Creation**: Minimal overhead, cached error instances -- **Boundary Rendering**: Optimized with memoization -- **Recovery Logic**: Asynchronous, non-blocking -- **Analytics Reporting**: Batched and throttled - -## Security Considerations - -### Data Privacy - -1. **Sanitize Errors**: Remove sensitive information from reports -2. **User Consent**: Inform users about error reporting -3. **Data Minimization**: Only collect necessary error data -4. **Secure Transmission**: Use HTTPS for error reporting - -### Error Information - -1. **No PII**: Never include personal information -2. **Limited Context**: Only relevant technical details -3. **Sanitized Stack Traces**: Remove internal paths and secrets -4. **Rate Limiting**: Prevent error spamming - -## Future Enhancements - -### Planned Features - -1. **Machine Learning**: Error pattern recognition and prediction -2. **Automated Fixes**: Self-healing capabilities for common issues -3. **Enhanced Analytics**: Advanced error correlation and analysis -4. **User Feedback**: In-app error reporting and feedback -5. **Integration Testing**: Automated error boundary testing - -### Scalability - -The current architecture supports: -- Easy addition of new error categories -- Flexible recovery strategy configuration -- Pluggable analytics integration -- Custom error boundary creation - -## Conclusion - -This comprehensive error handling system provides PropChain with: - -- โœ… Contextual error boundaries for all application domains -- โœ… Intelligent recovery mechanisms with user guidance -- โœ… Comprehensive error analytics and reporting -- โœ… Graceful degradation for non-critical features -- โœ… Developer-friendly testing and debugging tools -- โœ… Production-ready error monitoring - -The system significantly improves user experience during error conditions while providing developers with the tools needed to diagnose and fix issues efficiently. diff --git a/docs/I18N_IMPLEMENTATION.md b/docs/I18N_IMPLEMENTATION.md index 1d9308a0..851f0345 100644 --- a/docs/I18N_IMPLEMENTATION.md +++ b/docs/I18N_IMPLEMENTATION.md @@ -78,6 +78,16 @@ Translations are organized in logical groups: "polygon": "Polygon", "bsc": "BSC" }, + "mortgageCalculator": { + "title": "Investment Calculator", + "holdingPeriodValue_one": "{{count}} Year", + "holdingPeriodValue_other": "{{count}} Years", + "breakEvenMonths_one": "{{count}} mo", + "breakEvenMonths_other": "{{count}} mo" + }, + "qrCode": { + "invalidUrl": "Unable to generate QR code for this URL" + }, "mobile": { "title": "Mobile Properties", "mobileFirstPropertyExperience": "Mobile-First Property Experience" diff --git a/docs/QRCODE_SECURITY.md b/docs/QRCODE_SECURITY.md new file mode 100644 index 00000000..739ce067 --- /dev/null +++ b/docs/QRCODE_SECURITY.md @@ -0,0 +1,45 @@ +# QR Code Security + +## Overview + +The `QRCode` component renders shareable property links for print views. Because QR codes encode arbitrary strings that wallets and browsers may open, URLs are validated before rendering. + +## Validation Rules + +Implemented in `src/utils/security/qrCodeSecurity.ts`: + +- Only `http:` and `https:` protocols are allowed +- `javascript:`, `data:`, `blob:`, and `vbscript:` schemes are blocked +- URLs longer than 2048 characters are rejected +- Known phishing domains are blocked via `PhishingProtection.detectPhishing` +- Unofficial domains produce a non-blocking warning in the UI + +## Usage + +```tsx +import { QRCode } from '@/components/QRCode'; + + +``` + +Invalid URLs render an accessible error state instead of encoding unsafe content. + +## Testing + +- Unit tests: `src/components/__tests__/QRCode.test.tsx` +- Security utility tests: `src/utils/security/__tests__/qrCodeSecurity.test.ts` + +Run: + +```bash +npm run test -- QRCode qrCodeSecurity +``` + +## Related Components + +- `src/components/PropertyDetail.tsx` โ€” embeds `QRCode` in print-only view +- `src/utils/security/phishingProtection.ts` โ€” phishing domain detection diff --git a/docs/RFC_ZK_IDENTITY_VERIFICATION.md b/docs/RFC_ZK_IDENTITY_VERIFICATION.md new file mode 100644 index 00000000..20eadd53 --- /dev/null +++ b/docs/RFC_ZK_IDENTITY_VERIFICATION.md @@ -0,0 +1,178 @@ +# RFC: ZK-Proof Based Identity Verification for PropChain KYC + +## Status + +Draft + +## Authors + +Magrexy + +## Summary + +This RFC proposes replacing the current centralized KYC (Know Your Customer) identity verification system with a Zero-Knowledge Proof (ZKP) based approach using either Semaphore or Polygon ID. This would reduce data exposure while maintaining regulatory compliance. + +## Problem Statement + +The current KYC system in `src/lib/kyc.ts` and `src/types/kyc.ts` relies on centralized identity providers, which: + +1. **Exposes user data**: Users must share sensitive personal information with third parties +2. **Creates single points of failure**: Centralized databases are attractive targets for attackers +3. **Limits user control**: Users have little control over how their data is stored and used +4. **Regulatory concerns**: Data storage and processing must comply with GDPR, CCPA, etc. + +## Proposed Solution + +Implement ZK-proof based identity verification to prove identity claims without revealing the underlying data. + +### Option 1: Semaphore + +**Overview:** +Semaphore is a zero-knowledge protocol that allows users to prove their membership in a group without revealing their identity. + +**Key Features:** +- Group-based identity proofs +- Anonymous signaling and voting +- Simple integration with existing smart contracts +- Mature ecosystem with Ethereum foundation support + +**Pros:** +- Well-documented and battle-tested +- Strong community support +- Simple API for developers +- Lower gas costs for proofs + +**Cons:** +- Limited to group membership proofs +- No support for complex identity attributes +- Requires trusted setup for some configurations + +**Integration Points:** +- `src/lib/kyc.ts`: Replace centralized KYC with Semaphore group membership +- `src/types/kyc.ts`: Add Semaphore-specific types +- `src/components/kyc/`: Update UI for Semaphore proof generation + +### Option 2: Polygon ID + +**Overview:** +Polygon ID is a decentralized identity system that supports verifiable credentials and zero-knowledge proofs. + +**Key Features:** +- Verifiable credentials support +- Complex identity attribute proofs +- On-chain and off-chain verification +- Integration with Polygon ecosystem + +**Pros:** +- Support for complex identity attributes (age, nationality, etc.) +- Verifiable credentials standard compliance +- On-chain verification capability +- Growing ecosystem + +**Cons:** +- More complex integration +- Higher gas costs for complex proofs +- Newer technology with smaller community +- Requires credential issuer infrastructure + +**Integration Points:** +- `src/lib/kyc.ts`: Replace with Polygon ID credential verification +- `src/types/kyc.ts`: Add Polygon ID credential types +- `src/components/kyc/`: Update UI for credential issuance and presentation + +## Comparison Matrix + +| Criteria | Semaphore | Polygon ID | +|----------|-----------|------------| +| Proof Complexity | Simple (group membership) | Complex (attribute-based) | +| Gas Costs | Low | Medium-High | +| Credential Support | No | Yes | +| On-chain Verification | Limited | Full | +| Community Support | Strong | Growing | +| Integration Complexity | Low | Medium | +| Privacy Guarantees | High | Very High | +| Regulatory Compliance | Partial | Full | + +## Recommendation + +**For PropChain, we recommend Polygon ID** for the following reasons: + +1. **Regulatory Compliance**: Support for verifiable credentials aligns with KYC requirements +2. **Attribute Proofs**: Ability to prove age, nationality, and other KYC-relevant attributes +3. **Future-Proof**: Growing ecosystem and standard compliance +4. **On-chain Verification**: Enables smart contract integration for automated compliance + +## Implementation Plan + +### Phase 1: Research & Prototype (2-4 weeks) +- [ ] Set up Polygon ID development environment +- [ ] Create proof-of-concept integration +- [ ] Test with sample identity attributes +- [ ] Document API integration patterns + +### Phase 2: Core Integration (4-6 weeks) +- [ ] Update `src/types/kyc.ts` with Polygon ID types +- [ ] Modify `src/lib/kyc.ts` for ZKP-based verification +- [ ] Create credential issuance flow +- [ ] Implement proof generation and verification + +### Phase 3: UI/UX Updates (2-3 weeks) +- [ ] Update `src/components/kyc/` for new flows +- [ ] Add wallet integration for identity management +- [ ] Create user-facing proof generation UI + +### Phase 4: Testing & Deployment (2-3 weeks) +- [ ] Security audit of ZKP implementation +- [ ] Load testing for proof generation +- [ ] Gradual rollout with feature flags + +## Technical Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ZKP KYC Architecture โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ User Device โ”‚ PropChain Backend โ”‚ Blockchain โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Identity Wallet โ”‚ Verification API โ”‚ Smart Contract โ”‚ +โ”‚ Proof Generation โ”‚ Credential Registry โ”‚ On-chain Verify โ”‚ +โ”‚ Credential Storage โ”‚ Compliance Checks โ”‚ State Updates โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Security Considerations + +1. **Credential Security**: Ensure credentials are stored securely on user devices +2. **Proof Verification**: Validate proofs on-chain to prevent replay attacks +3. **Privacy Protection**: Minimize data exposure during proof generation +4. **Key Management**: Implement secure key generation and storage + +## Risks and Mitigations + +| Risk | Impact | Mitigation | +|------|--------|------------| +| User adoption resistance | High | Provide clear UX benefits documentation | +| Regulatory uncertainty | Medium | Engage legal counsel early | +| Technical complexity | Medium | Start with simple proofs, iterate | +| Gas cost increases | Low | Optimize proof generation | + +## Open Questions + +1. Should we support both Semaphore and Polygon ID for flexibility? +2. How do we handle credential revocation? +3. What's the migration path for existing KYC users? +4. How do we ensure backward compatibility with existing integrations? + +## References + +- [Semaphore Documentation](https://semaphore.pse.dev/) +- [Polygon ID Documentation](https://polygonid.com/) +- [EIP-712 Typed Data](https://eips.ethereum.org/EIPS/eip-712) +- [W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) + +## Next Steps + +1. Review this RFC with the team +2. Set up a proof-of-concept environment +3. Conduct user research on privacy preferences +4. Engage legal counsel on regulatory compliance diff --git a/docs/TRANSACTION_LIFECYCLE.md b/docs/TRANSACTION_LIFECYCLE.md new file mode 100644 index 00000000..d40cd9b0 --- /dev/null +++ b/docs/TRANSACTION_LIFECYCLE.md @@ -0,0 +1,253 @@ +# Transaction Lifecycle Architecture + +This document provides a diagrammatic summary of the transaction lifecycle from sign through confirmation. + +## Overview + +The PropChain transaction lifecycle involves multiple components working together to securely sign, submit, and confirm blockchain transactions. + +## Transaction Flow + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ TRANSACTION LIFECYCLE โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +User Action Backend Processing Blockchain +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 1. User โ”‚ +โ”‚ Initiates โ”‚ +โ”‚ Transaction โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 2. Validate โ”‚ +โ”‚ Input & โ”‚ +โ”‚ Permissions โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 3. EIP-712 โ”‚ +โ”‚ Sign Typed โ”‚ +โ”‚ Data โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 4. Sign โ”‚ +โ”‚ Transaction โ”‚ +โ”‚ with Wallet โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 5. Submit โ”‚ +โ”‚ to Network โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 6. Monitor โ”‚ +โ”‚ Transaction โ”‚ +โ”‚ Status โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 7. Confirm โ”‚ +โ”‚ & Update โ”‚ +โ”‚ State โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Detailed Component Flow + +### 1. User Initiates Transaction + +**Components:** +- `src/components/TransactionButton.tsx` +- `src/hooks/useTransaction.ts` + +**Flow:** +``` +User Click โ†’ Validate Inputs โ†’ Check Wallet Connection โ†’ Proceed to Signing +``` + +### 2. Validate Input & Permissions + +**Components:** +- `src/utils/validation.ts` +- `src/utils/security/permissions.ts` + +**Checks:** +- Sufficient token balance +- Correct network selected +- Required permissions granted +- Input validation (amounts, addresses) + +### 3. EIP-712 Sign Typed Data + +**Components:** +- `src/utils/eip712/eip712Signing.ts` +- `src/utils/eip712/types.ts` + +**Flow:** +``` +Construct EIP-712 Domain โ†’ Create Typed Data โ†’ Prepare for Signing +``` + +**Key Functions:** +```typescript +// src/utils/eip712/eip712Signing.ts +export async function signTypedData(params: SignParams): Promise +export function constructDomain(chainId: number): EIP712Domain +export function createTypedData(message: TransactionMessage): TypedData +``` + +### 4. Sign Transaction with Wallet + +**Components:** +- `src/hooks/useSecureTransaction.ts` +- `src/utils/walletConnectors/` + +**Flow:** +``` +Request Signature โ†’ Wallet Popup โ†’ User Approves โ†’ Signature Returned +``` + +**Security Features:** +- Transaction simulation before signing +- Gas estimation +- Nonce management +- Replay protection + +### 5. Submit to Network + +**Components:** +- `src/lib/transactionService.ts` +- `src/lib/blockchainSecurity.ts` + +**Flow:** +``` +Serialize Transaction โ†’ Send to RPC Node โ†’ Get Transaction Hash โ†’ Return to Client +``` + +**Error Handling:** +- Network congestion detection +- Gas price optimization +- Retry logic for transient failures + +### 6. Monitor Transaction Status + +**Components:** +- `src/lib/transactionMonitor.ts` +- `src/store/transactionStore.ts` + +**Flow:** +``` +Poll Transaction Receipt โ†’ Check Block Confirmations โ†’ Update Status +``` + +**States:** +- `pending` - Transaction submitted, not yet confirmed +- `confirming` - Transaction in mempool, waiting for blocks +- `confirmed` - Transaction included in block +- `failed` - Transaction reverted or dropped + +### 7. Confirm & Update State + +**Components:** +- `src/store/transactionStore.ts` +- `src/hooks/useTransactionHistory.ts` + +**Actions:** +- Update transaction status in store +- Refresh token balances +- Show success/error notification +- Update UI state + +## Security Layers + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ SECURITY LAYERS โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Layer 1: Input Validation โ”‚ +โ”‚ - Amount validation โ”‚ +โ”‚ - Address format validation โ”‚ +โ”‚ - Permission checks โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Layer 2: EIP-712 Typed Data โ”‚ +โ”‚ - Structured data signing โ”‚ +โ”‚ - Domain separation โ”‚ +โ”‚ - Replay protection โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Layer 3: Wallet Security โ”‚ +โ”‚ - Secure enclave signing โ”‚ +โ”‚ - User confirmation โ”‚ +โ”‚ - Transaction simulation โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Layer 4: Network Security โ”‚ +โ”‚ - RPC endpoint validation โ”‚ +โ”‚ - Chain ID verification โ”‚ +โ”‚ - Nonce management โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Error Recovery + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ERROR RECOVERY โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Error Type โ”‚ Recovery Action โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Insufficient Gas โ”‚ Re-estimate and retry โ”‚ +โ”‚ Nonce Too Low โ”‚ Refresh nonce and retry โ”‚ +โ”‚ Network Congestion โ”‚ Increase gas price โ”‚ +โ”‚ Transaction Reverted โ”‚ Show error, allow retry โ”‚ +โ”‚ Wallet Disconnected โ”‚ Prompt reconnection โ”‚ +โ”‚ Chain Mismatch โ”‚ Prompt network switch โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## State Management + +```typescript +// Transaction State Interface +interface TransactionState { + id: string; + hash: string; + from: string; + to: string; + value: string; + chainId: number; + status: 'pending' | 'confirming' | 'confirmed' | 'failed'; + confirmations: number; + blockNumber?: number; + timestamp: number; + error?: string; +} +``` + +## Testing Strategy + +1. **Unit Tests**: Individual component testing +2. **Integration Tests**: End-to-end transaction flow +3. **E2E Tests**: Full user journey with wallet simulation +4. **Security Tests**: Signature verification, replay attacks + +## Key Files + +| File | Purpose | +|------|---------| +| `src/utils/eip712/eip712Signing.ts` | EIP-712 typed data signing | +| `src/hooks/useSecureTransaction.ts` | Secure transaction hook | +| `src/lib/transactionService.ts` | Transaction submission | +| `src/lib/blockchainSecurity.ts` | Security validations | +| `src/lib/transactionMonitor.ts` | Status monitoring | +| `src/store/transactionStore.ts` | State management | diff --git a/docs/WALLET_ADDRESS_INPUT.md b/docs/WALLET_ADDRESS_INPUT.md new file mode 100644 index 00000000..eb05a793 --- /dev/null +++ b/docs/WALLET_ADDRESS_INPUT.md @@ -0,0 +1,317 @@ +# WalletAddressInput Component + +## Overview + +The `WalletAddressInput` component is a type-safe React component for inputting and validating wallet addresses with comprehensive TypeScript typing and real-time validation feedback. It provides a robust solution for handling Ethereum address input with built-in validation, auto-formatting, and accessibility features. + +## Features + +- **Strong TypeScript Typing**: No `any` or `unknown` types - all interfaces and enums are properly typed +- **Real-time Validation**: Validates Ethereum address format as user types +- **Checksum Validation**: Supports EIP-55 checksum validation for mixed-case addresses +- **Auto-formatting**: Automatically adds `0x` prefix when enabled +- **Custom Validation**: Supports custom validation functions +- **Accessibility**: Full ARIA support and keyboard navigation +- **Error Handling**: Comprehensive error messaging and visual feedback +- **Responsive Design**: Mobile-friendly with Tailwind CSS styling + +## Installation + +The component is located in `src/components/security/WalletAddressInput.tsx`. + +## Usage + +### Basic Usage + +```tsx +import { WalletAddressInput } from '@/components/security'; + +function MyComponent() { + const [address, setAddress] = useState(''); + + return ( + + ); +} +``` + +### With Validation Callback + +```tsx +import { WalletAddressInput, AddressValidationStatus } from '@/components/security'; + +function MyComponent() { + const [address, setAddress] = useState(''); + const [validationStatus, setValidationStatus] = useState( + AddressValidationStatus.EMPTY + ); + + const handleValidationChange = (status: AddressValidationStatus, addr: string) => { + setValidationStatus(status); + console.log(`Validation status: ${status}, Address: ${addr}`); + }; + + return ( + + ); +} +``` + +### With Custom Validation + +```tsx +import { WalletAddressInput } from '@/components/security'; + +function MyComponent() { + const [address, setAddress] = useState(''); + + const customValidator = (addr: string): boolean => { + // Only allow addresses starting with 0x123 + return addr.startsWith('0x123'); + }; + + return ( + + ); +} +``` + +### With Custom Styling + +```tsx +import { WalletAddressInput } from '@/components/security'; + +function MyComponent() { + const [address, setAddress] = useState(''); + + return ( + + ); +} +``` + +## Props + +### WalletAddressInputProps + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `value` | `string` | **Required** | Current value of the wallet address | +| `onChange` | `(address: string) => void` | **Required** | Callback when address changes | +| `onValidationChange` | `(status: AddressValidationStatus, address: string) => void` | `undefined` | Callback when address is validated | +| `placeholder` | `string` | `'0x...'` | Placeholder text for the input | +| `disabled` | `boolean` | `false` | Whether the input is disabled | +| `showValidation` | `boolean` | `true` | Whether to show validation status | +| `error` | `string` | `undefined` | Custom error message | +| `maxLength` | `number` | `42` | Maximum length for input | +| `autoFormat` | `boolean` | `true` | Whether to auto-format the address | +| `className` | `string` | `''` | CSS class name for custom styling | +| `id` | `string` | `'wallet-address-input'` | ID attribute for the input element | +| `name` | `string` | `'walletAddress'` | Name attribute for the input element | +| `required` | `boolean` | `false` | Whether the input is required | +| `customValidator` | `(address: string) => boolean` | `undefined` | Custom validation function | + +## Types + +### AddressValidationStatus + +Enum representing the validation status of a wallet address: + +```typescript +enum AddressValidationStatus { + EMPTY = 'empty', // No input + INVALID = 'invalid', // Invalid format + VALID = 'valid', // Valid lowercase address + CHECKSUM = 'checksum' // Valid address with checksum +} +``` + +## Validation Rules + +### Ethereum Address Validation + +The component validates Ethereum addresses according to the following rules: + +1. **Format Check**: Must match pattern `^0x[a-fA-F0-9]{40}$` +2. **Prefix Check**: Must start with `0x` +3. **Length Check**: Must be exactly 42 characters (including prefix) +4. **Character Check**: Must only contain hexadecimal characters +5. **Checksum Check**: Detects mixed-case addresses for EIP-55 compliance + +### Auto-formatting + +When `autoFormat` is enabled (default), the component: + +- Automatically adds `0x` prefix if missing +- Trims whitespace from input +- Maintains proper case for checksum validation + +## Accessibility + +The component follows WCAG 2.1 guidelines with: + +- **ARIA Attributes**: Proper `aria-invalid` and `aria-describedby` attributes +- **Keyboard Navigation**: Full keyboard support +- **Screen Reader Support**: Descriptive error messages and status indicators +- **Focus States**: Clear visual feedback for focus states +- **Color Contrast**: Meets AA standards for color contrast + +## Examples + +### Form Integration + +```tsx +import { WalletAddressInput } from '@/components/security'; +import { useForm } from 'react-hook-form'; + +function WalletForm() { + const { register, handleSubmit, formState: { errors } } = useForm(); + + const onSubmit = (data) => { + console.log('Form data:', data); + }; + + return ( +
+ setValue('walletAddress', addr)} + error={errors.walletAddress?.message} + required + /> + + + ); +} +``` + +### Transaction Input + +```tsx +import { WalletAddressInput, AddressValidationStatus } from '@/components/security'; + +function SendTransaction() { + const [recipientAddress, setRecipientAddress] = useState(''); + const [isValid, setIsValid] = useState(false); + + const handleValidationChange = (status: AddressValidationStatus) => { + setIsValid( + status === AddressValidationStatus.VALID || + status === AddressValidationStatus.CHECKSUM + ); + }; + + return ( +
+ + +
+ ); +} +``` + +## Testing + +The component includes comprehensive unit tests covering: + +- Rendering with various props +- Address validation scenarios +- User interactions (typing, focus, blur) +- Custom validation functions +- Error handling and display +- Accessibility attributes +- Edge cases (long addresses, special characters, etc.) + +Run tests with: + +```bash +npm test WalletAddressInput +``` + +## Browser Support + +- Chrome/Edge: Latest 2 versions +- Firefox: Latest 2 versions +- Safari: Latest 2 versions +- Mobile browsers: iOS Safari, Chrome Mobile + +## Performance + +- Optimized with `useCallback` for event handlers +- Memoized validation results with `useMemo` +- Efficient re-rendering with proper dependency arrays +- Minimal DOM manipulation + +## Security Considerations + +- Input sanitization and validation +- Protection against XSS attacks +- Proper encoding of user input +- Length limitations to prevent DoS +- Validation runs client-side for immediate feedback + +## Future Enhancements + +Potential future improvements: + +- Support for multiple blockchain address formats +- Address book integration +- QR code scanning for mobile +- ENS domain name resolution +- Multi-signature wallet support +- Hardware wallet integration + +## Troubleshooting + +### Common Issues + +**Issue**: Input not updating +- **Solution**: Ensure `onChange` prop is properly implemented and calls `setState` or updates parent state + +**Issue**: Validation not working +- **Solution**: Check that addresses start with `0x` and are 42 characters long. Verify `autoFormat` is enabled if needed. + +**Issue**: Custom validation not triggering +- **Solution**: Ensure custom validator function returns a boolean and handles all edge cases. + +## Contributing + +When contributing to this component: + +1. Maintain TypeScript strict mode compliance +2. Add tests for new features +3. Update documentation for prop changes +4. Follow existing code style and patterns +5. Ensure accessibility standards are met + +## License + +This component is part of the PropChain Frontend project and follows the same MIT license. \ No newline at end of file diff --git a/docs/abi.md b/docs/abi.md new file mode 100644 index 00000000..1f59ec12 --- /dev/null +++ b/docs/abi.md @@ -0,0 +1,57 @@ +# Smart Contract ABI Integration Guide + +This guide explains how to integrate new smart contract ABIs into the frontend codebase, generate typed hooks, and use them in the application. + +## Overview + +The process involves three main steps: + +1. **Store the ABI:** Add the new contract ABI to the `src/config/abis.ts` file. +2. **Generate Hooks:** Use the `wagmi/cli` to generate typed hooks for the new ABI. +3. **Use the Hooks:** Import and use the generated hooks in your components. + +## 1. Store the ABI + +All smart contract ABIs are stored in the `src/config/abis.ts` file. This provides a single source of truth for all contract interfaces. + +To add a new ABI, simply export it as a new constant from this file: + +```typescript +// src/config/abis.ts + +export const MyNewContractABI = [...] as const; +``` + +## 2. Generate Hooks + +We use the `wagmi/cli` to automatically generate typed React hooks from the ABIs. This is configured in the `wagmi.config.ts` file. + +To regenerate the hooks after adding a new ABI, run the following command: + +```bash +npm run wagmi:generate +``` + +This will create a new file with the generated hooks, which you can then import and use in your application. + +## 3. Use the Hooks + +The generated hooks provide a simple and type-safe way to interact with your smart contracts. + +Here's an example of how to use a generated hook to call a contract method: + +```typescript +import { useMyNewContractWrite } from '../generated'; + +function MyComponent() { + const { write } = useMyNewContractWrite({ + functionName: 'myFunction', + }); + + return ; +} +``` + +## Cross-linking to Backend Repo + +The canonical source for all ABIs is the [Backend repository](https://github.com/your-org/backend-repo). Please ensure that you are using the latest version of the ABI from that repository. diff --git a/docs/adr/ADR-005-error-handling.md b/docs/adr/ADR-005-error-handling.md index 77cb3f68..e6878d6f 100644 --- a/docs/adr/ADR-005-error-handling.md +++ b/docs/adr/ADR-005-error-handling.md @@ -73,6 +73,7 @@ class ValidationError extends Error { - `` wraps the entire app and shows a full-page error screen for catastrophic failures - `` wraps major page sections (property list, wallet panel) and shows inline fallback UI +- `` renders route-level full-screen fallback UI for page-specific failures with retry and home navigation - `` wraps individual widgets and renders a compact error state ### Global Error Handler diff --git a/docs/as-any-survivors.md b/docs/as-any-survivors.md new file mode 100644 index 00000000..f31894e1 --- /dev/null +++ b/docs/as-any-survivors.md @@ -0,0 +1,16 @@ +# `as any` allow-list + +This document lists the remaining justified `as any` (or `no-explicit-any`) sites. +The ESLint rule `@typescript-eslint/no-explicit-any` is set to `"warn"` so new occurrences +surface in CI without breaking the build. + +## Production source survivors + +| File | Line | Reason | +|------|------|--------| +| `src/components/TransactionHistory.tsx:248` | Dynamic key access on generic row object in CSV export helper. Requires a keyed index signature that is intentionally not typed upstream. Replace when the row type is refined. | + +## Test-only survivors + +Test files (`__tests__/`, `*.test.*`, `*.spec.*`) are excluded from the warning because +mocking and spy utilities routinely require `as any`. These do not affect runtime type safety. diff --git a/docs/audit-retention.md b/docs/audit-retention.md new file mode 100644 index 00000000..396ffd94 --- /dev/null +++ b/docs/audit-retention.md @@ -0,0 +1,25 @@ +# Audit Log Retention Policy + +## Overview + +The SecurityAuditLogger manages a local in-memory audit log with deterministic rotation and quota enforcement. + +## Configuration + +| Parameter | Default | Description | +|-----------|---------|-------------| +| MAX_LOG_SIZE | 10,000 | Maximum number of log entries in memory | +| MAX_ALERT_SIZE | 1,000 | Maximum number of security alerts in memory | +| RETENTION_PERIOD_MS | 7 days | Maximum age of log entries before cleanup | +| EVICTION_THRESHOLD | 0.9 (90%) | Capacity threshold for eviction warning | + +## Rotation Behavior + +1. **LRU-by-time eviction**: When MAX_LOG_SIZE is exceeded, the oldest 30% of entries are evicted +2. **Warning**: A console warning is issued when storage reaches 90% capacity +3. **Remote export**: Evicted entries are exported to the remote sink (if configured) before removal +4. **Alert rotation**: When MAX_ALERT_SIZE is exceeded, alerts are sorted by recency and the oldest 30% are removed + +## Remote Export + +Set `NEXT_PUBLIC_AUDIT_EXPORT_URL` to enable remote export of evicted entries. The export is sent as a POST request with JSON body containing the entries, session ID, and timestamp. diff --git a/docs/cache-api.md b/docs/cache-api.md new file mode 100644 index 00000000..97ad2467 --- /dev/null +++ b/docs/cache-api.md @@ -0,0 +1,25 @@ +# Cache Manager API + +This document summarizes the public API surface of the cache manager. + +## Functions + +- `initCacheManager()`: Initializes the cache manager. +- `addNetworkStateListener(listener)`: Adds a listener for network state changes. +- `isNetworkOnline()`: Checks if the network is currently online. +- `getLastSyncTime()`: Gets the timestamp of the last successful sync. +- `performBackgroundSync()`: Performs a background sync. +- `addToSyncQueue(type, payload)`: Adds an item to the sync queue. +- `getSyncQueueLength()`: Gets the number of items in the sync queue. +- `clearSyncQueue()`: Clears the sync queue. +- `registerVersionMigration(version, handler)`: Registers a migration handler for a specific cache version. +- `getCacheVersion()`: Gets the current version of the cache. +- `onMutation(mutationType, handler)`: Registers a listener for a specific mutation type. +- `triggerMutation(mutationType, payload, invalidationPatterns)`: Triggers a mutation and invalidates the cache. +- `invalidateCache(pattern)`: Invalidates cache entries that match a given regex pattern. +- `invalidateAllCache()`: Invalidates the entire cache. +- `getCacheHealth()`: Gets the health status of the cache. +- `optimizeCache()`: Optimizes the cache by cleaning up expired entries. +- `exportCacheData()`: Exports the cache data to a JSON string. +- `importCacheData(jsonData)`: Imports cache data from a JSON string. +- `createCachedFetch(fetcher, key, strategy, ttl)`: Creates a cached fetch wrapper that supports different caching strategies. diff --git a/docs/csp.md b/docs/csp.md new file mode 100644 index 00000000..d7cd89a2 --- /dev/null +++ b/docs/csp.md @@ -0,0 +1,67 @@ +# Content Security Policy (CSP) + +## Overview + +PropChain enforces a strict Content Security Policy to prevent XSS attacks. The policy is applied via Next.js middleware. + +## Policy Directives + +- `default-src 'self'` - Only same-origin resources by default +- `img-src 'self' data: ipfs:` - Images from self, data URIs, and IPFS +- `script-src 'self' 'nonce-...'` - Only same-origin scripts with valid nonce +- `style-src 'self' 'unsafe-inline'` - Styles from self (inline allowed for Tailwind) +- `font-src 'self' data:` - Fonts from self and data URIs +- `connect-src 'self'` - API connections only to same origin +- `frame-src 'self'` - Frames only from same origin +- `base-uri 'self'` - Base URIs restricted to same origin +- `form-action 'self'` - Form submissions only to same origin +- `frame-ancestors 'self'` - Framing only by same origin + +## Environment Behavior + +- **Production**: `Content-Security-Policy` header (enforced) +- **Non-production**: `Content-Security-Policy-Report-Only` header (reported only) + +## CSP Reports + +CSP violations are reported to `POST /api/csp-report`. In development mode, reports are logged to the console. + +## Environment Control: `CSP_ENFORCE` + +The middleware uses the environment variable `CSP_ENFORCE` to toggle between **enforcement** and **report-only** modes: + +| `CSP_ENFORCE` | Environment | Header Sent | Behaviour | +|---|---|---|---| +| `"true"` | Any | `Content-Security-Policy` | Violations are **blocked** by the browser | +| anything else (or unset) | Any | *No CSP header* | CSP is disabled entirely | + +> **Note**: In development (`NODE_ENV=development`), the `script-src` directive includes `'unsafe-eval'` to support hot reload. This is **never** included in production builds. + +### Adding `CSP_ENFORCE` to your environment + +```env +# .env.local (development โ€” CSP disabled by default for easier debugging) +# CSP_ENFORCE=true # uncomment to test CSP enforcement locally + +# .env.production (production โ€” CSP should be enforced) +CSP_ENFORCE=true +``` + +### How to extend the CSP + +To add new directives or allow additional origins: + +1. Edit `src/middleware.ts` โ†’ `buildCspHeader()`. +2. Add the new directive to the `directives` array. +3. Ensure nonce-based scripts are properly handled (the `x-nonce` request header is forwarded). +4. Test in report-only mode first by setting `CSP_ENFORCE=false` and checking the browser console for violation reports. +5. Violations are automatically posted to `POST /api/csp-report` for monitoring. + +## Exclusions + +The following paths are excluded from CSP: +- `/api/*` - API routes +- `/sw.js` - Service Worker script +- `/_next/static/*` - Next.js static assets +- `/_next/image/*` - Next.js image optimization +- `/favicon.ico`, `/sitemap.xml`, `/robots.txt` diff --git a/docs/hooks.md b/docs/hooks.md index a34a5a88..ac0fba38 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -122,9 +122,9 @@ function TransactionForm() { ## `usePropertySearch` -**File**: `src/hooks/usePropertySearch.ts` +**File**: `src/hooks/usePropertySearchQuery.ts` (React Query implementation) -Combines the search store, property API calls, and URL synchronization into a single hook. Automatically initializes filters from URL query parameters on mount and keeps the URL in sync as filters change. +Combines the Zustand search store, React Query caching, and URL synchronization into a single hook. Reads filters/sort/page from the store, delegates fetching to `usePropertySearchQuery` which uses `useQuery` under the hood for automatic caching, deduplication, and stale-while-revalidate behaviour. ### Returns @@ -137,15 +137,27 @@ Combines the search store, property API calls, and URL synchronization into a si | `properties` | `Property[]` | Current page of search results | | `totalResults` | `number` | Total matching properties across all pages | | `totalPages` | `number` | Computed total page count | -| `isLoading` | `boolean` | `true` while a fetch is in progress | +| `isLoading` | `boolean` | `true` while a React Query fetch is pending or refetching | | `error` | `string \| null` | Error message from the last failed fetch | -| `lastUpdated` | `number \| null` | Timestamp of the last successful fetch | +| `lastUpdated` | `Date \| undefined` | Date of the last successful React Query fetch | | `setFilters` | `(filters: SearchFilters) => void` | Replace all filters at once | | `setFilter` | `(key, value) => void` | Update a single filter key | | `clearFilters` | `() => void` | Reset all filters to defaults | | `setSortBy` | `(sort: SortOption) => void` | Change the sort order | | `setPage` | `(page: number) => void` | Navigate to a page (also scrolls to top) | -| `refetch` | `() => Promise` | Manually trigger a fresh fetch | +| `setResultsPerPage` | `(count: number) => void` | Change the number of results per page | +| `loadMore` | `() => void` | Append the next page without scrolling to top | +| `refetch` | `() => Promise` | Manually trigger a fresh React Query refetch | + +### Edge cases + +- **Empty results**: `properties` is `[]`, `totalResults` is `0`, `totalPages` is `0`. +- **Fetch error**: `error` receives the message; `properties` is cleared to `[]`. +- **Rapid filter changes**: React Query deduplicates concurrent requests for the same query key. +- **Stale data**: Cached results are served instantly for 5 minutes (`staleTime`); a background refetch updates silently. +- **Window refocus**: Does NOT trigger a refetch (`refetchOnWindowFocus: false`). +- **4xx errors**: Not retried. Network/5xx errors retried up to 3 times. +- **URL sync**: The parent component is responsible for URL parameter synchronization (see `src/hooks/usePropertySearch.ts`). ### Example diff --git a/docs/mocking.md b/docs/mocking.md new file mode 100644 index 00000000..0c66a2b9 --- /dev/null +++ b/docs/mocking.md @@ -0,0 +1,26 @@ +# Mocking and Local Overrides + +This document outlines environment variables and other mechanisms available to override default application behavior for local development, testing, and design review. + +## Wallet Mocking + +To facilitate development and testing without requiring a live wallet connection (e.g., MetaMask), you can enable a mock wallet provider. + +### `NEXT_PUBLIC_MOCK_WALLET` + +- **Values**: `true` | `false` (default) +- **Description**: When set to `true`, the application will use a mock wallet provider that simulates a connected wallet. This is useful for developers, designers, or QA who need to interact with the application without connecting their own wallet. +- **Dev-only**: This flag is **ignored in production builds**. The mock connector is only activated when `NODE_ENV !== "production"`, so `NEXT_PUBLIC_MOCK_WALLET=true` can never enable the mock in a `next build` production bundle. +- **No committed key**: The mock signs with a randomly generated, per-session private key (created fresh on each page load) instead of a hardcoded key, so there is no long-lived secret that could sign real-looking transactions if the mock ever leaked into a non-dev environment. + +**Example `env.local`:** + +``` +NEXT_PUBLIC_MOCK_WALLET=true +``` + +**Example `env.production` (must be false/unset):** + +``` +NEXT_PUBLIC_MOCK_WALLET=false +``` diff --git a/docs/phishing-denylist.md b/docs/phishing-denylist.md new file mode 100644 index 00000000..9cda1e9f --- /dev/null +++ b/docs/phishing-denylist.md @@ -0,0 +1,61 @@ +# Phishing Denylist + +## Overview + +The phishing denylist is sourced from a trusted CDN at runtime with a signed manifest. A small fallback list is bundled for offline protection. + +## CDN Manifest Schema + +```json +{ + "version": "1.0.0", + "updatedAt": "2026-06-27T00:00:00Z", + "domains": ["phishing-domain-1.com", "phishing-domain-2.com"], + "contracts": ["0x1234..."], + "signature": "base64-encoded-signature" +} +``` + +## Update Procedure + +1. Update the phishing manifest JSON with new domains/contracts +2. Sign the manifest with the project's signing key +3. Upload to the CDN at `https://cdn.propchain.io/security/phishing-manifest.json` +4. The frontend automatically fetches the latest manifest (cached for 1 hour) +5. If the manifest fails verification, the fallback list is used + +## Signing + +The manifest `signature` is an EIP-191 personal-message signature over the +canonical JSON payload of the manifest with the `signature` field excluded, +using stable key order: `version`, `updatedAt`, `domains`, `contracts`. + +Example (Node.js): + +```js +import { privateKeyToAccount } from 'viem/accounts'; + +const { signature, ...payload } = manifest; // payload = { version, updatedAt, domains, contracts } +const account = privateKeyToAccount(process.env.MANIFEST_PRIVATE_KEY); +const signature = await account.signMessage({ message: JSON.stringify(payload) }); +``` + +The verifier recovers the signer address from the signature and requires it to +match `NEXT_PUBLIC_MANIFEST_SIGNING_KEY`. + +## Configuration + +Set `NEXT_PUBLIC_PHISHING_MANIFEST_URL` and `NEXT_PUBLIC_MANIFEST_SIGNING_KEY` in your environment. + +`NEXT_PUBLIC_MANIFEST_SIGNING_KEY` is the Ethereum address of the key that +signs the manifest. It is **required**: the manifest is never fetched or +applied without it, and a manifest whose signature does not recover to this +address is rejected. When unset, the CDN manifest is disabled (the bundled +fallback list is still used) and a warning is logged. + +## Fallback List + +A minimal fallback list is bundled for offline/startup scenarios: +- `metamask.io.fake` +- `myetherwallet.com.scam` +- `trustwallet.app.phish` diff --git a/docs/preview.md b/docs/preview.md new file mode 100644 index 00000000..8f577f58 --- /dev/null +++ b/docs/preview.md @@ -0,0 +1,62 @@ +# Ephemeral PR Preview Environments + +## Overview + +PropChain uses ephemeral preview environments for every pull request. Each PR gets: + +- **Frontend Preview**: Deployed to Vercel with a unique preview URL +- **Backend Fork**: A Hardhat fork node for isolated blockchain state + +This enables full-stack integration testing without affecting shared testnets. + +## How It Works + +1. **PR Opened / Updated**: GitHub Actions triggers the preview workflow +2. **Hardhat Fork**: Spawns an isolated Hardhat node at `http://preview-fork:8545` +3. **Vercel Preview**: Deploys the frontend with `NEXT_PUBLIC_PREVIEW_FORK_URL` pointing to the fork +4. **Comment**: A GitHub bot posts the preview URL in the PR +5. **PR Closed**: Both the Vercel deployment and Hardhat fork are destroyed + +## Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ GitHub PR โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ GitHub Actions โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ Vercel Preview โ”‚ +โ”‚ (Webhook) โ”‚ โ”‚ (preview.yml) โ”‚ โ”‚ (Frontend) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ–ผ โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Hardhat Fork โ”‚โ—€โ”€โ”€โ”€โ”‚ wagmi config โ”‚ + โ”‚ (preview-fork) โ”‚ โ”‚ transports โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Wagmi Configuration + +The `src/config/wagmi.ts` file adds a `hardhatPreview` chain (chain ID `31338`) when `NEXT_PUBLIC_PREVIEW_FORK_URL` is set: + +```typescript +const hardhatPreview = defineChain({ + id: 31338, + name: "Hardhat Preview Fork", + rpcUrls: { + default: { http: ["http://preview-fork:8545"] }, + }, +}); +``` + +## Required Secrets + +- `VERCEL_ORG_ID`: Vercel organization ID +- `VERCEL_PROJECT_ID`: Vercel project ID +- `VERCEL_TOKEN`: Vercel personal access token + +## Workflow File + +`.github/workflows/preview.yml` + +| Trigger | Action | +|---------|--------| +| PR opened/synced/reopened | Deploy preview + comment URL | +| PR closed | Destroy preview + comment teardown | diff --git a/docs/smart-contract-integration.md b/docs/smart-contract-integration.md index 070f97eb..f55951f1 100644 --- a/docs/smart-contract-integration.md +++ b/docs/smart-contract-integration.md @@ -18,8 +18,15 @@ Set contract addresses via environment variables: NEXT_PUBLIC_PROPERTY_NFT_ADDRESS=0x... NEXT_PUBLIC_MARKETPLACE_ADDRESS=0x... NEXT_PUBLIC_STAKING_ADDRESS=0x... +NEXT_PUBLIC_BATCH_PURCHASE_ADDRESS=0x... ``` +The batch purchase contract (`NEXT_PUBLIC_BATCH_PURCHASE_ADDRESS`) backs the +cart checkout flow. When it is unset, checkout fails closed with a +configuration error; when set, `src/lib/batchTransaction.ts` submits a real +`batchPurchase` transaction via the connected wallet and only reports success +after the receipt is observed on chain. + ## ABI Files ABI files live in `src/lib/abis/`. To update an ABI after a contract upgrade: diff --git a/docs/wallet-matrix.md b/docs/wallet-matrix.md new file mode 100644 index 00000000..1b5971fd --- /dev/null +++ b/docs/wallet-matrix.md @@ -0,0 +1,34 @@ +# Wallet Compatibility Matrix + +This document outlines the officially tested wallets, their supported versions, and any known issues when using them with our platform. + +## Officially Supported Wallets + +| Wallet | Tested Version(s) | Browser Support | Known Issues | +| --------------- | ----------------- | ---------------------------- | ------------------------------------------------------------ | +| MetaMask | 10.18.0 | Chrome, Firefox, Brave, Edge | - Older versions may have issues with EIP-1559 transactions. | +| Coinbase Wallet | 3.4.0 | Chrome, Brave | - Does not support all networks. | +| WalletConnect | 2.0.0 | (Protocol) | - Connection can be slow on some mobile networks. | + +## Experimental Wallets + +The following wallets are not officially supported, but we provide experimental connectors for them. Use them at your own risk. + +| Wallet | Connector Status | Known Issues | +| ------------ | ---------------- | ------------ | +| Rabby | Not Implemented | - | +| Phantom | Not Implemented | - | +| Trust Wallet | Not Implemented | - | +| Frame | Not Implemented | - | +| Safe | Not Implemented | - | +| Rainbow | Not Implemented | - | + +## Connector Implementation Notes + +The `WalletConnector.tsx` component handles the connection logic for all wallets. It uses the `useWalletConnector` hook to abstract the details of each wallet's connection process. + +### `ethereum.{method}` Quirks + +- **MetaMask:** No known quirks. +- **Coinbase Wallet:** No known quirks. +- **WalletConnect:** No known quirks. diff --git a/eslint.config.mjs b/eslint.config.mjs index 810d5cc1..478d808f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,28 +1,130 @@ // For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format import storybook from "eslint-plugin-storybook"; +import jsdoc from "eslint-plugin-jsdoc"; import tseslint from "@typescript-eslint/eslint-plugin"; import tsParser from "@typescript-eslint/parser"; -export default [{ - ignores: ["node_modules/**", ".next/**", "out/**", "dist/**", "coverage/**"], -}, { - files: ["src/**/*.{ts,tsx}"], - languageOptions: { - parser: tsParser, - parserOptions: { - project: ["./tsconfig.json"], - tsconfigRootDir: import.meta.dirname, - ecmaVersion: "latest", - sourceType: "module", +/* + * ESLint Allow-List (Issue #482) + * + * The following files contain intentional eslint-disable directives that + * cannot be expressed via flat-config rule overrides because the relevant + * plugins (react, @next/next, jsx-a11y) are loaded by Next.js's built-in + * ESLint integration, not by this configuration file. + * + * Known exceptions: + * + * react/no-danger + * - src/app/layout.tsx: Inline theme-bootstrap script (static string, no XSS risk). + * - src/components/ui/chart.tsx: ChartStyle DOMPurify-sanitised CSS (no XSS risk). + * + * @next/next/no-img-element + * - src/app/accessibility/page.tsx: Accessibility demo page using with alt text. + * - src/components/security/TransactionSecuritySettings.tsx: Dynamically generated QR + * code data URL โ€” Next.js does not support data: URLs. + * - src/components/__tests__/MobilePropertyViewer.test.tsx: next/image mock in test. + * - src/components/__tests__/MobilePropertyCard.test.tsx: next/image mock in test. + * + * jsx-a11y/alt-text + * - src/components/__tests__/MobilePropertyViewer.test.tsx: next/image mock in test. + * - src/components/__tests__/MobilePropertyCard.test.tsx: next/image mock in test. + * + * react/display-name + * - src/components/__tests__/RecentlyViewed.test.tsx: next/link mock in test. + * - src/components/__tests__/ComparisonBar.test.tsx: next/link mock in test. + * + * no-var + * - src/utils/security/__tests__/totp.test.ts: declare global { var crypto } pattern + * for Web Crypto API polyfill (required by the language). + * + * react-hooks/exhaustive-deps + * - src/app/properties/page.tsx: Intentional sync-on-URL-change effects that must + * not re-run when the store setter references change. + */ + +export default [ + { + ignores: [ + "node_modules/**", + ".next/**", + "out/**", + "dist/**", + "coverage/**", + ], + }, + { + files: ["src/**/*.{ts,tsx}"], + languageOptions: { + parser: tsParser, + parserOptions: { + project: ["./tsconfig.json"], + tsconfigRootDir: import.meta.dirname, + ecmaVersion: "latest", + sourceType: "module", + }, + }, + plugins: { + "@typescript-eslint": tseslint, + jsdoc, + }, + rules: { + // Warn on `as any` / `: any` in new code; existing justified survivors are + // documented in docs/as-any-survivors.md. Set to "warn" so CI surfaces + // regressions without hard-failing on the one remaining legacy site. + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/consistent-type-imports": "off", + "@typescript-eslint/no-unnecessary-type-assertion": "off", + // Escalated to `error` to enforce routing through the structured logger. + // The override below re-enables console.* in `src/utils/logger.ts` only โ€” + // that file is the canonical, mandatory sink for the logger pipeline. + "no-console": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: true, + FunctionExpression: true, + }, + contexts: ["ExportNamedDeclaration"], + }, + ], }, }, - plugins: { - "@typescript-eslint": tseslint, + { + // The structured logger sink is the only file allowed to call console.* + // directly. Any other module must go through `logger.{debug,info,warn,error}`. + files: ["src/utils/logger.ts"], + rules: { + "no-console": "off", + }, }, - rules: { - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/consistent-type-imports": "off", - "@typescript-eslint/no-unnecessary-type-assertion": "off", + { + // Apply `no-console` to everything else so future direct console.* calls + // are caught at lint time. + files: ["src/**/*.{ts,tsx}"], + ignores: [ + // earlyErrorSuppression.ts intentionally uses raw console; logger.ts + // and the deprecated structuredLogger.ts wrap it. + "src/utils/earlyErrorSuppression.ts", + "src/utils/logger.ts", + "src/utils/structuredLogger.ts", + // extensionDetection.ts intentionally overrides console.error to filter + // noisy browser-extension errors that are not actionable. + "src/utils/extensionDetection.ts", + // Test files and stories legitimately use console.* for debug output + // and assertions. + "src/**/__tests__/**", + "src/**/*.test.{ts,tsx}", + "src/**/*.stories.{ts,tsx}", + ], + rules: { + // disallow all console.* (no `allow` options provided). + "no-console": "error", + }, }, -}, ...storybook.configs["flat/recommended"]]; + ...storybook.configs["flat/recommended"], +]; diff --git a/jest.config.js b/jest.config.cjs similarity index 100% rename from jest.config.js rename to jest.config.cjs diff --git a/jest.setup.js b/jest.setup.js index 53202842..6067b2ba 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,4 +1,9 @@ import '@testing-library/jest-dom' + +// Next server modules expect Fetch API constructors in the Jest environment. +if (typeof globalThis.Request === 'undefined') globalThis.Request = class {}; +if (typeof globalThis.Response === 'undefined') globalThis.Response = class {}; +if (typeof globalThis.Headers === 'undefined') globalThis.Headers = class {}; import 'jest-axe/extend-expect' import { configure } from '@testing-library/react' @@ -65,24 +70,15 @@ Object.defineProperty(window, 'ethereum', { writable: true, }) -// Mock Web3Wallet -jest.mock('@walletconnect/web3-provider', () => { - return jest.fn().mockImplementation(() => ({ - enable: jest.fn(), - on: jest.fn(), - close: jest.fn(), - })) -}) - -// Mock Coinbase Wallet SDK +// Web3Wallet, Coinbase Wallet SDK, and MetaMask SDK mocks +// are defined per-test in walletConnectors tests to allow dynamic behavior. +// Mocks for other suites that need generic stubs: jest.mock('@coinbase/wallet-sdk', () => { return jest.fn().mockImplementation(() => ({ makeWeb3Provider: jest.fn(), disconnect: jest.fn(), })) }) - -// Mock MetaMask SDK jest.mock('@metamask/sdk', () => { return jest.fn().mockImplementation(() => ({ connect: jest.fn(), @@ -137,3 +133,14 @@ const sessionStorageMock = { clear: jest.fn(), } global.sessionStorage = sessionStorageMock + +// Polyfill crypto.randomUUID for jsdom (Node.js <19 / jsdom without randomUUID) +if (typeof globalThis.crypto !== 'undefined' && !globalThis.crypto.randomUUID) { + globalThis.crypto.randomUUID = function randomUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } +} diff --git a/middleware.ts b/middleware.ts index 5cfd3bc8..3446d5b2 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,32 +1,62 @@ -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { jwtVerify } from "jose"; // Define paths that require authentication -const PROTECTED_ROUTES = ['/dashboard', '/portfolio', '/settings', '/invest']; +const PROTECTED_ROUTES = ["/dashboard", "/portfolio", "/settings", "/invest"]; -export function middleware(request: NextRequest) { +export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; - + // Check if the current path is a protected route - const isProtectedRoute = PROTECTED_ROUTES.some(route => pathname.startsWith(route)); + const isProtectedRoute = PROTECTED_ROUTES.some((route) => + pathname.startsWith(route), + ); if (isProtectedRoute) { // Look for the auth token in cookies - // This assumes your auth flow sets a cookie named 'auth-token' - const token = request.cookies.get('auth-token')?.value; + const token = request.cookies.get("auth-token")?.value; if (!token) { // Redirect to home or login page if no token is found - const loginUrl = new URL('/', request.url); - // Optionally add a redirect parameter to return the user after login - loginUrl.searchParams.set('callbackUrl', pathname); + const loginUrl = new URL("/", request.url); + loginUrl.searchParams.set("callbackUrl", pathname); return NextResponse.redirect(loginUrl); } + + try { + const secretKey = process.env.AUTH_SECRET?.trim(); + if (!secretKey) { + throw new Error("AUTH_SECRET is not configured"); + } + + const secret = new TextEncoder().encode(secretKey); + + // Verify signature and expiry with 15s clock tolerance + await jwtVerify(token, secret, { + clockTolerance: 15, + }); + } catch (error) { + // Token is invalid, expired, or tampered with + const loginUrl = new URL("/", request.url); + loginUrl.searchParams.set("callbackUrl", pathname); + const response = NextResponse.redirect(loginUrl); + + // Clear the invalid cookie + response.cookies.delete("auth-token"); + + return response; + } } return NextResponse.next(); } export const config = { - matcher: ['/dashboard/:path*', '/portfolio/:path*', '/settings/:path*', '/invest/:path*'], -}; \ No newline at end of file + matcher: [ + "/dashboard/:path*", + "/portfolio/:path*", + "/settings/:path*", + "/invest/:path*", + ], +}; diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c7..0c7fad71 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,7 @@ /// /// -import "./.next/types/routes.d.ts"; +/// +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.ts b/next.config.ts index 17e30ec8..c2ebcace 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,11 +2,21 @@ import type { NextConfig } from "next"; const isAnalyzeEnabled = process.env.ANALYZE === "true"; const isDev = process.env.NODE_ENV === "development"; +const isProd = process.env.NODE_ENV === "production"; -const cspReportOnly = [ +// `BuildStatsPlugin` writes a JSON payload into `.next/` for on-demand +// inspection. It is ONLY meant for local development/debugging โ€” production +// builds must never emit it. +// - Gate on the explicit `ANALYZE=true` opt-in flag. +// - Hard-disable on production builds even if `ANALYZE=true` is set +// (e.g. misconfigured CI). +// - Skip on server builds (this plugin is client-side only). +// See README ยง "Build stats plugin" for details. + +const csp = [ "default-src 'self'", `script-src 'self'${isDev ? " 'unsafe-eval'" : ""}`, - "style-src 'self' 'unsafe-inline'", + "style-src 'self'", "img-src 'self' data: blob: https:", "font-src 'self' data: https:", isDev @@ -24,6 +34,7 @@ const cspReportOnly = [ ].join("; "); const nextConfig: NextConfig = { + output: "standalone", experimental: { optimizePackageImports: [ "lucide-react", @@ -77,7 +88,8 @@ const nextConfig: NextConfig = { headers: [ { key: "Cache-Control", - value: "public, max-age=60, stale-while-revalidate=300, s-maxage=300", + value: + "public, max-age=60, stale-while-revalidate=300, s-maxage=300", }, { key: "Vary", @@ -98,8 +110,8 @@ const nextConfig: NextConfig = { source: "/:path*", headers: [ { - key: "Content-Security-Policy-Report-Only", - value: cspReportOnly, + key: "Content-Security-Policy", + value: csp, }, ], }, @@ -109,14 +121,11 @@ const nextConfig: NextConfig = { config.resolve = config.resolve ?? {}; config.resolve.alias = { ...(config.resolve.alias ?? {}), - "@walletconnect/ethereum-provider": false, - "@safe-global/safe-apps-sdk": false, - "@safe-global/safe-apps-provider": false, - "@base-org/account": false, - "@gemini-wallet/core": false, - "@react-native-async-storage/async-storage": false, - porto: false, - "porto/internal": false, + // Wallet SDKs are intentionally NOT aliased to `false`. + // They are lazy-loaded via dynamic imports in useWalletConnector.ts + // and the wallet connector modules under src/lib/walletConnectors/. + // Setting them to `false` breaks wagmi connector detection and + // prevents WalletConnect v2, Safe, Coinbase, and MetaMask connections. }; if (!isServer && config.optimization?.splitChunks) { @@ -136,17 +145,26 @@ const nextConfig: NextConfig = { chunks: "all", priority: 25, }, + safe: { + name: "safe-vendors", + test: /[\\/]node_modules[\\/]@safe-global[\\/]/, + chunks: "all", + priority: 30, + }, }, }; } - if (isAnalyzeEnabled && !isServer) { + if (isAnalyzeEnabled && !isServer && !isProd) { class BuildStatsPlugin { apply(compiler: any) { compiler.hooks.done.tap("BuildStatsPlugin", (stats: any) => { const fs = require("fs"); const path = require("path"); - const outputPath = path.join(compiler.options.output.path ?? ".next", "build-stats.json"); + const outputPath = path.join( + compiler.options.output.path ?? ".next", + "build-stats.json", + ); fs.writeFileSync( outputPath, JSON.stringify( @@ -157,8 +175,8 @@ const nextConfig: NextConfig = { chunkGroups: true, }), null, - 2 - ) + 2, + ), ); }); } diff --git a/package-lock.json b/package-lock.json index 1bb512f9..b5559021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,8 +40,10 @@ "@tanstack/react-query": "^5.90.19", "@tanstack/react-query-devtools": "^5.100.2", "@tanstack/react-virtual": "^3.13.24", + "@testing-library/dom": "^10.4.1", + "@upstash/redis": "^1.38.0", "@wagmi/connectors": "^7.1.2", - "@wagmi/core": "^3.2.2", + "@wagmi/core": "3.4.0", "@walletconnect/web3-provider": "^1.8.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -55,6 +57,7 @@ "i18next-browser-languagedetector": "^8.2.1", "input-otp": "^1.4.2", "ioredis": "^5.3.2", + "jose": "^6.2.4", "jspdf": "^4.0.0", "jspdf-autotable": "^5.0.7", "leaflet": "^1.9.4", @@ -117,6 +120,7 @@ "jest-environment-jsdom": "^29.7.0", "msw": "^2.13.6", "playwright": "^1.58.2", + "plop": "^4.0.0", "postcss": "^8.5.3", "storybook": "^10.3.3", "tailwindcss": "^4.1.4", @@ -971,29 +975,31 @@ } }, "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", "dev": true, "license": "MIT", "optional": true, @@ -2351,6 +2357,45 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@inquirer/figures": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", @@ -5515,6 +5560,40 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -6247,70 +6326,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.8.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.8.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", @@ -6360,9 +6375,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.100.5", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.5.tgz", - "integrity": "sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg==", + "version": "5.100.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.2.tgz", + "integrity": "sha512-HzzOC7xgSfGGzZ1gTsFZqYz6rxGg3tYF77nTPctin+wEYYLNMP7LjwPVFALEGNdjxkHvcewh1EM5ywixeukS4w==", "license": "MIT", "funding": { "type": "github", @@ -6370,9 +6385,9 @@ } }, "node_modules/@tanstack/query-devtools": { - "version": "5.100.5", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.100.5.tgz", - "integrity": "sha512-SuCkVCqqliRYJvm+LEL2U/TcFv92zTnHj6OGrJFHp1v/RsiwamI+ZDgQzbeUrLsJb8/Nj/52aIw0NyDMcVHl4A==", + "version": "5.100.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.100.2.tgz", + "integrity": "sha512-0vAp4Y9RyywcZ3gb+wFoiR+pEViDT2ZG/ZaUhn7zXHuUbxuAdeEKuhlh9SDW2vjsPdm9F2AWqplr/QxhOeoqEQ==", "license": "MIT", "funding": { "type": "github", @@ -6380,12 +6395,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.100.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.5.tgz", - "integrity": "sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA==", + "version": "5.100.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.2.tgz", + "integrity": "sha512-MvvzPcurtzVh4EcbsTfI1BL5GOfdi1S0dk/qhigEghW07MvcHUl/dhfc1FT8hPEquuMtUC+IIAxC0bdmSp/7kA==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.100.5" + "@tanstack/query-core": "5.100.2" }, "funding": { "type": "github", @@ -6396,19 +6411,19 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.100.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.100.5.tgz", - "integrity": "sha512-bItQERx7dJoiI0WEoS4tIrvNnmk4kUYsaQLdIpm4o9Kttmsi5B6xlY6JBDkavstR3hH/R2+VT5dr3L5LBFPW4g==", + "version": "5.100.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.100.2.tgz", + "integrity": "sha512-PE5Pgotl8GKv4Mi0s4YiwTcA+evvb2fHMMWexJDx0D3EsBjtf3MbhYuv9kt+oBnbbsjQj4LJTza2PG2vw2pdOQ==", "license": "MIT", "dependencies": { - "@tanstack/query-devtools": "5.100.5" + "@tanstack/query-devtools": "5.100.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.100.5", + "@tanstack/react-query": "^5.100.2", "react": "^18 || ^19" } }, @@ -6749,6 +6764,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/fined": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/fined/-/fined-1.1.5.tgz", + "integrity": "sha512-2N93vadEGDFhASTIRbizbl4bNqpMOId5zZfj6hHqYZfEzEfO9onnU4Im8xvzo8uudySDveDHBOOSlTWf38ErfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -6759,6 +6781,17 @@ "@types/node": "*" } }, + "node_modules/@types/inquirer": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.10.tgz", + "integrity": "sha512-vFW2WbXwO9eZpRT5GJGFJ/shgyMNnYozmnjakt9jCQSS1lvqX8pZEQMjJ9RdDPct/YxwciQ8+V8OYn9euIrZDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/through": "*", + "rxjs": "^7.2.0" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -6884,6 +6917,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/liftoff": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/liftoff/-/liftoff-4.0.3.tgz", + "integrity": "sha512-UgbL2kR5pLrWICvr8+fuSg0u43LY250q7ZMkC+XKC3E+rs/YBDEnQIzsnhU5dYsLlwMi3R75UvCL87pObP1sxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fined": "*", + "@types/node": "*" + } + }, "node_modules/@types/mdx": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", @@ -6921,6 +6965,13 @@ "@types/node": "*" } }, + "node_modules/@types/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/qrcode": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", @@ -7012,6 +7063,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/tmp": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", @@ -7062,17 +7123,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz", - "integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.1", - "@typescript-eslint/type-utils": "8.59.1", - "@typescript-eslint/utils": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/type-utils": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -7085,20 +7146,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.1", + "@typescript-eslint/parser": "^8.59.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", - "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==", + "node_modules/@typescript-eslint/parser": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.1", - "@typescript-eslint/types": "^8.59.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3" }, "engines": { @@ -7109,33 +7172,46 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", - "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==", + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1" + "ms": "^2.1.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", - "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==", + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.0", + "@typescript-eslint/types": "^8.59.0", + "debug": "^4.4.3" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7147,12 +7223,41 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", - "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", + "node_modules/@typescript-eslint/project-service/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/project-service/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7161,23 +7266,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", - "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.1", - "@typescript-eslint/tsconfig-utils": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7189,17 +7283,18 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz", - "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/typescript-estree": "8.59.1" + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7213,494 +7308,10 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", - "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.1", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz", - "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/typescript-estree": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", - "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.1", - "@typescript-eslint/types": "^8.59.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", - "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", - "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", - "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", - "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.1", - "@typescript-eslint/tsconfig-utils": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", - "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.1", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", - "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.0", - "@typescript-eslint/types": "^8.59.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/project-service/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", - "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", - "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz", - "integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/typescript-estree": "8.59.1", - "@typescript-eslint/utils": "8.59.1", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", - "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.1", - "@typescript-eslint/types": "^8.59.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", - "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", - "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", - "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", - "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.1", - "@typescript-eslint/tsconfig-utils": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz", - "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/typescript-estree": "8.59.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", - "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.1", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -7715,19 +7326,6 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@typescript-eslint/type-utils/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8126,6 +7724,15 @@ "win32" ] }, + "node_modules/@upstash/redis": { + "version": "1.38.3", + "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.38.3.tgz", + "integrity": "sha512-vtS0BonQHU6kDSWvHTISh+LOuLIEk+jeMXebv20CDJ/aHGOG085SGa8OpI+I+Ow8WgPFhqH23XG8kQ2LXXRnag==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, "node_modules/@vitest/browser": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.5.tgz", @@ -8425,9 +8032,9 @@ } }, "node_modules/@wagmi/core": { - "version": "3.4.6", - "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-3.4.6.tgz", - "integrity": "sha512-wDZpRfzQo6NJj770mt23HdeU9O0MDO3cnxVP7tP/1HL7DLqOGMN3hADIc0wEF51ejrpnJlGLf8hS1qb2ZAzqJA==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-3.4.0.tgz", + "integrity": "sha512-EU5gDsUp5t7+cuLv12/L8hfyWfCIKsBNiiBqpOqxZJxvAcAiQk4xFe2jMgaQPqApc3Omvxrk032M8AQ4N0cQeg==", "license": "MIT", "dependencies": { "eventemitter3": "5.0.1", @@ -8439,7 +8046,7 @@ }, "peerDependencies": { "@tanstack/query-core": ">=5.0.0", - "accounts": "~0.8.1", + "ox": ">=0.11.1", "typescript": ">=5.7.3", "viem": "2.x" }, @@ -8447,7 +8054,7 @@ "@tanstack/query-core": { "optional": true }, - "accounts": { + "ox": { "optional": true }, "typescript": { @@ -9109,6 +8716,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -9132,6 +8749,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -9741,6 +9368,18 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/blakejs": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", @@ -10160,6 +9799,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -10170,6 +9816,13 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -10285,6 +9938,19 @@ "node": ">=8" } }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-table3": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", @@ -11225,6 +10891,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/deferred-leveldown": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-1.2.2.tgz", @@ -11317,6 +11006,16 @@ "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", "license": "MIT" }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -11369,6 +11068,13 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -13339,6 +13045,19 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -13681,6 +13400,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/fined": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", + "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0", + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/flagged-respawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -13717,6 +13479,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -14071,13 +13856,65 @@ "dev": true, "license": "MIT", "dependencies": { - "ini": "2.0.0" + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "which": "bin/which" } }, "node_modules/globals": { @@ -14137,15 +13974,37 @@ "license": "ISC" }, "node_modules/graphql": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.0.tgz", - "integrity": "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==", + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -14392,6 +14251,19 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -14686,6 +14558,78 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/inquirer": { + "version": "9.3.8", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz", + "integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/external-editor": "^1.0.2", + "@inquirer/figures": "^1.0.3", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -14710,6 +14654,16 @@ "node": ">=12" } }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/iobuffer": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", @@ -14717,9 +14671,9 @@ "license": "MIT" }, "node_modules/ioredis": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.0.tgz", - "integrity": "sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==", + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", + "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", "license": "MIT", "dependencies": { "@ioredis/commands": "1.10.0", @@ -14761,6 +14715,20 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-arguments": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", @@ -15082,6 +15050,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -15152,6 +15130,16 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -15177,6 +15165,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-set": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", @@ -15274,6 +15275,19 @@ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "license": "MIT" }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -15333,6 +15347,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", @@ -15355,6 +15379,19 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -15362,6 +15399,16 @@ "dev": true, "license": "ISC" }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isows": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", @@ -16598,6 +16645,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz", + "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -17091,6 +17147,25 @@ "node": ">= 0.8.0" } }, + "node_modules/liftoff": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.1.tgz", + "integrity": "sha512-wwLXMbuxSF8gMvubFcFRp56lkFV69twvbU5vDPbaw+Q+/rF8j0HKjGbIdlSi+LuJm9jf7k9PB+nTxnsLMPcv2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -17619,6 +17694,16 @@ "tmpl": "1.0.5" } }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -17980,22 +18065,22 @@ } }, "node_modules/msw/node_modules/tldts": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", - "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.5.tgz", + "integrity": "sha512-RfEzKWcq5fHUOFq7J3rl3Oz6ylKGtcHqUznzj4EcXsxLSIjJcvpbXAQtWGeJQ0xKnimR5e0Cn+cn9TssfMzm+g==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.2" + "tldts-core": "^7.4.5" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/msw/node_modules/tldts-core": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", - "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.5.tgz", + "integrity": "sha512-pGrwzZDvPwKe+7NNUqAunb6rqTfynr0VOUhCMdqbu5xlvNiszsAJygRzwvpVycdzejlbpY+SWJOn+s75Og7FEA==", "dev": true, "license": "MIT" }, @@ -18056,6 +18141,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanospinner": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/nanospinner/-/nanospinner-1.2.2.tgz", + "integrity": "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1" + } + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -18079,6 +18174,13 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/next": { "version": "16.2.4", "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", @@ -18265,6 +18367,28 @@ "dev": true, "license": "MIT" }, + "node_modules/node-plop": { + "version": "0.32.3", + "resolved": "https://registry.npmjs.org/node-plop/-/node-plop-0.32.3.tgz", + "integrity": "sha512-tn+OxutdqhvoByKJ7p84FZBSUDfUB76bcvj0ugLBvgE9V52LFcnz8cauCDKi6otnctvFCqa9XkrU35pBY5Baig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/inquirer": "^9.0.9", + "@types/picomatch": "^4.0.2", + "change-case": "^5.4.4", + "dlv": "^1.1.3", + "handlebars": "^4.7.8", + "inquirer": "^9.3.8", + "isbinaryfile": "^5.0.6", + "resolve": "^1.22.10", + "tinyglobby": "^0.2.15", + "title-case": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/node-releases": { "version": "2.0.38", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", @@ -18409,6 +18533,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object.entries": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", @@ -18459,6 +18599,19 @@ "node": ">= 0.4" } }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object.values": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", @@ -18566,6 +18719,43 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ospath": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", @@ -18770,6 +18960,21 @@ "node": ">=6" } }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/parse-headers": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", @@ -18795,6 +19000,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -18844,6 +19059,29 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -19079,6 +19317,28 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/plop": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/plop/-/plop-4.0.5.tgz", + "integrity": "sha512-pJz6oWC9LyBp5mBrRp8AUV2RNiuGW+t/HOs4zwN+b/3YxoObZOOFvjn1mJMpAeKi2pbXADMFOOVQVTVXEdDHDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/liftoff": "^4.0.3", + "interpret": "^3.1.1", + "liftoff": "^5.0.1", + "nanospinner": "^1.2.2", + "node-plop": "^0.32.3", + "picocolors": "^1.1.1", + "v8flags": "^4.0.1" + }, + "bin": { + "plop": "bin/plop.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/pngjs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", @@ -19108,9 +19368,9 @@ } }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "dev": true, "funding": [ { @@ -19721,9 +19981,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.74.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.74.0.tgz", - "integrity": "sha512-yR6wHr99p9wFv686jhRWVSFhUvDvNbdUf2dKlbno8/VKOCuoNobDGC6S+M2dua9A9Yo8vpcrp8assIYbsZCQ9g==", + "version": "7.73.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.73.1.tgz", + "integrity": "sha512-VAfVYOPcx3piiEVQy95vyFmBwbVUsP/AUIN+mpFG8h11yshDd444nn0VyfaGWSRnhOLVgiDu7HIuBtAIzxn9dA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -19985,6 +20245,19 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -20246,6 +20519,20 @@ "node": ">=8" } }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -20403,6 +20690,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -20606,9 +20903,9 @@ "license": "ISC" }, "node_modules/set-cookie-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", - "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.1.tgz", + "integrity": "sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA==", "dev": true, "license": "MIT" }, @@ -21780,6 +22077,13 @@ "node": ">=14.0.0" } }, + "node_modules/title-case": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-4.3.2.tgz", + "integrity": "sha512-I/nkcBo73mO42Idfv08jhInV61IMb61OdIFxk+B4Gu1oBjWBPOLmhZdsli+oJCVaD+86pYQA93cJfFt224ZFAA==", + "dev": true, + "license": "MIT" + }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -22216,110 +22520,20 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", - "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/type-utils": "8.59.0", - "@typescript-eslint/utils": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.59.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", - "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", - "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/utils": "8.59.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/typescript-eslint/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=0.8.0" } }, - "node_modules/typescript-eslint/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -22339,6 +22553,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -22601,6 +22831,16 @@ "node": ">=10.12.0" } }, + "node_modules/v8flags": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/vaul": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", @@ -23009,13 +23249,13 @@ } }, "node_modules/wagmi": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-3.6.5.tgz", - "integrity": "sha512-TBN/h26CX/FQROEk4zXCtRXGfL2erBEZ9BAbfRpn+sujMtQAoDzGM7LFAr4ODCiDcRAqJcMQWGJvk25DMEnFaQ==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-3.6.4.tgz", + "integrity": "sha512-aAvjKlRv1pMlw/fcZUFCCyeR4b32iCtcPqPH9cphuIygxag3wnzvGR2/kaXbY08qKeHZEkD8bOvb8acBOfy05A==", "license": "MIT", "dependencies": { - "@wagmi/connectors": "8.0.5", - "@wagmi/core": "3.4.6", + "@wagmi/connectors": "8.0.4", + "@wagmi/core": "3.4.5", "use-sync-external-store": "1.4.0" }, "funding": { @@ -23034,9 +23274,9 @@ } }, "node_modules/wagmi/node_modules/@wagmi/connectors": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-8.0.5.tgz", - "integrity": "sha512-Xxysn4jalQS5W4b687LX0znp2eswonS/1fvRRVAlPD+LG15YRs8nHaC7xAjI9lVMWAx2TePw9Car6pQ5nzYVsA==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-8.0.4.tgz", + "integrity": "sha512-zRxRmd4TnNv/LxYm/IcrUsXMFBECzEQAOAcUDRMUfRRKfneJFqmmv1kmUbWSIc/gBkBu6o3BAGPJjV5j3BUcLA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/wevm" @@ -23047,7 +23287,7 @@ "@metamask/connect-evm": "~0.9.0", "@safe-global/safe-apps-provider": "~0.18.6", "@safe-global/safe-apps-sdk": "^9.1.0", - "@wagmi/core": "3.4.6", + "@wagmi/core": "3.4.5", "@walletconnect/ethereum-provider": "^2.21.1", "accounts": "~0.6.7", "porto": "~0.2.35", @@ -23084,6 +23324,43 @@ } } }, + "node_modules/wagmi/node_modules/@wagmi/core": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-3.4.5.tgz", + "integrity": "sha512-rmqnLRlyFWcP2VvvQtS1XMmupaSruxCwSTfwB8v7pRyeVDywsOoJwIpLh4PI5o//b3ia4P0gND4vkQ1MmU8C3g==", + "license": "MIT", + "dependencies": { + "eventemitter3": "5.0.1", + "mipd": "0.0.7", + "zustand": "5.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@tanstack/query-core": ">=5.0.0", + "accounts": "~0.6.7", + "typescript": ">=5.7.3", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "@tanstack/query-core": { + "optional": true + }, + "accounts": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/wagmi/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/wagmi/node_modules/use-sync-external-store": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", @@ -23093,6 +23370,35 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/wagmi/node_modules/zustand": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", + "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -23103,6 +23409,16 @@ "makeerror": "1.0.12" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/web3-provider-engine": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/web3-provider-engine/-/web3-provider-engine-16.0.1.tgz", @@ -23420,6 +23736,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -23664,6 +23987,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index 644b53ae..a1a509be 100644 --- a/package.json +++ b/package.json @@ -3,31 +3,31 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", - "typecheck": "tsc --noEmit --incremental false", "build": "npm run typecheck && next build", + "build-storybook": "storybook build", "build:analyze": "node scripts/run-analyze-build.mjs", "bundle:measure": "node scripts/measure-bundle-size.mjs", - "start": "next start", - "lint": "eslint . --max-warnings=0", + "dev": "next dev", + "lint": "eslint . --max-warnings=0 && node scripts/sort-package-json.mjs", "perf:budgets": "node scripts/check-performance-budgets.mjs", "perf:ci": "npm run build && npm run perf:budgets", + "security:check-globals": "node scripts/check-exposed-globals.mjs", "validate:env": "node scripts/validate-env.js", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build", "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", "test:ci": "jest --coverage --watchAll=false --ci", + "test:coverage": "jest --coverage", + "test:cy": "cypress run", + "test:cy:component": "cypress run --component", + "test:cy:component:ui": "cypress open --component", + "test:cy:ui": "cypress open", "test:e2e": "playwright test", - "test:e2e:mock": "SKIP_WEBSERVER=true playwright test tests/e2e/property-purchase-flow.spec.ts", - "test:e2e:ui": "playwright test --ui", "test:e2e:debug": "playwright test --debug", "test:e2e:install": "playwright install", - "test:cy": "cypress run", - "test:cy:ui": "cypress open", - "test:cy:component": "cypress run --component", - "test:cy:component:ui": "cypress open --component" + "test:e2e:mock": "SKIP_WEBSERVER=true playwright test tests/e2e/property-purchase-flow.spec.ts", + "test:e2e:ui": "playwright test --ui", + "test:watch": "jest --watch", + "typecheck": "tsc --noEmit --incremental false" }, "dependencies": { "@coinbase/wallet-sdk": "^4.3.7", @@ -62,6 +62,7 @@ "@tanstack/react-query": "^5.90.19", "@tanstack/react-query-devtools": "^5.100.2", "@tanstack/react-virtual": "^3.13.24", + "@testing-library/dom": "^10.4.1", "@wagmi/connectors": "^7.1.2", "@wagmi/core": "3.4.0", "@walletconnect/web3-provider": "^1.8.0", @@ -77,6 +78,7 @@ "i18next-browser-languagedetector": "^8.2.1", "input-otp": "^1.4.2", "ioredis": "^5.3.2", + "jose": "^6.2.4", "jspdf": "^4.0.0", "jspdf-autotable": "^5.0.7", "leaflet": "^1.9.4", @@ -92,6 +94,7 @@ "react-leaflet-cluster": "^4.1.3", "react-resizable-panels": "^4.4.1", "recharts": "^2.15.4", + "@upstash/redis": "^1.38.0", "redis": "^4.6.10", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", @@ -139,6 +142,7 @@ "jest-environment-jsdom": "^29.7.0", "msw": "^2.13.6", "playwright": "^1.58.2", + "plop": "^4.0.0", "postcss": "^8.5.3", "storybook": "^10.3.3", "tailwindcss": "^4.1.4", diff --git a/test-results/property-purchase-Property-a52d4-onfirm-purchase-transaction-chromium/error-context.md b/playwright-report/data/a2f291aa2753c8abec9317ab3c797f2f2b47a946.md similarity index 72% rename from test-results/property-purchase-Property-a52d4-onfirm-purchase-transaction-chromium/error-context.md rename to playwright-report/data/a2f291aa2753c8abec9317ab3c797f2f2b47a946.md index 23903d53..e97a44e8 100644 --- a/test-results/property-purchase-Property-a52d4-onfirm-purchase-transaction-chromium/error-context.md +++ b/playwright-report/data/a2f291aa2753c8abec9317ab3c797f2f2b47a946.md @@ -6,13 +6,13 @@ # Test info -- Name: property-purchase.spec.ts >> Property Purchase Flow >> should confirm purchase transaction -- Location: tests/e2e/property-purchase.spec.ts:201:7 +- Name: wallet-properties-integration.spec.ts >> Wallet + Properties Integration >> should display properties and show connected wallet status +- Location: tests/e2e/wallet-properties-integration.spec.ts:4:3 # Error details ``` -Error: browserType.launch: Executable doesn't exist at /home/codespace/.cache/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-linux64/chrome-headless-shell +Error: browserType.launch: Executable doesn't exist at /home/zakayola/.cache/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-linux64/chrome-headless-shell โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— โ•‘ Looks like Playwright was just installed or updated. โ•‘ โ•‘ Please run the following command to download new browsers: โ•‘ diff --git a/test-results/property-purchase-Property-66cbe-filter-transactions-by-type-chromium/error-context.md b/playwright-report/data/bb6e31a9249c2edb16388e7a968692f65e7c0891.md similarity index 71% rename from test-results/property-purchase-Property-66cbe-filter-transactions-by-type-chromium/error-context.md rename to playwright-report/data/bb6e31a9249c2edb16388e7a968692f65e7c0891.md index d54d1940..41b41ccd 100644 --- a/test-results/property-purchase-Property-66cbe-filter-transactions-by-type-chromium/error-context.md +++ b/playwright-report/data/bb6e31a9249c2edb16388e7a968692f65e7c0891.md @@ -6,13 +6,13 @@ # Test info -- Name: property-purchase.spec.ts >> Property Purchase Flow >> should filter transactions by type -- Location: tests/e2e/property-purchase.spec.ts:298:7 +- Name: walletconnect-only.spec.ts >> WalletConnect-Only Flow (No Injected Provider) >> should offer WalletConnect as primary option and successfully connect +- Location: tests/e2e/walletconnect-only.spec.ts:16:3 # Error details ``` -Error: browserType.launch: Executable doesn't exist at /home/codespace/.cache/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-linux64/chrome-headless-shell +Error: browserType.launch: Executable doesn't exist at /home/zakayola/.cache/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-linux64/chrome-headless-shell โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— โ•‘ Looks like Playwright was just installed or updated. โ•‘ โ•‘ Please run the following command to download new browsers: โ•‘ diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 00000000..b5afdc91 --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,90 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts index fdc749b7..367659a5 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,4 +1,5 @@ import { defineConfig, devices } from '@playwright/test'; +import { walletFixture } from './tests/fixtures/wallet-msw'; /** * @see https://playwright.dev/docs/test-configuration diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 044c66c6..f4dd9d2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,6 +252,9 @@ importers: '@tailwindcss/postcss': specifier: ^4 version: 4.2.4 + '@testing-library/dom': + specifier: ^10.4.1 + version: 10.4.1 '@testing-library/jest-dom': specifier: ^6.5.0 version: 6.9.1 @@ -314,7 +317,7 @@ importers: version: 10.3.5(eslint@9.39.4(jiti@2.6.1))(storybook@10.3.5(@testing-library/dom@10.4.1)(bufferutil@4.1.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(utf-8-validate@5.0.10))(typescript@5.9.3) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.19.39) + version: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) jest-axe: specifier: ^10.0.0 version: 10.0.0 @@ -336,11 +339,14 @@ importers: tailwindcss: specifier: ^4.1.4 version: 4.2.4 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.19.39)(typescript@5.9.3) tw-animate-css: specifier: ^1.4.0 version: 1.4.0 typescript: - specifier: ^5 + specifier: ^5.9.3 version: 5.9.3 vite: specifier: ^8.0.3 @@ -565,6 +571,10 @@ packages: '@coinbase/wallet-sdk@4.3.7': resolution: {integrity: sha512-z6e5XDw6EF06RqkeyEa+qD0dZ2ZbLci99vx3zwDY//XO8X7166tqKJrR2XlQnzVmtcUuJtCd5fCvr9Cu6zzX7w==} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@cypress/react@9.0.2': resolution: {integrity: sha512-b20a0g6Ot3u92wdmDD+4/r5NkAKPJz+yN2miuydDwy63Hzpk9fLQ0tee5xzx/0VPxYrh3dzedoO8dwUR3qpBlg==} peerDependencies: @@ -1142,6 +1152,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@mdx-js/react@3.1.1': resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} peerDependencies: @@ -2458,6 +2471,18 @@ packages: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -3160,6 +3185,9 @@ packages: arch@2.2.0: resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -3682,6 +3710,9 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-fetch@2.2.6: resolution: {integrity: sha512-9JZz+vXCmfKUZ68zAptS7k4Nu8e2qcibe7WVZYps7sAgk5R8GYTc+T1WR0v1rlP9HxgARmOX1UTIJZFytajpNA==} @@ -3912,6 +3943,10 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + dijkstrajs@1.0.3: resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} @@ -5487,6 +5522,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} @@ -6748,6 +6786,20 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} @@ -6934,6 +6986,9 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} @@ -7299,6 +7354,10 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -7606,6 +7665,10 @@ snapshots: - utf-8-validate - zod + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@cypress/react@9.0.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(cypress@15.14.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@types/react-dom': 19.2.3(@types/react@19.2.14) @@ -7992,7 +8055,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -8006,7 +8069,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.19.39) + jest-config: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -8172,6 +8235,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: '@types/mdx': 2.0.13 @@ -9507,6 +9575,14 @@ snapshots: '@tootallnate/once@2.0.0': {} + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -10287,6 +10363,8 @@ snapshots: arch@2.2.0: {} + arg@4.1.3: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -10844,13 +10922,13 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.12 - create-jest@29.7.0(@types/node@20.19.39): + create-jest@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.19.39) + jest-config: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -10859,6 +10937,8 @@ snapshots: - supports-color - ts-node + create-require@1.1.1: {} + cross-fetch@2.2.6: dependencies: node-fetch: 2.7.0 @@ -11094,6 +11174,8 @@ snapshots: diff-sequences@29.6.3: {} + diff@4.0.4: {} + dijkstrajs@1.0.3: {} doctrine@2.1.0: @@ -12548,16 +12630,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.19.39): + jest-cli@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.19.39) + create-jest: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.19.39) + jest-config: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -12567,7 +12649,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.19.39): + jest-config@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 '@jest/test-sequencer': 29.7.0 @@ -12593,6 +12675,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.19.39 + ts-node: 10.9.2(@types/node@20.19.39)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -12834,12 +12917,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.19.39): + jest@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.19.39) + jest-cli: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -13178,6 +13261,8 @@ snapshots: dependencies: semver: 7.7.4 + make-error@1.3.6: {} + makeerror@1.0.12: dependencies: tmpl: 1.0.5 @@ -14542,6 +14627,24 @@ snapshots: ts-dedent@2.2.0: {} + ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.19.39 + acorn: 8.16.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + tsconfck@3.1.6(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -14750,6 +14853,8 @@ snapshots: uuid@9.0.1: {} + v8-compile-cache-lib@3.0.1: {} + v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -15154,6 +15259,8 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + yn@3.1.1: {} + yocto-queue@0.1.0: {} zod-validation-error@4.0.2(zod@4.3.6): diff --git a/scripts/check-exposed-globals.mjs b/scripts/check-exposed-globals.mjs new file mode 100644 index 00000000..aef1dfe0 --- /dev/null +++ b/scripts/check-exposed-globals.mjs @@ -0,0 +1,37 @@ +import { readFileSync, existsSync } from 'fs'; +import { glob } from 'glob'; + +const SENSITIVE_PATTERNS = [ + /__[A-Z][A-Z_]+__/g, +]; + +async function main() { + const files = await glob('src/**/*.{ts,tsx,js,jsx}', { + ignore: ['src/**/*.test.*', 'src/**/__tests__/**', 'node_modules/**'], + }); + + let hasError = false; + + for (const file of files) { + if (!existsSync(file)) continue; + const content = readFileSync(file, 'utf-8'); + const matches = content.match(SENSITIVE_PATTERNS[0]); + if (matches) { + for (const match of matches) { + console.error(`[FAIL] Found exposed global '${match}' in ${file}`); + hasError = true; + } + } + } + + if (hasError) { + process.exit(1); + } + + console.log('[PASS] No exposed globals found.'); +} + +main().catch((err) => { + console.error('Script failed:', err); + process.exit(1); +}); diff --git a/scripts/check-performance-budgets.mjs b/scripts/check-performance-budgets.mjs index 0bc7482a..50d13bd4 100644 --- a/scripts/check-performance-budgets.mjs +++ b/scripts/check-performance-budgets.mjs @@ -6,8 +6,9 @@ const nextDir = path.join(projectRoot, ".next"); const chunksDir = path.join(nextDir, "static", "chunks"); const buildManifestPath = path.join(nextDir, "build-manifest.json"); -const totalBudgetKb = Number(process.env.TOTAL_JS_BUDGET_KB || 650); -const chunkBudgetKb = Number(process.env.MAX_CHUNK_BUDGET_KB || 250); +const INITIAL_JS_BUDGET_KB = 220; +const WEB3_VENDORS_BUDGET_KB = 180; +const CSS_BUDGET_KB = 60; if (!fs.existsSync(chunksDir)) { console.error("Missing .next/static/chunks. Run `next build` first."); @@ -33,6 +34,8 @@ const manifest = fs.existsSync(buildManifestPath) : null; const initialFiles = new Set(); +const web3VendorFiles = new Set(); + if (manifest) { for (const file of [...(manifest.polyfillFiles || []), ...(manifest.rootMainFiles || [])]) { if (typeof file === "string" && file.endsWith(".js")) { @@ -46,44 +49,74 @@ if (manifest) { } } } -} - -let allBytes = 0; -for (const filePath of allChunkFiles) { - const size = fs.statSync(filePath).size; - allBytes += size; + for (const file of allChunkFiles) { + const rel = path.relative(nextDir, file).replace(/\\/g, "/"); + if (rel.includes("web3-vendors") || rel.includes("wagmi") || rel.includes("viem") || rel.includes("ethers")) { + web3VendorFiles.add(rel); + } + } } let initialBytes = 0; -const offenders = []; +const initialOffenders = []; for (const relativePath of initialFiles) { const filePath = path.join(nextDir, relativePath); if (!fs.existsSync(filePath)) continue; const size = fs.statSync(filePath).size; initialBytes += size; - if (size > chunkBudgetKb * 1024) { - offenders.push({ - file: relativePath, - kb: (size / 1024).toFixed(1), - }); + if (size > 100 * 1024) { + initialOffenders.push({ file: relativePath, kb: (size / 1024).toFixed(1) }); } } -const totalKb = initialBytes / 1024; -console.log(`Initial JS size (build manifest): ${totalKb.toFixed(1)} KB`); -console.log(`All chunk JS size: ${(allBytes / 1024).toFixed(1)} KB`); -console.log(`Budget: ${totalBudgetKb} KB`); +let web3Bytes = 0; +for (const rel of web3VendorFiles) { + const filePath = path.join(nextDir, rel); + if (!fs.existsSync(filePath)) continue; + web3Bytes += fs.statSync(filePath).size; +} + +const cssDir = path.join(nextDir, "static", "css"); +let cssBytes = 0; +if (fs.existsSync(cssDir)) { + const walk = (dir) => { + for (const item of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, item.name); + if (item.isDirectory()) walk(full); + else if (item.isFile() && item.name.endsWith(".css")) { + cssBytes += fs.statSync(full).size; + } + } + }; + walk(cssDir); +} + +const initialKb = initialBytes / 1024; +const web3Kb = web3Bytes / 1024; +const cssKb = cssBytes / 1024; + +console.log("Initial JS: " + initialKb.toFixed(1) + " KB (budget: " + INITIAL_JS_BUDGET_KB + " KB)"); +console.log("Web3-vendor JS: " + web3Kb.toFixed(1) + " KB (budget: " + WEB3_VENDORS_BUDGET_KB + " KB)"); +console.log("CSS: " + cssKb.toFixed(1) + " KB (budget: " + CSS_BUDGET_KB + " KB)"); let hasError = false; -if (totalKb > totalBudgetKb) { - console.error(`Total JS budget exceeded by ${(totalKb - totalBudgetKb).toFixed(1)} KB`); + +if (initialKb > INITIAL_JS_BUDGET_KB) { + console.error("FAIL: Initial JS exceeded by " + (initialKb - INITIAL_JS_BUDGET_KB).toFixed(1) + " KB"); hasError = true; } - -if (offenders.length > 0) { - console.error(`Chunks above ${chunkBudgetKb} KB:`); - for (const item of offenders) { - console.error(`- ${item.file}: ${item.kb} KB`); +if (web3Kb > WEB3_VENDORS_BUDGET_KB) { + console.error("FAIL: Web3 vendor JS exceeded by " + (web3Kb - WEB3_VENDORS_BUDGET_KB).toFixed(1) + " KB"); + hasError = true; +} +if (cssKb > CSS_BUDGET_KB) { + console.error("FAIL: CSS exceeded by " + (cssKb - CSS_BUDGET_KB).toFixed(1) + " KB"); + hasError = true; +} +if (initialOffenders.length > 0) { + console.error("Large initial chunks:"); + for (const item of initialOffenders) { + console.error(" - " + item.file + ": " + item.kb + " KB"); } hasError = true; } @@ -91,5 +124,4 @@ if (offenders.length > 0) { if (hasError) { process.exit(1); } - -console.log("Performance budgets passed."); +console.log("All performance budgets passed."); diff --git a/scripts/sort-package-json.mjs b/scripts/sort-package-json.mjs new file mode 100644 index 00000000..2e96a3ca --- /dev/null +++ b/scripts/sort-package-json.mjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node + +/** + * CI check: ensures package.json top-level keys, scripts, dependencies, and + * devDependencies are alphabetically sorted. + * + * Usage: + * node scripts/sort-package-json.mjs # check only (exit 1 if unsorted) + * node scripts/sort-package-json.mjs --fix # sort in-place and exit 0 + * + * Run as a CI gate to prevent noisy diffs from unsorted keys. + */ + +import { readFileSync, writeFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pkgPath = resolve(__dirname, '..', 'package.json'); +const shouldFix = process.argv.includes('--fix'); + +const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); + +/** + * Sort an object's keys alphabetically, preserving the original order for + * non-string keys (shouldn't exist in package.json, but be safe). + */ +function sortKeys(obj) { + if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj; + const sorted = {}; + const keys = Object.keys(obj).sort((a, b) => a.localeCompare(b, 'en')); + for (const key of keys) { + sorted[key] = obj[key]; + } + return sorted; +} + +// Fields to check for alphabetical sorting +const fieldsToCheck = ['scripts', 'dependencies', 'devDependencies']; +const fieldsToSkip = ['name', 'version', 'private', 'type']; // conventional top-level order + +/** Check if keys of an object are in alphabetical order */ +function isSorted(obj) { + const keys = Object.keys(obj); + for (let i = 1; i < keys.length; i++) { + if (keys[i].localeCompare(keys[i - 1], 'en') < 0) { + return { sorted: false, firstUnsorted: keys[i], previous: keys[i - 1] }; + } + } + return { sorted: true }; +} + +let hasErrors = false; + +for (const field of fieldsToCheck) { + if (!pkg[field]) continue; + const result = isSorted(pkg[field]); + if (!result.sorted) { + console.error( + `โŒ package.json โ†’ "${field}" keys are NOT sorted.\n` + + ` First unsorted key: "${result.firstUnsorted}" (comes after "${result.previous}")` + ); + hasErrors = true; + } +} + +if (hasErrors) { + if (shouldFix) { + console.log('\n๐Ÿ”ง Auto-fixing package.json key order...'); + for (const field of fieldsToCheck) { + if (pkg[field]) { + pkg[field] = sortKeys(pkg[field]); + } + } + writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8'); + console.log('โœ… package.json keys sorted successfully.'); + } else { + console.log( + '\n๐Ÿ’ก Run with --fix to auto-sort:\n' + + ' node scripts/sort-package-json.mjs --fix' + ); + process.exit(1); + } +} else { + console.log('โœ… package.json keys are properly sorted.'); +} diff --git a/scripts/validate-env.js b/scripts/validate-env.js index c6d18b5b..b5bc3e5f 100644 --- a/scripts/validate-env.js +++ b/scripts/validate-env.js @@ -26,6 +26,10 @@ function isValidUrl(val) { // Environment variable schema definition - using inline validators const envSchema = { + AUTH_SECRET: { + validate: (v) => typeof v === "string" && v.trim().length >= 32, + default: undefined, + }, NEXT_PUBLIC_APP_NAME: { validate: (v) => v && v.length > 0, default: "PropChain", @@ -38,6 +42,10 @@ const envSchema = { validate: (v) => ["development", "staging", "production"].includes(v), default: "development", }, + CSRF_SECRET: { + validate: (v) => typeof v === "string" && v.length > 0, + default: undefined, + }, ANALYZE: { validate: (v) => !v || v === "true" || v === "false", default: "false", @@ -111,19 +119,24 @@ const envSchema = { // Environment-specific requirements const envRequirements = { development: { - ETHEREUM_MAINNET_RPC_URL: {validate: (v) => !v || isValidUrl(v)}, + AUTH_SECRET: { + validate: (v) => typeof v === "string" && v.trim().length >= 32, + }, + ETHEREUM_MAINNET_RPC_URL: { validate: (v) => !v || isValidUrl(v) }, }, staging: { - ETHEREUM_MAINNET_RPC_URL: {validate: isValidUrl}, + ETHEREUM_MAINNET_RPC_URL: { validate: isValidUrl }, NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: { validate: (v) => typeof v === "string", }, }, production: { - ETHEREUM_MAINNET_RPC_URL: {validate: isValidUrl}, - POLYGON_MAINNET_RPC_URL: {validate: isValidUrl}, - BSC_MAINNET_RPC_URL: {validate: isValidUrl}, - NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: {validate: (v) => v && v.length > 0}, + ETHEREUM_MAINNET_RPC_URL: { validate: isValidUrl }, + POLYGON_MAINNET_RPC_URL: { validate: isValidUrl }, + BSC_MAINNET_RPC_URL: { validate: isValidUrl }, + NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: { + validate: (v) => v && v.length > 0, + }, }, }; @@ -228,6 +241,9 @@ async function main() { ` - Mock Data: ${transformBool(config.NEXT_PUBLIC_USE_MOCK_DATA) ? "Enabled" : "Disabled"}`, ); console.log("\nWeb3 Configuration:"); + console.log( + ` - AUTH_SECRET: ${config.AUTH_SECRET ? "Configured" : "Not set"}`, + ); console.log( ` - Ethereum RPC: ${config.ETHEREUM_MAINNET_RPC_URL ? "Configured" : "Using default"}`, ); diff --git a/src/.husky/pre-push b/src/.husky/pre-push new file mode 100644 index 00000000..19d15a3f --- /dev/null +++ b/src/.husky/pre-push @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +. "$(dirname "$0")/_/husky.sh" + +echo "==========================================================" +echo "๐Ÿš€ Husky Gatekeeper: Running pre-push typecheck & tests..." +echo "==========================================================" + +# 1. Execute strict compile-time type verification across the frontend workspace +echo "๐Ÿ“ฆ Validating frontend type-safety compliance..." +cd frontend && npm run typecheck +if [ $? -ne 0 ]; then + echo "โŒ Error: TypeScript compilation compilation targets failed. Push blocked." + exit 1 +fi +cd .. + +# 2. Execute local unit test suites to guard against regression breaks +echo "๐Ÿงช Running localized unit tests..." +npm run test -- --watchAll=false --passWithNoTests +if [ $? -ne 0 ]; then + echo "โŒ Error: Unit testing suite encountered failures. Push blocked." + exit 1 +fi + +echo "โœ… Success: All gates cleared. Safe to push upstream." +exit 0 \ No newline at end of file diff --git a/src/__tests__/middleware.test.ts b/src/__tests__/middleware.test.ts new file mode 100644 index 00000000..16f9ce09 --- /dev/null +++ b/src/__tests__/middleware.test.ts @@ -0,0 +1,218 @@ +/** + * Tests for src/middleware.ts + * Covers CSP header generation, nonce uniqueness, and API-path skip behaviour. + * Issue #936 + */ + +import type { NextRequest } from 'next/server'; + +// Track all headers set on responses across module resets +let capturedHeaders: Map; +// Track calls to NextResponse.next across module resets +let nextCalls: unknown[][]; + +jest.mock('next/server', () => { + capturedHeaders = new Map(); + nextCalls = []; + return { + NextResponse: { + next: jest.fn((...args: unknown[]) => { + nextCalls.push(args); + return { + headers: { + set: jest.fn((key: string, value: string) => { + capturedHeaders.set(key, value); + }), + get: jest.fn((key: string) => capturedHeaders.get(key)), + }, + }; + }), + }, + }; +}); + +jest.mock('@/lib/initRedisCache', () => ({ + initRedisCacheSystem: jest.fn().mockResolvedValue(undefined), +})); + +jest.mock('@/utils/logger', () => ({ + logger: { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }, +})); + +// Helper to create a mock NextRequest with proper headers +function createMockRequest( + pathname: string, + acceptHeader = 'text/html', +): NextRequest { + const headersObj: Record = {}; + if (acceptHeader) { + headersObj['accept'] = acceptHeader; + } + + return { + nextUrl: { pathname }, + headers: { + get: (name: string) => headersObj[name] ?? null, + forEach: (cb: (value: string, key: string) => void) => { + Object.entries(headersObj).forEach(([k, v]) => cb(v, k)); + }, + entries: () => Object.entries(headersObj)[Symbol.iterator](), + [Symbol.iterator]: () => Object.entries(headersObj)[Symbol.iterator](), + }, + } as unknown as NextRequest; +} + +// Reset state before each test +function resetState() { + capturedHeaders = new Map(); + nextCalls = []; +} + +describe('middleware CSP enforcement', () => { + const originalEnv = process.env; + + beforeEach(() => { + resetState(); + process.env = { ...originalEnv }; + process.env.NODE_ENV = 'production'; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + it('returns NextResponse.next() when CSP_ENFORCE is not true', async () => { + process.env.CSP_ENFORCE = 'false'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + expect(nextCalls.length).toBeGreaterThan(0); + expect(capturedHeaders.has('Content-Security-Policy')).toBe(false); + }); + + it('adds CSP header and nonce for HTML requests when CSP_ENFORCE=true', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + expect(capturedHeaders.has('Content-Security-Policy')).toBe(true); + expect(capturedHeaders.get('Content-Security-Policy')).toContain( + "default-src 'self'", + ); + }); + + it('skips CSP for API routes', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/api/csp-report')); + + expect(capturedHeaders.has('Content-Security-Policy')).toBe(false); + expect(nextCalls.length).toBeGreaterThan(0); + }); + + it('skips CSP for non-HTML accept headers', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/page', 'application/json')); + + expect(capturedHeaders.has('Content-Security-Policy')).toBe(false); + expect(nextCalls.length).toBeGreaterThan(0); + }); + + it('generates unique nonces for different requests', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + + const nonces = new Set(); + + for (let i = 0; i < 20; i++) { + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + const cspHeader = capturedHeaders.get('Content-Security-Policy'); + if (cspHeader) { + const nonceMatch = cspHeader.match(/nonce-([A-Za-z0-9+/=]+)/); + if (nonceMatch) nonces.add(nonceMatch[1]); + } + } + + expect(nonces.size).toBeGreaterThan(1); + }); + + it('includes upgrade-insecure-requests in production', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + const cspHeader = capturedHeaders.get('Content-Security-Policy'); + expect(cspHeader).toContain('upgrade-insecure-requests'); + }); + + it('does not include unsafe-eval in production CSP', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + const cspHeader = capturedHeaders.get('Content-Security-Policy'); + expect(cspHeader).not.toContain("'unsafe-eval'"); + }); + + it('includes unsafe-eval in development CSP', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'development'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + const cspHeader = capturedHeaders.get('Content-Security-Policy'); + expect(cspHeader).toContain("'unsafe-eval'"); + }); + + it('sets x-nonce header on the request', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + // The x-nonce should be set on the request headers (passed to NextResponse.next) + expect(nextCalls.length).toBeGreaterThan(0); + const callArgs = nextCalls[0]; + expect(callArgs[0]).toHaveProperty('request'); + }); +}); diff --git a/src/app/api/cache/stats/route.ts b/src/app/api/cache/stats/route.ts index 617cf1c1..febf4512 100644 --- a/src/app/api/cache/stats/route.ts +++ b/src/app/api/cache/stats/route.ts @@ -4,6 +4,7 @@ */ import { NextRequest, NextResponse } from 'next/server'; +import { withCsrf } from '@/lib/csrf'; import { redisCacheService } from '@/lib/redisCache'; import { getRedisInfo, testRedisConnection } from '@/lib/redis'; import { logger } from '@/utils/logger'; @@ -47,7 +48,7 @@ export async function GET(request: NextRequest) { if (detailed && redisInfo) { // Add relevant Redis metrics - (response as any).redisMetrics = { + const redisMetrics = { usedMemory: redisInfo.used_memory_human, usedMemoryRss: redisInfo.used_memory_rss_human, usedMemoryPeak: redisInfo.used_memory_peak_human, @@ -57,6 +58,7 @@ export async function GET(request: NextRequest) { keyspaceMisses: redisInfo.keyspace_misses, uptimeInSeconds: redisInfo.uptime_in_seconds, }; + return NextResponse.json({ ...response, redisMetrics }); } return NextResponse.json(response); @@ -70,7 +72,7 @@ export async function GET(request: NextRequest) { } // DELETE handler to clear cache statistics -export async function DELETE(request: NextRequest) { +export const DELETE = withCsrf(async function (request: NextRequest) { try { await redisCacheService.clearStats(); @@ -87,4 +89,5 @@ export async function DELETE(request: NextRequest) { { status: 500 } ); } -} +}); + diff --git a/src/app/api/errors/route.test.ts b/src/app/api/errors/route.test.ts new file mode 100644 index 00000000..fb4c955d --- /dev/null +++ b/src/app/api/errors/route.test.ts @@ -0,0 +1,92 @@ +import { NextRequest } from 'next/server'; +import type { ErrorReportingData } from '@/types/errors'; + +jest.mock('@/lib/rateLimit', () => ({ + withRateLimit: (handler: T): T => handler, +})); + +jest.mock('@/lib/csrf', () => ({ + withCsrf: (handler: T): T => handler, +})); + +jest.mock('@/utils/logger', () => ({ + logger: { + error: jest.fn(), + }, +})); + +const fetchMock = jest.fn, Parameters>(); +const endpoint = 'https://errors.example.com/reports'; + +const validReport: ErrorReportingData = { + errorId: 'error-123', + category: 'ui', + severity: 'medium', + message: 'A component failed to render', + userAgent: 'Jest', + url: 'https://propchain.example.com/dashboard', + timestamp: '2026-08-26T00:00:00.000Z', + sessionId: 'session-123', +}; + +function createRequest(body: unknown): NextRequest { + return new NextRequest('http://localhost/api/errors', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +async function post(body: unknown) { + const { POST } = await import('./route'); + return POST(createRequest(body)); +} + +describe('POST /api/errors', () => { + beforeEach(() => { + jest.resetModules(); + process.env.ERROR_REPORTING_ENDPOINT = endpoint; + delete process.env.ERROR_REPORTING_API_KEY; + fetchMock.mockReset(); + globalThis.fetch = fetchMock; + }); + + afterEach(() => { + delete process.env.ERROR_REPORTING_ENDPOINT; + delete process.env.ERROR_REPORTING_API_KEY; + }); + + it('forwards a valid report to the configured destination', async () => { + fetchMock.mockResolvedValue({ ok: true, status: 202 } as Response); + + const response = await post(validReport); + + expect(response.status).toBe(200); + expect(fetchMock).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(validReport), + }), + ); + }); + + it('rejects reports missing required fields', async () => { + const response = await post({ errorId: validReport.errorId }); + + expect(response.status).toBe(400); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('returns a service-unavailable response when forwarding fails', async () => { + fetchMock.mockRejectedValue(new Error('destination unavailable')); + + const response = await post(validReport); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toEqual({ + error: 'Unable to persist error report', + }); + }); +}); diff --git a/src/app/api/errors/route.ts b/src/app/api/errors/route.ts index 12ac2508..4fb7e09b 100644 --- a/src/app/api/errors/route.ts +++ b/src/app/api/errors/route.ts @@ -3,20 +3,65 @@ import { NextResponse } from 'next/server'; import type { ErrorReportingData } from '@/types/errors'; import { logger } from '@/utils/logger'; import { withRateLimit } from '@/lib/rateLimit'; +import { withCsrf } from '@/lib/csrf'; + +const ERROR_REPORTING_TIMEOUT_MS = 5_000; + +function isValidErrorReport(body: unknown): body is ErrorReportingData { + if (typeof body !== 'object' || body === null) { + return false; + } + + const report = body as Partial; + return [report.errorId, report.category, report.message].every( + (value) => typeof value === 'string' && value.trim().length > 0, + ); +} + +async function forwardErrorReport(report: ErrorReportingData): Promise { + const endpoint = process.env.ERROR_REPORTING_ENDPOINT; + if (!endpoint) { + throw new Error('ERROR_REPORTING_ENDPOINT is not configured'); + } + + const headers: Record = { + 'Content-Type': 'application/json', + }; + const apiKey = process.env.ERROR_REPORTING_API_KEY; + if (apiKey) { + headers.Authorization = `Bearer ${apiKey}`; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), ERROR_REPORTING_TIMEOUT_MS); + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers, + body: JSON.stringify(report), + signal: controller.signal, + }); + + if (!response.ok) { + throw new Error(`Error reporting destination returned HTTP ${response.status}`); + } + } finally { + clearTimeout(timeout); + } +} async function handleErrorsPost(request: NextRequest) { try { - const body: ErrorReportingData = await request.json(); - - // Validate required fields - if (!body.errorId || !body.category || !body.message) { + const body: unknown = await request.json(); + + if (!isValidErrorReport(body)) { return NextResponse.json( { error: 'Missing required fields' }, - { status: 400 } + { status: 400 }, ); } - // Log error (in production, this would go to your analytics service) logger.error('Error Report:', { id: body.errorId, category: body.category, @@ -28,33 +73,36 @@ async function handleErrorsPost(request: NextRequest) { context: body.context, }); - // Store in database (placeholder for actual implementation) - // await db.errors.create({ ...body }); - - // Send to external service (placeholder for actual implementation) - // await analyticsService.trackError(body); + try { + await forwardErrorReport(body); + } catch (error) { + logger.error('Error report forwarding failed:', error); + return NextResponse.json( + { error: 'Unable to persist error report' }, + { status: 503 }, + ); + } return NextResponse.json( { success: true, message: 'Error reported successfully' }, - { status: 200 } + { status: 200 }, ); - } catch (error) { logger.error('Error reporting failed:', error); return NextResponse.json( { error: 'Internal server error' }, - { status: 500 } + { status: 500 }, ); } } // Export the rate-limited POST handler -export const POST = withRateLimit(handleErrorsPost); +export const POST = withRateLimit(withCsrf(handleErrorsPost)); async function handleErrorsGet() { return NextResponse.json( { message: 'Error reporting endpoint. Use POST to report errors.' }, - { status: 200 } + { status: 200 }, ); } diff --git a/src/app/api/properties/[id]/route.ts b/src/app/api/properties/[id]/route.ts index 118f4134..24c10697 100644 --- a/src/app/api/properties/[id]/route.ts +++ b/src/app/api/properties/[id]/route.ts @@ -4,6 +4,7 @@ */ import { NextRequest, NextResponse } from 'next/server'; +import { withCsrf } from '@/lib/csrf'; import { propertyService } from '@/lib/propertyService'; import { redisCacheService } from '@/lib/redisCache'; import { logger } from '@/utils/logger'; @@ -84,7 +85,7 @@ export async function GET( } // PUT handler for updating property (invalidates cache) -export async function PUT( +export const PUT = withCsrf(async function ( request: NextRequest, { params }: RouteParams ) { @@ -119,10 +120,10 @@ export async function PUT( { status: 500 } ); } -} +}); // DELETE handler for deleting property (invalidates cache) -export async function DELETE( +export const DELETE = withCsrf(async function ( request: NextRequest, { params }: RouteParams ) { @@ -156,4 +157,4 @@ export async function DELETE( { status: 500 } ); } -} +}); diff --git a/src/app/api/properties/route.ts b/src/app/api/properties/route.ts index b56aa1b7..7c999dbb 100644 --- a/src/app/api/properties/route.ts +++ b/src/app/api/properties/route.ts @@ -4,6 +4,7 @@ */ import { NextRequest, NextResponse } from 'next/server'; +import { withCsrf } from '@/lib/csrf'; import { propertyService } from '@/lib/propertyService'; import { redisCacheService } from '@/lib/redisCache'; import { logger } from '@/utils/logger'; @@ -92,7 +93,7 @@ export async function GET(request: NextRequest) { } // POST handler for creating/updating properties (invalidates cache) -export async function POST(request: NextRequest) { +export const POST = withCsrf(async function (request: NextRequest) { try { const propertyData = await request.json(); @@ -115,4 +116,5 @@ export async function POST(request: NextRequest) { { status: 500 } ); } -} +}); + diff --git a/src/app/api/revalidate/route.ts b/src/app/api/revalidate/route.ts index 732a3b76..930a2eea 100644 --- a/src/app/api/revalidate/route.ts +++ b/src/app/api/revalidate/route.ts @@ -1,13 +1,20 @@ import { logger } from '@/utils/logger'; import { NextRequest, NextResponse } from 'next/server'; import { revalidateProperty, revalidateAllProperties } from '@/lib/propertyServiceServer'; +import { requireEnvStrict } from '@/lib/requireEnv'; import crypto from 'crypto'; -// Webhook secret for security - should be stored in environment variables -const WEBHOOK_SECRET = process.env.REVALIDATE_WEBHOOK_SECRET || 'your-webhook-secret'; +const WEBHOOK_SECRET: string | undefined = process.env.REVALIDATE_WEBHOOK_SECRET; export async function POST(request: NextRequest) { try { + if (!WEBHOOK_SECRET) { + return NextResponse.json( + { error: 'REVALIDATE_WEBHOOK_SECRET is not configured' }, + { status: 500 } + ); + } + // Verify webhook signature for security const signature = request.headers.get('x-webhook-signature'); const body = await request.text(); diff --git a/src/app/api/security/address-check/route.ts b/src/app/api/security/address-check/route.ts new file mode 100644 index 00000000..e934afe4 --- /dev/null +++ b/src/app/api/security/address-check/route.ts @@ -0,0 +1,54 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const address = searchParams.get('address')?.trim(); + + if (!address) { + return NextResponse.json({ error: 'Address parameter required' }, { status: 400 }); + } + + if (!/^0x[a-fA-F0-9]{40}$/.test(address)) { + return NextResponse.json({ error: 'Invalid Ethereum address' }, { status: 400 }); + } + + const apiKey = process.env.CHAINALYSIS_API_KEY; + + if (!apiKey) { + return NextResponse.json({ + address, + risk_score: 50, + categories: ['unknown'], + description: 'Risk check unavailable (service not configured)', + }); + } + + try { + const response = await fetch( + `https://api.chainalysis.com/api/v2/address/${address}`, + { + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + signal: AbortSignal.timeout(10000), + } + ); + + if (!response.ok) { + const errorText = await response.text(); + return NextResponse.json( + { error: `Upstream service error: ${response.status}`, detail: errorText }, + { status: response.status } + ); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 502 } + ); + } +} diff --git a/src/app/api/security/csrf/__tests__/route.test.ts b/src/app/api/security/csrf/__tests__/route.test.ts new file mode 100644 index 00000000..4b07f529 --- /dev/null +++ b/src/app/api/security/csrf/__tests__/route.test.ts @@ -0,0 +1,72 @@ +import { GET } from '../route'; + +jest.mock('@/lib/csrf', () => ({ + getCsrfSessionId: jest.fn(), + getAuthStatePart: jest.fn(), + generateTokenForSession: jest.fn(), +})); + +import { getCsrfSessionId, getAuthStatePart, generateTokenForSession } from '@/lib/csrf'; + +function makeRequest(cookieValues: Record = {}) { + const cookieStore = new Map(Object.entries(cookieValues)); + return { + cookies: { + get: (name: string) => (cookieStore.has(name) ? { value: cookieStore.get(name) } : undefined), + }, + } as any; +} + +describe('CSRF route handler', () => { + beforeEach(() => { + jest.clearAllMocks(); + (getCsrfSessionId as jest.Mock).mockReturnValue({ sessionId: 'existing-session', isNew: false }); + (getAuthStatePart as jest.Mock).mockReturnValue('auth-state'); + (generateTokenForSession as jest.Mock).mockReturnValue('generated-token-123'); + }); + + it('returns 200 with token', async () => { + const request = makeRequest(); + const response = await GET(request); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ csrfToken: 'generated-token-123' }); + expect(generateTokenForSession).toHaveBeenCalledWith('existing-session', 'auth-state'); + }); + + it('sets cookie for new session', async () => { + (getCsrfSessionId as jest.Mock).mockReturnValue({ sessionId: 'new-uuid-session', isNew: true }); + + const request = makeRequest(); + const response = await GET(request); + + const setCookieHeader = response.headers.get('set-cookie'); + expect(setCookieHeader).toContain('csrf-session=new-uuid-session'); + expect(setCookieHeader).toContain('HttpOnly'); + expect(setCookieHeader).toContain('SameSite=Lax'); + }); + + it('does not set cookie for existing session', async () => { + (getCsrfSessionId as jest.Mock).mockReturnValue({ sessionId: 'existing-session', isNew: false }); + + const request = makeRequest({ 'csrf-session': 'existing-session' }); + const response = await GET(request); + + const setCookieHeader = response.headers.get('set-cookie'); + expect(setCookieHeader).toBeNull(); + }); + + it('returns existing token for valid session', async () => { + (getCsrfSessionId as jest.Mock).mockReturnValue({ sessionId: 'valid-session', isNew: false }); + (generateTokenForSession as jest.Mock).mockReturnValue('session-specific-token'); + + const request = makeRequest({ 'csrf-session': 'valid-session' }); + const response = await GET(request); + const body = await response.json(); + + expect(body.csrfToken).toBe('session-specific-token'); + expect(getCsrfSessionId).toHaveBeenCalledWith(request); + expect(getAuthStatePart).toHaveBeenCalledWith(request); + }); +}); diff --git a/src/app/api/security/csrf/route.ts b/src/app/api/security/csrf/route.ts new file mode 100644 index 00000000..d4c31e81 --- /dev/null +++ b/src/app/api/security/csrf/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getCsrfSessionId, getAuthStatePart, generateTokenForSession } from '@/lib/csrf'; + +export async function GET(request: NextRequest) { + const { sessionId, isNew } = getCsrfSessionId(request); + const authState = getAuthStatePart(request); + const token = generateTokenForSession(sessionId, authState); + + const response = NextResponse.json({ csrfToken: token }); + + if (isNew) { + response.cookies.set('csrf-session', sessionId, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + }); + } + + return response; +} diff --git a/src/app/api/simulate/route.ts b/src/app/api/simulate/route.ts new file mode 100644 index 00000000..feec1135 --- /dev/null +++ b/src/app/api/simulate/route.ts @@ -0,0 +1,77 @@ +import { NextRequest, NextResponse } from "next/server"; +import { logger } from "@/utils/logger"; + +const TENDERLY_API_KEY = process.env.TENDERLY_API_KEY; +const TENDERLY_PROJECT_SLUG = process.env.TENDERLY_PROJECT_SLUG; +const TENDERLY_USER = process.env.TENDERLY_USER; + +if (!TENDERLY_API_KEY || !TENDERLY_PROJECT_SLUG || !TENDERLY_USER) { + logger.warn( + "Tenderly environment variables are not set. Simulation API will not work.", + ); +} + +export async function POST(req: NextRequest) { + if (!TENDERLY_API_KEY || !TENDERLY_PROJECT_SLUG || !TENDERLY_USER) { + return NextResponse.json( + { error: "Tenderly not configured" }, + { status: 500 }, + ); + } + + try { + const txRequest = await req.json(); + logger.info("Received simulation request", { + to: txRequest.to, + value: txRequest.value, + }); + + const tenderlyApiUrl = `https://api.tenderly.co/api/v1/account/${TENDERLY_USER}/project/${TENDERLY_PROJECT_SLUG}/simulate`; + + const simResponse = await fetch(tenderlyApiUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Tenderly-Access-Key": TENDERLY_API_KEY, + }, + body: JSON.stringify({ + network_id: "1", + from: txRequest.from, + to: txRequest.to, + input: txRequest.data || "0x", + gas: txRequest.gasLimit ? parseInt(txRequest.gasLimit, 16) : 100000, + gas_price: txRequest.gasPrice + ? parseInt(txRequest.gasPrice, 16).toString() + : "0", + value: txRequest.value ? parseInt(txRequest.value, 16).toString() : "0", + save: true, + save_if_fails: true, + }), + }); + + if (!simResponse.ok) { + const errorBody = await simResponse.text(); + logger.error("Tenderly simulation failed", { + status: simResponse.status, + error: errorBody, + }); + return NextResponse.json( + { error: "Tenderly simulation failed", details: errorBody }, + { status: simResponse.status }, + ); + } + + const simData = await simResponse.json(); + logger.info("Tenderly simulation successful", { + transactionHash: simData.transaction.hash, + }); + + return NextResponse.json(simData); + } catch (error) { + logger.error("Error in /api/simulate:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/transactions/route.ts b/src/app/api/transactions/route.ts new file mode 100644 index 00000000..5d4af90d --- /dev/null +++ b/src/app/api/transactions/route.ts @@ -0,0 +1,17 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getMockApiTransactions } from '@/lib/mockTransactionData'; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const walletAddress = searchParams.get('walletAddress')?.trim(); + + if (!walletAddress) { + return NextResponse.json({ error: 'Wallet address required' }, { status: 400 }); + } + + if (walletAddress.length < 6) { + return NextResponse.json({ error: 'Invalid wallet address' }, { status: 400 }); + } + + return NextResponse.json(getMockApiTransactions()); +} diff --git a/src/app/compare/page.tsx b/src/app/compare/page.tsx index 0ceaacf2..2607f281 100644 --- a/src/app/compare/page.tsx +++ b/src/app/compare/page.tsx @@ -7,6 +7,7 @@ import { ArrowLeft, Share2, Download, Clock, Trash2, FileText } from 'lucide-rea import { propertyService } from '@/lib/propertyService'; import { useComparisonHistoryStore } from '@/store/comparisonHistoryStore'; import { useComparisonStore } from '@/store/comparisonStore'; +import { withRouteErrorBoundary } from '@/components/error/withRouteErrorBoundary'; import type { Property } from '@/types/property'; import { formatPrice, formatROI } from '@/utils/searchUtils'; @@ -93,7 +94,7 @@ function getBestValue(properties: Property[], metric: ComparisonMetric): number return metric.higherIsBetter ? Math.max(...numericValues) : Math.min(...numericValues); } -export default function ComparePage() { +function ComparePage() { const router = useRouter(); const searchParams = useSearchParams(); const { selectedProperties, clearProperties } = useComparisonStore(); @@ -150,45 +151,111 @@ export default function ComparePage() { const printWindow = window.open('', '_blank'); if (!printWindow) return; - const html = ` - - - - Property Comparison - - - -

Property Comparison Report

-

Generated on ${new Date().toLocaleDateString()}

- - - - - ${properties.map(p => ``).join('')} - - - - ${comparisonMetrics.map(metric => ` - - - ${properties.map(p => ``).join('')} - - `).join('')} - -
Metric${p.name}
${metric.label}${metric.format(getNestedValue(p, metric.key), p)}
- - + // Build document via safe DOM APIs instead of document.write to avoid + // CSP bypass and script re-execution risks. + const doc = printWindow.document; + + // Create title + const title = doc.createElement('title'); + title.textContent = 'Property Comparison'; + doc.head.appendChild(title); + + // Create styles + const doc = printWindow.document; + doc.open(); + doc.write(''); + + const html = doc.createElement('html'); + + const head = doc.createElement('head'); + const title = doc.createElement('title'); + title.textContent = 'Property Comparison'; + head.appendChild(title); + const style = doc.createElement('style'); + style.textContent = ` + body { font-family: Arial, sans-serif; padding: 20px; } + table { width: 100%; border-collapse: collapse; margin-top: 20px; } + th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } + th { background-color: #f5f5f5; font-weight: bold; } + h1 { color: #333; } + @media print { body { padding: 0; } } `; + doc.head.appendChild(style); + + // Build body + const h1 = doc.createElement('h1'); + h1.textContent = 'Property Comparison Report'; + doc.body.appendChild(h1); - printWindow.document.write(html); - printWindow.document.close(); + const dateP = doc.createElement('p'); + dateP.textContent = `Generated on ${new Date().toLocaleDateString()}`; + doc.body.appendChild(dateP); + + // Build table + const table = doc.createElement('table'); + head.appendChild(style); + html.appendChild(head); + + const body = doc.createElement('body'); + const h1 = doc.createElement('h1'); + h1.textContent = 'Property Comparison Report'; + body.appendChild(h1); + + const p = doc.createElement('p'); + p.textContent = `Generated on ${new Date().toLocaleDateString()}`; + body.appendChild(p); + + const table = doc.createElement('table'); + + const thead = doc.createElement('thead'); + const headerRow = doc.createElement('tr'); + const metricTh = doc.createElement('th'); + metricTh.textContent = 'Metric'; + headerRow.appendChild(metricTh); + properties.forEach(p => { + const th = doc.createElement('th'); + th.textContent = p.name; + properties.forEach(prop => { + const th = doc.createElement('th'); + th.textContent = prop.name; + headerRow.appendChild(th); + }); + thead.appendChild(headerRow); + table.appendChild(thead); + + const tbody = doc.createElement('tbody'); + comparisonMetrics.forEach(metric => { + const row = doc.createElement('tr'); + const labelTd = doc.createElement('td'); + labelTd.textContent = metric.label; + row.appendChild(labelTd); + properties.forEach(p => { + const td = doc.createElement('td'); + td.textContent = metric.format(getNestedValue(p, metric.key), p); + row.appendChild(td); + + const tbody = doc.createElement('tbody'); + comparisonMetrics.forEach(metric => { + const row = doc.createElement('tr'); + const labelCell = doc.createElement('td'); + labelCell.textContent = metric.label; + row.appendChild(labelCell); + properties.forEach(prop => { + const cell = doc.createElement('td'); + cell.textContent = metric.format(getNestedValue(prop, metric.key), prop); + row.appendChild(cell); + }); + tbody.appendChild(row); + }); + table.appendChild(tbody); + doc.body.appendChild(table); + + // Close the document stream so browsers finish rendering before printing + body.appendChild(table); + html.appendChild(body); + + doc.documentElement.replaceWith(html); + doc.close(); printWindow.print(); }; @@ -403,3 +470,5 @@ export default function ComparePage() { ); } + +export default withRouteErrorBoundary(ComparePage, { routeName: 'compare' }); diff --git a/src/app/dashboard/DashboardEmptyState.tsx b/src/app/dashboard/DashboardEmptyState.tsx new file mode 100644 index 00000000..1cc5473e --- /dev/null +++ b/src/app/dashboard/DashboardEmptyState.tsx @@ -0,0 +1,36 @@ +import Link from "next/link"; +import { Button } from "@/components/ui/button"; + +export function DashboardEmptyState() { + return ( +
+

+ Welcome to your dashboard +

+ +

+ You don't have any portfolio activity yet. + Connect your wallet, browse properties, + or take a quick tour to get started. +

+ +
+ + + + + +
+
+ ); +} \ No newline at end of file diff --git a/src/app/dashboard/__tests__/page.test.tsx b/src/app/dashboard/__tests__/page.test.tsx new file mode 100644 index 00000000..5ca47e8f --- /dev/null +++ b/src/app/dashboard/__tests__/page.test.tsx @@ -0,0 +1,168 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +jest.mock('next/dynamic', () => { + const dynamic = (factory: () => Promise<{ default: React.ComponentType }>, _options?: Record) => { + const name = factory.toString().match(/import\("@\/components\/dashboard\/(\w+)"\)/)?.[1] || 'Widget'; + const Stub = ({ children, ...props }: { children?: React.ReactNode; [key: string]: unknown }) => ( +
{children}
+ ); + Stub.displayName = `Dynamic${name}`; + return React.forwardRef>((props, ref) => ); + }; + return { __esModule: true, default: dynamic }; +}); + +jest.mock('next/image', () => ({ + __esModule: true, + default: (props: Record) => { + // eslint-disable-next-line @next/next/no-img-element + return )} alt={String(props.alt || '')} />; + }, +})); + +jest.mock('@/store/walletStore', () => ({ + useWalletStore: () => ({ + address: '0x1234...5678', + isConnected: true, + chainId: 1, + }), +})); + +jest.mock('@/store/kycStore', () => ({ + useKycStore: () => ({ + profile: { status: 'verified', thresholdEth: 0 }, + }), +})); + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, fallback?: string) => fallback || key, + }), +})); + +jest.mock('@/components/dashboard/Sidebar', () => ({ + Sidebar: (props: Record) =>