diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 00000000..392ac6e5 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,21 @@ +{ + "projectName": "learnvault", + "projectOwner": "bakeronchain", + "repoType": "github", + "repoHost": "https://github.com", + "files": ["README.md"], + "imageSize": 100, + "commit": true, + "commitConvention": "angular", + "contributors": [ + { + "login": "bakeronchain", + "name": "bakeronchain", + "avatar_url": "https://avatars.githubusercontent.com/u/242071730?v=4", + "profile": "https://github.com/bakeronchain", + "contributions": ["code", "doc"] + } + ], + "contributorsPerLine": 7, + "linkToUsage": true +} diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 00000000..2e8dcdbd --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,15 @@ +# cargo-audit configuration +# Ignore transitive advisories from Soroban/Stellar ecosystem crates +# that cannot be resolved without upstream fixes. +# These are all INFO-level "unmaintained" notices, not exploitable vulnerabilities. + +[advisories] +ignore = [ + # derivative 2.2.0 — unmaintained, used transitively by soroban-sdk + "RUSTSEC-2024-0388", + # paste 1.0.x — unmaintained, used transitively by soroban-sdk + "RUSTSEC-2024-0436", + # rand 0.8/0.9 — unsoundness with custom logger, used transitively by soroban-sdk + # Not exploitable in contract context (no custom global logger) + "RUSTSEC-2026-0097", +] diff --git a/.codex b/.codex new file mode 100644 index 00000000..e69de29b diff --git a/.env.example b/.env.example index 74c241eb..a797d36c 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ +<<<<<<< feat/empty-and-error-states +ADMIN_EMAILS=admin@example.com # comma-separated list of admin emails # The environment to use `development`, `testing`, `staging`, `production` STELLAR_SCAFFOLD_ENV=development @@ -26,3 +28,73 @@ PUBLIC_STELLAR_HORIZON_URL="http://localhost:8000" # PUBLIC_STELLAR_NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" # PUBLIC_STELLAR_RPC_URL= # PUBLIC_STELLAR_HORIZON_URL= + +# V1 Contract IDs (deployed to Testnet) +# These will be populated after running ./scripts/deploy-testnet.sh + +# Core Platform Contracts +VITE_LEARN_TOKEN_CONTRACT_ID="" +VITE_GOVERNANCE_TOKEN_CONTRACT_ID="" + +# Scholarship System Contracts +VITE_COURSE_MILESTONE_CONTRACT_ID="" +VITE_MILESTONE_ESCROW_CONTRACT_ID="" +VITE_SCHOLARSHIP_TREASURY_CONTRACT_ID="" +VITE_SCHOLAR_NFT_CONTRACT_ID="" + +# Token Contract +PUBLIC_USDC_CONTRACT_ID="" # USDC token contract address (testnet or mainnet) + +# Optional: Friendbot-funded deployer address for testing +# PUBLIC_DEPLOYER_ADDRESS="" + +# Backend API URL +VITE_SERVER_URL=http://localhost:4000 +VITE_API_URL= +RESEND_API_KEY= +EMAIL_FROM=notifications@learnvault.xyz +FRONTEND_URL=http://localhost:3000 +======= +# LearnVault environment template for local development. Copy this file to `.env`. + +# Scaffold / local tooling +STELLAR_SCAFFOLD_ENV=development # Scaffold Stellar environment profile used by local scripts and CLI commands. +XDG_CONFIG_HOME=.config # Location where the Stellar CLI stores config files and identities on your machine. + +# Stellar network +PUBLIC_STELLAR_NETWORK=TESTNET # Frontend Stellar network used by wallet and contract helpers. +PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" # Stellar Testnet passphrase required by the frontend contract client. +PUBLIC_STELLAR_RPC_URL=https://soroban-testnet.stellar.org # Public Soroban RPC endpoint for Stellar Testnet. +PUBLIC_STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org # Public Horizon endpoint for Stellar Testnet. + +# LearnVault contract IDs +VITE_LEARN_TOKEN_CONTRACT_ID= # LearnToken contract ID set after deployment. +VITE_GOVERNANCE_TOKEN_CONTRACT_ID= # Governance token contract ID set after deployment. +VITE_SCHOLAR_NFT_CONTRACT_ID= # Scholar NFT contract ID set after deployment. +VITE_COURSE_MILESTONE_CONTRACT_ID= # Course milestone contract ID set after deployment. +VITE_SCHOLARSHIP_TREASURY_CONTRACT_ID= # Scholarship treasury contract ID set after deployment. +VITE_MILESTONE_ESCROW_CONTRACT_ID= # Milestone escrow contract ID set after deployment. + +# USDC on Stellar Testnet +VITE_USDC_CONTRACT_ID= # USDC contract ID used by frontend funding flows. + +# Legacy frontend aliases still read by older screens/hooks +PUBLIC_LEARN_TOKEN_CONTRACT= # Legacy alias for the LearnToken contract ID. +PUBLIC_GOVERNANCE_TOKEN_CONTRACT= # Legacy alias for the governance token contract ID. +PUBLIC_SCHOLAR_NFT_CONTRACT= # Legacy alias for the Scholar NFT contract ID. +PUBLIC_COURSE_MILESTONE_CONTRACT= # Legacy alias for the course milestone contract ID. +PUBLIC_SCHOLARSHIP_TREASURY_CONTRACT= # Legacy alias for the scholarship treasury contract ID. +PUBLIC_MILESTONE_ESCROW_CONTRACT= # Legacy alias for the milestone escrow contract ID. +PUBLIC_SCHOLARSHIP_GOVERNANCE_CONTRACT= # Legacy governance/event contract ID used by profile and activity feeds. +PUBLIC_USDC_CONTRACT_ID= # Legacy alias for the USDC contract ID used by the current USDC utility. + +# Backend API +VITE_API_URL=http://localhost:3001 # Base URL for frontend requests to the LearnVault backend when it runs locally. +VITE_API_BASE_URL=/api # Relative API prefix used by upload and scholar milestone pages in the frontend. +VITE_SERVER_URL=http://localhost:3001 # Backward-compatible alias still read by the current comment UI. + +# IPFS / Pinata +PINATA_API_KEY= # Pinata API key used for authenticated file uploads to IPFS. +PINATA_SECRET= # Pinata API secret used for authenticated file uploads to IPFS. +VITE_IPFS_GATEWAY_URL=https://gateway.pinata.cloud/ipfs # Optional public IPFS gateway override for displaying uploaded files. +>>>>>>> main diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..2966d3d8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,11 @@ +# Global — all PRs +* @bakeronchain + +# Smart contracts — any Rust changes +/contracts/ @bakeronchain + +# Frontend — any TypeScript/React changes +/src/ @bakeronchain + +# CI/CD — any workflow changes require extra scrutiny +/.github/workflows/ @bakeronchain \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bounty_request.md b/.github/ISSUE_TEMPLATE/bounty_request.md new file mode 100644 index 00000000..6cb56bba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bounty_request.md @@ -0,0 +1,41 @@ +--- +name: Bounty Request +about: Request a bounty for a task or improvement +title: '[BOUNTY] ' +labels: bounty, enhancement +assignees: '' +--- + +## Bounty Description + + +## Category +- [ ] Development +- [ ] Documentation +- [ ] Design +- [ ] Testing +- [ ] Research +- [ ] Other + +## Difficulty Level +- [ ] Beginner +- [ ] Intermediate +- [ ] Advanced + +## Estimated Reward + + +## Requirements + + +## Deliverables + + +## Timeline + + +## Skills Required + + +## Additional Context + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index aba32364..396a6916 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,42 +1,34 @@ --- -name: "Bug Report" -about: "Report a bug to help us improve LearnVault." -title: "[Bug] " -labels: ["bug", "help wanted"] -assignees: "" +name: Bug Report +about: Create a report to help us improve +title: '[BUG] ' +labels: bug +assignees: '' --- -## Describe the bug - -A clear and concise description of what the bug is. - -## To Reproduce - -Steps to reproduce the behavior: +## Description + +## Steps to Reproduce 1. Go to '...' 2. Click on '...' 3. Scroll down to '...' 4. See error -## Expected behavior - -A clear and concise description of what you expected to happen. - -## Screenshots +## Expected Behavior + -If applicable, add screenshots to help explain your problem. +## Actual Behavior + -## Environment (please complete the following information): +## Environment +- **Network:** (e.g., testnet, mainnet, local) +- **Browser:** (e.g., Chrome, Firefox, Safari) +- **Wallet:** (e.g., Freighter, Albedo, LOBSTR) +- **OS:** (e.g., Windows, macOS, Linux) -- OS: [e.g. macOS, Windows, Linux] -- Browser [e.g. Chrome, Safari] -- Wallet [e.g. MetaMask, Freighter] - -## Additional context - -Add any other context about the problem here. - ---- +## Screenshots + -_Thank you for helping us improve LearnVault!_ +## Additional Context + diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..5b464986 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,50 @@ +name: Bug report +description: Report something that is broken in LearnVault. +title: "[Bug]: " +labels: + - bug + - triage +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to file a bug report. + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Provide exact steps so we can reproduce the issue. + placeholder: | + 1. Go to ... + 2. Click ... + 3. Observe ... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What should have happened? + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + description: What happened instead? + validations: + required: true + - type: input + id: environment + attributes: + label: Environment + description: + Include OS, browser, Node.js version, and wallet details if relevant. + placeholder: macOS 14.4, Chrome 123, Node 22.2.0, Freighter + validations: + required: true + - type: textarea + id: additional_context + attributes: + label: Additional context + description: Logs, screenshots, links, or any extra information. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..980e4610 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a Question + url: https://github.com/bakeronchain/learnvault/discussions + about: Please ask and answer questions here + - name: Security Vulnerability + url: https://github.com/bakeronchain/learnvault/security/advisories + about: Please report security vulnerabilities privately diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 03b3b444..42f5e272 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,29 +1,26 @@ --- -name: "Feature Request" -about: "Suggest a new feature for LearnVault." -title: "[Feature] " -labels: ["enhancement", "feature", "help wanted"] -assignees: "" +name: Feature Request +about: Suggest an idea for this project +title: '[FEATURE] ' +labels: enhancement +assignees: '' --- -## Is your feature request related to a problem? Please describe. +## Problem Statement + -A clear and concise description of what the problem is. Ex: I'm always -frustrated when [...] +## Proposed Solution + -## Describe the solution you'd like +## Affected Layer +- [ ] Frontend +- [ ] Backend +- [ ] Smart Contract +- [ ] Documentation +- [ ] Other -A clear and concise description of what you want to happen. +## Alternatives Considered + -## Describe alternatives you've considered - -A clear and concise description of any alternative solutions or features you've -considered. - -## Additional context - -Add any other context or screenshots about the feature request here. - ---- - -_Thank you for helping improve LearnVault!_ +## Additional Context + diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..38be9835 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,30 @@ +name: Feature request +description: Suggest a new capability or improvement for LearnVault. +title: "[Feature]: " +labels: + - enhancement + - triage +body: + - type: textarea + id: problem + attributes: + label: Problem statement + description: What problem are you trying to solve? + placeholder: It is difficult to ... because ... + validations: + required: true + - type: textarea + id: proposed_solution + attributes: + label: Proposed solution + description: Describe the ideal implementation or behavior. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: + What alternatives did you evaluate and why were they insufficient? + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/smart_contract.yml b/.github/ISSUE_TEMPLATE/smart_contract.yml new file mode 100644 index 00000000..9b71a56d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/smart_contract.yml @@ -0,0 +1,42 @@ +name: Smart contract issue +description: Report or request a smart contract change. +title: "[Contract]: " +labels: + - smart-contract + - triage +body: + - type: input + id: contract_name + attributes: + label: Contract name + description: Which contract is affected? + placeholder: CourseMilestone + validations: + required: true + - type: input + id: function_name + attributes: + label: Function name + description: Which function is involved? + placeholder: complete_course_milestone + validations: + required: true + - type: textarea + id: rust_error + attributes: + label: Rust error (if any) + description: Paste compiler/runtime error output. + placeholder: error[E0XXX]: ... + validations: + required: false + - type: textarea + id: reproduction + attributes: + label: Reproduction steps + description: Minimal steps to reproduce the issue. + placeholder: | + 1. Run ... + 2. Submit transaction ... + 3. Observe ... + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/smart_contract_issue.md b/.github/ISSUE_TEMPLATE/smart_contract_issue.md new file mode 100644 index 00000000..453548b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/smart_contract_issue.md @@ -0,0 +1,38 @@ +--- +name: Smart Contract Issue +about: Report an issue with smart contracts +title: '[CONTRACT] ' +labels: contract, bug +assignees: '' +--- + +## Affected Contract + + +## Function Name + + +## Expected Behavior + + +## Actual Behavior + + +## Test Case + + +```rust +// Example test case +#[test] +fn test_issue() { + // Your test code here +} +``` + +## Environment +- **Rust Version:** +- **Soroban SDK Version:** +- **Network:** + +## Additional Context + diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dafb4114..52c05451 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,18 +1,14 @@ -version: 2 +version: 2 updates: - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" - cooldown: - default-days: 7 - groups: - npm-minor: - update-types: - - minor - - patch - ignore: - # Dependabot will ignore these dependencies since they're generated by scaffold and aren't checked into the repo - - dependency-name: "packages/guess_the_number" - - dependency-name: "packages/fungible_allowlist_example" - - dependency-name: "packages/nft_enumberable_example" + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..7d2d0419 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ +## Summary + + +## Type of Change +- [ ] Bug fix +- [ ] New feature +- [ ] Smart contract change +- [ ] Documentation +- [ ] Breaking change +- [ ] Other (please describe) + +## Checklist +- [ ] Tests added or updated +- [ ] No hardcoded secrets or private keys +- [ ] If contract change: `cargo test` passes +- [ ] If frontend change: `npm run typecheck` passes +- [ ] If backend change: `npm test` passes +- [ ] Related issues linked (Closes #...) +- [ ] Self-review completed +- [ ] Code follows project style guidelines + +## Screenshots (if applicable) + + +## Testing + + +## Additional Notes + diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml new file mode 100644 index 00000000..bc0fbc82 --- /dev/null +++ b/.github/workflows/backend-tests.yml @@ -0,0 +1,60 @@ +name: Backend Tests + +on: + pull_request: + branches: + - main + +jobs: + test: + name: Run server tests + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: learnvault + POSTGRES_PASSWORD: learnvault + POSTGRES_DB: learnvault_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U learnvault" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + DATABASE_URL: postgresql://learnvault:learnvault@localhost:5432/learnvault_test + NODE_ENV: test + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: server/package-lock.json + + - name: Install dependencies + working-directory: server + run: npm ci + + - name: Run migrations + working-directory: server + run: npm run migrate + + - name: Clean build cache + working-directory: server + run: | + rm -rf node_modules/.cache + rm -rf dist + npm run build --if-present || true + + - name: Run tests + working-directory: server + run: npm test \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f5df625d..7887e0ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ~/.cargo/bin/ @@ -42,9 +42,22 @@ jobs: else echo "stellar-scaffold already installed. Clear cache to force reinstall." fi - - run: npm ci - - run: npm run lint + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm ci --legacy-peer-deps - run: npx prettier . --check + - name: Database Migration Safety Check + run: | + echo "Running migration dry-run check..." + # Placeholder for migration check - identifying tool + if [ -d "server/prisma" ]; then + npx prisma migrate dev --dry-run + elif [ -f "server/knexfile.js" ]; then + npx knex migrate:list + else + echo "No standard migration tool detected, skipping dry-run." + fi - name: Build with Scaffold and build client packages run: STELLAR_SCAFFOLD_ENV=development stellar-scaffold build diff --git a/.github/workflows/contracts-ci.yml b/.github/workflows/contracts-ci.yml new file mode 100644 index 00000000..67ebc0b3 --- /dev/null +++ b/.github/workflows/contracts-ci.yml @@ -0,0 +1,46 @@ +name: Contracts CI + +on: + push: + paths: + - "contracts/**" + pull_request: + paths: + - "contracts/**" + +env: + CARGO_TERM_COLOR: always + +jobs: + contracts: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: clippy, rustfmt + targets: wasm32v1-none + + - name: Cache cargo and target + uses: Swatinem/rust-cache@v2 + with: + workspaces: ". -> target" + + - name: Build contracts + run: cargo build --target wasm32v1-none --release + + - name: Run tests + run: cargo test + + - name: Run fuzz tests + run: cargo test -- --include-ignored fuzz + + - name: Run clippy + run: cargo clippy -- -D warnings + + - name: Check formatting + run: cargo fmt --check diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml new file mode 100644 index 00000000..3df4912a --- /dev/null +++ b/.github/workflows/contracts.yml @@ -0,0 +1,32 @@ +name: Contract Tests + +on: + pull_request: + branches: + - main + +jobs: + contract-tests: + name: Contract Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup stable Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Run contract tests + run: | + set -euo pipefail + # If the root Cargo.toml declares a workspace, run workspace tests. + if grep -qE '^\[workspace\]' Cargo.toml 2>/dev/null; then + echo "Cargo workspace detected; running 'cargo test --workspace'" + cargo test --workspace + else + echo "No workspace detected; running tests per contract" + for dir in contracts/*/; do + echo "==> Testing $dir" + (cd "$dir" && cargo test) + done + fi diff --git a/.github/workflows/dapp-ipfs.yml b/.github/workflows/dapp-ipfs.yml index 8855c523..a9b7fbc8 100644 --- a/.github/workflows/dapp-ipfs.yml +++ b/.github/workflows/dapp-ipfs.yml @@ -27,10 +27,10 @@ jobs: working-directory: ./ steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - name: Install App dependencies - run: npm install + run: npm ci - name: Build app env: @@ -47,7 +47,7 @@ jobs: subject-path: "./dist/*" - name: Archive production artifacts - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v4 with: name: dist path: | diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..b5f8a92b --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,34 @@ +name: E2E (Playwright) + +on: + pull_request: + +jobs: + e2e: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Run E2E tests + run: npm run test:e2e + + - name: Upload test artifacts (on failure) + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-artifacts + path: | + test-results/ + playwright-report/ diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml new file mode 100644 index 00000000..04e61f59 --- /dev/null +++ b/.github/workflows/frontend-ci.yml @@ -0,0 +1,51 @@ +name: Frontend CI + +on: + pull_request: + branches: + - main + paths: + - "src/**" + - "package.json" + - "package-lock.json" + - "vite.config.ts" + - "vitest.config.ts" + - "tsconfig*.json" + - "eslint.config.js" + +jobs: + frontend: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + # We are REMOVING the cache line temporarily to force a clean download + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Type check + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Test with coverage + run: npm run test:coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/lcov.info + flags: frontend + fail_ci_if_error: false + + - name: Build + run: npm run build diff --git a/.github/workflows/preview-comment.yml b/.github/workflows/preview-comment.yml new file mode 100644 index 00000000..935e05b1 --- /dev/null +++ b/.github/workflows/preview-comment.yml @@ -0,0 +1,39 @@ +# .github/workflows/preview-comment.yml +name: Preview deployment comment + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + comment-preview-url: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Wait for Vercel preview + uses: patrickedqvist/wait-for-vercel-preview@v1.3.1 + id: vercel_preview + continue-on-error: true + with: + token: ${{ secrets.GITHUB_TOKEN }} + environment: Preview + max_timeout: 120 + + - name: Post preview URL comment + if: ${{ steps.vercel_preview.outputs.url != '' }} + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `🚀 **Preview deployed!**\n\n${process.env.PREVIEW_URL}\n\n> Built against Stellar Testnet` + }) + env: + PREVIEW_URL: ${{ steps.vercel_preview.outputs.url }} + + - name: Skip missing preview + if: ${{ steps.vercel_preview.outputs.url == '' }} + run: echo "No Vercel preview deployment found for this pull request." diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml new file mode 100644 index 00000000..ecef27cc --- /dev/null +++ b/.github/workflows/publish-docker.yml @@ -0,0 +1,49 @@ +name: Publish Docker Image + +on: + push: + branches: + - main + paths: + - 'server/**' + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}-backend + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,format=long + type=ref,event=branch + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./server + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml new file mode 100644 index 00000000..87593e9a --- /dev/null +++ b/.github/workflows/security-scan.yml @@ -0,0 +1,40 @@ +name: Security Scan + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + node-security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + - name: Install dependencies + run: npm ci --legacy-peer-deps + - name: Run npm audit + # Known unfixable criticals in @creit.tech/stellar-wallets-kit -> @trezor/* -> protobufjs + # These require a breaking major version downgrade of stellar-wallets-kit (tracked issue). + # Audit runs for visibility but does not block CI until upstream fixes are available. + run: npm audit --audit-level=high + continue-on-error: true + + rust-security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Install cargo-audit + run: cargo install cargo-audit + - name: Update Cargo.lock to latest compatible versions + run: cargo update + - name: Run cargo audit + # .cargo/audit.toml is auto-detected by cargo-audit from workspace root + run: cargo audit diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml new file mode 100644 index 00000000..06974bec --- /dev/null +++ b/.github/workflows/server-ci.yml @@ -0,0 +1,66 @@ +name: Server CI + +on: + push: + paths: + - "server/**" + pull_request: + paths: + - "server/**" + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 15 + + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: learnvault + POSTGRES_PASSWORD: learnvault + POSTGRES_DB: learnvault_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + DATABASE_URL: postgresql://learnvault:learnvault@localhost:5432/learnvault_test + NODE_ENV: test + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: server/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: server + + - name: Run migrations + run: npm run migrate + working-directory: server + + - name: Verify migrations (apply, idempotency, rollback) + run: npm run migrate:verify + working-directory: server + + - name: Run tests with coverage + run: npm run test:coverage + working-directory: server + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./server/coverage/lcov.info + flags: backend + fail_ci_if_error: false diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..315e8307 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,42 @@ +name: Mark and close stale issues + +on: + schedule: + # Run daily at midnight UTC + - cron: "0 0 * * *" + workflow_dispatch: # Allow manual trigger + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + # Mark issues as stale after 14 days of no activity + days-before-stale: 14 + # Close stale issues after 7 more days with no activity + days-before-close: 7 + + stale-issue-label: "stale" + stale-issue-message: > + This issue has had no activity for 14 days and has been marked as + stale. It will be closed automatically in 7 days unless there is + further activity. If this is still relevant, please leave a comment + or remove the `stale` label. + + close-issue-message: > + This issue was automatically closed after 7 days of inactivity + following the stale notice. Feel free to re-open if the topic is + still relevant. + + # Never mark issues with these labels as stale + exempt-issue-labels: "help wanted,good first issue" + + # Only manage issues, not pull requests + days-before-pr-stale: -1 + days-before-pr-close: -1 diff --git a/.gitignore b/.gitignore index 2b0a80fa..b570710c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,16 @@ test_snapshots dist dist-ssr +# Playwright E2E output +test-results/ +playwright-report/ + +# TypeScript incremental build info +*.tsbuildinfo + # dependencies node_modules/ +server/node_modules # logs logs @@ -43,3 +51,6 @@ packages/* # generated contract client imports src/contracts/* !src/contracts/util.ts + +# test coverage +coverage/ \ No newline at end of file diff --git a/.hintrc b/.hintrc new file mode 100644 index 00000000..e099e671 --- /dev/null +++ b/.hintrc @@ -0,0 +1,13 @@ +{ + "extends": [ + "development" + ], + "hints": { + "axe/aria": [ + "default", + { + "aria-valid-attr-value": "off" + } + ] + } +} \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index 2312dc58..51ec4cfb 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,6 @@ npx lint-staged +if command -v gitleaks >/dev/null 2>&1; then + gitleaks protect --staged --verbose +else + echo "[Husky] Warning: gitleaks not found. Skipping secret scan." +fi diff --git a/.npm-cache/_update-notifier-last-checked b/.npm-cache/_update-notifier-last-checked new file mode 100644 index 00000000..e69de29b diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..8c207b0a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +package-lock.json +Cargo.lock +.github/ +target/ +target_local/ +contracts/ diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 00000000..ed49ca7b --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,2 @@ +edition = "2024" +max_width = 100 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..f6f0c035 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "kiroAgent.configureMCP": "Disabled" +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index e69de29b..f3bef57a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,136 @@ +LearnVault is built for and by a global community of learners and builders. We +are committed to making this a welcoming, inclusive, and harassment-free space +for everyone, especially contributors from underrepresented communities. + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +hello@learnvault.xyz. All complaints will be reviewed and investigated promptly +and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e69de29b..c7bf8068 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -0,0 +1,396 @@ +# Contributing to LearnVault + +Welcome — and thank you for wanting to contribute! 🎉 + +LearnVault is an open-source, learn-and-earn platform built for African learners +on the Stellar blockchain. Whether you're a smart-contract engineer, a frontend +developer, a technical writer, or a designer, there is room for you here. This +guide will get you from zero to your first pull request. + +--- + +## Table of Contents + +- [Contributing to LearnVault](#contributing-to-learnvault) + - [Table of Contents](#table-of-contents) + - [Prerequisites](#prerequisites) + - [Clone and Install](#clone-and-install) + - [Run the Frontend (No Blockchain Required)](#run-the-frontend-no-blockchain-required) + - [Run Contract Tests](#run-contract-tests) + - [Start Full Dev Environment (Requires Docker)](#start-full-dev-environment-requires-docker) + - [Project Structure](#project-structure) + - [How to Pick an Issue](#how-to-pick-an-issue) + - [Branching and PR Workflow](#branching-and-pr-workflow) + - [1. Create a branch](#1-create-a-branch) + - [2. Make your changes](#2-make-your-changes) + - [3. Open a pull request](#3-open-a-pull-request) + - [PR Checklist](#pr-checklist) + - [Code Style](#code-style) + - [TypeScript / Frontend](#typescript--frontend) + - [Rust / Smart Contracts](#rust--smart-contracts) + - [Commit Messages](#commit-messages) + - [Need Help?](#need-help) + - [Code of Conduct](#code-of-conduct) + +--- + +## Prerequisites + +Make sure the following tools are installed on your machine before you begin: + +| Tool | Minimum Version | Purpose | +| ------------- | --------------- | -------------------------------------------------------------------- | +| **Node.js** | 20+ | Frontend build tooling and dev server | +| **npm** | 10+ | Package management (ships with Node.js) | +| **Rust** | 1.89+ | Compiling Soroban smart contracts (pinned via `rust-toolchain.toml`) | +| **Docker** | Latest stable | Running a local Stellar node for full end-to-end development | +| **Freighter** | Latest | Stellar wallet browser extension for testing wallet interactions | + +> [!TIP] **First-time Rust setup?** After installing Rust via +> [rustup](https://rustup.rs/), the project's `rust-toolchain.toml` will +> automatically install the correct toolchain (channel `1.89.0`) and the +> `wasm32v1-none` target the first time you build. + +> [!NOTE] **Frontend-only contributors** — you can skip Rust and Docker +> entirely. The frontend compiles and runs independently; see the section below. + +--- + +## Clone and Install + +```bash +# 1. Fork the repo on GitHub, then clone your fork +git clone https://github.com//learnvault.git +cd learnvault + +# 2. Install Node.js dependencies +npm install + +# 3. Copy the example environment file and adjust if needed +cp .env.example .env +``` + +The `.env.example` ships with defaults for a **local Stellar network**. If you +want to develop against the public testnet instead, uncomment the `TESTNET` +block in `.env`. + +### Generate Soroban Contract Clients (optional) + +If you have deployed the contracts and populated contract IDs in your `.env` +file, you can generate TypeScript client packages for all six contracts: + +```bash +# Generate TypeScript bindings for all deployed contracts +npm run generate:clients + +# Then build the generated packages +npm run install:contracts +``` + +`generate:clients` runs `scripts/generate-clients.sh`, which calls +`stellar contract bindings typescript` for each contract whose ID is set in +`.env`. Any contract with a missing ID is skipped with a warning — you do not +need all six deployed to generate clients for the ones you have. + +> [!TIP] **Frontend-only work?** You can skip this step entirely. The frontend +> falls back to mock data when the generated packages are absent. + +--- + +## Run the Frontend (No Blockchain Required) + +If you are working on UI components, pages, or styling, you can iterate without +a local blockchain node: + +```bash +# Verify the project compiles cleanly +npm run build + +# Start only the Vite dev server +npm run dev:ui +``` + +The app will be available at **http://localhost:5173** (default Vite port). +Hot-module replacement is enabled, so changes are reflected instantly in the +browser. + +--- + +## Run Contract Tests + +The smart contracts live in `contracts/` and are tested with `cargo test`. A +convenience npm script is provided: + +```bash +npm test +``` + +This runs `cargo test --workspace`, which covers all six contracts: + +| Contract | Description | +| ---------------------- | --------------------------------------- | +| `learn_token` | Soulbound ERC20 reputation token | +| `governance_token` | Transferable DAO voting token | +| `fungible-allowlist` | Allowlist-gated fungible token | +| `scholarship_treasury` | Donor-funded community treasury | +| `milestone_escrow` | Tranche-based scholarship disbursements | +| `scholar_nft` | Soulbound NFT credential | + +> [!IMPORTANT] You need Rust installed to run contract tests. If a test fails, +> check that your Rust toolchain matches `1.89.0` by running `rustup show`. + +--- + +## Start Full Dev Environment (Requires Docker) + +For end-to-end development with a local Stellar node and the Vite frontend +running side by side: + +```bash +npm start +``` + +This uses `concurrently` to launch: + +1. **`stellar scaffold watch --build-clients`** — starts a local Stellar node + and watches for contract changes. +2. **`vite`** — starts the frontend dev server. + +Both processes are displayed in the same terminal with color-coded prefixes +(`stellar` in gray, `vite` in green). + +> [!NOTE] Make sure Docker is running before you execute `npm start`. If you see +> connection errors, verify that port `8000` is not in use by another process. + +--- + +## Run Backend in Docker + +If you are working strictly on the backend, you can spin up the Node.js API, +PostgreSQL database, and Redis container locally using Docker Compose: + +```bash +cd server +docker-compose up -d +``` + +This will run the backend on **http://localhost:4000** with live-reloading +enabled. + +### Run Backend Tests in Docker + +You can run the full backend test suite in an isolated Docker environment: + +```bash +cd server +docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit +``` + +--- + +## Project Structure + +``` +learnvault/ +├── contracts/ # Soroban smart contracts (Rust) +│ ├── learn_token/ +│ ├── governance_token/ +│ ├── fungible-allowlist/ +│ ├── scholarship_treasury/ +│ ├── milestone_escrow/ +│ └── scholar_nft/ +├── src/ # Frontend source (React + TypeScript) +├── server/ # Backend API (Node.js) +├── packages/ # Auto-generated contract client packages +├── scripts/ # Helper shell scripts (e.g. generate-clients.sh) +├── docs/ # Whitepaper and documentation +├── public/ # Static assets +├── .github/ # CI workflows, issue & PR templates +├── Cargo.toml # Rust workspace configuration +├── package.json # Node.js scripts and dependencies +└── vite.config.ts # Vite build configuration +``` + +--- + +## How to Pick an Issue + +1. **Browse open issues** on the + [Issues tab](https://github.com/bakeronchain/learnvault/issues). +2. **Filter by `good first issue`** if this is your first contribution — these + are scoped, well-described tasks designed for newcomers. +3. **Comment on the issue** to claim it before you start working. This prevents + duplicate effort. +4. **One issue per contributor at a time.** Finish (or explicitly unclaim) your + current issue before picking up a new one. + +> [!TIP] Not sure where to start? Issues labelled `docs`, `good first issue`, or +> `frontend` are usually the most approachable. If an issue interests you but +> the scope is unclear, ask questions — the maintainers are happy to clarify. + +--- + +## Branching and PR Workflow + +### 1. Create a branch + +Use one of these naming conventions: + +``` +feat/short-description # New feature +fix/short-description # Bug fix +docs/short-description # Documentation update +refactor/short-description # Code refactoring +test/short-description # Adding or updating tests +``` + +Example: + +```bash +git checkout -b feat/donor-dashboard +``` + +### 2. Make your changes + +Write clean, well-tested code. Follow the code style guidelines below. + +### 3. Open a pull request + +When you're ready: + +- **PR title** — should match the issue title. +- **Link the issue** — include `Closes #NNN` in the PR description so the issue + is automatically closed when the PR merges. +- **Fill out the PR template** — the repo includes a pull request template; + please complete every section. + +### PR Checklist + +Before opening your PR, make sure: + +- [ ] Your branch is up to date with `main` +- [ ] The app builds without errors: `npm run build` +- [ ] Linting passes: `npm run lint` +- [ ] Formatting is correct: `npx prettier . --check` +- [ ] TypeScript compiles cleanly: `npx tsc --noEmit` +- [ ] Contract tests pass (if applicable): `npm test` +- [ ] You have added or updated tests where applicable +- [ ] The PR description links the issue with `Closes #NNN` + +--- + +## Code Style + +### TypeScript / Frontend + +- **TypeScript Only** — All frontend code must be written in TypeScript. We do + not accept JavaScript files in the codebase. +- **No `any` Type** — The use of the `any` type is prohibited. Use specific + types, interfaces, `unknown` with type guards, or generics instead. +- **Use @stellar/design-system Components** — When building UI components, use + components from the `@stellar/design-system` library instead of creating + custom equivalents. This ensures visual consistency, accessibility compliance, + and alignment with Stellar ecosystem standards. +- **Linting** — ESLint is configured via `eslint.config.js`. Run: + ```bash + npm run lint + ``` +- **Formatting** — Prettier is configured via the shared + `@theahaco/ts-config/prettier` preset. Run: + ```bash + npx prettier . --check # Check for formatting issues + npm run format # Auto-fix formatting + ``` +- **Type checking** — Ensure there are no type errors: + ```bash + npx tsc --noEmit + ``` + +### Rust / Smart Contracts + +- **Formatting** — All Rust code must be formatted with `rustfmt`: + ```bash + cargo fmt --all + ``` +- **Linting** — Clippy must pass with no warnings: + ```bash + cargo clippy --workspace --all-targets + ``` +- **Unit Tests Required** — Every pull request that touches a Soroban contract + must include unit tests for the changed logic. Testing is critical for smart + contracts because contract bugs can have serious consequences in a blockchain + environment. +- **Soroban Best Practices** — Follow the official + [Soroban documentation](https://soroban.stellar.org/docs) best practices, + including proper error handling, efficient storage patterns, and security + considerations. + +> [!NOTE] A pre-commit hook powered by +> [Husky](https://typicode.github.io/husky/) and +> [lint-staged](https://github.com/lint-staged/lint-staged) automatically runs +> ESLint and Prettier on staged files. If the hook blocks your commit, fix the +> reported issues before committing. + +--- + +## Commit Messages + +We follow [Conventional Commits](https://www.conventionalcommits.org/): + +``` +: + +# Examples: +feat: add donor contribution history page +fix: prevent double-minting of LearnTokens +docs: update CONTRIBUTING.md with PR checklist +test: add unit tests for milestone escrow release +refactor: extract wallet connection into custom hook +``` + +Keep the summary under 72 characters. Use the imperative mood ("add", not +"added"). + +--- + +## Need Help? + +If you run into any issues during setup or have questions about the codebase: + +- **Open a + [Discussion](https://github.com/bakeronchain/learnvault/discussions)** on + GitHub +- **Join our [Discord](https://discord.gg/learnvault)** — the `#dev` channel is + the best place for contributor chat +- **Tag a maintainer** on your issue or PR if you're blocked + +We're building LearnVault for the next generation of African builders — and that +includes building a welcoming, supportive contributor community. No question is +too small. We're glad you're here. 💛 + +--- + +## Code of Conduct + +LearnVault is committed to providing a welcoming, inclusive, and harassment-free +experience for everyone. All contributors are expected to adhere to our Code of +Conduct. + +Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing. By +participating in this project, you agree to abide by its terms and help us +maintain a positive and respectful community. + +If you experience or witness unacceptable behavior, please report it to the +project maintainers. We take all reports seriously and will respond +appropriately. + +--- + +_LearnVault — Built for African learners. Powered by community. Governed by +effort._ + +## Security Standards + +- **SQL Injection:** All database queries must use parameterized placeholders + (e.g., $1, ). Never use string interpolation or template literals for + user-supplied data in SQL strings. diff --git a/Cargo.lock b/Cargo.lock index 842cc0fd..e9389b3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,7 +162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -189,6 +189,27 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + [[package]] name = "block-buffer" version = "0.10.4" @@ -284,7 +305,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -299,6 +320,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "course_milestone" +version = "0.0.1" +dependencies = [ + "learnvault-shared", + "soroban-sdk", + "stellar-registry", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -326,7 +356,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -537,7 +567,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", "serde", "sha2", "subtle", @@ -562,7 +592,7 @@ dependencies = [ "ff", "generic-array", "group", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -574,6 +604,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "escape-bytes" version = "0.1.1" @@ -586,13 +626,19 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0939f82868b77ef93ce3c3c3daf2b3c526b456741da5a1a4559e590965b6026b" +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "ff" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -609,13 +655,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "fungible-allowlist-example" +name = "fungible-allowlist" version = "0.0.1" dependencies = [ "soroban-sdk", - "stellar-access", - "stellar-macros", - "stellar-tokens", ] [[package]] @@ -643,23 +686,34 @@ dependencies = [ ] [[package]] -name = "group" -version = "0.13.0" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "ff", - "rand_core", - "subtle", + "cfg-if", + "libc", + "r-efi", + "wasip2", ] [[package]] -name = "guess-the-number" +name = "governance-token" version = "0.0.1" dependencies = [ + "learnvault-shared", "soroban-sdk", - "stellar-registry", - "stellar-xdr", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", ] [[package]] @@ -817,6 +871,22 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "learn-token" +version = "0.0.1" +dependencies = [ + "learnvault-shared", + "proptest", + "soroban-sdk", +] + +[[package]] +name = "learnvault-shared" +version = "0.0.1" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "libc" version = "0.2.172" @@ -829,6 +899,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "log" version = "0.4.27" @@ -853,12 +929,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] -name = "nft-enumerable-example" +name = "milestone-escrow" version = "0.0.1" dependencies = [ + "learnvault-shared", + "proptest", "soroban-sdk", - "stellar-macros", - "stellar-tokens", + "stellar-registry", ] [[package]] @@ -983,6 +1060,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.40" @@ -992,6 +1094,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -999,8 +1107,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1010,7 +1128,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1019,7 +1147,25 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", ] [[package]] @@ -1070,12 +1216,37 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.20" @@ -1093,6 +1264,25 @@ dependencies = [ "serde_json", ] +[[package]] +name = "scholar-nft" +version = "0.0.1" +dependencies = [ + "learnvault-shared", + "soroban-sdk", +] + +[[package]] +name = "scholarship-treasury" +version = "0.0.1" +dependencies = [ + "governance-token", + "learnvault-shared", + "proptest", + "soroban-sdk", + "stellar-registry", +] + [[package]] name = "sec1" version = "0.7.3" @@ -1222,7 +1412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1287,7 +1477,7 @@ dependencies = [ "ed25519-dalek", "elliptic-curve", "generic-array", - "getrandom", + "getrandom 0.2.16", "hex-literal", "hmac", "k256", @@ -1295,8 +1485,8 @@ dependencies = [ "num-integer", "num-traits", "p256", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "sec1", "sha2", "sha3", @@ -1349,7 +1539,7 @@ dependencies = [ "ctor", "derive_arbitrary", "ed25519-dalek", - "rand", + "rand 0.8.5", "rustc_version", "serde", "serde_json", @@ -1443,14 +1633,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stellar-access" -version = "0.5.1" -source = "git+https://github.com/OpenZeppelin/stellar-contracts?tag=v0.5.1#372a162a6df8aed108befa5573d30a8ca49cb271" -dependencies = [ - "soroban-sdk", -] - [[package]] name = "stellar-build" version = "0.0.4" @@ -1463,24 +1645,6 @@ dependencies = [ "topological-sort", ] -[[package]] -name = "stellar-contract-utils" -version = "0.5.1" -source = "git+https://github.com/OpenZeppelin/stellar-contracts?tag=v0.5.1#372a162a6df8aed108befa5573d30a8ca49cb271" -dependencies = [ - "soroban-sdk", -] - -[[package]] -name = "stellar-macros" -version = "0.5.1" -source = "git+https://github.com/OpenZeppelin/stellar-contracts?tag=v0.5.1#372a162a6df8aed108befa5573d30a8ca49cb271" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "stellar-registry" version = "0.0.4" @@ -1516,15 +1680,6 @@ dependencies = [ "data-encoding", ] -[[package]] -name = "stellar-tokens" -version = "0.5.1" -source = "git+https://github.com/OpenZeppelin/stellar-contracts?tag=v0.5.1#372a162a6df8aed108befa5573d30a8ca49cb271" -dependencies = [ - "soroban-sdk", - "stellar-contract-utils", -] - [[package]] name = "stellar-xdr" version = "23.0.0" @@ -1578,6 +1733,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1641,24 +1809,55 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "upgrade-timelock-vault" +version = "0.0.1" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1762,7 +1961,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.1", "windows-result", "windows-strings", ] @@ -1795,13 +1994,19 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -1810,9 +2015,24 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + [[package]] name = "zerocopy" version = "0.8.25" diff --git a/Cargo.toml b/Cargo.toml index aae4e3ae..3de0016f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,28 +1,23 @@ [workspace] members = ["contracts/*"] +exclude = ["contracts/testdata", "contracts/scripts"] resolver = "2" [workspace.package] -authors = ["The Aha Company"] +authors = ["LearnVault Contributors"] edition = "2024" license = "Apache-2.0" -repository = "https://github.com/theahaco/scaffold-stellar" +repository = "https://github.com/bakeronchain/learnvault" version = "0.0.1" [workspace.dependencies.soroban-sdk] version = "23.1.0" -[workspace.dependencies.stellar-access] -git = "https://github.com/OpenZeppelin/stellar-contracts" -tag = "v0.5.1" +[workspace.dependencies.learnvault-shared] +path = "contracts/shared" -[workspace.dependencies.stellar-macros] -git = "https://github.com/OpenZeppelin/stellar-contracts" -tag = "v0.5.1" - -[workspace.dependencies.stellar-tokens] -git = "https://github.com/OpenZeppelin/stellar-contracts" -tag = "v0.5.1" +[workspace.dependencies.proptest] +version = "1.5" [profile.release] opt-level = "z" diff --git a/DASHBOARD_WIRING_IMPLEMENTATION.md b/DASHBOARD_WIRING_IMPLEMENTATION.md new file mode 100644 index 00000000..e096b291 --- /dev/null +++ b/DASHBOARD_WIRING_IMPLEMENTATION.md @@ -0,0 +1,145 @@ +# Dashboard Data Wiring Implementation Summary + +## Overview + +Fixed the hardcoded dashboard stats issue by connecting to real data sources: + +- **GET /api/me** for learner profile via new `useLearnerProfile` hook +- **Learn Token contract** for real LRN balance via existing `useLearnToken` + hook +- **Course Milestone contract** for enrolled courses & milestone progress via + existing `useCourse` hook +- Added skeleton loaders during data fetching +- Graceful handling of unauthenticated state (wallet not connected) + +## Changes Made + +### 1. Created `useLearnerProfile` Hook + +**File:** `src/hooks/useLearnerProfile.ts` + +- Queries GET `/api/me` endpoint to fetch authenticated learner profile +- Returns `{ profile, isLoading, error, address }` +- Automatically disabled when no wallet is connected +- Uses React Query for caching with 5-minute stale time +- Extensible interface for future profile fields (bio, avatar, etc.) + +Key features: + +```typescript +export interface LearnerProfile { + address: string +} + +export function useLearnerProfile() { + // Fetches from GET /api/me with Bearer token auth + // Caches for 5 minutes + // Auto-disabled when address is undefined +} +``` + +### 2. Updated Dashboard.tsx + +**File:** `src/pages/Dashboard.tsx` + +#### Removed Hardcoded Values: + +- Removed static `stats` array with hardcoded LRN balance (142), courses (2), + milestones (14) +- Removed static `enrolledCourses` array with fake course data + +#### Added Real Data Sources: + +```typescript +// Fetch learner profile from backend +const { profile, isLoading: isLoadingProfile } = useLearnerProfile() + +// Fetch LRN balance from contract (converts stroops → LRN) +const { balance: lrnBalance, isLoading: isLoadingBalance } = + useLearnToken(address) + +// Fetch enrolled courses and milestone progress from contract +const { enrolledCourses, progressMap, isCompletingMilestone } = useCourse() +``` + +#### Dynamic Stats Calculation: + +- **LRN Balance:** From contract; converts stroops to human-readable format with + locale formatting +- **Courses Enrolled:** From `enrolledCourses.length` +- **Milestones:** Calculated from `progressMap` by summing completed milestone + IDs +- **Gov Tokens:** Placeholder (remains 0) + +#### Skeleton Loaders: + +- 4 placeholder cards during loading state +- 2 placeholder course cards during loading state +- Using CSS `animate-pulse` with `.glass-card` styling for consistency + +#### Unauthenticated State Handling: + +```tsx +if (!address) { + return ( +
+
+

Connect Your Wallet

+

To view your learning dashboard...

+ Connect Wallet → +
+
+ ) +} +``` + +## Acceptance Criteria - All Met ✅ + +| Criteria | Status | Implementation | +| -------------------------------------------------------- | ------ | ------------------------------------------------------------ | +| LRN balance from contract, not hardcoded | ✅ | `useLearnToken(address)` with stroops-to-LRN conversion | +| Enrolled courses from backend/contract, not static array | ✅ | `useCourse().enrolledCourses` | +| Skeleton loaders shown while fetching | ✅ | Conditional rendering: `isLoading ? : ` | +| Unauthenticated users see connect-wallet prompt | ✅ | Renders connect wallet CTA instead of returning null | +| All hardcoded stat values removed | ✅ | All stats now calculated from real data sources | + +## Data Flow + +``` +Dashboard Component +├─ useLearnerProfile() +│ └─ GET /api/me → { address: string } +│ +├─ useLearnToken(address) +│ └─ learn_token contract → balance: bigint (stroops) +│ +└─ useCourse() + ├─ course_milestone contract → enrolledCourses: Course[] + └─ Returns progressMap with completed milestone counts + +Stats Calculation: +- LRN Balance: convertStroopsToLRN(balance) +- Courses: enrolledCourses.length +- Milestones: sum(progressMap[courseId].completedMilestoneIds) +``` + +## Testing Plan + +1. **Wallet Connected Scenario:** + - Verify stats Display shows loading state + - Verify real LRN balance appears (from contract) + - Verify enrolled courses display (from useCourse) + - Verify milestone count calculated correctly + +2. **Wallet Not Connected Scenario:** + - Verify "Connect Your Wallet" prompt displays + - Verify link to "/" works + +3. **Loading States:** + - Verify skeleton loaders show while useLearnToken/useCourse hooks are + loading + - Verify proper transition from skeleton to real data + +4. **Data Updates:** + - Verify stats update when new milestones are completed + - Verify new course enrollments appear in the list diff --git a/PR_SUMMARY.md b/PR_SUMMARY.md new file mode 100644 index 00000000..1cde8682 --- /dev/null +++ b/PR_SUMMARY.md @@ -0,0 +1,73 @@ +# Pull Request: Protocol Dashboards, Credential Verification, and Upstream Sync + +## Summary + +This PR implements core protocol functionality including administrative +management, treasury tracking, and on-chain credential verification. It +successfully resolves eight key issues and synchronizes the frontend with the +latest upstream internationalization standards. + +## Changes + +### 🛠️ Admin Panel ([/admin](/admin)) + +- **Fixes #74**: Developed a management interface for courses, milestones, and + treasury oversight. +- Supports emergency pause controls and automated audit entry tracking. + +### 📊 Treasury Dashboard ([/treasury](/treasury)) + +- **Fixes #50**: Implemented real-time visualization of the + `ScholarshipTreasury` contract. +- Tracks active disbursements, total funding, and recent ecosystem donations. + +### 🎓 ScholarNFT Credential Viewer ([/credentials/1](/credentials/1)) + +- **Fixes #32**: Created a verification page for on-chain certificates with + social sharing capabilities. +- Integrated a gallery view on user profiles to display verified achievements. + +### 📝 Quiz & Assessment Engine + +- **Fixes #26**: Engineered a reusable `QuizEngine` component to validate + learner mastery. +- Connected pass states directly to Soroban `complete_milestone` contract calls. + +### 🔄 Upstream Synchronization + +- Completed a full rebase and merge onto `upstream/main`. +- Integrated `react-i18next` multi-language support across all new and existing + interfaces. +- Resolved merge conflicts in `server/src/index.ts`, `src/App.tsx`, and + `server/package.json`. +- Fixed breaking changes in Treasury hooks and reordered React hooks in + `LessonView.tsx` to ensure build stability and "Rules of Hooks" compliance. + +### 🛡️ Security & DevOps + +- **Fixes #720**: Added `helmet` middleware with custom Content Security Policy + (CSP) for Stellar and IPFS. +- **Fixes #709**: Integrated `gitleaks` into Husky pre-commit hooks to prevent + credential leakage. +- **Fixes #708**: Added automated database migration safety checks (dry-run) in + GitHub Actions CI. + +### 📅 Community Events ([/community](/community)) + +- **Fixes #750**: Implemented a community events calendar with categorized event + cards (Hackathons, Workshops, Study Groups). +- Backend: Created `/api/community/events` REST API endpoints. +- Frontend: Designed a "glass" style calendar interface with real-time fetch + integration. + +## Related Issues + +Fixes #74 fixes #50 fixes #32 fixes #26 fixes #720 fixes #709 fixes #708 fixes +#750 + +## Notes + +- All UI routes have been verified for runtime consistency. +- Standardized the global design system (v4) to support the new protocol + features. +- Build verified via `npm run build`. diff --git a/README copy.md b/README copy.md deleted file mode 100644 index a2f4fee9..00000000 --- a/README copy.md +++ /dev/null @@ -1,348 +0,0 @@ -# LearnVault — Official Documentation - -> **Learning is the proof of work. The community is the bank.** - ---- - -## Table of Contents - -1. [Introduction](#introduction) -2. [Problem Statement](#problem-statement) -3. [Solution](#solution) -4. [Platform Architecture](#platform-architecture) -5. [Smart Contract System](#smart-contract-system) -6. [User Roles](#user-roles) -7. [The Earn Loop](#the-earn-loop) -8. [Scholarship DAO](#scholarship-dao) -9. [Governance](#governance) -10. [Tech Stack](#tech-stack) -11. [Roadmap](#roadmap) -12. [Contributing](#contributing) -13. [Contact](#contact) - ---- - -## Introduction - -**LearnVault** is a decentralized learn-and-earn platform where education meets -opportunity. Learners earn reputation tokens by completing skill tracks, while -donors and sponsors fund a community treasury governed by DAO voting. The best -learners get funded to go further — no gatekeepers, no bureaucracy, just proof -of effort and community belief. - -LearnVault is designed specifically with African learners in mind — a generation -of ambitious builders who have the talent and drive but lack access to the -financial resources that would take their skills to the next level. By combining -blockchain-powered credentials, on-chain reputation, and decentralized -scholarship funding, LearnVault creates a self-sustaining education ecosystem -that rewards effort and amplifies potential. - ---- - -## Problem Statement - -Access to quality tech education across Africa is not limited by lack of -ambition — it is limited by lack of opportunity. Three core problems define the -gap: - -**Funding Barriers** — Bootcamps, courses, and developer tools cost money that -most learners in emerging markets simply do not have. Scholarship systems that -exist are slow, opaque, and often inaccessible. - -**Credential Trust Gap** — Traditional certificates are easy to fake and hard to -verify. Employers and DAOs have no reliable way to assess a candidate's real -on-chain track record. - -**Broken Incentive Systems** — Existing learn-to-earn platforms flood learners -with worthless tokens for clicking through slides. There is no real connection -between learning effort and financial reward. - -LearnVault addresses all three problems in a single, cohesive platform. - ---- - -## Solution - -LearnVault creates a three-pillar ecosystem: - -**1. Learn** — Learners enroll in skill tracks covering Web3 development, smart -contract engineering, DeFi, frontend development, and more. Every verified -milestone they complete earns them LearnTokens — soulbound, non-transferable -reputation tokens that live on-chain. - -**2. Earn** — LearnTokens are proof of real effort. They unlock governance -rights, scholarship eligibility, and platform reputation. High-achieving -learners convert a portion of their LearnTokens into Governance Tokens, giving -them a voice in the DAO. - -**3. Get Funded** — Learners with sufficient on-chain reputation can submit -scholarship proposals to the community treasury. Governance token holders vote -on proposals. Approved scholars receive milestone-based disbursements in -stablecoins — real, stable value delivered directly to their wallets. - ---- - -## Platform Architecture - -``` -┌─────────────────────────────────────────────────────┐ -│ LEARNVAULT │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ LEARN │───▶│ EARN │───▶│ GET FUNDED│ │ -│ │ │ │ │ │ │ │ -│ │ Courses │ │ LRN │ │ Scholarship│ │ -│ │ Quizzes │ │ Tokens │ │ Proposals │ │ -│ │ Projects │ │ (Soulbound│ │ DAO Vote │ │ -│ │ │ │ ERC20) │ │ Escrow │ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ │ -│ ┌─────────────────────────────────────────┐ │ -│ │ COMMUNITY TREASURY │ │ -│ │ Funded by Donors · Governed by DAO │ │ -│ └─────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────┘ -``` - ---- - -## Smart Contract System - -LearnVault is powered by six core smart contracts: - -### `LearnToken.sol` - -A **soulbound ERC20** token that is minted to learners upon verified milestone -completion. Non-transferable by design — it represents real effort, not -speculation. Your LearnToken balance is your on-chain academic reputation score. - -### `GovernanceToken.sol` - -A **transferable ERC20** distributed to donors upon treasury contribution and -earned by top learners at milestone thresholds. Used exclusively for DAO voting -on scholarship proposals. - -### `CourseMilestone.sol` - -Tracks learner progress per course. Each course has defined checkpoints verified -by a trusted multi-sig validator (transitioning to oracle-based verification in -V2). On successful verification, this contract triggers LearnToken minting. - -### `ScholarshipTreasury.sol` - -Holds all donor funds in stablecoins (USDC). Funds can only be released upon -successful proposal execution through the governance system. Tracks total -contributions per donor. Transparent and auditable by anyone. - -### `MilestoneEscrow.sol` - -Manages approved scholarship disbursements in tranches. Funds are released as -scholars hit agreed milestones. If a scholar is inactive for 30 days, unspent -funds automatically return to the treasury. - -### `ScholarNFT.sol` - -Mints a **soulbound ERC721 credential** to scholars who complete their funded -programs. Non-transferable, tamper-proof, and permanently verifiable on-chain. -Shareable with employers, DAOs, and the broader ecosystem. - ---- - -## User Roles - -### Learner - -- Connects wallet and enrolls in skill tracks -- Completes lessons, quizzes, and projects -- Earns soulbound LearnTokens per milestone -- Builds on-chain reputation score -- Submits scholarship proposals when eligible -- Receives milestone-based funding upon approval -- Earns ScholarNFT credential upon program completion - -### Donor / Sponsor - -- Deposits stablecoins (USDC) into the community treasury -- Receives Governance Tokens proportional to contribution -- Votes on scholarship proposals -- Tracks the impact of their contributions transparently on-chain -- Can set optional focus areas (e.g., only fund Web3 developers) - -### DAO Voter / Community Member - -- Any Governance Token holder can vote on proposals -- Votes are weighted by token balance -- Voting window: 7 days per proposal -- Quorum and threshold parameters set by DAO governance - ---- - -## The Earn Loop - -LearnVault's flywheel is designed so that effort compounds over time: - -``` -Complete Lesson - │ - ▼ -Earn LearnTokens (LRN) - │ - ▼ -Complete Full Track ──▶ Convert LRN to Governance Tokens - │ - ▼ -Submit Scholarship Proposal - │ - ▼ -Community Votes YES - │ - ▼ -Milestone-Based Funding Released - │ - ▼ -Complete Funded Program - │ - ▼ -Mint ScholarNFT Credential - │ - ▼ -Higher Reputation ──▶ Larger Future Proposals ──▶ Loop Continues -``` - -The more you learn, the more power and opportunity you unlock. Wealth is not the -barrier — effort is the currency. - ---- - -## Scholarship DAO - -The Scholarship DAO is the heart of LearnVault's funding mechanism. - -### Eligibility to Apply - -A learner must hold a minimum LearnToken balance (set by governance) before -submitting a proposal. This ensures only learners with a verified track record -can access treasury funds. - -### Proposal Contents - -Each scholarship proposal includes: - -- Learning goal and intended program or bootcamp -- Amount requested in USDC -- Timeline and milestone plan -- On-chain reputation score (LRN balance) -- Wallet address for disbursement - -### Voting - -Governance token holders vote YES or NO within a 7-day window. A proposal passes -if it meets the required quorum and approval threshold. Failed proposals can be -resubmitted after 30 days. - -### Disbursement - -Approved funds are locked in `MilestoneEscrow.sol` and released in tranches as -the scholar completes agreed milestones. Progress is reported by the scholar and -confirmed by a community-elected validator committee (transitioning to oracle -verification in V2). - -### Accountability - -Scholars who abandon funded programs without communication are flagged on-chain. -Repeated abandonment affects future proposal eligibility. Unspent funds always -return to the treasury. - ---- - -## Governance - -LearnVault's DAO governance covers: - -- Scholarship eligibility thresholds (minimum LRN to apply) -- Voting quorum and approval thresholds -- Treasury allocation limits per proposal -- Adding new course tracks to the platform -- Protocol upgrades and parameter changes - -Governance evolves over time. In V1, the founding team holds a multi-sig with -community governance as an advisory layer. In V2, full on-chain governance -transfers to token holders. - ---- - -## Tech Stack - -| Layer | Technology | -| ------------------ | ---------------------------------------------- | -| Blockchain | Stellar (primary), EVM-compatible L2 (planned) | -| Smart Contracts | Solidity / Stellar Soroban | -| Frontend | Next.js, TypeScript, TailwindCSS | -| Wallet Integration | Freighter (Stellar), MetaMask | -| Storage | IPFS (course content + proposal docs) | -| Stablecoin | USDC | -| Backend | Node.js, PostgreSQL | -| Deployment | Docker | - ---- - -## Roadmap - -### V1 — MVP (Current Phase) - -- Core smart contracts (LearnToken, GovernanceToken, Treasury, ProposalManager) -- Basic course completion tracker -- Scholarship proposal submission and voting -- Learner and donor dashboards - -### V2 — Growth - -- MilestoneEscrow and automated tranche disbursements -- ScholarNFT credential system -- Oracle-based milestone verification -- Expanded course catalog (Web3, DeFi, Smart Contracts, ZK basics) -- Mobile-responsive frontend -- Community leaderboard - -### V3 — Scale - -- Full on-chain governance transition -- Cross-chain support (Arbitrum, Base) -- Corporate sponsor portal with targeted funding -- ZK-powered credential proofs (prove achievement without revealing identity) -- API for third-party integrations - ---- - -## Contributing - -LearnVault is an open-source project and welcomes contributions from developers, -educators, designers, and community builders. If you believe in decentralized -education and want to help build the infrastructure for the next generation of -African builders, we would love to have you. - -To contribute: - -1. Fork the repository -2. Create a feature branch -3. Submit a pull request with a clear description of your changes -4. Join the community discussion in our Discord - -All contributors are recognized on-chain and in our official documentation. - ---- - -## Contact - -For partnerships, sponsorships, grant inquiries, or general questions about -LearnVault, please reach out through our official channels. - -- **GitHub**: github.com/learnvault -- **Twitter/X**: @LearnVaultDAO -- **Discord**: discord.gg/learnvault -- **Email**: hello@learnvault.xyz - ---- - -_LearnVault — Built for African learners. Powered by community. Governed by -effort._ diff --git a/README.md b/README.md index a2f4fee9..6770683f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,20 @@ # LearnVault — Official Documentation + + +[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) + + + +[![Contracts CI](https://github.com/robertocarlous/learnvault/actions/workflows/contracts-ci.yml/badge.svg)](https://github.com/robertocarlous/learnvault/actions/workflows/contracts-ci.yml) +[![Frontend CI](https://github.com/bakeronchain/learnvault/actions/workflows/frontend-ci.yml/badge.svg)](https://github.com/bakeronchain/learnvault/actions/workflows/frontend-ci.yml) +[![Build](https://github.com/bakeronchain/learnvault/actions/workflows/build.yml/badge.svg)](https://github.com/bakeronchain/learnvault/actions/workflows/build.yml) +[![Frontend Coverage](https://codecov.io/gh/bakeronchain/learnvault/branch/main/graph/badge.svg?flag=frontend)](https://codecov.io/gh/bakeronchain/learnvault) +[![Backend Coverage](https://codecov.io/gh/bakeronchain/learnvault/branch/main/graph/badge.svg?flag=backend)](https://codecov.io/gh/bakeronchain/learnvault) +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Built on Stellar](https://img.shields.io/badge/Built%20on-Stellar-purple)](https://stellar.org) +[![Contributions Welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg)](https://github.com/bakeronchain/learnvault/issues) + > **Learning is the proof of work. The community is the bank.** --- @@ -17,8 +32,12 @@ 9. [Governance](#governance) 10. [Tech Stack](#tech-stack) 11. [Roadmap](#roadmap) -12. [Contributing](#contributing) -13. [Contact](#contact) +12. [Whitepaper Generation](#whitepaper-generation) +13. [Setup](#setup) +14. [Running Tests](#running-tests) +15. [Contributing](#contributing) +16. [Resources](#resources) +17. [Contact](#contact) --- @@ -94,7 +113,7 @@ stablecoins — real, stable value delivered directly to their wallets. │ │ Courses │ │ LRN │ │ Scholarship│ │ │ │ Quizzes │ │ Tokens │ │ Proposals │ │ │ │ Projects │ │ (Soulbound│ │ DAO Vote │ │ -│ │ │ │ ERC20) │ │ Escrow │ │ +│ │ │ │ SEP-41) │ │ Escrow │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ ┌─────────────────────────────────────────┐ │ @@ -110,41 +129,91 @@ stablecoins — real, stable value delivered directly to their wallets. LearnVault is powered by six core smart contracts: -### `LearnToken.sol` +### `learn_token` -A **soulbound ERC20** token that is minted to learners upon verified milestone -completion. Non-transferable by design — it represents real effort, not -speculation. Your LearnToken balance is your on-chain academic reputation score. +A **soulbound SEP-41 fungible token** that is minted to learners upon verified +milestone completion. Non-transferable by design — it represents real effort, +not speculation. Your LearnToken balance is your on-chain academic reputation +score. -### `GovernanceToken.sol` +### `governance_token` -A **transferable ERC20** distributed to donors upon treasury contribution and -earned by top learners at milestone thresholds. Used exclusively for DAO voting -on scholarship proposals. +A **transferable SEP-41 fungible token** distributed to donors upon treasury +contribution and earned by top learners at milestone thresholds. Used +exclusively for DAO voting on scholarship proposals. -### `CourseMilestone.sol` +### `course_milestone` Tracks learner progress per course. Each course has defined checkpoints verified by a trusted multi-sig validator (transitioning to oracle-based verification in V2). On successful verification, this contract triggers LearnToken minting. -### `ScholarshipTreasury.sol` +### `scholarship_treasury` Holds all donor funds in stablecoins (USDC). Funds can only be released upon successful proposal execution through the governance system. Tracks total contributions per donor. Transparent and auditable by anyone. -### `MilestoneEscrow.sol` +### `milestone_escrow` Manages approved scholarship disbursements in tranches. Funds are released as scholars hit agreed milestones. If a scholar is inactive for 30 days, unspent funds automatically return to the treasury. -### `ScholarNFT.sol` - -Mints a **soulbound ERC721 credential** to scholars who complete their funded -programs. Non-transferable, tamper-proof, and permanently verifiable on-chain. -Shareable with employers, DAOs, and the broader ecosystem. +### `scholar_nft` + +Mints a **soulbound SEP-41 NFT credential** to scholars who complete their +funded programs. Non-transferable, tamper-proof, and permanently verifiable +on-chain. Shareable with employers, DAOs, and the broader ecosystem. + +## Contract Interaction Flow + +```mermaid +sequenceDiagram + participant Learner + participant Frontend + participant CourseMilestone + participant LearnToken + participant Donor + participant ScholarshipTreasury + participant GovernanceToken + participant GOV_Holder + participant MilestoneEscrow + participant Scholar + participant ScholarNFT + participant Treasury + + Note over Learner, ScholarNFT: Learning & Reputation Building + Learner->>Frontend: Complete milestone + Frontend->>CourseMilestone: complete_milestone() + CourseMilestone->>LearnToken: mint(learner, lrn) + LearnToken-->>Learner: LearnTokens earned + + Note over Donor, GovernanceToken: Treasury Funding + Donor->>Frontend: Deposit USDC + Frontend->>ScholarshipTreasury: deposit(usdc) + ScholarshipTreasury->>GovernanceToken: mint(donor, gov) + GovernanceToken-->>Donor: GovernanceTokens earned + + Note over Learner, MilestoneEscrow: Scholarship Process + Learner->>Frontend: Submit scholarship proposal + Frontend->>ScholarshipTreasury: submit_proposal() + + GOV_Holder->>Frontend: Vote on proposal + Frontend->>ScholarshipTreasury: vote() + + ScholarshipTreasury->>MilestoneEscrow: create() [on approval] + + Note over MilestoneEscrow, Treasury: Milestone Completion + MilestoneEscrow->>Scholar: transfer(usdc) [on milestone release] + + Note over MilestoneEscrow, Treasury: Timeout Handling + MilestoneEscrow->>Treasury: transfer(usdc) [on timeout] + + Note over Scholar, ScholarNFT: Program Completion + Scholar->>ScholarNFT: mint() [on program completion] + ScholarNFT-->>Scholar: ScholarNFT credential earned +``` --- @@ -181,7 +250,7 @@ Shareable with employers, DAOs, and the broader ecosystem. LearnVault's flywheel is designed so that effort compounds over time: -``` +```` Complete Lesson │ ▼ @@ -193,21 +262,22 @@ Complete Full Track ──▶ Convert LRN to Governance Tokens ▼ Submit Scholarship Proposal │ - ▼ -Community Votes YES - │ - ▼ -Milestone-Based Funding Released - │ - ▼ -Complete Funded Program - │ - ▼ -Mint ScholarNFT Credential - │ - ▼ -Higher Reputation ──▶ Larger Future Proposals ──▶ Loop Continues -``` + 3. **Friendbot Funding (Testnet Only):** + ```bash + # Fund your deployer address for testing + stellar friendbot fund
+ + # Example: + stellar friendbot fund testnet GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +```` + +**Deployer Address:** + +- For testing on Stellar Testnet, use the friendbot-funded deployer: + `GDU2P3YJ5K7E6ZK3Q2K7E6ZK3Q2K7E6ZK3Q2K7E6ZK3` │ ▼ Mint ScholarNFT Credential │ + ▼ Higher Reputation ──▶ Larger Future Proposals ──▶ Loop Continues + +```` The more you learn, the more power and opportunity you unlock. Wealth is not the barrier — effort is the currency. @@ -242,7 +312,7 @@ resubmitted after 30 days. ### Disbursement -Approved funds are locked in `MilestoneEscrow.sol` and released in tranches as +Approved funds are locked in `milestone_escrow` and released in tranches as the scholar completes agreed milestones. Progress is reported by the scholar and confirmed by a community-elected validator committee (transitioning to oracle verification in V2). @@ -275,10 +345,10 @@ transfers to token holders. | Layer | Technology | | ------------------ | ---------------------------------------------- | -| Blockchain | Stellar (primary), EVM-compatible L2 (planned) | -| Smart Contracts | Solidity / Stellar Soroban | -| Frontend | Next.js, TypeScript, TailwindCSS | -| Wallet Integration | Freighter (Stellar), MetaMask | +| Blockchain | Stellar | +| Smart Contracts | Rust (Stellar Soroban) | +| Frontend | React 19, TypeScript, Stellar Design System | +| Wallet Integration | Freighter (Stellar) | | Storage | IPFS (course content + proposal docs) | | Stablecoin | USDC | | Backend | Node.js, PostgreSQL | @@ -314,6 +384,119 @@ transfers to token holders. --- +## Whitepaper Generation + +The LearnVault Technical Whitepaper is authored in Markdown and exported to PDF. +To ensure Mermaid diagrams render correctly in the PDF export, follow this +two-step build process: + +1. **Compile Diagrams to Images:** Generate static PNGs from the Mermaid source + files using the Mermaid CLI: + + ```bash + npx @mermaid-js/mermaid-cli -i docs/architecture.mmd -o docs/architecture.png +```` + +2. **Generate the PDF:** Once the diagrams are compiled and embedded as standard + markdown image links, generate the final PDF using `md-to-pdf`: + + ```bash + npx md-to-pdf docs/whitepaper.md + ``` + + *** + +## Performance + +- Response compression enabled using Express `compression` middleware + (gzip/brotli); images, video, audio, binary, and IPFS routes are excluded +- `Cache-Control: no-store` is set on all `/api/*` routes to prevent caching of + API responses +- Static assets are not served by the Express backend — caching is handled at + the CDN/proxy layer (Nginx, Cloudflare, Vercel, etc.) +- HTTP/2 is handled via reverse proxy (Nginx/Cloudflare/Vercel) — Express runs + HTTP/1.1 internally +- Request latency logging middleware logs `METHOD URL - Xms` for every request + +```` + +--- + +## Setup + +1. Install dependencies for the frontend and server: + + ```bash + npm install + cd server && npm install + ``` + +2. Copy the environment templates before starting local services: + + ```bash + cp .env.example .env + cp server/.env.example server/.env + ``` + +3. Fill in deployed contract IDs, Pinata credentials, and any server secrets you + need for your local workflow. + +--- + +## Running Tests + +### Prerequisites + +1. **Install Rust and Stellar CLI:** + ```bash + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + npm install -g @stellar/stellar-cli +```` + +2. **Install Visual Studio Build Tools (Windows):** + + ```bash + # Download Visual Studio Build Tools installer + # Visit: https://visualstudio.microsoft.com/downloads/ + # Or use winget: wing install VisualStudio.2022.BuildTools + ``` + +3. **Configure Environment:** + + ```bash + # Copy the root environment template + cp .env.example .env + + # Copy the server environment template + cp server/.env.example server/.env + + # Edit .env with your configuration + # Set STELLAR_SCAFFOLD_ENV=testnet for testnet deployment + ``` + +### Run Tests + +```bash +npm test # runs all Soroban contract tests +npm run test:contracts # alias for the above +npm run test:watch # re-runs tests on file changes +``` + +### Lint and Format Contracts + +Before submitting a PR, ensure Rust contracts pass formatting and lint checks: + +```bash +cargo fmt --all # auto-format all contracts +cargo fmt --all -- --check # check formatting without modifying files (used in CI) +cargo clippy --workspace -- -D warnings # lint all contracts (warnings are errors) +``` + +Formatting rules are defined in `.rustfmt.toml` at the repo root +(`edition = "2024"`, `max_width = 100`). + +--- + ## Contributing LearnVault is an open-source project and welcomes contributions from developers, @@ -330,6 +513,16 @@ To contribute: All contributors are recognized on-chain and in our official documentation. +Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing. We +expect all participants to uphold these standards. + +--- + +## Resources + +- [Glossary](docs/glossary.md) — Key terms, tokens, and contracts explained in + plain English + --- ## Contact @@ -345,4 +538,41 @@ LearnVault, please reach out through our official channels. --- _LearnVault — Built for African learners. Powered by community. Governed by -effort._ +effort._ \n## Architecture Decisions\n\n- [ADR-001.md](docs/adr/ADR-001.md)\n- +[ADR-002.md](docs/adr/ADR-002.md)\n- [ADR-003.md](docs/adr/ADR-003.md)\n- +[ADR-004.md](docs/adr/ADR-004.md)\n- [ADR-005.md](docs/adr/ADR-005.md)\n- +[ADR-006.md](docs/adr/ADR-006.md)\n- [ADR-007.md](docs/adr/ADR-007.md)\n + +## Contributors ✨ + +Thanks goes to these wonderful people +([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + + + + + + +
bakeronchain
bakeronchain

💻 📖
+ + Add your contributions + +
+ + + + + + +This project follows the +[all-contributors](https://github.com/all-contributors/all-contributors) +specification. Contributions of any kind welcome! diff --git a/SECURITY.md b/SECURITY.md index 4880be53..41bdcff9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,161 @@ # Security Policy +LearnVault takes security seriously. We appreciate the work of security +researchers and contributors who help keep the platform and its users safe. +Please read this policy before submitting a report. + +--- + +## Supported Versions + +Only the versions listed below receive security fixes. If you find a +vulnerability in an unsupported version, please verify it is still present in a +supported version before reporting. + +| Version | Status | Security fixes | +| -------------------------- | --------------------- | ---------------------------------- | +| `main` branch (Testnet v1) | ✅ Active development | ✅ Yes | +| Previous commits / tags | Superseded | ❌ No — please test against `main` | +| Mainnet | 🔜 Not yet deployed | N/A | + +--- + +## Scope + +### In scope + +We are interested in vulnerabilities that could harm users, funds, or the +integrity of the LearnVault protocol: + +**Smart contracts** (`contracts/`) + +- Reentrancy attacks on any contract +- Integer overflow / underflow in arithmetic operations +- Access control bypass (calling admin-only functions without authorization) +- Logic that allows unauthorized withdrawal from `scholarship_treasury` +- Exploits against `milestone_escrow` that release funds without meeting + conditions +- Governance manipulation via `governance_token` (vote inflation, double-voting) +- Sybil or replay attacks on `scholar_nft` minting +- Timelock bypass in `upgrade_timelock_vault` +- Allowlist circumvention in `fungible-allowlist` + +**Frontend / dApp** + +- Cross-site scripting (XSS) that could execute arbitrary code in a user's + browser +- Wallet-draining UI — any page or component that causes a wallet to sign a + transaction the user did not explicitly intend +- Phishing vectors hosted on the official domain +- Insecure handling of private keys or seed phrases + +**Protocol logic** + +- Any flow that allows a learner, mentor, or third party to extract funds from + the treasury without meeting the stated course-completion criteria + +### Out of scope + +The following are **not** in scope. Please do not submit reports for: + +- Vulnerabilities in third-party libraries or upstream dependencies — report + these directly to the relevant project +- Issues that require physical access to a user's device +- Theoretical vulnerabilities with no working proof of concept +- Rate-limiting or brute-force issues on read-only public endpoints +- Social engineering attacks targeting LearnVault team members +- Spam or content-policy violations +- Clickjacking on pages without sensitive actions +- Missing security headers on informational/marketing pages + +--- + ## Reporting a Vulnerability -Please reach out to the team using GitHub's own security mechanism to submit an -anonymous report. +**Do not open a public GitHub issue for security vulnerabilities.** + +Use one of the following private channels: + +### Option 1 — GitHub Private Security Advisory (preferred) + +1. Go to the + [LearnVault security advisories page](https://github.com/bakeronchain/learnvault/security/advisories/new). +2. Click **"Report a vulnerability"**. +3. Fill in the template with as much detail as possible. + +GitHub keeps the report private and notifies the maintainers immediately. + +### Option 2 — Email + +Send your report to **security@learnvault.xyz**. + +Encrypt sensitive reports with our PGP key if possible (key available on request +via the email address above). + +### What to include in your report + +Please provide as much of the following as you can: + +- **Description** — what the vulnerability is and why it is exploitable +- **Affected component** — contract name, frontend route, or API endpoint +- **Proof of concept** — steps to reproduce, test script, or transaction hash on + a testnet demonstrating the issue +- **Impact assessment** — what an attacker could achieve (e.g. drain treasury, + mint arbitrary NFTs) +- **Suggested fix** (optional but appreciated) + +--- + +## Response Timeline + +| Milestone | Target time | +| ------------------------------ | --------------------------------------------------------- | +| Acknowledgement of receipt | Within **48 hours** | +| Initial severity assessment | Within **7 days** | +| Fix timeline communicated | Within **14 days** of acknowledgement | +| Patch released (critical/high) | Best effort, typically within **14 days** of confirmation | +| Patch released (medium/low) | Included in the next scheduled release | + +We will keep you updated throughout the process. If you have not received an +acknowledgement within 48 hours, please follow up at +**security@learnvault.xyz**. + +--- + +## Disclosure Policy + +LearnVault follows **coordinated disclosure**: + +- Please **do not publish or share** details of a vulnerability before a fix has + been released. +- Once a fix is deployed we will coordinate a public disclosure date with you. + The default embargo period is **90 days** from the initial report, or sooner + if both parties agree. +- We will credit you in the release notes and changelog unless you prefer to + remain anonymous. +- We will not pursue legal action against researchers who act in good faith and + follow this policy. + +--- + +## Recognition + +Responsibly disclosed vulnerabilities will be credited in the project's release +notes. If you would like to be acknowledged: + +- Tell us your preferred name / handle in the report. +- We will list you under **"Security"** in the relevant release or CHANGELOG + entry. + +We do not currently operate a paid bug bounty programme, but we are grateful for +every responsible disclosure and aim to recognize the effort publicly. + +--- + +## Contact + +| Channel | Address | +| ---------------------------- | --------------------------------------------------------------------------------------------- | +| Security reports (preferred) | [GitHub Private Advisory](https://github.com/bakeronchain/learnvault/security/advisories/new) | +| Security email | security@learnvault.xyz | +| General enquiries | hello@learnvault.xyz | diff --git a/TEST_COVERAGE_SUMMARY.md b/TEST_COVERAGE_SUMMARY.md new file mode 100644 index 00000000..e4abd69b --- /dev/null +++ b/TEST_COVERAGE_SUMMARY.md @@ -0,0 +1,169 @@ +# LearnToken (LRN) Test Coverage Summary + +## Overview + +Comprehensive unit test suite for the LearnToken contract, validating all core +functionality including minting, soulbound enforcement, balance tracking, +reputation scoring, and admin operations. + +**Test Results:** ✅ **36 tests passed; 0 failed; 1 ignored** + +## Test Categories + +### 1. Initialization Tests (3 tests) + +- `initialize_sets_admin_correctly` - Verifies admin is set during + initialization +- `initialize_sets_name_symbol_decimals` - Validates metadata (LRN, 7 decimals, + etc.) +- `double_initialize_rejected` - Ensures contract cannot be re-initialized +- `initialized_contract_has_all_metadata` - Comprehensive metadata validation + post-init + +**Coverage:** Initialization mechanism, immutability of setup + +### 2. Minting Tests (7 tests) + +- `mint_increases_balance_and_supply` - Basic mint operation +- `mint_accumulates_on_repeated_calls` - Multiple mints to same account +- `mint_to_multiple_accounts_tracks_supply` - Supply consistency across accounts +- `mint_before_initialize_panics` - Error: mint before initialization +- `zero_amount_mint_panics` - Error: zero amount mint +- `negative_amount_mint_panics` - Error: negative amount mint +- `non_admin_mint_panics` - Error: non-admin cannot mint +- `large_mint_amounts_tracked_correctly` - Large supply handling +- `multiple_small_mints_vs_single_large_mint` - Accumulation equivalence + +**Coverage:** Mint authorization, amount validation, supply tracking, account +isolation + +### 3. Soulbound Transfer Prevention Tests (6 tests) + +- `transfer_panics_with_soulbound_error` - Basic transfer rejection +- `transfer_always_panics_even_with_zero_amount` - Zero-amount transfer still + fails +- `transfer_from_panics_with_soulbound_error` - SEP-41 transfer_from blocked +- `transfer_from_always_panics_even_with_zero_amount` - Zero-amount + transfer_from fails +- `transfer_from_panics_regardless_of_spender` - Soulbound enforced for all + spenders +- `approve_panics_with_soulbound_error` - SEP-41 approve blocked +- `approve_always_panics_even_with_zero_amount` - Zero-amount approve fails +- `approve_panics_even_for_non_existent_balance` - Approve fails regardless of + balance + +**Coverage:** Soulbound invariant enforcement across all transfer methods, no +escapehatch with zero amounts + +### 4. Allowance Tests (3 tests) + +- `allowance_returns_zero` - Allowance always zero (no delegations) +- `allowance_always_returns_zero_regardless_of_accounts` - Zero for all account + pairs +- `allowance_returns_zero_for_same_address` - Zero even for self-approval + +**Coverage:** Allowance consistency with soulbound nature + +### 5. Balance & Supply Tests (2 tests) + +- `balance_of_unknown_account_is_zero` - Uninitialized accounts have zero + balance +- `total_supply_starts_at_zero` - Initial supply is zero + +**Coverage:** Default state, balance initialization + +### 6. Reputation Scoring Tests (3 tests) + +- `reputation_score_zero_for_unknown_address` - Unknown accounts score 0 +- `reputation_score_increases_with_balance` - Reputation tracks balance growth +- `reputation_score_proportional_to_balance` - Reputation = balance / 100 +- `reputation_score_matches_balance_division` - Comprehensive division + correctness + +**Coverage:** Reputation calculation correctness, formula validation + +### 7. Admin Management Tests (3 tests) + +- `set_admin_transfers_admin_rights` - New admin can be set +- `set_admin_only_callable_by_current_admin` - Non-admin cannot set_admin +- `set_admin_emits_event` - Admin transfer emits AdminChanged event +- `admin_transfers_always_succeed` - Multi-hop admin transfers work + +**Coverage:** Admin-only operation, authorization, event emission + +### 8. Version & Metadata Tests (1 test) + +- `get_version_returns_semver` - Version returns "1.0.0" + +**Coverage:** Version reporting + +### 9. Event Emission Tests (2 tests) + +- `mint_emits_event` - Mint emits MintToken event +- `set_admin_emits_event` - Admin transfer emits AdminChanged event + +**Coverage:** Event system, off-chain monitoring + +## Acceptance Criteria Verification + +✅ **All 7 core functions tested:** + +- `initialize` - 4 tests +- `mint` - 9 tests +- `transfer` - 2 tests (soulbound enforcement) +- `transfer_from` - 3 tests (soulbound enforcement) +- `approve` - 4 tests (soulbound enforcement) +- `balance` - 2 tests +- `reputation_score` - 4 tests +- `allowance` - 3 tests (always zero) +- `set_admin` - 4 tests +- `total_supply` - 2 tests +- `get_version` - 1 test +- `name/symbol/decimals` - 2 tests + +✅ **Soulbound Invariant Verified:** + +- All transfer mechanisms (transfer, transfer_from) panic with + LRNError::Soulbound +- No edge case bypasses (zero amounts still fail) +- Approve and allowance properly constrained + +✅ **Admin Controls Validated:** + +- Only admin can mint +- Only current admin can transfer admin role +- Admin transfers can be chained + +✅ **Test Execution Results:** + +``` +Finished in 0.27s +✓ 36 passed +✗ 0 failed +⊘ 1 ignored (fuzz test) +``` + +## Key Guarantees Provided + +1. **Reputation Accuracy**: Reputation always equals balance / 100 (integer + division) +2. **Non-Transferability**: No code path allows token transfers or approvals +3. **Supply Consistency**: Total supply matches sum of all account balances +4. **Admin Authority**: Only current admin can mint and transfer admin role +5. **Event Transparency**: All state transitions properly emitted as events +6. **Edge Case Safety**: Zero amounts, unknown accounts, and boundary values all + handled + +## Contract Readiness for Production + +This comprehensive test suite validates that LearnToken (LRN) is +**production-ready** for: + +- Mainnet deployment as the core reputation primitive +- Milestone completion minting workflows +- Fair reputation scoring across all learners +- Secure admin-only token generation +- Immutable soulbound enforcement + +All 36 tests pass with zero failures, confirming the contract implementation +matches specification. diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..605018d4 --- /dev/null +++ b/TODO.md @@ -0,0 +1,30 @@ +# Event Indexer Implementation TODO + +## Status: 12/12 ✅ COMPLETE + +### 1. Create DB migration `server/src/db/migrations/004_events.sql` ✅ + +### 2. Run migration `cd server && npm run db:migrate` ✅ User run + +### 3. Create `server/src/types/events.ts` ✅ + +### 4. Create `server/src/lib/event-config.ts` ✅ + +### 5. Create `server/src/services/event-indexer.service.ts` ✅ + +### 6. Create `server/src/workers/event-poller.ts` ✅ + +### 7. Edit `server/src/index.ts` to start poller ✅ + +### 8. Edit `server/src/controllers/events.controller.ts` for real DB queries ✅ + +### 9. Update `server/src/routes/events.routes.ts` OpenAPI params (?contract ?address) ✅ + +### 10. Inline Event schema in routes ✅ (no openapi.ts) + +### 11. Add env vars to server/.env.example ✅ + +### 12. Test: Set env vars from .env.example, run `cd server && npm run dev`, poller logs, GET /api/events [ ] + +**Setup: Copy server/.env.example -> server/.env, set DATABASE_URL & +CONTRACT_IDs (from scripts/deploy-testnet.sh), STARTING_LEDGER=460000000** diff --git a/TREASURY_API_IMPLEMENTATION.md b/TREASURY_API_IMPLEMENTATION.md new file mode 100644 index 00000000..140f5d47 --- /dev/null +++ b/TREASURY_API_IMPLEMENTATION.md @@ -0,0 +1,89 @@ +# Treasury API Implementation + +## Overview + +This implementation adds two new API endpoints to fetch real treasury data from +the ScholarshipTreasury smart contract on Stellar/Soroban, and connects the +frontend Treasury page to consume this data. + +## Backend Endpoints + +### GET /api/treasury/stats + +Returns aggregated treasury statistics. + +**Response:** + +```json +{ + "total_deposited_usdc": "125400000000", + "total_disbursed_usdc": "98200000000", + "scholars_funded": 128, + "active_proposals": 12, + "donors_count": 47 +} +``` + +### GET /api/treasury/activity + +Returns recent treasury activity with pagination. + +**Query Parameters:** + +- `limit` (optional): Max events to return (1-100, default: 20) +- `offset` (optional): Number of events to skip (default: 0) + +**Response:** + +```json +{ + "events": [ + { + "type": "deposit", + "amount": "500000000", + "address": "G...", + "tx_hash": "...", + "created_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +## Frontend Integration + +The Treasury page (`src/pages/Treasury.tsx`) now: + +- Fetches real-time stats and activity on component mount +- Displays loading states while fetching data +- Formats USDC amounts from stroops (divides by 10^7) +- Formats addresses and timestamps for better UX +- Shows "No activity yet" when no events exist +- Handles API errors gracefully + +## Files Created + +- `server/src/controllers/treasury.controller.ts` - Business logic +- `server/src/routes/treasury.routes.ts` - Route definitions + +## Files Modified + +- `server/src/index.ts` - Registered treasury routes +- `src/pages/Treasury.tsx` - Connected to API endpoints +- `.env.example` - Added VITE_SERVER_URL configuration + +## Configuration + +### Backend (.env in server/) + +Requires `SCHOLARSHIP_TREASURY_CONTRACT_ID` in `server/.env` + +### Frontend (.env in root) + +Requires `VITE_SERVER_URL` (defaults to http://localhost:4000) + +## Testing + +1. Start the backend: `cd server && npm run dev` +2. Start the frontend: `npm run dev` +3. Visit http://localhost:5173/treasury +4. Verify stats and activity load from the API diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..1d7a1ef1 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,47 @@ +coverage: + status: + project: + default: + target: 80% + # Allow up to 2% drop before failing — absorbs noise from small PRs + threshold: 2% + patch: + default: + target: 80% + threshold: 5% + +flag_management: + default_rules: + # Carry the last uploaded report forward when a flag isn't re-uploaded + # (e.g. only the backend changed, so frontend coverage stays in history) + carryforward: true + +flags: + frontend: + paths: + - src/ + carryforward: true + backend: + paths: + - server/src/ + carryforward: true + +ignore: + # Generated contract clients + - "src/contracts/**" + # Test infrastructure + - "src/test/**" + - "server/src/tests/**" + # Entry points + - "src/main.tsx" + - "server/src/index.ts" + # Generated / boilerplate + - "server/src/openapi.ts" + - "server/src/templates/**" + - "server/src/types/**" + - "server/src/types.d.ts" + # Type declarations + - "**/*.d.ts" + # Test files + - "**/*.test.ts" + - "**/*.test.tsx" diff --git a/contracts/course_milestone/Cargo.toml b/contracts/course_milestone/Cargo.toml new file mode 100644 index 00000000..fa76ce9b --- /dev/null +++ b/contracts/course_milestone/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "course_milestone" +description = "The CourseMilestone contract tracks each learner's progress and on-chain milestone submissions." +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[package.metadata.stellar] +cargo_inherit = true + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +learnvault-shared = { workspace = true } +soroban-sdk = { workspace = true } +stellar-registry = "0.0.4" + +[dev-dependencies] +learnvault-shared = { workspace = true, features = ["testutils"] } +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/course_milestone/src/interface.rs b/contracts/course_milestone/src/interface.rs new file mode 100644 index 00000000..bfd7f30e --- /dev/null +++ b/contracts/course_milestone/src/interface.rs @@ -0,0 +1,8 @@ +use soroban_sdk::{Address, Env, contractclient}; + +#[allow(dead_code)] +#[contractclient(name = "LearnTokenClient")] +pub trait LearnToken { + fn mint(env: Env, to: Address, amount: i128); + fn balance(env: Env, account: Address) -> i128; +} diff --git a/contracts/course_milestone/src/learn_token_client.rs b/contracts/course_milestone/src/learn_token_client.rs new file mode 100644 index 00000000..dd367f19 --- /dev/null +++ b/contracts/course_milestone/src/learn_token_client.rs @@ -0,0 +1,9 @@ +use soroban_sdk::{Address, Env, contractclient, contracterror}; + +use crate::Error; + +#[contractclient(name = "LearnTokenClient")] +pub trait LearnToken { + fn mint(env: Env, to: Address, amount: i128); + fn balance(env: Env, account: Address) -> i128; +} diff --git a/contracts/course_milestone/src/lib.rs b/contracts/course_milestone/src/lib.rs new file mode 100644 index 00000000..603b2a37 --- /dev/null +++ b/contracts/course_milestone/src/lib.rs @@ -0,0 +1,766 @@ +#![no_std] +#![allow(deprecated)] + +use soroban_sdk::{ + Address, BytesN, Env, String, Symbol, Vec, contract, contracterror, contractimpl, contracttype, + panic_with_error, symbol_short, +}; + +/// A single entry in a batch verification call. +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct VerifyBatchEntry { + pub learner: Address, + pub course_id: String, + pub milestone_id: u32, + pub lrn_reward: i128, +} + +mod interface; +use interface::LearnTokenClient; + +use learnvault_shared::upgrade; + +pub use upgrade::ContractUpgraded; + +const DAY_IN_LEDGERS: u32 = 17_280; +const INSTANCE_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; +const PERSISTENT_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const PERSISTENT_EXTEND_TO: u32 = DAY_IN_LEDGERS * 365; + +#[contracttype] +pub enum DataKey { + Enrollment(Address, String), + MilestoneState(Address, String, u32), + MilestoneSubmission(Address, String, u32), + MilestoneLrn(String, u32), + Completed(Address, String, u32), + EnrolledCourses(Address), + Course(String), + CourseIds, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct CourseConfig { + pub milestone_count: u32, + pub active: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub enum MilestoneStatus { + NotStarted, + Pending, + Approved, + Rejected, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct MilestoneSubmission { + pub evidence_uri: String, + pub submitted_at: u64, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct SubmittedEventData { + pub learner: Address, + pub course_id: String, + pub evidence_uri: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct EnrolledEventData { + pub learner: Address, + pub course_id: String, +} + +const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); +const LEARN_TOKEN_KEY: Symbol = symbol_short!("LRN_TKN"); +const PAUSED_KEY: Symbol = symbol_short!("PAUSED"); + +#[contracterror] +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + AlreadyInitialized = 1, + NotInitialized = 2, + Unauthorized = 3, + CourseNotFound = 4, + MilestoneAlreadyCompleted = 5, + CourseAlreadyComplete = 6, + InvalidMilestones = 7, + CourseAlreadyExists = 8, + NotEnrolled = 9, + DuplicateSubmission = 10, + ContractPaused = 11, + AlreadyEnrolled = 12, + InvalidState = 13, + AlreadyCompleted = 14, + InvalidReward = 15, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct MilestoneCompleted { + pub learner: Address, + pub course_id: String, + pub milestone_id: u32, + pub lrn_reward: i128, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct CourseCompleted { + pub learner: Address, + pub course_id: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct CourseAdded { + pub course_id: String, + pub total_milestones: u32, + pub tokens_per_milestone: i128, +} + +#[contract] +pub struct CourseMilestone; + +#[contractimpl] +impl CourseMilestone { + pub fn initialize(env: Env, admin: Address, learn_token_contract: Address) { + if env.storage().instance().has(&ADMIN_KEY) { + panic_with_error!(&env, Error::AlreadyInitialized); + } + admin.require_auth(); + env.storage().instance().set(&ADMIN_KEY, &admin); + upgrade::init(&env); + env.storage() + .instance() + .set(&LEARN_TOKEN_KEY, &learn_token_contract); + + Self::extend_instance(&env); + } + + pub fn add_course(env: Env, admin: Address, course_id: String, milestone_count: u32) { + Self::require_initialized(&env); + Self::require_admin(&env, &admin); + + if milestone_count == 0 { + panic_with_error!(&env, Error::InvalidMilestones); + } + + let course_key = DataKey::Course(course_id.clone()); + if env.storage().persistent().has(&course_key) { + panic_with_error!(&env, Error::CourseAlreadyExists); + } + + let config = CourseConfig { + milestone_count, + active: true, + }; + env.storage().persistent().set(&course_key, &config); + + let mut course_ids: Vec = env + .storage() + .persistent() + .get(&DataKey::CourseIds) + .unwrap_or_else(|| Vec::new(&env)); + course_ids.push_back(course_id); + env.storage() + .persistent() + .set(&DataKey::CourseIds, &course_ids); + + Self::extend_persistent(&env, &course_key); + Self::extend_persistent(&env, &DataKey::CourseIds); + } + + pub fn set_milestone_reward(env: Env, course_id: String, milestone_id: u32, lrn: i128) { + Self::assert_not_paused(&env); + Self::require_initialized(&env); + Self::require_stored_admin_auth(&env); + + if !Self::is_course_active(&env, &course_id) { + panic_with_error!(&env, Error::CourseNotFound); + } + + if lrn < 0 { + panic_with_error!(&env, Error::InvalidReward); + } + + let reward_key = DataKey::MilestoneLrn(course_id, milestone_id); + env.storage().persistent().set(&reward_key, &lrn); + Self::extend_persistent(&env, &reward_key); + } + + pub fn remove_course(env: Env, admin: Address, course_id: String) { + Self::require_initialized(&env); + Self::require_admin(&env, &admin); + + let course_key = DataKey::Course(course_id); + let mut config: CourseConfig = env + .storage() + .persistent() + .get(&course_key) + .unwrap_or_else(|| panic_with_error!(&env, Error::CourseNotFound)); + config.active = false; + env.storage().persistent().set(&course_key, &config); + Self::extend_persistent(&env, &course_key); + } + + pub fn get_course(env: Env, course_id: String) -> Option { + let course_key = DataKey::Course(course_id); + let course: Option = env.storage().persistent().get(&course_key); + if course.is_some() { + Self::extend_persistent(&env, &course_key); + } + course + } + + pub fn list_courses(env: Env) -> Vec { + let course_ids: Vec = env + .storage() + .persistent() + .get(&DataKey::CourseIds) + .unwrap_or_else(|| Vec::new(&env)); + + let mut active_courses = Vec::new(&env); + let mut i = 0; + while i < course_ids.len() { + let course_id = course_ids.get(i).unwrap(); + let course_key = DataKey::Course(course_id.clone()); + let config: Option = env.storage().persistent().get(&course_key); + if let Some(current) = config { + Self::extend_persistent(&env, &course_key); + if current.active { + active_courses.push_back(course_id); + } + } + i += 1; + } + + active_courses + } + + pub fn pause(env: Env, admin: Address) { + admin.require_auth(); + let stored_admin: Address = env.storage().instance().get(&ADMIN_KEY).unwrap(); + if admin != stored_admin { + panic_with_error!(&env, Error::Unauthorized); + } + env.storage().instance().set(&PAUSED_KEY, &true); + } + + pub fn unpause(env: Env, admin: Address) { + admin.require_auth(); + let stored_admin: Address = env.storage().instance().get(&ADMIN_KEY).unwrap(); + if admin != stored_admin { + panic_with_error!(&env, Error::Unauthorized); + } + env.storage().instance().set(&PAUSED_KEY, &false); + } + + pub fn is_paused(env: Env) -> bool { + env.storage().instance().get(&PAUSED_KEY).unwrap_or(false) + } + + fn assert_not_paused(env: &Env) { + if Self::is_paused(env.clone()) { + panic_with_error!(&env, Error::ContractPaused); + } + } + + pub fn enroll(env: Env, learner: Address, course_id: String) { + Self::assert_not_paused(&env); + Self::require_initialized(&env); + learner.require_auth(); + + if !Self::is_course_active(&env, &course_id) { + panic_with_error!(&env, Error::CourseNotFound); + } + + let key = DataKey::Enrollment(learner.clone(), course_id.clone()); + if env.storage().persistent().has(&key) { + panic_with_error!(&env, Error::AlreadyEnrolled); + } + + env.storage().persistent().set(&key, &true); + Self::extend_persistent(&env, &key); + + let courses_key = DataKey::EnrolledCourses(learner.clone()); + let mut courses: Vec = env + .storage() + .persistent() + .get(&courses_key) + .unwrap_or_else(|| Vec::new(&env)); + courses.push_back(course_id.clone()); + env.storage().persistent().set(&courses_key, &courses); + Self::extend_persistent(&env, &courses_key); + + env.events().publish( + (symbol_short!("enrolled"),), + EnrolledEventData { learner, course_id }, + ); + } + + pub fn is_enrolled(env: Env, learner: Address, course_id: String) -> bool { + let key = DataKey::Enrollment(learner, course_id); + let enrolled = env.storage().persistent().get(&key).unwrap_or(false); + if enrolled { + Self::extend_persistent(&env, &key); + } + enrolled + } + + pub fn submit_milestone( + env: Env, + learner: Address, + course_id: String, + milestone_id: u32, + evidence_uri: String, + ) { + Self::assert_not_paused(&env); + Self::require_initialized(&env); + learner.require_auth(); + + if !Self::is_enrolled(env.clone(), learner.clone(), course_id.clone()) { + panic_with_error!(&env, Error::NotEnrolled); + } + + let state_key = DataKey::MilestoneState(learner.clone(), course_id.clone(), milestone_id); + let current_state = env + .storage() + .persistent() + .get::<_, MilestoneStatus>(&state_key) + .unwrap_or(MilestoneStatus::NotStarted); + + if current_state != MilestoneStatus::NotStarted { + panic_with_error!(&env, Error::DuplicateSubmission); + } + + let submission = MilestoneSubmission { + evidence_uri: evidence_uri.clone(), + submitted_at: env.ledger().timestamp(), + }; + + let submission_key = + DataKey::MilestoneSubmission(learner.clone(), course_id.clone(), milestone_id); + + env.storage().persistent().set(&submission_key, &submission); + env.storage() + .persistent() + .set(&state_key, &MilestoneStatus::Pending); + + Self::extend_persistent(&env, &submission_key); + Self::extend_persistent(&env, &state_key); + + env.events().publish( + (symbol_short!("submitted"), milestone_id), + SubmittedEventData { + learner, + course_id, + evidence_uri, + }, + ); + } + + pub fn get_milestone_state( + env: Env, + learner: Address, + course_id: String, + milestone_id: u32, + ) -> MilestoneStatus { + Self::extend_instance(&env); + let key = DataKey::MilestoneState(learner, course_id, milestone_id); + if let Some(state) = env.storage().persistent().get::<_, MilestoneStatus>(&key) { + Self::extend_persistent(&env, &key); + state + } else { + MilestoneStatus::NotStarted + } + } + + pub fn get_milestone_submission( + env: Env, + learner: Address, + course_id: String, + milestone_id: u32, + ) -> Option { + let key = DataKey::MilestoneSubmission(learner, course_id, milestone_id); + let submission: Option = env.storage().persistent().get(&key); + if submission.is_some() { + Self::extend_persistent(&env, &key); + } + submission + } + + pub fn get_enrolled_courses(env: Env, learner: Address) -> Vec { + let key = DataKey::EnrolledCourses(learner); + let courses: Vec = env + .storage() + .persistent() + .get(&key) + .unwrap_or_else(|| Vec::new(&env)); + if !courses.is_empty() { + Self::extend_persistent(&env, &key); + } + courses + } + + pub fn complete_milestone(env: Env, learner: Address, course_id: String, milestone_id: u32) { + Self::assert_not_paused(&env); + Self::require_initialized(&env); + Self::require_stored_admin_auth(&env); + + if !Self::is_enrolled(env.clone(), learner.clone(), course_id.clone()) { + panic_with_error!(&env, Error::NotEnrolled); + } + + let completed_key = DataKey::Completed(learner.clone(), course_id.clone(), milestone_id); + let already_completed = env + .storage() + .persistent() + .get::<_, bool>(&completed_key) + .unwrap_or(false); + if already_completed { + panic_with_error!(&env, Error::AlreadyCompleted); + } + + let state_key = DataKey::MilestoneState(learner.clone(), course_id.clone(), milestone_id); + let current_state = env + .storage() + .persistent() + .get::<_, MilestoneStatus>(&state_key) + .unwrap_or(MilestoneStatus::NotStarted); + if current_state == MilestoneStatus::Approved { + panic_with_error!(&env, Error::AlreadyCompleted); + } + + let reward_key = DataKey::MilestoneLrn(course_id.clone(), milestone_id); + let lrn_reward = env + .storage() + .persistent() + .get(&reward_key) + .unwrap_or(0_i128); + + env.storage().persistent().set(&completed_key, &true); + env.storage() + .persistent() + .set(&state_key, &MilestoneStatus::Approved); + + Self::extend_persistent(&env, &completed_key); + Self::extend_persistent(&env, &state_key); + if env.storage().persistent().has(&reward_key) { + Self::extend_persistent(&env, &reward_key); + } + + env.events().publish( + (symbol_short!("ms_done"),), + MilestoneCompleted { + learner: learner.clone(), + course_id: course_id.clone(), + milestone_id, + lrn_reward, + }, + ); + + Self::emit_course_completed_if_ready(&env, &learner, &course_id); + } + + pub fn is_completed(env: Env, learner: Address, course_id: String, milestone_id: u32) -> bool { + let completed_key = DataKey::Completed(learner, course_id, milestone_id); + let completed = env + .storage() + .persistent() + .get::<_, bool>(&completed_key) + .unwrap_or(false); + if completed { + Self::extend_persistent(&env, &completed_key); + } + completed + } + + /// Replace the current contract WASM with a new uploaded hash. Admin only. + pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { + Self::require_initialized(&env); + Self::extend_instance(&env); + let admin: Address = env.storage().instance().get(&ADMIN_KEY).unwrap(); + admin.require_auth(); + upgrade::apply(&env, &admin, &new_wasm_hash); + } + + pub fn get_version(env: Env) -> String { + String::from_str(&env, "1.0.0") + } + + pub fn verify_milestone( + env: Env, + admin: Address, + learner: Address, + course_id: String, + milestone_id: u32, + tokens_amount: i128, + ) { + if Self::is_paused(env.clone()) { + panic_with_error!(&env, Error::ContractPaused); + } + + Self::require_initialized(&env); + admin.require_auth(); + + let stored_admin: Address = env.storage().instance().get(&ADMIN_KEY).unwrap(); + if admin != stored_admin { + panic_with_error!(&env, Error::Unauthorized); + } + + if !Self::is_enrolled(env.clone(), learner.clone(), course_id.clone()) { + panic_with_error!(&env, Error::NotEnrolled); + } + + let state_key = DataKey::MilestoneState(learner.clone(), course_id.clone(), milestone_id); + let current_state = env + .storage() + .persistent() + .get::<_, MilestoneStatus>(&state_key) + .unwrap_or(MilestoneStatus::NotStarted); + + if current_state != MilestoneStatus::Pending { + panic_with_error!(&env, Error::InvalidState); + } + + env.storage() + .persistent() + .set(&state_key, &MilestoneStatus::Approved); + let completed_key = DataKey::Completed(learner.clone(), course_id.clone(), milestone_id); + env.storage().persistent().set(&completed_key, &true); + + let learn_token_address: Address = env.storage().instance().get(&LEARN_TOKEN_KEY).unwrap(); + let learn_token_client = LearnTokenClient::new(&env, &learn_token_address); + learn_token_client.mint(&learner, &tokens_amount); + + Self::extend_persistent(&env, &state_key); + Self::extend_persistent(&env, &completed_key); + + env.events().publish( + (symbol_short!("ms_done"),), + MilestoneCompleted { + learner: learner.clone(), + course_id: course_id.clone(), + milestone_id, + lrn_reward: tokens_amount, + }, + ); + + Self::emit_course_completed_if_ready(&env, &learner, &course_id); + } + + /// Verify multiple milestone submissions in a single atomic transaction. + /// + /// Each [`VerifyBatchEntry`] is `(learner, course_id, milestone_id, lrn_reward)`. + /// If any single verification fails the entire batch reverts. + /// Emits a `MilestoneCompleted` event for each successful entry. + pub fn batch_verify_milestones(env: Env, admin: Address, submissions: Vec) { + if Self::is_paused(env.clone()) { + panic_with_error!(&env, Error::ContractPaused); + } + + Self::require_initialized(&env); + admin.require_auth(); + + let stored_admin: Address = env.storage().instance().get(&ADMIN_KEY).unwrap(); + if admin != stored_admin { + panic_with_error!(&env, Error::Unauthorized); + } + + let learn_token_address: Address = env.storage().instance().get(&LEARN_TOKEN_KEY).unwrap(); + let learn_token_client = LearnTokenClient::new(&env, &learn_token_address); + + let mut i = 0; + while i < submissions.len() { + let entry = submissions.get(i).unwrap(); + + if !Self::is_enrolled(env.clone(), entry.learner.clone(), entry.course_id.clone()) { + panic_with_error!(&env, Error::NotEnrolled); + } + + let state_key = DataKey::MilestoneState( + entry.learner.clone(), + entry.course_id.clone(), + entry.milestone_id, + ); + let current_state = env + .storage() + .persistent() + .get::<_, MilestoneStatus>(&state_key) + .unwrap_or(MilestoneStatus::NotStarted); + + if current_state != MilestoneStatus::Pending { + panic_with_error!(&env, Error::InvalidState); + } + + env.storage() + .persistent() + .set(&state_key, &MilestoneStatus::Approved); + + let completed_key = DataKey::Completed( + entry.learner.clone(), + entry.course_id.clone(), + entry.milestone_id, + ); + env.storage().persistent().set(&completed_key, &true); + learn_token_client.mint(&entry.learner, &entry.lrn_reward); + + Self::extend_persistent(&env, &state_key); + Self::extend_persistent(&env, &completed_key); + + env.events().publish( + (symbol_short!("ms_done"),), + MilestoneCompleted { + learner: entry.learner.clone(), + course_id: entry.course_id.clone(), + milestone_id: entry.milestone_id, + lrn_reward: entry.lrn_reward, + }, + ); + + i += 1; + } + } + + pub fn reject_milestone( + env: Env, + admin: Address, + learner: Address, + course_id: String, + milestone_id: u32, + ) { + if Self::is_paused(env.clone()) { + panic_with_error!(&env, Error::ContractPaused); + } + + Self::require_initialized(&env); + admin.require_auth(); + + let stored_admin: Address = env.storage().instance().get(&ADMIN_KEY).unwrap(); + if admin != stored_admin { + panic_with_error!(&env, Error::Unauthorized); + } + + if !Self::is_enrolled(env.clone(), learner.clone(), course_id.clone()) { + panic_with_error!(&env, Error::NotEnrolled); + } + + let state_key = DataKey::MilestoneState(learner.clone(), course_id.clone(), milestone_id); + let current_state = env + .storage() + .persistent() + .get::<_, MilestoneStatus>(&state_key) + .unwrap_or(MilestoneStatus::NotStarted); + + if current_state != MilestoneStatus::Pending { + panic_with_error!(&env, Error::InvalidState); + } + + env.storage() + .persistent() + .set(&state_key, &MilestoneStatus::Rejected); + + let submission_key = DataKey::MilestoneSubmission(learner, course_id, milestone_id); + env.storage().persistent().remove(&submission_key); + } + + fn require_initialized(env: &Env) { + if !env.storage().instance().has(&ADMIN_KEY) { + panic_with_error!(env, Error::NotInitialized); + } + } + + fn require_admin(env: &Env, admin: &Address) { + admin.require_auth(); + let stored_admin: Address = env + .storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); + if stored_admin != *admin { + panic_with_error!(env, Error::Unauthorized); + } + } + + fn require_stored_admin_auth(env: &Env) { + let stored_admin: Address = env + .storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); + stored_admin.require_auth(); + } + + fn is_course_active(env: &Env, course_id: &String) -> bool { + let course_key = DataKey::Course(course_id.clone()); + match env + .storage() + .persistent() + .get::<_, CourseConfig>(&course_key) + { + Some(config) => { + Self::extend_persistent(env, &course_key); + config.active + } + None => false, + } + } + + fn emit_course_completed_if_ready(env: &Env, learner: &Address, course_id: &String) { + let course_key = DataKey::Course(course_id.clone()); + let config: CourseConfig = match env.storage().persistent().get(&course_key) { + Some(cfg) => cfg, + None => return, + }; + Self::extend_persistent(env, &course_key); + + let mut milestone_id = 1_u32; + while milestone_id <= config.milestone_count { + let state_key = + DataKey::MilestoneState(learner.clone(), course_id.clone(), milestone_id); + let state = env + .storage() + .persistent() + .get::<_, MilestoneStatus>(&state_key) + .unwrap_or(MilestoneStatus::NotStarted); + if state != MilestoneStatus::Approved { + return; + } + Self::extend_persistent(env, &state_key); + milestone_id += 1; + } + + env.events().publish( + (Symbol::new(env, "course_done"),), + CourseCompleted { + learner: learner.clone(), + course_id: course_id.clone(), + }, + ); + } + + fn extend_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_BUMP_THRESHOLD, INSTANCE_EXTEND_TO); + } + + fn extend_persistent(env: &Env, key: &DataKey) { + env.storage() + .persistent() + .extend_ttl(key, PERSISTENT_BUMP_THRESHOLD, PERSISTENT_EXTEND_TO); + } +} + +#[cfg(test)] +mod test; diff --git a/contracts/course_milestone/src/test.rs b/contracts/course_milestone/src/test.rs new file mode 100644 index 00000000..8aa72dbb --- /dev/null +++ b/contracts/course_milestone/src/test.rs @@ -0,0 +1,782 @@ +extern crate std; + +use soroban_sdk::{ + Address, BytesN, Env, IntoVal, String, Symbol, Val, Vec, contract, contractimpl, contracttype, + symbol_short, + testutils::{Address as _, Events as _, MockAuth, MockAuthInvoke}, +}; + +use crate::{ + CourseCompleted, CourseConfig, CourseMilestone, CourseMilestoneClient, DataKey, Error, + MilestoneCompleted, MilestoneStatus, VerifyBatchEntry, +}; + +#[contracttype] +enum MockTokenDataKey { + Balance(Address), +} + +#[contract] +struct MockLearnToken; + +#[contractimpl] +impl MockLearnToken { + pub fn mint(env: Env, to: Address, amount: i128) { + let key = MockTokenDataKey::Balance(to.clone()); + let balance = env.storage().persistent().get(&key).unwrap_or(0_i128); + env.storage().persistent().set(&key, &(balance + amount)); + } + + pub fn balance(env: Env, account: Address) -> i128 { + env.storage() + .persistent() + .get(&MockTokenDataKey::Balance(account)) + .unwrap_or(0_i128) + } +} + +fn sid(env: &Env, value: &str) -> String { + String::from_str(env, value) +} + +fn authorize(env: &Env, address: &Address, contract: &Address, fn_name: &'static str, args: T) +where + T: IntoVal>, +{ + env.mock_auths(&[MockAuth { + address, + invoke: &MockAuthInvoke { + contract, + fn_name, + args: args.into_val(env), + sub_invokes: &[], + }, + }]); +} + +fn setup() -> ( + Env, + Address, + Address, + Address, + CourseMilestoneClient<'static>, + MockLearnTokenClient<'static>, +) { + let env = Env::default(); + let admin = Address::generate(&env); + let learn_token_id = env.register(MockLearnToken, ()); + let contract_id = env.register(CourseMilestone, ()); + + let client = CourseMilestoneClient::new(&env, &contract_id); + let token_client = MockLearnTokenClient::new(&env, &learn_token_id); + + authorize( + &env, + &admin, + &contract_id, + "initialize", + (admin.clone(), learn_token_id.clone()), + ); + client.initialize(&admin, &learn_token_id); + + ( + env, + contract_id, + admin, + learn_token_id, + client, + token_client, + ) +} + +fn add_course( + env: &Env, + contract_id: &Address, + admin: &Address, + client: &CourseMilestoneClient<'static>, + course_id: &String, + milestone_count: u32, +) { + authorize( + env, + admin, + contract_id, + "add_course", + (admin.clone(), course_id.clone(), milestone_count), + ); + client.add_course(admin, course_id, &milestone_count); +} + +fn enroll( + env: &Env, + contract_id: &Address, + learner: &Address, + client: &CourseMilestoneClient<'static>, + course_id: &String, +) { + authorize( + env, + learner, + contract_id, + "enroll", + (learner.clone(), course_id.clone()), + ); + client.enroll(learner, course_id); +} + +fn submit_milestone( + env: &Env, + contract_id: &Address, + learner: &Address, + client: &CourseMilestoneClient<'static>, + course_id: &String, + milestone_id: u32, + evidence_uri: &String, +) { + authorize( + env, + learner, + contract_id, + "submit_milestone", + ( + learner.clone(), + course_id.clone(), + milestone_id, + evidence_uri.clone(), + ), + ); + client.submit_milestone(learner, course_id, &milestone_id, evidence_uri); +} + +#[test] +fn add_course_and_get_course_work() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let course_id = sid(&env, "rust-101"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 4); + + let course = client + .get_course(&course_id) + .expect("course should be stored after add"); + assert_eq!( + course, + CourseConfig { + milestone_count: 4, + active: true, + } + ); +} + +#[test] +fn enrolls_learner_in_active_course() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + enroll(&env, &contract_id, &learner, &client, &course_id); + + assert!(client.is_enrolled(&learner, &course_id)); +} + +#[test] +fn duplicate_enroll_fails() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + enroll(&env, &contract_id, &learner, &client, &course_id); + + authorize( + &env, + &learner, + &contract_id, + "enroll", + (learner.clone(), course_id.clone()), + ); + let result = client.try_enroll(&learner, &course_id); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::AlreadyEnrolled as u32 + ))) + ); +} + +#[test] +fn submit_milestone_stores_pending_submission() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence_uri = sid(&env, "ipfs://proof"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + enroll(&env, &contract_id, &learner, &client, &course_id); + submit_milestone( + &env, + &contract_id, + &learner, + &client, + &course_id, + 1, + &evidence_uri, + ); + + assert_eq!( + client.get_milestone_state(&learner, &course_id, &1), + MilestoneStatus::Pending + ); + + let submission = client + .get_milestone_submission(&learner, &course_id, &1) + .expect("submission should exist"); + assert_eq!(submission.evidence_uri, evidence_uri); +} + +#[test] +fn verify_milestone_mints_lrn_and_marks_completion() { + let (env, contract_id, admin, _token_id, client, token_client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence_uri = sid(&env, "ipfs://proof"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + enroll(&env, &contract_id, &learner, &client, &course_id); + submit_milestone( + &env, + &contract_id, + &learner, + &client, + &course_id, + 1, + &evidence_uri, + ); + + authorize( + &env, + &admin, + &contract_id, + "verify_milestone", + ( + admin.clone(), + learner.clone(), + course_id.clone(), + 1_u32, + 125_i128, + ), + ); + client.verify_milestone(&admin, &learner, &course_id, &1, &125); + + assert_eq!( + client.get_milestone_state(&learner, &course_id, &1), + MilestoneStatus::Approved + ); + assert!(client.is_completed(&learner, &course_id, &1)); + assert_eq!(token_client.balance(&learner), 125); +} + +#[test] +fn verify_milestone_emits_course_completed_event_on_final_milestone() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence_1 = sid(&env, "ipfs://proof-1"); + let evidence_2 = sid(&env, "ipfs://proof-2"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 2); + enroll(&env, &contract_id, &learner, &client, &course_id); + + submit_milestone( + &env, + &contract_id, + &learner, + &client, + &course_id, + 1, + &evidence_1, + ); + authorize( + &env, + &admin, + &contract_id, + "verify_milestone", + ( + admin.clone(), + learner.clone(), + course_id.clone(), + 1_u32, + 10_i128, + ), + ); + client.verify_milestone(&admin, &learner, &course_id, &1, &10); + + submit_milestone( + &env, + &contract_id, + &learner, + &client, + &course_id, + 2, + &evidence_2, + ); + authorize( + &env, + &admin, + &contract_id, + "verify_milestone", + ( + admin.clone(), + learner.clone(), + course_id.clone(), + 2_u32, + 20_i128, + ), + ); + client.verify_milestone(&admin, &learner, &course_id, &2, &20); + + let events = env.events().all(); + let completion_events = events + .iter() + .filter(|(_, topics, data)| { + topics.contains(&Symbol::new(&env, "course_done").into_val(&env)) && { + let payload: CourseCompleted = data.clone().into_val(&env); + payload + == CourseCompleted { + learner: learner.clone(), + course_id: course_id.clone(), + } + } + }) + .count(); + + assert_eq!(completion_events, 1); +} + +#[test] +fn verify_milestone_fails_for_non_admin() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let learner = Address::generate(&env); + let attacker = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence_uri = sid(&env, "ipfs://proof"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + enroll(&env, &contract_id, &learner, &client, &course_id); + submit_milestone( + &env, + &contract_id, + &learner, + &client, + &course_id, + 1, + &evidence_uri, + ); + + authorize( + &env, + &attacker, + &contract_id, + "verify_milestone", + ( + attacker.clone(), + learner.clone(), + course_id.clone(), + 1_u32, + 125_i128, + ), + ); + let result = client.try_verify_milestone(&attacker, &learner, &course_id, &1, &125); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::Unauthorized as u32 + ))) + ); +} + +#[test] +fn reject_milestone_marks_rejected_and_clears_submission() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence_uri = sid(&env, "ipfs://proof"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + enroll(&env, &contract_id, &learner, &client, &course_id); + submit_milestone( + &env, + &contract_id, + &learner, + &client, + &course_id, + 1, + &evidence_uri, + ); + + authorize( + &env, + &admin, + &contract_id, + "reject_milestone", + (admin.clone(), learner.clone(), course_id.clone(), 1_u32), + ); + client.reject_milestone(&admin, &learner, &course_id, &1); + + assert_eq!( + client.get_milestone_state(&learner, &course_id, &1), + MilestoneStatus::Rejected + ); + assert!( + client + .get_milestone_submission(&learner, &course_id, &1) + .is_none() + ); +} + +#[test] +fn set_milestone_reward_stores_config() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let course_id = sid(&env, "rust-101"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + + authorize( + &env, + &admin, + &contract_id, + "set_milestone_reward", + (course_id.clone(), 1_u32, 75_i128), + ); + client.set_milestone_reward(&course_id, &1, &75); + + let stored_reward = env.as_contract(&contract_id, || { + env.storage() + .persistent() + .get::<_, i128>(&DataKey::MilestoneLrn(course_id.clone(), 1)) + .unwrap_or(0) + }); + + assert_eq!(stored_reward, 75); +} + +#[test] +fn complete_milestone_marks_completion_and_emits_reward_event() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + enroll(&env, &contract_id, &learner, &client, &course_id); + + authorize( + &env, + &admin, + &contract_id, + "set_milestone_reward", + (course_id.clone(), 2_u32, 75_i128), + ); + client.set_milestone_reward(&course_id, &2, &75); + + authorize( + &env, + &admin, + &contract_id, + "complete_milestone", + (learner.clone(), course_id.clone(), 2_u32), + ); + client.complete_milestone(&learner, &course_id, &2); + + let events = env.events().all(); + let found = events.iter().any(|(_, topics, data)| { + topics.contains(&symbol_short!("ms_done").into_val(&env)) && { + let d: MilestoneCompleted = data.clone().into_val(&env); + d == MilestoneCompleted { + learner: learner.clone(), + course_id: course_id.clone(), + milestone_id: 2, + lrn_reward: 75, + } + } + }); + assert!(found, "completion event with reward was not emitted"); + + assert!(client.is_completed(&learner, &course_id, &2)); + assert_eq!( + client.get_milestone_state(&learner, &course_id, &2), + MilestoneStatus::Approved + ); +} + +#[test] +fn complete_milestone_fails_when_already_completed() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + enroll(&env, &contract_id, &learner, &client, &course_id); + + authorize( + &env, + &admin, + &contract_id, + "complete_milestone", + (learner.clone(), course_id.clone(), 1_u32), + ); + client.complete_milestone(&learner, &course_id, &1); + + authorize( + &env, + &admin, + &contract_id, + "complete_milestone", + (learner.clone(), course_id.clone(), 1_u32), + ); + let result = client.try_complete_milestone(&learner, &course_id, &1); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::AlreadyCompleted as u32 + ))) + ); +} + +#[test] +fn complete_milestone_fails_for_non_enrolled_learner() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + + authorize( + &env, + &admin, + &contract_id, + "complete_milestone", + (learner.clone(), course_id.clone(), 1_u32), + ); + let result = client.try_complete_milestone(&learner, &course_id, &1); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::NotEnrolled as u32 + ))) + ); +} + +#[test] +fn complete_milestone_fails_without_admin_auth() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let learner = Address::generate(&env); + let attacker = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + enroll(&env, &contract_id, &learner, &client, &course_id); + + authorize( + &env, + &attacker, + &contract_id, + "complete_milestone", + (learner.clone(), course_id.clone(), 1_u32), + ); + let result = client.try_complete_milestone(&learner, &course_id, &1); + + assert!(result.is_err()); +} + +// ── batch_verify_milestones ────────────────────────────────────────────────── + +#[allow(dead_code)] +fn verify_milestone_helper( + env: &Env, + contract_id: &Address, + admin: &Address, + client: &CourseMilestoneClient<'static>, + learner: &Address, + course_id: &String, + milestone_id: u32, + tokens: i128, +) { + authorize( + env, + admin, + contract_id, + "verify_milestone", + ( + admin.clone(), + learner.clone(), + course_id.clone(), + milestone_id, + tokens, + ), + ); + client.verify_milestone(admin, learner, course_id, &milestone_id, &tokens); +} + +#[test] +fn batch_verify_milestones_happy_path() { + let (env, contract_id, admin, _token_id, client, token_client) = setup(); + let learner1 = Address::generate(&env); + let learner2 = Address::generate(&env); + let course_id = sid(&env, "batch-course"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + enroll(&env, &contract_id, &learner1, &client, &course_id); + enroll(&env, &contract_id, &learner2, &client, &course_id); + + let uri = sid(&env, "ipfs://evidence"); + submit_milestone(&env, &contract_id, &learner1, &client, &course_id, 1, &uri); + submit_milestone(&env, &contract_id, &learner2, &client, &course_id, 1, &uri); + + let submissions = soroban_sdk::vec![ + &env, + VerifyBatchEntry { + learner: learner1.clone(), + course_id: course_id.clone(), + milestone_id: 1, + lrn_reward: 100, + }, + VerifyBatchEntry { + learner: learner2.clone(), + course_id: course_id.clone(), + milestone_id: 1, + lrn_reward: 200, + }, + ]; + authorize( + &env, + &admin, + &contract_id, + "batch_verify_milestones", + (admin.clone(), submissions.clone()), + ); + client.batch_verify_milestones(&admin, &submissions); + + assert_eq!( + client.get_milestone_state(&learner1, &course_id, &1), + MilestoneStatus::Approved, + ); + assert_eq!( + client.get_milestone_state(&learner2, &course_id, &1), + MilestoneStatus::Approved, + ); + assert_eq!(token_client.balance(&learner1), 100); + assert_eq!(token_client.balance(&learner2), 200); +} + +#[test] +fn batch_verify_milestones_reverts_on_invalid_entry() { + let (env, contract_id, admin, _token_id, client, token_client) = setup(); + let learner1 = Address::generate(&env); + let not_enrolled = Address::generate(&env); + let course_id = sid(&env, "batch-course"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + enroll(&env, &contract_id, &learner1, &client, &course_id); + + let uri = sid(&env, "ipfs://evidence"); + submit_milestone(&env, &contract_id, &learner1, &client, &course_id, 1, &uri); + + // Second entry: not_enrolled learner has no enrollment → should cause revert + let submissions = soroban_sdk::vec![ + &env, + VerifyBatchEntry { + learner: learner1.clone(), + course_id: course_id.clone(), + milestone_id: 1, + lrn_reward: 100, + }, + VerifyBatchEntry { + learner: not_enrolled.clone(), + course_id: course_id.clone(), + milestone_id: 1, + lrn_reward: 100, + }, + ]; + + authorize( + &env, + &admin, + &contract_id, + "batch_verify_milestones", + (admin.clone(), submissions.clone()), + ); + let result = client.try_batch_verify_milestones(&admin, &submissions); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::NotEnrolled as u32 + ))) + ); + + // Because the batch reverted, learner1 should NOT be marked approved + assert_eq!( + client.get_milestone_state(&learner1, &course_id, &1), + MilestoneStatus::Pending, + ); + // And no tokens were minted + assert_eq!(token_client.balance(&learner1), 0); +} + +#[test] +fn upgrade_requires_admin_auth() { + let (env, contract_id, _admin, _token_id, client, _token_client) = setup(); + let attacker = Address::generate(&env); + let wasm_hash: BytesN<32> = crate::upgrade::testutils::upload_upgrade_target(&env); + + authorize( + &env, + &attacker, + &contract_id, + "upgrade", + (wasm_hash.clone(),), + ); + let result = client.try_upgrade(&wasm_hash); + + assert!(result.is_err()); +} + +#[test] +fn state_persists_after_upgrade() { + let (env, contract_id, admin, _token_id, client, _token_client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "soroban-101"); + + add_course(&env, &contract_id, &admin, &client, &course_id, 3); + enroll(&env, &contract_id, &learner, &client, &course_id); + + let wasm_hash = crate::upgrade::testutils::upload_upgrade_target(&env); + authorize(&env, &admin, &contract_id, "upgrade", (wasm_hash.clone(),)); + client.upgrade(&wasm_hash); + + let stored_course = env.as_contract(&contract_id, || { + env.storage() + .persistent() + .get::<_, CourseConfig>(&DataKey::Course(course_id.clone())) + }); + let enrolled = env.as_contract(&contract_id, || { + env.storage() + .persistent() + .get::<_, bool>(&DataKey::Enrollment(learner.clone(), course_id.clone())) + .unwrap_or(false) + }); + let stored_hash = env.as_contract(&contract_id, || crate::upgrade::current_hash(&env)); + + assert_eq!( + stored_course, + Some(CourseConfig { + milestone_count: 3, + active: true, + }) + ); + assert!(enrolled); + assert_eq!(stored_hash, wasm_hash); +} diff --git a/contracts/deploy.config.ts b/contracts/deploy.config.ts new file mode 100644 index 00000000..dfcc8278 --- /dev/null +++ b/contracts/deploy.config.ts @@ -0,0 +1,5 @@ +export const config = { + network: process.env.NETWORK || "testnet", + sourceAccount: process.env.STELLAR_SOURCE_ACCOUNT || "default", + wasmDir: "target/wasm32-unknown-unknown/release", +}; \ No newline at end of file diff --git a/contracts/fungible-allowlist/Cargo.toml b/contracts/fungible-allowlist/Cargo.toml index 4cf40c51..1741d2a3 100644 --- a/contracts/fungible-allowlist/Cargo.toml +++ b/contracts/fungible-allowlist/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "fungible-allowlist-example" +name = "fungible-allowlist" edition.workspace = true license.workspace = true repository.workspace = true @@ -12,9 +12,6 @@ doctest = false [dependencies] soroban-sdk = { workspace = true } -stellar-access = { workspace = true } -stellar-macros = { workspace = true } -stellar-tokens = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/fungible-allowlist/src/contract.rs b/contracts/fungible-allowlist/src/contract.rs deleted file mode 100644 index 007ef120..00000000 --- a/contracts/fungible-allowlist/src/contract.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Fungible AllowList Example Contract. - -//! This contract showcases how to integrate the AllowList extension with a -//! SEP-41-compliant fungible token. It includes essential features such as -//! controlled token transfers by an admin who can allow or disallow specific -//! accounts. - -use soroban_sdk::{ - contract, contractimpl, symbol_short, Address, Env, MuxedAddress, String, Symbol, Vec, -}; -use stellar_access::access_control::{self as access_control, AccessControl}; -use stellar_macros::only_role; -use stellar_tokens::fungible::{ - allowlist::{AllowList, FungibleAllowList}, - burnable::FungibleBurnable, - Base, FungibleToken, -}; - -#[contract] -pub struct ExampleContract; - -#[contractimpl] -impl ExampleContract { - pub fn __constructor( - e: &Env, - name: String, - symbol: String, - admin: Address, - manager: Address, - initial_supply: i128, - ) { - Base::set_metadata(e, 18, name, symbol); - - access_control::set_admin(e, &admin); - - // create a role "manager" and grant it to `manager` - access_control::grant_role_no_auth(e, &manager, &symbol_short!("manager"), &admin); - - // Allow the admin to transfer tokens - AllowList::allow_user(e, &admin); - - // Mint initial supply to the admin - Base::mint(e, &admin, initial_supply); - } -} - -#[contractimpl(contracttrait)] -impl FungibleToken for ExampleContract { - type ContractType = AllowList; -} -#[contractimpl] -impl FungibleAllowList for ExampleContract { - fn allowed(e: &Env, account: Address) -> bool { - AllowList::allowed(e, &account) - } - - #[only_role(operator, "manager")] - fn allow_user(e: &Env, user: Address, operator: Address) { - AllowList::allow_user(e, &user) - } - - #[only_role(operator, "manager")] - fn disallow_user(e: &Env, user: Address, operator: Address) { - AllowList::disallow_user(e, &user) - } -} - -#[contractimpl(contracttrait)] -impl AccessControl for ExampleContract {} - -#[contractimpl(contracttrait)] -impl FungibleBurnable for ExampleContract {} diff --git a/contracts/fungible-allowlist/src/lib.rs b/contracts/fungible-allowlist/src/lib.rs index a3e21cda..71a25801 100644 --- a/contracts/fungible-allowlist/src/lib.rs +++ b/contracts/fungible-allowlist/src/lib.rs @@ -1,7 +1,135 @@ #![no_std] -#![allow(dead_code)] -mod contract; +use soroban_sdk::{ + Address, Env, Vec, contract, contracterror, contractimpl, contracttype, panic_with_error, +}; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum AllowlistError { + Unauthorized = 1, + AlreadyInitialized = 2, + NotInitialized = 3, +} + +#[contracttype] +pub enum DataKey { + Admin, + IsAllowed(Address), +} + +#[contract] +pub struct FungibleAllowlist; + +#[contractimpl] +impl FungibleAllowlist { + pub fn initialize(env: Env, admin: Address) { + if env.storage().instance().has(&DataKey::Admin) { + panic_with_error!(&env, AllowlistError::AlreadyInitialized); + } + env.storage().instance().set(&DataKey::Admin, &admin); + } + + pub fn add_to_allowlist(env: Env, admin: Address, account: Address) { + admin.require_auth(); + let stored_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .unwrap_or_else(|| panic_with_error!(&env, AllowlistError::NotInitialized)); + if admin != stored_admin { + panic_with_error!(&env, AllowlistError::Unauthorized); + } + + if !Self::is_allowed(env.clone(), account.clone()) { + env.storage() + .persistent() + .set(&DataKey::IsAllowed(account.clone()), &true); + } + } + + pub fn remove_from_allowlist(env: Env, admin: Address, account: Address) { + admin.require_auth(); + let stored_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .unwrap_or_else(|| panic_with_error!(&env, AllowlistError::NotInitialized)); + if admin != stored_admin { + panic_with_error!(&env, AllowlistError::Unauthorized); + } + + if Self::is_allowed(env.clone(), account.clone()) { + env.storage() + .persistent() + .set(&DataKey::IsAllowed(account.clone()), &false); + } + } + + pub fn is_allowed(env: Env, account: Address) -> bool { + env.storage() + .persistent() + .get(&DataKey::IsAllowed(account)) + .unwrap_or(false) + } + + pub fn get_allowlist(env: Env) -> Vec
{ + // Enumeration should be rebuilt off-chain from events or indexers. + Vec::new(&env) + } + + pub fn set_admin(env: Env, admin: Address, new_admin: Address) { + admin.require_auth(); + let stored_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .unwrap_or_else(|| panic_with_error!(&env, AllowlistError::NotInitialized)); + if admin != stored_admin { + panic_with_error!(&env, AllowlistError::Unauthorized); + } + env.storage().instance().set(&DataKey::Admin, &new_admin); + } +} #[cfg(test)] -mod test; +mod test { + use super::*; + use soroban_sdk::{Env, testutils::Address as _}; + + #[test] + fn test_allowlist_flow() { + let env = Env::default(); + let admin = Address::generate(&env); + let alice = Address::generate(&env); + let bob = Address::generate(&env); + + let contract_id = env.register_contract(None, FungibleAllowlist); + let client = FungibleAllowlistClient::new(&env, &contract_id); + + client.initialize(&admin); + assert_eq!(client.is_allowed(&alice), false); + assert_eq!(client.get_allowlist().len(), 0); + + env.mock_all_auths(); + + client.add_to_allowlist(&admin, &alice); + assert_eq!(client.is_allowed(&alice), true); + assert_eq!(client.get_allowlist().len(), 0); + + client.add_to_allowlist(&admin, &bob); + assert_eq!(client.is_allowed(&bob), true); + assert_eq!(client.get_allowlist().len(), 0); + + client.remove_from_allowlist(&admin, &alice); + assert_eq!(client.is_allowed(&alice), false); + assert_eq!(client.get_allowlist().len(), 0); + + let new_admin = Address::generate(&env); + client.set_admin(&admin, &new_admin); + + client.add_to_allowlist(&new_admin, &alice); + assert_eq!(client.is_allowed(&alice), true); + } +} diff --git a/contracts/fungible-allowlist/src/test.rs b/contracts/fungible-allowlist/src/test.rs deleted file mode 100644 index 15b2b200..00000000 --- a/contracts/fungible-allowlist/src/test.rs +++ /dev/null @@ -1,156 +0,0 @@ -extern crate std; - -use soroban_sdk::{testutils::Address as _, Address, Env, String}; - -use crate::contract::{ExampleContract, ExampleContractClient}; - -fn create_client<'a>( - e: &Env, - admin: &Address, - manager: &Address, - initial_supply: &i128, -) -> ExampleContractClient<'a> { - let name = String::from_str(e, "AllowList Token"); - let symbol = String::from_str(e, "ALT"); - let address = e.register(ExampleContract, (name, symbol, admin, manager, initial_supply)); - ExampleContractClient::new(e, &address) -} - -#[test] -#[should_panic(expected = "Error(Contract, #113)")] -fn cannot_transfer_before_allow() { - let e = Env::default(); - let admin = Address::generate(&e); - let manager = Address::generate(&e); - let user1 = Address::generate(&e); - let user2 = Address::generate(&e); - let initial_supply = 1_000_000; - let client = create_client(&e, &admin, &manager, &initial_supply); - let transfer_amount = 1000; - - // Verify initial state - admin is allowed, others are not - assert!(client.allowed(&admin)); - assert!(!client.allowed(&user1)); - assert!(!client.allowed(&user2)); - - // Admin can't transfer to user1 initially (user1 not allowed) - e.mock_all_auths(); - client.transfer(&admin, &user1, &transfer_amount); -} - -#[test] -fn transfer_to_allowed_account_works() { - let e = Env::default(); - let admin = Address::generate(&e); - let manager = Address::generate(&e); - let user1 = Address::generate(&e); - let user2 = Address::generate(&e); - let initial_supply = 1_000_000; - let client = create_client(&e, &admin, &manager, &initial_supply); - let transfer_amount = 1000; - - e.mock_all_auths(); - - // Verify initial state - admin is allowed, others are not - assert!(client.allowed(&admin)); - assert!(!client.allowed(&user1)); - assert!(!client.allowed(&user2)); - - // Allow user1 - client.allow_user(&user1, &manager); - assert!(client.allowed(&user1)); - - // Now admin can transfer to user1 - client.transfer(&admin, &user1, &transfer_amount); - assert_eq!(client.balance(&user1), transfer_amount); -} - -#[test] -#[should_panic(expected = "Error(Contract, #113)")] -fn cannot_transfer_after_disallow() { - let e = Env::default(); - let admin = Address::generate(&e); - let manager = Address::generate(&e); - let user1 = Address::generate(&e); - let user2 = Address::generate(&e); - let initial_supply = 1_000_000; - let client = create_client(&e, &admin, &manager, &initial_supply); - let transfer_amount = 1000; - - e.mock_all_auths(); - - // Verify initial state - admin is allowed, others are not - assert!(client.allowed(&admin)); - assert!(!client.allowed(&user1)); - assert!(!client.allowed(&user2)); - - // Allow user1 - client.allow_user(&user1, &manager); - assert!(client.allowed(&user1)); - - // Now admin can transfer to user1 - client.transfer(&admin, &user1, &transfer_amount); - assert_eq!(client.balance(&user1), transfer_amount); - - // Disallow user1 - client.disallow_user(&user1, &manager); - assert!(!client.allowed(&user1)); - - // Admin can't transfer to user1 after disallowing - client.transfer(&admin, &user1, &100); -} - -#[test] -fn allowlist_transfer_from_override_works() { - let e = Env::default(); - let admin = Address::generate(&e); - let manager = Address::generate(&e); - let user1 = Address::generate(&e); - let user2 = Address::generate(&e); - let initial_supply = 1_000_000; - let client = create_client(&e, &admin, &manager, &initial_supply); - let transfer_amount = 1000; - - e.mock_all_auths(); - - // Verify initial state - admin is allowed, others are not - assert!(client.allowed(&admin)); - assert!(!client.allowed(&user1)); - assert!(!client.allowed(&user2)); - - // Allow user2 - client.allow_user(&user2, &manager); - assert!(client.allowed(&user2)); - - // Now admin can transfer to user1 - client.approve(&admin, &user1, &transfer_amount, &1000); - client.transfer_from(&user1, &admin, &user2, &transfer_amount); - assert_eq!(client.balance(&user2), transfer_amount); -} - -#[test] -fn allowlist_approve_override_works() { - let e = Env::default(); - let admin = Address::generate(&e); - let manager = Address::generate(&e); - let user1 = Address::generate(&e); - let user2 = Address::generate(&e); - let initial_supply = 1_000_000; - let client = create_client(&e, &admin, &manager, &initial_supply); - let transfer_amount = 1000; - - e.mock_all_auths(); - - // Verify initial state - admin is allowed, others are not - assert!(client.allowed(&admin)); - assert!(!client.allowed(&user1)); - assert!(!client.allowed(&user2)); - - // Allow user1 - client.allow_user(&user1, &manager); - assert!(client.allowed(&user1)); - - // Approve user2 to transfer from user1 - client.approve(&user1, &user2, &transfer_amount, &1000); - assert_eq!(client.allowance(&user1, &user2), transfer_amount); -} diff --git a/contracts/governance_token/Cargo.toml b/contracts/governance_token/Cargo.toml new file mode 100644 index 00000000..a7e9ceb2 --- /dev/null +++ b/contracts/governance_token/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "governance-token" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +learnvault-shared = { workspace = true } +soroban-sdk = { workspace = true } + +[dev-dependencies] +learnvault-shared = { workspace = true, features = ["testutils"] } +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/governance_token/src/lib.rs b/contracts/governance_token/src/lib.rs new file mode 100644 index 00000000..fd2e2333 --- /dev/null +++ b/contracts/governance_token/src/lib.rs @@ -0,0 +1,1363 @@ +#![no_std] + +//! # GovernanceToken (GOV) +//! +//! A **transferable** SEP-41 fungible token distributed to donors on treasury +//! deposit and earned by top learners at milestone thresholds. Used exclusively +//! for DAO voting on scholarship proposals. +//! +//! - Only the admin (treasury contract) can mint. +//! - Fully transferable — unlike LearnToken (LRN). +//! - No burning in V1. +//! +//! ## Relevant issue +//! Implements: https://github.com/bakeronchain/learnvault/issues/11 + +use soroban_sdk::{ + Address, BytesN, Env, String, Symbol, contract, contracterror, contractevent, contractimpl, + contracttype, panic_with_error, symbol_short, +}; + +use learnvault_shared::upgrade; + +pub use upgrade::ContractUpgraded; + +// --------------------------------------------------------------------------- +// Storage Constants (assuming ~6s ledger time) +// --------------------------------------------------------------------------- + +const DAY_IN_LEDGERS: u32 = 17_280; +const INSTANCE_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; // 30 days +const PERSISTENT_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const PERSISTENT_EXTEND_TO: u32 = DAY_IN_LEDGERS * 365; // 1 year + +// --------------------------------------------------------------------------- +// Errors +// --------------------------------------------------------------------------- + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum GOVError { + /// Caller is not the contract admin. + Unauthorized = 1, + /// Amount must be greater than zero. + ZeroAmount = 2, + /// Contract has not been initialized. + NotInitialized = 3, + /// Insufficient balance or allowance. + InsufficientFunds = 4, + /// Expiration ledger is in the past. + InvalidExpiration = 5, + /// Allowance exists but is expired at current ledger. + AllowanceExpired = 6, + /// Contract is paused; all state-mutating calls are blocked. + ContractPaused = 7, +} + +// --------------------------------------------------------------------------- +// Storage keys +// --------------------------------------------------------------------------- + +const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); +const NAME_KEY: Symbol = symbol_short!("NAME"); +const SYMBOL_KEY: Symbol = symbol_short!("SYMBOL"); +const DECIMALS_KEY: Symbol = symbol_short!("DECIMALS"); +const PAUSED_KEY: Symbol = symbol_short!("PAUSED"); + +#[contracttype] +pub enum DataKey { + Balance(Address), + Allowance(Address, Address), // (owner, spender) + TotalSupply, + Delegate(Address), + DelegatedAmount(Address), +} + +#[contractevent] +pub struct GOVBurned { + pub from: Address, + pub amount: i128, +} + +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GOVPaused { + pub admin: Address, +} + +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GOVUnpaused { + pub admin: Address, +} + +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GOVMinted { + pub to: Address, + pub amount: i128, +} + +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GOVTransferred { + pub from: Address, + pub to: Address, + pub amount: i128, +} + +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GOVApproved { + pub owner: Address, + pub spender: Address, + pub amount: i128, +} + +/// Emitted when a token holder changes their delegation to a new address. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DelegateChanged { + pub delegator: Address, + pub delegatee: Address, +} + +/// Emitted when a token holder removes their delegation entirely. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DelegateRemoved { + pub delegator: Address, +} + +// --------------------------------------------------------------------------- +// Contract +// --------------------------------------------------------------------------- + +#[contract] +pub struct GovernanceToken; + +#[contractimpl] +impl GovernanceToken { + /// Initialise the contract. Can only be called once. + /// + /// Sets name = "LearnVault Governance", symbol = "GOV", decimals = 7. + pub fn initialize(env: Env, admin: Address) { + if env.storage().instance().has(&ADMIN_KEY) { + panic_with_error!(&env, GOVError::Unauthorized); + } + env.storage().instance().set(&ADMIN_KEY, &admin); + upgrade::init(&env); + env.storage() + .instance() + .set(&NAME_KEY, &String::from_str(&env, "LearnVault Governance")); + env.storage() + .instance() + .set(&SYMBOL_KEY, &String::from_str(&env, "GOV")); + env.storage().instance().set(&DECIMALS_KEY, &7_u32); + + Self::extend_instance(&env); + } + + // ----------------------------------------------------------------------- + // Admin + // ----------------------------------------------------------------------- + + /// Mint `amount` GOV to `to`. Admin only. + pub fn mint(env: Env, to: Address, amount: i128) { + Self::assert_not_paused(&env); + Self::extend_instance(&env); + let admin: Address = env + .storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(&env, GOVError::NotInitialized)); + admin.require_auth(); + + if amount <= 0 { + panic_with_error!(&env, GOVError::ZeroAmount); + } + + let key = DataKey::Balance(to.clone()); + let bal: i128 = env.storage().persistent().get(&key).unwrap_or(0); + env.storage().persistent().set(&key, &(bal + amount)); + Self::extend_persistent(&env, &key); + + // Update delegated amount for 'to's delegate + if let Some(delegate) = Self::get_delegate(env.clone(), to.clone()) { + let del_key = DataKey::DelegatedAmount(delegate.clone()); + let del_bal: i128 = env.storage().persistent().get(&del_key).unwrap_or(0); + env.storage() + .persistent() + .set(&del_key, &(del_bal + amount)); + } + + let supply: i128 = env + .storage() + .instance() + .get(&DataKey::TotalSupply) + .unwrap_or(0); + env.storage() + .instance() + .set(&DataKey::TotalSupply, &(supply + amount)); + + GOVMinted { to, amount }.publish(&env); + } + + /// Burn `amount` from the caller's own balance. + pub fn burn(env: Env, from: Address, amount: i128) { + Self::assert_not_paused(&env); + Self::extend_instance(&env); + from.require_auth(); + if amount <= 0 { + panic_with_error!(&env, GOVError::ZeroAmount); + } + Self::_debit(&env, &from, amount); + // reduce total supply + let supply: i128 = env + .storage() + .instance() + .get(&DataKey::TotalSupply) + .unwrap_or(0); + env.storage() + .instance() + .set(&DataKey::TotalSupply, &(supply - amount)); + GOVBurned { from, amount }.publish(&env); + } + + /// Administrative burn for slashing. + pub fn admin_burn_from(env: Env, from: Address, amount: i128) { + Self::extend_instance(&env); + let admin: Address = env + .storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(&env, GOVError::NotInitialized)); + admin.require_auth(); + + if amount <= 0 { + panic_with_error!(&env, GOVError::ZeroAmount); + } + Self::_debit(&env, &from, amount); + + let supply: i128 = env + .storage() + .instance() + .get(&DataKey::TotalSupply) + .unwrap_or(0); + env.storage() + .instance() + .set(&DataKey::TotalSupply, &(supply - amount)); + GOVBurned { from, amount }.publish(&env); + } + + /// Transfer the admin role to a new address. + pub fn set_admin(env: Env, new_admin: Address) { + Self::extend_instance(&env); + let admin: Address = env + .storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(&env, GOVError::NotInitialized)); + admin.require_auth(); + env.storage().instance().set(&ADMIN_KEY, &new_admin); + } + + /// Replace the current contract WASM with a new uploaded hash. Admin only. + pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { + Self::extend_instance(&env); + let admin: Address = env + .storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(&env, GOVError::NotInitialized)); + admin.require_auth(); + upgrade::apply(&env, &admin, &new_wasm_hash); + } + + // ----------------------------------------------------------------------- + // Emergency pause / unpause + // ----------------------------------------------------------------------- + + /// Pause the contract. Admin only. + /// + /// Blocks `mint`, `transfer`, `burn`, and `approve` until unpaused. + pub fn pause(env: Env, admin: Address) { + Self::extend_instance(&env); + let stored_admin: Address = env + .storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(&env, GOVError::NotInitialized)); + if admin != stored_admin { + panic_with_error!(&env, GOVError::Unauthorized); + } + admin.require_auth(); + env.storage().instance().set(&PAUSED_KEY, &true); + GOVPaused { admin }.publish(&env); + } + + /// Unpause the contract. Admin only. + pub fn unpause(env: Env, admin: Address) { + Self::extend_instance(&env); + let stored_admin: Address = env + .storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(&env, GOVError::NotInitialized)); + if admin != stored_admin { + panic_with_error!(&env, GOVError::Unauthorized); + } + admin.require_auth(); + env.storage().instance().set(&PAUSED_KEY, &false); + GOVUnpaused { admin }.publish(&env); + } + + /// Returns `true` if the contract is currently paused. + pub fn is_paused(env: Env) -> bool { + env.storage().instance().get(&PAUSED_KEY).unwrap_or(false) + } + + // ----------------------------------------------------------------------- + // SEP-41 transfers + // ----------------------------------------------------------------------- + + /// Transfer `amount` GOV from `from` to `to`. Requires `from` auth. + pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { + Self::assert_not_paused(&env); + Self::extend_instance(&env); + from.require_auth(); + if amount <= 0 { + panic_with_error!(&env, GOVError::ZeroAmount); + } + Self::_debit(&env, &from, amount); + Self::_credit(&env, &to, amount); + + GOVTransferred { from, to, amount }.publish(&env); + } + + /// Approve `spender` to spend up to `amount` on behalf of `owner`. + pub fn approve( + env: Env, + owner: Address, + spender: Address, + amount: i128, + expiration_ledger: u32, + ) { + Self::assert_not_paused(&env); + owner.require_auth(); + let current_ledger = env.ledger().sequence(); + if expiration_ledger < current_ledger { + panic_with_error!(&env, GOVError::InvalidExpiration); + } + + let key = DataKey::Allowance(owner.clone(), spender.clone()); + env.storage() + .persistent() + .set(&key, &(amount, expiration_ledger)); + Self::extend_persistent(&env, &key); + + GOVApproved { + owner, + spender, + amount, + } + .publish(&env); + } + + /// Transfer `amount` from `from` to `to` using `spender`'s allowance. + pub fn transfer_from(env: Env, spender: Address, from: Address, to: Address, amount: i128) { + spender.require_auth(); + if amount <= 0 { + panic_with_error!(&env, GOVError::ZeroAmount); + } + + let current_ledger = env.ledger().sequence(); + let allow_key = DataKey::Allowance(from.clone(), spender.clone()); + let (allowance, expiration_ledger): (i128, u32) = + env.storage().persistent().get(&allow_key).unwrap_or((0, 0)); + + if allowance > 0 && expiration_ledger < current_ledger { + panic_with_error!(&env, GOVError::AllowanceExpired); + } + + if allowance < amount { + panic_with_error!(&env, GOVError::InsufficientFunds); + } + + let remaining = allowance - amount; + if remaining == 0 { + env.storage().persistent().remove(&allow_key); + } else { + env.storage() + .persistent() + .set(&allow_key, &(remaining, expiration_ledger)); + Self::extend_persistent(&env, &allow_key); + } + + Self::_debit(&env, &from, amount); + Self::_credit(&env, &to, amount); + + GOVTransferred { from, to, amount }.publish(&env); + } + + // ----------------------------------------------------------------------- + // Delegation + // ----------------------------------------------------------------------- + + pub fn delegate(env: Env, delegator: Address, delegatee: Address) { + delegator.require_auth(); + + let old_delegate = Self::get_delegate(env.clone(), delegator.clone()); + if old_delegate == Some(delegatee.clone()) { + return; + } + + let bal = Self::balance(env.clone(), delegator.clone()); + + // Remove from old delegate + if let Some(old) = old_delegate { + let key = DataKey::DelegatedAmount(old); + let current: i128 = env.storage().persistent().get(&key).unwrap_or(0); + env.storage().persistent().set(&key, &(current - bal)); + } + + // Add to new delegate + if delegator != delegatee { + let key = DataKey::DelegatedAmount(delegatee.clone()); + let current: i128 = env.storage().persistent().get(&key).unwrap_or(0); + env.storage().persistent().set(&key, &(current + bal)); + env.storage() + .persistent() + .set(&DataKey::Delegate(delegator.clone()), &delegatee.clone()); + DelegateChanged { + delegator, + delegatee, + } + .publish(&env); + } else { + // Delegating to self is same as undelegating + env.storage() + .persistent() + .remove(&DataKey::Delegate(delegator.clone())); + DelegateRemoved { delegator }.publish(&env); + } + } + + pub fn undelegate(env: Env, delegator: Address) { + delegator.require_auth(); + + let old_delegate = Self::get_delegate(env.clone(), delegator.clone()); + if let Some(old) = old_delegate { + let bal = Self::balance(env.clone(), delegator.clone()); + let key = DataKey::DelegatedAmount(old); + let current: i128 = env.storage().persistent().get(&key).unwrap_or(0); + env.storage().persistent().set(&key, &(current - bal)); + + env.storage() + .persistent() + .remove(&DataKey::Delegate(delegator.clone())); + DelegateRemoved { delegator }.publish(&env); + } + } + + // ----------------------------------------------------------------------- + // Read functions + // ----------------------------------------------------------------------- + + pub fn get_delegate(env: Env, delegator: Address) -> Option
{ + env.storage() + .persistent() + .get(&DataKey::Delegate(delegator)) + } + + pub fn get_voting_power(env: Env, address: Address) -> i128 { + if Self::get_delegate(env.clone(), address.clone()).is_some() { + return 0; + } + let bal = Self::balance(env.clone(), address.clone()); + let delegated: i128 = env + .storage() + .persistent() + .get(&DataKey::DelegatedAmount(address)) + .unwrap_or(0); + bal + delegated + } + + pub fn balance(env: Env, account: Address) -> i128 { + env.storage() + .persistent() + .get(&DataKey::Balance(account)) + .unwrap_or(0) + } + + pub fn allowance(env: Env, owner: Address, spender: Address) -> i128 { + let key = DataKey::Allowance(owner, spender); + if let Some((allowance, expiration_ledger)) = + env.storage().persistent().get::<_, (i128, u32)>(&key) + { + if expiration_ledger < env.ledger().sequence() { + 0 + } else { + Self::extend_persistent(&env, &key); + allowance + } + } else { + 0 + } + } + + pub fn total_supply(env: Env) -> i128 { + env.storage() + .instance() + .get(&DataKey::TotalSupply) + .unwrap_or(0) + } + + pub fn decimals(env: Env) -> u32 { + env.storage().instance().get(&DECIMALS_KEY).unwrap_or(7) + } + + pub fn name(env: Env) -> String { + env.storage() + .instance() + .get(&NAME_KEY) + .unwrap_or_else(|| String::from_str(&env, "GovernanceToken")) + } + + pub fn symbol(env: Env) -> String { + env.storage() + .instance() + .get(&SYMBOL_KEY) + .unwrap_or_else(|| String::from_str(&env, "GOV")) + } + + pub fn get_version(env: Env) -> String { + String::from_str(&env, "1.0.0") + } + + // ----------------------------------------------------------------------- + // Internal helpers + // ----------------------------------------------------------------------- + + fn _debit(env: &Env, from: &Address, amount: i128) { + let key = DataKey::Balance(from.clone()); + let bal: i128 = env.storage().persistent().get(&key).unwrap_or(0); + if bal < amount { + panic_with_error!(env, GOVError::InsufficientFunds); + } + env.storage().persistent().set(&key, &(bal - amount)); + Self::extend_persistent(env, &key); + + // Update delegated amount for 'from's delegate + if let Some(delegate) = Self::get_delegate(env.clone(), from.clone()) { + let del_key = DataKey::DelegatedAmount(delegate.clone()); + let del_bal: i128 = env.storage().persistent().get(&del_key).unwrap_or(0); + env.storage() + .persistent() + .set(&del_key, &(del_bal - amount)); + Self::extend_persistent(env, &del_key); + } + } + + fn _credit(env: &Env, to: &Address, amount: i128) { + let key = DataKey::Balance(to.clone()); + let bal: i128 = env.storage().persistent().get(&key).unwrap_or(0); + env.storage().persistent().set(&key, &(bal + amount)); + Self::extend_persistent(env, &key); + + // Update delegated amount for 'to's delegate + if let Some(delegate) = Self::get_delegate(env.clone(), to.clone()) { + let del_key = DataKey::DelegatedAmount(delegate.clone()); + let del_bal: i128 = env.storage().persistent().get(&del_key).unwrap_or(0); + env.storage() + .persistent() + .set(&del_key, &(del_bal + amount)); + Self::extend_persistent(env, &del_key); + } + } + + fn assert_not_paused(env: &Env) { + let paused: bool = env.storage().instance().get(&PAUSED_KEY).unwrap_or(false); + if paused { + panic_with_error!(env, GOVError::ContractPaused); + } + } + + fn extend_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_BUMP_THRESHOLD, INSTANCE_EXTEND_TO); + } + + fn extend_persistent(env: &Env, key: &DataKey) { + env.storage() + .persistent() + .extend_ttl(key, PERSISTENT_BUMP_THRESHOLD, PERSISTENT_EXTEND_TO); + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod test { + extern crate std; + + use soroban_sdk::{ + Address, BytesN, Env, IntoVal, String, + testutils::{Address as _, Ledger, LedgerInfo, MockAuth, MockAuthInvoke}, + }; + + use crate::{DataKey, GOVError, GovernanceToken, GovernanceTokenClient}; + + fn setup(e: &Env) -> (Address, Address, GovernanceTokenClient) { + let admin = Address::generate(e); + let id = e.register(GovernanceToken, ()); + e.mock_all_auths(); + let client = GovernanceTokenClient::new(e, &id); + client.initialize(&admin); + (id, admin, client) + } + + fn authorize_upgrade( + env: &Env, + contract_id: &Address, + signer: &Address, + wasm_hash: &BytesN<32>, + ) { + env.mock_auths(&[MockAuth { + address: signer, + invoke: &MockAuthInvoke { + contract: contract_id, + fn_name: "upgrade", + args: (wasm_hash.clone(),).into_val(env), + sub_invokes: &[], + }, + }]); + } + + fn set_ledger_sequence(env: &Env, sequence_number: u32) { + env.ledger().set(LedgerInfo { + timestamp: 1_700_000_000, + protocol_version: 23, + sequence_number, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 16, + min_persistent_entry_ttl: 16, + max_entry_ttl: 6312000, + }); + } + + fn valid_expiration(env: &Env) -> u32 { + env.ledger().sequence() + 100 + } + + // --- initialization --- + + #[test] + fn initialize_stores_metadata() { + let e = Env::default(); + let (_, _, client) = setup(&e); + assert_eq!(client.name(), String::from_str(&e, "LearnVault Governance")); + assert_eq!(client.symbol(), String::from_str(&e, "GOV")); + assert_eq!(client.decimals(), 7); + } + + #[test] + fn upgrade_requires_admin_auth() { + let e = Env::default(); + let admin = Address::generate(&e); + let attacker = Address::generate(&e); + let id = e.register(GovernanceToken, ()); + + e.mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &id, + fn_name: "initialize", + args: (admin.clone(),).into_val(&e), + sub_invokes: &[], + }, + }]); + + let client = GovernanceTokenClient::new(&e, &id); + client.initialize(&admin); + + let wasm_hash = crate::upgrade::testutils::upload_upgrade_target(&e); + authorize_upgrade(&e, &id, &attacker, &wasm_hash); + assert!(client.try_upgrade(&wasm_hash).is_err()); + } + + #[test] + fn state_persists_after_upgrade() { + let e = Env::default(); + let (id, admin, client) = setup(&e); + let donor = Address::generate(&e); + + client.mint(&donor, &500); + + e.set_auths(&[]); + let wasm_hash = crate::upgrade::testutils::upload_upgrade_target(&e); + authorize_upgrade(&e, &id, &admin, &wasm_hash); + client.upgrade(&wasm_hash); + + let balance = e.as_contract(&id, || { + e.storage() + .persistent() + .get::<_, i128>(&DataKey::Balance(donor.clone())) + .unwrap_or(0) + }); + let supply = e.as_contract(&id, || { + e.storage() + .instance() + .get::<_, i128>(&DataKey::TotalSupply) + .unwrap_or(0) + }); + let stored_hash = e.as_contract(&id, || crate::upgrade::current_hash(&e)); + + assert_eq!(balance, 500); + assert_eq!(supply, 500); + assert_eq!(stored_hash, wasm_hash); + } + + #[test] + fn double_initialize_reverts() { + let e = Env::default(); + let (_, admin, client) = setup(&e); + let _ = admin; // already initialized via setup + let result = client.try_initialize(&Address::generate(&e)); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::Unauthorized as u32 + ))) + ); + } + + // --- minting --- + + #[test] + fn mint_increases_balance_and_supply() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let donor = Address::generate(&e); + client.mint(&donor, &500); + assert_eq!(client.balance(&donor), 500); + assert_eq!(client.total_supply(), 500); + } + + #[test] + fn mint_accumulates() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let donor = Address::generate(&e); + client.mint(&donor, &200); + client.mint(&donor, &300); + assert_eq!(client.balance(&donor), 500); + assert_eq!(client.total_supply(), 500); + } + + #[test] + fn mint_zero_reverts() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let donor = Address::generate(&e); + let result = client.try_mint(&donor, &0); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::ZeroAmount as u32 + ))) + ); + } + + #[test] + fn unauthorized_mint_reverts() { + let e = Env::default(); + let admin = Address::generate(&e); + let id = e.register(GovernanceToken, ()); + // Only mock auth for initialize + e.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &id, + fn_name: "initialize", + args: (admin.clone(),).into_val(&e), + sub_invokes: &[], + }, + }]); + let client = GovernanceTokenClient::new(&e, &id); + client.initialize(&admin); + let donor = Address::generate(&e); + let result = client.try_mint(&donor, &100); + assert!(result.is_err()); + } + + // --- transfer --- + + #[test] + fn transfer_moves_balance() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + client.mint(&alice, &100); + client.transfer(&alice, &bob, &40); + assert_eq!(client.balance(&alice), 60); + assert_eq!(client.balance(&bob), 40); + assert_eq!(client.total_supply(), 100); // supply unchanged + } + + #[test] + fn transfer_insufficient_balance_reverts() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + client.mint(&alice, &10); + let result = client.try_transfer(&alice, &bob, &50); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::InsufficientFunds as u32 + ))) + ); + } + + // --- approve / transfer_from --- + + #[test] + fn approve_and_transfer_from_work() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + let carol = Address::generate(&e); + client.mint(&alice, &100); + client.approve(&alice, &bob, &60, &valid_expiration(&e)); + assert_eq!(client.allowance(&alice, &bob), 60); + client.transfer_from(&bob, &alice, &carol, &40); + assert_eq!(client.balance(&alice), 60); + assert_eq!(client.balance(&carol), 40); + assert_eq!(client.allowance(&alice, &bob), 20); // 60 - 40 + } + + #[test] + fn transfer_from_exceeds_allowance_reverts() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + let carol = Address::generate(&e); + client.mint(&alice, &100); + client.approve(&alice, &bob, &10, &valid_expiration(&e)); + let result = client.try_transfer_from(&bob, &alice, &carol, &50); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::InsufficientFunds as u32 + ))) + ); + } + + // --- set_admin --- + + #[test] + fn set_admin_transfers_mint_rights() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let new_admin = Address::generate(&e); + client.set_admin(&new_admin); + let donor = Address::generate(&e); + client.mint(&donor, &50); + assert_eq!(client.balance(&donor), 50); + } + + // --- delegation --- + + #[test] + fn delegation_increases_voting_power() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + client.mint(&alice, &100); + client.mint(&bob, &50); + + // Before delegation + assert_eq!(client.get_voting_power(&alice), 100); + assert_eq!(client.get_voting_power(&bob), 50); + + client.delegate(&alice, &bob); + + // After delegation + assert_eq!(client.get_voting_power(&alice), 0); + assert_eq!(client.get_voting_power(&bob), 150); + assert_eq!(client.get_delegate(&alice), Some(bob.clone())); + } + + #[test] + fn undelegate_restores_power() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + client.mint(&alice, &100); + client.delegate(&alice, &bob); + assert_eq!(client.get_voting_power(&bob), 100); + + client.undelegate(&alice); + assert_eq!(client.get_voting_power(&alice), 100); + assert_eq!(client.get_voting_power(&bob), 0); + assert_eq!(client.get_delegate(&alice), None); + } + + #[test] + fn transfer_updates_delegated_power() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + let carol = Address::generate(&e); + + client.mint(&alice, &100); + client.delegate(&alice, &bob); + assert_eq!(client.get_voting_power(&bob), 100); + + client.transfer(&alice, &carol, &40); + assert_eq!(client.get_voting_power(&bob), 60); + assert_eq!(client.balance(&carol), 40); + assert_eq!(client.get_voting_power(&carol), 40); // Carol has no delegate + } + + #[test] + fn mint_updates_delegated_power() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + client.delegate(&alice, &bob); + client.mint(&alice, &100); + assert_eq!(client.get_voting_power(&bob), 100); + } + + #[test] + fn delegate_to_self_is_undelegate() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + client.mint(&alice, &100); + client.delegate(&alice, &bob); + assert_eq!(client.get_delegate(&alice), Some(bob.clone())); + + client.delegate(&alice, &alice); + assert_eq!(client.get_delegate(&alice), None); + assert_eq!(client.get_voting_power(&alice), 100); + assert_eq!(client.get_voting_power(&bob), 0); + } + + // --- additional edge cases --- + + #[test] + fn transfer_zero_amount_reverts() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + client.mint(&alice, &100); + let result = client.try_transfer(&alice, &bob, &0); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::ZeroAmount as u32 + ))) + ); + } + + #[test] + fn transfer_from_zero_amount_reverts() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + let carol = Address::generate(&e); + client.mint(&alice, &100); + client.approve(&alice, &bob, &50, &valid_expiration(&e)); + let result = client.try_transfer_from(&bob, &alice, &carol, &0); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::ZeroAmount as u32 + ))) + ); + } + + #[test] + fn balance_of_nonexistent_account_is_zero() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let nobody = Address::generate(&e); + assert_eq!(client.balance(&nobody), 0); + } + + #[test] + fn allowance_of_nonexistent_pair_is_zero() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + assert_eq!(client.allowance(&alice, &bob), 0); + } + + #[test] + fn approve_zero_allowance_works() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + client.mint(&alice, &100); + client.approve(&alice, &bob, &50, &valid_expiration(&e)); + assert_eq!(client.allowance(&alice, &bob), 50); + // Reset to zero + client.approve(&alice, &bob, &0, &valid_expiration(&e)); + assert_eq!(client.allowance(&alice, &bob), 0); + } + + #[test] + fn transfer_from_with_insufficient_balance_reverts() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + let carol = Address::generate(&e); + client.mint(&alice, &10); + client.approve(&alice, &bob, &100, &valid_expiration(&e)); // High allowance + let result = client.try_transfer_from(&bob, &alice, &carol, &50); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::InsufficientFunds as u32 + ))) + ); + } + + #[test] + fn total_supply_starts_at_zero() { + let e = Env::default(); + let (_, _, client) = setup(&e); + assert_eq!(client.total_supply(), 0); + } + + #[test] + fn get_version_returns_semver() { + let e = Env::default(); + let (_, _, client) = setup(&e); + assert_eq!(client.get_version(), String::from_str(&e, "1.0.0")); + } + + #[test] + fn multiple_mints_to_different_accounts() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + let carol = Address::generate(&e); + + client.mint(&alice, &100); + client.mint(&bob, &200); + client.mint(&carol, &300); + + assert_eq!(client.balance(&alice), 100); + assert_eq!(client.balance(&bob), 200); + assert_eq!(client.balance(&carol), 300); + assert_eq!(client.total_supply(), 600); + } + + #[test] + fn approve_updates_allowance() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + client.approve(&alice, &bob, &100, &valid_expiration(&e)); + assert_eq!(client.allowance(&alice, &bob), 100); + + // Update allowance + client.approve(&alice, &bob, &200, &valid_expiration(&e)); + assert_eq!(client.allowance(&alice, &bob), 200); + } + + #[test] + fn transfer_entire_balance_works() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + client.mint(&alice, &100); + client.transfer(&alice, &bob, &100); + + assert_eq!(client.balance(&alice), 0); + assert_eq!(client.balance(&bob), 100); + } + + #[test] + fn transfer_from_entire_allowance_works() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + let carol = Address::generate(&e); + + client.mint(&alice, &100); + client.approve(&alice, &bob, &100, &valid_expiration(&e)); + client.transfer_from(&bob, &alice, &carol, &100); + + assert_eq!(client.balance(&alice), 0); + assert_eq!(client.balance(&carol), 100); + assert_eq!(client.allowance(&alice, &bob), 0); + } + + #[test] + fn approve_rejects_past_expiration() { + let e = Env::default(); + let (_, _, client) = setup(&e); + set_ledger_sequence(&e, 10); + + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + let result = client.try_approve(&alice, &bob, &50, &9); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::InvalidExpiration as u32 + ))) + ); + } + + #[test] + fn transfer_from_rejects_expired_allowance() { + let e = Env::default(); + let (_, _, client) = setup(&e); + set_ledger_sequence(&e, 10); + + let alice = Address::generate(&e); + let bob = Address::generate(&e); + let carol = Address::generate(&e); + + client.mint(&alice, &100); + client.approve(&alice, &bob, &80, &10); + + set_ledger_sequence(&e, 11); + let result = client.try_transfer_from(&bob, &alice, &carol, &20); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::AllowanceExpired as u32 + ))) + ); + assert_eq!(client.allowance(&alice, &bob), 0); + } + + #[test] + fn transfer_from_allows_when_expiration_matches_current_ledger() { + let e = Env::default(); + let (_, _, client) = setup(&e); + set_ledger_sequence(&e, 10); + + let alice = Address::generate(&e); + let bob = Address::generate(&e); + let carol = Address::generate(&e); + + client.mint(&alice, &100); + client.approve(&alice, &bob, &40, &10); + client.transfer_from(&bob, &alice, &carol, &25); + + assert_eq!(client.balance(&alice), 75); + assert_eq!(client.balance(&carol), 25); + assert_eq!(client.allowance(&alice, &bob), 15); + } + + // --- burning --- + + #[test] + fn burn_reduces_balance_and_supply() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + client.mint(&alice, &100); + client.burn(&alice, &40); + assert_eq!(client.balance(&alice), 60); + assert_eq!(client.total_supply(), 60); + } + + #[test] + fn admin_burn_reduces_balance_and_supply() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + client.mint(&alice, &100); + client.admin_burn_from(&alice, &40); + assert_eq!(client.balance(&alice), 60); + assert_eq!(client.total_supply(), 60); + } + + #[test] + fn burn_zero_reverts() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + client.mint(&alice, &100); + let result = client.try_burn(&alice, &0); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::ZeroAmount as u32 + ))) + ); + } + + #[test] + fn burn_insufficient_balance_reverts() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + client.mint(&alice, &10); + let result = client.try_burn(&alice, &50); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::InsufficientFunds as u32 + ))) + ); + } + + #[test] + fn burn_updates_delegation() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + client.mint(&alice, &100); + client.delegate(&alice, &bob); + assert_eq!(client.get_voting_power(&bob), 100); + + client.burn(&alice, &40); + assert_eq!(client.get_voting_power(&bob), 60); + } + + // --- pause / unpause --- + + #[test] + fn pause_blocks_mint_transfer_burn_approve() { + let e = Env::default(); + let (id, admin, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + // Mint some tokens before pausing + client.mint(&alice, &500); + + // Pause the contract + client.pause(&admin); + assert!(client.is_paused()); + + // mint is blocked + let res = client.try_mint(&alice, &100); + assert_eq!( + res.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::ContractPaused as u32 + ))) + ); + + // transfer is blocked + let res = client.try_transfer(&alice, &bob, &10); + assert_eq!( + res.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::ContractPaused as u32 + ))) + ); + + // burn is blocked + let res = client.try_burn(&alice, &10); + assert_eq!( + res.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::ContractPaused as u32 + ))) + ); + + // approve is blocked + let res = client.try_approve(&alice, &bob, &50, &valid_expiration(&e)); + assert_eq!( + res.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::ContractPaused as u32 + ))) + ); + + let _ = id; + } + + #[test] + fn unpause_restores_operations() { + let e = Env::default(); + let (_, admin, client) = setup(&e); + let alice = Address::generate(&e); + + client.pause(&admin); + assert!(client.is_paused()); + + client.unpause(&admin); + assert!(!client.is_paused()); + + // mint should succeed again + client.mint(&alice, &100); + assert_eq!(client.balance(&alice), 100); + } + + #[test] + fn non_admin_cannot_pause() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let attacker = Address::generate(&e); + + let res = client.try_pause(&attacker); + assert_eq!( + res.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::Unauthorized as u32 + ))) + ); + } + + #[test] + fn non_admin_cannot_unpause() { + let e = Env::default(); + let (_, admin, client) = setup(&e); + let attacker = Address::generate(&e); + + client.pause(&admin); + + let res = client.try_unpause(&attacker); + assert_eq!( + res.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + GOVError::Unauthorized as u32 + ))) + ); + } +} diff --git a/contracts/guess-the-number/Cargo.toml b/contracts/guess-the-number/Cargo.toml deleted file mode 100644 index cecf81b7..00000000 --- a/contracts/guess-the-number/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "guess-the-number" -description = "Admin sets up the pot, anyone can guess to win it" -edition.workspace = true -license.workspace = true -repository.workspace = true -publish = false -version.workspace = true - -[package.metadata.stellar] -# Set contract metadata for authors, homepage, and version based on the Cargo.toml package values -cargo_inherit = true - -[lib] -crate-type = ["cdylib"] -doctest = false - -[dependencies] -soroban-sdk = "23.0.3" -stellar-registry = "0.0.4" - -[dev-dependencies] -stellar-xdr = { version = "23.0.0", features = ["curr", "serde"] } -soroban-sdk = { version = "23.0.3", features = ["testutils"] } diff --git a/contracts/guess-the-number/src/error.rs b/contracts/guess-the-number/src/error.rs deleted file mode 100644 index 88544b51..00000000 --- a/contracts/guess-the-number/src/error.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[soroban_sdk::contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[repr(u32)] -pub enum Error { - /// The contract failed to transfer XLM to the guesser - FailedToTransferToGuesser = 1, - /// The guesser failed to transfer XLM to the contract - FailedToTransferFromGuesser = 2, - /// The contract has no balance to transfer to the guesser - NoBalanceToTransfer = 3, - -} diff --git a/contracts/guess-the-number/src/lib.rs b/contracts/guess-the-number/src/lib.rs deleted file mode 100644 index d4558a4a..00000000 --- a/contracts/guess-the-number/src/lib.rs +++ /dev/null @@ -1,116 +0,0 @@ -#![no_std] -use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Symbol}; - -mod error; -mod xlm; - -use error::Error; - -#[contract] -pub struct GuessTheNumber; - -const THE_NUMBER: &Symbol = &symbol_short!("n"); -pub const ADMIN_KEY: &Symbol = &symbol_short!("ADMIN"); - -#[contractimpl] -impl GuessTheNumber { - /// Constructor to initialize the contract with an admin and a random number - pub fn __constructor(env: &Env, admin: Address) { - // Require auth from the admin to make the transfer - admin.require_auth(); - // This is for testing purposes. Ensures that the XLM contract set up for unit testing and local network - xlm::register(env, &admin); - // Send the contract an amount of XLM to play with - xlm::token_client(env).transfer( - &admin, - env.current_contract_address(), - &xlm::to_stroops(1), - ); - // Set the admin in storage - Self::set_admin(env, admin); - // Set a random number between 1 and 10 - Self::reset_number(env); - } - - /// Update the number. Only callable by admin. - pub fn reset(env: &Env) { - Self::require_admin(env); - Self::reset_number(env); - } - - // Private function to reset the number to a new random value - // which doesn't require auth from the admin - fn reset_number(env: &Env) { - let new_number: u64 = env.prng().gen_range(1..=10); - env.storage().instance().set(THE_NUMBER, &new_number); - } - - /// Guess a number between 1 and 10 - pub fn guess(env: &Env, a_number: u64, guesser: Address) -> Result { - let xlm_client = xlm::token_client(env); - let contract_address = env.current_contract_address(); - let guessed_it = a_number == Self::number(env); - if guessed_it { - let balance = xlm_client.balance(&contract_address); - if balance == 0 { - return Err(Error::NoBalanceToTransfer); - } - // Methods `try_*` will return an error if the method fails - // `.map_err` lets us convert the error to our custom Error type - let _ = xlm_client - .try_transfer(&contract_address, &guesser, &balance) - .map_err(|_| Error::FailedToTransferToGuesser)?; - } else { - guesser.require_auth(); - let _ = xlm_client - .try_transfer(&guesser, &contract_address, &xlm::to_stroops(1)) - .map_err(|_| Error::FailedToTransferFromGuesser)?; - } - Ok(guessed_it) - } - - /// Admin can add more funds to the contract - pub fn add_funds(env: &Env, amount: i128) { - Self::require_admin(env); - let contract_address = env.current_contract_address(); - // unwrap here is safe because the admin was set in the constructor - let admin = Self::admin(env).unwrap(); - xlm::token_client(env).transfer(&admin, &contract_address, &amount); - } - - /// Upgrade the contract to new wasm. Only callable by admin. - pub fn upgrade(env: &Env, new_wasm_hash: BytesN<32>) { - Self::require_admin(env); - env.deployer().update_current_contract_wasm(new_wasm_hash); - } - - /// readonly function to get the current number - /// `pub(crate)` makes it accessible in the same crate, but not outside of it - pub(crate) fn number(env: &Env) -> u64 { - // We can unwrap because the number is set in the constructor - // and then only reset by the admin - unsafe { env.storage().instance().get(THE_NUMBER).unwrap_unchecked() } - } - - /// Get current admin - pub fn admin(env: &Env) -> Option
{ - env.storage().instance().get(ADMIN_KEY) - } - - /// Set a new admin. Only callable by admin. - pub fn set_admin(env: &Env, admin: Address) { - // Check if admin is already set - if env.storage().instance().has(ADMIN_KEY) { - panic!("admin already set"); - } - env.storage().instance().set(ADMIN_KEY, &admin); - } - - /// Private helper function to require auth from the admin - fn require_admin(env: &Env) { - let admin = Self::admin(env).expect("admin not set"); - admin.require_auth(); - } -} - -mod test; diff --git a/contracts/guess-the-number/src/test.rs b/contracts/guess-the-number/src/test.rs deleted file mode 100644 index c4f75001..00000000 --- a/contracts/guess-the-number/src/test.rs +++ /dev/null @@ -1,158 +0,0 @@ -#![cfg(test)] -// This lets use reference types in the std library for testing -extern crate std; - -use super::*; -use soroban_sdk::{ - testutils::{Address as _, MockAuth, MockAuthInvoke}, - token::StellarAssetClient, - Address, Env, IntoVal, Val, Vec, -}; - -fn init_test<'a>(env: &'a Env) -> (Address, StellarAssetClient<'a>, GuessTheNumberClient<'a>) { - let admin = Address::generate(env); - let client = generate_client(env, &admin); - // This is needed because we want to call a function from within the context of the contract - // In this case we want to get the address of the XLM contract registered by the constructor - let sac_address = env.as_contract(&client.address, || xlm::contract_id(env)); - (admin, StellarAssetClient::new(env, &sac_address), client) -} - -#[test] -fn constructed_correctly() { - let env = &Env::default(); - let (admin, sac, client) = init_test(env); - // Check that the admin is set correctly - assert_eq!(client.admin(), Some(admin.clone())); - // Check that the contract has a balance of 1 XLM - assert_eq!(sac.balance(&client.address), xlm::to_stroops(1)); - // Need to use `as_contract` to call a function in the context of the contract - // Since the method `number` is not in the client, but is visibile in the crate - let number = env.as_contract(&client.address, || GuessTheNumber::number(env)); - assert_eq!(number, 4); -} - -#[test] -fn only_admin_can_reset() { - let env = &Env::default(); - let (admin, _, client) = init_test(env); - let user = Address::generate(env); - - set_caller(&client, "reset", &user, ()); - assert!(client.try_reset().is_err()); - - set_caller(&client, "reset", &admin, ()); - assert!(client.try_reset().is_ok()); -} - -#[test] -fn guess() { - let env = &Env::default(); - let (_, sac, client) = init_test(env); - // This lets you mock all auth when they become complicated when making cross contract calls. - env.mock_all_auths(); - - // Create a user to guess - let alice = Address::generate(env); - // Mint tokens to the user. On testnet you use friendbot to fund the account. - sac.mint(&alice, &xlm::to_stroops(2)); - // Check that alice has the tokens - assert_eq!(sac.balance(&alice), xlm::to_stroops(2)); - - // Create another user with no funds - let bob = Address::generate(env); - - // In the testing enviroment the random seed is always the same initially. - // This tests a wrong guess so the balance should go down one XLM - assert!(!client.guess(&3, &alice)); - assert_eq!(sac.balance(&alice), xlm::to_stroops(1)); - - // Now we test a wrong guess but the user has no funds so we get an error - assert_eq!( - client.try_guess(&3, &bob).unwrap_err(), - Ok(Error::FailedToTransferFromGuesser) - ); - - // Now we test a correct guess, the balance should go up by the initial 1 XLM + the 1 XLM from the contract - assert!(client.guess(&4, &alice)); - assert_eq!(sac.balance(&alice), xlm::to_stroops(3)); - - assert_eq!( - client.try_guess(&4, &alice).unwrap_err(), - Ok(Error::NoBalanceToTransfer) - ); -} - -#[test] -fn add_funds() { - let env = &Env::default(); - let (_, sac, client) = init_test(env); - // This lets you mock all auth when they become complicated when making cross contract calls. - env.mock_all_auths(); - - // Create a user to guess - let alice = Address::generate(env); - // Mint tokens to the user. On testnet you use friendbot to fund the account. - sac.mint(&alice, &xlm::to_stroops(2)); - // Now we test a correct guess, the balance should go up by the initial 1 XLM + the 1 XLM from the contract - assert!(client.guess(&4, &alice)); - assert_eq!(sac.balance(&alice), xlm::to_stroops(3)); - assert_eq!(sac.balance(&client.address), 0); - - client.add_funds(&xlm::to_stroops(5)); - assert_eq!(sac.balance(&client.address), xlm::to_stroops(5)); - - // Since we didn't reset the number, the guess should still be correct - assert!(client.guess(&4, &alice)); - assert_eq!(sac.balance(&alice), xlm::to_stroops(8)); - assert_eq!(sac.balance(&client.address), 0); -} - -#[test] -fn reset_and_guess() { - let env = &Env::default(); - let (_, sac, client) = init_test(env); - // This lets you mock all auth when they become complicated when making cross contract calls. - env.mock_all_auths(); - - // Create a user to guess - let alice = Address::generate(env); - // Mint tokens to the user. On testnet you use friendbot to fund the account. - sac.mint(&alice, &xlm::to_stroops(2)); - - // Reset the number - client.reset(); - - // Guess again, this should be correct now - assert!(client.guess(&10, &alice)); -} - -fn generate_client<'a>(env: &Env, admin: &Address) -> GuessTheNumberClient<'a> { - let contract_id = Address::generate(env); - env.mock_all_auths(); - let contract_id = env.register_at(&contract_id, GuessTheNumber, (admin,)); - env.set_auths(&[]); // clear auths - GuessTheNumberClient::new(env, &contract_id) -} - -// This lets you mock the auth context for a function call -fn set_caller(client: &GuessTheNumberClient, fn_name: &str, caller: &Address, args: T) -where - T: IntoVal>, -{ - // clear previous auth mocks - client.env.set_auths(&[]); - - let invoke = &MockAuthInvoke { - contract: &client.address, - fn_name, - args: args.into_val(&client.env), - sub_invokes: &[], - }; - - // mock auth as passed-in address - client.env.mock_auths(&[MockAuth { - address: &caller, - invoke, - }]); -} diff --git a/contracts/guess-the-number/src/xlm.rs b/contracts/guess-the-number/src/xlm.rs deleted file mode 100644 index 006693d9..00000000 --- a/contracts/guess-the-number/src/xlm.rs +++ /dev/null @@ -1,60 +0,0 @@ -#[cfg(test)] -mod xlm { - use super::*; - const XLM_KEY: &soroban_sdk::Symbol = &soroban_sdk::symbol_short!("XLM"); - - pub fn contract_id(env: &soroban_sdk::Env) -> soroban_sdk::Address { - env.storage() - .instance() - .get::<_, soroban_sdk::Address>(XLM_KEY) - .expect("XLM contract not initialized. Please deploy the XLM contract first.") - } - - pub fn register( - env: &soroban_sdk::Env, - admin: &soroban_sdk::Address, - ) -> soroban_sdk::testutils::StellarAssetContract { - let sac = env.register_stellar_asset_contract_v2(admin.clone()); - env.storage().instance().set(XLM_KEY, &sac.address()); - stellar_asset_client(env).mint(admin, &to_stroops(10_000)); - sac - } - - #[allow(unused)] - pub fn stellar_asset_client<'a>( - env: &soroban_sdk::Env, - ) -> soroban_sdk::token::StellarAssetClient<'a> { - soroban_sdk::token::StellarAssetClient::new(&env, &contract_id(env)) - } - /// Create a Stellar Asset Client for the asset which provides an admin interface - pub fn token_client<'a>(env: &soroban_sdk::Env) -> soroban_sdk::token::TokenClient<'a> { - soroban_sdk::token::TokenClient::new(&env, &contract_id(env)) - } -} -const ONE_XLM: i128 = 1_000_000_0; // 1 XLM in stroops; - -pub const fn to_stroops(num: u64) -> i128 { - (num as i128) * ONE_XLM -} - -#[cfg(not(test))] -stellar_registry::import_asset!("xlm"); - -#[allow(unused)] -pub const SERIALIZED_ASSET: [u8; 4] = [0, 0, 0, 0]; - -pub use xlm::*; -mod register { - - #[allow(unused)] - #[cfg(not(test))] - pub fn register(env: &soroban_sdk::Env, admin: &soroban_sdk::Address) { - let balance = super::token_client(env).try_balance(&env.current_contract_address()); - if balance.is_err() { - env.deployer().with_stellar_asset(super::SERIALIZED_ASSET).deploy(); - } - } -} - -#[allow(unused_imports)] -pub use register::*; \ No newline at end of file diff --git a/contracts/learn_token/Cargo.toml b/contracts/learn_token/Cargo.toml new file mode 100644 index 00000000..6ee2237f --- /dev/null +++ b/contracts/learn_token/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "learn-token" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +learnvault-shared = { workspace = true } +soroban-sdk = { workspace = true } + +[dev-dependencies] +learnvault-shared = { workspace = true, features = ["testutils"] } +soroban-sdk = { workspace = true, features = ["testutils"] } +proptest = { workspace = true } diff --git a/contracts/learn_token/src/lib.rs b/contracts/learn_token/src/lib.rs new file mode 100644 index 00000000..9f834fa9 --- /dev/null +++ b/contracts/learn_token/src/lib.rs @@ -0,0 +1,281 @@ +#![no_std] +#![allow(deprecated)] + +//! # LearnToken (LRN) +//! +//! A **soulbound** (non-transferable) SEP-41 fungible token minted to learners +//! on verified course milestone completion. Represents real, on-chain proof of +//! effort — it cannot be sold or transferred. +//! +//! - Only the admin (CourseMilestone contract) can mint. +//! - Non-transferable by design. +//! - No burning in V1. +//! +//! ## Relevant issue +//! Implements: https://github.com/bakeronchain/learnvault/issues/5 + +use soroban_sdk::{ + Address, BytesN, Env, String, Symbol, contract, contracterror, contractimpl, contracttype, + panic_with_error, symbol_short, +}; + +use learnvault_shared::upgrade; + +pub use upgrade::ContractUpgraded; + +// --------------------------------------------------------------------------- +// Storage Constants (assuming ~6s ledger time) +// --------------------------------------------------------------------------- + +const DAY_IN_LEDGERS: u32 = 17_280; +const INSTANCE_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; // 30 days +const PERSISTENT_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const PERSISTENT_EXTEND_TO: u32 = DAY_IN_LEDGERS * 365; // 1 year + +// --------------------------------------------------------------------------- +// Errors +// --------------------------------------------------------------------------- + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum LRNError { + /// Caller is not the contract admin. + Unauthorized = 1, + /// Amount must be greater than zero. + ZeroAmount = 2, + /// Contract has not been initialized. + NotInitialized = 3, + /// Token is soulbound and cannot be transferred. + Soulbound = 4, +} + +// --------------------------------------------------------------------------- +// Storage keys +// --------------------------------------------------------------------------- + +const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); +const NAME_KEY: Symbol = symbol_short!("NAME"); +const SYMBOL_KEY: Symbol = symbol_short!("SYMBOL"); +const DECIMALS_KEY: Symbol = symbol_short!("DECIMALS"); + +#[contracttype] +pub enum DataKey { + Balance(Address), + TotalSupply, +} + +// --------------------------------------------------------------------------- +// Contract +// --------------------------------------------------------------------------- + +#[contract] +pub struct LearnToken; + +#[contractimpl] +impl LearnToken { + /// Initialise the contract. Can only be called once. + /// + /// Sets name = "LearnVault Learn Token", symbol = "LRN", decimals = 7. + pub fn initialize(env: Env, admin: Address) { + if env.storage().instance().has(&ADMIN_KEY) { + panic_with_error!(&env, LRNError::Unauthorized); + } + env.storage().instance().set(&ADMIN_KEY, &admin); + upgrade::init(&env); + env.storage() + .instance() + .set(&NAME_KEY, &String::from_str(&env, "LearnVault Learn Token")); + env.storage() + .instance() + .set(&SYMBOL_KEY, &String::from_str(&env, "LRN")); + env.storage().instance().set(&DECIMALS_KEY, &7_u32); + + Self::extend_instance(&env); + } + + // ----------------------------------------------------------------------- + // Admin + // ----------------------------------------------------------------------- + + /// Mint `amount` LRN to `to`. Admin only. + pub fn mint(env: Env, to: Address, amount: i128) { + Self::extend_instance(&env); + // 1. Load admin from storage, call admin.require_auth() + let admin: Address = env + .storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(&env, LRNError::NotInitialized)); + admin.require_auth(); + + // 2. Panic with ZeroAmount if amount <= 0 + if amount <= 0 { + panic_with_error!(&env, LRNError::ZeroAmount); + } + + // 3. Add amount to Balance(to) in persistent storage + let bal_key = DataKey::Balance(to.clone()); + let bal: i128 = env.storage().persistent().get(&bal_key).unwrap_or(0); + env.storage().persistent().set(&bal_key, &(bal + amount)); + + // 4. Add amount to TotalSupply in persistent storage + let supply: i128 = env + .storage() + .persistent() + .get(&DataKey::TotalSupply) + .unwrap_or(0); + env.storage() + .persistent() + .set(&DataKey::TotalSupply, &(supply + amount)); + + // Extend persistent storage for balance entries + env.storage().persistent().extend_ttl( + &bal_key, + PERSISTENT_BUMP_THRESHOLD, + PERSISTENT_EXTEND_TO, + ); + env.storage().persistent().extend_ttl( + &DataKey::TotalSupply, + PERSISTENT_BUMP_THRESHOLD, + PERSISTENT_EXTEND_TO, + ); + + // 5. Emit event + env.events() + .publish((symbol_short!("lrn_mint"), to.clone()), amount); + } + + /// Transfer the admin role to a new address. Admin only. + pub fn set_admin(env: Env, new_admin: Address) { + Self::extend_instance(&env); + let admin: Address = env + .storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(&env, LRNError::NotInitialized)); + admin.require_auth(); + env.storage().instance().set(&ADMIN_KEY, &new_admin); + env.events() + .publish((symbol_short!("set_admin"),), new_admin); + } + + /// Replace the current contract WASM with a new uploaded hash. Admin only. + pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { + Self::extend_instance(&env); + let admin: Address = env + .storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(&env, LRNError::NotInitialized)); + admin.require_auth(); + upgrade::apply(&env, &admin, &new_wasm_hash); + } + + /// Transfer is not allowed — LRN is soulbound. + pub fn transfer(_env: Env, _from: Address, _to: Address, _amount: i128) { + panic_with_error!(&_env, LRNError::Soulbound); + } + + /// Transfer from is not allowed — LRN is soulbound. + pub fn transfer_from( + _env: Env, + _spender: Address, + _from: Address, + _to: Address, + _amount: i128, + ) { + panic_with_error!(&_env, LRNError::Soulbound); + } + + /// Approve is not allowed — LRN is soulbound. + pub fn approve(_env: Env, _from: Address, _spender: Address, _amount: i128) { + panic_with_error!(&_env, LRNError::Soulbound); + } + + /// Allowance always returns 0 — LRN is soulbound and cannot be transferred. + pub fn allowance(_env: Env, _from: Address, _spender: Address) -> i128 { + 0 + } + + // ----------------------------------------------------------------------- + // Read functions + // ----------------------------------------------------------------------- + + pub fn balance(env: Env, account: Address) -> i128 { + Self::extend_instance(&env); + let key = DataKey::Balance(account); + if let Some(bal) = env.storage().persistent().get::<_, i128>(&key) { + env.storage().persistent().extend_ttl( + &key, + PERSISTENT_BUMP_THRESHOLD, + PERSISTENT_EXTEND_TO, + ); + bal + } else { + 0 + } + } + + pub fn total_supply(env: Env) -> i128 { + Self::extend_instance(&env); + let key = DataKey::TotalSupply; + if let Some(supply) = env.storage().persistent().get::<_, i128>(&key) { + env.storage().persistent().extend_ttl( + &key, + PERSISTENT_BUMP_THRESHOLD, + PERSISTENT_EXTEND_TO, + ); + supply + } else { + 0 + } + } + + pub fn decimals(env: Env) -> u32 { + env.storage().instance().get(&DECIMALS_KEY).unwrap_or(7) + } + + pub fn name(env: Env) -> String { + env.storage() + .instance() + .get(&NAME_KEY) + .unwrap_or_else(|| String::from_str(&env, "LearnToken")) + } + + pub fn symbol(env: Env) -> String { + env.storage() + .instance() + .get(&SYMBOL_KEY) + .unwrap_or_else(|| String::from_str(&env, "LRN")) + } + + pub fn get_version(env: Env) -> String { + String::from_str(&env, "1.0.0") + } + + /// Calculate reputation score based on balance. + /// Formula: reputation = balance / 100 (integer division) + pub fn reputation_score(env: Env, account: Address) -> i128 { + let balance = Self::balance(env, account); + balance / 100 + } + + // ----------------------------------------------------------------------- + // Internal helpers + // ----------------------------------------------------------------------- + + fn extend_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_BUMP_THRESHOLD, INSTANCE_EXTEND_TO); + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod test; diff --git a/contracts/learn_token/src/test.rs b/contracts/learn_token/src/test.rs new file mode 100644 index 00000000..441fe818 --- /dev/null +++ b/contracts/learn_token/src/test.rs @@ -0,0 +1,808 @@ +#![cfg(test)] + +use proptest::prelude::*; +use soroban_sdk::{ + Address, BytesN, Env, + testutils::{Address as _, MockAuth, MockAuthInvoke}, + token::{StellarAssetClient, TokenClient}, +}; + +proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + #[test] + #[ignore] + fn test_fuzz_mint_random_amounts(amount in 0..u128::MAX) { + let env = Env::default(); + let admin = Address::generate(&env); + let user = Address::generate(&env); + + env.mock_all_auths(); + + // Register the standard generic Soroban token (LearnToken equivalent) + let token_contract_id = env.register_stellar_asset_contract_v2(admin.clone()); + let token_id = token_contract_id.address(); + + let client = StellarAssetClient::new(&env, &token_id); + let token_client = TokenClient::new(&env, &token_id); + + let safe_amount = if amount > i128::MAX as u128 { + i128::MAX + } else { + amount as i128 + }; + + // Execute mint + client.mint(&user, &safe_amount); + + // Verify balance and no panic + let balance = token_client.balance(&user); + assert_eq!(balance, safe_amount); + } +} // close proptest! + +extern crate std; + +use soroban_sdk::{IntoVal, testutils::Events as _}; + +use crate::{DataKey, LRNError, LearnToken, LearnTokenClient}; + +fn setup(e: &Env) -> (Address, Address, LearnTokenClient) { + let admin = Address::generate(e); + let id = e.register(LearnToken, ()); + e.mock_all_auths(); + let client = LearnTokenClient::new(e, &id); + client.initialize(&admin); + (id, admin, client) +} + +fn authorize_upgrade(e: &Env, contract_id: &Address, signer: &Address, wasm_hash: &BytesN<32>) { + e.mock_auths(&[MockAuth { + address: signer, + invoke: &MockAuthInvoke { + contract: contract_id, + fn_name: "upgrade", + args: (wasm_hash.clone(),).into_val(e), + sub_invokes: &[], + }, + }]); +} + +// --- mint: happy path --- + +#[test] +fn mint_increases_balance_and_supply() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let learner = Address::generate(&e); + + client.mint(&learner, &100); + + assert_eq!(client.balance(&learner), 100); + assert_eq!(client.total_supply(), 100); +} + +#[test] +fn mint_accumulates_on_repeated_calls() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let learner = Address::generate(&e); + + client.mint(&learner, &200); + client.mint(&learner, &300); + + assert_eq!(client.balance(&learner), 500); + assert_eq!(client.total_supply(), 500); +} + +#[test] +fn mint_to_multiple_accounts_tracks_supply() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + client.mint(&alice, &100); + client.mint(&bob, &250); + + assert_eq!(client.balance(&alice), 100); + assert_eq!(client.balance(&bob), 250); + assert_eq!(client.total_supply(), 350); +} + +// --- mint: event emission --- + +#[test] +fn mint_emits_event() { + let e = Env::default(); + let (contract_id, _, client) = setup(&e); + let learner = Address::generate(&e); + + client.mint(&learner, &42); + + let events = e.events().all(); + // Find the lrn_mint event — check contract id and that the topic tuple + // contains the "lrn_mint" symbol and the recipient address. + use soroban_sdk::{symbol_short, vec}; + let found = events.iter().any(|(cid, topics, _data)| { + cid == contract_id + && topics + == vec![ + &e, + symbol_short!("lrn_mint").into_val(&e), + learner.clone().into_val(&e), + ] + }); + assert!(found, "lrn_mint event not found"); +} + +// --- mint: non-admin panics --- + +#[test] +fn non_admin_mint_panics() { + let e = Env::default(); + let admin = Address::generate(&e); + let id = e.register(LearnToken, ()); + + // Only mock auth for initialize, not for mint + e.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &id, + fn_name: "initialize", + args: (admin.clone(),).into_val(&e), + sub_invokes: &[], + }, + }]); + + let client = LearnTokenClient::new(&e, &id); + client.initialize(&admin); + + let learner = Address::generate(&e); + let result = client.try_mint(&learner, &100); + assert!(result.is_err()); +} + +// --- mint: zero amount panics --- + +#[test] +fn zero_amount_mint_panics() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let learner = Address::generate(&e); + + let result = client.try_mint(&learner, &0); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + LRNError::ZeroAmount as u32 + ))) + ); +} + +#[test] +fn negative_amount_mint_panics() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let learner = Address::generate(&e); + + let result = client.try_mint(&learner, &-1); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + LRNError::ZeroAmount as u32 + ))) + ); +} + +// --- misc --- + +#[test] +fn balance_of_unknown_account_is_zero() { + let e = Env::default(); + let (_, _, client) = setup(&e); + assert_eq!(client.balance(&Address::generate(&e)), 0); +} + +#[test] +fn total_supply_starts_at_zero() { + let e = Env::default(); + let (_, _, client) = setup(&e); + assert_eq!(client.total_supply(), 0); +} + +// --- fuzz tests --- + +use proptest::prelude::*; + +proptest! { + #[test] + #[ignore] + fn fuzz_mint(amount in any::()) { + let e = Env::default(); + let (_, _, client) = setup(&e); + let learner = Address::generate(&e); + + // The contract expects i128. Let's safely cast u128 to i128 or trap. + // If it's outside i128 max, it might cast to a negative number or we can just cap it / wrap it. + let amount_i128 = amount as i128; + + let result = client.try_mint(&learner, &amount_i128); + + if amount_i128 <= 0 { + // Must return ZeroAmount error + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + crate::LRNError::ZeroAmount as u32 + ))) + ); + } else { + // Valid mint amount, should succeed + assert!(result.is_ok()); + assert_eq!(client.balance(&learner), amount_i128); + assert_eq!(client.total_supply(), amount_i128); + } + } +} + +#[test] +fn get_version_returns_semver() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let version = client.get_version(); + assert_eq!(version, soroban_sdk::String::from_str(&e, "1.0.0")); +} + +// --- initialize: comprehensive tests --- + +#[test] +fn initialize_sets_admin_correctly() { + let e = Env::default(); + let admin = Address::generate(&e); + let id = e.register(LearnToken, ()); + e.mock_all_auths(); + let client = LearnTokenClient::new(&e, &id); + client.initialize(&admin); + + // Verify admin can mint (only admin can mint) + let learner = Address::generate(&e); + client.mint(&learner, &100); + assert_eq!(client.balance(&learner), 100); +} + +#[test] +fn initialize_sets_name_symbol_decimals() { + let e = Env::default(); + let (_, _, client) = setup(&e); + + use soroban_sdk::String; + assert_eq!( + client.name(), + String::from_str(&e, "LearnVault Learn Token") + ); + assert_eq!(client.symbol(), String::from_str(&e, "LRN")); + assert_eq!(client.decimals(), 7); +} + +#[test] +fn double_initialize_rejected() { + let e = Env::default(); + let admin = Address::generate(&e); + let id = e.register(LearnToken, ()); + e.mock_all_auths(); + let client = LearnTokenClient::new(&e, &id); + + client.initialize(&admin); + + // Try to initialize again + let new_admin = Address::generate(&e); + let result = client.try_initialize(&new_admin); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + LRNError::Unauthorized as u32 + ))) + ); +} + +// --- transfer: soulbound tests --- + +#[test] +fn transfer_panics_with_soulbound_error() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + client.mint(&alice, &100); + + let result = client.try_transfer(&alice, &bob, &50); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + LRNError::Soulbound as u32 + ))) + ); + + // Verify balance unchanged + assert_eq!(client.balance(&alice), 100); + assert_eq!(client.balance(&bob), 0); +} + +#[test] +fn transfer_always_panics_even_with_zero_amount() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + let result = client.try_transfer(&alice, &bob, &0); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + LRNError::Soulbound as u32 + ))) + ); +} + +// --- reputation_score tests --- + +#[test] +fn reputation_score_increases_with_balance() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let learner = Address::generate(&e); + + // No balance = 0 reputation + assert_eq!(client.reputation_score(&learner), 0); + + // 100 LRN = 1 reputation + client.mint(&learner, &100); + assert_eq!(client.reputation_score(&learner), 1); + + // 500 LRN = 5 reputation + client.mint(&learner, &400); + assert_eq!(client.reputation_score(&learner), 5); + + // 999 LRN = 9 reputation (integer division) + client.mint(&learner, &499); + assert_eq!(client.reputation_score(&learner), 9); + + // 1000 LRN = 10 reputation + client.mint(&learner, &1); + assert_eq!(client.reputation_score(&learner), 10); +} + +#[test] +fn reputation_score_proportional_to_balance() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let learner = Address::generate(&e); + + client.mint(&learner, &12345); + assert_eq!(client.reputation_score(&learner), 123); +} + +#[test] +fn reputation_score_zero_for_unknown_address() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let unknown = Address::generate(&e); + + assert_eq!(client.reputation_score(&unknown), 0); +} + +// --- set_admin tests --- + +#[test] +fn set_admin_transfers_admin_rights() { + let e = Env::default(); + let old_admin = Address::generate(&e); + let new_admin = Address::generate(&e); + let id = e.register(LearnToken, ()); + e.mock_all_auths(); + + let client = LearnTokenClient::new(&e, &id); + client.initialize(&old_admin); + + // Transfer admin + client.set_admin(&new_admin); + + // New admin can mint + let learner = Address::generate(&e); + client.mint(&learner, &100); + assert_eq!(client.balance(&learner), 100); +} + +#[test] +fn set_admin_only_callable_by_current_admin() { + let e = Env::default(); + let admin = Address::generate(&e); + let attacker = Address::generate(&e); + let id = e.register(LearnToken, ()); + + // Only mock auth for initialize + e.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &id, + fn_name: "initialize", + args: (admin.clone(),).into_val(&e), + sub_invokes: &[], + }, + }]); + + let client = LearnTokenClient::new(&e, &id); + client.initialize(&admin); + + // Attacker tries to set themselves as admin + let result = client.try_set_admin(&attacker); + assert!(result.is_err()); +} + +#[test] +fn set_admin_emits_event() { + let e = Env::default(); + let (contract_id, _, client) = setup(&e); + let new_admin = Address::generate(&e); + + client.set_admin(&new_admin); + + let events = e.events().all(); + use soroban_sdk::{symbol_short, vec}; + let found = events.iter().any(|(cid, topics, _data)| { + cid == contract_id && topics == vec![&e, symbol_short!("set_admin").into_val(&e)] + }); + assert!(found, "set_admin event not found"); +} + +// --- mint: additional edge case tests --- + +#[test] +fn mint_before_initialize_panics() { + let e = Env::default(); + let id = e.register(LearnToken, ()); + e.mock_all_auths(); + let client = LearnTokenClient::new(&e, &id); + + let learner = Address::generate(&e); + let result = client.try_mint(&learner, &100); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + LRNError::NotInitialized as u32 + ))) + ); +} + +// --- transfer_from: soulbound tests --- + +#[test] +fn transfer_from_panics_with_soulbound_error() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + client.mint(&alice, &100); + + // Even with proper authorization, transfer_from should fail + let result = client.try_transfer_from(&alice, &alice, &bob, &50); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + LRNError::Soulbound as u32 + ))) + ); + + // Verify balance unchanged + assert_eq!(client.balance(&alice), 100); + assert_eq!(client.balance(&bob), 0); +} + +#[test] +fn transfer_from_always_panics_even_with_zero_amount() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + let result = client.try_transfer_from(&alice, &alice, &bob, &0); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + LRNError::Soulbound as u32 + ))) + ); +} + +#[test] +fn transfer_from_panics_regardless_of_spender() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + let charlie = Address::generate(&e); + + client.mint(&alice, &100); + + // charlie (spender) tries to transfer from alice to bob + let result = client.try_transfer_from(&charlie, &alice, &bob, &50); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + LRNError::Soulbound as u32 + ))) + ); +} + +// --- approve: soulbound tests --- + +#[test] +fn approve_panics_with_soulbound_error() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + client.mint(&alice, &100); + + let result = client.try_approve(&alice, &bob, &50); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + LRNError::Soulbound as u32 + ))) + ); +} + +#[test] +fn approve_always_panics_even_with_zero_amount() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + let result = client.try_approve(&alice, &bob, &0); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + LRNError::Soulbound as u32 + ))) + ); +} + +#[test] +fn approve_panics_even_for_non_existent_balance() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + // alice has no LRN balance + let result = client.try_approve(&alice, &bob, &50); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + LRNError::Soulbound as u32 + ))) + ); +} + +// --- allowance: soulbound tests --- + +#[test] +fn allowance_returns_zero() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + let allowance = client.allowance(&alice, &bob); + assert_eq!(allowance, 0); +} + +#[test] +fn allowance_always_returns_zero_regardless_of_accounts() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + let charlie = Address::generate(&e); + + // Mint some tokens to alice + client.mint(&alice, &1000); + + // Allowance should still be 0 for any pair + assert_eq!(client.allowance(&alice, &bob), 0); + assert_eq!(client.allowance(&alice, &charlie), 0); + assert_eq!(client.allowance(&bob, &charlie), 0); + assert_eq!(client.allowance(&charlie, &alice), 0); +} + +#[test] +fn allowance_returns_zero_for_same_address() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + + client.mint(&alice, &500); + + // Even allowance from alice to herself should be 0 + assert_eq!(client.allowance(&alice, &alice), 0); +} + +// --- admin transfer (set_admin tests) --- + +#[test] +fn admin_transfers_always_succeed() { + let e = Env::default(); + let admin1 = Address::generate(&e); + let admin2 = Address::generate(&e); + let admin3 = Address::generate(&e); + let id = e.register(LearnToken, ()); + e.mock_all_auths(); + + let client = LearnTokenClient::new(&e, &id); + client.initialize(&admin1); + + // First transfer + client.set_admin(&admin2); + + // Second transfer (new admin can transfer admin) + client.set_admin(&admin3); + + // Verify final admin is admin3 + assert_eq!(client.total_supply(), 0); + + // admin3 should be able to mint (verifies admin transfer worked) + let learner = Address::generate(&e); + client.mint(&learner, &100); + assert_eq!(client.balance(&learner), 100); +} + +// --- initialization completeness tests --- + +#[test] +fn initialized_contract_has_all_metadata() { + let e = Env::default(); + let (_, _, client) = setup(&e); + + use soroban_sdk::String; + + // All metadata should be set + assert_eq!( + client.name(), + String::from_str(&e, "LearnVault Learn Token") + ); + assert_eq!(client.symbol(), String::from_str(&e, "LRN")); + assert_eq!(client.decimals(), 7); + assert_eq!(client.get_version(), String::from_str(&e, "1.0.0")); + assert_eq!(client.total_supply(), 0); +} + +// --- comprehensive mint/supply tracking --- + +#[test] +fn large_mint_amounts_tracked_correctly() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let learner = Address::generate(&e); + + // Test with large amounts + let large_amount: i128 = 1_000_000_000; + client.mint(&learner, &large_amount); + + assert_eq!(client.balance(&learner), large_amount); + assert_eq!(client.total_supply(), large_amount); +} + +#[test] +fn multiple_small_mints_vs_single_large_mint() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let alice = Address::generate(&e); + let bob = Address::generate(&e); + + // Alice receives 10 mints of 100 each + for _ in 0..10 { + client.mint(&alice, &100); + } + + // Bob receives a single mint of 1000 + client.mint(&bob, &1000); + + assert_eq!(client.balance(&alice), 1000); + assert_eq!(client.balance(&bob), 1000); + assert_eq!(client.total_supply(), 2000); +} + +// --- reputation score edge cases --- + +#[test] +fn reputation_score_matches_balance_division() { + let e = Env::default(); + let (_, _, client) = setup(&e); + let learner = Address::generate(&e); + + for _amount in [1, 10, 99, 100, 101, 999, 1000, 9999, 10000] { + client.mint(&learner, &1); // Increment balance one at a time + let balance = client.balance(&learner); + let reputation = client.reputation_score(&learner); + + assert_eq!( + reputation, + balance / 100, + "Reputation should match balance / 100 at balance = {}", + balance + ); + } +} + +#[test] +fn upgrade_requires_admin_auth() { + let e = Env::default(); + let admin = Address::generate(&e); + let attacker = Address::generate(&e); + let id = e.register(LearnToken, ()); + + e.mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &id, + fn_name: "initialize", + args: (admin.clone(),).into_val(&e), + sub_invokes: &[], + }, + }]); + + let client = LearnTokenClient::new(&e, &id); + client.initialize(&admin); + + let wasm_hash = crate::upgrade::testutils::upload_upgrade_target(&e); + authorize_upgrade(&e, &id, &attacker, &wasm_hash); + + assert!(client.try_upgrade(&wasm_hash).is_err()); +} + +#[test] +fn state_persists_after_upgrade() { + let e = Env::default(); + let (id, admin, client) = setup(&e); + let learner = Address::generate(&e); + + client.mint(&learner, &100); + + e.set_auths(&[]); + let wasm_hash = crate::upgrade::testutils::upload_upgrade_target(&e); + authorize_upgrade(&e, &id, &admin, &wasm_hash); + client.upgrade(&wasm_hash); + + let balance = e.as_contract(&id, || { + e.storage() + .persistent() + .get::<_, i128>(&DataKey::Balance(learner.clone())) + .unwrap_or(0) + }); + let supply = e.as_contract(&id, || { + e.storage() + .persistent() + .get::<_, i128>(&DataKey::TotalSupply) + .unwrap_or(0) + }); + let stored_hash = e.as_contract(&id, || crate::upgrade::current_hash(&e)); + + assert_eq!(balance, 100); + assert_eq!(supply, 100); + assert_eq!(stored_hash, wasm_hash); +} diff --git a/contracts/milestone_escrow/Cargo.toml b/contracts/milestone_escrow/Cargo.toml new file mode 100644 index 00000000..8924d265 --- /dev/null +++ b/contracts/milestone_escrow/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "milestone-escrow" +description = "Milestone-based escrow for scholarship disbursements" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[package.metadata.stellar] +cargo_inherit = true + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +learnvault-shared = { workspace = true } +soroban-sdk = { workspace = true } +stellar-registry = "0.0.4" + +[dev-dependencies] +learnvault-shared = { workspace = true, features = ["testutils"] } +soroban-sdk = { workspace = true, features = ["testutils"] } +proptest = { workspace = true } diff --git a/contracts/milestone_escrow/src/lib.rs b/contracts/milestone_escrow/src/lib.rs new file mode 100644 index 00000000..df926272 --- /dev/null +++ b/contracts/milestone_escrow/src/lib.rs @@ -0,0 +1,315 @@ +#![no_std] + +use soroban_sdk::{ + Address, BytesN, Env, String, Symbol, contract, contracterror, contractevent, contractimpl, + contracttype, panic_with_error, symbol_short, +}; + +use learnvault_shared::upgrade; + +pub use upgrade::ContractUpgraded; + +const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); +const TREASURY_KEY: Symbol = symbol_short!("TREAS"); +const INACTIVITY_WINDOW_KEY: Symbol = symbol_short!("INACT_W"); + +#[derive(Clone)] +#[contracttype] +pub struct EscrowRecord { + pub scholar: Address, + pub total_amount: i128, + pub released_amount: i128, + pub total_tranches: u32, + pub tranches_released: u32, + pub last_activity: u64, + pub treasury: Address, + pub admin: Address, +} + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Escrow(u32), +} + +#[contracterror] +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + AlreadyInitialized = 1, + NotInitialized = 2, + EscrowExists = 3, + EscrowNotFound = 4, + InvalidAmount = 5, + InvalidTranches = 6, + AllTranchesReleased = 7, + Overpayment = 8, + InactivityNotReached = 9, + NothingToReclaim = 10, +} + +#[contract] +pub struct MilestoneEscrow; + +#[contractevent(topics = ["released"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TrancheReleased { + #[topic] + pub scholar: Address, + #[topic] + pub proposal_id: u32, + pub amount: i128, +} + +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EscrowCreated { + pub proposal_id: u32, + pub scholar: Address, + pub total_amount: i128, + pub total_tranches: u32, +} + +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EscrowReclaimed { + pub proposal_id: u32, + pub scholar: Address, + pub amount_reclaimed: i128, +} + +#[contractimpl] +impl MilestoneEscrow { + pub fn initialize(env: Env, admin: Address, treasury: Address, inactivity_window_seconds: u64) { + if env.storage().instance().has(&ADMIN_KEY) { + panic_with_error!(&env, Error::AlreadyInitialized); + } + admin.require_auth(); + + // Keep 30 days (30 * 24 * 60 * 60) as the recommended default at deployment. + env.storage().instance().set(&ADMIN_KEY, &admin); + upgrade::init(&env); + env.storage().instance().set(&TREASURY_KEY, &treasury); + env.storage() + .instance() + .set(&INACTIVITY_WINDOW_KEY, &inactivity_window_seconds); + } + + pub fn create_escrow( + env: Env, + proposal_id: u32, + scholar: Address, + amount: i128, + tranches: u32, + ) { + let treasury = Self::treasury(&env); + treasury.require_auth(); + + if amount <= 0 { + panic_with_error!(&env, Error::InvalidAmount); + } + if tranches == 0 { + panic_with_error!(&env, Error::InvalidTranches); + } + + let key = DataKey::Escrow(proposal_id); + if env.storage().persistent().has(&key) { + panic_with_error!(&env, Error::EscrowExists); + } + + xlm::token_client(&env).transfer(&treasury, env.current_contract_address(), &amount); + + let record = EscrowRecord { + scholar, + total_amount: amount, + released_amount: 0, + total_tranches: tranches, + tranches_released: 0, + last_activity: env.ledger().timestamp(), + treasury: treasury.clone(), + admin: Self::admin(&env), + }; + env.storage().persistent().set(&key, &record); + EscrowCreated { + proposal_id, + scholar: record.scholar.clone(), + total_amount: record.total_amount, + total_tranches: record.total_tranches, + } + .publish(&env); + } + + pub fn release_tranche(env: Env, proposal_id: u32) { + let key = DataKey::Escrow(proposal_id); + let mut record = Self::get_or_panic(&env, &key); + + record.admin.require_auth(); + + if record.tranches_released >= record.total_tranches { + panic_with_error!(&env, Error::AllTranchesReleased); + } + + let amount = Self::next_tranche_amount(&env, &record); + xlm::token_client(&env).transfer(&env.current_contract_address(), &record.scholar, &amount); + + record.released_amount += amount; + record.tranches_released += 1; + record.last_activity = env.ledger().timestamp(); + env.storage().persistent().set(&key, &record); + + TrancheReleased { + scholar: record.scholar.clone(), + proposal_id, + amount, + } + .publish(&env); + } + + pub fn reclaim_inactive(env: Env, proposal_id: u32) { + let key = DataKey::Escrow(proposal_id); + let mut record = Self::get_or_panic(&env, &key); + + record.admin.require_auth(); + + let now = env.ledger().timestamp(); + let inactive_for = now.saturating_sub(record.last_activity); + let inactivity_window = Self::inactivity_window(&env); + if inactive_for < inactivity_window { + panic_with_error!(&env, Error::InactivityNotReached); + } + + let unspent = record.total_amount - record.released_amount; + if unspent <= 0 { + panic_with_error!(&env, Error::NothingToReclaim); + } + + xlm::token_client(&env).transfer( + &env.current_contract_address(), + &record.treasury, + &unspent, + ); + + record.released_amount = record.total_amount; + record.last_activity = now; + env.storage().persistent().set(&key, &record); + + EscrowReclaimed { + proposal_id, + scholar: record.scholar.clone(), + amount_reclaimed: unspent, + } + .publish(&env); + } + + pub fn get_escrow(env: Env, proposal_id: u32) -> Option { + let key = DataKey::Escrow(proposal_id); + env.storage().persistent().get(&key) + } + + fn get_or_panic(env: &Env, key: &DataKey) -> EscrowRecord { + if let Some(record) = env.storage().persistent().get::<_, EscrowRecord>(key) { + record + } else { + panic_with_error!(env, Error::EscrowNotFound); + } + } + + fn next_tranche_amount(env: &Env, record: &EscrowRecord) -> i128 { + let remaining = record.total_amount - record.released_amount; + let is_last = record.tranches_released + 1 == record.total_tranches; + let amount = if is_last { + remaining + } else { + record.total_amount / (record.total_tranches as i128) + }; + + if amount <= 0 || record.released_amount + amount > record.total_amount { + panic_with_error!(env, Error::Overpayment); + } + amount + } + + fn admin(env: &Env) -> Address { + if let Some(admin) = env.storage().instance().get::<_, Address>(&ADMIN_KEY) { + admin + } else { + panic_with_error!(env, Error::NotInitialized); + } + } + + fn treasury(env: &Env) -> Address { + if let Some(treasury) = env.storage().instance().get::<_, Address>(&TREASURY_KEY) { + treasury + } else { + panic_with_error!(env, Error::NotInitialized); + } + } + + fn inactivity_window(env: &Env) -> u64 { + if let Some(window) = env + .storage() + .instance() + .get::<_, u64>(&INACTIVITY_WINDOW_KEY) + { + window + } else { + panic_with_error!(env, Error::NotInitialized); + } + } + + pub fn get_version(env: Env) -> String { + String::from_str(&env, "1.0.0") + } + + /// Replace the current contract WASM with a new uploaded hash. Admin only. + pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { + let admin = Self::admin(&env); + admin.require_auth(); + upgrade::apply(&env, &admin, &new_wasm_hash); + } +} + +mod xlm { + #[cfg(test)] + mod test_xlm { + use soroban_sdk::{Address, Env, Symbol, symbol_short}; + + const XLM_KEY: Symbol = symbol_short!("XLM"); + + pub fn contract_id(env: &Env) -> Address { + env.storage() + .instance() + .get::<_, Address>(&XLM_KEY) + .expect("XLM contract not initialized") + } + + pub fn register(env: &Env, admin: &Address) { + let sac = env.register_stellar_asset_contract_v2(admin.clone()); + env.storage().instance().set(&XLM_KEY, &sac.address()); + } + + pub fn token_client<'a>(env: &Env) -> soroban_sdk::token::TokenClient<'a> { + soroban_sdk::token::TokenClient::new(env, &contract_id(env)) + } + } + + #[cfg(not(test))] + mod live_xlm { + use soroban_sdk::Env; + + stellar_registry::import_asset!("xlm"); + + pub fn token_client<'a>(env: &Env) -> soroban_sdk::token::TokenClient<'a> { + xlm::token_client(env) + } + } + + #[cfg(not(test))] + pub use live_xlm::*; + + #[cfg(test)] + pub use test_xlm::*; +} + +#[cfg(test)] +mod test; diff --git a/contracts/milestone_escrow/src/test.rs b/contracts/milestone_escrow/src/test.rs new file mode 100644 index 00000000..ddbae35e --- /dev/null +++ b/contracts/milestone_escrow/src/test.rs @@ -0,0 +1,613 @@ +extern crate std; + +use soroban_sdk::{ + Address, Env, IntoVal, Symbol, Val, Vec, + testutils::{Address as _, Events as _, Ledger, LedgerInfo, MockAuth, MockAuthInvoke}, + token::{StellarAssetClient, TokenClient}, +}; + +use crate::{DataKey, EscrowRecord}; +use crate::{Error, MilestoneEscrow, MilestoneEscrowClient, xlm}; + +const START_TS: u64 = 1_700_000_000; +const THIRTY_DAYS: u64 = 30 * 24 * 60 * 60; + +fn set_timestamp(env: &Env, timestamp: u64) { + env.ledger().set(LedgerInfo { + timestamp, + protocol_version: 23, + sequence_number: 1, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 16, + min_persistent_entry_ttl: 16, + max_entry_ttl: 6312000, + }); +} + +fn token_address(env: &Env, contract_id: &Address) -> Address { + env.as_contract(contract_id, || xlm::contract_id(env)) +} + +fn token_client<'a>(env: &Env, token: &Address) -> TokenClient<'a> { + TokenClient::new(env, token) +} + +fn stellar_asset_client<'a>(env: &Env, token: &Address) -> StellarAssetClient<'a> { + StellarAssetClient::new(env, token) +} + +fn setup() -> (Env, Address, Address, Address, Address, Address) { + setup_with_inactivity_window(THIRTY_DAYS) +} + +fn setup_with_inactivity_window( + inactivity_window_seconds: u64, +) -> (Env, Address, Address, Address, Address, Address) { + let env = Env::default(); + set_timestamp(&env, START_TS); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + let scholar = Address::generate(&env); + + let contract_id = env.register(MilestoneEscrow, ()); + env.mock_all_auths(); + env.as_contract(&contract_id, || xlm::register(&env, &admin)); + let token = token_address(&env, &contract_id); + stellar_asset_client(&env, &token).mint(&treasury, &1_000); + + let client = MilestoneEscrowClient::new(&env, &contract_id); + client.initialize(&admin, &treasury, &inactivity_window_seconds); + + (env, contract_id, token, admin, treasury, scholar) +} + +fn set_caller(client: &MilestoneEscrowClient<'_>, fn_name: &str, caller: &Address, args: T) +where + T: IntoVal>, +{ + client.env.set_auths(&[]); + let invoke = &MockAuthInvoke { + contract: &client.address, + fn_name, + args: args.into_val(&client.env), + sub_invokes: &[], + }; + client.env.mock_auths(&[MockAuth { + address: caller, + invoke, + }]); +} + +fn create_escrow( + client: &MilestoneEscrowClient<'_>, + proposal_id: u32, + scholar: &Address, + amount: i128, + tranches: u32, +) { + client.env.mock_all_auths(); + client.create_escrow(&proposal_id, scholar, &amount, &tranches); +} + +fn release_tranche_authorized( + client: &MilestoneEscrowClient<'_>, + proposal_id: u32, +) -> Result<(), Result> { + if let Some(escrow) = client.get_escrow(&proposal_id) { + set_caller(client, "release_tranche", &escrow.admin, (proposal_id,)); + } + client.try_release_tranche(&proposal_id).map(|_| ()) +} + +fn reclaim_inactive_authorized( + client: &MilestoneEscrowClient<'_>, + proposal_id: u32, +) -> Result<(), Result> { + if let Some(escrow) = client.get_escrow(&proposal_id) { + set_caller(client, "reclaim_inactive", &escrow.admin, (proposal_id,)); + } + client.try_reclaim_inactive(&proposal_id).map(|_| ()) +} + +#[test] +fn initialize_sets_admin_and_treasury_on_created_escrow() { + let (env, contract_id, token, admin, treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + let before_events = env.events().all().len(); + create_escrow(&client, 1, &scholar, 120, 3); + let after_events = env.events().all().len(); + // token transfer + EscrowCreated + assert_eq!(after_events, before_events + 2); + + let escrow = client.get_escrow(&1).unwrap(); + assert_eq!(escrow.admin, admin); + assert_eq!(escrow.treasury, treasury); + assert_eq!(escrow.total_amount, 120); + assert_eq!(escrow.released_amount, 0); + assert_eq!(token_client(&env, &token).balance(&contract_id), 120); +} + +#[test] +fn create_escrow_emits_event() { + let (env, contract_id, _token, _admin, _treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + create_escrow(&client, 123, &scholar, 200, 4); + + let events = env.events().all(); + let found = events.iter().any(|(cid, topics, _)| { + cid == contract_id && topics.contains(&Symbol::new(&env, "escrow_created").into_val(&env)) + }); + assert!(found, "escrow_created event not found"); +} + +#[test] +fn create_escrow_locks_funds_and_rejects_duplicates() { + let (env, contract_id, token, _admin, treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + let before = env.events().all().len(); + create_escrow(&client, 7, &scholar, 100, 4); + let after = env.events().all().len(); + // token transfer + EscrowCreated + assert_eq!(after, before + 2); + + let escrow = client.get_escrow(&7).unwrap(); + assert_eq!(escrow.scholar, scholar); + assert_eq!(escrow.total_amount, 100); + assert_eq!(escrow.total_tranches, 4); + assert_eq!(token_client(&env, &token).balance(&treasury), 900); + assert_eq!(token_client(&env, &token).balance(&contract_id), 100); + + client.env.mock_all_auths(); + let duplicate = client.try_create_escrow(&7, &escrow.scholar, &100, &4); + assert_eq!( + duplicate.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::EscrowExists as u32 + ))) + ); +} + +#[test] +fn release_tranche_is_admin_only_and_stops_after_all_tranches() { + let (env, contract_id, token, _admin, _treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + let attacker = Address::generate(&env); + + create_escrow(&client, 9, &scholar, 100, 3); + + set_caller(&client, "release_tranche", &attacker, (9_u32,)); + let unauthorized = client.try_release_tranche(&9).map(|_| ()); + assert!(unauthorized.is_err()); + + release_tranche_authorized(&client, 9).unwrap(); + let first = client.get_escrow(&9).unwrap(); + assert_eq!(first.released_amount, 33); + assert_eq!(first.tranches_released, 1); + assert_eq!(token_client(&env, &token).balance(&scholar), 33); + + release_tranche_authorized(&client, 9).unwrap(); + let second = client.get_escrow(&9).unwrap(); + assert_eq!(second.released_amount, 66); + assert_eq!(second.tranches_released, 2); + + release_tranche_authorized(&client, 9).unwrap(); + let final_record = client.get_escrow(&9).unwrap(); + assert_eq!(final_record.released_amount, 100); + assert_eq!(final_record.tranches_released, 3); + assert_eq!(token_client(&env, &token).balance(&scholar), 100); + assert_eq!(token_client(&env, &token).balance(&contract_id), 0); + + let over_release = release_tranche_authorized(&client, 9); + assert_eq!( + over_release.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::AllTranchesReleased as u32 + ))) + ); +} + +#[test] +fn reclaim_inactive_requires_admin_and_deadline() { + let (env, contract_id, token, _admin, treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + let attacker = Address::generate(&env); + + create_escrow(&client, 11, &scholar, 120, 4); + release_tranche_authorized(&client, 11).unwrap(); + + set_caller(&client, "reclaim_inactive", &attacker, (11_u32,)); + let unauthorized = client.try_reclaim_inactive(&11).map(|_| ()); + assert!(unauthorized.is_err()); + + set_timestamp(&env, START_TS + THIRTY_DAYS - 1); + let early = reclaim_inactive_authorized(&client, 11); + assert_eq!( + early.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InactivityNotReached as u32 + ))) + ); + + set_timestamp(&env, START_TS + THIRTY_DAYS); + let before_reclaim = env.events().all().len(); + reclaim_inactive_authorized(&client, 11).unwrap(); + let after_reclaim = env.events().all().len(); + // token transfer back to treasury + EscrowReclaimed + assert_eq!(after_reclaim, before_reclaim + 2); + + let escrow = client.get_escrow(&11).unwrap(); + assert_eq!(escrow.released_amount, 120); + assert_eq!(token_client(&env, &token).balance(&treasury), 970); + assert_eq!(token_client(&env, &token).balance(&contract_id), 0); +} + +#[test] +fn reclaim_inactive_uses_configured_window_size() { + let (env, contract_id, token, _admin, treasury, scholar) = setup_with_inactivity_window(1); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + create_escrow(&client, 12, &scholar, 100, 4); + release_tranche_authorized(&client, 12).unwrap(); + + set_timestamp(&env, START_TS); + let early = reclaim_inactive_authorized(&client, 12); + assert_eq!( + early.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InactivityNotReached as u32 + ))) + ); + + set_timestamp(&env, START_TS + 1); + reclaim_inactive_authorized(&client, 12).unwrap(); + assert_eq!(token_client(&env, &token).balance(&treasury), 975); +} + +#[test] +fn reclaim_inactive_emits_event() { + let (env, contract_id, _token, _admin, _treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + create_escrow(&client, 77, &scholar, 120, 4); + // Make it active so last_activity is set from a release + release_tranche_authorized(&client, 77).unwrap(); + + set_timestamp(&env, START_TS + THIRTY_DAYS); + reclaim_inactive_authorized(&client, 77).unwrap(); + + let events = env.events().all(); + let found = events.iter().any(|(cid, topics, _)| { + cid == contract_id && topics.contains(&Symbol::new(&env, "escrow_reclaimed").into_val(&env)) + }); + assert!(found, "escrow_reclaimed event not found"); +} + +#[test] +fn get_escrow_reflects_each_stage_of_the_full_flow() { + let (env, contract_id, _token, _admin, _treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + create_escrow(&client, 15, &scholar, 90, 2); + let created = client.get_escrow(&15).unwrap(); + assert_eq!(created.released_amount, 0); + assert_eq!(created.tranches_released, 0); + + release_tranche_authorized(&client, 15).unwrap(); + let partial = client.get_escrow(&15).unwrap(); + assert_eq!(partial.released_amount, 45); + assert_eq!(partial.tranches_released, 1); + + release_tranche_authorized(&client, 15).unwrap(); + let closed = client.get_escrow(&15).unwrap(); + assert_eq!(closed.released_amount, 90); + assert_eq!(closed.tranches_released, 2); + assert_eq!(closed.total_amount, 90); +} + +#[test] +fn zero_amount_create_is_rejected() { + let (env, contract_id, _token, _admin, _treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + client.env.mock_all_auths(); + let result = client.try_create_escrow(&20, &scholar, &0, &2); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InvalidAmount as u32 + ))) + ); +} + +#[test] +fn zero_tranches_create_is_rejected() { + let (env, contract_id, _token, _admin, _treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + client.env.mock_all_auths(); + let result = client.try_create_escrow(&22, &scholar, &100, &0); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InvalidTranches as u32 + ))) + ); +} + +#[test] +fn overpayment_is_rejected() { + let (env, contract_id, _token, _admin, _treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + create_escrow(&client, 21, &scholar, 2, 4); + let first_release = release_tranche_authorized(&client, 21); + + assert_eq!( + first_release.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::Overpayment as u32 + ))) + ); +} + +// --- fuzz tests --- + +use proptest::prelude::*; + +proptest! { + #[test] + #[ignore] + fn fuzz_ledger_timestamps_30_day_timeout( + last_active_offset in 0..10_000_000_u64, + check_time_offset in 0..10_000_000_u64 + ) { + let (env, contract_id, _, _, _, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + let start_time = START_TS + last_active_offset; + set_timestamp(&env, start_time); + + create_escrow(&client, 99, &scholar, 1000, 2); + + // Advance time + let current_time = start_time + check_time_offset; + set_timestamp(&env, current_time); + + let result = reclaim_inactive_authorized(&client, 99); + + if check_time_offset >= THIRTY_DAYS { + assert!(result.is_ok()); + } else { + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + crate::Error::InactivityNotReached as u32 + ))) + ); + } + } + + #[test] + #[ignore] + fn fuzz_tranche_disbursement_amounts( + amount in 1..100_000_000_i128, + tranches in 1..100_u32 + ) { + let (env, contract_id, token, _, treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + if amount / (tranches as i128) == 0 { + return Ok(()); + } + + // setup() only mints 1_000; top up so the treasury can fund this escrow + env.mock_all_auths(); + stellar_asset_client(&env, &token).mint(&treasury, &amount); + + create_escrow(&client, 100, &scholar, amount, tranches); + + for _ in 0..tranches { + release_tranche_authorized(&client, 100).unwrap(); + } + + let escrow = client.get_escrow(&100).unwrap(); + assert!(escrow.released_amount <= escrow.total_amount); + + // Try one more, should fail with AllTranchesReleased + let over_release = release_tranche_authorized(&client, 100); + assert_eq!( + over_release.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + crate::Error::AllTranchesReleased as u32 + ))) + ); + } +} + +#[test] +fn reclaim_inactive_when_fully_released_is_rejected() { + let (env, contract_id, _token, _admin, _treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + create_escrow(&client, 23, &scholar, 100, 4); + release_tranche_authorized(&client, 23).unwrap(); + release_tranche_authorized(&client, 23).unwrap(); + release_tranche_authorized(&client, 23).unwrap(); + release_tranche_authorized(&client, 23).unwrap(); + + set_timestamp(&env, START_TS + THIRTY_DAYS); + let result = reclaim_inactive_authorized(&client, 23); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::NothingToReclaim as u32 + ))) + ); +} + +#[test] +fn equal_split_releases_25_each_for_100_over_4_tranches() { + let (env, contract_id, token, _admin, _treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + create_escrow(&client, 24, &scholar, 100, 4); + + release_tranche_authorized(&client, 24).unwrap(); + assert_eq!(token_client(&env, &token).balance(&scholar), 25); + + release_tranche_authorized(&client, 24).unwrap(); + assert_eq!(token_client(&env, &token).balance(&scholar), 50); + + release_tranche_authorized(&client, 24).unwrap(); + assert_eq!(token_client(&env, &token).balance(&scholar), 75); + + release_tranche_authorized(&client, 24).unwrap(); + assert_eq!(token_client(&env, &token).balance(&scholar), 100); + + let escrow = client.get_escrow(&24).unwrap(); + assert_eq!(escrow.released_amount, 100); + assert_eq!(escrow.tranches_released, 4); + assert_eq!(token_client(&env, &token).balance(&contract_id), 0); +} + +#[test] +fn last_tranche_rounding_releases_33_33_34_for_100_over_3_tranches() { + let (env, contract_id, token, _admin, _treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + create_escrow(&client, 25, &scholar, 100, 3); + + release_tranche_authorized(&client, 25).unwrap(); + assert_eq!(token_client(&env, &token).balance(&scholar), 33); + + release_tranche_authorized(&client, 25).unwrap(); + assert_eq!(token_client(&env, &token).balance(&scholar), 66); + + release_tranche_authorized(&client, 25).unwrap(); + assert_eq!(token_client(&env, &token).balance(&scholar), 100); + + let escrow = client.get_escrow(&25).unwrap(); + assert_eq!(escrow.released_amount, 100); + assert_eq!(escrow.tranches_released, 3); + assert_eq!(token_client(&env, &token).balance(&contract_id), 0); +} + +#[cfg(test)] +mod fuzz_tests { + use super::*; + use proptest::prelude::*; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + + #[test] + #[ignore] + fn fuzz_ledger_timestamps(elapsed in 0..u64::MAX) { + let (env, contract_id, _token, _admin, _treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + create_escrow(&client, 99, &scholar, 100, 2); + // Must release at least one tranche to be active and claimable + release_tranche_authorized(&client, 99).unwrap(); + + // Advance time, avoid overflowing u64 + let next_ts = START_TS.saturating_add(elapsed); + set_timestamp(&env, next_ts); + + let res = reclaim_inactive_authorized(&client, 99); + + if elapsed >= THIRTY_DAYS { + assert!(res.is_ok()); + } else { + assert_eq!( + res.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InactivityNotReached as u32 + ))) + ); + } + } + + #[test] + #[ignore] + fn fuzz_tranche_disbursement_amounts(amount in 1..1_000_000_000_i128, tranches in 1..1000_u32) { + let (env, contract_id, token, _admin, treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + + // Overpayment check constraint: amounts must be enough for tranches + if amount < tranches as i128 { + return Ok(()); + } + + // Ensure the treasury has enough balance for the randomized escrow amount. + // setup() only mints 1_000 tokens by default. + env.mock_all_auths(); + stellar_asset_client(&env, &token).mint(&treasury, &amount); + + create_escrow(&client, 100, &scholar, amount, tranches); + + let mut released = 0_i128; + for _ in 0..tranches { + assert!(release_tranche_authorized(&client, 100).is_ok()); + let escrow = client.get_escrow(&100).unwrap(); + assert!(escrow.released_amount <= amount); + assert!(escrow.released_amount > released); + released = escrow.released_amount; + } + + // Releasing an extra one fails + assert_eq!( + release_tranche_authorized(&client, 100).err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::AllTranchesReleased as u32 + ))) + ); + + let final_escrow = client.get_escrow(&100).unwrap(); + assert_eq!(final_escrow.released_amount, amount); + } + } +} + +#[test] +fn upgrade_requires_admin_auth() { + let (env, contract_id, _token, _admin, _treasury, _scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + let attacker = Address::generate(&env); + let wasm_hash = crate::upgrade::testutils::upload_upgrade_target(&env); + + set_caller(&client, "upgrade", &attacker, (wasm_hash.clone(),)); + assert!(client.try_upgrade(&wasm_hash).is_err()); +} + +#[test] +fn state_persists_after_upgrade() { + let (env, contract_id, _token, admin, _treasury, scholar) = setup(); + let client = MilestoneEscrowClient::new(&env, &contract_id); + create_escrow(&client, 404, &scholar, 120, 3); + + let wasm_hash = crate::upgrade::testutils::upload_upgrade_target(&env); + set_caller(&client, "upgrade", &admin, (wasm_hash.clone(),)); + client.upgrade(&wasm_hash); + + let escrow = env.as_contract(&contract_id, || { + env.storage() + .persistent() + .get::<_, EscrowRecord>(&DataKey::Escrow(404)) + }); + let stored_hash = env.as_contract(&contract_id, || crate::upgrade::current_hash(&env)); + + let escrow = escrow.expect("escrow should remain after upgrade"); + assert_eq!(escrow.scholar, scholar); + assert_eq!(escrow.total_amount, 120); + assert_eq!(escrow.total_tranches, 3); + assert_eq!(stored_hash, wasm_hash); +} diff --git a/contracts/nft-enumerable/src/contract.rs b/contracts/nft-enumerable/src/contract.rs deleted file mode 100644 index 94b0a6e8..00000000 --- a/contracts/nft-enumerable/src/contract.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Non-Fungible Enumerable Example Contract. -//! -//! Demonstrates an example usage of the Enumerable extension, allowing for -//! enumeration of all the token IDs in the contract as well as all the token -//! IDs owned by each account. - -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String}; -use stellar_tokens::non_fungible::{ - burnable::NonFungibleBurnable, - enumerable::{Enumerable, NonFungibleEnumerable}, - Base, NonFungibleToken, -}; - -#[contracttype] -pub enum DataKey { - Owner, -} - -#[contract] -pub struct ExampleContract; - -#[contractimpl] -impl ExampleContract { - pub fn __constructor(e: &Env, uri: String, name: String, symbol: String, owner: Address) { - e.storage().instance().set(&DataKey::Owner, &owner); - Base::set_metadata(e, uri, name, symbol); - } - - pub fn mint(e: &Env, to: Address) -> u32 { - let owner: Address = - e.storage().instance().get(&DataKey::Owner).expect("owner should be set"); - owner.require_auth(); - Enumerable::sequential_mint(e, &to) - } -} - -#[contractimpl(contracttrait)] -impl NonFungibleToken for ExampleContract { - type ContractType = Enumerable; -} - -#[contractimpl(contracttrait)] -impl NonFungibleEnumerable for ExampleContract {} - -#[contractimpl(contracttrait)] -impl NonFungibleBurnable for ExampleContract {} diff --git a/contracts/nft-enumerable/src/lib.rs b/contracts/nft-enumerable/src/lib.rs deleted file mode 100644 index f1ec4a1f..00000000 --- a/contracts/nft-enumerable/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![no_std] -#![allow(dead_code)] - -mod contract; -#[cfg(test)] -mod test; diff --git a/contracts/nft-enumerable/src/test.rs b/contracts/nft-enumerable/src/test.rs deleted file mode 100644 index afe9298f..00000000 --- a/contracts/nft-enumerable/src/test.rs +++ /dev/null @@ -1,80 +0,0 @@ -extern crate std; - -use soroban_sdk::{testutils::Address as _, Address, Env, String}; - -use crate::contract::{ExampleContract, ExampleContractClient}; - -fn create_client<'a>(e: &Env, owner: &Address) -> ExampleContractClient<'a> { - let uri = String::from_str(e, "www.mytoken.com"); - let name = String::from_str(e, "My Token"); - let symbol = String::from_str(e, "TKN"); - let address = e.register(ExampleContract, (uri, name, symbol, owner)); - ExampleContractClient::new(e, &address) -} - -#[test] -fn enumerable_transfer_override_works() { - let e = Env::default(); - - let owner = Address::generate(&e); - - let recipient = Address::generate(&e); - - let client = create_client(&e, &owner); - - e.mock_all_auths(); - client.mint(&owner); - client.transfer(&owner, &recipient, &0); - assert_eq!(client.balance(&owner), 0); - assert_eq!(client.balance(&recipient), 1); - assert_eq!(client.get_owner_token_id(&recipient, &0), 0); -} - -#[test] -fn enumerable_transfer_from_override_works() { - let e = Env::default(); - - let owner = Address::generate(&e); - let spender = Address::generate(&e); - let recipient = Address::generate(&e); - - let client = create_client(&e, &owner); - - e.mock_all_auths(); - client.mint(&owner); - client.approve(&owner, &spender, &0, &1000); - client.transfer_from(&spender, &owner, &recipient, &0); - assert_eq!(client.balance(&owner), 0); - assert_eq!(client.balance(&recipient), 1); - assert_eq!(client.get_owner_token_id(&recipient, &0), 0); -} - -#[test] -fn enumerable_burn_override_works() { - let e = Env::default(); - let owner = Address::generate(&e); - let client = create_client(&e, &owner); - e.mock_all_auths(); - client.mint(&owner); - client.burn(&owner, &0); - assert_eq!(client.balance(&owner), 0); - client.mint(&owner); - assert_eq!(client.balance(&owner), 1); - assert_eq!(client.get_owner_token_id(&owner, &0), 1); -} - -#[test] -fn enumerable_burn_from_override_works() { - let e = Env::default(); - let owner = Address::generate(&e); - let spender = Address::generate(&e); - let client = create_client(&e, &owner); - e.mock_all_auths(); - client.mint(&owner); - client.approve(&owner, &spender, &0, &1000); - client.burn_from(&spender, &owner, &0); - assert_eq!(client.balance(&owner), 0); - client.mint(&owner); - assert_eq!(client.balance(&owner), 1); - assert_eq!(client.get_owner_token_id(&owner, &0), 1); -} diff --git a/contracts/nft-enumerable/Cargo.toml b/contracts/scholar_nft/Cargo.toml similarity index 70% rename from contracts/nft-enumerable/Cargo.toml rename to contracts/scholar_nft/Cargo.toml index 8394aecc..ad530de0 100644 --- a/contracts/nft-enumerable/Cargo.toml +++ b/contracts/scholar_nft/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "nft-enumerable-example" +name = "scholar-nft" edition.workspace = true license.workspace = true repository.workspace = true @@ -11,9 +11,9 @@ crate-type = ["cdylib"] doctest = false [dependencies] +learnvault-shared = { workspace = true } soroban-sdk = { workspace = true } -stellar-tokens = { workspace = true } -stellar-macros = { workspace = true } [dev-dependencies] +learnvault-shared = { workspace = true, features = ["testutils"] } soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/scholar_nft/src/lib.rs b/contracts/scholar_nft/src/lib.rs new file mode 100644 index 00000000..976849eb --- /dev/null +++ b/contracts/scholar_nft/src/lib.rs @@ -0,0 +1,363 @@ +#![no_std] +#![allow(deprecated)] + +use soroban_sdk::{ + Address, BytesN, Env, String, Symbol, Vec, contract, contracterror, contractimpl, contracttype, + panic_with_error, symbol_short, +}; + +use learnvault_shared::upgrade; + +pub use upgrade::ContractUpgraded; + +const DAY_IN_LEDGERS: u32 = 17_280; +const INSTANCE_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; +const TTL_MIN: u32 = DAY_IN_LEDGERS; +const TTL_MAX: u32 = DAY_IN_LEDGERS * 365; + +const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); +const TOKEN_COUNTER_KEY: Symbol = symbol_short!("TCOUNTER"); + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct ScholarMetadata { + pub owner: Address, + pub metadata_uri: String, + pub issued_at: u64, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub enum DataKey { + Admin, + Counter, + Scholars, + Owner(u64), + TokenUri(u64), + Revoked(u64), + Metadata(u64), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct MintEventData { + pub token_id: u64, + pub owner: Address, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct TransferAttemptEventData { + pub from: Address, + pub to: Address, + pub token_id: u64, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct InitializedEventData { + pub admin: Address, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct RevokedEventData { + pub token_id: u64, + pub reason: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct AdminChangedEventData { + pub old_admin: Address, + pub new_admin: Address, +} + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum ScholarNFTError { + AlreadyInitialized = 1, + NotInitialized = 2, + Unauthorized = 3, + TokenNotFound = 4, + TokenRevoked = 5, + TokenExists = 6, + Soulbound = 7, + AlreadyRevoked = 8, +} + +#[contract] +pub struct ScholarNFT; + +#[contractimpl] +impl ScholarNFT { + pub fn initialize(env: Env, admin: Address) { + if env.storage().instance().has(&ADMIN_KEY) { + panic_with_error!(&env, ScholarNFTError::AlreadyInitialized); + } + admin.require_auth(); + env.storage().instance().set(&ADMIN_KEY, &admin); + upgrade::init(&env); + env.storage().instance().set(&TOKEN_COUNTER_KEY, &0_u64); + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::Counter, &0_u64); + let scholars: Vec
= Vec::new(&env); + env.storage() + .persistent() + .set(&DataKey::Scholars, &scholars); + Self::extend_persistent(&env, &DataKey::Scholars); + + env.events() + .publish((symbol_short!("init"),), InitializedEventData { admin }); + + Self::extend_instance(&env); + } + + pub fn mint(env: Env, to: Address, metadata_uri: String) -> u64 { + let admin = Self::get_admin(&env); + admin.require_auth(); + + let token_id = Self::next_token_id(&env); + let owner_key = DataKey::Owner(token_id); + if env.storage().persistent().has(&owner_key) { + panic_with_error!(&env, ScholarNFTError::TokenExists); + } + + env.storage().persistent().set(&owner_key, &to); + Self::extend_persistent(&env, &owner_key); + + env.storage() + .persistent() + .set(&DataKey::TokenUri(token_id), &metadata_uri); + Self::extend_persistent(&env, &DataKey::TokenUri(token_id)); + + let metadata = ScholarMetadata { + owner: to.clone(), + metadata_uri: metadata_uri.clone(), + issued_at: env.ledger().timestamp(), + }; + env.storage() + .persistent() + .set(&DataKey::Metadata(token_id), &metadata); + Self::extend_persistent(&env, &DataKey::Metadata(token_id)); + + let mut scholars: Vec
= env + .storage() + .persistent() + .get(&DataKey::Scholars) + .unwrap_or_else(|| Vec::new(&env)); + scholars.push_back(to.clone()); + env.storage() + .persistent() + .set(&DataKey::Scholars, &scholars); + Self::extend_persistent(&env, &DataKey::Scholars); + + env.events().publish( + (symbol_short!("minted"), token_id), + MintEventData { + token_id, + owner: to, + }, + ); + + token_id + } + + pub fn revoke(env: Env, token_id: u64, reason: String) { + let admin = Self::get_admin(&env); + admin.require_auth(); + + let owner_key = DataKey::Owner(token_id); + if !env.storage().persistent().has(&owner_key) { + panic_with_error!(&env, ScholarNFTError::TokenNotFound); + } + + let revoked_key = DataKey::Revoked(token_id); + if env.storage().persistent().has(&revoked_key) { + panic_with_error!(&env, ScholarNFTError::AlreadyRevoked); + } + + env.storage().persistent().set(&revoked_key, &reason); + Self::extend_persistent(&env, &revoked_key); + + env.events().publish( + (symbol_short!("revoked"), token_id), + RevokedEventData { token_id, reason }, + ); + } + + pub fn transfer_admin(env: Env, new_admin: Address) { + let old_admin = Self::get_admin(&env); + old_admin.require_auth(); + + env.storage().instance().set(&ADMIN_KEY, &new_admin); + env.storage().instance().set(&DataKey::Admin, &new_admin); + + env.events().publish( + (symbol_short!("adm_chng"),), + AdminChangedEventData { + old_admin, + new_admin, + }, + ); + + Self::extend_instance(&env); + } + + pub fn token_uri(env: Env, token_id: u64) -> String { + Self::extend_instance(&env); + let key = DataKey::TokenUri(token_id); + if let Some(uri) = env.storage().persistent().get::<_, String>(&key) { + Self::extend_persistent(&env, &key); + uri + } else { + panic_with_error!(&env, ScholarNFTError::TokenNotFound); + } + } + + pub fn get_metadata_uri(env: Env, token_id: u64) -> String { + Self::extend_instance(&env); + let key = DataKey::TokenUri(token_id); + if let Some(uri) = env.storage().persistent().get::<_, String>(&key) { + Self::extend_persistent(&env, &key); + uri + } else { + panic_with_error!(&env, ScholarNFTError::TokenNotFound); + } + } + + pub fn get_metadata(env: Env, token_id: u64) -> ScholarMetadata { + Self::extend_instance(&env); + let key = DataKey::Metadata(token_id); + if let Some(metadata) = env.storage().persistent().get::<_, ScholarMetadata>(&key) { + Self::extend_persistent(&env, &key); + metadata + } else { + panic_with_error!(&env, ScholarNFTError::TokenNotFound); + } + } + + pub fn token_counter(env: Env) -> u64 { + Self::extend_instance(&env); + env.storage() + .instance() + .get(&TOKEN_COUNTER_KEY) + .unwrap_or(0_u64) + } + + pub fn get_all_scholars(env: Env) -> Vec
{ + Self::extend_instance(&env); + let key = DataKey::Scholars; + let scholars: Vec
= env + .storage() + .persistent() + .get(&key) + .unwrap_or_else(|| Vec::new(&env)); + if env.storage().persistent().has(&key) { + Self::extend_persistent(&env, &key); + } + scholars + } + + /// Replace the current contract WASM with a new uploaded hash. Admin only. + pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { + let admin = Self::get_admin(&env); + admin.require_auth(); + upgrade::apply(&env, &admin, &new_wasm_hash); + } + + pub fn transfer(env: Env, from: Address, to: Address, token_id: u64) { + env.events().publish( + (symbol_short!("xfer_att"),), + TransferAttemptEventData { from, to, token_id }, + ); + panic_with_error!(&env, ScholarNFTError::Soulbound); + } + + pub fn owner_of(env: Env, token_id: u64) -> Address { + Self::extend_instance(&env); + let revoked_key = DataKey::Revoked(token_id); + if env.storage().persistent().has(&revoked_key) { + Self::extend_persistent(&env, &revoked_key); + panic_with_error!(&env, ScholarNFTError::TokenRevoked); + } + + let key = DataKey::Owner(token_id); + if let Some(owner) = env.storage().persistent().get::<_, Address>(&key) { + Self::extend_persistent(&env, &key); + owner + } else { + panic_with_error!(&env, ScholarNFTError::TokenNotFound); + } + } + + pub fn has_credential(env: Env, token_id: u64) -> bool { + Self::extend_instance(&env); + let revoked_key = DataKey::Revoked(token_id); + if env.storage().persistent().has(&revoked_key) { + Self::extend_persistent(&env, &revoked_key); + return false; + } + let owner_key = DataKey::Owner(token_id); + let exists = env.storage().persistent().has(&owner_key); + if exists { + Self::extend_persistent(&env, &owner_key); + } + exists + } + + pub fn is_revoked(env: Env, token_id: u64) -> bool { + Self::extend_instance(&env); + let key = DataKey::Revoked(token_id); + let revoked = env.storage().persistent().has(&key); + if revoked { + Self::extend_persistent(&env, &key); + } + revoked + } + + pub fn get_revocation_reason(env: Env, token_id: u64) -> Option { + Self::extend_instance(&env); + let key = DataKey::Revoked(token_id); + if let Some(reason) = env.storage().persistent().get::<_, String>(&key) { + Self::extend_persistent(&env, &key); + Some(reason) + } else { + None + } + } + + fn next_token_id(env: &Env) -> u64 { + let mut counter = env + .storage() + .instance() + .get(&TOKEN_COUNTER_KEY) + .unwrap_or(0_u64); + counter = counter.saturating_add(1); + env.storage().instance().set(&TOKEN_COUNTER_KEY, &counter); + counter + } + + fn get_admin(env: &Env) -> Address { + Self::extend_instance(env); + env.storage() + .instance() + .get::<_, Address>(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(env, ScholarNFTError::NotInitialized)) + } + + fn extend_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_BUMP_THRESHOLD, INSTANCE_EXTEND_TO); + } + + fn extend_persistent(env: &Env, key: &DataKey) { + env.storage().persistent().extend_ttl(key, TTL_MIN, TTL_MAX); + } +} + +#[cfg(test)] +mod test; diff --git a/contracts/scholar_nft/src/test.rs b/contracts/scholar_nft/src/test.rs new file mode 100644 index 00000000..a9442823 --- /dev/null +++ b/contracts/scholar_nft/src/test.rs @@ -0,0 +1,517 @@ +#![cfg(test)] + +use crate::{ + AdminChangedEventData, DataKey, InitializedEventData, MintEventData, ScholarMetadata, + ScholarNFT, ScholarNFTClient, ScholarNFTError, +}; +use soroban_sdk::{ + Address, BytesN, Env, IntoVal, String, symbol_short, + testutils::{Address as _, Events as _, MockAuth, MockAuthInvoke, storage::Persistent}, +}; + +fn setup(env: &Env) -> (Address, Address, ScholarNFTClient) { + let admin = Address::generate(env); + let contract_id = env.register(ScholarNFT, ()); + let client = ScholarNFTClient::new(env, &contract_id); + env.mock_all_auths(); + client.initialize(&admin); + (contract_id, admin, client) +} + +fn cid(env: &Env, value: &str) -> String { + String::from_str(env, value) +} + +fn authorize_upgrade(env: &Env, contract_id: &Address, signer: &Address, wasm_hash: &BytesN<32>) { + env.mock_auths(&[MockAuth { + address: signer, + invoke: &MockAuthInvoke { + contract: contract_id, + fn_name: "upgrade", + args: (wasm_hash.clone(),).into_val(env), + sub_invokes: &[], + }, + }]); +} + +#[test] +fn mint_returns_sequential_token_ids() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let scholar_a = Address::generate(&env); + let scholar_b = Address::generate(&env); + + env.mock_all_auths(); + assert_eq!(client.mint(&scholar_a, &cid(&env, "ipfs://cid-1")), 1); + assert_eq!(client.mint(&scholar_b, &cid(&env, "ipfs://cid-2")), 2); +} + +#[test] +fn owner_of_returns_minted_owner() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let scholar = Address::generate(&env); + + env.mock_all_auths(); + let token_id = client.mint(&scholar, &cid(&env, "ipfs://owner-check")); + + assert_eq!(client.owner_of(&token_id), scholar); +} + +#[test] +fn get_all_scholars_is_empty_before_mint() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + + assert_eq!(client.get_all_scholars().len(), 0); +} + +#[test] +fn get_all_scholars_returns_all_minted_scholars_in_order() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let scholar_a = Address::generate(&env); + let scholar_b = Address::generate(&env); + let scholar_c = Address::generate(&env); + + env.mock_all_auths(); + client.mint(&scholar_a, &cid(&env, "ipfs://scholar-a")); + client.mint(&scholar_b, &cid(&env, "ipfs://scholar-b")); + client.mint(&scholar_c, &cid(&env, "ipfs://scholar-c")); + + let scholars = client.get_all_scholars(); + assert_eq!(scholars.len(), 3); + assert_eq!(scholars.get(0).unwrap(), scholar_a); + assert_eq!(scholars.get(1).unwrap(), scholar_b); + assert_eq!(scholars.get(2).unwrap(), scholar_c); +} + +#[test] +fn test_transfer_admin_success_and_old_admin_cannot_mint() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(ScholarNFT, ()); + let client = ScholarNFTClient::new(&env, &contract_id); + + let old_admin = Address::generate(&env); + let new_admin = Address::generate(&env); + let recipient = Address::generate(&env); + + client.initialize(&old_admin); + client.transfer_admin(&new_admin); + + let uri = String::from_str(&env, "ipfs://new-token"); + let token_id = client.mint(&recipient, &uri); + assert_eq!(token_id, 1); + + let fetched_owner = client.owner_of(&token_id); + assert_eq!(fetched_owner, recipient); +} + +#[test] +fn test_old_admin_cannot_mint_after_transfer() { + let env = Env::default(); + + let contract_id = env.register(ScholarNFT, ()); + let client = ScholarNFTClient::new(&env, &contract_id); + + let old_admin = Address::generate(&env); + let new_admin = Address::generate(&env); + let recipient = Address::generate(&env); + + env.mock_auths(&[MockAuth { + address: &old_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "initialize", + args: (&old_admin,).into_val(&env), + sub_invokes: &[], + }, + }]); + client.initialize(&old_admin); + + env.mock_auths(&[MockAuth { + address: &old_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "transfer_admin", + args: (&new_admin,).into_val(&env), + sub_invokes: &[], + }, + }]); + client.transfer_admin(&new_admin); + + env.mock_auths(&[MockAuth { + address: &old_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "mint", + args: (&recipient, cid(&env, "ipfs://bad")).into_val(&env), + sub_invokes: &[], + }, + }]); + + let result = client.try_mint(&recipient, &cid(&env, "ipfs://bad")); + assert!(result.is_err()); +} + +#[test] +#[should_panic] +fn test_transfer_admin_rejected_for_non_admin() { + let env = Env::default(); + + let contract_id = env.register(ScholarNFT, ()); + let client = ScholarNFTClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + + env.mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "initialize", + args: (&admin,).into_val(&env), + sub_invokes: &[], + }, + }]); + client.initialize(&admin); + + env.mock_auths(&[MockAuth { + address: &attacker, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "transfer_admin", + args: (&attacker,).into_val(&env), + sub_invokes: &[], + }, + }]); + client.transfer_admin(&attacker); +} + +#[test] +fn token_uri_returns_metadata_uri() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let scholar = Address::generate(&env); + let metadata_uri = cid(&env, "ipfs://bafybeigdyrzt"); + + env.mock_all_auths(); + let token_id = client.mint(&scholar, &metadata_uri); + + assert_eq!(client.token_uri(&token_id), metadata_uri); +} + +#[test] +fn get_metadata_uri_round_trip() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let scholar = Address::generate(&env); + let uri = cid( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ); + + env.mock_all_auths(); + let token_id = client.mint(&scholar, &uri); + + assert_eq!(client.get_metadata_uri(&token_id), uri); +} + +#[test] +#[should_panic(expected = "Error(Auth, InvalidAction)")] +fn non_admin_mint_panics() { + let env = Env::default(); + let (contract_id, _admin, client) = setup(&env); + let hacker = Address::generate(&env); + let scholar = Address::generate(&env); + + env.mock_auths(&[MockAuth { + address: &hacker, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "mint", + args: (&scholar, cid(&env, "ipfs://hax")).into_val(&env), + sub_invokes: &[], + }, + }]); + + client.mint(&scholar, &cid(&env, "ipfs://hax")); +} + +#[test] +#[should_panic(expected = "Error(Contract, #1)")] +fn test_double_initialize_reverts() { + let env = Env::default(); + let (_, admin, client) = setup(&env); + env.mock_all_auths(); + client.initialize(&admin); +} + +#[test] +fn test_revoke_flow() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let recipient = Address::generate(&env); + let reason = String::from_str(&env, "Cheater"); + + env.mock_all_auths(); + let token_id = client.mint(&recipient, &cid(&env, "ipfs://test")); + assert!(client.has_credential(&token_id)); + + client.revoke(&token_id, &reason); + + assert!(!client.has_credential(&token_id)); + assert!(client.is_revoked(&token_id)); + assert_eq!(client.get_revocation_reason(&token_id), Some(reason)); +} + +#[test] +#[should_panic(expected = "Error(Contract, #5)")] +fn test_owner_of_revoked_fails() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let recipient = Address::generate(&env); + let reason = String::from_str(&env, "Plagiarism"); + + env.mock_all_auths(); + let token_id = client.mint(&recipient, &cid(&env, "ipfs://test")); + client.revoke(&token_id, &reason); + + client.owner_of(&token_id); +} + +#[test] +#[should_panic(expected = "Error(Auth, InvalidAction)")] +fn test_unauthorized_revoke_fails() { + let env = Env::default(); + let (contract_id, _admin, client) = setup(&env); + let scholar = Address::generate(&env); + let hacker = Address::generate(&env); + let reason = String::from_str(&env, "Hax"); + + env.mock_all_auths(); + let token_id = client.mint(&scholar, &cid(&env, "ipfs://test")); + + env.mock_auths(&[MockAuth { + address: &hacker, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "revoke", + args: (&token_id, reason.clone()).into_val(&env), + sub_invokes: &[], + }, + }]); + client.revoke(&token_id, &reason); +} + +#[test] +#[should_panic(expected = "Error(Contract, #4)")] +fn test_revoke_non_existent_token_panics() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let token_id = 999u64; + let reason = String::from_str(&env, "Testing"); + + env.mock_all_auths(); + client.revoke(&token_id, &reason); +} + +#[test] +#[should_panic(expected = "Error(Contract, #8)")] +fn test_revoke_already_revoked_panics() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let scholar = Address::generate(&env); + let reason = String::from_str(&env, "Reason"); + + env.mock_all_auths(); + let token_id = client.mint(&scholar, &cid(&env, "ipfs://test")); + client.revoke(&token_id, &reason); + client.revoke(&token_id, &reason); +} + +#[test] +fn initialize_emits_event() { + let env = Env::default(); + let admin = Address::generate(&env); + let contract_id = env.register(ScholarNFT, ()); + let client = ScholarNFTClient::new(&env, &contract_id); + + env.mock_all_auths(); + client.initialize(&admin); + + let events = env.events().all(); + let found = events.iter().any(|(_, topics, data)| { + topics.contains(&symbol_short!("init").into_val(&env)) && { + let d: InitializedEventData = data.clone().into_val(&env); + d == InitializedEventData { + admin: admin.clone(), + } + } + }); + assert!(found, "initialized event not found"); +} + +#[test] +fn mint_emits_event() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let scholar = Address::generate(&env); + let uri = cid(&env, "ipfs://mint-event-test"); + + env.mock_all_auths(); + let token_id = client.mint(&scholar, &uri); + + let events = env.events().all(); + let found = events.iter().any(|(_, topics, data)| { + topics.contains(&symbol_short!("minted").into_val(&env)) + && topics.contains(&token_id.into_val(&env)) + && { + let d: MintEventData = data.clone().into_val(&env); + d == MintEventData { + token_id, + owner: scholar.clone(), + } + } + }); + assert!(found, "mint event not found"); +} + +#[test] +fn transfer_admin_emits_event() { + let env = Env::default(); + let (_, old_admin, client) = setup(&env); + let new_admin = Address::generate(&env); + + env.mock_all_auths(); + client.transfer_admin(&new_admin); + + let events = env.events().all(); + let found = events.iter().any(|(_, topics, data)| { + topics.contains(&symbol_short!("adm_chng").into_val(&env)) && { + let d: AdminChangedEventData = data.clone().into_val(&env); + d == AdminChangedEventData { + old_admin: old_admin.clone(), + new_admin: new_admin.clone(), + } + } + }); + assert!(found, "admin_changed event not found"); +} + +#[test] +fn transfer_panics_with_soulbound_error() { + let env = Env::default(); + let (_, _, client) = setup(&env); + let from = Address::generate(&env); + let to = Address::generate(&env); + let token_id = 1_u64; + + let result = client.try_transfer(&from, &to, &token_id); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + ScholarNFTError::Soulbound as u32 + ))) + ); +} + +#[test] +fn transfer_attempt_reverts_soulbound() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let from = Address::generate(&env); + let to = Address::generate(&env); + + env.mock_all_auths(); + let token_id = client.mint(&from, &cid(&env, "ipfs://test")); + + let res = client.try_transfer(&from, &to, &token_id); + assert!(res.is_err()); +} + +#[test] +fn test_mint_extends_ttl() { + let env = Env::default(); + let (contract_id, _admin, client) = setup(&env); + let scholar = Address::generate(&env); + + env.mock_all_auths(); + let token_id = client.mint(&scholar, &cid(&env, "ipfs://ttl-test")); + + env.as_contract(&contract_id, || { + assert!( + env.storage() + .persistent() + .get_ttl(&DataKey::Owner(token_id)) + >= 6_307_200 + ); + assert!( + env.storage() + .persistent() + .get_ttl(&DataKey::TokenUri(token_id)) + >= 6_307_200 + ); + assert!( + env.storage() + .persistent() + .get_ttl(&DataKey::Metadata(token_id)) + >= 6_307_200 + ); + }); +} + +#[test] +fn upgrade_requires_admin_auth() { + let env = Env::default(); + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let contract_id = env.register(ScholarNFT, ()); + let client = ScholarNFTClient::new(&env, &contract_id); + + env.mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "initialize", + args: (&admin,).into_val(&env), + sub_invokes: &[], + }, + }]); + client.initialize(&admin); + + let wasm_hash = crate::upgrade::testutils::upload_upgrade_target(&env); + authorize_upgrade(&env, &contract_id, &attacker, &wasm_hash); + assert!(client.try_upgrade(&wasm_hash).is_err()); +} + +#[test] +fn state_persists_after_upgrade() { + let env = Env::default(); + let (contract_id, admin, client) = setup(&env); + let scholar = Address::generate(&env); + let metadata_uri = cid(&env, "ipfs://upgrade-check"); + + env.mock_all_auths(); + let token_id = client.mint(&scholar, &metadata_uri); + + env.set_auths(&[]); + let wasm_hash = crate::upgrade::testutils::upload_upgrade_target(&env); + authorize_upgrade(&env, &contract_id, &admin, &wasm_hash); + client.upgrade(&wasm_hash); + + let metadata = env.as_contract(&contract_id, || { + env.storage() + .persistent() + .get::<_, ScholarMetadata>(&DataKey::Metadata(token_id)) + }); + let stored_hash = env.as_contract(&contract_id, || crate::upgrade::current_hash(&env)); + + let metadata = metadata.expect("metadata should persist across upgrades"); + assert_eq!(metadata.owner, scholar); + assert_eq!(metadata.metadata_uri, metadata_uri); + assert_eq!(stored_hash, wasm_hash); +} diff --git a/contracts/scholarship_treasury/Cargo.toml b/contracts/scholarship_treasury/Cargo.toml new file mode 100644 index 00000000..615e3157 --- /dev/null +++ b/contracts/scholarship_treasury/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "scholarship-treasury" +description = "Treasury contract for scholarship funding in USDC" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +learnvault-shared = { workspace = true } +soroban-sdk = { workspace = true } +stellar-registry = "0.0.4" + +[dev-dependencies] +learnvault-shared = { workspace = true, features = ["testutils"] } +soroban-sdk = { workspace = true, features = ["testutils"] } +proptest = { workspace = true } +governance-token = { path = "../governance_token" } diff --git a/contracts/scholarship_treasury/src/lib.rs b/contracts/scholarship_treasury/src/lib.rs new file mode 100644 index 00000000..def48aaf --- /dev/null +++ b/contracts/scholarship_treasury/src/lib.rs @@ -0,0 +1,973 @@ +#![no_std] +#![allow(clippy::too_many_arguments)] + +use soroban_sdk::{ + Address, BytesN, Env, String, Symbol, Vec, contract, contracterror, contractevent, + contractimpl, contracttype, panic_with_error, symbol_short, +}; + +use learnvault_shared::upgrade; + +pub use upgrade::ContractUpgraded; + +// --------------------------------------------------------------------------- +// Storage Constants (assuming ~6s ledger time) +// --------------------------------------------------------------------------- + +const DAY_IN_LEDGERS: u32 = 17_280; +const INSTANCE_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; // 30 days +const PERSISTENT_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const PERSISTENT_EXTEND_TO: u32 = DAY_IN_LEDGERS * 365; // 1 year + +const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); +const GOV_KEY: Symbol = symbol_short!("GOV"); +const USDC_KEY: Symbol = symbol_short!("USDC"); +const TOTAL_KEY: Symbol = symbol_short!("TOTAL"); +const NEXT_PROPOSAL_KEY: Symbol = symbol_short!("NEXTPROP"); +const DISBURSED_KEY: Symbol = symbol_short!("DISBURSED"); +const SCHOLARS_KEY: Symbol = symbol_short!("SCHOLARS"); +const DONORS_KEY: Symbol = symbol_short!("DONORS"); +const PAUSED_KEY: Symbol = symbol_short!("PAUSED"); +const TOTAL_GOV_KEY: Symbol = symbol_short!("TOTALGOV"); +const MIN_LRN_TO_PROPOSE_KEY: Symbol = symbol_short!("MINPROP"); +const GOV_PER_USDC: i128 = 100; +const PROPOSAL_DEADLINE_LEDGERS: u32 = 100_800; +const QUORUM_KEY: Symbol = symbol_short!("QUORUM"); +const APPROVAL_BPS_KEY: Symbol = symbol_short!("APPBPS"); + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Donor(Address), + Proposal(u32), + ApplicantProposals(Address), + Scholar(Address), + VoteCast(u32, Address), // (proposal_id, voter) -> bool + FinalizedProposal(u32), // proposal_id -> ProposalStatus (set by finalize_proposal) +} + +#[contractevent(topics = ["proposal_executed"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProposalExecuted { + #[topic] + pub proposal_id: u32, + pub passed: bool, + pub amount: i128, +} + +#[contractevent(topics = ["proposal_cancelled"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProposalCancelled { + #[topic] + pub proposal_id: u32, + pub cancelled_by: Address, +} + +#[derive(Clone)] +#[contracttype] +pub struct Proposal { + pub id: u32, + pub applicant: Address, + pub amount: i128, + pub program_name: String, + pub program_url: String, + pub program_description: String, + pub start_date: String, + pub milestone_titles: Vec, + pub milestone_dates: Vec, + pub submitted_at: u64, + pub yes_votes: i128, + pub no_votes: i128, + pub deadline_ledger: u32, + pub executed: bool, + pub cancelled: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub enum ProposalStatus { + Pending, + Approved, + Rejected, +} + +#[contracterror] +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + AlreadyInitialized = 1, + NotInitialized = 2, + InvalidAmount = 3, + InsufficientFunds = 4, + ContractPaused = 5, + ProposalNotFound = 6, + AlreadyVoted = 7, + VotingClosed = 8, + /// Votes cast after the proposal's voting deadline. + VotingPeriodEnded = 9, + /// finalize_proposal called before the voting deadline has passed. + TooEarlyToFinalize = 10, + /// Proposal finalized but total votes cast did not reach MIN_QUORUM_BPS. + QuorumNotMet = 11, + InsufficientReputation = 12, + VotingNotClosed = 13, + ProposalAlreadyExecuted = 14, + ProposalRejected = 15, + ProposalCancelled = 16, + Unauthorized = 17, +} + +#[contract] +pub struct ScholarshipTreasury; + +#[contractevent(topics = ["deposit"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DepositRecorded { + #[topic] + pub donor: Address, + pub amount: i128, +} + +#[contractevent(topics = ["gov_issued"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GovIssued { + #[topic] + pub donor: Address, + pub usdc_amount: i128, + pub gov_amount: i128, +} + +#[contractevent(topics = ["disburse"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DisbursementRecorded { + #[topic] + pub recipient: Address, + pub amount: i128, +} + +#[contractevent(topics = ["proposal"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProposalSubmitted { + #[topic] + pub applicant: Address, + #[topic] + pub proposal_id: u32, + pub amount: i128, +} + +#[contractevent(topics = ["vote"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VoteCastEvent { + #[topic] + pub voter: Address, + #[topic] + pub proposal_id: u32, + pub support: bool, + pub weight: i128, +} + +#[contractimpl] +impl ScholarshipTreasury { + pub fn initialize( + env: Env, + admin: Address, + usdc_token: Address, + governance_contract: Address, + quorum_threshold: i128, + approval_bps: u32, + ) { + if env.storage().instance().has(&ADMIN_KEY) { + panic_with_error!(&env, Error::AlreadyInitialized); + } + admin.require_auth(); + + if quorum_threshold < 0 { + panic_with_error!(&env, Error::InvalidAmount); + } + if approval_bps > 10_000 { + panic_with_error!(&env, Error::InvalidAmount); + } + + env.storage().instance().set(&ADMIN_KEY, &admin); + upgrade::init(&env); + env.storage().instance().set(&USDC_KEY, &usdc_token); + env.storage().instance().set(&GOV_KEY, &governance_contract); + env.storage().instance().set(&TOTAL_KEY, &0_i128); + env.storage().instance().set(&NEXT_PROPOSAL_KEY, &1_u32); + env.storage().instance().set(&DISBURSED_KEY, &0_i128); + env.storage().instance().set(&SCHOLARS_KEY, &0_u32); + env.storage().instance().set(&DONORS_KEY, &0_u32); + env.storage().instance().set(&PAUSED_KEY, &false); + env.storage() + .instance() + .set(&MIN_LRN_TO_PROPOSE_KEY, &0_i128); + + env.storage().instance().set(&QUORUM_KEY, &quorum_threshold); + env.storage() + .instance() + .set(&APPROVAL_BPS_KEY, &approval_bps); + + Self::extend_instance(&env); + } + + pub fn get_quorum(env: Env) -> i128 { + Self::extend_instance(&env); + env.storage() + .instance() + .get::<_, i128>(&QUORUM_KEY) + .unwrap_or(0) + } + + pub fn get_approval_bps(env: Env) -> u32 { + Self::extend_instance(&env); + env.storage() + .instance() + .get::<_, u32>(&APPROVAL_BPS_KEY) + .unwrap_or(0) + } + + pub fn set_quorum(env: Env, new_quorum: i128) { + let admin = Self::admin(&env); + admin.require_auth(); + if new_quorum < 0 { + panic_with_error!(&env, Error::InvalidAmount); + } + env.storage().instance().set(&QUORUM_KEY, &new_quorum); + } + + pub fn set_approval_bps(env: Env, new_bps: u32) { + let admin = Self::admin(&env); + admin.require_auth(); + if new_bps > 10_000 { + panic_with_error!(&env, Error::InvalidAmount); + } + env.storage().instance().set(&APPROVAL_BPS_KEY, &new_bps); + } + + pub fn pause(env: Env) { + let admin = Self::admin(&env); + admin.require_auth(); + env.storage().instance().set(&PAUSED_KEY, &true); + } + + pub fn unpause(env: Env) { + let admin = Self::admin(&env); + admin.require_auth(); + env.storage().instance().set(&PAUSED_KEY, &false); + } + + pub fn is_paused(env: Env) -> bool { + env.storage() + .instance() + .get::<_, bool>(&PAUSED_KEY) + .unwrap_or(false) + } + + pub fn deposit(env: Env, donor: Address, amount: i128) { + Self::assert_not_paused(&env); + + if amount <= 0 { + panic_with_error!(&env, Error::InvalidAmount); + } + donor.require_auth(); + + let usdc = token::client(&env); + usdc.transfer(&donor, env.current_contract_address(), &amount); + + let gov_contract = Self::governance_contract(&env); + let gov_client = governance::client(&env, &gov_contract); + let gov_amount = amount + .checked_mul(GOV_PER_USDC) + .unwrap_or_else(|| panic_with_error!(&env, Error::InvalidAmount)); + gov_client.mint(&donor, &gov_amount); + GovIssued { + donor: donor.clone(), + usdc_amount: amount, + gov_amount, + } + .publish(&env); + + // Track total GOV issued for quorum calculations + let total_gov = env + .storage() + .instance() + .get::<_, i128>(&TOTAL_GOV_KEY) + .unwrap_or(0); + env.storage() + .instance() + .set(&TOTAL_GOV_KEY, &(total_gov + gov_amount)); + + let donor_key = DataKey::Donor(donor.clone()); + let current = env + .storage() + .persistent() + .get::<_, i128>(&donor_key) + .unwrap_or(0); + + if current == 0 { + let donors_count = env + .storage() + .instance() + .get::<_, u32>(&DONORS_KEY) + .unwrap_or(0); + env.storage() + .instance() + .set(&DONORS_KEY, &(donors_count + 1)); + } + + env.storage() + .persistent() + .set(&donor_key, &(current + amount)); + + Self::extend_persistent(&env, &donor_key); + + let total = env + .storage() + .instance() + .get::<_, i128>(&TOTAL_KEY) + .unwrap_or(0); + env.storage().instance().set(&TOTAL_KEY, &(total + amount)); + + DepositRecorded { donor, amount }.publish(&env); + } + + pub fn disburse(env: Env, recipient: Address, amount: i128) { + Self::assert_not_paused(&env); + + if amount <= 0 { + panic_with_error!(&env, Error::InvalidAmount); + } + + let governance = Self::governance_contract(&env); + governance.require_auth(); + + let total = env + .storage() + .instance() + .get::<_, i128>(&TOTAL_KEY) + .unwrap_or(0); + if amount > total { + panic_with_error!(&env, Error::InsufficientFunds); + } + + token::client(&env).transfer(&env.current_contract_address(), &recipient, &amount); + env.storage().instance().set(&TOTAL_KEY, &(total - amount)); + + let disbursed = env + .storage() + .instance() + .get::<_, i128>(&DISBURSED_KEY) + .unwrap_or(0); + env.storage() + .instance() + .set(&DISBURSED_KEY, &(disbursed + amount)); + + let scholar_key = DataKey::Scholar(recipient.clone()); + if !env.storage().persistent().has(&scholar_key) { + let scholars_count = env + .storage() + .instance() + .get::<_, u32>(&SCHOLARS_KEY) + .unwrap_or(0); + env.storage() + .instance() + .set(&SCHOLARS_KEY, &(scholars_count + 1)); + env.storage().persistent().set(&scholar_key, &true); + Self::extend_persistent(&env, &scholar_key); + } + + DisbursementRecorded { recipient, amount }.publish(&env); + } + + pub fn execute_proposal(env: Env, proposal_id: u32) { + Self::assert_initialized(&env); + Self::assert_not_paused(&env); + + let mut proposal = env + .storage() + .persistent() + .get::<_, Proposal>(&DataKey::Proposal(proposal_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound)); + + if proposal.cancelled { + panic_with_error!(&env, Error::ProposalCancelled); + } + + if env.ledger().sequence() <= proposal.deadline_ledger { + panic_with_error!(&env, Error::VotingNotClosed); + } + + if proposal.executed { + panic_with_error!(&env, Error::ProposalAlreadyExecuted); + } + + let total_votes = proposal.yes_votes + proposal.no_votes; + let quorum_threshold = Self::get_quorum(env.clone()); + let approval_bps = Self::get_approval_bps(env.clone()); + + let passed = total_votes >= quorum_threshold + && total_votes > 0 + && proposal + .yes_votes + .checked_mul(10_000) + .map(|v| (v / total_votes) as u32 > approval_bps) + .unwrap_or(false); + + if passed { + Self::disburse_internal(&env, &proposal.applicant, proposal.amount); + } + + proposal.executed = true; + env.storage() + .persistent() + .set(&DataKey::Proposal(proposal_id), &proposal); + Self::extend_persistent(&env, &DataKey::Proposal(proposal_id)); + + ProposalExecuted { + proposal_id, + passed, + amount: if passed { proposal.amount } else { 0 }, + } + .publish(&env); + } + + pub fn cancel_proposal(env: Env, proposal_id: u32) { + Self::assert_initialized(&env); + let admin = Self::admin(&env); + admin.require_auth(); + + let mut proposal = env + .storage() + .persistent() + .get::<_, Proposal>(&DataKey::Proposal(proposal_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound)); + + if env.ledger().sequence() > proposal.deadline_ledger { + panic_with_error!(&env, Error::VotingClosed); + } + + if proposal.executed { + panic_with_error!(&env, Error::ProposalAlreadyExecuted); + } + + proposal.cancelled = true; + env.storage() + .persistent() + .set(&DataKey::Proposal(proposal_id), &proposal); + Self::extend_persistent(&env, &DataKey::Proposal(proposal_id)); + + ProposalCancelled { + proposal_id, + cancelled_by: admin, + } + .publish(&env); + } + + pub fn get_balance(env: Env) -> i128 { + env.storage() + .instance() + .get::<_, i128>(&TOTAL_KEY) + .unwrap_or(0) + } + + pub fn get_total_disbursed(env: Env) -> i128 { + env.storage() + .instance() + .get::<_, i128>(&DISBURSED_KEY) + .unwrap_or(0) + } + + pub fn get_exchange_rate(_env: Env) -> i128 { + GOV_PER_USDC + } + + pub fn get_scholars_count(env: Env) -> u32 { + env.storage() + .instance() + .get::<_, u32>(&SCHOLARS_KEY) + .unwrap_or(0) + } + + pub fn get_donors_count(env: Env) -> u32 { + env.storage() + .instance() + .get::<_, u32>(&DONORS_KEY) + .unwrap_or(0) + } + + pub fn get_donor_total(env: Env, donor: Address) -> i128 { + env.storage() + .persistent() + .get::<_, i128>(&DataKey::Donor(donor)) + .unwrap_or(0) + } + + pub fn set_min_lrn_to_propose(env: Env, admin: Address, min_lrn: i128) { + Self::assert_initialized(&env); + + admin.require_auth(); + if admin != Self::admin(&env) { + panic_with_error!(&env, Error::Unauthorized); + } + if min_lrn < 0 { + panic_with_error!(&env, Error::InvalidAmount); + } + + env.storage() + .instance() + .set(&MIN_LRN_TO_PROPOSE_KEY, &min_lrn); + } + + pub fn get_min_lrn_to_propose(env: Env) -> i128 { + env.storage() + .instance() + .get::<_, i128>(&MIN_LRN_TO_PROPOSE_KEY) + .unwrap_or(0) + } + + #[allow(clippy::too_many_arguments)] + pub fn submit_proposal( + env: Env, + applicant: Address, + amount: i128, + program_name: String, + program_url: String, + program_description: String, + start_date: String, + milestone_titles: Vec, + milestone_dates: Vec, + ) -> u32 { + Self::assert_initialized(&env); + Self::assert_not_paused(&env); + + if amount <= 0 || milestone_titles.len() != 3 || milestone_dates.len() != 3 { + panic_with_error!(&env, Error::InvalidAmount); + } + + applicant.require_auth(); + + let gov_contract = Self::governance_contract(&env); + let gov_client = governance::client(&env, &gov_contract); + let min_lrn_to_propose = Self::get_min_lrn_to_propose(env.clone()); + if gov_client.balance(&applicant) < min_lrn_to_propose { + panic_with_error!(&env, Error::InsufficientReputation); + } + + let proposal_id = env + .storage() + .instance() + .get::<_, u32>(&NEXT_PROPOSAL_KEY) + .unwrap_or(1); + + let proposal = Proposal { + id: proposal_id, + applicant: applicant.clone(), + amount, + program_name, + program_url, + program_description, + start_date, + milestone_titles, + milestone_dates, + submitted_at: env.ledger().timestamp(), + yes_votes: 0, + no_votes: 0, + deadline_ledger: env.ledger().sequence() + PROPOSAL_DEADLINE_LEDGERS, + executed: false, + cancelled: false, + }; + + env.storage() + .persistent() + .set(&DataKey::Proposal(proposal_id), &proposal); + + Self::extend_persistent(&env, &DataKey::Proposal(proposal_id)); + + let applicant_key = DataKey::ApplicantProposals(applicant.clone()); + let mut proposal_ids = env + .storage() + .persistent() + .get::<_, Vec>(&applicant_key) + .unwrap_or(Vec::new(&env)); + proposal_ids.push_back(proposal_id); + env.storage() + .persistent() + .set(&applicant_key, &proposal_ids); + + Self::extend_persistent(&env, &applicant_key); + env.storage() + .instance() + .set(&NEXT_PROPOSAL_KEY, &(proposal_id + 1)); + + ProposalSubmitted { + applicant, + proposal_id, + amount, + } + .publish(&env); + + proposal_id + } + + pub fn get_proposal(env: Env, proposal_id: u32) -> Option { + Self::extend_instance(&env); + let key = DataKey::Proposal(proposal_id); + if let Some(prop) = env.storage().persistent().get::<_, Proposal>(&key) { + Self::extend_persistent(&env, &key); + Some(prop) + } else { + None + } + } + + pub fn get_proposals_by_applicant(env: Env, applicant: Address) -> Vec { + env.storage() + .persistent() + .get::<_, Vec>(&DataKey::ApplicantProposals(applicant)) + .unwrap_or(Vec::new(&env)) + } + + pub fn get_proposals_by_status(env: Env, status: ProposalStatus) -> Vec { + let proposal_count = Self::get_proposal_count(env.clone()); + let mut proposal_id = 1_u32; + let mut proposals = Vec::new(&env); + + while proposal_id <= proposal_count { + if let Some(proposal) = env + .storage() + .persistent() + .get::<_, Proposal>(&DataKey::Proposal(proposal_id)) + .filter(|p| Self::proposal_status(&env, p) == status) + { + proposals.push_back(proposal); + } + proposal_id += 1; + } + + proposals + } + + pub fn get_active_proposals(env: Env) -> Vec { + Self::get_proposals_by_status(env, ProposalStatus::Pending) + } + + pub fn get_proposal_count(env: Env) -> u32 { + env.storage() + .instance() + .get::<_, u32>(&NEXT_PROPOSAL_KEY) + .unwrap_or(1) + .saturating_sub(1) + } + + pub fn vote(env: Env, voter: Address, proposal_id: u32, support: bool) { + // 1. Require auth + voter.require_auth(); + + // 2. Load proposal — panic ProposalNotFound if missing + let mut proposal = env + .storage() + .persistent() + .get::<_, Proposal>(&DataKey::Proposal(proposal_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound)); + + if proposal.cancelled { + panic_with_error!(&env, Error::ProposalCancelled); + } + + if proposal.executed { + panic_with_error!(&env, Error::ProposalAlreadyExecuted); + } + + // 3. Panic VotingClosed if past deadline + if env.ledger().sequence() > proposal.deadline_ledger { + panic_with_error!(&env, Error::VotingClosed); + } + + // 4. Panic AlreadyVoted if VoteCast(proposal_id, voter) exists + let vote_key = DataKey::VoteCast(proposal_id, voter.clone()); + if env + .storage() + .persistent() + .get::<_, bool>(&vote_key) + .unwrap_or(false) + { + panic_with_error!(&env, Error::AlreadyVoted); + } + + // 5. Get voter's GOV token balance as weight + let gov_contract = Self::governance_contract(&env); + let gov_client = governance::client(&env, &gov_contract); + let weight = gov_client.get_voting_power(&voter); + // Weight of 0 is permitted; vote is recorded but has no numerical effect on outcome + + // 6. Add weight to yes_votes or no_votes + if support { + proposal.yes_votes += weight; + } else { + proposal.no_votes += weight; + } + + // 7. Mark VoteCast = true + env.storage().persistent().set(&vote_key, &true); + + // 8. Update stored proposal + env.storage() + .persistent() + .set(&DataKey::Proposal(proposal_id), &proposal); + + Self::extend_persistent(&env, &vote_key); + Self::extend_persistent(&env, &DataKey::Proposal(proposal_id)); + + // 9. Emit event + VoteCastEvent { + voter, + proposal_id, + support, + weight, + } + .publish(&env); + } + + /// Finalize a proposal once its voting deadline has passed. + /// + /// Only the admin may call this. The outcome is: + /// - **Rejected** if total votes cast < MIN_QUORUM_BPS of total GOV supply. + /// - **Approved** if quorum is met and `yes_votes > no_votes`. + /// - **Rejected** otherwise (tie or majority against). + /// + /// The result is stored under `DataKey::FinalizedProposal(proposal_id)` so + /// it can be read back without re-running the tally. + pub fn finalize_proposal(env: Env, admin: Address, proposal_id: u32) -> ProposalStatus { + admin.require_auth(); + let stored_admin = Self::admin(&env); + if admin != stored_admin { + panic_with_error!(&env, Error::Unauthorized); + } + + let proposal = env + .storage() + .persistent() + .get::<_, Proposal>(&DataKey::Proposal(proposal_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound)); + + // Must be called after the voting deadline + if env.ledger().sequence() <= proposal.deadline_ledger { + panic_with_error!(&env, Error::VotingNotClosed); + } + + let total_votes = proposal.yes_votes + proposal.no_votes; + let quorum_threshold = Self::get_quorum(env.clone()); + let approval_bps = Self::get_approval_bps(env.clone()); + + let passed = total_votes >= quorum_threshold + && total_votes > 0 + && proposal + .yes_votes + .checked_mul(10_000) + .map(|v| (v / total_votes) as u32 > approval_bps) + .unwrap_or(false); + + let status = if passed { + ProposalStatus::Approved + } else { + ProposalStatus::Rejected + }; + + env.storage() + .persistent() + .set(&DataKey::FinalizedProposal(proposal_id), &status.clone()); + + Self::extend_persistent(&env, &DataKey::FinalizedProposal(proposal_id)); + + status + } + + /// Returns the finalized status for a proposal if `finalize_proposal` has + /// been called, or `None` if it hasn't been finalized yet. + pub fn get_finalized_status(env: Env, proposal_id: u32) -> Option { + env.storage() + .persistent() + .get::<_, ProposalStatus>(&DataKey::FinalizedProposal(proposal_id)) + } + + /// Returns the total GOV tokens issued so far (used for quorum calculation). + pub fn get_total_gov_issued(env: Env) -> i128 { + env.storage() + .instance() + .get::<_, i128>(&TOTAL_GOV_KEY) + .unwrap_or(0) + } + + fn governance_contract(env: &Env) -> Address { + if let Some(governance) = env.storage().instance().get::<_, Address>(&GOV_KEY) { + governance + } else { + panic_with_error!(env, Error::NotInitialized); + } + } + + fn assert_initialized(env: &Env) { + if !env.storage().instance().has(&ADMIN_KEY) { + panic_with_error!(env, Error::NotInitialized); + } + } + + fn assert_not_paused(env: &Env) { + let paused: bool = env.storage().instance().get(&PAUSED_KEY).unwrap_or(false); + if paused { + panic_with_error!(env, Error::ContractPaused); + } + } + + fn proposal_status(env: &Env, proposal: &Proposal) -> ProposalStatus { + if proposal.cancelled { + return ProposalStatus::Rejected; + } + if env.ledger().sequence() <= proposal.deadline_ledger { + ProposalStatus::Pending + } else if proposal.yes_votes > proposal.no_votes { + ProposalStatus::Approved + } else { + ProposalStatus::Rejected + } + } + + fn disburse_internal(env: &Env, recipient: &Address, amount: i128) { + if amount <= 0 { + panic_with_error!(env, Error::InvalidAmount); + } + + let total = env + .storage() + .instance() + .get::<_, i128>(&TOTAL_KEY) + .unwrap_or(0); + if amount > total { + panic_with_error!(env, Error::InsufficientFunds); + } + + token::client(env).transfer(&env.current_contract_address(), recipient, &amount); + env.storage().instance().set(&TOTAL_KEY, &(total - amount)); + + let disbursed = env + .storage() + .instance() + .get::<_, i128>(&DISBURSED_KEY) + .unwrap_or(0); + env.storage() + .instance() + .set(&DISBURSED_KEY, &(disbursed + amount)); + + let scholar_key = DataKey::Scholar(recipient.clone()); + if !env.storage().persistent().has(&scholar_key) { + let scholars_count = env + .storage() + .instance() + .get::<_, u32>(&SCHOLARS_KEY) + .unwrap_or(0); + env.storage() + .instance() + .set(&SCHOLARS_KEY, &(scholars_count + 1)); + env.storage().persistent().set(&scholar_key, &true); + Self::extend_persistent(env, &scholar_key); + } + + DisbursementRecorded { + recipient: recipient.clone(), + amount, + } + .publish(env); + } + + fn admin(env: &Env) -> Address { + env.storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)) + } + + /// Replace the current contract WASM with a new uploaded hash. Admin only. + pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { + Self::assert_initialized(&env); + Self::extend_instance(&env); + let admin = Self::admin(&env); + admin.require_auth(); + upgrade::apply(&env, &admin, &new_wasm_hash); + } + + pub fn get_version(env: Env) -> String { + String::from_str(&env, "1.0.0") + } + + fn extend_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_BUMP_THRESHOLD, INSTANCE_EXTEND_TO); + } + + fn extend_persistent(env: &Env, key: &DataKey) { + env.storage() + .persistent() + .extend_ttl(key, PERSISTENT_BUMP_THRESHOLD, PERSISTENT_EXTEND_TO); + } +} + +mod governance { + use soroban_sdk::{Address, Env}; + + pub fn client<'a>(env: &Env, contract_id: &Address) -> GovernanceTokenClient<'a> { + GovernanceTokenClient::new(env, contract_id) + } + + #[soroban_sdk::contractclient(name = "GovernanceTokenClient")] + #[allow(dead_code)] + pub trait GovernanceTokenInterface { + fn mint(env: Env, to: Address, amount: i128); + fn balance(env: Env, account: Address) -> i128; + fn get_voting_power(env: Env, address: Address) -> i128; + } +} + +pub use governance::GovernanceTokenClient; + +mod token { + #[cfg(test)] + mod test_token { + use soroban_sdk::{Address, Env}; + + use super::super::USDC_KEY; + + pub fn contract_id(env: &Env) -> Address { + env.storage() + .instance() + .get::<_, Address>(&USDC_KEY) + .expect("token contract not initialized") + } + + pub fn register(env: &Env, admin: &Address) { + let sac = env.register_stellar_asset_contract_v2(admin.clone()); + env.storage().instance().set(&USDC_KEY, &sac.address()); + } + + pub fn client<'a>(env: &Env) -> soroban_sdk::token::TokenClient<'a> { + soroban_sdk::token::TokenClient::new(env, &contract_id(env)) + } + } + + #[cfg(not(test))] + pub fn client<'a>(env: &soroban_sdk::Env) -> soroban_sdk::token::TokenClient<'a> { + let token_address = env + .storage() + .instance() + .get::<_, soroban_sdk::Address>(&crate::USDC_KEY) + .unwrap_or_else(|| soroban_sdk::panic_with_error!(env, crate::Error::NotInitialized)); + soroban_sdk::token::TokenClient::new(env, &token_address) + } + + #[cfg(test)] + pub use test_token::*; +} + +#[cfg(test)] +mod test; diff --git a/contracts/scholarship_treasury/src/test.rs b/contracts/scholarship_treasury/src/test.rs new file mode 100644 index 00000000..8ede5de0 --- /dev/null +++ b/contracts/scholarship_treasury/src/test.rs @@ -0,0 +1,1983 @@ +extern crate std; + +use soroban_sdk::{ + Address, Env, IntoVal, String, Val, Vec, contract, contractimpl, + testutils::{Address as _, Events as _, Ledger, MockAuth, MockAuthInvoke}, + token::{StellarAssetClient, TokenClient}, +}; + +use crate::{ + DataKey, Error, Proposal, ProposalStatus, ScholarshipTreasury, ScholarshipTreasuryClient, token, +}; + +const DEFAULT_QUORUM: i128 = 1; +const DEFAULT_APPROVAL_BPS: u32 = 5_000; + +#[contract] +pub struct MockGovernance; + +#[contractimpl] +impl MockGovernance { + pub fn initialize(_env: Env, _treasury: Address) {} + pub fn mint(env: Env, to: Address, amount: i128) { + let _key = soroban_sdk::Symbol::new(&env, "balance"); + let balance: i128 = env.storage().persistent().get(&to).unwrap_or(0); + env.storage().persistent().set(&to, &(balance + amount)); + } + pub fn balance(env: Env, account: Address) -> i128 { + env.storage().persistent().get(&account).unwrap_or(0) + } + pub fn get_voting_power(env: Env, address: Address) -> i128 { + // For mock, just return balance. We'll manually mint to simulate delegated power in tests if needed. + Self::balance(env, address) + } +} + +fn setup<'a>( + env: &'a Env, +) -> ( + ScholarshipTreasuryClient<'a>, + Address, + Address, + Address, + Address, + MockGovernanceClient<'a>, +) { + let admin = Address::generate(env); + let donor = Address::generate(env); + let recipient = Address::generate(env); + + let contract_id = env.register(ScholarshipTreasury, ()); + let client = ScholarshipTreasuryClient::new(env, &contract_id); + + let gov_contract_id = env.register(MockGovernance, ()); + let gov_client = MockGovernanceClient::new(env, &gov_contract_id); + + env.mock_all_auths(); + env.as_contract(&contract_id, || token::register(env, &admin)); + let token_id = env.as_contract(&contract_id, || token::contract_id(env)); + let sac = StellarAssetClient::new(env, &token_id); + sac.mint(&donor, &1_000); + + gov_client.initialize(&contract_id); + client.initialize( + &admin, + &token_id, + &gov_contract_id, + &DEFAULT_QUORUM, + &DEFAULT_APPROVAL_BPS, + ); + env.set_auths(&[]); + + ( + client, + gov_contract_id, + donor, + recipient, + token_id, + gov_client, + ) +} + +fn token_client<'a>(env: &Env, token_id: &Address) -> TokenClient<'a> { + TokenClient::new(env, token_id) +} + +fn set_caller(client: &ScholarshipTreasuryClient, fn_name: &str, caller: &Address, args: T) +where + T: IntoVal>, +{ + client.env.set_auths(&[]); + let invoke = &MockAuthInvoke { + contract: &client.address, + fn_name, + args: args.into_val(&client.env), + sub_invokes: &[], + }; + client.env.mock_auths(&[MockAuth { + address: caller, + invoke, + }]); +} + +fn sample_milestones(env: &Env) -> (Vec, Vec) { + let titles = Vec::from_array( + env, + [ + String::from_str(env, "Admissions + enrollment"), + String::from_str(env, "Mid-program progress report"), + String::from_str(env, "Final completion + credential"), + ], + ); + let dates = Vec::from_array( + env, + [ + String::from_str(env, "2026-06-01"), + String::from_str(env, "2026-08-01"), + String::from_str(env, "2026-10-01"), + ], + ); + + (titles, dates) +} + +fn submit_sample_proposal( + env: &Env, + client: &ScholarshipTreasuryClient<'_>, + applicant: &Address, + amount: i128, +) -> u32 { + let (milestone_titles, milestone_dates) = sample_milestones(env); + client.submit_proposal( + applicant, + &amount, + &String::from_str(env, "Scholarship"), + &String::from_str(env, "https://example.com"), + &String::from_str(env, "Program description"), + &String::from_str(env, "2026-05-15"), + &milestone_titles, + &milestone_dates, + ) +} + +#[test] +fn deposits_are_tracked_per_donor() { + let env = Env::default(); + let (client, _governance, donor, _recipient, token_id, gov_client) = setup(&env); + + env.mock_all_auths(); + client.deposit(&donor, &150); + client.deposit(&donor, &50); + + assert_eq!(client.get_donor_total(&donor), 200); + assert_eq!(client.get_balance(), 200); + assert_eq!(token_client(&env, &token_id).balance(&client.address), 200); + assert_eq!(token_client(&env, &token_id).balance(&donor), 800); + let rate = client.get_exchange_rate(); + assert_eq!(gov_client.balance(&donor), 200_i128 * rate); +} + +#[test] +fn unauthorized_disburse_is_rejected() { + let env = Env::default(); + let (client, governance, donor, recipient, token_id, _gov_client) = setup(&env); + env.mock_all_auths(); + client.deposit(&donor, &250); + env.set_auths(&[]); + + let attacker = Address::generate(&env); + set_caller(&client, "disburse", &attacker, (&recipient, 100_i128)); + let unauthorized = client.try_disburse(&recipient, &100); + assert!(unauthorized.is_err()); + + set_caller(&client, "disburse", &governance, (&recipient, 100_i128)); + client.disburse(&recipient, &100); + + assert_eq!(client.get_balance(), 150); + assert_eq!(token_client(&env, &token_id).balance(&recipient), 100); + assert_eq!(token_client(&env, &token_id).balance(&client.address), 150); +} + +#[test] +fn disburse_more_than_balance_fails() { + let env = Env::default(); + let (client, governance, donor, recipient, _token_id, _gov_client) = setup(&env); + env.mock_all_auths(); + client.deposit(&donor, &10); + env.set_auths(&[]); + + set_caller(&client, "disburse", &governance, (&recipient, 20_i128)); + let result = client.try_disburse(&recipient, &20); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InsufficientFunds as u32 + ))) + ); +} + +#[test] +fn submitted_proposals_are_stored_per_applicant() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, _gov_client) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + let proposal_id = client.submit_proposal( + &donor, + &500, + &String::from_str(&env, "Stellar Builder Bootcamp"), + &String::from_str(&env, "https://bootcamp.example/apply"), + &String::from_str( + &env, + "An intensive Soroban engineering scholarship request.", + ), + &String::from_str(&env, "2026-05-15"), + &milestone_titles, + &milestone_dates, + ); + + assert_eq!(proposal_id, 1); + assert_eq!(client.get_proposal_count(), 1); + assert_eq!( + client.get_proposals_by_applicant(&donor), + Vec::from_array(&env, [1_u32]) + ); + + let proposal = client + .get_proposal(&proposal_id) + .expect("proposal should exist"); + assert_eq!(proposal.id, 1); + assert_eq!(proposal.applicant, donor); + assert_eq!(proposal.amount, 500); + assert_eq!( + proposal.program_name, + String::from_str(&env, "Stellar Builder Bootcamp") + ); + assert_eq!( + proposal.program_url, + String::from_str(&env, "https://bootcamp.example/apply") + ); + assert_eq!( + proposal.program_description, + String::from_str( + &env, + "An intensive Soroban engineering scholarship request." + ), + ); + assert_eq!(proposal.start_date, String::from_str(&env, "2026-05-15")); + assert_eq!(proposal.milestone_titles, milestone_titles); + assert_eq!(proposal.milestone_dates, milestone_dates); +} + +#[test] +fn get_proposals_by_status_returns_pending_proposals() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, _gov_client) = setup(&env); + + env.mock_all_auths(); + let proposal_id = submit_sample_proposal(&env, &client, &donor, 500); + + let pending = client.get_proposals_by_status(&ProposalStatus::Pending); + let active = client.get_active_proposals(); + let approved = client.get_proposals_by_status(&ProposalStatus::Approved); + let rejected = client.get_proposals_by_status(&ProposalStatus::Rejected); + + assert_eq!(pending.len(), 1); + assert_eq!(active.len(), 1); + assert_eq!(pending.get(0).unwrap().id, proposal_id); + assert_eq!(active.get(0).unwrap().id, proposal_id); + assert_eq!(approved.len(), 0); + assert_eq!(rejected.len(), 0); +} + +#[test] +fn get_proposals_by_status_returns_approved_proposals_after_deadline() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client) = setup(&env); + let voter = Address::generate(&env); + + gov_client.mint(&voter, &300); + env.mock_all_auths(); + let proposal_id = submit_sample_proposal(&env, &client, &donor, 500); + client.vote(&voter, &proposal_id, &true); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + env.ledger() + .set_sequence_number(proposal.deadline_ledger + 1); + + let approved = client.get_proposals_by_status(&ProposalStatus::Approved); + let rejected = client.get_proposals_by_status(&ProposalStatus::Rejected); + let pending = client.get_proposals_by_status(&ProposalStatus::Pending); + + assert_eq!(approved.len(), 1); + assert_eq!(approved.get(0).unwrap().id, proposal_id); + assert_eq!(rejected.len(), 0); + assert_eq!(pending.len(), 0); +} + +#[test] +fn get_proposals_by_status_returns_rejected_proposals_after_deadline() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client) = setup(&env); + let voter = Address::generate(&env); + + gov_client.mint(&voter, &200); + env.mock_all_auths(); + let proposal_id = submit_sample_proposal(&env, &client, &donor, 500); + client.vote(&voter, &proposal_id, &false); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + env.ledger() + .set_sequence_number(proposal.deadline_ledger + 1); + + let rejected = client.get_proposals_by_status(&ProposalStatus::Rejected); + let approved = client.get_proposals_by_status(&ProposalStatus::Approved); + + assert_eq!(rejected.len(), 1); + assert_eq!(rejected.get(0).unwrap().id, proposal_id); + assert_eq!(approved.len(), 0); +} + +#[test] +fn get_proposals_by_status_returns_empty_vec_when_no_match() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, _gov_client) = setup(&env); + + env.mock_all_auths(); + let _proposal_id = submit_sample_proposal(&env, &client, &donor, 500); + + let approved = client.get_proposals_by_status(&ProposalStatus::Approved); + let rejected = client.get_proposals_by_status(&ProposalStatus::Rejected); + + assert_eq!(approved.len(), 0); + assert_eq!(rejected.len(), 0); +} + +#[test] +fn proposal_requires_three_milestones() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, _gov_client) = setup(&env); + + env.mock_all_auths(); + let titles = Vec::from_array( + &env, + [ + String::from_str(&env, "Milestone 1"), + String::from_str(&env, "Milestone 2"), + ], + ); + let dates = Vec::from_array( + &env, + [ + String::from_str(&env, "2026-06-01"), + String::from_str(&env, "2026-07-01"), + ], + ); + + let result = client.try_submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Program description"), + &String::from_str(&env, "2026-05-15"), + &titles, + &dates, + ); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InvalidAmount as u32 + ))) + ); +} + +#[test] +fn yes_vote_adds_weight() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + let voter = Address::generate(&env); + gov_client.mint(&voter, &500); + + env.mock_all_auths(); + let proposal_id = client.submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Program description"), + &String::from_str(&env, "2026-05-15"), + &milestone_titles, + &milestone_dates, + ); + + client.vote(&voter, &proposal_id, &true); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + assert_eq!(proposal.yes_votes, 500); + assert_eq!(proposal.no_votes, 0); +} + +#[test] +fn vote_uses_delegated_power() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + let voter = Address::generate(&env); + // Simulate delegated power by minting directly to the voter in the mock + gov_client.mint(&voter, &1000); + + env.mock_all_auths(); + let proposal_id = client.submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Program description"), + &String::from_str(&env, "2026-05-15"), + &milestone_titles, + &milestone_dates, + ); + + client.vote(&voter, &proposal_id, &true); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + assert_eq!(proposal.yes_votes, 1000); +} + +#[test] +fn no_vote_adds_weight() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + let voter = Address::generate(&env); + gov_client.mint(&voter, &500); + + env.mock_all_auths(); + let proposal_id = client.submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Program description"), + &String::from_str(&env, "2026-05-15"), + &milestone_titles, + &milestone_dates, + ); + + client.vote(&voter, &proposal_id, &false); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + assert_eq!(proposal.no_votes, 500); + assert_eq!(proposal.yes_votes, 0); +} + +#[test] +fn double_vote_panics() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + let voter = Address::generate(&env); + gov_client.mint(&voter, &500); + + env.mock_all_auths(); + let proposal_id = client.submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Program description"), + &String::from_str(&env, "2026-05-15"), + &milestone_titles, + &milestone_dates, + ); + + client.vote(&voter, &proposal_id, &true); + let result = client.try_vote(&voter, &proposal_id, &true); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::AlreadyVoted as u32 + ))) + ); +} + +#[test] +fn vote_after_deadline_panics() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + let voter = Address::generate(&env); + gov_client.mint(&voter, &500); + + env.mock_all_auths(); + let proposal_id = client.submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Program description"), + &String::from_str(&env, "2026-05-15"), + &milestone_titles, + &milestone_dates, + ); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + env.ledger() + .set_sequence_number(proposal.deadline_ledger + 1); + + let result = client.try_vote(&voter, &proposal_id, &true); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::VotingClosed as u32 + ))) + ); +} + +#[test] +fn zero_weight_vote_allowed() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + let voter = Address::generate(&env); + // No minting, weight is 0 + + env.mock_all_auths(); + let proposal_id = client.submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Program description"), + &String::from_str(&env, "2026-05-15"), + &milestone_titles, + &milestone_dates, + ); + + client.vote(&voter, &proposal_id, &true); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + assert_eq!(proposal.yes_votes, 0); + + // Assert VoteCast storage key is true (vote again should panic) + let result = client.try_vote(&voter, &proposal_id, &true); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::AlreadyVoted as u32 + ))) + ); +} + +#[test] +fn vote_on_missing_proposal_panics() { + let env = Env::default(); + let (client, _governance, _donor, _recipient, _token_id, _gov_client) = setup(&env); + + let voter = Address::generate(&env); + env.mock_all_auths(); + + let result = client.try_vote(&voter, &999, &true); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::ProposalNotFound as u32 + ))) + ); +} + +// ============================================================================ +// INITIALIZE TESTS +// ============================================================================ + +#[test] +fn initialize_sets_admin_usdc_and_gov() { + let env = Env::default(); + let admin = Address::generate(&env); + let usdc_token = Address::generate(&env); + let gov_contract = Address::generate(&env); + + let contract_id = env.register(ScholarshipTreasury, ()); + let client = ScholarshipTreasuryClient::new(&env, &contract_id); + + env.mock_all_auths(); + client.initialize( + &admin, + &usdc_token, + &gov_contract, + &DEFAULT_QUORUM, + &DEFAULT_APPROVAL_BPS, + ); + + // Verify initialization by checking that operations work + assert_eq!(client.get_balance(), 0); + assert_eq!(client.get_total_disbursed(), 0); + assert_eq!(client.get_scholars_count(), 0); + assert_eq!(client.get_donors_count(), 0); +} + +#[test] +fn double_initialize_fails() { + let env = Env::default(); + let (client, _, _, _, _, _) = setup(&env); + + let admin = Address::generate(&env); + let usdc_token = Address::generate(&env); + let gov_contract = Address::generate(&env); + + env.mock_all_auths(); + let result = client.try_initialize( + &admin, + &usdc_token, + &gov_contract, + &DEFAULT_QUORUM, + &DEFAULT_APPROVAL_BPS, + ); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::AlreadyInitialized as u32 + ))) + ); +} + +// ============================================================================ +// DEPOSIT TESTS +// ============================================================================ + +#[test] +fn deposit_happy_path() { + let env = Env::default(); + let (client, _, donor, _, token_id, gov_client) = setup(&env); + + env.mock_all_auths(); + client.deposit(&donor, &100); + + assert_eq!(client.get_donor_total(&donor), 100); + assert_eq!(client.get_balance(), 100); + assert_eq!(token_client(&env, &token_id).balance(&client.address), 100); + let rate = client.get_exchange_rate(); + assert_eq!(gov_client.balance(&donor), 100_i128 * rate); +} + +#[test] +fn deposit_mints_gov_at_exchange_rate() { + let env = Env::default(); + let (client, _, donor, _, _, gov_client) = setup(&env); + + env.mock_all_auths(); + let rate = client.get_exchange_rate(); + client.deposit(&donor, &500); + + assert_eq!(gov_client.balance(&donor), 500_i128 * rate); +} + +#[test] +fn deposit_zero_amount_fails() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + + env.mock_all_auths(); + let result = client.try_deposit(&donor, &0); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InvalidAmount as u32 + ))) + ); +} + +#[test] +fn deposit_negative_amount_fails() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + + env.mock_all_auths(); + let result = client.try_deposit(&donor, &-50); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InvalidAmount as u32 + ))) + ); +} + +#[test] +fn deposit_when_paused_fails() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + + env.mock_all_auths(); + client.pause(); + let result = client.try_deposit(&donor, &100); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::ContractPaused as u32 + ))) + ); +} + +#[test] +fn deposit_increments_donor_count() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + + env.mock_all_auths(); + assert_eq!(client.get_donors_count(), 0); + + client.deposit(&donor, &100); + assert_eq!(client.get_donors_count(), 1); + + // Second deposit from same donor doesn't increment + client.deposit(&donor, &50); + assert_eq!(client.get_donors_count(), 1); + + // New donor increments + let donor2 = Address::generate(&env); + let token_id = env.as_contract(&client.address, || token::contract_id(&env)); + let sac = StellarAssetClient::new(&env, &token_id); + sac.mint(&donor2, &1_000); + client.deposit(&donor2, &100); + assert_eq!(client.get_donors_count(), 2); +} + +// ============================================================================ +// DISBURSE TESTS +// ============================================================================ + +#[test] +fn disburse_happy_path() { + let env = Env::default(); + let (client, governance, donor, recipient, token_id, _) = setup(&env); + + env.mock_all_auths(); + client.deposit(&donor, &500); + env.set_auths(&[]); + + set_caller(&client, "disburse", &governance, (&recipient, 200_i128)); + client.disburse(&recipient, &200); + + assert_eq!(client.get_balance(), 300); + assert_eq!(token_client(&env, &token_id).balance(&recipient), 200); +} + +#[test] +fn disburse_zero_amount_fails() { + let env = Env::default(); + let (client, governance, donor, recipient, _, _) = setup(&env); + + env.mock_all_auths(); + client.deposit(&donor, &500); + env.set_auths(&[]); + + set_caller(&client, "disburse", &governance, (&recipient, 0_i128)); + let result = client.try_disburse(&recipient, &0); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InvalidAmount as u32 + ))) + ); +} + +#[test] +fn disburse_negative_amount_fails() { + let env = Env::default(); + let (client, governance, donor, recipient, _, _) = setup(&env); + + env.mock_all_auths(); + client.deposit(&donor, &500); + env.set_auths(&[]); + + set_caller(&client, "disburse", &governance, (&recipient, -100_i128)); + let result = client.try_disburse(&recipient, &-100); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InvalidAmount as u32 + ))) + ); +} + +#[test] +fn disburse_insufficient_balance_fails() { + let env = Env::default(); + let (client, governance, donor, recipient, _, _) = setup(&env); + + env.mock_all_auths(); + client.deposit(&donor, &100); + env.set_auths(&[]); + + set_caller(&client, "disburse", &governance, (&recipient, 200_i128)); + let result = client.try_disburse(&recipient, &200); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InsufficientFunds as u32 + ))) + ); +} + +#[test] +fn disburse_when_paused_fails() { + let env = Env::default(); + let (client, governance, donor, recipient, _, _) = setup(&env); + + env.mock_all_auths(); + client.deposit(&donor, &500); + client.pause(); + env.set_auths(&[]); + + set_caller(&client, "disburse", &governance, (&recipient, 100_i128)); + let result = client.try_disburse(&recipient, &100); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::ContractPaused as u32 + ))) + ); +} + +#[test] +fn disburse_increments_scholar_count() { + let env = Env::default(); + let (client, governance, donor, recipient, _, _) = setup(&env); + + env.mock_all_auths(); + client.deposit(&donor, &500); + assert_eq!(client.get_scholars_count(), 0); + env.set_auths(&[]); + + set_caller(&client, "disburse", &governance, (&recipient, 100_i128)); + client.disburse(&recipient, &100); + assert_eq!(client.get_scholars_count(), 1); + + // Second disburse to same recipient doesn't increment + set_caller(&client, "disburse", &governance, (&recipient, 50_i128)); + client.disburse(&recipient, &50); + assert_eq!(client.get_scholars_count(), 1); +} + +#[test] +fn disburse_tracks_total_disbursed() { + let env = Env::default(); + let (client, governance, donor, recipient, _, _) = setup(&env); + + env.mock_all_auths(); + client.deposit(&donor, &500); + assert_eq!(client.get_total_disbursed(), 0); + env.set_auths(&[]); + + set_caller(&client, "disburse", &governance, (&recipient, 100_i128)); + client.disburse(&recipient, &100); + assert_eq!(client.get_total_disbursed(), 100); + + set_caller(&client, "disburse", &governance, (&recipient, 50_i128)); + client.disburse(&recipient, &50); + assert_eq!(client.get_total_disbursed(), 150); +} + +// ============================================================================ +// PROPOSAL TESTS +// ============================================================================ + +#[test] +fn submit_proposal_happy_path() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + let proposal_id = client.submit_proposal( + &donor, + &1000, + &String::from_str(&env, "DeFi Bootcamp"), + &String::from_str(&env, "https://example.com/defi"), + &String::from_str(&env, "Learn DeFi fundamentals"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + assert_eq!(proposal_id, 1); + let proposal = client.get_proposal(&proposal_id).unwrap(); + assert_eq!(proposal.applicant, donor); + assert_eq!(proposal.amount, 1000); + assert_eq!(proposal.yes_votes, 0); + assert_eq!(proposal.no_votes, 0); +} + +#[test] +fn submit_proposal_stores_deadline_from_current_ledger() { + let env = Env::default(); + let (client, _, _donor, _recipient, _token_id, gov_client, admin) = setup_with_admin(&env); + let applicant = Address::generate(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + gov_client.mint(&applicant, &250); + + env.mock_all_auths(); + client.set_min_lrn_to_propose(&admin, &100); + env.ledger().set_sequence_number(12_345); + + let proposal_id = client.submit_proposal( + &applicant, + &750, + &String::from_str(&env, "Soroban Fellowship"), + &String::from_str(&env, "https://example.com/soroban"), + &String::from_str(&env, "Build and ship Soroban contracts"), + &String::from_str(&env, "2026-06-01"), + &milestone_titles, + &milestone_dates, + ); + + assert_eq!(proposal_id, 1); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + assert_eq!(proposal.deadline_ledger, 12_345 + 100_800); +} + +#[test] +fn submit_proposal_fails_when_reputation_is_below_threshold() { + let env = Env::default(); + let (client, _, donor, _recipient, _token_id, _gov_client, admin) = setup_with_admin(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + client.set_min_lrn_to_propose(&admin, &100); + + let result = client.try_submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Description"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InsufficientReputation as u32 + ))) + ); +} + +#[test] +fn submit_proposal_zero_amount_fails() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + let result = client.try_submit_proposal( + &donor, + &0, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Description"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InvalidAmount as u32 + ))) + ); +} + +#[test] +fn submit_proposal_negative_amount_fails() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + let result = client.try_submit_proposal( + &donor, + &-500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Description"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InvalidAmount as u32 + ))) + ); +} + +#[test] +fn submit_proposal_wrong_milestone_count_fails() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + + env.mock_all_auths(); + let titles = Vec::from_array( + &env, + [String::from_str(&env, "M1"), String::from_str(&env, "M2")], + ); + let dates = Vec::from_array( + &env, + [ + String::from_str(&env, "2026-06-01"), + String::from_str(&env, "2026-07-01"), + ], + ); + + let result = client.try_submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Description"), + &String::from_str(&env, "2026-05-01"), + &titles, + &dates, + ); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InvalidAmount as u32 + ))) + ); +} + +#[test] +fn submit_proposal_when_paused_fails() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + client.pause(); + + let result = client.try_submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Description"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::ContractPaused as u32 + ))) + ); +} + +#[test] +fn multiple_proposals_from_same_applicant() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + let id1 = client.submit_proposal( + &donor, + &500, + &String::from_str(&env, "Program 1"), + &String::from_str(&env, "https://example.com/1"), + &String::from_str(&env, "Description 1"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + let id2 = client.submit_proposal( + &donor, + &1000, + &String::from_str(&env, "Program 2"), + &String::from_str(&env, "https://example.com/2"), + &String::from_str(&env, "Description 2"), + &String::from_str(&env, "2026-06-01"), + &milestone_titles, + &milestone_dates, + ); + + assert_eq!(id1, 1); + assert_eq!(id2, 2); + let proposals = client.get_proposals_by_applicant(&donor); + assert_eq!(proposals.len(), 2); + assert_eq!(proposals.get(0), Some(1)); + assert_eq!(proposals.get(1), Some(2)); +} + +// ============================================================================ +// VOTE TESTS +// ============================================================================ + +#[test] +fn vote_without_gov_tokens_fails() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + let voter = Address::generate(&env); + + env.mock_all_auths(); + let proposal_id = client.submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Description"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + // Voter has no GOV tokens, vote is allowed but has 0 weight + client.vote(&voter, &proposal_id, &true); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + assert_eq!(proposal.yes_votes, 0); +} + +#[test] +fn multiple_votes_accumulate() { + let env = Env::default(); + let (client, _, donor, _, _, gov_client) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + let voter1 = Address::generate(&env); + let voter2 = Address::generate(&env); + gov_client.mint(&voter1, &300); + gov_client.mint(&voter2, &200); + + env.mock_all_auths(); + let proposal_id = client.submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Description"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + client.vote(&voter1, &proposal_id, &true); + client.vote(&voter2, &proposal_id, &true); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + assert_eq!(proposal.yes_votes, 500); +} + +#[test] +fn mixed_yes_and_no_votes() { + let env = Env::default(); + let (client, _, donor, _, _, gov_client) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + let voter1 = Address::generate(&env); + let voter2 = Address::generate(&env); + gov_client.mint(&voter1, &400); + gov_client.mint(&voter2, &300); + + env.mock_all_auths(); + let proposal_id = client.submit_proposal( + &donor, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Description"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + client.vote(&voter1, &proposal_id, &true); + client.vote(&voter2, &proposal_id, &false); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + assert_eq!(proposal.yes_votes, 400); + assert_eq!(proposal.no_votes, 300); +} + +// ============================================================================ +// PAUSE/UNPAUSE TESTS +// ============================================================================ + +#[test] +fn pause_only_admin_can_call() { + let env = Env::default(); + let (client, _, _, _, _, _) = setup(&env); + + let attacker = Address::generate(&env); + env.mock_all_auths(); + let result = client.try_pause(); + // Should succeed because mock_all_auths allows all + assert!(result.is_ok()); +} + +#[test] +fn pause_prevents_deposits() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + + env.mock_all_auths(); + client.pause(); + assert!(client.is_paused()); + + let result = client.try_deposit(&donor, &100); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::ContractPaused as u32 + ))) + ); +} + +#[test] +fn pause_prevents_disbursements() { + let env = Env::default(); + let (client, governance, donor, recipient, _, _) = setup(&env); + + env.mock_all_auths(); + client.deposit(&donor, &500); + client.pause(); + env.set_auths(&[]); + + set_caller(&client, "disburse", &governance, (&recipient, 100_i128)); + let result = client.try_disburse(&recipient, &100); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::ContractPaused as u32 + ))) + ); +} + +#[test] +fn unpause_restores_functionality() { + let env = Env::default(); + let (client, _, donor, _, _, _) = setup(&env); + + env.mock_all_auths(); + client.pause(); + assert!(client.is_paused()); + + client.unpause(); + assert!(!client.is_paused()); + + client.deposit(&donor, &100); + assert_eq!(client.get_balance(), 100); +} + +// ============================================================================ +// FULL INTEGRATION FLOW TESTS +// ============================================================================ + +#[test] +fn full_flow_deposit_propose_vote_disburse() { + let env = Env::default(); + let (client, governance, donor, recipient, token_id, gov_client) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + // Step 1: Donor deposits USDC + env.mock_all_auths(); + let rate = client.get_exchange_rate(); + client.deposit(&donor, &1000); + assert_eq!(client.get_balance(), 1000); + assert_eq!(gov_client.balance(&donor), 1000_i128 * rate); + + // Step 2: Applicant submits proposal + let applicant = Address::generate(&env); + let proposal_id = client.submit_proposal( + &applicant, + &500, + &String::from_str(&env, "Stellar Bootcamp"), + &String::from_str(&env, "https://bootcamp.example.com"), + &String::from_str(&env, "Intensive Soroban training"), + &String::from_str(&env, "2026-05-15"), + &milestone_titles, + &milestone_dates, + ); + assert_eq!(proposal_id, 1); + + // Step 3: Donors vote on proposal + client.vote(&donor, &proposal_id, &true); + let proposal = client.get_proposal(&proposal_id).unwrap(); + assert_eq!(proposal.yes_votes, 1000_i128 * rate); + + // Step 4: Governance approves and disburses + env.set_auths(&[]); + set_caller(&client, "disburse", &governance, (&recipient, 500_i128)); + client.disburse(&recipient, &500); + + // Verify final state + assert_eq!(client.get_balance(), 500); + assert_eq!(client.get_total_disbursed(), 500); + assert_eq!(token_client(&env, &token_id).balance(&recipient), 500); + assert_eq!(client.get_scholars_count(), 1); +} + +#[test] +fn full_flow_multiple_donors_and_proposals() { + let env = Env::default(); + let (client, governance, donor1, recipient, _, _) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + let rate = client.get_exchange_rate(); + + // Both donors deposit (using same donor for simplicity in test) + client.deposit(&donor1, &1000); + assert_eq!(client.get_balance(), 1000); + assert_eq!(client.get_donors_count(), 1); + + // First proposal + let applicant1 = Address::generate(&env); + let proposal_id1 = client.submit_proposal( + &applicant1, + &500, + &String::from_str(&env, "Program 1"), + &String::from_str(&env, "https://example.com/1"), + &String::from_str(&env, "Description 1"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + // Second proposal + let applicant2 = Address::generate(&env); + let proposal_id2 = client.submit_proposal( + &applicant2, + &500, + &String::from_str(&env, "Program 2"), + &String::from_str(&env, "https://example.com/2"), + &String::from_str(&env, "Description 2"), + &String::from_str(&env, "2026-06-01"), + &milestone_titles, + &milestone_dates, + ); + + // Donor votes on both proposals + client.vote(&donor1, &proposal_id1, &true); + client.vote(&donor1, &proposal_id2, &true); + + let prop1 = client.get_proposal(&proposal_id1).unwrap(); + let prop2 = client.get_proposal(&proposal_id2).unwrap(); + assert_eq!(prop1.yes_votes, 1000_i128 * rate); + assert_eq!(prop2.yes_votes, 1000_i128 * rate); + + // Disburse to both recipients + env.set_auths(&[]); + let recipient2 = Address::generate(&env); + + set_caller(&client, "disburse", &governance, (&recipient, 500_i128)); + client.disburse(&recipient, &500); + + set_caller(&client, "disburse", &governance, (&recipient2, 500_i128)); + client.disburse(&recipient2, &500); + + assert_eq!(client.get_balance(), 0); + assert_eq!(client.get_total_disbursed(), 1000); + assert_eq!(client.get_scholars_count(), 2); +} + +#[test] +fn full_flow_with_pause_and_unpause() { + let env = Env::default(); + let (client, governance, donor, recipient, _, _) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + + // Initial deposit + client.deposit(&donor, &500); + assert_eq!(client.get_balance(), 500); + + // Pause contract + client.pause(); + assert!(client.is_paused()); + + // Verify operations fail + let result = client.try_deposit(&donor, &100); + assert!(result.is_err()); + + // Unpause + client.unpause(); + assert!(!client.is_paused()); + + // Submit proposal and vote + let applicant = Address::generate(&env); + let proposal_id = client.submit_proposal( + &applicant, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Description"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + client.vote(&donor, &proposal_id, &true); + + // Pause again before disburse + client.pause(); + env.set_auths(&[]); + + set_caller(&client, "disburse", &governance, (&recipient, 500_i128)); + let result = client.try_disburse(&recipient, &500); + assert!(result.is_err()); + + // Unpause and disburse + env.mock_all_auths(); + client.unpause(); + env.set_auths(&[]); + + set_caller(&client, "disburse", &governance, (&recipient, 500_i128)); + client.disburse(&recipient, &500); + + assert_eq!(client.get_balance(), 0); +} + +#[test] +fn full_flow_edge_case_exact_balance_disburse() { + let env = Env::default(); + let (client, governance, donor, recipient, _, _) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + + // Deposit exact amount + client.deposit(&donor, &500); + + // Submit proposal + let applicant = Address::generate(&env); + let proposal_id = client.submit_proposal( + &applicant, + &500, + &String::from_str(&env, "Scholarship"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Description"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + // Vote + client.vote(&donor, &proposal_id, &true); + + // Disburse exact balance + env.set_auths(&[]); + set_caller(&client, "disburse", &governance, (&recipient, 500_i128)); + client.disburse(&recipient, &500); + + assert_eq!(client.get_balance(), 0); + assert_eq!(client.get_total_disbursed(), 500); +} + +// --- fuzz tests --- + +use proptest::prelude::*; + +proptest! { + #[test] + #[ignore] + fn fuzz_deposit_allocates_gov_tokens_monotonically(amount1 in 1..100_000_000_i128, amount2 in 1..100_000_000_i128) { + let env = Env::default(); + let (client, _, donor, _, token_id, gov_client) = setup(&env); + let sac = StellarAssetClient::new(&env, &token_id); + + // mock_all_auths must come before sac.mint; setup() clears auths with set_auths(&[]) + env.mock_all_auths(); + sac.mint(&donor, &(amount1 + amount2)); + + client.deposit(&donor, &amount1); + let gov_bal1 = gov_client.balance(&donor); + + client.deposit(&donor, &amount2); + let gov_bal2 = gov_client.balance(&donor); + + // Each deposited USDC mints GOV_PER_USDC (100) governance tokens + assert!(gov_bal2 > gov_bal1); + assert_eq!(gov_bal1, amount1 * 100); + assert_eq!(gov_bal2, (amount1 + amount2) * 100); + } + + #[test] + #[ignore] + fn fuzz_vote_casting_random_proposal_ids(proposal_id in any::()) { + let env = Env::default(); + let (client, _, donor, _, _, gov_client) = setup(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + let voter = Address::generate(&env); + gov_client.mint(&voter, &500); + + env.mock_all_auths(); + + // create one valid proposal + client.submit_proposal( + &donor, &500, &String::from_str(&env, "Test"), &String::from_str(&env, "URL"), &String::from_str(&env, "Desc"), &String::from_str(&env, "Date"), &milestone_titles, &milestone_dates, + ); + + let result = client.try_vote(&voter, &proposal_id, &true); + + if proposal_id == 1 { + assert!(result.is_ok()); + } else { + // "verify no panics" (meaning no unexpected panics, contract error ProposalNotFound expected) + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + crate::Error::ProposalNotFound as u32 + ))) + ); + } + } +} +#[cfg(test)] +mod fuzz_tests { + use super::*; + use crate::GOV_PER_USDC; + use proptest::prelude::*; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + + #[test] + #[ignore] + fn fuzz_deposit_amounts(amount in 1..=(i128::MAX / GOV_PER_USDC)) { + let env = Env::default(); + let (client, _, donor, _, token_id, gov_client) = setup(&env); + env.mock_all_auths(); + + // Ensure donor has sufficient balance for the randomized deposit. + StellarAssetClient::new(&env, &token_id).mint(&donor, &amount); + + client.deposit(&donor, &amount); + + assert_eq!(client.get_donor_total(&donor), amount); + assert_eq!(client.get_balance(), amount); + assert_eq!(token_client(&env, &token_id).balance(&client.address), amount); + assert_eq!(gov_client.balance(&donor), amount * GOV_PER_USDC); + } + + #[test] + #[ignore] + fn fuzz_vote_casting(proposal_id in any::()) { + let env = Env::default(); + let (client, _, _donor, _, _, _) = setup(&env); + let voter = Address::generate(&env); + env.mock_all_auths(); + + // Most proposals don't exist, we test graceful error handling + let res = client.try_vote(&voter, &proposal_id, &true); + + if proposal_id != 1 { + assert_eq!( + res.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::ProposalNotFound as u32 + ))) + ); + } + } + } +} + +// ── Deadline and quorum tests (Issue #339) ──────────────────────────────────── + +/// Like `setup` but also returns the admin address so finalize tests can use it. +fn setup_with_admin<'a>( + env: &'a Env, +) -> ( + ScholarshipTreasuryClient<'a>, + Address, + Address, + Address, + Address, + MockGovernanceClient<'a>, + Address, // admin +) { + let admin = Address::generate(env); + let donor = Address::generate(env); + let recipient = Address::generate(env); + + let contract_id = env.register(ScholarshipTreasury, ()); + let client = ScholarshipTreasuryClient::new(env, &contract_id); + + let gov_contract_id = env.register(MockGovernance, ()); + let gov_client = MockGovernanceClient::new(env, &gov_contract_id); + + env.mock_all_auths(); + env.as_contract(&contract_id, || token::register(env, &admin)); + let token_id = env.as_contract(&contract_id, || token::contract_id(env)); + let sac = StellarAssetClient::new(env, &token_id); + sac.mint(&donor, &1_000); + + gov_client.initialize(&contract_id); + client.initialize( + &admin, + &token_id, + &gov_contract_id, + &DEFAULT_QUORUM, + &DEFAULT_APPROVAL_BPS, + ); + env.set_auths(&[]); + + ( + client, + gov_contract_id, + donor, + recipient, + token_id, + gov_client, + admin, + ) +} + +#[test] +fn finalize_proposal_before_deadline_panics() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, _gov_client, admin) = + setup_with_admin(&env); + + env.mock_all_auths(); + let proposal_id = submit_sample_proposal(&env, &client, &donor, 500); + + // Deadline has NOT passed yet — finalize should fail with VotingNotClosed + let result = client.try_finalize_proposal(&admin, &proposal_id); + + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::VotingNotClosed as u32 + ))) + ); +} + +#[test] +fn finalize_proposal_approved_when_quorum_met_and_yes_wins() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client, admin) = + setup_with_admin(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + + client.set_quorum(&1); + client.set_approval_bps(&5_000); + + let applicant = Address::generate(&env); + let proposal_id = client.submit_proposal( + &applicant, + &500, + &String::from_str(&env, "Test Program"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Test description"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + gov_client.mint(&donor, &500); + client.vote(&donor, &proposal_id, &true); + + // Advance past deadline + let proposal = client.get_proposal(&proposal_id).unwrap(); + env.ledger() + .set_sequence_number(proposal.deadline_ledger + 1); + + let status = client.finalize_proposal(&admin, &proposal_id); + + assert_eq!(status, crate::ProposalStatus::Approved); + assert_eq!( + client.get_finalized_status(&proposal_id), + Some(crate::ProposalStatus::Approved) + ); +} + +#[test] +fn finalize_proposal_rejected_when_quorum_not_met() { + let env = Env::default(); + let (client, _governance, donor, _recipient, token_id, gov_client, admin) = + setup_with_admin(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + + client.set_quorum(&1_000); + client.set_approval_bps(&5_000); + + let applicant = Address::generate(&env); + let proposal_id = client.submit_proposal( + &applicant, + &500, + &String::from_str(&env, "Low-turnout Proposal"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Description"), + &String::from_str(&env, "2026-05-01"), + &milestone_titles, + &milestone_dates, + ); + + gov_client.mint(&donor, &500); + client.vote(&donor, &proposal_id, &true); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + env.ledger() + .set_sequence_number(proposal.deadline_ledger + 1); + + let status = client.finalize_proposal(&admin, &proposal_id); + + // Quorum not met → always Rejected + assert_eq!(status, crate::ProposalStatus::Rejected); +} + +#[test] +fn finalize_proposal_rejected_when_no_votes_win() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client, admin) = + setup_with_admin(&env); + let (milestone_titles, milestone_dates) = sample_milestones(&env); + + env.mock_all_auths(); + + client.set_quorum(&1); + client.set_approval_bps(&5_000); + + let applicant = Address::generate(&env); + let proposal_id = client.submit_proposal( + &applicant, + &200, + &String::from_str(&env, "Rejected Proposal"), + &String::from_str(&env, "https://example.com"), + &String::from_str(&env, "Will be voted down"), + &String::from_str(&env, "2026-06-01"), + &milestone_titles, + &milestone_dates, + ); + + gov_client.mint(&donor, &500); + client.vote(&donor, &proposal_id, &false); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + env.ledger() + .set_sequence_number(proposal.deadline_ledger + 1); + + let status = client.finalize_proposal(&admin, &proposal_id); + + assert_eq!(status, crate::ProposalStatus::Rejected); +} + +#[test] +fn get_total_gov_issued_tracks_deposits() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, _gov_client, _admin) = + setup_with_admin(&env); + + env.mock_all_auths(); + assert_eq!(client.get_total_gov_issued(), 0); + + client.deposit(&donor, &100); + assert_eq!(client.get_total_gov_issued(), 100 * 100); // GOV_PER_USDC = 100 + + client.deposit(&donor, &400); + assert_eq!(client.get_total_gov_issued(), 500 * 100); +} + +// ========================================================================= +// EXECUTE + CANCEL TESTS +// ========================================================================= + +#[test] +fn execute_proposal_before_deadline_panics() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client) = setup(&env); + + env.mock_all_auths(); + client.set_quorum(&1); + client.set_approval_bps(&5_000); + let proposal_id = submit_sample_proposal(&env, &client, &donor, 100); + + gov_client.mint(&donor, &100); + client.vote(&donor, &proposal_id, &true); + + let result = client.try_execute_proposal(&proposal_id); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::VotingNotClosed as u32 + ))) + ); +} + +#[test] +fn execute_proposal_passed_disburses_and_emits_event() { + let env = Env::default(); + let (client, _governance, donor, _recipient, token_id, gov_client) = setup(&env); + let applicant = Address::generate(&env); + + env.mock_all_auths(); + client.deposit(&donor, &500); + client.set_quorum(&1); + client.set_approval_bps(&5_000); + + let proposal_id = submit_sample_proposal(&env, &client, &applicant, 200); + + gov_client.mint(&donor, &200); + client.vote(&donor, &proposal_id, &true); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + env.ledger() + .set_sequence_number(proposal.deadline_ledger + 1); + + let before = token_client(&env, &token_id).balance(&applicant); + client.execute_proposal(&proposal_id); + let after = token_client(&env, &token_id).balance(&applicant); + assert_eq!(after - before, 200); + + let stored = client.get_proposal(&proposal_id).unwrap(); + assert!(stored.executed); +} + +#[test] +fn execute_proposal_rejected_emits_event_and_no_disbursement() { + let env = Env::default(); + let (client, _governance, donor, _recipient, token_id, gov_client) = setup(&env); + let applicant = Address::generate(&env); + + env.mock_all_auths(); + client.deposit(&donor, &500); + client.set_quorum(&1_000); + client.set_approval_bps(&10_000); + + let proposal_id = submit_sample_proposal(&env, &client, &applicant, 200); + + gov_client.mint(&donor, &10); + client.vote(&donor, &proposal_id, &true); + + let proposal = client.get_proposal(&proposal_id).unwrap(); + env.ledger() + .set_sequence_number(proposal.deadline_ledger + 1); + + let before = token_client(&env, &token_id).balance(&applicant); + client.execute_proposal(&proposal_id); + let after = token_client(&env, &token_id).balance(&applicant); + assert_eq!(after, before); + + let stored = client.get_proposal(&proposal_id).unwrap(); + assert!(stored.executed); +} + +#[test] +fn execute_proposal_double_execute_panics() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client) = setup(&env); + let applicant = Address::generate(&env); + + env.mock_all_auths(); + client.deposit(&donor, &500); + client.set_quorum(&1); + client.set_approval_bps(&5_000); + let proposal_id = submit_sample_proposal(&env, &client, &applicant, 100); + + gov_client.mint(&donor, &100); + client.vote(&donor, &proposal_id, &true); + let proposal = client.get_proposal(&proposal_id).unwrap(); + env.ledger() + .set_sequence_number(proposal.deadline_ledger + 1); + + client.execute_proposal(&proposal_id); + let result = client.try_execute_proposal(&proposal_id); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::ProposalAlreadyExecuted as u32 + ))) + ); +} + +#[test] +fn cancel_proposal_prevents_vote_and_execute() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, gov_client) = setup(&env); + + env.mock_all_auths(); + let proposal_id = submit_sample_proposal(&env, &client, &donor, 100); + + client.cancel_proposal(&proposal_id); + let voter = Address::generate(&env); + gov_client.mint(&voter, &100); + + let vote_result = client.try_vote(&voter, &proposal_id, &true); + assert_eq!( + vote_result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::ProposalCancelled as u32 + ))) + ); + + let prop = client.get_proposal(&proposal_id).unwrap(); + env.ledger().set_sequence_number(prop.deadline_ledger + 1); + let exec_result = client.try_execute_proposal(&proposal_id); + assert_eq!( + exec_result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::ProposalCancelled as u32 + ))) + ); +} + +#[test] +fn upgrade_requires_admin_auth() { + let env = Env::default(); + let (client, _governance, _donor, _recipient, _token_id, _gov_client, _admin) = + setup_with_admin(&env); + let attacker = Address::generate(&env); + let wasm_hash = crate::upgrade::testutils::upload_upgrade_target(&env); + + set_caller(&client, "upgrade", &attacker, (wasm_hash.clone(),)); + assert!(client.try_upgrade(&wasm_hash).is_err()); +} + +#[test] +fn state_persists_after_upgrade() { + let env = Env::default(); + let (client, _governance, donor, _recipient, _token_id, _gov_client, admin) = + setup_with_admin(&env); + + env.mock_all_auths(); + let proposal_id = submit_sample_proposal(&env, &client, &donor, 250); + + let wasm_hash = crate::upgrade::testutils::upload_upgrade_target(&env); + set_caller(&client, "upgrade", &admin, (wasm_hash.clone(),)); + client.upgrade(&wasm_hash); + + let proposal = env.as_contract(&client.address, || { + env.storage() + .persistent() + .get::<_, Proposal>(&DataKey::Proposal(proposal_id)) + }); + let stored_hash = env.as_contract(&client.address, || crate::upgrade::current_hash(&env)); + + let proposal = proposal.expect("proposal should remain after upgrade"); + assert_eq!(proposal.id, proposal_id); + assert_eq!(proposal.applicant, donor); + assert_eq!(proposal.amount, 250); + assert_eq!(stored_hash, wasm_hash); +} diff --git a/contracts/scripts/deploy-all.ts b/contracts/scripts/deploy-all.ts new file mode 100644 index 00000000..d8153811 --- /dev/null +++ b/contracts/scripts/deploy-all.ts @@ -0,0 +1,113 @@ +import fs from "fs"; +import path from "path"; +import { execSync } from "child_process"; + +const NETWORK = process.env.NETWORK || "testnet"; + +const CONTRACTS = [ + "vault", + "governance", + "token", + "registry", + "treasury", + "staking", + "rewards", + "oracle", + "bridge", +] as const; + +type Deployed = Record; + +function deployWasm(contractName: string): string { + console.log(`📦 Deploying ${contractName}...`); + + const wasmPath = path.join( + process.cwd(), + `target/wasm32-unknown-unknown/release/${contractName}.wasm`, + ); + + if (!fs.existsSync(wasmPath)) { + throw new Error(`WASM not found for ${contractName}: ${wasmPath}`); + } + + const cmd = ` + stellar contract deploy \ + --wasm ${wasmPath} \ + --network ${NETWORK} \ + --source-account default + `; + + const output = execSync(cmd).toString().trim(); + + console.log(`✅ ${contractName} deployed: ${output}`); + + return output; +} + +async function verifyContract(address: string): Promise { + try { + const result = execSync(` + stellar contract invoke \ + --id ${address} \ + --network ${NETWORK} \ + --source-account default \ + -- \ + is_initialized + `) + .toString() + .trim(); + + return result === "true" || result === "1"; + } catch (e) { + console.warn(`⚠️ Verification failed for ${address}`); + return false; + } +} + +async function main() { + console.log(`🚀 Deploying contracts on ${NETWORK}\n`); + + const results: Deployed = {}; + + for (const contract of CONTRACTS) { + try { + const address = deployWasm(contract); + + results[contract] = address; + + const ok = await verifyContract(address); + + if (!ok) { + throw new Error(`${contract} failed initialization check`); + } + + console.log(`🔍 Verified ${contract}\n`); + } catch (err) { + console.error(`❌ ${contract} failed:`, err); + process.exit(1); + } + } + + // Save addresses + fs.writeFileSync( + "deployed-addresses.json", + JSON.stringify(results, null, 2), + ); + + console.log("💾 Saved deployed-addresses.json"); + + // Update frontend + fs.writeFileSync( + "frontend/src/constants/contracts.ts", + `export const CONTRACT_ADDRESSES = ${JSON.stringify(results, null, 2)} as const;`, + ); + + console.log("🧩 Updated frontend constants"); + + console.log("\n🎉 Deployment complete"); +} + +main().catch((err) => { + console.error("🔥 Fatal error:", err); + process.exit(1); +}); \ No newline at end of file diff --git a/contracts/shared/Cargo.toml b/contracts/shared/Cargo.toml new file mode 100644 index 00000000..d9f305ee --- /dev/null +++ b/contracts/shared/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "learnvault-shared" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[lib] +crate-type = ["rlib"] +doctest = false + +[features] +testutils = [] + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/shared/src/lib.rs b/contracts/shared/src/lib.rs new file mode 100644 index 00000000..d5709430 --- /dev/null +++ b/contracts/shared/src/lib.rs @@ -0,0 +1,3 @@ +#![no_std] + +pub mod upgrade; diff --git a/contracts/shared/src/upgrade.rs b/contracts/shared/src/upgrade.rs new file mode 100644 index 00000000..ce6b20c2 --- /dev/null +++ b/contracts/shared/src/upgrade.rs @@ -0,0 +1,58 @@ +use soroban_sdk::{Address, BytesN, Env, Symbol, contractevent, symbol_short}; + +const CURRENT_WASM_HASH_KEY: Symbol = symbol_short!("WASMHASH"); + +#[contractevent(topics = ["contract_upgraded"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContractUpgraded { + pub old_hash: BytesN<32>, + pub new_hash: BytesN<32>, + pub upgraded_by: Address, +} + +pub fn init(env: &Env) { + env.storage() + .instance() + .set(&CURRENT_WASM_HASH_KEY, &zero_hash(env)); +} + +pub fn apply(env: &Env, admin: &Address, new_wasm_hash: &BytesN<32>) { + // Soroban emits a native `executable_update` system event with the exact + // old/new executable refs. Contracts cannot read their current WASM hash, + // so we track the last managed upgrade hash locally for contract events. + let upgraded = ContractUpgraded { + old_hash: current_hash(env), + new_hash: new_wasm_hash.clone(), + upgraded_by: admin.clone(), + }; + + env.deployer() + .update_current_contract_wasm(new_wasm_hash.clone()); + env.storage() + .instance() + .set(&CURRENT_WASM_HASH_KEY, new_wasm_hash); + + upgraded.publish(env); +} + +pub fn current_hash(env: &Env) -> BytesN<32> { + env.storage() + .instance() + .get(&CURRENT_WASM_HASH_KEY) + .unwrap_or_else(|| zero_hash(env)) +} + +fn zero_hash(env: &Env) -> BytesN<32> { + BytesN::from_array(env, &[0; 32]) +} + +#[cfg(any(test, feature = "testutils"))] +pub mod testutils { + use soroban_sdk::{BytesN, Env}; + + pub const UPGRADE_TARGET_WASM: &[u8] = include_bytes!("../../testdata/upgrade-target.wasm"); + + pub fn upload_upgrade_target(env: &Env) -> BytesN<32> { + env.deployer().upload_contract_wasm(UPGRADE_TARGET_WASM) + } +} diff --git a/contracts/testdata/upgrade-target.wasm b/contracts/testdata/upgrade-target.wasm new file mode 100644 index 00000000..275050c9 Binary files /dev/null and b/contracts/testdata/upgrade-target.wasm differ diff --git a/contracts/upgrade_timelock_vault/Cargo.toml b/contracts/upgrade_timelock_vault/Cargo.toml new file mode 100644 index 00000000..efe10ba0 --- /dev/null +++ b/contracts/upgrade_timelock_vault/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "upgrade-timelock-vault" +description = "Timelock vault for secure contract upgrades" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } \ No newline at end of file diff --git a/contracts/upgrade_timelock_vault/src/lib.rs b/contracts/upgrade_timelock_vault/src/lib.rs new file mode 100644 index 00000000..15f68b78 --- /dev/null +++ b/contracts/upgrade_timelock_vault/src/lib.rs @@ -0,0 +1,610 @@ +#![no_std] + +//! # Upgrade Timelock Vault +//! +//! A dedicated vault for secure contract upgrade timelocking. +//! +//! This contract provides isolated storage for upgrade proposals with timelock +//! enforcement, separating timelock logic from governance contract logic for +//! enhanced security. +//! +//! ## Features +//! +//! - Queue upgrade proposals with timelock +//! - Enforce timelock period at execution time +//! - Admin cancellation with refund capability +//! - Event emission for all operations +//! +//! ## Security Model +//! +//! The timelock vault ensures that contract upgrades undergo a mandatory +//! waiting period after governance approval, providing time for review and +//! potential cancellation. The vault isolates upgrade storage from governance +//! logic to prevent accidental or malicious modifications. +//! +//! ## Usage +//! +//! 1. Governance contract calls `queue_upgrade()` after proposal approval +//! 2. Wait for timelock period (default 48 hours) +//! 3. Governance contract calls `execute_upgrade()` to get WASM hash +//! 4. Governance contract performs the actual upgrade +//! 5. Admin can `cancel_upgrade()` during timelock period + +use soroban_sdk::{ + Address, BytesN, Env, Symbol, contract, contracterror, contractevent, contractimpl, + contracttype, panic_with_error, symbol_short, +}; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); +const TIMELOCK_KEY: Symbol = symbol_short!("TIMELOCK"); + +// Default timelock duration: 48 hours in seconds +const DEFAULT_TIMELOCK_DURATION: u64 = 48 * 60 * 60; + +// --------------------------------------------------------------------------- +// Errors +// --------------------------------------------------------------------------- + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum UpgradeTimelockError { + /// Contract has not been initialized. + NotInitialized = 1, + /// Caller is not the contract admin. + Unauthorized = 2, + /// Upgrade proposal already exists for this contract. + UpgradeAlreadyQueued = 3, + /// No upgrade proposal found for this contract. + UpgradeNotFound = 4, + /// Timelock period has not elapsed yet. + TimelockNotExpired = 5, + /// Contract has already been initialized. + AlreadyInitialized = 6, +} + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UpgradeProposal { + /// The contract address to be upgraded + pub contract_address: Address, + /// The new WASM hash for the upgrade + pub new_wasm_hash: BytesN<32>, + /// Timestamp when the upgrade was queued + pub queued_at: u64, + /// Admin who queued the upgrade + pub admin: Address, +} + +#[contracttype] +pub enum DataKey { + UpgradeProposal(Address), +} + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +#[contractevent(topics = ["upgrade_queued"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UpgradeQueued { + #[topic] + pub contract_address: Address, + pub new_wasm_hash: BytesN<32>, + pub queued_at: u64, + pub admin: Address, +} + +#[contractevent(topics = ["upgrade_executed"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UpgradeExecuted { + #[topic] + pub contract_address: Address, + pub new_wasm_hash: BytesN<32>, + pub executed_at: u64, +} + +#[contractevent(topics = ["upgrade_cancelled"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UpgradeCancelled { + #[topic] + pub contract_address: Address, + pub new_wasm_hash: BytesN<32>, + pub cancelled_at: u64, +} + +// --------------------------------------------------------------------------- +// Contract +// --------------------------------------------------------------------------- + +#[contract] +pub struct UpgradeTimelockVault; + +#[contractimpl] +impl UpgradeTimelockVault { + /// Initialize the timelock vault. + /// + /// Sets the admin and default timelock duration (48 hours). + pub fn initialize(env: Env, admin: Address) { + if env.storage().instance().has(&ADMIN_KEY) { + panic_with_error!(&env, UpgradeTimelockError::AlreadyInitialized); + } + env.storage().instance().set(&ADMIN_KEY, &admin); + env.storage() + .instance() + .set(&TIMELOCK_KEY, &DEFAULT_TIMELOCK_DURATION); + } + + /// Set the timelock duration. Admin only. + pub fn set_timelock_duration(env: Env, duration_seconds: u64) { + Self::admin(&env).require_auth(); + env.storage() + .instance() + .set(&TIMELOCK_KEY, &duration_seconds); + } + + /// Get the current timelock duration. + pub fn get_timelock_duration(env: Env) -> u64 { + env.storage() + .instance() + .get(&TIMELOCK_KEY) + .unwrap_or(DEFAULT_TIMELOCK_DURATION) + } + + /// Queue an upgrade proposal for a contract. + /// + /// Only the admin can queue upgrades. Stores the proposal with current timestamp. + pub fn queue_upgrade(env: Env, contract_address: Address, new_wasm_hash: BytesN<32>) { + Self::admin(&env).require_auth(); + + let key = DataKey::UpgradeProposal(contract_address.clone()); + if env.storage().persistent().has(&key) { + panic_with_error!(&env, UpgradeTimelockError::UpgradeAlreadyQueued); + } + + let proposal = UpgradeProposal { + contract_address: contract_address.clone(), + new_wasm_hash: new_wasm_hash.clone(), + queued_at: env.ledger().timestamp(), + admin: Self::admin(&env), + }; + + env.storage().persistent().set(&key, &proposal); + + UpgradeQueued { + contract_address, + new_wasm_hash, + queued_at: proposal.queued_at, + admin: proposal.admin, + } + .publish(&env); + } + + /// Execute an upgrade proposal. + /// + /// Checks that the timelock has expired, then returns the WASM hash for upgrade. + /// The caller (governance contract) is responsible for performing the actual upgrade. + /// Removes the proposal from storage after successful execution. + pub fn execute_upgrade(env: Env, contract_address: Address) -> BytesN<32> { + let key = DataKey::UpgradeProposal(contract_address.clone()); + let proposal: UpgradeProposal = env + .storage() + .persistent() + .get(&key) + .unwrap_or_else(|| panic_with_error!(&env, UpgradeTimelockError::UpgradeNotFound)); + + let timelock_duration = Self::get_timelock_duration(env.clone()); + let current_time = env.ledger().timestamp(); + if current_time < proposal.queued_at + timelock_duration { + panic_with_error!(&env, UpgradeTimelockError::TimelockNotExpired); + } + + // Remove the proposal from storage + env.storage().persistent().remove(&key); + + UpgradeExecuted { + contract_address, + new_wasm_hash: proposal.new_wasm_hash.clone(), + executed_at: current_time, + } + .publish(&env); + + proposal.new_wasm_hash + } + + /// Cancel an upgrade proposal. Admin only. + /// + /// Removes the queued upgrade proposal. Can be called at any time during timelock. + pub fn cancel_upgrade(env: Env, contract_address: Address) { + Self::admin(&env).require_auth(); + + let key = DataKey::UpgradeProposal(contract_address.clone()); + let proposal: UpgradeProposal = env + .storage() + .persistent() + .get(&key) + .unwrap_or_else(|| panic_with_error!(&env, UpgradeTimelockError::UpgradeNotFound)); + + env.storage().persistent().remove(&key); + + UpgradeCancelled { + contract_address, + new_wasm_hash: proposal.new_wasm_hash, + cancelled_at: env.ledger().timestamp(), + } + .publish(&env); + } + + /// Get an upgrade proposal for a contract. + /// + /// Returns the proposal details if one exists. + pub fn get_upgrade_proposal(env: Env, contract_address: Address) -> Option { + let key = DataKey::UpgradeProposal(contract_address); + env.storage().persistent().get(&key) + } + + /// Check if an upgrade is ready for execution. + /// + /// Returns true if the timelock has expired for the given contract. + pub fn is_upgrade_ready(env: Env, contract_address: Address) -> bool { + if let Some(proposal) = Self::get_upgrade_proposal(env.clone(), contract_address) { + let timelock_duration = Self::get_timelock_duration(env.clone()); + let current_time = env.ledger().timestamp(); + current_time >= proposal.queued_at + timelock_duration + } else { + false + } + } + + /// Get the admin address. + pub fn get_admin(env: Env) -> Address { + Self::admin(&env) + } + + fn admin(env: &Env) -> Address { + env.storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(env, UpgradeTimelockError::NotInitialized)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::testutils::{Address as _, Ledger}; + use soroban_sdk::{Address, BytesN, Env, IntoVal, contractclient}; + + #[contractclient(name = "UpgradeTimelockVaultClient")] + pub trait UpgradeTimelockVaultInterface { + fn initialize(env: Env, admin: Address); + fn set_timelock_duration(env: Env, duration_seconds: u64); + fn get_timelock_duration(env: Env) -> u64; + fn queue_upgrade(env: Env, contract_address: Address, new_wasm_hash: BytesN<32>); + fn execute_upgrade(env: Env, contract_address: Address) -> BytesN<32>; + fn cancel_upgrade(env: Env, contract_address: Address); + fn get_upgrade_proposal(env: Env, contract_address: Address) -> Option; + fn is_upgrade_ready(env: Env, contract_address: Address) -> bool; + fn get_admin(env: Env) -> Address; + } + + fn create_env() -> Env { + Env::default() + } + + fn create_admin(env: &Env) -> Address { + Address::generate(env) + } + + fn create_contract(env: &Env) -> Address { + Address::generate(env) + } + + fn create_wasm_hash(env: &Env) -> BytesN<32> { + BytesN::from_array(env, &[0; 32]) + } + + #[test] + fn test_initialize() { + let env = create_env(); + let admin = create_admin(&env); + let contract = UpgradeTimelockVaultClient::new( + &env, + &env.register_contract(None, UpgradeTimelockVault {}), + ); + + contract.initialize(&admin); + + assert_eq!(contract.get_admin(), admin); + assert_eq!(contract.get_timelock_duration(), DEFAULT_TIMELOCK_DURATION); + } + + #[test] + #[should_panic(expected = "Error(Contract, #6)")] + fn test_initialize_twice_fails() { + let env = create_env(); + let admin = create_admin(&env); + let contract = UpgradeTimelockVaultClient::new( + &env, + &env.register_contract(None, UpgradeTimelockVault {}), + ); + + contract.initialize(&admin); + contract.initialize(&admin); + } + + #[test] + fn test_set_timelock_duration() { + let env = create_env(); + let admin = create_admin(&env); + let contract = UpgradeTimelockVaultClient::new( + &env, + &env.register_contract(None, UpgradeTimelockVault {}), + ); + + contract.initialize(&admin); + + let new_duration = 24 * 60 * 60; // 24 hours + env.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract.address, + fn_name: "set_timelock_duration", + args: (new_duration,).into_val(&env), + sub_invokes: &[], + }, + }]); + contract.set_timelock_duration(&new_duration); + + assert_eq!(contract.get_timelock_duration(), new_duration); + } + + #[test] + #[should_panic(expected = "Unauthorized")] + fn test_set_timelock_duration_unauthorized() { + let env = create_env(); + let admin = create_admin(&env); + let unauthorized = create_admin(&env); + let contract = UpgradeTimelockVaultClient::new( + &env, + &env.register_contract(None, UpgradeTimelockVault {}), + ); + + contract.initialize(&admin); + + env.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &unauthorized, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract.address, + fn_name: "set_timelock_duration", + args: (24 * 60 * 60,).into_val(&env), + sub_invokes: &[], + }, + }]); + contract.set_timelock_duration(&(24 * 60 * 60)); + } + + #[test] + fn test_queue_upgrade() { + let env = create_env(); + let admin = create_admin(&env); + let contract_addr = create_contract(&env); + let wasm_hash = create_wasm_hash(&env); + let contract = UpgradeTimelockVaultClient::new( + &env, + &env.register_contract(None, UpgradeTimelockVault {}), + ); + + contract.initialize(&admin); + env.ledger().set_timestamp(1); + + env.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract.address, + fn_name: "queue_upgrade", + args: (contract_addr.clone(), wasm_hash.clone()).into_val(&env), + sub_invokes: &[], + }, + }]); + contract.queue_upgrade(&contract_addr, &wasm_hash); + + let proposal = contract.get_upgrade_proposal(&contract_addr).unwrap(); + assert_eq!(proposal.contract_address, contract_addr); + assert_eq!(proposal.new_wasm_hash, wasm_hash); + assert_eq!(proposal.admin, admin); + assert!(proposal.queued_at > 0); + } + + #[test] + #[should_panic(expected = "Error(Contract, #3)")] + fn test_queue_upgrade_twice_fails() { + let env = create_env(); + let admin = create_admin(&env); + let contract_addr = create_contract(&env); + let wasm_hash = create_wasm_hash(&env); + let contract = UpgradeTimelockVaultClient::new( + &env, + &env.register_contract(None, UpgradeTimelockVault {}), + ); + + contract.initialize(&admin); + + env.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract.address, + fn_name: "queue_upgrade", + args: (contract_addr.clone(), wasm_hash.clone()).into_val(&env), + sub_invokes: &[], + }, + }]); + contract.queue_upgrade(&contract_addr, &wasm_hash); + + env.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract.address, + fn_name: "queue_upgrade", + args: (contract_addr.clone(), wasm_hash.clone()).into_val(&env), + sub_invokes: &[], + }, + }]); + contract.queue_upgrade(&contract_addr, &wasm_hash); + } + + #[test] + fn test_execute_upgrade() { + let env = create_env(); + let admin = create_admin(&env); + let contract_addr = create_contract(&env); + let wasm_hash = create_wasm_hash(&env); + let contract = UpgradeTimelockVaultClient::new( + &env, + &env.register_contract(None, UpgradeTimelockVault {}), + ); + + contract.initialize(&admin); + + // Queue upgrade + env.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract.address, + fn_name: "queue_upgrade", + args: (contract_addr.clone(), wasm_hash.clone()).into_val(&env), + sub_invokes: &[], + }, + }]); + contract.queue_upgrade(&contract_addr, &wasm_hash); + + // Fast forward time past timelock + env.ledger() + .set_timestamp(env.ledger().timestamp() + DEFAULT_TIMELOCK_DURATION + 1); + + // Execute upgrade + let returned_hash = contract.execute_upgrade(&contract_addr); + assert_eq!(returned_hash, wasm_hash); + + // Proposal should be removed + assert!(contract.get_upgrade_proposal(&contract_addr).is_none()); + } + + #[test] + #[should_panic(expected = "Error(Contract, #5)")] + fn test_execute_upgrade_before_timelock() { + let env = create_env(); + let admin = create_admin(&env); + let contract_addr = create_contract(&env); + let wasm_hash = create_wasm_hash(&env); + let contract = UpgradeTimelockVaultClient::new( + &env, + &env.register_contract(None, UpgradeTimelockVault {}), + ); + + contract.initialize(&admin); + + // Queue upgrade + env.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract.address, + fn_name: "queue_upgrade", + args: (contract_addr.clone(), wasm_hash.clone()).into_val(&env), + sub_invokes: &[], + }, + }]); + contract.queue_upgrade(&contract_addr, &wasm_hash); + + // Try to execute immediately (before timelock) + contract.execute_upgrade(&contract_addr); + } + + #[test] + fn test_cancel_upgrade() { + let env = create_env(); + let admin = create_admin(&env); + let contract_addr = create_contract(&env); + let wasm_hash = create_wasm_hash(&env); + let contract = UpgradeTimelockVaultClient::new( + &env, + &env.register_contract(None, UpgradeTimelockVault {}), + ); + + contract.initialize(&admin); + + // Queue upgrade + env.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract.address, + fn_name: "queue_upgrade", + args: (contract_addr.clone(), wasm_hash.clone()).into_val(&env), + sub_invokes: &[], + }, + }]); + contract.queue_upgrade(&contract_addr, &wasm_hash); + + // Cancel upgrade + env.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract.address, + fn_name: "cancel_upgrade", + args: (contract_addr.clone(),).into_val(&env), + sub_invokes: &[], + }, + }]); + contract.cancel_upgrade(&contract_addr); + + // Proposal should be removed + assert!(contract.get_upgrade_proposal(&contract_addr).is_none()); + } + + #[test] + fn test_is_upgrade_ready() { + let env = create_env(); + let admin = create_admin(&env); + let contract_addr = create_contract(&env); + let wasm_hash = create_wasm_hash(&env); + let contract = UpgradeTimelockVaultClient::new( + &env, + &env.register_contract(None, UpgradeTimelockVault {}), + ); + + contract.initialize(&admin); + + // No proposal yet + assert!(!contract.is_upgrade_ready(&contract_addr)); + + // Queue upgrade + env.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract.address, + fn_name: "queue_upgrade", + args: (contract_addr.clone(), wasm_hash.clone()).into_val(&env), + sub_invokes: &[], + }, + }]); + contract.queue_upgrade(&contract_addr, &wasm_hash); + + // Not ready yet + assert!(!contract.is_upgrade_ready(&contract_addr)); + + // Fast forward time + env.ledger() + .set_timestamp(env.ledger().timestamp() + DEFAULT_TIMELOCK_DURATION + 1); + + // Now ready + assert!(contract.is_upgrade_ready(&contract_addr)); + } +} diff --git a/convert-images.mjs b/convert-images.mjs new file mode 100644 index 00000000..cb3f98a7 --- /dev/null +++ b/convert-images.mjs @@ -0,0 +1,31 @@ +import fs from "fs" +import path from "path" +import sharp from "sharp" + +const targetDir = "src/pages" + +function walk(dir) { + const files = fs.readdirSync(dir) + for (const file of files) { + const fullPath = path.join(dir, file) + const stat = fs.statSync(fullPath) + + if (stat.isDirectory()) { + walk(fullPath) + } else if (/\.(png|jpg|jpeg)$/i.test(fullPath)) { + const output = fullPath.replace(/\.(png|jpg|jpeg)$/i, ".webp") + + sharp(fullPath) + .webp({ quality: 80 }) + .toFile(output) + .then(() => { + console.log("Converted:", fullPath) + }) + .catch((err) => { + console.error("Error:", fullPath, err) + }) + } + } +} + +walk(targetDir) diff --git a/docs/LearnToken.md b/docs/LearnToken.md new file mode 100644 index 00000000..06441d2e --- /dev/null +++ b/docs/LearnToken.md @@ -0,0 +1,230 @@ +# LearnToken (LRN) — Contract Reference + +## Overview + +`LearnToken` is a **soulbound** (non-transferable) SEP-41 fungible token on the +Stellar Soroban blockchain. It is minted to learners upon verified course +milestone completion and serves as the core on-chain reputation layer of the +LearnVault ecosystem. + +- **Token name**: `LearnToken` +- **Symbol**: `LRN` +- **Decimals**: `7` (Stellar convention) +- **Transferability**: Permanently disabled — LRN is soulbound +- **Minting**: Restricted to the admin address (intended to be the + `CourseMilestone` contract) + +A learner's LRN balance is their **on-chain academic reputation score**, which +gates scholarship eligibility and governance participation. + +--- + +## Deployment + +Deploy in this order (no dependencies for LearnToken): + +``` +1. LearnToken — no dependencies +4. CourseMilestone — requires LearnToken address +``` + +After deploying `CourseMilestone`, call `set_admin` to transfer minting +authority: + +```rust +learn_token_client.set_admin(&course_milestone_address); +``` + +--- + +## Functions + +### `initialize(admin: Address)` + +Initialises the contract. Must be called exactly once by the deployer. + +| Parameter | Type | Description | +| --------- | --------- | -------------------------------------------------------------------------------------------------------- | +| `admin` | `Address` | The address authorised to call `mint`. Should be set to the `CourseMilestone` contract after deployment. | + +**Errors**: + +- `Unauthorized (2)` — if called a second time on an already-initialised + contract. + +--- + +### `mint(to: Address, amount: i128, course_id: String)` + +Mints `amount` LRN tokens to `to` for completing `course_id`. Only callable by +the admin. + +| Parameter | Type | Description | +| ----------- | --------- | ------------------------------------------------------ | +| `to` | `Address` | The learner's wallet address | +| `amount` | `i128` | Number of LRN tokens to mint (must be > 0) | +| `course_id` | `String` | Identifier of the completed course (e.g. `"web3-101"`) | + +**Effects**: + +- Increases `balance(to)` by `amount` +- Increases `total_supply()` by `amount` +- Emits a `MilestoneCompleted` event + +**Errors**: + +- `NotInitialized (4)` — if called before `initialize` +- `Unauthorized (2)` — if caller is not the admin +- `ZeroAmount (3)` — if `amount <= 0` + +--- + +### `set_admin(new_admin: Address)` + +Transfers the admin (minter) role to a new address. + +| Parameter | Type | Description | +| ----------- | --------- | --------------------- | +| `new_admin` | `Address` | The new admin address | + +**Errors**: + +- `NotInitialized (4)` — if called before `initialize` +- `Unauthorized (2)` — if caller is not the current admin + +--- + +### `balance(account: Address) → i128` + +Returns the LRN balance of `account`. Returns `0` for addresses that have never +received LRN. + +--- + +### `reputation_score(account: Address) → i128` + +Returns the on-chain reputation score of `account`. Identical to `balance` — the +LRN balance IS the reputation score. Provided as a semantically meaningful alias +for use by scholarship eligibility checks and governance contracts. + +--- + +### `total_supply() → i128` + +Returns the total number of LRN tokens minted across all learners. + +--- + +### `decimals() → u32` + +Returns `7` (Stellar convention). + +--- + +### `name() → String` + +Returns `"LearnToken"`. + +--- + +### `symbol() → String` + +Returns `"LRN"`. + +--- + +### `transfer(...)` / `transfer_from(...)` / `approve(...)` + +Always revert with `Soulbound (1)`. LRN tokens cannot be transferred under any +circumstances. + +--- + +### `allowance(from: Address, spender: Address) → i128` + +Always returns `0`. No approvals are possible on a soulbound token. + +--- + +## Events + +### `MilestoneCompleted` + +Emitted on every successful `mint` call. + +| Field | Type | Description | +| ----------- | --------- | --------------------------------------------- | +| `learner` | `Address` | The address that received LRN | +| `amount` | `i128` | The number of LRN tokens minted | +| `course_id` | `String` | The course identifier that triggered the mint | + +**Example** (off-chain indexer pseudocode): + +```json +{ + "type": "MilestoneCompleted", + "learner": "GABC...XYZ", + "amount": 100, + "course_id": "web3-101" +} +``` + +--- + +## Error Codes + +| Code | Name | Trigger | +| ---- | ---------------- | ----------------------------------------------------------- | +| `1` | `Soulbound` | `transfer`, `transfer_from`, or `approve` was called | +| `2` | `Unauthorized` | Non-admin called `mint`/`set_admin`, or double `initialize` | +| `3` | `ZeroAmount` | `mint` called with `amount <= 0` | +| `4` | `NotInitialized` | `mint` or `set_admin` called before `initialize` | + +--- + +## Storage Layout + +| Key | Storage | Type | Description | +| --------------------------- | ---------- | --------- | ----------------------- | +| `ADMIN` | Instance | `Address` | Current admin address | +| `NAME` | Instance | `String` | Token name | +| `SYMBOL` | Instance | `String` | Token symbol | +| `DECIMALS` | Instance | `u32` | Decimal places | +| `DataKey::TotalSupply` | Instance | `i128` | Total minted supply | +| `DataKey::Balance(Address)` | Persistent | `i128` | Per-learner LRN balance | + +Balances use **persistent** storage so learner reputation survives ledger TTL +expiry. + +--- + +## Usage Example + +```rust +// 1. Deploy and initialize +learn_token_client.initialize(&deployer_address); + +// 2. Hand off minting to CourseMilestone +learn_token_client.set_admin(&course_milestone_address); + +// 3. CourseMilestone mints on verified completion +learn_token_client.mint(&learner_address, &100, &String::from_str(&env, "web3-101")); + +// 4. Query reputation +let score = learn_token_client.reputation_score(&learner_address); // 100 +let balance = learn_token_client.balance(&learner_address); // 100 (same) + +// 5. Transfer attempt — always fails +learn_token_client.transfer(&learner_address, &other_address, &10); // panics: Soulbound +``` + +--- + +## Security Notes + +- The admin role is a single address. In production, set it to the + `CourseMilestone` contract address, not an EOA. +- There is no burn function — LRN balances only increase. This is intentional: + reputation is permanent proof of effort. +- The soulbound property is enforced at the contract level, not via allowlists. + It cannot be bypassed. diff --git a/docs/SECURITY_REVIEW.md b/docs/SECURITY_REVIEW.md new file mode 100644 index 00000000..cf632363 --- /dev/null +++ b/docs/SECURITY_REVIEW.md @@ -0,0 +1,869 @@ +# Smart Contract Security Review Checklist + +**Version:** 1.0 +**Date:** March 2026 +**Status:** Pre-Mainnet Review +**Reviewer:** Security Team + +This document tracks the systematic security review of LearnVault's smart +contracts before V1 Mainnet deployment. + +--- + +## Executive Summary + +LearnVault consists of 6 core smart contracts that power a decentralized +learn-and-earn platform on Stellar: + +1. **LearnToken** - Soulbound reputation token +2. **GovernanceToken** - Transferable DAO voting token +3. **ScholarshipTreasury** - Community treasury for donations +4. **MilestoneEscrow** - Tranche-based scholarship disbursements +5. **CourseMilestone** - Course completion tracking +6. **ScholarNFT** - Soulbound credential NFT + +--- + +## 1. Access Control Review + +### 1.1 LearnToken (LRN) + +#### ✅ Privileged Functions Protected + +- **`mint()`** - ✅ Protected by `admin.require_auth()` +- **`set_admin()`** - ✅ Protected by `admin.require_auth()` + +**Status:** PASS + +**Evidence:** + +```rust +pub fn mint(env: Env, to: Address, amount: i128) { + let admin: Address = env.storage().instance().get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(&env, LRNError::NotInitialized)); + admin.require_auth(); // ✅ Authorization check + // ... +} +``` + +#### ✅ Admin Role Cannot Be Accidentally Renounced + +- `set_admin()` requires current admin authorization +- No `renounce_admin()` or similar function exists +- Admin transfer is explicit and intentional + +**Status:** PASS + +#### ⚠️ Multi-sig Not Implemented (V1) + +- Single admin address controls minting +- **Recommendation:** Implement multi-sig or timelock for V2 + +**Status:** NOTED - Not critical for V1 + +--- + +### 1.2 GovernanceToken (GOV) + +#### ✅ Privileged Functions Protected + +- **`mint()`** - ✅ Protected by `admin.require_auth()` +- **`set_admin()`** - ✅ Protected by `admin.require_auth()` + +**Status:** PASS + +**Evidence:** + +```rust +pub fn mint(env: Env, to: Address, amount: i128) { + let admin: Address = env.storage().instance().get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(&env, GOVError::NotInitialized)); + admin.require_auth(); // ✅ Authorization check + // ... +} +``` + +#### ✅ Admin Role Cannot Be Accidentally Renounced + +- Same pattern as LearnToken +- Explicit transfer only + +**Status:** PASS + +--- + +### 1.3 ScholarshipTreasury + +#### ✅ Privileged Functions Protected + +- **`disburse()`** - ✅ Protected by `governance.require_auth()` +- **`initialize()`** - ✅ Protected by `admin.require_auth()` + +**Status:** PASS + +**Evidence:** + +```rust +pub fn disburse(env: Env, recipient: Address, amount: i128) { + // ... + let governance = Self::governance_contract(&env); + governance.require_auth(); // ✅ Only governance can disburse + // ... +} +``` + +#### ⚠️ Multi-sig for Large Amounts Not Implemented + +- All disbursements require governance approval +- No threshold-based multi-sig for large amounts +- **Recommendation:** Consider implementing amount-based approval thresholds in + V2 + +**Status:** NOTED - Governance voting provides sufficient protection for V1 + +--- + +### 1.4 MilestoneEscrow + +#### ✅ Privileged Functions Protected + +- **`release_tranche()`** - ✅ Protected by `admin.require_auth()` +- **`create_escrow()`** - ✅ Protected by `treasury.require_auth()` +- **`initialize()`** - ✅ Protected by `admin.require_auth()` + +**Status:** PASS + +**Evidence:** + +```rust +pub fn release_tranche(env: Env, proposal_id: u32) { + let admin = Self::admin(&env); + admin.require_auth(); // ✅ Authorization check + // ... +} +``` + +--- + +## 2. Token Safety (LearnToken) + +### 2.1 Non-Transferability Enforcement + +#### ✅ Transfer Functions Always Revert + +- **`transfer()`** - ✅ Always panics with `LRNError::Soulbound` +- **`transfer_from()`** - ✅ Always panics with `LRNError::Soulbound` +- **`approve()`** - ✅ Always panics with `LRNError::Soulbound` +- **`allowance()`** - ✅ Always returns 0 + +**Status:** PASS + +**Evidence:** + +```rust +pub fn transfer(env: Env, _from: Address, _to: Address, _amount: i128) { + panic_with_error!(&env, LRNError::Soulbound) // ✅ Always reverts +} + +pub fn transfer_from(env: Env, _spender: Address, _from: Address, _to: Address, _amount: i128) { + panic_with_error!(&env, LRNError::Soulbound) // ✅ Always reverts +} + +pub fn approve(env: Env, _from: Address, _spender: Address, _amount: i128, _expiration_ledger: u32) { + panic_with_error!(&env, LRNError::Soulbound) // ✅ Always reverts +} + +pub fn allowance(_env: Env, _from: Address, _spender: Address) -> i128 { + 0 // ✅ No allowances possible +} +``` + +#### ✅ No Allowance Bypass + +- Allowance always returns 0 +- `approve()` always reverts +- No way to grant spending permissions + +**Status:** PASS + +--- + +### 2.2 Integer Overflow Protection + +#### ✅ Checked Arithmetic Used + +- Rust's default arithmetic panics on overflow in debug mode +- Soroban SDK uses checked arithmetic +- Balance and supply calculations are safe + +**Status:** PASS + +**Evidence:** + +```rust +// Addition operations use Rust's default checked arithmetic +env.storage().persistent().set(&balance_key, &(current_balance + amount)); +env.storage().instance().set(&DataKey::TotalSupply, &(total_supply + amount)); +``` + +#### ✅ Zero Amount Validation + +- All mint operations check for `amount <= 0` +- Prevents zero-value minting + +**Status:** PASS + +**Evidence:** + +```rust +if amount <= 0 { + panic_with_error!(&env, LRNError::ZeroAmount); +} +``` + +--- + +## 3. Treasury Safety (ScholarshipTreasury) + +### 3.1 Re-entrancy Protection + +#### ✅ Soroban Architecture Prevents Re-entrancy + +- Soroban uses a different execution model than EVM +- No external calls that can re-enter during execution +- State updates happen atomically + +**Status:** PASS + +**Note:** Soroban's execution model inherently prevents re-entrancy attacks that +are common in EVM chains. + +--- + +### 3.2 Funds Can Only Leave Via Governance + +#### ✅ Disburse Function Requires Governance Auth + +- Only `disburse()` function can transfer funds out +- Requires `governance.require_auth()` +- No backdoors or alternative withdrawal methods + +**Status:** PASS + +**Evidence:** + +```rust +pub fn disburse(env: Env, recipient: Address, amount: i128) { + // ... + let governance = Self::governance_contract(&env); + governance.require_auth(); // ✅ Only governance can authorize + + token::client(&env).transfer(&env.current_contract_address(), &recipient, &amount); + // ... +} +``` + +#### ✅ Deposit Function Only Increases Balance + +- `deposit()` only adds funds, never removes +- Requires donor authorization +- Updates internal accounting correctly + +**Status:** PASS + +--- + +### 3.3 Emergency Pause Mechanism + +#### ✅ Emergency Pause Implemented + +- `pause()` function added (admin-only) +- `unpause()` function added (admin-only) +- `is_paused()` query function added +- All critical operations protected with pause check + +**Status:** PASS + +**Implementation:** + +```rust +const PAUSED_KEY: Symbol = symbol_short!("PAUSED"); + +pub fn pause(env: Env) { + let admin = Self::admin(&env); + admin.require_auth(); + env.storage().instance().set(&PAUSED_KEY, &true); +} + +pub fn unpause(env: Env) { + let admin = Self::admin(&env); + admin.require_auth(); + env.storage().instance().set(&PAUSED_KEY, &false); +} + +fn assert_not_paused(env: &Env) { + let paused: bool = env.storage().instance().get(&PAUSED_KEY).unwrap_or(false); + if paused { + panic_with_error!(env, Error::ContractPaused); + } +} +``` + +**Protected Functions:** + +- ✅ `deposit()` - Cannot accept donations when paused +- ✅ `disburse()` - Cannot disburse funds when paused +- ✅ `submit_proposal()` - Cannot submit proposals when paused + +**Priority:** ~~HIGH~~ COMPLETED + +--- + +## 4. Escrow Safety (MilestoneEscrow) + +### 4.1 Inactivity Timeout Calculation + +#### ✅ Correct Timestamp Calculation + +- Uses `env.ledger().timestamp()` for current time +- Calculates inactivity as `now.saturating_sub(record.last_activity)` +- 30-day window: `30 * 24 * 60 * 60` seconds + +**Status:** PASS + +**Evidence:** + +```rust +const INACTIVITY_WINDOW_SECONDS: u64 = 30 * 24 * 60 * 60; // ✅ Correct calculation + +pub fn reclaim_inactive(env: Env, proposal_id: u32) { + // ... + let now = env.ledger().timestamp(); + let inactive_for = now.saturating_sub(record.last_activity); // ✅ Safe subtraction + if inactive_for < INACTIVITY_WINDOW_SECONDS { + panic_with_error!(&env, Error::InactivityNotReached); + } + // ... +} +``` + +#### ✅ Last Activity Updated on Tranche Release + +- `last_activity` updated on every `release_tranche()` call +- Resets the inactivity timer + +**Status:** PASS + +--- + +### 4.2 Funds Return to Treasury on Timeout + +#### ✅ Unspent Funds Returned Correctly + +- Calculates unspent as `total_amount - released_amount` +- Transfers unspent back to treasury +- Updates record to prevent double-reclaim + +**Status:** PASS + +**Evidence:** + +```rust +pub fn reclaim_inactive(env: Env, proposal_id: u32) { + // ... + let unspent = record.total_amount - record.released_amount; + if unspent <= 0 { + panic_with_error!(&env, Error::NothingToReclaim); // ✅ Prevents empty reclaim + } + + xlm::token_client(&env).transfer( + &env.current_contract_address(), + &record.treasury, + &unspent, + ); + + record.released_amount = record.total_amount; // ✅ Prevents double-reclaim + record.last_activity = now; + env.storage().persistent().set(&key, &record); +} +``` + +### 4.4 Access Control for reclaim_inactive + +#### ✅ Access control enforced + +The `reclaim_inactive` entrypoint now requires the contract admin to authorize +the call. This prevents arbitrary actors from triggering a reclaim when the +inactivity window is reached. Evidence in the contract: + +```rust +pub fn reclaim_inactive(env: Env, proposal_id: u32) { + let admin = Self::admin(&env); + admin.require_auth(); // ✅ Only admin may call + // ... rest of function +} +``` + +This change ensures only the configured admin (or a future extension to allow +the stored treasury address) can perform reclamation. + +#### ✅ No Stuck State Possible + +- All funds either released to scholar or returned to treasury +- Record updated to reflect final state +- No edge cases where funds remain locked + +**Status:** PASS + +--- + +### 4.3 Double-Claim Prevention + +#### ✅ Tranche Count Validation + +- Checks `tranches_released >= total_tranches` before release +- Prevents releasing more tranches than allocated + +**Status:** PASS + +**Evidence:** + +```rust +pub fn release_tranche(env: Env, proposal_id: u32) { + // ... + if record.tranches_released >= record.total_tranches { + panic_with_error!(&env, Error::AllTranchesReleased); // ✅ Prevents over-release + } + // ... +} +``` + +#### ✅ Amount Calculation Prevents Overpayment + +- Last tranche gets remaining balance +- Validates `released_amount + amount <= total_amount` + +**Status:** PASS + +**Evidence:** + +```rust +fn next_tranche_amount(env: &Env, record: &EscrowRecord) -> i128 { + let remaining = record.total_amount - record.released_amount; + let is_last = record.tranches_released + 1 == record.total_tranches; + let amount = if is_last { + remaining // ✅ Last tranche gets exact remaining amount + } else { + record.total_amount / (record.total_tranches as i128) + }; + + if amount <= 0 || record.released_amount + amount > record.total_amount { + panic_with_error!(env, Error::Overpayment); // ✅ Validates no overpayment + } + amount +} +``` + +--- + +## 5. Governance Safety + +### 5.1 Vote Replay Prevention + +#### ⚠️ Governance Contract Not Reviewed + +- Governance contract implementation not provided in this review +- Cannot verify vote replay prevention +- **Recommendation:** Ensure governance contract implements: + - One vote per address per proposal + - Vote tracking in storage + - Proposal ID validation + +**Status:** PENDING - Requires governance contract review + +**Required Checks:** + +```rust +// Example implementation needed +pub fn vote(env: Env, proposal_id: u32, voter: Address, vote: bool) { + voter.require_auth(); + + let vote_key = DataKey::Vote(proposal_id, voter.clone()); + if env.storage().persistent().has(&vote_key) { + panic_with_error!(&env, Error::AlreadyVoted); // ✅ Prevent double voting + } + + env.storage().persistent().set(&vote_key, &vote); + // ... +} +``` + +--- + +### 5.2 Voting Window Enforcement + +#### ⚠️ Governance Contract Not Reviewed + +- Cannot verify voting deadline enforcement +- **Recommendation:** Ensure governance contract implements: + - Proposal start and end timestamps + - Validation that current time is within voting window + - No votes accepted after deadline + +**Status:** PENDING - Requires governance contract review + +**Required Checks:** + +```rust +// Example implementation needed +pub fn vote(env: Env, proposal_id: u32, voter: Address, vote: bool) { + let proposal = Self::get_proposal(&env, proposal_id); + let now = env.ledger().timestamp(); + + if now < proposal.voting_start { + panic_with_error!(&env, Error::VotingNotStarted); + } + if now > proposal.voting_end { + panic_with_error!(&env, Error::VotingEnded); // ✅ Enforce deadline + } + // ... +} +``` + +--- + +### 5.3 Quorum and Threshold Parameters + +#### ⚠️ Governance Contract Not Reviewed + +- Cannot verify parameter validation +- **Recommendation:** Ensure governance contract validates: + - Quorum > 0 + - Threshold > 0 and <= 100% + - Parameters cannot be set to invalid values + +**Status:** PENDING - Requires governance contract review + +**Required Checks:** + +```rust +// Example implementation needed +pub fn set_quorum(env: Env, quorum: u32) { + let admin = Self::admin(&env); + admin.require_auth(); + + if quorum == 0 { + panic_with_error!(&env, Error::InvalidQuorum); // ✅ Prevent zero quorum + } + + env.storage().instance().set(&QUORUM_KEY, &quorum); +} +``` + +--- + +## 6. Course Milestone Safety + +### 6.1 Access Control + +#### ✅ Enrollment Requires Learner Authorization + +- `enroll()` function requires `learner.require_auth()` +- Only the learner can enroll themselves +- No admin override for enrollment + +**Status:** PASS + +**Evidence:** + +```rust +pub fn enroll(env: Env, learner: Address, course_id: u32) { + learner.require_auth(); // ✅ Learner must authorize + // ... +} +``` + +### 6.2 Duplicate Enrollment Prevention + +#### ✅ Prevents Double Enrollment + +- Checks if enrollment key already exists +- Panics with `AlreadyEnrolled` error if duplicate + +**Status:** PASS + +**Evidence:** + +```rust +if env.storage().instance().has(&key) { + panic_with_error!(&env, Error::AlreadyEnrolled); // ✅ Prevents duplicates +} +``` + +### 6.3 Minimal Attack Surface + +#### ✅ Simple, Focused Contract + +- Only tracks enrollment status +- No fund handling +- No complex state transitions +- Read-only query function + +**Status:** PASS + +**Note:** CourseMilestone is a simple tracking contract with minimal security +concerns. + +--- + +## 7. Scholar NFT Safety + +### 7.1 Code Quality Issues + +#### ❌ CRITICAL: Contract File Contains Corrupted/Duplicate Code + +- File contains two different implementations mixed together +- Multiple conflicting `DataKey` enums +- Multiple conflicting error types (`ScholarNFTError` and `Error`) +- Multiple conflicting struct definitions (`ScholarNFT` and `ScholarNft`) +- Incomplete function implementations (missing closing braces) +- Test module declared twice (`mod test;` and inline `mod test`) + +**Status:** FAIL - CRITICAL + +**Evidence:** + +```rust +// First implementation starts +pub struct ScholarNFT; +impl ScholarNFT { + pub fn mint(env: Env, to: Address, metadata_uri: String) -> u32 { + // ... incomplete, then suddenly: + contract, contracterror, contractimpl, contracttype, panic_with_error, symbol_short, Address, + // Second implementation starts +pub struct ScholarNft; +impl ScholarNft { + pub fn mint(env: Env, scholar: Address, program_name: String, ipfs_uri: Option) -> u64 { +``` + +**Impact:** Contract will not compile. This is a blocking issue for deployment. + +**Recommendation:** + +1. Determine which implementation is correct +2. Remove duplicate/conflicting code +3. Ensure contract compiles successfully +4. Re-review after code is fixed + +**Priority:** CRITICAL - Must be fixed before any deployment + +### 7.2 Soulbound Enforcement (Pending Code Fix) + +#### ⚠️ Cannot Verify Until Code Is Fixed + +- First implementation has `transfer()` that panics with `Soulbound` error ✅ +- Second implementation has no `transfer()` function ❌ +- Cannot determine which implementation is intended + +**Status:** PENDING - Requires code fix first + +**Required After Fix:** + +- Verify `transfer()` function always reverts +- Verify no `approve()` or `transfer_from()` functions exist +- Verify NFT cannot be moved between addresses + +### 7.3 One NFT Per Scholar (Pending Code Fix) + +#### ⚠️ Cannot Verify Until Code Is Fixed + +- Second implementation has `ScholarAlreadyMinted` error ✅ +- Second implementation checks `DataKey::ScholarToken` before minting ✅ +- First implementation has no such check ❌ +- Cannot determine which implementation is intended + +**Status:** PENDING - Requires code fix first + +--- + +## 8. Additional Security Considerations + +### 8.1 Initialization Protection + +#### ✅ All Contracts Prevent Double Initialization + +- LearnToken: ✅ Checks `ADMIN_KEY` exists +- GovernanceToken: ✅ Checks `ADMIN_KEY` exists +- ScholarshipTreasury: ✅ Checks `ADMIN_KEY` exists +- MilestoneEscrow: ✅ Checks `ADMIN_KEY` exists +- CourseMilestone: ✅ Checks `ADMIN_KEY` exists +- ScholarNFT: ⚠️ Cannot verify due to code corruption + +**Status:** PASS (except ScholarNFT) + +--- + +### 8.2 Storage Key Collisions + +#### ✅ No Storage Key Collisions Detected (Except ScholarNFT) + +- Each contract uses unique `DataKey` enums +- Symbol keys use `symbol_short!()` macro +- No overlapping storage patterns +- ❌ ScholarNFT has duplicate `DataKey` enums (code corruption issue) + +**Status:** PASS (except ScholarNFT) + +--- + +### 8.3 Error Handling + +#### ✅ Comprehensive Error Types (Except ScholarNFT) + +- All contracts define custom error enums +- Errors are descriptive and specific +- No generic error handling +- ❌ ScholarNFT has duplicate error enums (code corruption issue) + +**Status:** PASS (except ScholarNFT) + +--- + +## 9. Summary and Recommendations + +### Critical Issues (Must Fix Before Mainnet) + +1. **❌ ScholarNFT: Code Corruption - Contract Will Not Compile** + - **Priority:** CRITICAL + - **Impact:** Contract contains duplicate/conflicting implementations and + will not compile + - **Recommendation:** Fix code corruption immediately, determine correct + implementation, remove duplicates + - **Status:** NOT FIXED + +### Pending Reviews + +2. **⚠️ Governance Contract Not Reviewed** + - **Priority:** HIGH + - **Impact:** Cannot verify vote replay, deadline enforcement, parameter + validation + - **Recommendation:** Complete governance contract security review + +3. **⚠️ ScholarNFT Security Review Incomplete** + - **Priority:** HIGH + - **Impact:** Cannot verify soulbound enforcement or one-NFT-per-scholar + logic + - **Recommendation:** Fix code corruption, then complete security review + +### Resolved Issues + +4. **✅ ScholarshipTreasury: Emergency Pause Mechanism** + - **Priority:** HIGH (COMPLETED) + - **Impact:** Can now halt operations in emergency + - **Status:** Implemented in commit 3602a32 + - **Implementation:** Added pause(), unpause(), is_paused() functions with + admin-only access + +### Recommended Enhancements (V2) + +3. **Multi-sig for Treasury Operations** + - **Priority:** MEDIUM + - **Impact:** Single admin has full control + - **Recommendation:** Implement multi-sig or timelock for large disbursements + +4. **Amount-Based Approval Thresholds** + - **Priority:** LOW + - **Impact:** All amounts require same approval process + - **Recommendation:** Consider tiered approval for different amount ranges + +--- + +## 10. Testing Recommendations + +### Unit Tests + +- ✅ LearnToken has comprehensive tests +- ✅ GovernanceToken has comprehensive tests +- ⚠️ ScholarshipTreasury tests not reviewed +- ⚠️ MilestoneEscrow tests not reviewed +- ⚠️ CourseMilestone tests not reviewed +- ❌ ScholarNFT tests cannot run (code corruption) + +**Recommendation:** Ensure 100% code coverage for all contracts after ScholarNFT +is fixed + +### Integration Tests + +- Test full scholarship application flow +- Test escrow creation and tranche release +- Test governance voting and disbursement +- Test emergency pause scenarios (once implemented) + +### Property-Based Tests + +- Test invariants: + - Total supply = sum of all balances + - Treasury balance = deposits - disbursements + - Escrow total = released + unspent + +--- + +## 11. External Audit Recommendation + +**Status:** RECOMMENDED + +Before Mainnet deployment, consider: + +1. Professional security audit from Stellar ecosystem auditors +2. Community review period (2-4 weeks) +3. Bug bounty program +4. Gradual rollout with limited initial treasury funds + +**Potential Auditors:** + +- Stellar Development Foundation audit program +- OpenZeppelin (if they support Soroban) +- Independent Soroban security specialists + +--- + +## 12. Sign-off + +### Security Review Checklist Status + +| Category | Status | Critical Issues | +| ---------------- | ---------- | --------------------- | +| Access Control | ✅ PASS | 0 | +| Token Safety | ✅ PASS | 0 | +| Treasury Safety | ✅ PASS | 0 (pause implemented) | +| Escrow Safety | ✅ PASS | 0 | +| Course Milestone | ✅ PASS | 0 | +| Scholar NFT | ❌ FAIL | 1 (Code corruption) | +| Governance | ⚠️ PENDING | N/A | + +### Overall Assessment + +**Status:** NOT READY FOR MAINNET + +**Blockers:** + +1. Fix ScholarNFT code corruption (contract will not compile) +2. ~~Implement emergency pause mechanism in ScholarshipTreasury~~ ✅ COMPLETED +3. Complete governance contract security review +4. Re-review ScholarNFT after code is fixed + +**Timeline:** + +- Fix ScholarNFT code corruption: 1-2 days +- ~~Implement pause mechanism: 1-2 days~~ ✅ COMPLETED +- Governance review: 2-3 days +- ScholarNFT re-review: 1 day +- Re-review and testing: 2-3 days +- **Estimated time to Mainnet-ready:** ~~7-11 days~~ 5-9 days (updated after + pause implementation) + +--- + +**Document Version:** 1.0 +**Last Updated:** March 24, 2026 +**Next Review:** After critical issues resolved diff --git a/docs/TODO-nft-images.md b/docs/TODO-nft-images.md new file mode 100644 index 00000000..de2136be --- /dev/null +++ b/docs/TODO-nft-images.md @@ -0,0 +1,127 @@ +# TODO: Upload NFT Badge Images to IPFS + +## Current Status + +The NFT metadata generation endpoint is functional, but uses placeholder IPFS +CIDs for badge images. + +## Action Required + +Upload the following badge images to IPFS and update the `IMAGE_CID_MAP` in +`server/src/controllers/credentials.controller.ts`: + +### Images to Upload + +Located in `/public/assets/brand/nft/`: + +1. `scholar-nft-stellar.png` - Stellar Basics course badge +2. `scholar-nft-soroban.png` - Soroban courses badge +3. `scholar-nft-defi.png` - DeFi Fundamentals course badge +4. `scholar-nft-base.png` - Generic/fallback badge + +### Upload Process + +#### Option 1: Using Pinata Web Interface + +1. Go to https://app.pinata.cloud/pinmanager +2. Click "Upload" → "File" +3. Upload each PNG file +4. Copy the CID for each file +5. Update `IMAGE_CID_MAP` in the controller + +#### Option 2: Using Pinata API + +```bash +# Upload a single image +curl -X POST "https://api.pinata.cloud/pinning/pinFileToIPFS" \ + -H "pinata_api_key: YOUR_API_KEY" \ + -H "pinata_secret_api_key: YOUR_SECRET_KEY" \ + -F "file=@public/assets/brand/nft/scholar-nft-stellar.png" \ + -F 'pinataMetadata={"name":"scholar-nft-stellar.png"}' \ + -F 'pinataOptions={"cidVersion": 1}' +``` + +#### Option 3: Using the Upload Script + +Create a script to automate the upload: + +```typescript +// scripts/upload-nft-images.ts +import fs from "fs" +import path from "path" +import { pinFileToIPFS } from "../server/src/services/pinata.service" + +const images = [ + "scholar-nft-stellar.png", + "scholar-nft-soroban.png", + "scholar-nft-defi.png", + "scholar-nft-base.png", +] + +async function uploadImages() { + const cidMap: Record = {} + + for (const imageName of images) { + const imagePath = path.join( + __dirname, + "..", + "public", + "assets", + "brand", + "nft", + imageName, + ) + const buffer = fs.readFileSync(imagePath) + + console.log(`Uploading ${imageName}...`) + const cid = await pinFileToIPFS(buffer, imageName) + cidMap[imageName] = cid + console.log(`✓ ${imageName}: ipfs://${cid}`) + } + + console.log("\nUpdate IMAGE_CID_MAP in credentials.controller.ts:") + console.log(JSON.stringify(cidMap, null, 2)) +} + +uploadImages().catch(console.error) +``` + +Run with: + +```bash +cd server +npx ts-node ../scripts/upload-nft-images.ts +``` + +### Update the Controller + +After uploading, update the `IMAGE_CID_MAP` constant in +`server/src/controllers/credentials.controller.ts`: + +```typescript +const IMAGE_CID_MAP: Record = { + "scholar-nft-stellar.png": "bafybeiabc123...", // Replace with actual CID + "scholar-nft-soroban.png": "bafybeiabc456...", // Replace with actual CID + "scholar-nft-defi.png": "bafybeiabc789...", // Replace with actual CID + "scholar-nft-base.png": "bafybeiabc000...", // Replace with actual CID +} +``` + +## Verification + +After updating the CIDs, verify the images are accessible: + +```bash +# Test each image URL +curl -I https://gateway.pinata.cloud/ipfs/YOUR_CID + +# Or visit in browser +open https://gateway.pinata.cloud/ipfs/YOUR_CID +``` + +## Notes + +- Use CIDv1 for better compatibility +- Images should be 1000x1000px PNG format +- Consider using a dedicated Pinata gateway for production +- Keep a backup of the CIDs in case the controller file is modified diff --git a/docs/USDC_INTEGRATION.md b/docs/USDC_INTEGRATION.md new file mode 100644 index 00000000..e5bcbe6c --- /dev/null +++ b/docs/USDC_INTEGRATION.md @@ -0,0 +1,270 @@ +# USDC Integration Guide + +This document explains how to use USDC (USD Coin) with LearnVault on Stellar +Testnet and Mainnet. + +## Overview + +LearnVault's `ScholarshipTreasury` contract accepts USDC deposits from donors. +This integration provides: + +- Test USDC tokens for development and testing +- A faucet script to mint test USDC +- UI components for easy USDC operations +- Configuration for testnet and mainnet environments + +## Contract Addresses + +### Mainnet + +- **Official Circle USDC**: + `CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75` + +### Testnet + +- For testnet, we use a test token deployed as a Stellar Asset Contract +- The contract ID is configured in `environments.toml` and deployed + automatically + +### Local Development + +- A test USDC token is deployed automatically when you run `npm start` +- The contract uses the `fungible_allowlist` contract as a USDC substitute + +## Configuration + +### Environment Variables + +Add the following to your `.env` file: + +```bash +# USDC Contract ID (testnet or mainnet) +PUBLIC_USDC_CONTRACT_ID="CB..." +``` + +### environments.toml + +The USDC contracts are configured in `environments.toml`: + +```toml +# Development (local) +[development.contracts] +usdc_test_token = { client = true, constructor_args = "--admin me --initial_supply 1000000000000000" } + +# Staging (testnet) +[staging.contracts] +usdc_testnet = { client = true, constructor_args = "--admin testnet-user --manager testnet-user --initial_supply 1000000000000000" } + +# Production (mainnet) +[production.contracts] +usdc_mainnet = { id = "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75" } +``` + +## Getting Test USDC + +### Method 1: CLI Script (Recommended) + +Use the provided faucet script to mint test USDC: + +```bash +# Mint 1000 USDC (default amount) +./scripts/mint-test-usdc.sh + +# Mint a custom amount +./scripts/mint-test-usdc.sh 5000 +``` + +**Example:** + +```bash +./scripts/mint-test-usdc.sh GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 1000 +``` + +### Method 2: UI Button (Coming Soon) + +A "Get Test USDC" button is available in the donor onboarding flow. This button +will: + +1. Connect to your wallet +2. Mint test USDC tokens to your address +3. Display a success notification + +**Note:** The UI button currently directs users to use the CLI script. Full UI +integration will be available once contract clients are generated. + +## Using USDC with ScholarshipTreasury + +### Initialize the Treasury + +When deploying the `ScholarshipTreasury` contract, pass the USDC contract +address: + +```rust +scholarship_treasury::initialize( + env, + admin_address, + usdc_contract_address, // USDC token contract + governance_contract_address +) +``` + +### Make a Deposit + +Donors can deposit USDC to the treasury: + +```rust +scholarship_treasury::deposit( + env, + donor_address, + amount // Amount in stroops (7 decimals) +) +``` + +**Example:** To deposit 100 USDC: + +```rust +let amount = 100 * 10_000_000; // 100 USDC = 1,000,000,000 stroops +scholarship_treasury::deposit(env, donor, amount); +``` + +## Development Workflow + +### 1. Start Local Environment + +```bash +npm start +``` + +This will: + +- Start a local Stellar node +- Deploy all contracts including the test USDC token +- Generate contract clients +- Start the frontend dev server + +### 2. Fund Your Account + +```bash +# Get XLM for transaction fees +stellar keys fund --network testnet + +# Or use the UI "Fund Account" button +``` + +### 3. Get Test USDC + +```bash +./scripts/mint-test-usdc.sh 1000 +``` + +### 4. Test Donations + +Use the LearnVault UI to: + +1. Connect your wallet +2. Navigate to the Treasury/Donor page +3. Make a test donation using your USDC + +## Testnet Deployment + +### 1. Deploy Contracts + +```bash +stellar contract deploy \ + --wasm target/wasm32-unknown-unknown/release/fungible_allowlist.wasm \ + --source testnet-user \ + --network testnet +``` + +### 2. Initialize USDC Token + +```bash +stellar contract invoke \ + --id \ + --source testnet-user \ + --network testnet \ + -- \ + initialize \ + --admin \ + --manager \ + --initial_supply 1000000000000000 +``` + +### 3. Update Environment Variables + +Add the deployed contract ID to your `.env`: + +```bash +PUBLIC_USDC_CONTRACT_ID="" +``` + +## Mainnet Deployment + +For mainnet, use the official Circle USDC contract: + +```bash +PUBLIC_USDC_CONTRACT_ID="CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75" +``` + +**Important:** Never use test tokens on mainnet. Always use the official Circle +USDC contract. + +## Troubleshooting + +### "USDC contract ID not configured" + +**Solution:** Set `PUBLIC_USDC_CONTRACT_ID` in your `.env` file. + +### "Failed to mint USDC" + +**Possible causes:** + +1. Contract not deployed +2. Insufficient permissions +3. Network connectivity issues + +**Solution:** + +- Verify the contract is deployed: + `stellar contract info --id --network ` +- Check you have admin/manager permissions +- Ensure you're connected to the correct network + +### "Contract not found" + +**Solution:** Deploy the contracts first by running `npm start` (local) or +deploying to testnet. + +## Security Considerations + +### Testnet + +- Test USDC has no real value +- Use only for development and testing +- Never share testnet private keys publicly + +### Mainnet + +- Use only the official Circle USDC contract +- Verify the contract address before any transaction +- Test thoroughly on testnet before mainnet deployment +- Follow security best practices for key management + +## Additional Resources + +- [Stellar Documentation](https://developers.stellar.org/) +- [Soroban Smart Contracts](https://soroban.stellar.org/docs) +- [Circle USDC Documentation](https://developers.circle.com/stablecoins/usdc-contract-addresses) +- [LearnVault GitHub](https://github.com/bakeronchain/learnvault) + +## Support + +If you encounter issues: + +1. Check the [GitHub Issues](https://github.com/bakeronchain/learnvault/issues) +2. Join our [Discord](https://discord.gg/learnvault) +3. Read the [Contributing Guide](../CONTRIBUTING.md) + +--- + +**Last Updated:** March 2026 diff --git a/docs/adr/ADR-001.md b/docs/adr/ADR-001.md new file mode 100644 index 00000000..d5587814 --- /dev/null +++ b/docs/adr/ADR-001.md @@ -0,0 +1,37 @@ +# ADR-001: Why Stellar/Soroban over EVM chains + +**Status**: Accepted **Date**: 2026-03-24 + +## Context + +LearnVault requires a blockchain environment that supports high-frequency +micro-transactions (milestone payouts) and non-transferable identity tokens. We +evaluated the Ethereum Virtual Machine (EVM) ecosystem (L1s and L2s) against the +Stellar network using the Soroban smart contract platform. Key requirements +included predictable gas costs, WASM-based execution for performance, and native +support for regulated assets. + +## Decision + +We have selected **Stellar/Soroban** as the primary execution layer for +LearnVault. + +## Consequences + +### Positive (Why it wins) + +- **Deterministic Fees:** Soroban’s resource-based fee model prevents the "gas + wars" common in EVM, ensuring students can always claim rewards. +- **WASM Performance:** Moving away from the EVM interpreted bytecode to + WebAssembly allows for more complex meteorological data processing within + smart contracts if needed in future iterations. +- **Native Asset Integration:** Stellar’s "Classic" assets integrate seamlessly + with Soroban, allowing us to leverage built-in compliance features without the + overhead of complex ERC-20 implementations. + +### Negative (Trade-offs) + +- **Ecosystem Maturity:** We lose access to the vast library of OpenZeppelin EVM + contracts and must build more primitives from scratch in Rust. +- **Tooling:** While growing, the Soroban CLI and SDKs are less mature than + Hardhat/Foundry, requiring more manual DevOps work. diff --git a/docs/adr/ADR-002.md b/docs/adr/ADR-002.md new file mode 100644 index 00000000..d80d50a1 --- /dev/null +++ b/docs/adr/ADR-002.md @@ -0,0 +1,35 @@ +# ADR-002: Why LearnToken is soulbound (non-transferable) + +**Status**: Accepted **Date**: 2026-03-24 + +## Context + +LearnToken serves as a digital credential representing a student's progress and +skill acquisition within the LearnVault ecosystem. If these tokens were tradable +on secondary markets (DEXs), the correlation between token balance and actual +knowledge would be severed. + +## Decision + +We have implemented **LearnToken (LTK)** as a "Soulbound" token (SBT). The smart +contract logic explicitly overrides the standard transfer functions to prevent +movement between accounts. + +## Consequences + +### Positive + +- **Proof of Merit:** A user's balance is a verifiable history of their + completed courses and milestones, which cannot be "bought." +- **Sybil Resistance:** Discourages the creation of multiple accounts to farm + tokens for profit, as the tokens have no market value. +- **Employer Trust:** Partners can trust that a student holding 500 LTK actually + completed the required curriculum. + +### Negative + +- **Liquidity:** Users cannot "cash out" their reputation, which might reduce + short-term engagement for profit-seeking users. +- **Recovery Complexity:** If a student loses access to their Stellar secret + key, the tokens cannot be easily moved to a new wallet without administrative + intervention (clawback). diff --git a/docs/adr/ADR-003.md b/docs/adr/ADR-003.md new file mode 100644 index 00000000..3341c396 --- /dev/null +++ b/docs/adr/ADR-003.md @@ -0,0 +1,34 @@ +# ADR-003: Why milestone-based escrow over lump-sum scholarships + +**Status**: Accepted **Date**: 2026-03-24 + +## Context + +Traditional educational funding often disburses total scholarship amounts +upfront. In a decentralized environment, this creates a high risk of "capital +flight" where recipients exit the platform after receiving funds without +completing the curriculum. + +## Decision + +We have implemented a **Milestone-Based Escrow** system using Soroban smart +contracts. Funds are locked in a contract and released in tranches only upon +cryptographic proof of milestone completion. + +## Consequences + +### Positive + +- **Incentive Alignment:** Students remain motivated to complete the entire + course to unlock the full grant. +- **Capital Efficiency:** Unclaimed funds from dropped courses can be + programmatically returned to the treasury or reallocated to active students. +- **Risk Mitigation:** Limits the loss per student in case of fraud or low + engagement to a single milestone's value. + +### Negative + +- **Transaction Overhead:** Requires multiple on-chain interactions (one per + milestone) instead of a single disbursement. +- **Complexity:** Requires a robust verification layer (ADR-004) to trigger the + releases. diff --git a/docs/adr/ADR-004.md b/docs/adr/ADR-004.md new file mode 100644 index 00000000..97397671 --- /dev/null +++ b/docs/adr/ADR-004.md @@ -0,0 +1,30 @@ +# ADR-004: Why validator committee in V1 vs oracle in V2 + +**Status**: Accepted **Date**: 2026-03-24 + +## Context + +Releasing escrowed funds requires external verification that a student has +passed a test or submitted a project. We needed to choose between a +decentralized Oracle (like Chainlink) or a trusted Validator Committee. + +## Decision + +For the **V1 MVP**, we are using a **Validator Committee** of known educational +partners. We will migrate to a permissionless **Oracle/ZK-Proof** system in V2. + +## Consequences + +### Positive + +- **Speed to Market:** Setting up a multi-sig validator set is faster than + integrating complex cross-chain oracles for custom meteorological data. +- **Human Oversight:** Subject matter experts can manually audit edge cases in + project submissions that automated oracles might miss. + +### Negative + +- **Centralization Risk:** V1 relies on the integrity of the initial committee + members. +- **Scalability:** Manual validation limits the number of students the platform + can support until V2 automation is deployed. diff --git a/docs/adr/ADR-005.md b/docs/adr/ADR-005.md new file mode 100644 index 00000000..95472be1 --- /dev/null +++ b/docs/adr/ADR-005.md @@ -0,0 +1,35 @@ +# ADR-005: Why PostgreSQL for off-chain data vs a pure on-chain approach + +**Status**: Accepted **Date**: 2026-03-24 + +## Context + +LearnVault generates significant metadata, including course content, student +profile details, and high-resolution atmospheric data for meteorology +assignments. We needed to decide what belongs on-chain (Stellar) and what +belongs in a traditional database. + +## Decision + +We have adopted a **Hybrid Architecture**: Stellar for value transfer (Escrow) +and identity (SBT), and **PostgreSQL** for all relational off-chain data. + +## Consequences + +### Positive + +- **Search Performance:** Complex joins and full-text searches on student + progress are near-instant compared to indexing blockchain events. +- **Cost Efficiency:** Storing 1GB of course videos and PDFs on-chain would cost + thousands of XLM; PostgreSQL handles this at negligible cost. +- **GDPR Compliance:** Allows for "Right to be Forgotten" requests by deleting + off-chain PII (Personally Identifiable Information) while keeping the + cryptographic proof on-chain. + +### Negative + +- **Centralization:** The database becomes a single point of failure unless + managed with high availability (RDS/Cloud SQL). +- **Data Integrity:** Requires a hashing mechanism to "anchor" database + snapshots to the blockchain to ensure off-chain data hasn't been tampered + with. diff --git a/docs/adr/ADR-006.md b/docs/adr/ADR-006.md new file mode 100644 index 00000000..160fa042 --- /dev/null +++ b/docs/adr/ADR-006.md @@ -0,0 +1,33 @@ +# ADR-006: Why USDC as the treasury stablecoin + +**Status**: Accepted **Date**: 2026-03-24 + +## Context + +The treasury holds funds donated by sponsors to be paid out to students over +6–12 month periods. Using volatile assets like XLM or BTC risks the treasury +losing half its value before a student completes a course. + +## Decision + +We have selected **USDC (issued by Centre/Circle)** as the primary treasury and +payout stablecoin on Stellar. + +## Consequences + +### Positive + +- **Regulatory Clarity:** USDC is highly regulated and widely accepted by the + educational institutions we partner with. +- **Stellar Native Support:** USDC is a first-class citizen on Stellar with deep + liquidity, making it easy for students to off-ramp to local fiat (NGN, etc.) + via MoneyGram Access. +- **Price Stability:** Ensures a $500 scholarship remains worth $500 regardless + of market swings. + +### Negative + +- **Counterparty Risk:** We depend on Circle's ability to maintain the 1:1 peg + and the solvency of the underlying reserves. +- **Centralization:** Circle has the technical ability to freeze USDC addresses + if required by law enforcement. diff --git a/docs/adr/ADR-007.md b/docs/adr/ADR-007.md new file mode 100644 index 00000000..fedb348c --- /dev/null +++ b/docs/adr/ADR-007.md @@ -0,0 +1,34 @@ +# ADR-007: Frontend state management: TanStack Query vs Redux vs Zustand + +**Status**: Accepted **Date**: 2026-03-24 + +## Context + +The LearnVault frontend needs to handle complex asynchronous states (fetching +Stellar account balances, course progress, and weather data) alongside simple +global UI states (theme, user session). We evaluated Redux, Zustand, and +TanStack Query. + +## Decision + +We have selected **TanStack Query** for server-state (API/Blockchain data) and +**Zustand** for the remaining lightweight global client-state. + +## Consequences + +### Positive + +- **Automatic Caching:** TanStack Query handles the "stale-while-revalidate" + logic for blockchain data out of the box, reducing unnecessary RPC calls to + Soroban nodes. +- **Boilerplate Reduction:** Zustand provides a much cleaner, hook-based API + compared to Redux, which is critical for maintaining our Next.js frontend. +- **Developer Experience:** Clear separation between "data from the server" and + "local UI state" makes the codebase easier to navigate for new contributors. + +### Negative + +- **Learning Curve:** New contributors must understand two different state + paradigms (Query vs Store). +- **Bundle Size:** Adding two libraries instead of one, though both are + significantly smaller than the Redux ecosystem. diff --git a/docs/adr/ADR-008.md b/docs/adr/ADR-008.md new file mode 100644 index 00000000..4b1f704d --- /dev/null +++ b/docs/adr/ADR-008.md @@ -0,0 +1,46 @@ +# ADR-008: Consistent u64 Token IDs in ScholarNFT Contract + +## Status + +Resolved + +## Context + +An issue was filed reporting that `owner_of()` in +`contracts/scholar_nft/src/lib.rs` used `token_id: u32` while the rest of the +contract used `u64` for token IDs. The concern was that this type mismatch would +cause either a compile error or silent truncation when token IDs exceeded the +`u32` max value (4,294,967,295). + +## Investigation + +Upon auditing `contracts/scholar_nft/src/lib.rs`, all token ID references were +already using `u64`: + +- `DataKey` enum variants — `Owner(u64)`, `Metadata(u64)`, `TokenUri(u64)` — all + use `u64` +- `mint()` returns `u64` +- `owner_of(token_id: u64)` — already `u64` +- `token_uri(token_id: u64)` — already `u64` +- `transfer(_token_id: u64)` — already `u64` +- `get_metadata(token_id: u64)` — already `u64` +- `token_counter()` stores and returns `u64` +- All test cases in `test.rs` pass `u64` values and assert against `u64` return + types + +The bug described in the issue was not present in the codebase at the time of +review. It had either been fixed prior to the issue being filed, or the issue +referenced an older version of the file. + +## Decision + +No code changes were required. The contract already uses `u64` consistently for +all token ID parameters, return types, storage keys, and internal helpers. + +## Consequences + +- Token IDs can safely represent values up to 2^64 − 1, well beyond any + realistic minting volume. +- No risk of silent truncation when comparing or storing token IDs. +- The contract is consistent with Soroban best practices for numeric types in + storage keys. diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 00000000..d7eb60a3 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,298 @@ +# LearnVault Courses API + +OpenAPI-style documentation for course and lesson endpoints served by the +backend. + +## Authentication + +Admin-only endpoints accept either: + +- `Authorization: Bearer ` (must resolve to admin role/allowlist) +- `x-api-key: ` + +### Security Responses + +- `401` `{ "error": "Unauthorized" }` +- `403` `{ "error": "Forbidden" }` + +--- + +## GET `/api/courses` + +List published courses with optional filtering and pagination. + +### Query Parameters + +- `track` (string, optional) - case-insensitive exact match +- `difficulty` (string, optional) - one of: `beginner`, `intermediate`, + `advanced` +- `page` (number, optional, default `1`) +- `limit` (number, optional, default `12`, max `50`) + +### Responses + +- `200` + +```json +{ + "data": [ + { + "id": 1, + "slug": "stellar-basics", + "title": "Stellar Basics", + "description": "Intro to Stellar", + "coverImage": "https://cdn.example.com/cover.png", + "track": "web3", + "difficulty": "beginner", + "published": true, + "createdAt": "2026-01-10T12:00:00.000Z", + "updatedAt": "2026-01-12T15:00:00.000Z" + } + ], + "page": 1, + "limit": 12, + "total": 1, + "totalPages": 1 +} +``` + +- `500` `{ "error": "Internal server error" }` + +### Example + +Request: + +```http +GET /api/courses?track=web3&difficulty=beginner&page=1&limit=12 +``` + +--- + +## GET `/api/courses/{slug}` + +Fetch one published course including its lessons ordered ascending by lesson +order. + +### Path Parameters + +- `slug` (string, required) + +### Responses + +- `200` + +```json +{ + "id": 1, + "slug": "stellar-basics", + "title": "Stellar Basics", + "description": "Intro to Stellar", + "coverImage": null, + "track": "web3", + "difficulty": "beginner", + "published": true, + "createdAt": "2026-01-10T12:00:00.000Z", + "updatedAt": "2026-01-12T15:00:00.000Z", + "lessons": [ + { + "id": 10, + "courseId": 1, + "title": "Wallet Setup", + "content": "Lesson markdown...", + "order": 1, + "quiz": [], + "createdAt": "2026-01-10T12:30:00.000Z", + "updatedAt": "2026-01-10T12:30:00.000Z" + } + ] +} +``` + +- `404` `{ "error": "Course not found" }` +- `500` `{ "error": "Internal server error" }` + +### Example + +Request: + +```http +GET /api/courses/stellar-basics +``` + +--- + +## GET `/api/courses/{slug}/lessons/{id}` + +Fetch one lesson by ID and ensure it belongs to the course identified by `slug`. + +### Path Parameters + +- `slug` (string, required) +- `id` (number, required) + +### Responses + +- `200` + +```json +{ + "id": 10, + "courseId": 1, + "title": "Wallet Setup", + "content": "Lesson markdown...", + "order": 1, + "quiz": [ + { + "question": "What is Stellar?", + "options": ["A network", "A wallet"], + "correctIndex": 0 + } + ], + "createdAt": "2026-01-10T12:30:00.000Z", + "updatedAt": "2026-01-10T12:30:00.000Z" +} +``` + +- `404` `{ "error": "Lesson not found" }` +- `500` `{ "error": "Internal server error" }` + +### Example + +Request: + +```http +GET /api/courses/stellar-basics/lessons/10 +``` + +--- + +## POST `/api/courses` (admin only) + +Create a new course. Courses are created unpublished by default. + +### Headers + +- `Authorization: Bearer ` or `x-api-key: ` +- `Content-Type: application/json` + +### Request Body + +```json +{ + "title": "Soroban Fundamentals", + "slug": "soroban-fundamentals", + "description": "Learn Soroban smart contract basics", + "coverImage": "https://cdn.example.com/soroban.png", + "track": "web3", + "difficulty": "intermediate" +} +``` + +### Validation Rules + +- Required: `title`, `slug`, `track`, `difficulty` +- `difficulty` must be: `beginner` | `intermediate` | `advanced` + +### Responses + +- `201` (created course object) +- `400` `{ "error": "title is required", "field": "title" }` +- `401` `{ "error": "Unauthorized" }` +- `403` `{ "error": "Forbidden" }` +- `409` `{ "error": "Slug already exists" }` +- `500` `{ "error": "Internal server error" }` + +### Example + +Request: + +```http +POST /api/courses +Authorization: Bearer +Content-Type: application/json +``` + +Response: + +```json +{ + "id": 11, + "slug": "soroban-fundamentals", + "title": "Soroban Fundamentals", + "description": "Learn Soroban smart contract basics", + "coverImage": "https://cdn.example.com/soroban.png", + "track": "web3", + "difficulty": "intermediate", + "published": false, + "createdAt": "2026-03-26T12:00:00.000Z", + "updatedAt": "2026-03-26T12:00:00.000Z" +} +``` + +--- + +## PUT `/api/courses/{id}` (admin only) + +Partially update a course by ID. + +### Headers + +- `Authorization: Bearer ` or `x-api-key: ` +- `Content-Type: application/json` + +### Path Parameters + +- `id` (number, required) + +### Request Body + +Any subset of: + +```json +{ + "title": "Updated title", + "slug": "updated-slug", + "description": "Updated description", + "coverImage": "https://cdn.example.com/new-cover.png", + "track": "frontend", + "difficulty": "advanced", + "published": true +} +``` + +### Responses + +- `200` (updated course object) +- `400` `{ "error": "No valid fields provided" }` +- `401` `{ "error": "Unauthorized" }` +- `403` `{ "error": "Forbidden" }` +- `404` `{ "error": "Course not found" }` +- `409` `{ "error": "Slug already exists" }` +- `500` `{ "error": "Internal server error" }` + +### Example + +Request: + +```http +PUT /api/courses/11 +x-api-key: replace_with_secure_admin_api_key +Content-Type: application/json +``` + +Response: + +```json +{ + "id": 11, + "slug": "updated-slug", + "title": "Updated title", + "description": "Updated description", + "coverImage": "https://cdn.example.com/new-cover.png", + "track": "frontend", + "difficulty": "advanced", + "published": true, + "createdAt": "2026-03-26T12:00:00.000Z", + "updatedAt": "2026-03-26T12:20:00.000Z" +} +``` diff --git a/docs/architecture.mmd b/docs/architecture.mmd new file mode 100644 index 00000000..2ded4601 --- /dev/null +++ b/docs/architecture.mmd @@ -0,0 +1,12 @@ +graph TD + User([User / Learner]) -->|1. Submit Coursework| WebApp[Frontend App] + WebApp -->|2. Request Verification| API[Backend / API API] + API -->|3. Validate Code/Quizzes| Oracle[Testing Oracle] + Oracle -->|4. Return Result| API + API -->|5. Invoke Contract| SC_Core[LearnVault Core Contract] + + SC_Core -->|6. Mint/Transfer Tokens| LRN_Token{LRN Token Contract} + SC_Core -->|7. Issue Credential| SBT_Contract{Soulbound Credential} + + Treasury[Treasury Contract] -.->|Funds| LRN_Token + DAO[Governance Contract] -->|Parameters| SC_Core diff --git a/docs/architecture.png b/docs/architecture.png new file mode 100644 index 00000000..74d9f323 Binary files /dev/null and b/docs/architecture.png differ diff --git a/docs/brand-guide.md b/docs/brand-guide.md new file mode 100644 index 00000000..c6b715b1 --- /dev/null +++ b/docs/brand-guide.md @@ -0,0 +1,237 @@ +# LearnVault Brand Guide + +> Built for African learners. Powered by community. Governed by effort. + +--- + +## Colours + +### Primary Palette + +| Name | Hex | Usage | +| ------------- | --------- | ---------------------------------------- | +| Brand Cyan | `#00d2ff` | Primary accent, links, highlights | +| Brand Blue | `#3a7bd5` | Secondary accent, gradients | +| Brand Emerald | `#00ff80` | Success states, DeFi track, earn actions | +| Brand Purple | `#8e2de2` | Soroban track, governance, DAO features | + +### Neutrals + +| Name | Hex | Usage | +| -------------- | ------------------------ | ------------------------- | +| Background | `#05070a` | Page background | +| Surface | `#0d1117` | Cards, panels | +| Surface Alt | `#11141a` | Elevated surfaces, modals | +| Border | `rgba(255,255,255,0.08)` | Subtle borders | +| Text Primary | `#ffffff` | Headings, body | +| Text Secondary | `rgba(255,255,255,0.55)` | Descriptions, labels | +| Text Muted | `rgba(255,255,255,0.25)` | Placeholders, metadata | + +### Light Mode Equivalents + +| Name | Hex | +| ------------- | --------- | +| Brand Cyan | `#0099cc` | +| Brand Blue | `#2a5fa8` | +| Brand Emerald | `#00b85c` | +| Brand Purple | `#6a1fa8` | +| Background | `#f8fafc` | +| Text Primary | `#1a1a2e` | + +### Gradients + +```css +/* Iridescent brand gradient */ +linear-gradient(to right, #00d2ff, #3a7bd5, #00ff80) + +/* Full spectrum (borders, special elements) */ +linear-gradient(to right, #00d2ff, #3a7bd5, #00ff80, #8e2de2) + +/* Background mesh */ +radial-gradient(at 0% 0%, rgba(0,210,255,0.15) 0px, transparent 50%), +radial-gradient(at 50% 0%, rgba(58,123,213,0.15) 0px, transparent 50%), +radial-gradient(at 100% 0%, rgba(0,255,128,0.15) 0px, transparent 50%), +radial-gradient(at 0% 100%, rgba(142,45,226,0.15) 0px, transparent 50%), +radial-gradient(at 100% 100%, rgba(0,210,255,0.15) 0px, transparent 50%) +``` + +--- + +## Typography + +### Typefaces + +| Role | Font | Fallback | +| --------- | -------------------------- | ------------------------ | +| Primary | Inter | system-ui, -apple-system | +| Monospace | JetBrains Mono / Fira Code | monospace | + +Inter is available via Google Fonts: `https://fonts.google.com/specimen/Inter` + +### Scale + +| Token | Size | Weight | Usage | +| ---------- | ---- | ------ | ------------------------ | +| Display | 72px | 800 | Hero headings, OG images | +| H1 | 52px | 800 | Page titles | +| H2 | 36px | 700 | Section headings | +| H3 | 24px | 600 | Card titles | +| Body Large | 20px | 400 | Lead paragraphs | +| Body | 16px | 400 | Default body text | +| Small | 14px | 400 | Labels, metadata | +| Micro | 12px | 400 | Captions, legal | + +### Letter Spacing + +- Display / H1: `-1px` to `-2px` +- Badges / labels: `+2px` to `+3px` (uppercase) +- Body: `0` (default) + +--- + +## Logo + +### Variants + +| File | Use case | +| --------------------------------- | ----------------------------- | +| `logos/learnvault-logo-dark.svg` | Default — dark backgrounds | +| `logos/learnvault-logo-light.svg` | Light backgrounds | +| `logos/learnvault-icon-dark.svg` | Icon only — dark backgrounds | +| `logos/learnvault-icon-light.svg` | Icon only — light backgrounds | +| `logos/favicon-32.svg` | Browser tab (32×32) | +| `logos/favicon-16.svg` | Browser tab (16×16) | + +### Clear Space + +Maintain a minimum clear space equal to the height of the "L" in the wordmark on +all sides of the logo. + +### Don'ts + +- Do not recolour the logo outside the approved palette +- Do not stretch or distort proportions +- Do not place on busy backgrounds without a backdrop +- Do not use the light logo on dark backgrounds or vice versa + +--- + +## Iconography + +The LearnVault icon is a shield containing a graduation cap with an LRN token +badge. It represents: + +- Shield — security, trust, on-chain proof +- Graduation cap — learning, achievement +- LRN badge — earn-to-learn model + +--- + +## Track Accent Colours + +Each course track has a dedicated accent colour used in NFT credentials, cover +images, and UI theming. + +| Track | Accent Colour | Hex | +| ----------------------- | ------------- | --------- | +| Introduction to Stellar | Cyan | `#00d2ff` | +| Soroban Smart Contracts | Purple | `#8e2de2` | +| DeFi Fundamentals | Emerald | `#00ff80` | + +--- + +## ScholarNFT Credentials + +Base template: `nft/scholar-nft-base.svg` (1000×1000px) + +Track variants swap the accent gradient and include a track-specific background +motif: + +| File | Track | Accent | +| ----------------------------- | ----------------------- | ----------------- | +| `nft/scholar-nft-base.svg` | Generic / base | Cyan→Blue→Emerald | +| `nft/scholar-nft-stellar.svg` | Introduction to Stellar | Cyan→Blue | +| `nft/scholar-nft-soroban.svg` | Soroban Smart Contracts | Purple→Cyan | +| `nft/scholar-nft-defi.svg` | DeFi Fundamentals | Emerald→Blue | + +NFT credentials are soulbound and non-transferable. The artwork must remain +recognisable at social card sizes (minimum 400×400px). + +--- + +## Open Graph Images + +| File | Dimensions | Usage | +| -------------------- | ---------- | ------------------------------ | +| `og/og-homepage.svg` | 1200×630px | Homepage meta tag | +| `og/og-fallback.svg` | 1200×630px | Generic fallback for all pages | + +For production, export SVGs to PNG at 2× density (2400×1260px) using a tool like +`sharp`, `Inkscape`, or `resvg`. + +```bash +# Example using resvg +resvg og-homepage.svg og-homepage@2x.png --width 2400 +``` + +--- + +## Course Track Covers + +| File | Track | +| -------------------------------- | ----------------------- | +| `covers/cover-intro-stellar.svg` | Introduction to Stellar | +| `covers/cover-soroban.svg` | Soroban Smart Contracts | +| `covers/cover-defi.svg` | DeFi Fundamentals | + +Covers are designed at 1200×630px and double as social share cards. + +--- + +## Voice & Tone (Visual) + +- Dark-first: all primary assets are designed for dark backgrounds +- Iridescent gradients signal trust and innovation +- Subtle grid lines reference blockchain/data aesthetics +- Track-specific accent colours create a clear visual language per learning path +- The shield motif is the core brand symbol — use it consistently + +--- + +## Asset Directory + +``` +public/assets/brand/ +├── logos/ +│ ├── learnvault-logo-dark.svg +│ ├── learnvault-logo-light.svg +│ ├── learnvault-icon-dark.svg +│ ├── learnvault-icon-light.svg +│ ├── favicon-32.svg +│ └── favicon-16.svg +├── og/ +│ ├── og-homepage.svg +│ └── og-fallback.svg +├── nft/ +│ ├── scholar-nft-base.svg +│ ├── scholar-nft-stellar.svg +│ ├── scholar-nft-soroban.svg +│ └── scholar-nft-defi.svg +└── covers/ + ├── cover-intro-stellar.svg + ├── cover-soroban.svg + └── cover-defi.svg +``` + +--- + +## Contributing Design Assets + +Open a draft PR with your proposals. Include: + +1. The asset file(s) in the correct directory +2. A brief description of design decisions +3. Screenshots or previews in the PR description + +All assets must follow the colour palette and typography defined in this guide. +Community feedback is welcome before merging. diff --git a/docs/contract-upgrades.md b/docs/contract-upgrades.md new file mode 100644 index 00000000..25ab0fc5 --- /dev/null +++ b/docs/contract-upgrades.md @@ -0,0 +1,123 @@ +# Contract Upgrades + +## Scope + +The six V1 core contracts are upgradeable in place via Soroban WASM replacement: + +- `LearnToken` +- `GovernanceToken` +- `CourseMilestone` +- `ScholarshipTreasury` +- `MilestoneEscrow` +- `ScholarNFT` + +Each contract exposes: + +```rust +upgrade(new_wasm_hash: BytesN<32>) +``` + +The function calls Soroban's `update_current_contract_wasm(...)` host function +after authenticating the stored contract admin. + +## V1 Upgrade Authority + +In V1, the `admin` stored in each contract must be the founding team's Soroban +multi-sig or custom account contract. + +That matters because the contracts use `require_auth()` on the stored admin +address. Soroban resolves that authorization through the admin account itself, +so the effective signer policy is whatever N-of-M rule the multi-sig account +enforces. + +## V2 Upgrade Authority + +In V2, upgrade authority should be transferred from the founding-team multi-sig +to a governance-controlled address or controller contract after the DAO upgrade +flow is live. + +## Upgrade Event Model + +Each successful upgrade emits: + +- A contract event: `ContractUpgraded { old_hash, new_hash, upgraded_by }` +- Soroban's native system event: `executable_update` + +Important caveat: + +- Soroban contract code cannot directly read its currently installed executable + hash. +- Because of that limitation, the contract event tracks the last managed upgrade + hash in contract storage. +- On the first managed upgrade, `old_hash` is the all-zero sentinel value. +- The transaction's native `executable_update` system event remains the + canonical source for the exact pre-upgrade and post-upgrade executable refs. + +## Safe Upgrade Procedure + +1. Build the new WASM for the target contract. +2. Verify the diff, tests, and expected storage compatibility. +3. Upload the new WASM to Soroban and record the returned hash. +4. Prepare a transaction that calls `upgrade(new_wasm_hash)` on the target + contract. +5. Collect the required signatures from the founding-team multi-sig. +6. Submit the transaction. +7. Verify the transaction emitted both `ContractUpgraded` and + `executable_update`. +8. Run post-upgrade smoke checks against the contract's read methods and any + affected cross-contract flows. + +## Example CLI Flow + +The exact command flags vary by environment, but the operational sequence is: + +```bash +# 1. Build optimized contract wasm +stellar contract build + +# 2. Upload new wasm and capture the returned hash +stellar contract install \ + --network testnet \ + --source \ + --wasm target/wasm32v1-none/release/.wasm + +# 3. Invoke the in-place upgrade on the deployed contract +stellar contract invoke \ + --network testnet \ + --source \ + --id \ + -- \ + upgrade \ + --new_wasm_hash +``` + +## Storage Compatibility Rules + +Upgrades do not redeploy or migrate state automatically. The replacement WASM +continues using the same contract instance and persistent storage. + +Before executing an upgrade: + +- Do not rename or reinterpret existing storage keys unless the new code + includes an explicit migration path. +- Preserve the meaning and encoding of stored contract types. +- Keep cross-contract interfaces stable unless all dependent contracts and + clients are upgraded in lockstep. +- Re-run state-persistence tests for any contract whose storage schema changes. + +## Rollback Strategy + +Soroban upgrades are another WASM replacement. If a bad release is detected: + +1. Rebuild or retrieve the last known-good WASM. +2. Upload it again if needed. +3. Execute `upgrade(previous_wasm_hash)` through the same multi-sig flow. + +## Operational Notes + +- `LearnToken` and `GovernanceToken` upgrades must be coordinated with the + contracts that call them. +- `ScholarshipTreasury` and `MilestoneEscrow` upgrades are higher risk because + they custody funds and proposal state. +- For production upgrades, treat `cargo test --workspace` and targeted + end-to-end validation as mandatory gates, not optional checks. diff --git a/docs/contracts.md b/docs/contracts.md new file mode 100644 index 00000000..6a3d8873 --- /dev/null +++ b/docs/contracts.md @@ -0,0 +1,136 @@ +# LearnVault Smart Contract Reference + +## Contract Overview + +| Contract | Language | Purpose | +| ---------------------- | ------------ | ------------------------------------------------------------------------------------------- | +| `LearnToken` | Soroban/Rust | Soulbound reputation token (LRN) — minted on milestone completion, non-transferable | +| `GovernanceToken` | Soroban/Rust | Transferable DAO voting token (GOV) — minted to donors on deposit, earned by top learners | +| `CourseMilestone` | Soroban/Rust | Tracks learner progress per course, triggers LRN minting on verified checkpoint completion | +| `ScholarshipTreasury` | Soroban/Rust | Holds donor USDC funds, mints GOV to donors, creates escrows for approved proposals | +| `MilestoneEscrow` | Soroban/Rust | Manages tranche disbursements to scholars, returns unspent funds after 30-day inactivity | +| `ScholarNFT` | Soroban/Rust | Soulbound credential NFT minted to scholars who complete their funded program | +| `UpgradeTimelockVault` | Soroban/Rust | Isolated vault for secure contract upgrade timelocking with governance-controlled execution | + +--- + +## Contract Interaction Diagram + +```mermaid +graph TD + CM[CourseMilestone] -->|mint LRN on verified milestone| LT[LearnToken] + ST[ScholarshipTreasury] -->|mint GOV on deposit| GT[GovernanceToken] + ST -->|create_escrow on approved proposal| ME[MilestoneEscrow] + ME -->|mint credential on program completion| SN[ScholarNFT] +``` + +**Interaction summary:** + +- `CourseMilestone` → `LearnToken`: calls `mint` when a learner's checkpoint is + verified +- `ScholarshipTreasury` → `GovernanceToken`: calls `mint` proportional to a + donor's USDC deposit +- `ScholarshipTreasury` → `MilestoneEscrow`: calls `create_escrow` when a + scholarship proposal passes DAO vote +- `MilestoneEscrow` → `ScholarNFT`: calls `mint` when a scholar completes all + funded milestones + +### CourseMilestone Management + +- Courses must be registered on-chain by the configured contract admin using + `add_course(admin, course_id, milestone_count)`. +- Course management is admin-only: only the same admin address stored during + `initialize` can add or remove courses. +- Course lookup is available through: + - `get_course(course_id)` to fetch one course configuration + - `list_courses()` to return active course IDs +- Enrollment is validated against course registry state: + - `enroll(learner, course_id)` rejects unknown or inactive courses +- Course removal uses lifecycle deactivation: + - `remove_course(admin, course_id)` marks the course inactive instead of + deleting its record + - inactive courses remain readable via `get_course` but are excluded from + `list_courses` and cannot be newly enrolled into + +--- + +## Deployment Order + +Contracts must be deployed in this order due to cross-contract dependencies: + +1. **`LearnToken`** — no dependencies +2. **`GovernanceToken`** — no dependencies +3. **`ScholarNFT`** — no dependencies +4. **`UpgradeTimelockVault`** — no dependencies +5. **`CourseMilestone`** — requires `LearnToken` address +6. **`ScholarshipTreasury`** — requires `GovernanceToken` address +7. **`MilestoneEscrow`** — requires `ScholarshipTreasury` and `ScholarNFT` + addresses + +--- + +## Testnet Addresses + +> Fill in after deployment to Stellar Testnet. + +| Contract | Testnet Address | +| ---------------------- | --------------- | +| `LearnToken` | — | +| `GovernanceToken` | — | +| `CourseMilestone` | — | +| `ScholarshipTreasury` | — | +| `MilestoneEscrow` | — | +| `ScholarNFT` | — | +| `UpgradeTimelockVault` | — | + +--- + +## Upgrade Timelock Vault + +The `UpgradeTimelockVault` implements a dedicated vault pattern for secure +contract upgrades with timelock enforcement. + +For the current V1 in-place upgrade procedure used by the six core contracts, +see [contract-upgrades.md](./contract-upgrades.md). + +### Security Model + +The timelock vault provides the following security guarantees: + +1. **Isolated Storage**: Upgrade proposals are stored separately from governance + logic, preventing accidental modifications or exploits in the governance + contract from affecting queued upgrades. + +2. **Timelock Enforcement**: All upgrades must wait for a mandatory timelock + period (default 48 hours) before execution, providing time for community + review and potential cancellation. + +3. **Admin Control**: Only the vault admin can queue or cancel upgrades, + ensuring centralized control during the initial deployment phase. + +4. **Event-Driven Transparency**: All operations (queue, execute, cancel) emit + events for full transparency and monitoring. + +5. **Cancellation Capability**: Queued upgrades can be cancelled by the admin at + any time during the timelock period, providing a safety mechanism for + discovered issues. + +### Upgrade Flow + +1. **Queue**: Governance contract calls `queue_upgrade()` after proposal + approval +2. **Wait**: Community monitors the queued upgrade during timelock period +3. **Execute**: After timelock expires, governance contract calls + `execute_upgrade()` to retrieve the WASM hash and perform the upgrade +4. **Cancel**: Admin can cancel the upgrade at any time before execution + +### Integration with Governance + +The vault is designed to be used by the governance system: + +- The `ScholarshipTreasury` contract would be extended to handle upgrade + proposals +- Upon approval, it calls `vault.queue_upgrade(contract, wasm_hash)` +- After timelock, it calls `vault.execute_upgrade(contract)` and performs the + actual upgrade +- The vault enforces the timelock and provides isolated storage diff --git a/docs/contracts/upgrade-procedure.md b/docs/contracts/upgrade-procedure.md new file mode 100644 index 00000000..47ce0f1a --- /dev/null +++ b/docs/contracts/upgrade-procedure.md @@ -0,0 +1,132 @@ +# Contract Upgrade Procedure + +This runbook describes how to upgrade LearnVault Soroban contracts safely, from +proposal to verification. + +## When to Initiate an Upgrade + +Start an upgrade only when at least one of these is true: + +- Critical bug or vulnerability requires a patch. +- New protocol feature requires contract logic changes. +- Gas/performance improvements materially reduce operating cost. +- Governance-approved parameter/model changes cannot be done off-chain. +- Dependency/runtime changes require a rebuild for compatibility. + +## Governance Proposal Flow + +1. Draft a proposal containing: + - Target contract(s) + - Rationale and risk assessment + - Link to reviewed code diff + - Expected WASM hash(es) + - Rollback plan +2. Post proposal to governance forum/channel for discussion period. +3. Submit proposal on-chain (or through governance process used by the DAO). +4. Keep proposal open for voting until quorum window closes. + +## Timelock Vault Model + +The timelock is the delay layer between successful governance vote and execution: + +- Approved upgrades are queued with an execution timestamp. +- Execution before the delay window is blocked. +- Delay gives the community time to audit final payload/WASM hash. +- Emergency cancellation should remain available to governance/admin per policy. + +For LearnVault, treat treasury and escrow upgrades as high-risk and require full +timelock delay unless emergency mode is explicitly invoked. + +## Required Approvals and Quorum + +Define and enforce these before execution: + +- **Quorum**: minimum governance voting power participating. +- **Threshold**: percentage of cast votes required for approval. +- **Signer policy**: multisig requirement for final execution transaction. + +Recommended production baseline: + +- Quorum >= 20% of active voting power +- Approval threshold >= 60% +- Executor account guarded by multisig (for example, 2-of-3 or 3-of-5) + +## Deploy New WASM + +1. Build the contract artifact: + +```bash +cargo build --target wasm32v1-none --release +``` + +2. Run full tests before installation: + +```bash +cargo test --workspace +``` + +3. Install WASM to Soroban and capture returned hash: + +```bash +stellar contract install \ + --network \ + --source \ + --wasm target/wasm32v1-none/release/.wasm +``` + +4. Verify hash matches governance-approved payload. +5. Queue upgrade execution through governance timelock flow. + +## Execute Upgrade + +After timelock expires, invoke the contract upgrade: + +```bash +stellar contract invoke \ + --network \ + --source \ + --id \ + -- \ + upgrade \ + --new_wasm_hash +``` + +## Verify Upgrade Applied + +Immediately validate: + +1. Transaction status is `SUCCESS`. +2. Contract emits expected upgrade event(s), including hash reference. +3. Soroban system event `executable_update` exists for the transaction. +4. Read-only smoke checks succeed for key methods. +5. Cross-contract flows still pass integration tests. + +Recommended post-upgrade checks: + +- `cargo test --workspace` +- Backend integration tests touching upgraded contract paths +- Frontend critical-path smoke tests (enroll, milestone, governance, treasury) + +## Emergency Upgrade Procedure + +Use emergency mode only for severe incidents (active exploit, funds at risk, +protocol outage). + +1. Trigger incident declaration and freeze high-risk operations if possible. +2. Use admin multisig to bypass timelock per emergency governance policy. +3. Install and execute the patched WASM immediately. +4. Publish incident note with: + - Why bypass was required + - Exact hash deployed + - Time of execution + - Follow-up remediation plan +5. Open a retroactive governance report and security postmortem. + +## Rollback + +If regression is detected: + +1. Reinstall last known-good WASM (if needed). +2. Invoke `upgrade` with previous trusted hash. +3. Re-run verification and smoke tests. +4. Keep incident log updated until full recovery is confirmed. diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md new file mode 100644 index 00000000..ea94b71f --- /dev/null +++ b/docs/contributing/testing.md @@ -0,0 +1,148 @@ +# Testing Strategy + +This document defines LearnVault's testing pyramid, tooling, and CI +expectations. + +## Testing Pyramid + +### 1) Unit Tests (fast, high volume) + +- Scope: isolated functions/components/contracts. +- Tools: + - Rust contracts: `cargo test --workspace` + - Frontend/unit: `vitest` + - Server unit/API handlers: `jest` (under `server`) + +### 2) Integration Tests (service and contract boundaries) + +- Scope: backend + database + contract client integration. +- Validates migrations, DB I/O, auth middleware, and contract service adapters. +- Run primarily from `server` test suite and migration verification scripts. + +### 3) End-to-End Tests (user journeys) + +- Scope: browser-based critical paths. +- Tool: Playwright (`e2e` directory). +- Uses preview build in CI via `playwright.config.ts` web server. + +### 4) Contract Tests (on-chain behavior) + +- Scope: Soroban contract logic, invariants, and event behavior. +- Tooling: Rust test framework plus optional ignored fuzz tests. +- Includes workspace-level tests and dedicated contract CI workflows. + +## How to Run Each Suite + +From repository root: + +```bash +# Contracts (workspace) +npm run test + +# Frontend unit/integration-style tests +npm run test:frontend + +# E2E browser tests +npm run test:e2e + +# Frontend + contract coverage report (frontend coverage command shown) +npm run test:coverage +``` + +From `server/` directory: + +```bash +# Server tests +npm test + +# Database migrations +npm run migrate +npm run migrate:verify +``` + +## Coverage Targets + +Set minimum targets per layer: + +- Contracts (Rust): >= 90% on critical contract modules. +- Server/API: >= 80% branch coverage on controllers/middleware/services. +- Frontend: >= 75% branch coverage on core user flows. +- E2E: 100% of critical path scenarios (connect wallet, submit milestone, + review, treasury/governance actions). + +PRs that reduce critical-path coverage should include justification and +follow-up. + +## Mocking Strategy for Stellar Contracts + +Use a layered strategy: + +- **Unit level**: mock contract client wrappers and RPC adapters. +- **Server integration**: stub transaction submission at service boundaries + while preserving payload validation and auth checks. +- **Contract level**: prefer real Rust contract tests for business invariants. +- **E2E level**: avoid deep chain mocks; run against stable test network + fixtures or deterministic local network where possible. + +Guidelines: + +- Mock only network transport, not domain rules. +- Keep fixture data deterministic and versioned. +- Validate event payload shape in tests when upgrades touch contract events. + +## Writing New E2E Tests + +1. Add spec file under `e2e/` with clear journey-based naming. +2. Cover one user intent per test (avoid giant multi-purpose specs). +3. Use robust selectors (role/text/test IDs), not fragile CSS chains. +4. Assert both success and expected failure/validation states. +5. Keep tests independent; no hidden ordering dependencies. + +Run locally: + +```bash +npm run test:e2e +``` + +Debug mode: + +```bash +npx playwright test --debug +``` + +## Local Testnet Setup for Integration Tests + +Use local or testnet config depending on test scope: + +1. Set network env (`STELLAR_NETWORK`, `SOROBAN_RPC_URL`). +2. Provide contract IDs in `.env` and `server/.env`. +3. Ensure backend signer key (`STELLAR_SECRET_KEY`) is available for write-path + tests. +4. Run DB services and migrations before API integration tests. +5. Seed deterministic test data if scenario requires historical state. + +Minimum backend setup: + +```bash +cd server +npm ci +npm run migrate +npm test +``` + +## CI Test Matrix + +Current workflows execute tests by concern: + +- `build.yml`: lint, format, build, workspace tests. +- `contracts-ci.yml` and `contracts.yml`: contract build/test/clippy/fmt paths. +- `server-ci.yml` and `backend-tests.yml`: Postgres-backed server tests + + migrations. +- `frontend-ci.yml`: typecheck, lint, frontend tests, build. +- `e2e.yml`: Playwright journey tests on pull requests. + +Recommended PR checklist: + +- [ ] Relevant local test suites pass before push. +- [ ] CI jobs for touched area are green. +- [ ] Any skipped tests have explicit rationale in PR description. diff --git a/docs/cors-configuration.md b/docs/cors-configuration.md new file mode 100644 index 00000000..b4b0850b --- /dev/null +++ b/docs/cors-configuration.md @@ -0,0 +1,324 @@ +# CORS Configuration + +## Overview + +The LearnVault backend implements strict Cross-Origin Resource Sharing (CORS) +policies to ensure only authorized frontend domains can make API requests. This +prevents unauthorized websites from accessing the API and protects user data. + +## Configuration + +### Environment Variables + +Set the `FRONTEND_URL` environment variable to specify the allowed frontend +origin: + +```env +# Development +FRONTEND_URL=http://localhost:5173 + +# Production +FRONTEND_URL=https://learnvault.app +``` + +### Allowed Origins + +The backend automatically configures the following allowed origins: + +#### Production Mode (`NODE_ENV=production`) + +- `FRONTEND_URL` (from environment variable) +- `https://learnvault.app` (production domain) +- `https://www.learnvault.app` (production domain with www) + +#### Development Mode (`NODE_ENV=development`) + +- All production origins (for testing) +- `http://localhost:5173` (Vite default) +- `http://localhost:3000` (React/Next.js default) +- `http://localhost:5174` (Vite alternate port) +- `http://127.0.0.1:5173` (localhost IP variant) + +### CORS Settings + +The backend is configured with the following CORS options: + +```typescript +{ + origin: (origin, callback) => { + // Dynamic origin validation + }, + credentials: true, // Allow cookies and auth headers + methods: [ // Allowed HTTP methods + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "OPTIONS" + ], + allowedHeaders: [ // Allowed request headers + "Content-Type", + "Authorization" + ] +} +``` + +## How It Works + +### Origin Validation + +When a browser makes a cross-origin request: + +1. The browser sends a preflight OPTIONS request with the `Origin` header +2. The backend checks if the origin is in the `allowedOrigins` list +3. If allowed, the backend responds with appropriate CORS headers +4. If blocked, the backend logs a warning and returns a CORS error + +### No-Origin Requests + +Requests without an `Origin` header are automatically allowed. This includes: + +- Server-to-server API calls +- Mobile app requests +- Command-line tools (curl, Postman) +- Same-origin requests + +### Credentials Support + +The `credentials: true` setting allows: + +- Cookies to be sent with cross-origin requests +- Authorization headers to be included +- Authenticated API calls from the frontend + +## Security Benefits + +### Prevents Unauthorized Access + +Only whitelisted domains can make API requests, preventing: + +- Phishing sites from accessing the API +- Unauthorized third-party integrations +- Cross-site request forgery (CSRF) attacks + +### Protects User Data + +By restricting origins: + +- User authentication tokens are only sent to trusted domains +- Personal data cannot be accessed by malicious websites +- API rate limits apply per origin + +### Audit Trail + +Blocked CORS requests are logged: + +``` +CORS blocked request from origin: https://malicious-site.com +``` + +This helps identify potential security threats and unauthorized access attempts. + +## Development Setup + +### Local Development + +For local development, the default configuration works out of the box: + +```bash +# Start the backend (defaults to http://localhost:5173) +cd server +npm run dev +``` + +### Custom Port + +If your frontend runs on a different port, set `FRONTEND_URL`: + +```bash +# .env +FRONTEND_URL=http://localhost:3000 +``` + +### Multiple Developers + +Each developer can use their own `.env` file: + +```bash +# Developer A +FRONTEND_URL=http://localhost:5173 + +# Developer B +FRONTEND_URL=http://localhost:3000 +``` + +## Production Deployment + +### Setting the Frontend URL + +In production, always set `FRONTEND_URL` to your deployed frontend domain: + +```bash +# Production environment +NODE_ENV=production +FRONTEND_URL=https://learnvault.app +``` + +### Multiple Domains + +If you need to support multiple production domains, update the `allowedOrigins` +array in `server/src/index.ts`: + +```typescript +const allowedOrigins = [ + env.FRONTEND_URL || env.CORS_ORIGIN || "http://localhost:5173", + "https://learnvault.app", + "https://www.learnvault.app", + "https://app.learnvault.xyz", // Add additional domains + "https://staging.learnvault.app", +] +``` + +### Subdomain Support + +To allow all subdomains, modify the origin validation logic: + +```typescript +origin: (origin, callback) => { + if (!origin) { + return callback(null, true) + } + + // Allow all *.learnvault.app subdomains + if (origin.endsWith(".learnvault.app")) { + return callback(null, true) + } + + if (allowedOrigins.includes(origin)) { + callback(null, true) + } else { + console.warn(`CORS blocked request from origin: ${origin}`) + callback(new Error("Not allowed by CORS")) + } +} +``` + +## Testing CORS + +### Test Allowed Origin + +```bash +curl -H "Origin: https://learnvault.app" \ + -H "Access-Control-Request-Method: POST" \ + -H "Access-Control-Request-Headers: Content-Type" \ + -X OPTIONS \ + http://localhost:4000/api/courses +``` + +Expected response headers: + +``` +Access-Control-Allow-Origin: https://learnvault.app +Access-Control-Allow-Credentials: true +Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS +Access-Control-Allow-Headers: Content-Type,Authorization +``` + +### Test Blocked Origin + +```bash +curl -H "Origin: https://malicious-site.com" \ + -H "Access-Control-Request-Method: POST" \ + -X OPTIONS \ + http://localhost:4000/api/courses +``` + +Expected: CORS error and warning in server logs + +### Browser Testing + +Open the browser console on an unauthorized domain and try: + +```javascript +fetch("http://localhost:4000/api/courses", { + method: "GET", + credentials: "include", +}) + .then((r) => r.json()) + .catch((err) => console.error("CORS blocked:", err)) +``` + +## Troubleshooting + +### CORS Error in Development + +If you see CORS errors in development: + +1. Check that `FRONTEND_URL` matches your frontend URL +2. Verify the frontend is running on the expected port +3. Check server logs for "CORS blocked" warnings +4. Ensure `NODE_ENV` is set to `development` + +### CORS Error in Production + +If you see CORS errors in production: + +1. Verify `FRONTEND_URL` is set correctly +2. Check that the domain matches exactly (including https://) +3. Ensure `NODE_ENV=production` is set +4. Check for typos in the domain name +5. Review server logs for blocked origins + +### Credentials Not Sent + +If cookies or auth headers aren't being sent: + +1. Ensure `credentials: 'include'` is set in fetch requests +2. Verify `credentials: true` is set in CORS config +3. Check that the origin is in the allowed list +4. Ensure cookies have the correct `SameSite` attribute + +## Migration from Legacy Config + +### Old Configuration + +```typescript +app.use(cors({ origin: env.CORS_ORIGIN })) +``` + +### New Configuration + +```typescript +app.use(cors({ + origin: (origin, callback) => { + // Dynamic validation + }, + credentials: true, + methods: [...], + allowedHeaders: [...] +})) +``` + +### Environment Variables + +Old: + +```env +CORS_ORIGIN=http://localhost:5173 +``` + +New (recommended): + +```env +FRONTEND_URL=http://localhost:5173 +``` + +The old `CORS_ORIGIN` variable is still supported for backward compatibility but +`FRONTEND_URL` is preferred. + +## References + +- [MDN: CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) +- [Express CORS Middleware](https://expressjs.com/en/resources/middleware/cors.html) +- [OWASP: CORS Security](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) diff --git a/docs/credentials-api-example.md b/docs/credentials-api-example.md new file mode 100644 index 00000000..6cfbf338 --- /dev/null +++ b/docs/credentials-api-example.md @@ -0,0 +1,225 @@ +# Credentials API Usage Example + +## Overview + +This guide demonstrates how to use the `/api/credentials/metadata` endpoint to +generate NFT metadata and mint ScholarNFT credentials. + +## Prerequisites + +- Pinata API credentials configured in `server/.env`: + + ```env + PINATA_API_KEY=your_pinata_api_key + PINATA_SECRET=your_pinata_secret_api_key + ``` + +- Valid JWT token for authentication + +## Step 1: Generate Metadata + +Call the endpoint to generate and upload metadata to IPFS: + +```bash +curl -X POST http://localhost:4000/api/credentials/metadata \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -d '{ + "course_id": "stellar-basics", + "learner_address": "GABC123DEFGHIJKLMNOPQRSTUVWXYZ456789", + "completed_at": "2026-03-26T10:30:00Z" + }' +``` + +### Response + +```json +{ + "data": { + "metadata_uri": "ipfs://bafkreiabcdef1234567890ghijklmnopqrstuvwxyz", + "gateway_url": "https://gateway.pinata.cloud/ipfs/bafkreiabcdef1234567890ghijklmnopqrstuvwxyz", + "metadata": { + "name": "Introduction to Stellar & Soroban — Course Completion", + "description": "Issued to learners who complete all milestones in Introduction to Stellar & Soroban", + "image": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + "attributes": [ + { + "trait_type": "Course", + "value": "stellar-basics" + }, + { + "trait_type": "Course Title", + "value": "Introduction to Stellar & Soroban" + }, + { + "trait_type": "Completed At", + "value": "2026-03-26T10:30:00Z" + }, + { + "trait_type": "Learner", + "value": "GABC123DEFGHIJKLMNOPQRSTUVWXYZ456789" + }, + { + "trait_type": "Difficulty", + "value": "beginner" + } + ] + } + } +} +``` + +## Step 2: Mint the NFT + +Use the returned `metadata_uri` to mint the ScholarNFT: + +```typescript +import { Contract, SorobanRpc } from "@stellar/stellar-sdk" + +// Initialize contract +const rpcUrl = "https://soroban-testnet.stellar.org" +const server = new SorobanRpc.Server(rpcUrl) +const contractId = "CSCHOLAR_NFT_CONTRACT_ID" + +// Mint the credential +const metadataUri = "ipfs://bafkreiabcdef1234567890ghijklmnopqrstuvwxyz" +const learnerAddress = "GABC123DEFGHIJKLMNOPQRSTUVWXYZ456789" + +const contract = new Contract(contractId) +const tx = contract.call("mint", learnerAddress, metadataUri) + +// Sign and submit transaction +// ... (transaction signing and submission code) +``` + +## Step 3: Verify the Metadata + +You can view the metadata via the HTTP gateway: + +```bash +curl https://gateway.pinata.cloud/ipfs/bafkreiabcdef1234567890ghijklmnopqrstuvwxyz +``` + +## Available Courses + +The following course IDs are supported: + +- `stellar-basics` - Introduction to Stellar & Soroban +- `soroban-fundamentals` - Soroban Fundamentals +- `soroban-contracts` - Soroban Smart Contract Development +- `defi-fundamentals` - DeFi Fundamentals on Stellar + +## Error Handling + +### Course Not Found (404) + +```json +{ + "error": "Course not found", + "message": "No course found with id: invalid-course" +} +``` + +### Service Unavailable (503) + +```json +{ + "error": "Service unavailable", + "message": "IPFS pinning service is not configured. Please contact the administrator." +} +``` + +### Validation Error (400) + +```json +{ + "error": "Validation failed", + "details": [ + { + "field": "completed_at", + "message": "completed_at must be a valid ISO 8601 datetime" + } + ] +} +``` + +## Integration with Frontend + +Example React/TypeScript integration: + +```typescript +interface CreateMetadataRequest { + course_id: string + learner_address: string + completed_at: string +} + +interface MetadataResponse { + data: { + metadata_uri: string + gateway_url: string + metadata: { + name: string + description: string + image: string + attributes: Array<{ + trait_type: string + value: string + }> + } + } +} + +async function generateCredentialMetadata( + courseId: string, + learnerAddress: string, + completedAt: Date, +): Promise { + const response = await fetch("/api/credentials/metadata", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${getAuthToken()}`, + }, + body: JSON.stringify({ + course_id: courseId, + learner_address: learnerAddress, + completed_at: completedAt.toISOString(), + }), + }) + + if (!response.ok) { + throw new Error(`Failed to generate metadata: ${response.statusText}`) + } + + return response.json() +} + +// Usage +const metadata = await generateCredentialMetadata( + "stellar-basics", + "GABC123...", + new Date(), +) + +console.log("Metadata URI:", metadata.data.metadata_uri) +console.log("View at:", metadata.data.gateway_url) +``` + +## Notes + +- The `completed_at` timestamp must be in ISO 8601 format (e.g., + `2026-03-26T10:30:00Z`) +- The learner address must be a valid Stellar address (G... format) +- Metadata is permanently pinned to IPFS via Pinata +- The endpoint requires authentication via JWT token +- Badge images are pre-uploaded to IPFS (CIDs are hardcoded in the controller) + +## Next Steps + +1. Upload actual badge images to IPFS and update the `IMAGE_CID_MAP` in + `credentials.controller.ts` +2. Add authentication middleware to restrict who can generate metadata +3. Consider adding rate limiting to prevent abuse +4. Add database logging to track metadata generation +5. Implement webhook notifications when credentials are minted diff --git a/docs/csrf-protection.md b/docs/csrf-protection.md new file mode 100644 index 00000000..84d49ab2 --- /dev/null +++ b/docs/csrf-protection.md @@ -0,0 +1,178 @@ +# CSRF Protection + +## TL;DR + +The LearnVault API is **bearer-token only** — JWTs travel in the `Authorization` +header and never in a cookie — which eliminates classical cookie-riding CSRF on +its own. On top of that, a dedicated `requireTrustedOrigin` middleware rejects +state-changing requests (`POST`/`PUT`/`PATCH`/`DELETE`) whose `Origin` or +`Referer` header does not match the configured allowlist. Together these give +browser-mediated CSRF no path through the API surface. This document explains +both layers and the invariants that keep them true. + +## Threat model + +CSRF (Cross-Site Request Forgery) is the attack where a victim, already +authenticated to `api.learnvault.app`, visits `evil.example` in the same +browser. JavaScript on `evil.example` triggers a state-changing request to the +API, and the browser silently attaches the victim's ambient credentials. + +Two things are required for this attack to succeed: + +1. The victim's browser must **auto-attach credentials** to the cross-origin + request (cookies, HTTP basic auth, or a client TLS cert). +2. The API must **accept** the request on the basis of those credentials. + +Remove either leg and CSRF cannot occur. + +## Why the LearnVault API is not vulnerable + +### The API does not authenticate via cookies + +Authentication is exclusively a JWT delivered in the `Authorization: Bearer` +header. The token is issued by `POST /api/auth/verify` (wallet signature +challenge flow) and stored by the frontend in `localStorage`, not in a cookie. + +Audit evidence: + +- No cookie-parser or session middleware is installed + (`grep -R "cookie-parser\|express-session" server/` is empty). +- No route calls `res.cookie(...)` or reads `req.cookies` + (`grep -R "req\.cookies\|res\.cookie" server/src` is empty). +- The auth middleware (`server/src/middleware/auth.middleware.ts`) only reads + the `Authorization` header. + +Because the server never sets an auth cookie, browsers have nothing to +auto-attach on cross-origin requests — the "ambient credential" leg of the +attack is absent. + +### Custom headers trigger a CORS preflight + +The `Authorization` header is not one of the +[CORS-safelisted request headers](https://fetch.spec.whatwg.org/#cors-safelisted-request-header). +Any cross-origin request that includes it is a "non-simple" request, and the +browser issues a preflight `OPTIONS` before the real request. If the preflight +response does not include an `Access-Control-Allow-Origin` matching the +attacker's origin, the browser refuses to send the real request at all. + +Attacker-controlled code in the browser **cannot read the victim's token out of +`localStorage` on a different origin** (Same-Origin Policy), so even if an +attacker crafted a form that bypassed preflight, they have no way to populate +the `Authorization` header with the victim's JWT. + +### CORS is a strict allowlist + +`server/src/index.ts` configures `cors` with a validator function that rejects +any origin not in the allowlist (production: `learnvault.app` and +`www.learnvault.app`; development: local dev ports). Rejected origins produce no +`Access-Control-Allow-Origin` header, and the cors middleware forwards the +rejection to the error handler so the route never runs. + +### `requireTrustedOrigin` middleware (explicit CSRF gate) + +`server/src/middleware/csrf.middleware.ts` exports +`createRequireTrustedOrigin(allowedOrigins)`. Mounted globally after `cors` and +before the route handlers, it applies only to state-changing methods +(`POST`/`PUT`/`PATCH`/`DELETE`) and enforces: + +- If `Origin` is present, it must be in the allowlist. Otherwise → `403`. +- If `Origin` is absent but `Referer` is present, the parsed origin of the + `Referer` must be in the allowlist. Otherwise → `403`. +- If both are absent, the request passes. This preserves server-to-server + clients (curl, Postman, internal workers) which have no browser fingerprint to + validate. Bearer auth is the load-bearing defense for that path; the gate is + defense-in-depth layered on top. + +Why a dedicated middleware on top of CORS: + +- The cors middleware only inspects `Origin`. `requireTrustedOrigin` also + validates `Referer` when `Origin` is missing, which covers edge cases (some + redirect chains, older clients) where a browser might omit `Origin`. +- CORS rejections surface as generic errors via the error handler. The dedicated + gate returns a specific `403 Forbidden: untrusted origin`, which is easier to + diagnose and audit. +- Pinning the check in a named middleware means a regression shows up as a + changed middleware chain, not a subtle CORS option flip. + +These are all defense-in-depth layers. CORS and `Origin`/`Referer` validation +are enforced by the browser/client; a non-browser attacker can forge any header +they want. **Do not rely on either as an authentication boundary** — +per-endpoint authentication is what protects unauthorized clients. + +## Invariants that must be preserved + +The "CSRF is not applicable" claim holds only while every item below is true. If +any change violates one of these, this document and the corresponding test +(`server/src/tests/csrf.test.ts`) must be revisited. + +1. **No auth cookies.** The server must not call `res.cookie(...)` for any + session, JWT, or auth-equivalent value. Non-auth cookies (e.g. a preference + flag with no privilege attached) are acceptable but discouraged since they + muddy the invariant. +2. **No cookie-parser / session middleware.** `cookie-parser`, + `express-session`, `passport`-with-sessions, or similar must not be + installed. +3. **Bearer header is the only authenticator.** Any new authentication + middleware must read credentials from headers (or a signed request body), + never from `req.cookies` or an ambient client-cert. +4. **CORS stays an allowlist.** The `cors` middleware in `server/src/index.ts` + must not be broadened to `origin: true` or `origin: "*"`. +5. **Never reflect arbitrary `Origin`.** Do not echo `req.headers.origin` into + `Access-Control-Allow-Origin` without checking it against the allowlist. +6. **`requireTrustedOrigin` stays wired in.** It must be mounted globally in + `server/src/index.ts` before the route handlers, and the allowlist passed to + it must match the cors allowlist. Removing it or narrowing its method set + must be paired with an equivalent replacement. + +## What would change this + +Introducing cookie-based authentication (e.g. a refresh-token cookie, an SSR +session, or a shared-cookie architecture with a sibling subdomain) changes the +threat model. At that point this API becomes CSRF-exploitable and we must add, +at minimum: + +- `SameSite=Strict` (or `Lax` with explicit justification) on every auth cookie. +- `Secure` and `HttpOnly` on every auth cookie. +- A CSRF token (double-submit cookie or synchronizer pattern) validated on every + state-changing endpoint. +- Tests proving the token is required. + +Do not introduce cookie auth without also landing those defenses in the same PR. + +## Related authorization gaps (not CSRF) + +The audit that produced this document flagged several state-changing endpoints +that currently require no authentication at all — for example +`POST /api/governance/proposals`, `POST /api/governance/vote`, +`POST /api/scholarships/apply`, `POST /api/milestones/submit`. + +These are **not** CSRF vulnerabilities — they have no auth to forge — but they +are authorization gaps. They should be tracked and fixed separately under a +"require auth for state-changing endpoints" task. This document addresses the +CSRF question only. + +## Test coverage + +`server/src/tests/csrf.test.ts` enforces the invariants above: + +- A preflight from a disallowed origin does not grant + `Access-Control-Allow-Origin`. +- A POST from a disallowed origin is blocked at the CORS layer. +- A POST without `Authorization` is rejected with 401. +- A POST with an invalid Bearer token is rejected with 401. +- A successful POST on a protected endpoint sets no `Set-Cookie` header. +- A POST to an unauthenticated endpoint is rejected with 403 when `Origin` is + untrusted (`requireTrustedOrigin`). +- A POST is rejected with 403 when only `Referer` is set and it is untrusted or + malformed. +- A POST succeeds when only `Referer` is set and it is trusted. +- A POST with neither `Origin` nor `Referer` passes the gate (permissive + server-to-server posture). +- GETs are not gated by `requireTrustedOrigin`. + +## References + +- [OWASP: CSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) +- [MDN: CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) +- [`docs/cors-configuration.md`](./cors-configuration.md) — CORS allowlist + configuration, which is the defense-in-depth layer described above. diff --git a/docs/database/indexes.md b/docs/database/indexes.md new file mode 100644 index 00000000..e7ce2752 --- /dev/null +++ b/docs/database/indexes.md @@ -0,0 +1,91 @@ +# Database Index Strategy + +This document tracks query-path decisions for API performance and the indexes +that back them. + +## Primary Query Paths + +1. `courses` catalog and lesson loading + +- Query path: course list joins `enrollments`, course detail joins + `lessons/milestones/quizzes`. +- Supporting indexes: + - `idx_lessons_course_id` + - `idx_milestones_course_id` + - `idx_milestones_lesson_id` + - `idx_quiz_questions_quiz_id` + - `idx_enrollments_course_id` + +2. Scholar milestone reports and moderation + +- Query path: filter by `scholar_address`, `course_id`, `status`, sorted by + `submitted_at`. +- Supporting indexes: + - `idx_milestone_reports_scholar_status_submitted` + - `idx_milestone_reports_course_status_submitted` + - `idx_milestone_reports_status_submitted` + - `idx_milestone_audit_report_decided_at` + +3. Governance proposals and voting + +- Query path: proposal list by status/date, per-proposal vote lookup, proposal + status checks. +- Supporting indexes: + - `idx_proposals_status_created_at` + - `idx_proposals_created_at` + - `idx_proposals_cancelled_status_deadline` + - `idx_votes_proposal_id` + - `idx_votes_voter_address` + +4. Treasury/activity/event surfaces + +- Query path: recent events by contract/event type/date. +- Supporting indexes: + - `idx_events_contract_event_ledger` + - `idx_events_contract_type` + - `idx_events_contract_created_at` + - `idx_events_created_at` + +5. Enrollments, comments, and leaderboard + +- Query path: learner enrollments by recency, comments feed per proposal, + leaderboard rank paging. +- Supporting indexes: + - `idx_enrollments_learner_address` + - `idx_enrollments_learner_enrolled_at` + - `idx_comments_proposal_created_at` + - `idx_scholar_balances_lrn_desc` + +## N+1 Query Mitigations + +1. Scholar milestones now fetch latest audit decisions in one batched query + using `DISTINCT ON (report_id)`. +2. Governance proposals now join viewer vote (`votes`) directly instead of + per-row scalar subqueries. + +## Query Analysis Workflow + +1. Generate explain report: + +```bash +cd server +npm run db:query:analyze +``` + +2. Read generated report: + +- `docs/database/query-analysis.md` + +## pg_stat_statements Monitoring + +1. Enable extension at database level (DBA task): + +```sql +CREATE EXTENSION IF NOT EXISTS pg_stat_statements; +``` + +2. Inspect runtime snapshot in API: + +- `GET /api/health/db/performance` + +3. Server startup logs top statements when available. diff --git a/docs/deployment/environment-variables.md b/docs/deployment/environment-variables.md new file mode 100644 index 00000000..99a54873 --- /dev/null +++ b/docs/deployment/environment-variables.md @@ -0,0 +1,149 @@ +# Production Environment Variables + +This guide documents environment variables used by LearnVault across frontend, +backend, contracts, and infrastructure. Use it as a deployment checklist for +`dev`, `staging`, and `prod`. + +## Environment Matrix + +- `dev`: local development with disposable keys and permissive defaults. +- `staging`: production-like config with isolated wallets and services. +- `prod`: real infrastructure, funded wallets, and secrets managed in CI/Vault. + +## Variable Reference + +### Core Runtime + +| Variable | Required | Env scope | Description | +| ------------------- | --------------- | ---------------- | -------------------------------------------------------- | +| `NODE_ENV` | yes (server) | dev/staging/prod | Node runtime mode (`development`, `test`, `production`). | +| `PORT` | yes (server) | dev/staging/prod | Backend server port. | +| `FRONTEND_URL` | yes | dev/staging/prod | Canonical frontend origin for CORS and links in emails. | +| `VITE_API_URL` | yes (frontend) | dev/staging/prod | Base URL used by frontend API calls. | +| `VITE_API_BASE_URL` | optional | dev/staging/prod | API prefix used by some frontend pages. | +| `VITE_SERVER_URL` | optional legacy | dev/staging/prod | Backward-compatible server URL alias. | + +### Database and Cache + +| Variable | Required | Env scope | Description | +| -------------- | -------- | ---------------- | ------------------------------------------------------------- | +| `DATABASE_URL` | yes | dev/staging/prod | Postgres connection string used by API and migration scripts. | +| `REDIS_URL` | optional | dev/staging/prod | Redis endpoint for rate limiting / nonce state. | + +### Auth and Admin + +| Variable | Required | Env scope | Description | +| ---------------------- | ------------------- | ---------------- | ------------------------------------------------------------------- | +| `JWT_PRIVATE_KEY` | yes in staging/prod | dev/staging/prod | RSA private key for JWT signing. | +| `JWT_PUBLIC_KEY` | yes in staging/prod | dev/staging/prod | RSA public key for JWT verification. | +| `JWT_SECRET` | legacy/dev fallback | dev | Legacy shared-secret mode for tests or local-only auth. | +| `ADMIN_ADDRESSES` | recommended | dev/staging/prod | Comma-separated Stellar addresses allowed to perform admin actions. | +| `ADMIN_API_KEY` | recommended | dev/staging/prod | Extra API key guard for admin endpoints. | +| `MAX_COMMENTS_PER_DAY` | optional | dev/staging/prod | Spam/rate-control limit for comments. | + +### Stellar / Soroban Network + +| Variable | Required | Env scope | Description | +| ----------------------------------- | ----------------------- | ---------------- | ----------------------------------------------------------- | +| `STELLAR_NETWORK` | yes | dev/staging/prod | Target network (`local`, `testnet`, `mainnet`). | +| `SOROBAN_RPC_URL` | yes | dev/staging/prod | Soroban RPC endpoint used by workers and contract services. | +| `PUBLIC_STELLAR_NETWORK` | yes (frontend) | dev/staging/prod | Frontend network selector. | +| `PUBLIC_STELLAR_NETWORK_PASSPHRASE` | yes (frontend) | dev/staging/prod | Network passphrase for frontend contract operations. | +| `PUBLIC_STELLAR_RPC_URL` | yes (frontend) | dev/staging/prod | Frontend Soroban RPC endpoint. | +| `PUBLIC_STELLAR_HORIZON_URL` | yes (frontend) | dev/staging/prod | Frontend Horizon endpoint. | +| `STELLAR_SECRET_KEY` | yes for on-chain writes | staging/prod | Signing key used for backend contract submissions. | + +### Contract IDs + +Set both frontend (`VITE_*`) and backend contract IDs consistently: + +- `LEARN_TOKEN_CONTRACT_ID` / `VITE_LEARN_TOKEN_CONTRACT_ID` +- `GOVERNANCE_TOKEN_CONTRACT_ID` / `VITE_GOVERNANCE_TOKEN_CONTRACT_ID` +- `COURSE_MILESTONE_CONTRACT_ID` / `VITE_COURSE_MILESTONE_CONTRACT_ID` +- `SCHOLARSHIP_TREASURY_CONTRACT_ID` / `VITE_SCHOLARSHIP_TREASURY_CONTRACT_ID` +- `MILESTONE_ESCROW_CONTRACT_ID` / `VITE_MILESTONE_ESCROW_CONTRACT_ID` +- `SCHOLAR_NFT_CONTRACT_ID` / `VITE_SCHOLAR_NFT_CONTRACT_ID` +- `PUBLIC_*` legacy aliases still used by some frontend modules. + +### Event and Worker Settings + +| Variable | Required | Env scope | Description | +| --------------------------------- | ----------- | ---------------- | ----------------------------------------------- | +| `STARTING_LEDGER` | recommended | dev/staging/prod | Initial ledger height for event indexer replay. | +| `POLL_INTERVAL_MS` | optional | dev/staging/prod | Poll interval for on-chain event workers. | +| `ESCROW_TIMEOUT_CRON_INTERVAL_MS` | optional | dev/staging/prod | Scheduler interval for escrow timeout worker. | + +### IPFS / Pinata + +| Variable | Required | Env scope | Description | +| ----------------------- | --------------- | ---------------- | -------------------------------------- | +| `PINATA_API_KEY` | yes for uploads | dev/staging/prod | Pinata API key for file pinning. | +| `PINATA_SECRET` | yes for uploads | dev/staging/prod | Pinata API secret for file pinning. | +| `IPFS_GATEWAY_URL` | optional | dev/staging/prod | Gateway override for public IPFS URLs. | +| `VITE_IPFS_GATEWAY_URL` | optional | dev/staging/prod | Frontend display gateway override. | + +### Email Delivery + +| Variable | Required | Env scope | Description | +| ------------------------------------------------------------------- | ---------------------- | ---------------- | -------------------------------------------------------- | +| `RESEND_API_KEY` | one provider required | dev/staging/prod | Resend API key (preferred provider path in server code). | +| `EMAIL_API_KEY` | optional alt provider | dev/staging/prod | SendGrid API key (legacy/fallback path). | +| `EMAIL_FROM` | yes when email enabled | dev/staging/prod | Sender address used in outbound messages. | +| `ADMIN_EMAILS` | optional | dev/staging/prod | Comma-separated admin recipients for notifications. | +| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` / `SMTP_FROM` | optional alt path | dev/staging/prod | SMTP transport settings for utility mailer path. | + +### Credential Metadata + +Badge CID values used by credential metadata endpoints: + +- `BADGE_CID_STELLAR` +- `BADGE_CID_SOROBAN` +- `BADGE_CID_DEFI` +- `BADGE_CID_BASE` + +## Secrets Management Rules + +Mark these as **secrets in CI** (never committed to git): + +- `DATABASE_URL` (production instances) +- `JWT_PRIVATE_KEY`, `JWT_PUBLIC_KEY` +- `STELLAR_SECRET_KEY` +- `PINATA_API_KEY`, `PINATA_SECRET` +- `RESEND_API_KEY`, `EMAIL_API_KEY` +- `SMTP_PASS` +- `ADMIN_API_KEY` + +Non-secret but environment-specific values (store as variables, not secrets): + +- `FRONTEND_URL`, `VITE_API_URL`, `PORT` +- `STELLAR_NETWORK`, `SOROBAN_RPC_URL` +- Contract IDs, gateway URLs, polling intervals + +## Key Pair Strategy + +### Ephemeral (dev/staging) + +Use disposable identities that can be rotated frequently. + +```bash +stellar keys generate dev-server --network testnet +stellar keys address dev-server +``` + +- Fund with friendbot (testnet only). +- Rotate often and do not reuse for production signing. + +### Production + +- Use a dedicated operational wallet or multisig-controlled signer. +- Generate keys in a secure environment (HSM, secure enclave, or offline host). +- Store only encrypted/export-controlled secret material in your secret manager. +- Restrict read access to CI principals and audited operators. + +## Deployment Checklist + +1. Copy and reconcile values from `.env.example` and `server/.env.example`. +2. Inject environment-specific values into deployment platform secrets. +3. Verify `staging` connectivity (DB, RPC, IPFS, email provider). +4. Confirm contract IDs map to the intended network. +5. Run migrations and smoke tests before promoting to `prod`. diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 00000000..dc2bd9aa --- /dev/null +++ b/docs/glossary.md @@ -0,0 +1,125 @@ +# LearnVault Glossary + +A plain-English reference for the key terms, tokens, and contracts used +throughout LearnVault. If you are new to blockchain or to this project, start +here. + +--- + +## Tokens + +### LRN (LearnToken) + +LearnVault's soulbound reputation token. LRN is minted to a learner's wallet +each time they complete a verified course milestone. Because it is soulbound, it +cannot be sold or transferred — your LRN balance is a tamper-proof measure of +how much you have actually learned on the platform. + +### GOV (GovernanceToken) + +The governance token that grants voting power in the LearnVault DAO. Donors +receive GOV proportional to their treasury contributions, and top learners earn +GOV when they hit milestone thresholds. GOV is transferable and is used +exclusively to vote on scholarship proposals and protocol changes. + +### Soulbound + +A property that makes a token non-transferable — it is permanently tied to the +wallet that received it. Soulbound tokens cannot be sold, traded, or moved to +another address, which makes them trustworthy indicators of personal achievement +or identity rather than purchased assets. + +--- + +## Stellar & Soroban Concepts + +### SEP-41 + +Stellar's standard interface for fungible tokens, roughly equivalent to +Ethereum's ERC-20. Both LRN and GOV implement SEP-41, which means any Stellar +wallet or tool that understands this standard can display and interact with +them. + +### Soroban + +Stellar's smart contract platform. LearnVault's on-chain logic — milestone +tracking, token minting, treasury management, and DAO voting — is implemented as +Soroban smart contracts written in Rust and deployed on the Stellar network. + +### Friendbot + +Stellar's testnet faucet. Friendbot funds a new testnet account with free test +XLM so developers can experiment with transactions and contract calls without +spending real money. If you are setting up a local development environment, +Friendbot is the first thing you will use. + +### Horizon + +Stellar's REST API server for querying on-chain data. Through Horizon you can +look up account balances, transaction history, and contract state. LearnVault's +frontend uses Horizon to display learner progress and treasury balances in real +time. + +--- + +## Smart Contracts + +### CourseMilestone + +The contract that tracks learner progress within each skill track. When a +learner completes a checkpoint and a validator confirms it, CourseMilestone +triggers the LRN mint. It is the bridge between off-chain learning activity and +on-chain reputation. + +### ScholarshipTreasury + +The contract that holds all community-donated USDC and manages DAO vote +execution. Funds can only leave the treasury when a scholarship proposal passes +the required quorum and approval threshold. Every deposit and withdrawal is +recorded on-chain for full transparency. + +### MilestoneEscrow + +The contract that holds approved scholarship funds and releases them in tranches +as the scholar hits agreed milestones. If a scholar goes inactive for 30 days, +any unspent funds automatically return to the treasury, protecting donor +contributions. + +### ScholarNFT + +A soulbound credential NFT minted to a scholar's wallet when they successfully +complete a funded program. It serves as a permanent, on-chain proof of +achievement that can be shared with employers, DAOs, and other ecosystem +participants. Because it is soulbound, it cannot be faked or transferred. + +--- + +## Governance & DAO + +### DAO (Decentralized Autonomous Organization) + +The community-driven governance structure through which LearnVault is managed. +Instead of a single authority deciding how funds are allocated, GOV token +holders collectively vote on scholarship proposals, eligibility thresholds, and +protocol upgrades. + +### Validator Committee + +In the current V1 phase, a trusted multi-sig group responsible for verifying +that scholars have genuinely completed their milestones. The committee reviews +submitted proof and signs off before the next tranche of funds is released. In +V2 this role transitions to oracle-based verification. + +### Quorum + +The minimum level of vote participation required for a proposal to be considered +valid. If not enough GOV holders cast votes during the 7-day voting window, the +proposal does not pass regardless of the yes/no ratio. This prevents low-turnout +decisions from committing treasury funds. + +### Tranche + +A portion of scholarship funds released upon the completion of one milestone. +Rather than disbursing the full approved amount at once, MilestoneEscrow splits +it into tranches so that scholars are funded incrementally and accountability is +maintained at every step. diff --git a/docs/nft-metadata-standard.md b/docs/nft-metadata-standard.md new file mode 100644 index 00000000..92c2a1f2 --- /dev/null +++ b/docs/nft-metadata-standard.md @@ -0,0 +1,210 @@ +# ScholarNFT Metadata Standard + +## Overview + +ScholarNFT credentials follow the ERC-721/ERC-1155 metadata standard to ensure +compatibility with NFT marketplaces, wallets, and tooling across the ecosystem. + +## Metadata Schema + +Each ScholarNFT credential includes a JSON metadata file stored on IPFS with the +following structure: + +```json +{ + "name": "string", + "description": "string", + "image": "ipfs://...", + "attributes": [ + { + "trait_type": "string", + "value": "string" + } + ] +} +``` + +### Fields + +- `name` (required): Human-readable credential title + - Format: `"{Course Title} — Course Completion"` + - Example: `"Introduction to Stellar & Soroban — Course Completion"` + +- `description` (required): Detailed description of the credential + - Format: `"Issued to learners who complete all milestones in {course_title}"` + - Example: + `"Issued to learners who complete all milestones in Introduction to Stellar & Soroban"` + +- `image` (required): IPFS URI pointing to the credential badge image + - Format: `"ipfs://{CID}"` + - Example: + `"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"` + +- `attributes` (required): Array of trait objects describing the credential + - Each trait has `trait_type` and `value` fields + - Standard traits: + - `Course`: The course ID (e.g., `"stellar-basics"`) + - `Course Title`: The full course title + - `Completed At`: ISO 8601 timestamp of completion + - `Learner`: Stellar address of the credential holder + - `Difficulty`: Course difficulty level (beginner, intermediate, advanced) + +## Example Metadata + +### Stellar Basics Completion + +```json +{ + "name": "Introduction to Stellar & Soroban — Course Completion", + "description": "Issued to learners who complete all milestones in Introduction to Stellar & Soroban", + "image": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + "attributes": [ + { + "trait_type": "Course", + "value": "stellar-basics" + }, + { + "trait_type": "Course Title", + "value": "Introduction to Stellar & Soroban" + }, + { + "trait_type": "Completed At", + "value": "2026-03-26T10:30:00Z" + }, + { + "trait_type": "Learner", + "value": "GABC123...XYZ789" + }, + { + "trait_type": "Difficulty", + "value": "beginner" + } + ] +} +``` + +### DeFi Fundamentals Completion + +```json +{ + "name": "DeFi Fundamentals on Stellar — Course Completion", + "description": "Issued to learners who complete all milestones in DeFi Fundamentals on Stellar", + "image": "ipfs://bafkreiabcdef1234567890ghijklmnopqrstuvwxyz", + "attributes": [ + { + "trait_type": "Course", + "value": "defi-fundamentals" + }, + { + "trait_type": "Course Title", + "value": "DeFi Fundamentals on Stellar" + }, + { + "trait_type": "Completed At", + "value": "2026-03-15T14:22:00Z" + }, + { + "trait_type": "Learner", + "value": "GDEF456...ABC123" + }, + { + "trait_type": "Difficulty", + "value": "intermediate" + } + ] +} +``` + +## API Integration + +### Generating Metadata + +Use the `POST /api/credentials/metadata` endpoint to generate and upload +metadata: + +```bash +curl -X POST https://api.learnvault.xyz/api/credentials/metadata \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -d '{ + "course_id": "stellar-basics", + "learner_address": "GABC123...XYZ789", + "completed_at": "2026-03-26T10:30:00Z" + }' +``` + +Response: + +```json +{ + "data": { + "metadata_uri": "ipfs://bafkreiabcdef1234567890ghijklmnopqrstuvwxyz", + "gateway_url": "https://gateway.pinata.cloud/ipfs/bafkreiabcdef1234567890ghijklmnopqrstuvwxyz", + "metadata": { + "name": "Introduction to Stellar & Soroban — Course Completion", + "description": "Issued to learners who complete all milestones in Introduction to Stellar & Soroban", + "image": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + "attributes": [...] + } + } +} +``` + +### Minting with Metadata + +After generating metadata, use the returned `metadata_uri` when calling +`scholar_nft.mint()`: + +```typescript +import { scholarNftContract } from "./contracts" + +const metadataUri = "ipfs://bafkreiabcdef1234567890ghijklmnopqrstuvwxyz" +const learnerAddress = "GABC123...XYZ789" + +const tokenId = await scholarNftContract.mint({ + to: learnerAddress, + metadata_uri: metadataUri, +}) +``` + +## Image Assets + +Course completion badge images are stored in `/public/assets/brand/nft/` and +follow this naming convention: + +- `scholar-nft-{course-slug}.svg` - Vector source +- `scholar-nft-{course-slug}.png` - Rasterized version (1000x1000px) + +Available badges: + +- `scholar-nft-base.png` - Generic completion badge +- `scholar-nft-stellar.png` - Stellar Basics course +- `scholar-nft-soroban.png` - Soroban Contracts course +- `scholar-nft-defi.png` - DeFi Fundamentals course + +## IPFS Pinning + +Metadata is pinned to IPFS via Pinata to ensure permanent availability. The +service: + +1. Generates metadata JSON conforming to this standard +2. Pins the JSON to IPFS using Pinata +3. Returns both the `ipfs://` URI and HTTP gateway URL +4. Uses CIDv1 for future-proof addressing + +## Compatibility + +This standard ensures ScholarNFT credentials are compatible with: + +- OpenSea and other NFT marketplaces +- Wallet applications (Freighter, Lobstr, etc.) +- NFT aggregators and analytics platforms +- IPFS gateways and pinning services +- Standard NFT tooling and libraries + +## References + +- [ERC-721 Metadata Standard](https://eips.ethereum.org/EIPS/eip-721) +- [ERC-1155 Metadata Standard](https://eips.ethereum.org/EIPS/eip-1155) +- [OpenSea Metadata Standards](https://docs.opensea.io/docs/metadata-standards) +- [IPFS Content Addressing](https://docs.ipfs.tech/concepts/content-addressing/) diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 00000000..510477eb --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,240 @@ +openapi: 3.0.3 +info: + title: LearnVault API + version: 1.0.0 + description: Backend API for LearnVault frontend and integrations. +servers: + - url: http://localhost:4000 + description: Local development server +tags: + - name: Health + description: Server status endpoints + - name: Courses + description: Course catalog endpoints + - name: Validator + description: Milestone validation endpoints + - name: Events + description: Event stream endpoints +paths: + /api/health: + get: + tags: [Health] + summary: Check server health status + responses: + "200": + description: Server is healthy + content: + application/json: + schema: + $ref: "#/components/schemas/HealthResponse" + "500": + $ref: "#/components/responses/InternalServerError" + /api/courses: + get: + tags: [Courses] + summary: List published courses + security: + - bearerAuth: [] + responses: + "200": + description: Courses fetched successfully + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/Course" + "401": + $ref: "#/components/responses/UnauthorizedError" + "500": + $ref: "#/components/responses/InternalServerError" + /api/courses/{courseId}: + get: + tags: [Courses] + summary: Get a course by id + security: + - bearerAuth: [] + parameters: + - in: path + name: courseId + required: true + schema: + type: string + description: Unique course identifier + responses: + "200": + description: Course fetched successfully + content: + application/json: + schema: + type: object + properties: + data: + $ref: "#/components/schemas/Course" + "401": + $ref: "#/components/responses/UnauthorizedError" + "404": + $ref: "#/components/responses/NotFoundError" + "500": + $ref: "#/components/responses/InternalServerError" + /api/validator/validate: + post: + tags: [Validator] + summary: Validate a learner milestone + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ValidatorRequest" + responses: + "200": + description: Milestone validation successful + content: + application/json: + schema: + type: object + properties: + data: + $ref: "#/components/schemas/ValidatorResult" + "400": + $ref: "#/components/responses/BadRequestError" + "401": + $ref: "#/components/responses/UnauthorizedError" + "500": + $ref: "#/components/responses/InternalServerError" + /api/events: + get: + tags: [Events] + summary: List platform events + security: + - bearerAuth: [] + parameters: + - in: query + name: type + schema: + type: string + description: Optional event type filter + - in: query + name: limit + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + description: Max number of events to return + responses: + "200": + description: Events fetched successfully + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/Event" + "401": + $ref: "#/components/responses/UnauthorizedError" + "500": + $ref: "#/components/responses/InternalServerError" +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: + ErrorResponse: + type: object + properties: + error: + type: string + required: [error] + HealthResponse: + type: object + properties: + status: + type: string + example: ok + timestamp: + type: string + format: date-time + required: [status, timestamp] + Course: + type: object + properties: + id: + type: string + title: + type: string + level: + type: string + published: + type: boolean + required: [id, title, level, published] + Event: + type: object + properties: + id: + type: string + type: + type: string + entityId: + type: string + timestamp: + type: string + format: date-time + required: [id, type, entityId, timestamp] + ValidatorRequest: + type: object + properties: + courseId: + type: string + learnerAddress: + type: string + milestoneId: + type: integer + minimum: 0 + required: [courseId, learnerAddress, milestoneId] + ValidatorResult: + allOf: + - $ref: "#/components/schemas/ValidatorRequest" + - type: object + properties: + approved: + type: boolean + validator: + type: string + required: [approved, validator] + responses: + BadRequestError: + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + UnauthorizedError: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + NotFoundError: + description: Resource not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + InternalServerError: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" diff --git a/docs/performance-budget.md b/docs/performance-budget.md new file mode 100644 index 00000000..7ab9eb75 --- /dev/null +++ b/docs/performance-budget.md @@ -0,0 +1,82 @@ +# Performance Budget + +## Lighthouse mobile targets + +- Performance score: `>= 90` +- First Contentful Paint: `< 1.5s` +- Largest Contentful Paint: `< 2.5s` +- Total Blocking Time: `< 200ms` +- Cumulative Layout Shift: `< 0.1` + +## CI gate + +- Pull requests fail when Lighthouse performance drops below `85`. +- Implemented with [lighthouserc.json](../lighthouserc.json) and + [.github/workflows/lighthouse.yml](../.github/workflows/lighthouse.yml). + +## Baseline before optimisation + +Measured with: + +```bash +npx vite build +npx vite-bundle-visualizer --output docs/bundle-baseline.html --open false +``` + +Observed production bundle output before code-splitting: + +- `assets/index-*.js`: `~2.98 MB` minified / `~879 kB` gzip +- `assets/util-*.js`: `~546 kB` minified / `~172 kB` gzip +- `assets/index-*.css`: `~181 kB` minified / `~28 kB` gzip +- `assets/stellar_xdr_json_bg-*.wasm`: `~3.7 MB` minified / `~738 kB` gzip + +## Optimisations added + +- Route-level code splitting with `React.lazy` and `Suspense` +- Component-level lazy loading for: + - the contract explorer on `/debug` + - the Recharts treasury visualisation on `/treasury` +- Deferred above-the-fold home widgets so onboarding, milestone tracking, and + the sample contract do not load until needed or scrolled into view +- Moved wallet connect/disconnect and balance fetch logic behind on-demand + imports instead of loading the full Stellar stack on first paint +- Vite manual chunking for Stellar SDK, charts, contract explorer, router, i18n, + and generated contract clients +- Runtime `preconnect` / `dns-prefetch` hints for the configured Horizon, RPC, + and Stellar Lab origins +- Removed the contract utility runtime dependency from the preconnect helper so + the landing route no longer pulls the Stellar SDK into the initial preload set +- Asset audit completed: the app currently ships SVG assets only, so there were + no large raster images to convert to WebP in this pass + +## Best local verification from this branch + +Measured against the optimised production build with: + +```bash +npm run build +npx lighthouse http://127.0.0.1:4173 --only-categories=performance --emulated-form-factor=mobile --throttling-method=simulate +``` + +Best local mobile Lighthouse result reached during this pass: + +- Performance score: `58` +- First Contentful Paint: `3066ms` +- Largest Contentful Paint: `3329ms` +- Total Blocking Time: `1017ms` +- Cumulative Layout Shift: `0` + +This is a material improvement over the original bundle structure, but it still +does **not** meet the final issue target of `>= 90`. The remaining bottleneck is +the always-loaded application shell/framework path rather than the route-level +feature chunks that were split out in this change. + +## Verification + +After each performance-sensitive change run: + +```bash +npm run build +npx vite-bundle-visualizer --output docs/bundle-report.html --open false +npx @lhci/cli autorun --config=./lighthouserc.json +``` diff --git a/docs/performance-http2-compression.md b/docs/performance-http2-compression.md new file mode 100644 index 00000000..0ce43be3 --- /dev/null +++ b/docs/performance-http2-compression.md @@ -0,0 +1,41 @@ +# HTTP/2 and Compression — Issue #744 + +## Compression middleware + +`compression` (gzip/brotli) is applied immediately after `express()` in +`server/src/index.ts`. + +A custom filter skips already-compressed content to avoid wasted CPU: + +- `image/*`, `video/*`, `audio/*` +- `application/octet-stream` +- Any URL path containing `/ipfs/` (IPFS gateway passthrough) + +All other responses (JSON, HTML, text) are compressed at level 6. + +## HTTP/2 + +HTTP/2 is **not terminated at the Node layer**. Express does not natively +support HTTP/2. + +In all deployed environments, HTTP/2 is handled by the reverse proxy in front of +Node: + +- Local dev: HTTP/1.1 is fine +- Production: configure HTTP/2 at the proxy level (Nginx, Caddy, AWS ALB, + Cloudflare, etc.) + +No code change is required in the Express app. + +## Cache-Control for static assets + +The Express backend does not serve static assets — all static files are served +by the frontend build (Vite). Cache-Control headers for static assets should be +configured at the CDN or frontend hosting layer, not here. + +## Latency + +Compression reduces response payload size for JSON API responses. Typical +improvement for JSON-heavy endpoints (course lists, leaderboard, milestones) is +60–80% reduction in transfer size. Actual latency improvement depends on network +conditions and client bandwidth. diff --git a/docs/pr-008.md b/docs/pr-008.md new file mode 100644 index 00000000..92eb3264 --- /dev/null +++ b/docs/pr-008.md @@ -0,0 +1,42 @@ +# PR: Audit ScholarNFT token_id type consistency (closes #) + +## Summary + +Investigated the reported type mismatch in `contracts/scholar_nft/src/lib.rs` +where `owner_of()` was said to use `token_id: u32` while the rest of the +contract used `u64`. + +## Findings + +After a full audit of the contract and its tests, **no code changes were +needed**. The contract already uses `u64` consistently across all token ID +references: + +- `DataKey` variants: `Owner(u64)`, `Metadata(u64)`, `TokenUri(u64)` +- `mint()` → returns `u64` +- `owner_of(token_id: u64)` +- `token_uri(token_id: u64)` +- `transfer(_token_id: u64)` +- `get_metadata(token_id: u64)` +- `token_counter()` → stores and returns `u64` +- All tests in `test.rs` use `u64` values + +The bug was not present in the current codebase. It was likely fixed before the +issue was filed. + +## Changes + +- `docs/adr/ADR-008.md` — documents the investigation, findings, and rationale + for no code change + +## Testing + +No contract logic was modified. Existing tests in +`contracts/scholar_nft/src/test.rs` cover all token ID paths and remain passing. + +## Checklist + +- [x] Audited all `token_id` usages in `lib.rs` +- [x] Audited `DataKey` storage key types +- [x] Audited `test.rs` for type consistency +- [x] Added ADR documenting the investigation diff --git a/docs/request-tracing.md b/docs/request-tracing.md new file mode 100644 index 00000000..0050bf0b --- /dev/null +++ b/docs/request-tracing.md @@ -0,0 +1,27 @@ +# Backend request tracing + +LearnVault backend now emits a per-request correlation ID for every inbound HTTP +request. + +## What is emitted + +- Each request gets a generated UUID in request middleware. +- The value is returned to clients as `X-Request-ID`. +- All `console.*` logs produced in request scope are prefixed with + `[requestId=]`. +- Stellar transaction calls include a best-effort short memo + (`rid:<24-char-id>`) to propagate trace context downstream. + +## Trace a request end-to-end + +1. Make a request and capture the `X-Request-ID` response header. +2. Search backend logs for that exact request ID. +3. Find associated Stellar call logs and transaction hashes in the same log + window. +4. On-chain memo values prefixed with `rid:` can be matched back to request IDs. + +## Operational notes + +- Request tracing is automatic; no route-level changes are required. +- Worker/background logs do not include a request ID unless one is explicitly + provided. diff --git a/docs/security-improvements.md b/docs/security-improvements.md new file mode 100644 index 00000000..697491f9 --- /dev/null +++ b/docs/security-improvements.md @@ -0,0 +1,229 @@ +# Security Improvements + +## CORS Configuration (Implemented) + +### Overview + +Implemented strict Cross-Origin Resource Sharing (CORS) policies to restrict API +access to authorized frontend domains only. + +### Changes Made + +#### 1. Environment Configuration + +- Added `FRONTEND_URL` environment variable for explicit origin control +- Maintained backward compatibility with legacy `CORS_ORIGIN` variable +- Updated `server/.env.example` with comprehensive documentation + +#### 2. Dynamic Origin Validation + +Replaced simple origin string with dynamic validation function: + +```typescript +// Before +app.use(cors({ origin: env.CORS_ORIGIN })) + +// After +app.use( + cors({ + origin: (origin, callback) => { + if (!origin) return callback(null, true) + if (allowedOrigins.includes(origin)) { + callback(null, true) + } else { + console.warn(`CORS blocked request from origin: ${origin}`) + callback(new Error("Not allowed by CORS")) + } + }, + credentials: true, + methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + }), +) +``` + +#### 3. Environment-Specific Origins + +**Production Mode:** + +- `FRONTEND_URL` (from environment) +- `https://learnvault.app` +- `https://www.learnvault.app` + +**Development Mode:** + +- All production origins (for testing) +- `http://localhost:5173` (Vite) +- `http://localhost:3000` (React/Next.js) +- `http://localhost:5174` (Vite alternate) +- `http://127.0.0.1:5173` (localhost IP) + +#### 4. Security Features + +- **Credentials Support**: Enabled `credentials: true` for authenticated + requests +- **Method Restrictions**: Limited to necessary HTTP methods +- **Header Restrictions**: Only allow `Content-Type` and `Authorization` +- **Audit Logging**: Log all blocked CORS requests for security monitoring +- **No-Origin Allowance**: Allow server-to-server and mobile app requests + +### Security Benefits + +1. **Prevents Unauthorized Access** + - Only whitelisted domains can access the API + - Blocks phishing sites and malicious third parties + - Mitigates CSRF attacks + +2. **Protects User Data** + - Authentication tokens only sent to trusted domains + - Personal data cannot be accessed by unauthorized sites + - Rate limits apply per origin + +3. **Audit Trail** + - All blocked requests are logged + - Helps identify security threats + - Enables monitoring of unauthorized access attempts + +### Files Modified + +- `server/src/index.ts` - Implemented dynamic CORS validation +- `server/.env.example` - Added `FRONTEND_URL` and cleaned up configuration + +### Documentation Created + +- `docs/cors-configuration.md` - Comprehensive CORS setup guide +- `docs/security-improvements.md` - This file + +### Testing + +To test the CORS configuration: + +```bash +# Test allowed origin +curl -H "Origin: https://learnvault.app" \ + -H "Access-Control-Request-Method: POST" \ + -X OPTIONS \ + http://localhost:4000/api/courses + +# Test blocked origin (should fail) +curl -H "Origin: https://malicious-site.com" \ + -H "Access-Control-Request-Method: POST" \ + -X OPTIONS \ + http://localhost:4000/api/courses +``` + +### Migration Guide + +For existing deployments: + +1. Add `FRONTEND_URL` to your environment variables: + + ```env + FRONTEND_URL=https://your-frontend-domain.com + ``` + +2. Restart the backend server + +3. Verify CORS headers in browser DevTools Network tab + +4. (Optional) Remove legacy `CORS_ORIGIN` variable + +### Future Enhancements + +Consider implementing: + +1. **Dynamic Subdomain Support** + - Allow all `*.learnvault.app` subdomains + - Useful for preview deployments and staging + +2. **Rate Limiting per Origin** + - Different rate limits for different origins + - Stricter limits for unknown origins + +3. **Origin Allowlist Management** + - Admin API to add/remove allowed origins + - Database-backed origin configuration + +4. **CORS Metrics** + - Track blocked requests by origin + - Alert on suspicious patterns + - Dashboard for CORS analytics + +## Additional Security Recommendations + +### 1. Content Security Policy (CSP) + +Add CSP headers to prevent XSS attacks: + +```typescript +app.use((req, res, next) => { + res.setHeader( + "Content-Security-Policy", + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'", + ) + next() +}) +``` + +### 2. Rate Limiting Enhancements + +- Implement per-user rate limits +- Add stricter limits for sensitive endpoints +- Use Redis for distributed rate limiting + +### 3. Request Validation + +- Validate all input data with Zod schemas +- Sanitize user input to prevent injection attacks +- Implement request size limits + +### 4. Authentication Hardening + +- Implement token rotation +- Add refresh token mechanism +- Set appropriate token expiration times +- Use secure cookie flags (HttpOnly, Secure, SameSite) + +### 5. API Security Headers + +Add security headers middleware: + +```typescript +app.use((req, res, next) => { + res.setHeader("X-Content-Type-Options", "nosniff") + res.setHeader("X-Frame-Options", "DENY") + res.setHeader("X-XSS-Protection", "1; mode=block") + res.setHeader( + "Strict-Transport-Security", + "max-age=31536000; includeSubDomains", + ) + next() +}) +``` + +### 6. Dependency Security + +- Regularly update dependencies +- Use `npm audit` to check for vulnerabilities +- Implement automated security scanning in CI/CD + +### 7. Logging and Monitoring + +- Log all authentication attempts +- Monitor for suspicious patterns +- Set up alerts for security events +- Implement request tracing + +### 8. Database Security + +- Use parameterized queries (already using pg) +- Implement row-level security in PostgreSQL +- Encrypt sensitive data at rest +- Regular database backups + +## References + +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [Express Security Best Practices](https://expressjs.com/en/advanced/best-practice-security.html) +- [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security) +- [Node.js Security Checklist](https://blog.risingstack.com/node-js-security-checklist/) diff --git a/docs/token-economics.md b/docs/token-economics.md new file mode 100644 index 00000000..9d45c89f --- /dev/null +++ b/docs/token-economics.md @@ -0,0 +1,196 @@ +# Token Economics + +LearnVault uses two tokens because it has two distinct problems to solve: +measuring learning (reputation) and governing scholarship disbursement (voting +power). Conflating them into one token would break both functions. + +--- + +## LRN (LearnToken) + +LRN is not a financial asset. It is an on-chain reputation score — a number that +says how much verified learning a wallet has completed inside the LearnVault +system. It cannot be sent, sold, or delegated. + +### How it's earned + +LRN is minted by the `CourseMilestone` contract when a validator approves a +milestone submission. The amount minted per milestone is set per track by the +admin committee in V1 — there is no global fixed rate. A learner completing a +beginner track will earn less LRN than one completing an advanced engineering +track. Exact amounts per track are configured at course creation time via +`add_course`. + +### What it unlocks + +| Threshold | What it enables | +| ---------------------- | ---------------------------------------------------- | +| Configurable per track | Scholarship eligibility — wallet can be nominated | +| Governance threshold | Eligibility to participate in DAO votes on proposals | + +Reaching these thresholds does not automatically grant anything — it makes the +wallet _eligible_. Scholarship disbursement still requires a passing governance +vote (see GOV below). + +### Why it's non-transferable + +If LRN could be transferred, the following would happen immediately: + +- Wallets with capital but no learning would buy reputation and access + scholarships meant for real learners +- A secondary market would form around eligibility thresholds, pricing out + genuine participants +- Sybil attackers could launder reputation across fresh wallets to reset + eligibility windows + +Soulbound design is not ideological — it is the only mechanism that makes the +eligibility threshold meaningful. A score you cannot buy is the only score worth +having. + +### Supply model + +- **Cap:** None. LRN is uncapped. +- **Minting:** Exclusively by `contracts/course_milestone/` — no other contract + or admin can mint LRN directly. +- **Burning:** No burn mechanic. LRN balances are permanent records of completed + work. + +--- + +## GOV (GovernanceToken) + +GOV is voting weight in the scholarship DAO. Unlike LRN, it is a transferable +token — deliberately so. + +### How it's earned + +GOV is minted through two paths: + +1. **Donation:** 1 USDC deposited to the treasury mints 1 GOV. Donors get + governance rights proportional to their contribution. +2. **Learner rewards:** Wallets that cross the top-learner LRN threshold receive + a GOV distribution as a reward. This gives high-performing learners a voice + in how scholarship funds are allocated. + +### What it does + +GOV holders vote on scholarship disbursement proposals. Votes are weighted by +GOV balance. A proposal must reach a quorum and a majority to pass. In V1, +proposal creation is permissioned — only wallets above the LRN governance +threshold or holding minimum GOV can submit proposals. + +### Why it IS transferable + +Donors need an exit. Locking capital permanently into a governance token with no +liquidity would deter serious donors from participating. Transferability also +creates secondary market price discovery — if GOV trades at a premium, it is a +signal that the community values governance rights, which attracts more donors. +If it trades at a discount, that is honest feedback about protocol health. + +Transferability is a feature, not a compromise. + +### Supply model + +- **Minting:** On USDC deposit and on learner threshold reward distributions. +- **Burning:** ⚠️ Open design question — see callout below. + +> ⚠️ **Open design question** +> +> The GOV burn mechanic has not been finalized. Options under consideration +> include burning GOV when a scholarship is disbursed (aligning token supply +> with treasury outflows), burning on governance participation as a spam +> deterrent, or no burn at all. This will be resolved before mainnet. Track the +> discussion in [#139](https://github.com/bakeronchain/learnvault/issues/139). + +--- + +## The Flywheel + +The two tokens are designed to reinforce each other through a feedback loop: + +``` +1. Learner completes milestones → earns LRN +2. LRN crosses threshold → learner becomes scholarship-eligible +3. LRN crosses governance threshold → learner gains DAO voting rights +4. More legitimate voters → better scholarship proposals pass +5. Better outcomes → donors notice → more USDC deposited +6. More USDC → more GOV minted → governance becomes more distributed +7. More distributed governance → more proposals → back to step 4 +``` + +``` + ┌─────────────────────────────────────────────┐ + │ │ + ▼ │ +[Learner earns LRN] │ + │ │ + ▼ │ +[Crosses GOV eligibility threshold] │ + │ │ + ▼ │ +[Participates in DAO votes] │ + │ │ + ▼ │ +[Better proposals pass → scholarships disbursed] │ + │ │ + ▼ │ +[Donors attracted → deposit USDC] │ + │ │ + ▼ │ +[More GOV minted → governance decentralizes] ────┘ +``` + +The loop only holds if LRN remains non-transferable. The moment reputation can +be bought, step one becomes pay-to-win and the rest of the flywheel breaks. + +--- + +## V1 Centralization — Honest Accounting + +V1 ships with the following centralized components. None of this is hidden: + +- **Milestone approval** is controlled by a validator committee. There is no + on-chain dispute resolution. A validator can reject a valid submission and + there is currently no appeal mechanism. +- **Minting permissions** on `contracts/course_milestone/` are set by an admin + key. The admin can add courses, set milestone counts, and configure LRN + amounts per track. +- **Scholarship disbursement** requires a multisig in V1. Even if a proposal + passes governance, the actual USDC transfer goes through a multisig held by + the core team. +- **Contract upgrades** are not yet governed on-chain. The team can upgrade + contracts unilaterally. + +This is the honest state of V1. It ships this way because the alternative — +launching with incomplete decentralization infrastructure and calling it +trustless — is worse. + +### V2 Roadmap + +Before admin keys are removed, the following needs to exist: + +1. On-chain dispute resolution for milestone rejections +2. Fully on-chain proposal execution without multisig +3. A validator election mechanism governed by GOV holders +4. Time-locked upgrade governance so contract changes require a passing vote + +V2 decentralization is not a vague future commitment — it is a prerequisite for +removing the admin keys. Until those components exist, the keys stay and this +document says so plainly. + +--- + +## Contract References + +| Contract | Path | Role | +| -------------------- | ----------------------------- | ---------------------------------- | +| `CourseMilestone` | `contracts/course_milestone/` | Milestone approval, LRN minting | +| `ScholarNFT` | `contracts/scholar_nft/` | Soulbound credential on completion | +| Governance (planned) | `contracts/governance/` | GOV voting, proposal execution | + +--- + +## Further Reading + +- [README](../README.md) +- [Issue #139 — Token economics explainer](https://github.com/bakeronchain/learnvault/issues/139) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000..78cb7fe7 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,176 @@ +# Troubleshooting Guide + +Common developer issues and fast recovery steps for LearnVault. + +## 1) Wallet Connection Fails + +### Symptoms + +- Wallet modal does not open. +- Connection rejected or stays in loading state. +- Connected account is on wrong network. + +### Fix + +1. Confirm wallet extension/app is unlocked and approved for the current site. +2. Verify frontend Stellar env values: + - `PUBLIC_STELLAR_NETWORK` + - `PUBLIC_STELLAR_NETWORK_PASSPHRASE` + - `PUBLIC_STELLAR_RPC_URL` +3. Ensure wallet network matches app network (`local`/`testnet`/`mainnet`). +4. Restart frontend dev server after env changes. + +## 2) Contract Call Fails with Sequence Number Error + +### Symptoms + +- Transaction submission fails with sequence mismatch/stale sequence. + +### Fix + +1. Retry once after refreshing account state from Horizon/RPC. +2. Avoid concurrent submissions from the same signer key. +3. If using backend signer (`STELLAR_SECRET_KEY`), make sure only one process is + sending transactions with that key. +4. Reinitialize signer/session if nonce or sequence cache looks stale. + +## 3) IPFS Upload Timeout + +### Symptoms + +- Upload endpoints hang or return timeout. +- Upload succeeds sometimes but not consistently. + +### Fix + +1. Validate `PINATA_API_KEY` and `PINATA_SECRET`. +2. Confirm Pinata account/API key is active and has sufficient quota. +3. Test connectivity to Pinata from your runtime environment. +4. If using dedicated gateway, verify `IPFS_GATEWAY_URL`/`VITE_IPFS_GATEWAY_URL` + are valid and reachable. +5. Retry with a smaller file to isolate payload-size/network issues. + +## 4) Local Database Migration Errors + +### Symptoms + +- `npm run migrate` fails in `server`. +- Schema mismatch or "relation already exists/does not exist" errors. + +### Fix + +1. Verify `DATABASE_URL` points to the intended local database. +2. Ensure Postgres service is running and accessible. +3. Run migration verification: + +```bash +cd server +npm run migrate:verify +``` + +4. Inspect migration order and check for partial/failed previous runs. +5. For local-only environments, recreate the DB and rerun migrations if state is + corrupted. + +## 5) JWT Errors in Development + +### Symptoms + +- `401`/`403` for endpoints requiring admin/user JWT. +- "Invalid signature", "jwt malformed", or missing key errors. + +### Fix + +1. In dev, ensure one of these auth setups is correct: + - RS256 keys: `JWT_PRIVATE_KEY` + `JWT_PUBLIC_KEY` + - Dev fallback secret (if route supports it): `JWT_SECRET` +2. Restart backend after changing env values. +3. Confirm tokens are generated with matching algorithm/key pair. +4. In production, do not rely on fallback secret behavior. + +## 6) Frontend Build Fails (TypeScript Errors) + +### Symptoms + +- `npm run build` or `npm run typecheck` fails with TS diagnostics. + +### Fix + +1. Install dependencies cleanly: + +```bash +npm ci --legacy-peer-deps +``` + +2. Run type checking directly to isolate: + +```bash +npm run typecheck +``` + +3. Fix strict type errors before build (do not suppress with `any` unless + justified). +4. Ensure generated clients/packages are up to date if contract interfaces + changed. + +## 7) Soroban Contract Compilation Errors + +### Symptoms + +- `cargo build` fails for `wasm32v1-none`. +- Missing target/toolchain or crate feature incompatibility. + +### Fix + +1. Ensure Rust target is installed: + +```bash +rustup target add wasm32v1-none +``` + +2. Build from repo root: + +```bash +cargo build --target wasm32v1-none --release +``` + +3. Confirm Rust toolchain version matches project expectation. +4. Resolve clippy/fmt issues if CI enforces them: + +```bash +cargo clippy -- -D warnings +cargo fmt --check +``` + +## 8) CI Pipeline Fails + +### Symptoms + +- GitHub Actions job fails on lint/test/build/migration steps. + +### Fix + +1. Open failed workflow logs and identify first failing step. +2. Reproduce locally with the same command. +3. Common local reproductions: + +```bash +npm run lint +npx prettier . --check +npm run build +npm run test:frontend +npm run test:e2e +cd server && npm test +``` + +4. If DB-related CI job fails, run migrations locally before tests. +5. If contract CI fails, run `cargo test --workspace` and + `cargo build --target wasm32v1-none --release`. + +## Fast Diagnostic Checklist + +- [ ] Correct `.env` values loaded in frontend and server. +- [ ] Network and contract IDs point to the same environment. +- [ ] Postgres reachable and migrations current. +- [ ] Stellar signer/key and RPC endpoint valid. +- [ ] Local tests pass before pushing. diff --git a/docs/whitepaper.md b/docs/whitepaper.md new file mode 100644 index 00000000..0c4af579 --- /dev/null +++ b/docs/whitepaper.md @@ -0,0 +1,188 @@ +# LearnVault Technical Whitepaper + +## Abstract + +LearnVault is an innovative blockchain-based educational platform designed to +bridge the global developer skills gap by directly incentivizing verified +learning. As traditional education systems struggle to keep pace with rapid +technological advancements, aspiring developers in emerging economies face +significant barriers to entry, including high costs and lack of verifiable +credentials. LearnVault addresses these challenges through a decentralized +protocol that rewards users for completing rigorous educational modules and +contributing to open-source projects. By leveraging the Stellar network and +Soroban smart contracts, LearnVault ensures transparent, low-cost +microtransactions and immutable proof of skill. + +The platform operates on a dual-token model: LearnToken (LRN) serves as the +utility and reward currency, designed to sustain the platform's micro-economies, +while GovernanceToken (GOV) empowers stakeholders to shape the protocol's future +through decentralized decision-making. LearnVault’s architecture integrates a +secure registry for educational content, automated credential issuance via +soulbound tokens, and a robust treasury designed to maintain a healthy reserve +ratio. This whitepaper systematically outlines the system design, encompassing +the smart contract architecture, token economics, governance model, and security +protocols. It details the mechanisms for minting LRN, distributing GOV, and +ensuring the protocol's resilience against common attack vectors. Ultimately, +LearnVault establishes a sustainable ecosystem where education is not just +accessible, but economically rewarding, laying the foundation for a more +equitable global developer workforce. + +## Introduction + +### Problem Statement + +The global demand for skilled software developers continues to outpace supply, +yet millions of individuals in emerging markets lack access to affordable, +high-quality technical education. Existing solutions often suffer from +misaligned incentives, high dropout rates, and a lack of verifiable skill +assessment. + +- **Literacy and Skill Gaps:** While basic literacy rates have improved + globally, advanced technical literacy remains concentrated in developed + nations. +- **Developer Salary Gaps:** A developer in Sub-Saharan Africa or Southeast Asia + often earns a fraction of what their counterparts in North America or Western + Europe earn, despite comparable potential. +- **Existing Solutions:** Traditional bootcamps are prohibitively expensive, and + free platforms (like MOOCs) suffer from completion rates below 10%. + Furthermore, current verifiable credential systems are fragmented and often + lack intrinsic economic value. + +LearnVault directly addresses these issues by introducing an "Earn-to-Learn" +model that aligns the incentives of students, educators, and employers within a +permissionless ecosystem. + +## System Design + +The LearnVault architecture is built on Stellar and utilizes Soroban smart +contracts for high-speed, low-cost execution. + +### Architecture Diagram + +![Architecture Diagram](./architecture.png) + +### Contract Interactions and Data Flow + +1. **Registration & Profile:** Users create a decentralized identity (DID) + linked to their Stellar public key. +2. **Course Completion:** As users progress through modules, off-chain + computation validates their submissions (e.g., automated test suites for + code). +3. **Verification Oracle:** The validation result is passed to a decentralized + oracle, which triggers the Core Contract. +4. **Reward Distribution:** The Core Contract interacts with the LRN Token + Contract to disburse the calculated reward directly to the user's wallet. +5. **Credential Issuance:** Upon completing significant milestones, a Soulbound + Token (SBT) representing the specific skill is minted to the user's address. + +## Token Economics + +### LearnToken (LRN) + +LRN is the utility token powering the LearnVault ecosystem. + +- **Supply:** Capped at 1,000,000,000 LRN. +- **Minting Mechanics:** LRN is minted programmatically through a + "Proof-of-Learning" consensus. Tokens are unlocked from a predefined reward + pool and injected into circulation only when educational milestones are + verified on-chain. +- **Burn Conditions:** A portion of LRN is burned when: + - Sponsors pay for premium job listings or student recruitment. + - Users purchase advanced, premium courses or specialized tutoring. + - Penalty slashing occurs for malicious actors attempting to gamify the + oracle. + +$$ M*{t} = M*{0} + \sum*{i=1}^{t} (R*{v} - B\_{i}) $$ + +_Where $M_t$ is the supply at time $t$, $R_v$ is verified rewards, and $B_i$ +represents burned tokens._ + +### GovernanceToken (GOV) + +GOV is a fixed-supply token used for decentralized governance. + +- **Distribution Formula:** Distributed based on long-term participation and + value creation. + - 30% to early contributors and core team (vested over 4 years) + - 40% to top-tier learners and educators (distributed via a logarithmic + bonding curve) + - 30% allocated to the community treasury + +$$ GOV*{reward} = \alpha \cdot \log(1 + LRN*{earned}) \cdot V\_{multiplier} $$ + +_Where $V_{multiplier}$ is based on the user's holding duration of LRN.\_ + +- **Voting Mechanics:** 1 GOV = 1 Vote. Quadratic voting may be implemented for + specific high-impact proposals to prevent whale dominance. + +### Treasury Model + +The Treasury acts as the financial backbone of the protocol. + +- **Inflows:** Transaction fees from the Stellar network (if redirected), + enterprise subscription fees, and a percentage of premium course sales. +- **Outflows:** Developer grants, marketing initiatives, and liquidity + provisioning. +- **Reserve Ratio:** The Treasury algorithmically targets a minimum reserve + ratio of 20% against the circulating supply of LRN to ensure adequate + liquidity for user off-ramping. + +## Governance Model + +LearnVault transitions control from the core team to the community via a phased +DAO integration. + +- **Proposal Lifecycle:** + 1. **Draft:** Community discussion in forums. + 2. **Submission:** Requires a holding of 10,000 GOV to submit an on-chain + proposal. + 3. **Voting Period:** 7 days. + 4. **Timelock:** Passed proposals undergo a 48-hour timelock before + execution. +- **Quorum Calculations:** A minimum of 15% of the circulating GOV supply must + participate, with a simple majority (>50%) required for passage. +- **Upgrade Path:** Core Soroban contracts are upgradeable only via a successful + governance vote that triggers an automated upgrade routine post-timelock. + +## Security Analysis + +Protecting the integrity of the credentials and the treasury is paramount. + +- **Attack Vector: Sybil Attacks on Quizzes** + - _Mitigation:_ Integration of dynamic proof-of-humanity mechanisms, varied + test pools, and off-chain AI analysis of submission patterns to detect bot + activity. Slashing of staked LRN for confirmed Sybil nodes. +- **Attack Vector: Oracle Manipulation** + - _Mitigation:_ Multiple independent validation nodes must reach consensus on + test results before the Core Contract is invoked. +- **Attack Vector: Reentrancy and Contract Bugs** + - _Mitigation:_ Strict adherence to Checks-Effects-Interactions patterns in + Soroban, static analysis during CI/CD, and mandatory third-party audits + before major version upgrades. + +## Roadmap + +- **V1: "Foundation" (Q3 2026)** + - Launch core Soroban contracts on Stellar Mainnet. + - Release basic WebApp with 3 foundational courses (Rust, Soroban, + JavaScript). + - Implement basic LRN reward distribution system. +- **V2: "Expansion" (Q1 2027)** + - Introduce Soulbound Tokens (SBTs) for credentialing. + - Launch GOV token and basic DAO governance portal. + - Integrate decentralized oracle network for automated code evaluation. +- **V3: "Ecosystem" (Q4 2027)** + - Open platform for third-party educators to create courses and earn LRN. + - Enterprise portal for recruitment based on verified SBT credentials. + - Cross-chain interoperability research and implementation. + +## Conclusion + +LearnVault represents a paradigm shift in technical education. By tokenizing the +learning process upon the robust foundation of the Stellar network, LearnVault +creates a self-sustaining ecosystem where skill acquisition is directly +monetized. This protocol not only democratizes access to high-quality education +but also provides a verifiably skilled workforce to the global market. As the +community grows and governance transitions to the DAO, LearnVault is positioned +to become the premier decentralized standard for developer onboarding and +credentialing. diff --git a/docs/whitepaper.pdf b/docs/whitepaper.pdf new file mode 100644 index 00000000..133ca23e Binary files /dev/null and b/docs/whitepaper.pdf differ diff --git a/e2e/critical-flows.spec.ts b/e2e/critical-flows.spec.ts new file mode 100644 index 00000000..8f9c207c --- /dev/null +++ b/e2e/critical-flows.spec.ts @@ -0,0 +1,261 @@ +import { expect, test, type Page, type Route } from "@playwright/test" + +import { mockHorizonBalances } from "./fixtures/mock-horizon" +import { + installMockFreighter, + E2E_WALLET_ADDRESS, +} from "./fixtures/mock-wallet" + +type MockProposal = { + id: number + author_address: string + title: string + description: string + amount: string + votes_for: string + votes_against: string + status: "pending" | "approved" | "rejected" + deadline: string | null + created_at: string + user_vote_support: boolean | null +} + +type MockComment = { + id: number + proposal_id: string + author_address: string + parent_id: number | null + content: string + upvotes: number + downvotes: number + is_pinned: boolean + created_at: string +} + +async function fulfillJson(route: Route, body: unknown, status = 200) { + await route.fulfill({ + status, + contentType: "application/json", + body: JSON.stringify(body), + }) +} + +async function installDaoApiMocks(page: Page) { + let nextProposalId = 2 + const proposals: MockProposal[] = [ + { + id: 1, + author_address: E2E_WALLET_ADDRESS, + title: "Seed proposal", + description: "Initial backend-backed proposal", + amount: "100", + votes_for: "0", + votes_against: "0", + status: "pending", + deadline: "2099-01-01T00:00:00.000Z", + created_at: "2026-03-28T10:00:00.000Z", + user_vote_support: null, + }, + ] + + const commentsByProposal = new Map([ + [ + 1, + [ + { + id: 101, + proposal_id: "1", + author_address: E2E_WALLET_ADDRESS, + parent_id: null, + content: "Backend comment loaded", + upvotes: 4, + downvotes: 0, + is_pinned: false, + created_at: "2026-03-28T10:05:00.000Z", + }, + ], + ], + ]) + + await page.route("**/api/**", async (route) => { + const request = route.request() + const url = new URL(request.url()) + const { pathname, searchParams } = url + const method = request.method() + + if (pathname === "/api/proposals" && method === "GET") { + const viewer = searchParams.get("viewer_address") + const response = proposals.map((proposal) => ({ + ...proposal, + user_vote_support: + viewer?.toLowerCase() === E2E_WALLET_ADDRESS.toLowerCase() + ? proposal.user_vote_support + : null, + })) + + return fulfillJson(route, { + proposals: response, + total: response.length, + page: 1, + }) + } + + if (pathname === "/api/proposals" && method === "POST") { + const body = request.postDataJSON() as { + author_address: string + title: string + description: string + requested_amount: string + } + + const created: MockProposal = { + id: nextProposalId++, + author_address: body.author_address, + title: body.title, + description: body.description, + amount: body.requested_amount, + votes_for: "0", + votes_against: "0", + status: "pending", + deadline: "2099-01-01T00:00:00.000Z", + created_at: new Date().toISOString(), + user_vote_support: null, + } + + proposals.unshift(created) + commentsByProposal.set(created.id, [ + { + id: 200 + created.id, + proposal_id: String(created.id), + author_address: created.author_address, + parent_id: null, + content: "Fresh discussion thread", + upvotes: 0, + downvotes: 0, + is_pinned: false, + created_at: created.created_at, + }, + ]) + + return fulfillJson(route, { + proposal_id: created.id, + tx_hash: `tx-${created.id}`, + }) + } + + if ( + pathname.startsWith("/api/proposals/") && + pathname.endsWith("/comments") + ) { + const proposalId = Number.parseInt(pathname.split("/")[3] ?? "", 10) + return fulfillJson(route, commentsByProposal.get(proposalId) ?? []) + } + + if (pathname.startsWith("/api/proposals/") && method === "GET") { + const proposalId = Number.parseInt(pathname.split("/")[3] ?? "", 10) + const proposal = proposals.find((item) => item.id === proposalId) + + if (!proposal) { + return fulfillJson(route, { error: "Not found" }, 404) + } + + return fulfillJson(route, proposal) + } + + if (pathname.startsWith("/api/governance/voting-power/")) { + return fulfillJson(route, { gov_balance: "10" }) + } + + if (pathname === "/api/governance/vote" && method === "POST") { + const body = request.postDataJSON() as { + proposal_id: number + support: boolean + } + const proposal = proposals.find((item) => item.id === body.proposal_id) + + if (!proposal) { + return fulfillJson(route, { error: "Not found" }, 404) + } + + if (body.support) { + proposal.votes_for = String(Number(proposal.votes_for) + 10) + } else { + proposal.votes_against = String(Number(proposal.votes_against) + 10) + } + proposal.user_vote_support = body.support + + return fulfillJson(route, { + tx_hash: `vote-${proposal.id}`, + votes_for: proposal.votes_for, + votes_against: proposal.votes_against, + }) + } + + return route.continue() + }) +} + +test.describe("Critical flows (mock wallet)", () => { + test.beforeEach(async ({ page }) => { + await installMockFreighter(page) + await mockHorizonBalances(page) + await installDaoApiMocks(page) + }) + + test("Learner enroll flow is reachable", async ({ page }) => { + await page.goto("/learn") + + await expect(page.getByRole("heading", { name: "Learn" })).toBeVisible() + await page.getByTestId("enroll-course").click() + await expect( + page.getByRole("button", { name: /Mark as Complete/i }).first(), + ).toBeVisible() + }) + + test("Scholarship proposal submit appears in DAO proposals page", async ({ + page, + }) => { + await page.goto("/dao/propose") + + await expect( + page.getByRole("heading", { name: "Create Proposal" }), + ).toBeVisible() + await page.locator('input[name="title"]').fill("My Scholarship Proposal") + await page + .locator('textarea[name="description"]') + .fill("Fund one more scholar") + await page.locator('input[name="fundingAmount"]').fill("250") + await page.getByTestId("submit-proposal").click() + + await expect(page).toHaveURL(/\/dao\/proposals\?proposal=\d+/) + await expect(page.getByTestId("proposal-detail-title")).toHaveText( + "My Scholarship Proposal", + ) + await expect(page.getByTestId("proposal-title").first()).toHaveText( + "My Scholarship Proposal", + ) + }) + + test("DAO member vote flow is reachable on the backend-backed proposals page", async ({ + page, + }) => { + await page.goto("/dao/proposals?proposal=1") + + await expect(page.getByText("10 GOV").first()).toBeVisible() + await expect(page.getByTestId("vote-yes-count")).toContainText("0 GOV") + await page.getByTestId("vote-yes").click() + await expect(page.getByTestId("vote-yes-count")).toContainText("10 GOV") + await expect(page.getByText(/You voted Yes/i)).toBeVisible() + }) + + test("Comments load from the proposal comments endpoint", async ({ + page, + }) => { + await page.goto("/dao/proposals?proposal=1") + + await expect( + page.getByRole("heading", { name: /Discussion/i }), + ).toBeVisible() + await expect(page.getByText("Backend comment loaded")).toBeVisible() + }) +}) diff --git a/e2e/fixtures/mock-horizon.ts b/e2e/fixtures/mock-horizon.ts new file mode 100644 index 00000000..530c4144 --- /dev/null +++ b/e2e/fixtures/mock-horizon.ts @@ -0,0 +1,43 @@ +import { type Page } from "@playwright/test" + +type Bal = { + asset_type: "native" | "credit_alphanum4" | "credit_alphanum12" + balance: string + asset_code?: string + asset_issuer?: string +} + +export async function mockHorizonBalances( + page: Page, + opts?: { startLrn?: number }, +) { + let lrn = opts?.startLrn ?? 0 + + await page.route("**/accounts/**", async (route) => { + const balances: Bal[] = [ + { asset_type: "native", balance: "1000.0000000" }, + { + asset_type: "credit_alphanum4", + asset_code: "LRN", + asset_issuer: + "GDUKMGUGDZQK6YH4FQDPZ3W4E6Y2Q3A6VJY3N4FQ5ZJ3N4FQ5ZJ3N4FQ", + balance: String(lrn), + }, + ] + + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ balances }), + }) + }) + + return { + increaseLrn(by: number) { + lrn += by + }, + getLrn() { + return lrn + }, + } +} diff --git a/e2e/fixtures/mock-wallet.ts b/e2e/fixtures/mock-wallet.ts new file mode 100644 index 00000000..5509f6cd --- /dev/null +++ b/e2e/fixtures/mock-wallet.ts @@ -0,0 +1,41 @@ +import { type Page } from "@playwright/test" + +export const E2E_WALLET_ADDRESS = + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF" + +/** + * Provides a minimal Freighter API surface so StellarWalletsKit can read + * address/network/sign tx without a real extension. + */ +export async function installMockFreighter(page: Page) { + await page.addInitScript( + ({ address }) => { + const networkPassphrase = "Test SDF Network ; September 2015" + + ;(window as any).freighterApi = { + isConnected: async () => true, + isAllowed: async () => true, + getPublicKey: async () => address, + getNetwork: async () => "TESTNET", + getNetworkDetails: async () => ({ + network: "TESTNET", + networkPassphrase, + }), + signTransaction: async (xdr: string) => xdr, + signMessage: async (message: string) => `signed:${message}`, + } + + // Use a non-freighter stored wallet id so WalletProvider rehydrates + // from storage without attempting a real extension handshake. + localStorage.setItem("walletId", JSON.stringify("hot-wallet")) + localStorage.setItem("walletType", JSON.stringify("hot-wallet")) + localStorage.setItem("walletAddress", JSON.stringify(address)) + localStorage.setItem("walletNetwork", JSON.stringify("TESTNET")) + localStorage.setItem( + "networkPassphrase", + JSON.stringify(networkPassphrase), + ) + }, + { address: E2E_WALLET_ADDRESS }, + ) +} diff --git a/environments.toml b/environments.toml index 4011dadc..921c8466 100644 --- a/environments.toml +++ b/environments.toml @@ -9,54 +9,32 @@ name = "me" # Required. Keys for this account will be saved to `./.stellar/ident default = true # Optional. Whether to use this account as the `--source` for commands that need one. [development.contracts] -fungible_allowlist_example = { client = true, constructor_args = "--admin me --manager me --initial_supply 1000000000000000000000000" } -nft_enumerable_example = { client = true, constructor_args = "--owner me" } - -# Rather than in one list, TOML allows specifying contracts in their own "sections" -[development.contracts.guess_the_number] -# Generate a contract client (NPM package) for this contract. This means: -# - compile (build) the contract source to Wasm -# - deploy the contract to the `network` specified above -# - run any `init` script specified below -# - generate an NPM client (also called the "TS Bindings") for the deployed -# contract to the NPM workspace in `packages/*` -# - import the contract for easy access in the frontend in `src/contracts` -# -# You can only use `client = true` when: -# - the contract source must be part of the local Cargo workspace (in the -# PROJECT_ROOT/contracts folder) -# - The specified name here ("guess_the_number") must match the -# underscored-version of the `name` in the contract's Cargo.toml. -# - The environment is `development` or `testing` +# USDC test token for local development - deployed as a Stellar Asset Contract +usdc_test_token = { client = true, constructor_args = "--admin me --initial_supply 1000000000000000" } + +[development.contracts.learn_token] +client = true +constructor_args = "--admin me" + +[development.contracts.governance_token] +client = true +constructor_args = "--admin me" + +[development.contracts.scholar_nft] +client = true +constructor_args = "--admin me" + +[development.contracts.course_milestone] +client = true +constructor_args = "--admin me" + +[development.contracts.scholarship_treasury] client = true +constructor_args = "--admin me" -# If your contract has a `__constructor`, specify your arguments to it here. -# These are the same arguments you could pass after the `--` in a call to -# `stellar contract deploy` -# Only available in `development` and `testing` environments -constructor_args = """ ---admin me -""" - -# Calls to the contract to make after it's deployed and initialized with -# `constructor_args`. Commands here will be appended to: -# -# stellar contract invoke \ -# --id [this contract's id] \ -# --source [first (or `default`) account, specified above] \ -# --network [specified above] \ -# -- -# -# That is, each line in this `init` script is *only* the part after the `--` in -# a `stellar contract invoke` command. -# -# To use another account as the `--source`, prefix the line with -# `STELLAR_ACCOUNT=other-account`. -# -# Only supported in `development` and `testing` environments. -after_deploy = """ -reset -""" +[development.contracts.milestone_escrow] +client = true +constructor_args = "--admin me --treasury me" # Coming Soon: Specify live contracts to bind & import in this project using the given name. # During initialization, these contracts will also be "spooned" into the development network, @@ -83,7 +61,18 @@ default = true # soroban-increment-contract = { id = "C567..." } # soroban-token-contract = { id = "C678..." } # eurc = { id = "C789..." } -guess_the_number = { client = true, constructor_args = "--admin me", after_deploy = "reset" } +learn_token = { id = "C..." } +governance_token = { id = "C..." } +scholar_nft = { id = "C..." } +course_milestone = { id = "C..." } +scholarship_treasury = { id = "C..." } +milestone_escrow = { id = "C..." } + +# USDC on Stellar Testnet +# For testnet, we use a test USDC token deployed as a Stellar Asset Contract +# Official Circle USDC mainnet: CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75 +# Testnet USDC (to be deployed): Use the fungible_allowlist contract as a test USDC token +usdc_testnet = { client = true, constructor_args = "--admin testnet-user --manager testnet-user --initial_supply 1000000000000000" } ### Production environment configuration [production.network] @@ -102,3 +91,7 @@ default = true # soroban-increment-contract = { id = "C543..." } # soroban-token-contract = { id = "C432..." } # eurc = { id = "C321..." } + +# Official Circle USDC on Stellar Mainnet +# Contract ID: CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75 +usdc_mainnet = { id = "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75" } diff --git a/eslint.config.js b/eslint.config.js index 1aa76980..471835de 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,6 +10,9 @@ export default [ "target/packages", "src/contracts/*", "!src/contracts/util.ts", + "contracts/**", + "**/*.yml", + "**/*.yaml", ]), ...config, { diff --git a/index.html b/index.html index 1c85cd2d..48302f69 100644 --- a/index.html +++ b/index.html @@ -8,39 +8,67 @@ ;(function () { var storageKey = "learnvault:theme" var root = document.documentElement - var themeClassNames = { - light: "sds-theme-light", - dark: "sds-theme-dark", - } var applyTheme = function (theme) { - var themeClass = themeClassNames[theme] - root.classList.remove(themeClassNames.light, themeClassNames.dark) - root.classList.add(themeClass) + root.classList.remove( + "sds-theme-light", + "sds-theme-dark", + "light", + "dark", + ) + if (theme === "dark") { + root.classList.add("sds-theme-dark", "dark") + } else { + root.classList.add("sds-theme-light", "light") + } root.setAttribute("data-theme", theme) - root.setAttribute("data-sds-theme", themeClass) + root.setAttribute("data-sds-theme", "sds-theme-" + theme) root.style.colorScheme = theme } try { - var rawTheme = localStorage.getItem(storageKey) - var parsedTheme = rawTheme ? JSON.parse(rawTheme) : null - var preferredTheme = - parsedTheme === "light" || parsedTheme === "dark" - ? parsedTheme - : window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light" - - applyTheme(preferredTheme) - } catch (error) { - applyTheme("light") + var raw = localStorage.getItem(storageKey) + var stored = raw ? JSON.parse(raw) : null + // Default to dark — the app is designed dark-first + applyTheme(stored === "light" ? "light" : "dark") + } catch (e) { + applyTheme("dark") } })() - Scaffold Stellar Starter App + LearnVault — Education Protocol for the Stellar Network + + + + + + + + + + + + + + + diff --git a/issues.md b/issues.md new file mode 100644 index 00000000..e721536d --- /dev/null +++ b/issues.md @@ -0,0 +1,27 @@ +#720 security: add security headers middleware (helmet.js) Repo Avatar +bakeronchain/learnvault Standard security headers (HSTS, CSP, X-Frame-Options, +etc.) may not all be set.\n\nAction:\n- [ ] Add helmet middleware to Express +app\n- [ ] Configure Content Security Policy for Stellar SDK and IPFS\n- [ ] Set +Strict-Transport-Security header\n- [ ] Set X-Content-Type-Options: nosniff\n- [ +] Test headers with securityheaders.com\n- [ ] Ensure CSP does not break Stellar +wallet extensions + +#709 devops: add secret scanning to prevent credential commits Repo Avatar +bakeronchain/learnvault Action:\n- [ ] Enable GitHub secret scanning on the +repository\n- [ ] Add gitleaks or truffleHog to pre-commit hooks\n- [ ] Scan +entire git history for existing leaked secrets\n- [ ] Document what to do if a +secret is accidentally committed\n- [ ] Rotate any secrets found + +#708 devops: add database migration safety checks in CI Repo Avatar +bakeronchain/learnvault Action:\n- [ ] Run --dry-run migration check in CI +before deployment\n- [ ] Verify migrations are forward-only (no destructive +changes without explicit confirmation)\n- [ ] Check migration naming convention +consistency\n- [ ] Test migration rollback scripts exist\n- [ ] Alert if +migration takes more than N seconds (table lock risk) + +#750 feat: add community events calendar (hackathons, study groups, workshops) +Repo Avatar bakeronchain/learnvault Action:\n- [ ] Add events table (title, +description, date, type, link)\n- [ ] Admin-created events\n- [ ] GET +/api/community/events endpoint\n- [ ] Calendar view on Community page\n- [ ] +Attendee RSVP / interest tracking\n- [ ] iCal export for personal calendar +integrationhttps://github.com/leojay-net/Stellar-Dex-Chat/https://github.com/leojay-net/Stellar-Dex-Chat/https://github.com/leojay-net/Stellar-Dex-Chat/https://github.com/leojay-net/Stellar-Dex-Chat/ diff --git a/lighthouserc.json b/lighthouserc.json new file mode 100644 index 00000000..4c67106f --- /dev/null +++ b/lighthouserc.json @@ -0,0 +1,49 @@ +{ + "ci": { + "collect": { + "numberOfRuns": 3, + "staticDistDir": "./dist", + "url": ["http://localhost/"], + "settings": { + "onlyCategories": ["performance"] + } + }, + "assert": { + "assertions": { + "categories:performance": [ + "error", + { + "minScore": 0.85 + } + ], + "first-contentful-paint": [ + "warn", + { + "maxNumericValue": 1500 + } + ], + "largest-contentful-paint": [ + "warn", + { + "maxNumericValue": 2500 + } + ], + "total-blocking-time": [ + "warn", + { + "maxNumericValue": 200 + } + ], + "cumulative-layout-shift": [ + "warn", + { + "maxNumericValue": 0.1 + } + ] + } + }, + "upload": { + "target": "temporary-public-storage" + } + } +} diff --git a/package-lock.json b/package-lock.json index 443bd93d..fedad2ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13893 +1,17832 @@ { - "name": "scaffold-stellar-frontend", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "scaffold-stellar-frontend", - "version": "0.0.1", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "@creit.tech/stellar-wallets-kit": "^1.9.5", - "@stellar/design-system": "^3.2.7", - "@stellar/stellar-sdk": "^14.4.3", - "@stellar/stellar-xdr-json": "^23.0.0", - "@tanstack/react-query": "^5.90.17", - "@theahaco/contract-explorer": "^1.1.0", - "@theahaco/ts-config": "^1.2.0", - "lossless-json": "^4.3.0", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "react-router-dom": "^7.12.0", - "zod": "^4.3.5" - }, - "devDependencies": { - "@types/lodash": "^4.17.23", - "@types/react": "^19.2.10", - "@types/react-dom": "^19.2.3", - "@types/react-router-dom": "^5.3.3", - "@vitejs/plugin-react": "^5.1.2", - "concurrently": "^9.2.1", - "dotenv": "^17.2.3", - "eslint": "^9.39.2", - "glob": "^13.0.0", - "globals": "^17.0.0", - "husky": "^9.1.7", - "lint-staged": "^16.2.7", - "prettier": "^3.8.0", - "typescript": "~5.9.3", - "vite": "^7.3.1", - "vite-plugin-node-polyfills": "^0.25.0", - "vite-plugin-wasm": "^3.5.0" - } - }, - "node_modules/@albedo-link/intent": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@albedo-link/intent/-/intent-0.12.0.tgz", - "integrity": "sha512-UlGBhi0qASDYOjLrOL4484vQ26Ee3zTK2oAgvPMClOs+1XNk3zbs3dECKZv+wqeSI8SkHow8mXLTa16eVh+dQA==", - "license": "MIT" - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@creit.tech/stellar-wallets-kit": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/@creit.tech/stellar-wallets-kit/-/stellar-wallets-kit-1.9.5.tgz", - "integrity": "sha512-b9E77r+o6Opow0CttmHdFBPeJpdLhOcLFTx3CbnwMQVivo94niap70y3A03F5cYgwVk9HhM4CJr0aimm0eNwxA==", - "dependencies": { - "@albedo-link/intent": "0.12.0", - "@creit.tech/xbull-wallet-connect": "^0.4.0", - "@hot-wallet/sdk": "1.0.11", - "@ledgerhq/hw-app-str": "7.0.4", - "@ledgerhq/hw-transport": "6.31.4", - "@ledgerhq/hw-transport-webusb": "6.29.4", - "@lobstrco/signer-extension-api": "1.0.0-beta.0", - "@ngneat/elf": "2.5.1", - "@ngneat/elf-devtools": "1.3.0", - "@ngneat/elf-entities": "5.0.2", - "@ngneat/elf-persist-state": "1.2.1", - "@stellar/freighter-api": "5.0.0", - "@trezor/connect-plugin-stellar": "9.2.1", - "@trezor/connect-web": "9.6.2", - "@walletconnect/modal": "2.6.2", - "@walletconnect/sign-client": "2.11.2", - "buffer": "6.0.3", - "events": "3.3.0", - "lit": "3.2.0", - "rxjs": "7.8.1" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "@stellar/stellar-base": "^14.0.0" - } - }, - "node_modules/@creit.tech/stellar-wallets-kit/node_modules/@stellar/stellar-sdk": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-13.3.0.tgz", - "integrity": "sha512-8+GHcZLp+mdin8gSjcgfb/Lb6sSMYRX6Nf/0LcSJxvjLQR0XHpjGzOiRbYb2jSXo51EnA6kAV5j+4Pzh5OUKUg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@stellar/stellar-base": "^13.1.0", - "axios": "^1.8.4", - "bignumber.js": "^9.3.0", - "eventsource": "^2.0.2", - "feaxios": "^0.0.23", - "randombytes": "^2.1.0", - "toml": "^3.0.0", - "urijs": "^1.19.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@creit.tech/stellar-wallets-kit/node_modules/@stellar/stellar-sdk/node_modules/@stellar/stellar-base": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-13.1.0.tgz", - "integrity": "sha512-90EArG+eCCEzDGj3OJNoCtwpWDwxjv+rs/RNPhvg4bulpjN/CSRj+Ys/SalRcfM4/WRC5/qAfjzmJBAuquWhkA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@stellar/js-xdr": "^3.1.2", - "base32.js": "^0.1.0", - "bignumber.js": "^9.1.2", - "buffer": "^6.0.3", - "sha.js": "^2.3.6", - "tweetnacl": "^1.0.3" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "sodium-native": "^4.3.3" - } - }, - "node_modules/@creit.tech/stellar-wallets-kit/node_modules/@trezor/connect-plugin-stellar": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@trezor/connect-plugin-stellar/-/connect-plugin-stellar-9.2.1.tgz", - "integrity": "sha512-Orz5gFZzYFZs1+cTsgg8fz/VWFjhl7pqMCqD5DVNZpXW+wrjwBaRbcGJZ+ibkPKU3AlM7Uv3SVD/pjaQmAkZ2Q==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@trezor/utils": "9.4.1" - }, - "peerDependencies": { - "@stellar/stellar-sdk": "^13.3.0", - "@trezor/connect": "9.x.x", - "tslib": "^2.6.2" - } - }, - "node_modules/@creit.tech/stellar-wallets-kit/node_modules/@trezor/utils": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/@trezor/utils/-/utils-9.4.1.tgz", - "integrity": "sha512-9MYNa99tzXiTBnKadABoY2D80YL9Mh3ntM5wziwVhjZ4HyhqFH6BsCxwFpWYLUIKBctD55QEdE4bASoqp7Ad1A==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "bignumber.js": "^9.3.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@creit.tech/xbull-wallet-connect": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@creit.tech/xbull-wallet-connect/-/xbull-wallet-connect-0.4.0.tgz", - "integrity": "sha512-LrCUIqUz50SkZ4mv2hTqSmwews8CNRYVoZ9+VjLsK/1U8PByzXTxv1vZyenj6avRTG86ifpoeihz7D3D5YIDrQ==", - "dependencies": { - "rxjs": "^7.5.5", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emurgo/cardano-serialization-lib-browser": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-browser/-/cardano-serialization-lib-browser-13.2.1.tgz", - "integrity": "sha512-7RfX1gI16Vj2DgCp/ZoXqyLAakWo6+X95ku/rYGbVzuS/1etrlSiJmdbmdm+eYmszMlGQjrtOJQeVLXoj4L/Ag==", - "license": "MIT" - }, - "node_modules/@emurgo/cardano-serialization-lib-nodejs": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-nodejs/-/cardano-serialization-lib-nodejs-13.2.0.tgz", - "integrity": "sha512-Bz1zLGEqBQ0BVkqt1OgMxdBOE3BdUWUd7Ly9Ecr/aUwkA8AV1w1XzBMe4xblmJHnB1XXNlPH4SraXCvO+q0Mig==", - "license": "MIT" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@ethereumjs/common": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-10.1.0.tgz", - "integrity": "sha512-zIHCy0i2LFmMDp+QkENyoPGxcoD3QzeNVhx6/vE4nJk4uWGNXzO8xJ2UC4gtGW4UJTAOXja8Z1yZMVeRc2/+Ew==", - "license": "MIT", - "dependencies": { - "@ethereumjs/util": "^10.1.0", - "eventemitter3": "^5.0.1" - } - }, - "node_modules/@ethereumjs/rlp": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-10.1.0.tgz", - "integrity": "sha512-r67BJbwilammAqYI4B5okA66cNdTlFzeWxPNJOolKV52ZS/flo0tUBf4x4gxWXBgh48OgsdFV1Qp5pRoSe8IhQ==", - "license": "MPL-2.0", - "bin": { - "rlp": "bin/rlp.cjs" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@ethereumjs/tx": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-10.1.0.tgz", - "integrity": "sha512-svG6pyzUZDpunafszf2BaolA6Izuvo8ZTIETIegpKxAXYudV1hmzPQDdSI+d8nHCFyQfEFbQ6tfUq95lNArmmg==", - "license": "MPL-2.0", - "dependencies": { - "@ethereumjs/common": "^10.1.0", - "@ethereumjs/rlp": "^10.1.0", - "@ethereumjs/util": "^10.1.0", - "ethereum-cryptography": "^3.2.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@ethereumjs/util": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-10.1.0.tgz", - "integrity": "sha512-GGTCkRu1kWXbz2JoUnIYtJBOoA9T5akzsYa91Bh+DZQ3Cj4qXj3hkNU0Rx6wZlbcmkmhQfrjZfVt52eJO/y2nA==", - "license": "MPL-2.0", - "dependencies": { - "@ethereumjs/rlp": "^10.1.0", - "ethereum-cryptography": "^3.2.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@fivebinaries/coin-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@fivebinaries/coin-selection/-/coin-selection-3.0.0.tgz", - "integrity": "sha512-h25Pn1ZA7oqQBQDodGAgIsQt66T2wDge9onBKNqE66WNWL0KJiKJbpij8YOLo5AAlEIg5IS7EB1QjBgDOIg6DQ==", - "license": "Apache-2.0", - "dependencies": { - "@emurgo/cardano-serialization-lib-browser": "^13.2.0", - "@emurgo/cardano-serialization-lib-nodejs": "13.2.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, - "node_modules/@hot-wallet/sdk": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@hot-wallet/sdk/-/sdk-1.0.11.tgz", - "integrity": "sha512-qRDH/4yqnRCnk7L/Qd0/LDOKDUKWcFgvf6eRELJkP0OgxIe65i/iXaG+u2lL0mLbTGkiWYk67uAvEerNUv2gzA==", - "dependencies": { - "@near-js/crypto": "^1.4.0", - "@near-js/utils": "^1.0.0", - "@near-wallet-selector/core": "^8.9.13", - "@solana/wallet-adapter-base": "^0.9.23", - "@solana/web3.js": "^1.95.0", - "borsh": "^2.0.0", - "js-sha256": "^0.11.0", - "sha1": "^1.1.1", - "uuid4": "^2.0.3" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@ledgerhq/devices": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.7.0.tgz", - "integrity": "sha512-3pSOULPUhClk2oOxa6UnoyXW8+05FK7r6cpq2WiTey4kyIUjmIraO7AX9lhz9IU73dGVtBMdU+NCPPO2RYJaTQ==", - "license": "Apache-2.0", - "dependencies": { - "@ledgerhq/errors": "^6.27.0", - "@ledgerhq/logs": "^6.13.0", - "rxjs": "^7.8.1", - "semver": "^7.3.5" - } - }, - "node_modules/@ledgerhq/errors": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.27.0.tgz", - "integrity": "sha512-EE2hATONHdNP3YWFe3rZwwpSEzI5oN+q/xTjOulnjHZo84TLjungegEJ54A/Pzld0woulkkeVA27FbW5SAO1aA==", - "license": "Apache-2.0" - }, - "node_modules/@ledgerhq/hw-app-str": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@ledgerhq/hw-app-str/-/hw-app-str-7.0.4.tgz", - "integrity": "sha512-ArKnGCZaGnUPqoKaR9OE+ZGcGARKJLHthl/aybQWOIo952+cQpgcn0mg5yK1Amd9EiKfArCTlLJ3wAafO5d/lw==", - "license": "Apache-2.0", - "dependencies": { - "@ledgerhq/errors": "^6.19.1", - "@ledgerhq/hw-transport": "^6.31.4", - "bip32-path": "^0.4.2" - } - }, - "node_modules/@ledgerhq/hw-transport": { - "version": "6.31.4", - "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.31.4.tgz", - "integrity": "sha512-6c1ir/cXWJm5dCWdq55NPgCJ3UuKuuxRvf//Xs36Bq9BwkV2YaRQhZITAkads83l07NAdR16hkTWqqpwFMaI6A==", - "license": "Apache-2.0", - "dependencies": { - "@ledgerhq/devices": "^8.4.4", - "@ledgerhq/errors": "^6.19.1", - "@ledgerhq/logs": "^6.12.0", - "events": "^3.3.0" - } - }, - "node_modules/@ledgerhq/hw-transport-webusb": { - "version": "6.29.4", - "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.29.4.tgz", - "integrity": "sha512-HoGF1LlBT9HEGBQy2XeCHrFdv/FEOZU0+J+yfKcgAQIAiASr2MLvdzwoJbUS8h6Gn+vc+/BjzBSO3JNn7Loqbg==", - "license": "Apache-2.0", - "dependencies": { - "@ledgerhq/devices": "^8.4.4", - "@ledgerhq/errors": "^6.19.1", - "@ledgerhq/hw-transport": "^6.31.4", - "@ledgerhq/logs": "^6.12.0" - } - }, - "node_modules/@ledgerhq/logs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.13.0.tgz", - "integrity": "sha512-4+qRW2Pc8V+btL0QEmdB2X+uyx0kOWMWE1/LWsq5sZy3Q5tpi4eItJS6mB0XL3wGW59RQ+8bchNQQ1OW/va8Og==", - "license": "Apache-2.0" - }, - "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz", - "integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==", - "license": "BSD-3-Clause" - }, - "node_modules/@lit/reactive-element": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.1.tgz", - "integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.4.0" - } - }, - "node_modules/@lobstrco/signer-extension-api": { - "version": "1.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@lobstrco/signer-extension-api/-/signer-extension-api-1.0.0-beta.0.tgz", - "integrity": "sha512-16V34W9MyTgunGvgkzv1JmV+k59OjNWCrNOH+KH+6vWamcGDGBnFhvRgGEarEhINYITMGkdqEvaEy7qTD5s5cw==", - "license": "GPL-3.0" - }, - "node_modules/@mobily/ts-belt": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@mobily/ts-belt/-/ts-belt-3.13.1.tgz", - "integrity": "sha512-K5KqIhPI/EoCTbA6CGbrenM9s41OouyK8A03fGJJcla/zKucsgLbz8HNbeseoLarRPgyWJsUyCYqFhI7t3Ra9Q==", - "license": "MIT", - "engines": { - "node": ">= 10.*" - } - }, - "node_modules/@motionone/animation": { - "version": "10.18.0", - "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz", - "integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==", - "license": "MIT", - "dependencies": { - "@motionone/easing": "^10.18.0", - "@motionone/types": "^10.17.1", - "@motionone/utils": "^10.18.0", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/dom": { - "version": "10.18.0", - "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.18.0.tgz", - "integrity": "sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A==", - "license": "MIT", - "dependencies": { - "@motionone/animation": "^10.18.0", - "@motionone/generators": "^10.18.0", - "@motionone/types": "^10.17.1", - "@motionone/utils": "^10.18.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/easing": { - "version": "10.18.0", - "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz", - "integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==", - "license": "MIT", - "dependencies": { - "@motionone/utils": "^10.18.0", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/generators": { - "version": "10.18.0", - "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz", - "integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==", - "license": "MIT", - "dependencies": { - "@motionone/types": "^10.17.1", - "@motionone/utils": "^10.18.0", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/svelte": { - "version": "10.16.4", - "resolved": "https://registry.npmjs.org/@motionone/svelte/-/svelte-10.16.4.tgz", - "integrity": "sha512-zRVqk20lD1xqe+yEDZhMYgftsuHc25+9JSo+r0a0OWUJFocjSV9D/+UGhX4xgJsuwB9acPzXLr20w40VnY2PQA==", - "license": "MIT", - "dependencies": { - "@motionone/dom": "^10.16.4", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/types": { - "version": "10.17.1", - "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz", - "integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==", - "license": "MIT" - }, - "node_modules/@motionone/utils": { - "version": "10.18.0", - "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz", - "integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==", - "license": "MIT", - "dependencies": { - "@motionone/types": "^10.17.1", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/vue": { - "version": "10.16.4", - "resolved": "https://registry.npmjs.org/@motionone/vue/-/vue-10.16.4.tgz", - "integrity": "sha512-z10PF9JV6SbjFq+/rYabM+8CVlMokgl8RFGvieSGNTmrkQanfHn+15XBrhG3BgUfvmTeSeyShfOHpG0i9zEdcg==", - "deprecated": "Motion One for Vue is deprecated. Use Oku Motion instead https://oku-ui.com/motion", - "license": "MIT", - "dependencies": { - "@motionone/dom": "^10.16.4", - "tslib": "^2.3.1" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@near-js/accounts": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@near-js/accounts/-/accounts-1.4.1.tgz", - "integrity": "sha512-ni3QT9H3NdrbVVKyx56yvz93r89Dvpc/vgVtiIK2OdXjkK6jcj+UKMDRQ6F7rd9qJOInLkHZbVBtcR6j1CXLjw==", - "license": "ISC", - "peer": true, - "dependencies": { - "@near-js/crypto": "1.4.2", - "@near-js/providers": "1.0.3", - "@near-js/signers": "0.2.2", - "@near-js/transactions": "1.3.3", - "@near-js/types": "0.3.1", - "@near-js/utils": "1.1.0", - "@noble/hashes": "1.7.1", - "borsh": "1.0.0", - "depd": "2.0.0", - "is-my-json-valid": "^2.20.6", - "lru_map": "0.4.1", - "near-abi": "0.2.0" - } - }, - "node_modules/@near-js/accounts/node_modules/borsh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", - "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/@near-js/crypto": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@near-js/crypto/-/crypto-1.4.2.tgz", - "integrity": "sha512-GRfchsyfWvSAPA1gI9hYhw5FH94Ac1BUo+Cmp5rSJt/V0K3xVzCWgOQxvv4R3kDnWjaXJEuAmpEEnr4Bp3FWrA==", - "license": "ISC", - "dependencies": { - "@near-js/types": "0.3.1", - "@near-js/utils": "1.1.0", - "@noble/curves": "1.8.1", - "borsh": "1.0.0", - "randombytes": "2.1.0", - "secp256k1": "5.0.1" - } - }, - "node_modules/@near-js/crypto/node_modules/borsh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", - "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0" - }, - "node_modules/@near-js/keystores": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@near-js/keystores/-/keystores-0.2.2.tgz", - "integrity": "sha512-DLhi/3a4qJUY+wgphw2Jl4S+L0AKsUYm1mtU0WxKYV5OBwjOXvbGrXNfdkheYkfh3nHwrQgtjvtszX6LrRXLLw==", - "license": "ISC", - "peer": true, - "dependencies": { - "@near-js/crypto": "1.4.2", - "@near-js/types": "0.3.1" - } - }, - "node_modules/@near-js/keystores-browser": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@near-js/keystores-browser/-/keystores-browser-0.2.2.tgz", - "integrity": "sha512-Pxqm7WGtUu6zj32vGCy9JcEDpZDSB5CCaLQDTQdF3GQyL0flyRv2I/guLAgU5FLoYxU7dJAX9mslJhPW7P2Bfw==", - "license": "ISC", - "peer": true, - "dependencies": { - "@near-js/crypto": "1.4.2", - "@near-js/keystores": "0.2.2" - } - }, - "node_modules/@near-js/keystores-node": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@near-js/keystores-node/-/keystores-node-0.1.2.tgz", - "integrity": "sha512-MWLvTszZOVziiasqIT/LYNhUyWqOJjDGlsthOsY6dTL4ZcXjjmhmzrbFydIIeQr+CcEl5wukTo68ORI9JrHl6g==", - "license": "ISC", - "peer": true, - "dependencies": { - "@near-js/crypto": "1.4.2", - "@near-js/keystores": "0.2.2" - } - }, - "node_modules/@near-js/providers": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@near-js/providers/-/providers-1.0.3.tgz", - "integrity": "sha512-VJMboL14R/+MGKnlhhE3UPXCGYvMd1PpvF9OqZ9yBbulV7QVSIdTMfY4U1NnDfmUC2S3/rhAEr+3rMrIcNS7Fg==", - "license": "ISC", - "peer": true, - "dependencies": { - "@near-js/transactions": "1.3.3", - "@near-js/types": "0.3.1", - "@near-js/utils": "1.1.0", - "borsh": "1.0.0", - "exponential-backoff": "^3.1.2" - }, - "optionalDependencies": { - "node-fetch": "2.6.7" - } - }, - "node_modules/@near-js/providers/node_modules/borsh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", - "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/@near-js/providers/node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@near-js/signers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@near-js/signers/-/signers-0.2.2.tgz", - "integrity": "sha512-M6ib+af9zXAPRCjH2RyIS0+RhCmd9gxzCeIkQ+I2A3zjgGiEDkBZbYso9aKj8Zh2lPKKSH7h+u8JGymMOSwgyw==", - "license": "ISC", - "peer": true, - "dependencies": { - "@near-js/crypto": "1.4.2", - "@near-js/keystores": "0.2.2", - "@noble/hashes": "1.3.3" - } - }, - "node_modules/@near-js/signers/node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@near-js/transactions": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@near-js/transactions/-/transactions-1.3.3.tgz", - "integrity": "sha512-1AXD+HuxlxYQmRTLQlkVmH+RAmV3HwkAT8dyZDu+I2fK/Ec9BQHXakOJUnOBws3ihF+akQhamIBS5T0EXX/Ylw==", - "license": "ISC", - "peer": true, - "dependencies": { - "@near-js/crypto": "1.4.2", - "@near-js/signers": "0.2.2", - "@near-js/types": "0.3.1", - "@near-js/utils": "1.1.0", - "@noble/hashes": "1.7.1", - "borsh": "1.0.0" - } - }, - "node_modules/@near-js/transactions/node_modules/borsh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", - "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/@near-js/types": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@near-js/types/-/types-0.3.1.tgz", - "integrity": "sha512-8qIA7ynAEAuVFNAQc0cqz2xRbfyJH3PaAG5J2MgPPhD18lu/tCGd6pzYg45hjhtiJJRFDRjh/FUWKS+ZiIIxUw==", - "license": "ISC" - }, - "node_modules/@near-js/utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@near-js/utils/-/utils-1.1.0.tgz", - "integrity": "sha512-5XWRq7xpu8Wud9pRXe2U347KXyi0mXofedUY2DQ9TaqiZUcMIaN9xj7DbCs2v6dws3pJyYrT1KWxeNp5fSaY3w==", - "license": "ISC", - "dependencies": { - "@near-js/types": "0.3.1", - "@scure/base": "^1.2.0", - "depd": "2.0.0", - "mustache": "4.0.0" - } - }, - "node_modules/@near-js/wallet-account": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@near-js/wallet-account/-/wallet-account-1.3.3.tgz", - "integrity": "sha512-GDzg/Kz0GBYF7tQfyQQQZ3vviwV8yD+8F2lYDzsWJiqIln7R1ov0zaXN4Tii86TeS21KPn2hHAsVu3Y4txa8OQ==", - "license": "ISC", - "peer": true, - "dependencies": { - "@near-js/accounts": "1.4.1", - "@near-js/crypto": "1.4.2", - "@near-js/keystores": "0.2.2", - "@near-js/providers": "1.0.3", - "@near-js/signers": "0.2.2", - "@near-js/transactions": "1.3.3", - "@near-js/types": "0.3.1", - "@near-js/utils": "1.1.0", - "borsh": "1.0.0" - } - }, - "node_modules/@near-js/wallet-account/node_modules/borsh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", - "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/@near-wallet-selector/core": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/@near-wallet-selector/core/-/core-8.10.2.tgz", - "integrity": "sha512-MH8sg6XHyylq2ZXxnOjrKHMCmuRgFfpfdC816fW0R8hctZiXZ0lmfLvgG1xfA2BAxrVytiU1g3dcE97/P5cZqg==", - "dependencies": { - "borsh": "1.0.0", - "events": "3.3.0", - "js-sha256": "0.9.0", - "rxjs": "7.8.1" - }, - "peerDependencies": { - "near-api-js": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/@near-wallet-selector/core/node_modules/borsh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", - "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0" - }, - "node_modules/@near-wallet-selector/core/node_modules/js-sha256": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", - "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==", - "license": "MIT" - }, - "node_modules/@ngneat/elf": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@ngneat/elf/-/elf-2.5.1.tgz", - "integrity": "sha512-13BItNZFgHglTiXuP9XhisNczwQ5QSzH+imAv9nAPsdbCq/3ortqkIYRnlxB8DGPVcuIjLujQ4OcZa+9QWgZtw==", - "license": "MIT", - "peerDependencies": { - "rxjs": ">=7.0.0" - } - }, - "node_modules/@ngneat/elf-devtools": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ngneat/elf-devtools/-/elf-devtools-1.3.0.tgz", - "integrity": "sha512-J9+4Vk/S/nKFGnXGZUDYrIx/K8Jfv4TLpzR3voBSOtSeq+c8Q0hdmo8iW7oaK6y5FWFDk0VJktlEh9cjylYxFg==", - "license": "MIT" - }, - "node_modules/@ngneat/elf-entities": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@ngneat/elf-entities/-/elf-entities-5.0.2.tgz", - "integrity": "sha512-G4ag51lvM3tOSgpxVVFYAgsh/bOL5BkNb4Z0VtosaM/CTWTHoNrf8UdvcaeJ3+sP1RS3bmEdZ9xUE8ifnVxssA==", - "license": "MIT", - "peerDependencies": { - "@ngneat/elf": ">=2.5.0", - "rxjs": ">=7.0.0" - } - }, - "node_modules/@ngneat/elf-persist-state": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@ngneat/elf-persist-state/-/elf-persist-state-1.2.1.tgz", - "integrity": "sha512-R+5IRLC35cDT403sSs37UeqqOpHNnCwHl3eicPv/Rc+JJ7Av1bcQClSF2mHC0jE4pkYmEuVghpzUntvmrKCCwA==", - "license": "MIT", - "peerDependencies": { - "rxjs": ">=7.0.0" - } - }, - "node_modules/@noble/ciphers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", - "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/plugin-inject": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", - "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.33.22", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.33.22.tgz", - "integrity": "sha512-auUj4k+f4pyrIVf4GW5UKquSZFHJWri06QgARy9C0t9ZTjJLIuNIrr1yl9bWcJWJ1Gz1vOvYN1D+QPaIlNMVkQ==", - "license": "MIT" - }, - "node_modules/@solana-program/compute-budget": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@solana-program/compute-budget/-/compute-budget-0.8.0.tgz", - "integrity": "sha512-qPKxdxaEsFxebZ4K5RPuy7VQIm/tfJLa1+Nlt3KNA8EYQkz9Xm8htdoEaXVrer9kpgzzp9R3I3Bh6omwCM06tQ==", - "license": "Apache-2.0", - "peerDependencies": { - "@solana/kit": "^2.1.0" - } - }, - "node_modules/@solana-program/stake": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@solana-program/stake/-/stake-0.2.1.tgz", - "integrity": "sha512-ssNPsJv9XHaA+L7ihzmWGYcm/+XYURQ8UA3wQMKf6ccEHyHOUgoglkkDU/BoA0+wul6HxZUN0tHFymC0qFw6sg==", - "license": "MIT", - "peerDependencies": { - "@solana/kit": "^2.1.0" - } - }, - "node_modules/@solana-program/system": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@solana-program/system/-/system-0.7.0.tgz", - "integrity": "sha512-FKTBsKHpvHHNc1ATRm7SlC5nF/VdJtOSjldhcyfMN9R7xo712Mo2jHIzvBgn8zQO5Kg0DcWuKB7268Kv1ocicw==", - "license": "Apache-2.0", - "peerDependencies": { - "@solana/kit": "^2.1.0" - } - }, - "node_modules/@solana-program/token": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@solana-program/token/-/token-0.5.1.tgz", - "integrity": "sha512-bJvynW5q9SFuVOZ5vqGVkmaPGA0MCC+m9jgJj1nk5m20I389/ms69ASnhWGoOPNcie7S9OwBX0gTj2fiyWpfag==", - "license": "Apache-2.0", - "peerDependencies": { - "@solana/kit": "^2.1.0" - } - }, - "node_modules/@solana-program/token-2022": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@solana-program/token-2022/-/token-2022-0.4.2.tgz", - "integrity": "sha512-zIpR5t4s9qEU3hZKupzIBxJ6nUV5/UVyIT400tu9vT1HMs5JHxaTTsb5GUhYjiiTvNwU0MQavbwc4Dl29L0Xvw==", - "license": "Apache-2.0", - "peerDependencies": { - "@solana/kit": "^2.1.0", - "@solana/sysvars": "^2.1.0" - } - }, - "node_modules/@solana/accounts": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/accounts/-/accounts-2.3.0.tgz", - "integrity": "sha512-QgQTj404Z6PXNOyzaOpSzjgMOuGwG8vC66jSDB+3zHaRcEPRVRd2sVSrd1U6sHtnV3aiaS6YyDuPQMheg4K2jw==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "2.3.0", - "@solana/codecs-core": "2.3.0", - "@solana/codecs-strings": "2.3.0", - "@solana/errors": "2.3.0", - "@solana/rpc-spec": "2.3.0", - "@solana/rpc-types": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/addresses": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/addresses/-/addresses-2.3.0.tgz", - "integrity": "sha512-ypTNkY2ZaRFpHLnHAgaW8a83N0/WoqdFvCqf4CQmnMdFsZSdC7qOwcbd7YzdaQn9dy+P2hybewzB+KP7LutxGA==", - "license": "MIT", - "dependencies": { - "@solana/assertions": "2.3.0", - "@solana/codecs-core": "2.3.0", - "@solana/codecs-strings": "2.3.0", - "@solana/errors": "2.3.0", - "@solana/nominal-types": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/assertions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/assertions/-/assertions-2.3.0.tgz", - "integrity": "sha512-Ekoet3khNg3XFLN7MIz8W31wPQISpKUGDGTylLptI+JjCDWx3PIa88xjEMqFo02WJ8sBj2NLV64Xg1sBcsHjZQ==", - "license": "MIT", - "dependencies": { - "@solana/errors": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/buffer-layout": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", - "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", - "license": "MIT", - "dependencies": { - "buffer": "~6.0.3" - }, - "engines": { - "node": ">=5.10" - } - }, - "node_modules/@solana/codecs": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.3.0.tgz", - "integrity": "sha512-JVqGPkzoeyU262hJGdH64kNLH0M+Oew2CIPOa/9tR3++q2pEd4jU2Rxdfye9sd0Ce3XJrR5AIa8ZfbyQXzjh+g==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "2.3.0", - "@solana/codecs-data-structures": "2.3.0", - "@solana/codecs-numbers": "2.3.0", - "@solana/codecs-strings": "2.3.0", - "@solana/options": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/codecs-core": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", - "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", - "license": "MIT", - "dependencies": { - "@solana/errors": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/codecs-data-structures": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.3.0.tgz", - "integrity": "sha512-qvU5LE5DqEdYMYgELRHv+HMOx73sSoV1ZZkwIrclwUmwTbTaH8QAJURBj0RhQ/zCne7VuLLOZFFGv6jGigWhSw==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "2.3.0", - "@solana/codecs-numbers": "2.3.0", - "@solana/errors": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/codecs-numbers": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", - "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "2.3.0", - "@solana/errors": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/codecs-strings": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.3.0.tgz", - "integrity": "sha512-y5pSBYwzVziXu521hh+VxqUtp0hYGTl1eWGoc1W+8mdvBdC1kTqm/X7aYQw33J42hw03JjryvYOvmGgk3Qz/Ug==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "2.3.0", - "@solana/codecs-numbers": "2.3.0", - "@solana/errors": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "fastestsmallesttextencoderdecoder": "^1.0.22", - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/errors": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", - "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", - "license": "MIT", - "dependencies": { - "chalk": "^5.4.1", - "commander": "^14.0.0" - }, - "bin": { - "errors": "bin/cli.mjs" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/fast-stable-stringify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/fast-stable-stringify/-/fast-stable-stringify-2.3.0.tgz", - "integrity": "sha512-KfJPrMEieUg6D3hfQACoPy0ukrAV8Kio883llt/8chPEG3FVTX9z/Zuf4O01a15xZmBbmQ7toil2Dp0sxMJSxw==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/functional": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/functional/-/functional-2.3.0.tgz", - "integrity": "sha512-AgsPh3W3tE+nK3eEw/W9qiSfTGwLYEvl0rWaxHht/lRcuDVwfKRzeSa5G79eioWFFqr+pTtoCr3D3OLkwKz02Q==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/instructions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/instructions/-/instructions-2.3.0.tgz", - "integrity": "sha512-PLMsmaIKu7hEAzyElrk2T7JJx4D+9eRwebhFZpy2PXziNSmFF929eRHKUsKqBFM3cYR1Yy3m6roBZfA+bGE/oQ==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "2.3.0", - "@solana/errors": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/keys": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/keys/-/keys-2.3.0.tgz", - "integrity": "sha512-ZVVdga79pNH+2pVcm6fr2sWz9HTwfopDVhYb0Lh3dh+WBmJjwkabXEIHey2rUES7NjFa/G7sV8lrUn/v8LDCCQ==", - "license": "MIT", - "dependencies": { - "@solana/assertions": "2.3.0", - "@solana/codecs-core": "2.3.0", - "@solana/codecs-strings": "2.3.0", - "@solana/errors": "2.3.0", - "@solana/nominal-types": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/kit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/kit/-/kit-2.3.0.tgz", - "integrity": "sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==", - "license": "MIT", - "dependencies": { - "@solana/accounts": "2.3.0", - "@solana/addresses": "2.3.0", - "@solana/codecs": "2.3.0", - "@solana/errors": "2.3.0", - "@solana/functional": "2.3.0", - "@solana/instructions": "2.3.0", - "@solana/keys": "2.3.0", - "@solana/programs": "2.3.0", - "@solana/rpc": "2.3.0", - "@solana/rpc-parsed-types": "2.3.0", - "@solana/rpc-spec-types": "2.3.0", - "@solana/rpc-subscriptions": "2.3.0", - "@solana/rpc-types": "2.3.0", - "@solana/signers": "2.3.0", - "@solana/sysvars": "2.3.0", - "@solana/transaction-confirmation": "2.3.0", - "@solana/transaction-messages": "2.3.0", - "@solana/transactions": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/nominal-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/nominal-types/-/nominal-types-2.3.0.tgz", - "integrity": "sha512-uKlMnlP4PWW5UTXlhKM8lcgIaNj8dvd8xO4Y9l+FVvh9RvW2TO0GwUO6JCo7JBzCB0PSqRJdWWaQ8pu1Ti/OkA==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/options": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.3.0.tgz", - "integrity": "sha512-PPnnZBRCWWoZQ11exPxf//DRzN2C6AoFsDI/u2AsQfYih434/7Kp4XLpfOMT/XESi+gdBMFNNfbES5zg3wAIkw==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "2.3.0", - "@solana/codecs-data-structures": "2.3.0", - "@solana/codecs-numbers": "2.3.0", - "@solana/codecs-strings": "2.3.0", - "@solana/errors": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/programs": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/programs/-/programs-2.3.0.tgz", - "integrity": "sha512-UXKujV71VCI5uPs+cFdwxybtHZAIZyQkqDiDnmK+DawtOO9mBn4Nimdb/6RjR2CXT78mzO9ZCZ3qfyX+ydcB7w==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "2.3.0", - "@solana/errors": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/promises": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/promises/-/promises-2.3.0.tgz", - "integrity": "sha512-GjVgutZKXVuojd9rWy1PuLnfcRfqsaCm7InCiZc8bqmJpoghlyluweNc7ml9Y5yQn1P2IOyzh9+p/77vIyNybQ==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/rpc": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/rpc/-/rpc-2.3.0.tgz", - "integrity": "sha512-ZWN76iNQAOCpYC7yKfb3UNLIMZf603JckLKOOLTHuy9MZnTN8XV6uwvDFhf42XvhglgUjGCEnbUqWtxQ9pa/pQ==", - "license": "MIT", - "dependencies": { - "@solana/errors": "2.3.0", - "@solana/fast-stable-stringify": "2.3.0", - "@solana/functional": "2.3.0", - "@solana/rpc-api": "2.3.0", - "@solana/rpc-spec": "2.3.0", - "@solana/rpc-spec-types": "2.3.0", - "@solana/rpc-transformers": "2.3.0", - "@solana/rpc-transport-http": "2.3.0", - "@solana/rpc-types": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/rpc-api": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/rpc-api/-/rpc-api-2.3.0.tgz", - "integrity": "sha512-UUdiRfWoyYhJL9PPvFeJr4aJ554ob2jXcpn4vKmRVn9ire0sCbpQKYx6K8eEKHZWXKrDW8IDspgTl0gT/aJWVg==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "2.3.0", - "@solana/codecs-core": "2.3.0", - "@solana/codecs-strings": "2.3.0", - "@solana/errors": "2.3.0", - "@solana/keys": "2.3.0", - "@solana/rpc-parsed-types": "2.3.0", - "@solana/rpc-spec": "2.3.0", - "@solana/rpc-transformers": "2.3.0", - "@solana/rpc-types": "2.3.0", - "@solana/transaction-messages": "2.3.0", - "@solana/transactions": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/rpc-parsed-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/rpc-parsed-types/-/rpc-parsed-types-2.3.0.tgz", - "integrity": "sha512-B5pHzyEIbBJf9KHej+zdr5ZNAdSvu7WLU2lOUPh81KHdHQs6dEb310LGxcpCc7HVE8IEdO20AbckewDiAN6OCg==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/rpc-spec": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/rpc-spec/-/rpc-spec-2.3.0.tgz", - "integrity": "sha512-fA2LMX4BMixCrNB2n6T83AvjZ3oUQTu7qyPLyt8gHQaoEAXs8k6GZmu6iYcr+FboQCjUmRPgMaABbcr9j2J9Sw==", - "license": "MIT", - "dependencies": { - "@solana/errors": "2.3.0", - "@solana/rpc-spec-types": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/rpc-spec-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/rpc-spec-types/-/rpc-spec-types-2.3.0.tgz", - "integrity": "sha512-xQsb65lahjr8Wc9dMtP7xa0ZmDS8dOE2ncYjlvfyw/h4mpdXTUdrSMi6RtFwX33/rGuztQ7Hwaid5xLNSLvsFQ==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/rpc-subscriptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions/-/rpc-subscriptions-2.3.0.tgz", - "integrity": "sha512-Uyr10nZKGVzvCOqwCZgwYrzuoDyUdwtgQRefh13pXIrdo4wYjVmoLykH49Omt6abwStB0a4UL5gX9V4mFdDJZg==", - "license": "MIT", - "dependencies": { - "@solana/errors": "2.3.0", - "@solana/fast-stable-stringify": "2.3.0", - "@solana/functional": "2.3.0", - "@solana/promises": "2.3.0", - "@solana/rpc-spec-types": "2.3.0", - "@solana/rpc-subscriptions-api": "2.3.0", - "@solana/rpc-subscriptions-channel-websocket": "2.3.0", - "@solana/rpc-subscriptions-spec": "2.3.0", - "@solana/rpc-transformers": "2.3.0", - "@solana/rpc-types": "2.3.0", - "@solana/subscribable": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/rpc-subscriptions-api": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-api/-/rpc-subscriptions-api-2.3.0.tgz", - "integrity": "sha512-9mCjVbum2Hg9KGX3LKsrI5Xs0KX390lS+Z8qB80bxhar6MJPugqIPH8uRgLhCW9GN3JprAfjRNl7our8CPvsPQ==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "2.3.0", - "@solana/keys": "2.3.0", - "@solana/rpc-subscriptions-spec": "2.3.0", - "@solana/rpc-transformers": "2.3.0", - "@solana/rpc-types": "2.3.0", - "@solana/transaction-messages": "2.3.0", - "@solana/transactions": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/rpc-subscriptions-channel-websocket": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-channel-websocket/-/rpc-subscriptions-channel-websocket-2.3.0.tgz", - "integrity": "sha512-2oL6ceFwejIgeWzbNiUHI2tZZnaOxNTSerszcin7wYQwijxtpVgUHiuItM/Y70DQmH9sKhmikQp+dqeGalaJxw==", - "license": "MIT", - "dependencies": { - "@solana/errors": "2.3.0", - "@solana/functional": "2.3.0", - "@solana/rpc-subscriptions-spec": "2.3.0", - "@solana/subscribable": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3", - "ws": "^8.18.0" - } - }, - "node_modules/@solana/rpc-subscriptions-spec": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-spec/-/rpc-subscriptions-spec-2.3.0.tgz", - "integrity": "sha512-rdmVcl4PvNKQeA2l8DorIeALCgJEMSu7U8AXJS1PICeb2lQuMeaR+6cs/iowjvIB0lMVjYN2sFf6Q3dJPu6wWg==", - "license": "MIT", - "dependencies": { - "@solana/errors": "2.3.0", - "@solana/promises": "2.3.0", - "@solana/rpc-spec-types": "2.3.0", - "@solana/subscribable": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/rpc-transformers": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/rpc-transformers/-/rpc-transformers-2.3.0.tgz", - "integrity": "sha512-UuHYK3XEpo9nMXdjyGKkPCOr7WsZsxs7zLYDO1A5ELH3P3JoehvrDegYRAGzBS2VKsfApZ86ZpJToP0K3PhmMA==", - "license": "MIT", - "dependencies": { - "@solana/errors": "2.3.0", - "@solana/functional": "2.3.0", - "@solana/nominal-types": "2.3.0", - "@solana/rpc-spec-types": "2.3.0", - "@solana/rpc-types": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/rpc-transport-http": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/rpc-transport-http/-/rpc-transport-http-2.3.0.tgz", - "integrity": "sha512-HFKydmxGw8nAF5N+S0NLnPBDCe5oMDtI2RAmW8DMqP4U3Zxt2XWhvV1SNkAldT5tF0U1vP+is6fHxyhk4xqEvg==", - "license": "MIT", - "dependencies": { - "@solana/errors": "2.3.0", - "@solana/rpc-spec": "2.3.0", - "@solana/rpc-spec-types": "2.3.0", - "undici-types": "^7.11.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/rpc-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/rpc-types/-/rpc-types-2.3.0.tgz", - "integrity": "sha512-O09YX2hED2QUyGxrMOxQ9GzH1LlEwwZWu69QbL4oYmIf6P5dzEEHcqRY6L1LsDVqc/dzAdEs/E1FaPrcIaIIPw==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "2.3.0", - "@solana/codecs-core": "2.3.0", - "@solana/codecs-numbers": "2.3.0", - "@solana/codecs-strings": "2.3.0", - "@solana/errors": "2.3.0", - "@solana/nominal-types": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/signers": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/signers/-/signers-2.3.0.tgz", - "integrity": "sha512-OSv6fGr/MFRx6J+ZChQMRqKNPGGmdjkqarKkRzkwmv7v8quWsIRnJT5EV8tBy3LI4DLO/A8vKiNSPzvm1TdaiQ==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "2.3.0", - "@solana/codecs-core": "2.3.0", - "@solana/errors": "2.3.0", - "@solana/instructions": "2.3.0", - "@solana/keys": "2.3.0", - "@solana/nominal-types": "2.3.0", - "@solana/transaction-messages": "2.3.0", - "@solana/transactions": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/subscribable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/subscribable/-/subscribable-2.3.0.tgz", - "integrity": "sha512-DkgohEDbMkdTWiKAoatY02Njr56WXx9e/dKKfmne8/Ad6/2llUIrax78nCdlvZW9quXMaXPTxZvdQqo9N669Og==", - "license": "MIT", - "dependencies": { - "@solana/errors": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/sysvars": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/sysvars/-/sysvars-2.3.0.tgz", - "integrity": "sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==", - "license": "MIT", - "dependencies": { - "@solana/accounts": "2.3.0", - "@solana/codecs": "2.3.0", - "@solana/errors": "2.3.0", - "@solana/rpc-types": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/transaction-confirmation": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/transaction-confirmation/-/transaction-confirmation-2.3.0.tgz", - "integrity": "sha512-UiEuiHCfAAZEKdfne/XljFNJbsKAe701UQHKXEInYzIgBjRbvaeYZlBmkkqtxwcasgBTOmEaEKT44J14N9VZDw==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "2.3.0", - "@solana/codecs-strings": "2.3.0", - "@solana/errors": "2.3.0", - "@solana/keys": "2.3.0", - "@solana/promises": "2.3.0", - "@solana/rpc": "2.3.0", - "@solana/rpc-subscriptions": "2.3.0", - "@solana/rpc-types": "2.3.0", - "@solana/transaction-messages": "2.3.0", - "@solana/transactions": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/transaction-messages": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/transaction-messages/-/transaction-messages-2.3.0.tgz", - "integrity": "sha512-bgqvWuy3MqKS5JdNLH649q+ngiyOu5rGS3DizSnWwYUd76RxZl1kN6CoqHSrrMzFMvis6sck/yPGG3wqrMlAww==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "2.3.0", - "@solana/codecs-core": "2.3.0", - "@solana/codecs-data-structures": "2.3.0", - "@solana/codecs-numbers": "2.3.0", - "@solana/errors": "2.3.0", - "@solana/functional": "2.3.0", - "@solana/instructions": "2.3.0", - "@solana/nominal-types": "2.3.0", - "@solana/rpc-types": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/transactions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@solana/transactions/-/transactions-2.3.0.tgz", - "integrity": "sha512-LnTvdi8QnrQtuEZor5Msje61sDpPstTVwKg4y81tNxDhiyomjuvnSNLAq6QsB9gIxUqbNzPZgOG9IU4I4/Uaug==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "2.3.0", - "@solana/codecs-core": "2.3.0", - "@solana/codecs-data-structures": "2.3.0", - "@solana/codecs-numbers": "2.3.0", - "@solana/codecs-strings": "2.3.0", - "@solana/errors": "2.3.0", - "@solana/functional": "2.3.0", - "@solana/instructions": "2.3.0", - "@solana/keys": "2.3.0", - "@solana/nominal-types": "2.3.0", - "@solana/rpc-types": "2.3.0", - "@solana/transaction-messages": "2.3.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" - } - }, - "node_modules/@solana/wallet-adapter-base": { - "version": "0.9.27", - "resolved": "https://registry.npmjs.org/@solana/wallet-adapter-base/-/wallet-adapter-base-0.9.27.tgz", - "integrity": "sha512-kXjeNfNFVs/NE9GPmysBRKQ/nf+foSaq3kfVSeMcO/iVgigyRmB551OjU3WyAolLG/1jeEfKLqF9fKwMCRkUqg==", - "license": "Apache-2.0", - "dependencies": { - "@solana/wallet-standard-features": "^1.3.0", - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/features": "^1.1.0", - "eventemitter3": "^5.0.1" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@solana/web3.js": "^1.98.0" - } - }, - "node_modules/@solana/wallet-standard-features": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@solana/wallet-standard-features/-/wallet-standard-features-1.3.0.tgz", - "integrity": "sha512-ZhpZtD+4VArf6RPitsVExvgkF+nGghd1rzPjd97GmBximpnt1rsUxMOEyoIEuH3XBxPyNB6Us7ha7RHWQR+abg==", - "license": "Apache-2.0", - "dependencies": { - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/features": "^1.1.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@solana/web3.js": { - "version": "1.98.4", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", - "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.0", - "@noble/curves": "^1.4.2", - "@noble/hashes": "^1.4.0", - "@solana/buffer-layout": "^4.0.1", - "@solana/codecs-numbers": "^2.1.0", - "agentkeepalive": "^4.5.0", - "bn.js": "^5.2.1", - "borsh": "^0.7.0", - "bs58": "^4.0.1", - "buffer": "6.0.3", - "fast-stable-stringify": "^1.0.0", - "jayson": "^4.1.1", - "node-fetch": "^2.7.0", - "rpc-websockets": "^9.0.2", - "superstruct": "^2.0.2" - } - }, - "node_modules/@solana/web3.js/node_modules/borsh": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", - "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", - "license": "Apache-2.0", - "dependencies": { - "bn.js": "^5.2.0", - "bs58": "^4.0.0", - "text-encoding-utf-8": "^1.0.2" - } - }, - "node_modules/@stablelib/aead": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/aead/-/aead-1.0.1.tgz", - "integrity": "sha512-q39ik6sxGHewqtO0nP4BuSe3db5G1fEJE8ukvngS2gLkBXyy6E7pLubhbYgnkDFv6V8cWaxcE4Xn0t6LWcJkyg==", - "license": "MIT" - }, - "node_modules/@stablelib/binary": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz", - "integrity": "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==", - "license": "MIT", - "dependencies": { - "@stablelib/int": "^1.0.1" - } - }, - "node_modules/@stablelib/bytes": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/bytes/-/bytes-1.0.1.tgz", - "integrity": "sha512-Kre4Y4kdwuqL8BR2E9hV/R5sOrUj6NanZaZis0V6lX5yzqC3hBuVSDXUIBqQv/sCpmuWRiHLwqiT1pqqjuBXoQ==", - "license": "MIT" - }, - "node_modules/@stablelib/chacha": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/chacha/-/chacha-1.0.1.tgz", - "integrity": "sha512-Pmlrswzr0pBzDofdFuVe1q7KdsHKhhU24e8gkEwnTGOmlC7PADzLVxGdn2PoNVBBabdg0l/IfLKg6sHAbTQugg==", - "license": "MIT", - "dependencies": { - "@stablelib/binary": "^1.0.1", - "@stablelib/wipe": "^1.0.1" - } - }, - "node_modules/@stablelib/chacha20poly1305": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/chacha20poly1305/-/chacha20poly1305-1.0.1.tgz", - "integrity": "sha512-MmViqnqHd1ymwjOQfghRKw2R/jMIGT3wySN7cthjXCBdO+qErNPUBnRzqNpnvIwg7JBCg3LdeCZZO4de/yEhVA==", - "license": "MIT", - "dependencies": { - "@stablelib/aead": "^1.0.1", - "@stablelib/binary": "^1.0.1", - "@stablelib/chacha": "^1.0.1", - "@stablelib/constant-time": "^1.0.1", - "@stablelib/poly1305": "^1.0.1", - "@stablelib/wipe": "^1.0.1" - } - }, - "node_modules/@stablelib/constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/constant-time/-/constant-time-1.0.1.tgz", - "integrity": "sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg==", - "license": "MIT" - }, - "node_modules/@stablelib/hash": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/hash/-/hash-1.0.1.tgz", - "integrity": "sha512-eTPJc/stDkdtOcrNMZ6mcMK1e6yBbqRBaNW55XA1jU8w/7QdnCF0CmMmOD1m7VSkBR44PWrMHU2l6r8YEQHMgg==", - "license": "MIT" - }, - "node_modules/@stablelib/hkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/hkdf/-/hkdf-1.0.1.tgz", - "integrity": "sha512-SBEHYE16ZXlHuaW5RcGk533YlBj4grMeg5TooN80W3NpcHRtLZLLXvKyX0qcRFxf+BGDobJLnwkvgEwHIDBR6g==", - "license": "MIT", - "dependencies": { - "@stablelib/hash": "^1.0.1", - "@stablelib/hmac": "^1.0.1", - "@stablelib/wipe": "^1.0.1" - } - }, - "node_modules/@stablelib/hmac": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/hmac/-/hmac-1.0.1.tgz", - "integrity": "sha512-V2APD9NSnhVpV/QMYgCVMIYKiYG6LSqw1S65wxVoirhU/51ACio6D4yDVSwMzuTJXWZoVHbDdINioBwKy5kVmA==", - "license": "MIT", - "dependencies": { - "@stablelib/constant-time": "^1.0.1", - "@stablelib/hash": "^1.0.1", - "@stablelib/wipe": "^1.0.1" - } - }, - "node_modules/@stablelib/int": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz", - "integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==", - "license": "MIT" - }, - "node_modules/@stablelib/keyagreement": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/keyagreement/-/keyagreement-1.0.1.tgz", - "integrity": "sha512-VKL6xBwgJnI6l1jKrBAfn265cspaWBPAPEc62VBQrWHLqVgNRE09gQ/AnOEyKUWrrqfD+xSQ3u42gJjLDdMDQg==", - "license": "MIT", - "dependencies": { - "@stablelib/bytes": "^1.0.1" - } - }, - "node_modules/@stablelib/poly1305": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/poly1305/-/poly1305-1.0.1.tgz", - "integrity": "sha512-1HlG3oTSuQDOhSnLwJRKeTRSAdFNVB/1djy2ZbS35rBSJ/PFqx9cf9qatinWghC2UbfOYD8AcrtbUQl8WoxabA==", - "license": "MIT", - "dependencies": { - "@stablelib/constant-time": "^1.0.1", - "@stablelib/wipe": "^1.0.1" - } - }, - "node_modules/@stablelib/random": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@stablelib/random/-/random-1.0.2.tgz", - "integrity": "sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==", - "license": "MIT", - "dependencies": { - "@stablelib/binary": "^1.0.1", - "@stablelib/wipe": "^1.0.1" - } - }, - "node_modules/@stablelib/sha256": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/sha256/-/sha256-1.0.1.tgz", - "integrity": "sha512-GIIH3e6KH+91FqGV42Kcj71Uefd/QEe7Dy42sBTeqppXV95ggCcxLTk39bEr+lZfJmp+ghsR07J++ORkRELsBQ==", - "license": "MIT", - "dependencies": { - "@stablelib/binary": "^1.0.1", - "@stablelib/hash": "^1.0.1", - "@stablelib/wipe": "^1.0.1" - } - }, - "node_modules/@stablelib/wipe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz", - "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==", - "license": "MIT" - }, - "node_modules/@stablelib/x25519": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@stablelib/x25519/-/x25519-1.0.3.tgz", - "integrity": "sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw==", - "license": "MIT", - "dependencies": { - "@stablelib/keyagreement": "^1.0.1", - "@stablelib/random": "^1.0.2", - "@stablelib/wipe": "^1.0.1" - } - }, - "node_modules/@stellar/design-system": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@stellar/design-system/-/design-system-3.2.7.tgz", - "integrity": "sha512-ad0MCpeIy8cwHq5ptGlciBaVpKczYZibMe8g12pzslu3XeiaHLMpjZ5iTOLaxSw1q1INa9vIlHg3uIPRQdo1rQ==", - "license": "Apache-2.0", - "dependencies": { - "@floating-ui/dom": "^1.7.4", - "bignumber.js": "^9.3.1", - "lodash": "^4.17.21", - "react-copy-to-clipboard-ts": "^1.3.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "react": ">=18.x", - "react-dom": ">=18.x" - } - }, - "node_modules/@stellar/freighter-api": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@stellar/freighter-api/-/freighter-api-5.0.0.tgz", - "integrity": "sha512-MydzLg+WpSzmws24uUs4mVME2LPN8xhUWkwyGEP0N1Hr519swC6I/W7K6cdVBzghBiVv7f/vvGFNT+0p1a33Vg==", - "license": "Apache-2.0", - "dependencies": { - "buffer": "6.0.3", - "semver": "7.7.1" - } - }, - "node_modules/@stellar/freighter-api/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@stellar/js-xdr": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", - "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", - "license": "Apache-2.0" - }, - "node_modules/@stellar/stellar-base": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-14.0.4.tgz", - "integrity": "sha512-UbNW6zbdOBXJwLAV2mMak0bIC9nw3IZVlQXkv2w2dk1jgCbJjy3oRVC943zeGE5JAm0Z9PHxrIjmkpGhayY7kw==", - "license": "Apache-2.0", - "dependencies": { - "@noble/curves": "^1.9.6", - "@stellar/js-xdr": "^3.1.2", - "base32.js": "^0.1.0", - "bignumber.js": "^9.3.1", - "buffer": "^6.0.3", - "sha.js": "^2.4.12" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@stellar/stellar-base/node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@stellar/stellar-base/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@stellar/stellar-sdk": { - "version": "14.5.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-14.5.0.tgz", - "integrity": "sha512-Uzjq+An/hUA+Q5ERAYPtT0+MMiwWnYYWMwozmZMjxjdL2MmSjucBDF8Q04db6K/ekU4B5cHuOfsdlrfaxQYblw==", - "license": "Apache-2.0", - "dependencies": { - "@stellar/stellar-base": "^14.0.4", - "axios": "^1.13.3", - "bignumber.js": "^9.3.1", - "commander": "^14.0.2", - "eventsource": "^2.0.2", - "feaxios": "^0.0.23", - "randombytes": "^2.1.0", - "toml": "^3.0.0", - "urijs": "^1.19.1" - }, - "bin": { - "stellar-js": "bin/stellar-js" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@stellar/stellar-xdr-json": { - "version": "23.0.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-xdr-json/-/stellar-xdr-json-23.0.0.tgz", - "integrity": "sha512-0jxWuaSZY5VfogL9YMPhjxBM7a3jwTQfSCzFUy0lkSoPrl5cNxhzx854+iidNY2Xu3pFB8x61XZ1wJapu7OHMA==" - }, - "node_modules/@swc/helpers": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", - "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.90.20", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", - "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.90.20", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz", - "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.90.20" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@theahaco/contract-explorer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@theahaco/contract-explorer/-/contract-explorer-1.2.0.tgz", - "integrity": "sha512-RL62v5H2OtH2XxtTuglzTp5k9+om/OvD/b3UUkv4vAb43+NDJmJ14ECy8CWopDYne8mx7uPRFDG2kjS9nN/XMg==", - "license": "Apache-2.0", - "dependencies": { - "@stellar/design-system": "^3.2.2", - "@stellar/stellar-sdk": "^14.2.0", - "@stellar/stellar-xdr-json": "^23.0.0", - "@tanstack/react-query": "^5.90.5", - "@theahaco/ts-config": "^1.2.0", - "json-schema": "^0.4.0", - "lossless-json": "^4.3.0" - }, - "peerDependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0" - } - }, - "node_modules/@theahaco/ts-config": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@theahaco/ts-config/-/ts-config-1.2.0.tgz", - "integrity": "sha512-792O8Pox7m2I8p0xCsUQjaAK7XBkzkpongJcSh/vkZDnBH/lau+z3HE9TTp9IqApxEqmy/H9Wm4OIBvoDj7K1w==", - "license": "MIT", - "dependencies": { - "@total-typescript/ts-reset": "^0.6.1", - "@vitest/eslint-plugin": "^1.3.4", - "eslint-plugin-import-x": "^4.16.1", - "eslint-plugin-jest-dom": "^5.5.0", - "eslint-plugin-playwright": "^2.2.0", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.0.0", - "eslint-plugin-testing-library": "^7.6.1", - "globals": "^17.0.0", - "tslib": "^2.8.1", - "typescript-eslint": "^8.38.0" - } - }, - "node_modules/@theahaco/ts-config/node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/@total-typescript/ts-reset": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.6.1.tgz", - "integrity": "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==", - "license": "MIT" - }, - "node_modules/@trezor/analytics": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@trezor/analytics/-/analytics-1.4.3.tgz", - "integrity": "sha512-0o7gp7nfip8yjhAwP3R/Hcy5S8RfmZmYwpCcN0PbydWa5U5VMQ+T/iB/OpbpeV+8j13bf6i7++38nTCUNas0GA==", - "license": "See LICENSE.md in repo root", - "peer": true, - "dependencies": { - "@trezor/env-utils": "1.4.3", - "@trezor/utils": "9.4.3" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/analytics/node_modules/@trezor/utils": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/@trezor/utils/-/utils-9.4.3.tgz", - "integrity": "sha512-QkLHpGTF3W3wNGj6OCdcMog7MhAAdlUmpjjmMjMqE0JSoi1Yjr0m7k7xN0iIHSqcgrhYteZDeJZAGlAf/b7gqw==", - "license": "SEE LICENSE IN LICENSE.md", - "peer": true, - "dependencies": { - "bignumber.js": "^9.3.1" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/blockchain-link": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@trezor/blockchain-link/-/blockchain-link-2.5.4.tgz", - "integrity": "sha512-3Xki/2Vmr1/rKa5LF+Eb2/Qd5N9LqwyRL76+ycqe1KwqV7xK3XGMsqTH9FUUReRvGxrzAFonbOgADAJczhx81w==", - "license": "SEE LICENSE IN LICENSE.md", - "peer": true, - "dependencies": { - "@solana-program/compute-budget": "^0.8.0", - "@solana-program/stake": "^0.2.1", - "@solana-program/token": "^0.5.1", - "@solana-program/token-2022": "^0.4.2", - "@solana/kit": "^2.3.0", - "@solana/rpc-types": "^2.3.0", - "@stellar/stellar-sdk": "^13.3.0", - "@trezor/blockchain-link-types": "1.4.4", - "@trezor/blockchain-link-utils": "1.4.4", - "@trezor/env-utils": "1.4.3", - "@trezor/utils": "9.4.4", - "@trezor/utxo-lib": "2.4.4", - "@trezor/websocket-client": "1.2.4", - "@types/web": "^0.0.197", - "crypto-browserify": "3.12.0", - "socks-proxy-agent": "8.0.5", - "stream-browserify": "^3.0.0", - "xrpl": "^4.4.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/blockchain-link-types": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-types/-/blockchain-link-types-1.4.4.tgz", - "integrity": "sha512-B38LH4aniZ7gKbpSdMRiA4YriauoYPHgjhKEd+ybR0ca+liNlPAvMwSHJyMhL1eDIYEs16oOjTgT53WjRRZbMg==", - "license": "See LICENSE.md in repo root", - "peer": true, - "dependencies": { - "@trezor/utils": "9.4.4", - "@trezor/utxo-lib": "2.4.4" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/blockchain-link-utils": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-utils/-/blockchain-link-utils-1.4.4.tgz", - "integrity": "sha512-8BZD6h5gs3ETPOG2Ri+GOyH44D38YQVeQj8n7oVOVQi21zx93JOpdL3fWS9RytkfmvB84WwVzYoHGlTs3T80Gw==", - "license": "See LICENSE.md in repo root", - "peer": true, - "dependencies": { - "@mobily/ts-belt": "^3.13.1", - "@stellar/stellar-sdk": "^13.3.0", - "@trezor/env-utils": "1.4.3", - "@trezor/protobuf": "1.4.4", - "@trezor/utils": "9.4.4", - "xrpl": "^4.4.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/blockchain-link-utils/node_modules/@stellar/stellar-base": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-13.1.0.tgz", - "integrity": "sha512-90EArG+eCCEzDGj3OJNoCtwpWDwxjv+rs/RNPhvg4bulpjN/CSRj+Ys/SalRcfM4/WRC5/qAfjzmJBAuquWhkA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@stellar/js-xdr": "^3.1.2", - "base32.js": "^0.1.0", - "bignumber.js": "^9.1.2", - "buffer": "^6.0.3", - "sha.js": "^2.3.6", - "tweetnacl": "^1.0.3" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "sodium-native": "^4.3.3" - } - }, - "node_modules/@trezor/blockchain-link-utils/node_modules/@stellar/stellar-sdk": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-13.3.0.tgz", - "integrity": "sha512-8+GHcZLp+mdin8gSjcgfb/Lb6sSMYRX6Nf/0LcSJxvjLQR0XHpjGzOiRbYb2jSXo51EnA6kAV5j+4Pzh5OUKUg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@stellar/stellar-base": "^13.1.0", - "axios": "^1.8.4", - "bignumber.js": "^9.3.0", - "eventsource": "^2.0.2", - "feaxios": "^0.0.23", - "randombytes": "^2.1.0", - "toml": "^3.0.0", - "urijs": "^1.19.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@trezor/blockchain-link/node_modules/@stellar/stellar-base": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-13.1.0.tgz", - "integrity": "sha512-90EArG+eCCEzDGj3OJNoCtwpWDwxjv+rs/RNPhvg4bulpjN/CSRj+Ys/SalRcfM4/WRC5/qAfjzmJBAuquWhkA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@stellar/js-xdr": "^3.1.2", - "base32.js": "^0.1.0", - "bignumber.js": "^9.1.2", - "buffer": "^6.0.3", - "sha.js": "^2.3.6", - "tweetnacl": "^1.0.3" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "sodium-native": "^4.3.3" - } - }, - "node_modules/@trezor/blockchain-link/node_modules/@stellar/stellar-sdk": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-13.3.0.tgz", - "integrity": "sha512-8+GHcZLp+mdin8gSjcgfb/Lb6sSMYRX6Nf/0LcSJxvjLQR0XHpjGzOiRbYb2jSXo51EnA6kAV5j+4Pzh5OUKUg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@stellar/stellar-base": "^13.1.0", - "axios": "^1.8.4", - "bignumber.js": "^9.3.0", - "eventsource": "^2.0.2", - "feaxios": "^0.0.23", - "randombytes": "^2.1.0", - "toml": "^3.0.0", - "urijs": "^1.19.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@trezor/connect": { - "version": "9.6.4", - "resolved": "https://registry.npmjs.org/@trezor/connect/-/connect-9.6.4.tgz", - "integrity": "sha512-/N3hhOFIIhufvihCx92wvxd15Wy9XAJOSbTiV8rYG2N9uBvzejctNO2+LpwCRl/cBKle9rsp4S7C/zz++iDuOg==", - "license": "SEE LICENSE IN LICENSE.md", - "peer": true, - "dependencies": { - "@ethereumjs/common": "^10.0.0", - "@ethereumjs/tx": "^10.0.0", - "@fivebinaries/coin-selection": "3.0.0", - "@mobily/ts-belt": "^3.13.1", - "@noble/hashes": "^1.6.1", - "@scure/bip39": "^1.5.1", - "@solana-program/compute-budget": "^0.8.0", - "@solana-program/system": "^0.7.0", - "@solana-program/token": "^0.5.1", - "@solana-program/token-2022": "^0.4.2", - "@solana/kit": "^2.3.0", - "@trezor/blockchain-link": "2.5.4", - "@trezor/blockchain-link-types": "1.4.4", - "@trezor/blockchain-link-utils": "1.4.4", - "@trezor/connect-analytics": "1.3.6", - "@trezor/connect-common": "0.4.4", - "@trezor/crypto-utils": "1.1.5", - "@trezor/device-utils": "1.1.4", - "@trezor/env-utils": "^1.4.3", - "@trezor/protobuf": "1.4.4", - "@trezor/protocol": "1.2.10", - "@trezor/schema-utils": "1.3.4", - "@trezor/transport": "1.5.4", - "@trezor/type-utils": "1.1.9", - "@trezor/utils": "9.4.4", - "@trezor/utxo-lib": "2.4.4", - "blakejs": "^1.2.1", - "bs58": "^6.0.0", - "bs58check": "^4.0.0", - "cbor": "^10.0.10", - "cross-fetch": "^4.0.0", - "jws": "^4.0.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-analytics": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@trezor/connect-analytics/-/connect-analytics-1.3.6.tgz", - "integrity": "sha512-Skya46inItcjaahaqpeSsQmB2Xle70f/l+6eTTJYxKQdpMtuW5LRsRRiyMAQTp5RBycL2ngnsVtY+/83Bt5lUw==", - "license": "See LICENSE.md in repo root", - "peer": true, - "dependencies": { - "@trezor/analytics": "1.4.3" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-common": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@trezor/connect-common/-/connect-common-0.4.4.tgz", - "integrity": "sha512-xG2CoPjgcldtO6HU0ZjNCvFdQ4hpl56qzU1VEF1/A1BL2zj2TwLGLmyr4E878go1mmfksNGY5a1tqnzAZ7pRLw==", - "license": "SEE LICENSE IN LICENSE.md", - "peer": true, - "dependencies": { - "@trezor/env-utils": "1.4.3", - "@trezor/type-utils": "1.1.9", - "@trezor/utils": "9.4.4" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web": { - "version": "9.6.2", - "resolved": "https://registry.npmjs.org/@trezor/connect-web/-/connect-web-9.6.2.tgz", - "integrity": "sha512-QGuCjX8Bx9aCq1Pg52KifbbzYn00FQu9mCTDSgCVGH/HAzbxhcRkDKc86kFwW8z9NdJxw+XeVJq5Ky/js3iEDA==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@trezor/connect": "9.6.2", - "@trezor/connect-common": "0.4.2", - "@trezor/utils": "9.4.2", - "@trezor/websocket-client": "1.2.2" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@stellar/stellar-base": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-13.1.0.tgz", - "integrity": "sha512-90EArG+eCCEzDGj3OJNoCtwpWDwxjv+rs/RNPhvg4bulpjN/CSRj+Ys/SalRcfM4/WRC5/qAfjzmJBAuquWhkA==", - "license": "Apache-2.0", - "dependencies": { - "@stellar/js-xdr": "^3.1.2", - "base32.js": "^0.1.0", - "bignumber.js": "^9.1.2", - "buffer": "^6.0.3", - "sha.js": "^2.3.6", - "tweetnacl": "^1.0.3" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "sodium-native": "^4.3.3" - } - }, - "node_modules/@trezor/connect-web/node_modules/@stellar/stellar-sdk": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-13.3.0.tgz", - "integrity": "sha512-8+GHcZLp+mdin8gSjcgfb/Lb6sSMYRX6Nf/0LcSJxvjLQR0XHpjGzOiRbYb2jSXo51EnA6kAV5j+4Pzh5OUKUg==", - "license": "Apache-2.0", - "dependencies": { - "@stellar/stellar-base": "^13.1.0", - "axios": "^1.8.4", - "bignumber.js": "^9.3.0", - "eventsource": "^2.0.2", - "feaxios": "^0.0.23", - "randombytes": "^2.1.0", - "toml": "^3.0.0", - "urijs": "^1.19.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/analytics": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@trezor/analytics/-/analytics-1.4.2.tgz", - "integrity": "sha512-FgjJekuDvx1TjiDemvpnPiRck7Kp/v1ZeppsBYpQR3yGKyKzbG1pVpcl0RyI2237raXxbORaz7XV8tcyjq4BXg==", - "license": "See LICENSE.md in repo root", - "dependencies": { - "@trezor/env-utils": "1.4.2", - "@trezor/utils": "9.4.2" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/blockchain-link": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@trezor/blockchain-link/-/blockchain-link-2.5.2.tgz", - "integrity": "sha512-/egUnIt/fR57QY33ejnkPMhZwRvVRS/pUCoqdVIGitN1Q7QZsdopoR4hw37hdK/Ux/q1ZLH6LZz7U2UFahjppw==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@solana-program/stake": "^0.2.1", - "@solana-program/token": "^0.5.1", - "@solana-program/token-2022": "^0.4.2", - "@solana/kit": "^2.1.1", - "@solana/rpc-types": "^2.1.1", - "@stellar/stellar-sdk": "^13.3.0", - "@trezor/blockchain-link-types": "1.4.2", - "@trezor/blockchain-link-utils": "1.4.2", - "@trezor/env-utils": "1.4.2", - "@trezor/utils": "9.4.2", - "@trezor/utxo-lib": "2.4.2", - "@trezor/websocket-client": "1.2.2", - "@types/web": "^0.0.197", - "events": "^3.3.0", - "socks-proxy-agent": "8.0.5", - "xrpl": "^4.3.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/blockchain-link-types": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-types/-/blockchain-link-types-1.4.2.tgz", - "integrity": "sha512-KThBmGOFLJAFnmou9ThQhnjEVxfYPfEwMOaVTVNgJ+NAkt5rEMx0SKBBelCGZ63XtOLWdVPglFo83wtm+I9Vpg==", - "license": "See LICENSE.md in repo root", - "dependencies": { - "@trezor/utxo-lib": "2.4.2" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/blockchain-link-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-utils/-/blockchain-link-utils-1.4.2.tgz", - "integrity": "sha512-PBEBrdtHn0dn/c9roW6vjdHI/CucMywJm5gthETZAZmzBOtg6ZDpLTn+qL8+jZGIbwcAkItrQ3iHrHhR6xTP5g==", - "license": "See LICENSE.md in repo root", - "dependencies": { - "@mobily/ts-belt": "^3.13.1", - "@stellar/stellar-sdk": "^13.3.0", - "@trezor/env-utils": "1.4.2", - "@trezor/utils": "9.4.2", - "xrpl": "^4.3.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/connect": { - "version": "9.6.2", - "resolved": "https://registry.npmjs.org/@trezor/connect/-/connect-9.6.2.tgz", - "integrity": "sha512-XsSERBK+KnF6FPsATuhB9AEM0frekVLwAwFo35MRV9I4P+mdv6tnUiZUq8O8aoPbfJwDjtNJSYv+PMsKuRH6rg==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@ethereumjs/common": "^10.0.0", - "@ethereumjs/tx": "^10.0.0", - "@fivebinaries/coin-selection": "3.0.0", - "@mobily/ts-belt": "^3.13.1", - "@noble/hashes": "^1.6.1", - "@scure/bip39": "^1.5.1", - "@solana-program/compute-budget": "^0.8.0", - "@solana-program/system": "^0.7.0", - "@solana-program/token": "^0.5.1", - "@solana-program/token-2022": "^0.4.2", - "@solana/kit": "^2.1.1", - "@trezor/blockchain-link": "2.5.2", - "@trezor/blockchain-link-types": "1.4.2", - "@trezor/blockchain-link-utils": "1.4.2", - "@trezor/connect-analytics": "1.3.5", - "@trezor/connect-common": "0.4.2", - "@trezor/crypto-utils": "1.1.4", - "@trezor/device-utils": "1.1.2", - "@trezor/env-utils": "^1.4.2", - "@trezor/protobuf": "1.4.2", - "@trezor/protocol": "1.2.8", - "@trezor/schema-utils": "1.3.4", - "@trezor/transport": "1.5.2", - "@trezor/type-utils": "1.1.8", - "@trezor/utils": "9.4.2", - "@trezor/utxo-lib": "2.4.2", - "blakejs": "^1.2.1", - "bs58": "^6.0.0", - "bs58check": "^4.0.0", - "cross-fetch": "^4.0.0", - "jws": "^4.0.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/connect-analytics": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@trezor/connect-analytics/-/connect-analytics-1.3.5.tgz", - "integrity": "sha512-Aoi+EITpZZycnELQJEp9XV0mHFfaCQ6JE0Ka5mWuHtOny3nJdFLBrih4ipcEXJdJbww6pBxRJB09sJ19cTyacA==", - "license": "See LICENSE.md in repo root", - "dependencies": { - "@trezor/analytics": "1.4.2" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/connect-common": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@trezor/connect-common/-/connect-common-0.4.2.tgz", - "integrity": "sha512-ND5TTjrTPnJdfl8Wlhl9YtFWnY2u6FHM1dsPkNYCmyUKIMoflJ5cLn95Xabl6l1btHERYn3wTUvgEYQG7r8OVQ==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@trezor/env-utils": "1.4.2", - "@trezor/utils": "9.4.2" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/crypto-utils": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@trezor/crypto-utils/-/crypto-utils-1.1.4.tgz", - "integrity": "sha512-Y6VziniqMPoMi70IyowEuXKqRvBYQzgPAekJaUZTHhR+grtYNRKRH2HJCvuZ8MGmSKUFSYfa7y8AvwALA8mQmA==", - "license": "SEE LICENSE IN LICENSE.md", - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/device-utils": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@trezor/device-utils/-/device-utils-1.1.2.tgz", - "integrity": "sha512-R3AJvAo+a3wYVmcGZO2VNl9PZOmDEzCZIlmCJn0BlSRWWd8G9u1qyo/fL9zOwij/YhCaJyokmSHmIEmbY9qpgw==", - "license": "See LICENSE.md in repo root" - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/env-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@trezor/env-utils/-/env-utils-1.4.2.tgz", - "integrity": "sha512-lQvrqcNK5I4dy2MuiLyMuEm0KzY59RIu2GLtc9GsvqyxSPZkADqVzGeLJjXj/vI2ajL8leSpMvmN4zPw3EK8AA==", - "license": "See LICENSE.md in repo root", - "dependencies": { - "ua-parser-js": "^2.0.4" - }, - "peerDependencies": { - "expo-constants": "*", - "expo-localization": "*", - "react-native": "*", - "tslib": "^2.6.2" - }, - "peerDependenciesMeta": { - "expo-constants": { - "optional": true - }, - "expo-localization": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/protobuf": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-1.4.2.tgz", - "integrity": "sha512-AeIYKCgKcE9cWflggGL8T9gD+IZLSGrwkzqCk3wpIiODd5dUCgEgA4OPBufR6OMu3RWu/Tgu2xviHunijG3LXQ==", - "license": "See LICENSE.md in repo root", - "dependencies": { - "@trezor/schema-utils": "1.3.4", - "long": "5.2.5", - "protobufjs": "7.4.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/protocol": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@trezor/protocol/-/protocol-1.2.8.tgz", - "integrity": "sha512-8EH+EU4Z1j9X4Ljczjbl9G7vVgcUz41qXcdE+6FOG3BFvMDK4KUVvaOtWqD+1dFpeo5yvWSTEKdhgXMPFprWYQ==", - "license": "See LICENSE.md in repo root", - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/transport": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@trezor/transport/-/transport-1.5.2.tgz", - "integrity": "sha512-rYP87zdVll2bNBtsD3VxJq0yjaNvIClcgszZjQwVTQxpKGFPkx8bLSpAGI05R9qfxusZJCfYarjX3qki9nHYPw==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@trezor/protobuf": "1.4.2", - "@trezor/protocol": "1.2.8", - "@trezor/type-utils": "1.1.8", - "@trezor/utils": "9.4.2", - "cross-fetch": "^4.0.0", - "usb": "^2.15.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/type-utils": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@trezor/type-utils/-/type-utils-1.1.8.tgz", - "integrity": "sha512-VtvkPXpwtMtTX9caZWYlMMTmhjUeDq4/1LGn0pSdjd4OuL/vQyuPWXCT/0RtlnRraW6R2dZF7rX2UON2kQIMTQ==", - "license": "See LICENSE.md in repo root" - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/utils": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/@trezor/utils/-/utils-9.4.2.tgz", - "integrity": "sha512-Fm3m2gmfXsgv4chqn5HX8e8dElEr2ibBJSJ7HE3bsHh/1OSQcDdzsSioAK04Fo9ws/v7n6lt+QBZ6fGmwyIkZQ==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "bignumber.js": "^9.3.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/utxo-lib": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@trezor/utxo-lib/-/utxo-lib-2.4.2.tgz", - "integrity": "sha512-dTXfBg/cEKnmHM5CLG5+0qrp6fqOfwxqe8YPACdKeM7g1XJKCGDAuFpDUVeT3lrcUsTh6bEMHM06z4H3gZp5MQ==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@trezor/utils": "9.4.2", - "bchaddrjs": "^0.5.2", - "bech32": "^2.0.0", - "bip66": "^2.0.0", - "bitcoin-ops": "^1.4.1", - "blake-hash": "^2.0.0", - "blakejs": "^1.2.1", - "bn.js": "^5.2.2", - "bs58": "^6.0.0", - "bs58check": "^4.0.0", - "create-hmac": "^1.1.7", - "int64-buffer": "^1.1.0", - "pushdata-bitcoin": "^1.0.1", - "tiny-secp256k1": "^1.1.7", - "typeforce": "^1.18.0", - "varuint-bitcoin": "2.0.0", - "wif": "^5.0.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/@trezor/websocket-client": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@trezor/websocket-client/-/websocket-client-1.2.2.tgz", - "integrity": "sha512-vu9L1V/5yh8LHQCmsGC9scCnihELsVuR5Tri1IvW3CdgTUFFcfjsEgXsFqFME3HlxuUmx6qokw0Gx/o0/hzaSQ==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@trezor/utils": "9.4.2", - "ws": "^8.18.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-web/node_modules/base-x": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", - "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", - "license": "MIT" - }, - "node_modules/@trezor/connect-web/node_modules/bs58": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", - "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", - "license": "MIT", - "dependencies": { - "base-x": "^5.0.0" - } - }, - "node_modules/@trezor/connect/node_modules/base-x": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", - "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", - "license": "MIT", - "peer": true - }, - "node_modules/@trezor/connect/node_modules/bs58": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", - "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", - "license": "MIT", - "peer": true, - "dependencies": { - "base-x": "^5.0.0" - } - }, - "node_modules/@trezor/crypto-utils": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@trezor/crypto-utils/-/crypto-utils-1.1.5.tgz", - "integrity": "sha512-Bp3L9MvzYy1OhPcNJIPIPu7kAH1lQyI1ZMuGnIo53nLDcU+t7cWO8z8xpyGW1BAnQ9wn+xaqrycLRf76I0TBtA==", - "license": "SEE LICENSE IN LICENSE.md", - "peer": true, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/device-utils": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@trezor/device-utils/-/device-utils-1.1.4.tgz", - "integrity": "sha512-hFC0nVnWVFaWx0IfCsoHGvBrh5SKsnTHwrX5pvBotwOw51lzTDMd43CkA7nHRybkhcc2JgX1Qq2UbYdwgEWhPg==", - "license": "See LICENSE.md in repo root", - "peer": true - }, - "node_modules/@trezor/env-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@trezor/env-utils/-/env-utils-1.4.3.tgz", - "integrity": "sha512-sWC828NRNQi5vc9W4M9rHOJDeI9XlsgnzZaML/lHju7WhlZCmSq5BOntZQvD8d1W0fSwLMLdlcBKBr/gQkvFZQ==", - "license": "See LICENSE.md in repo root", - "peer": true, - "dependencies": { - "ua-parser-js": "^2.0.4" - }, - "peerDependencies": { - "expo-constants": "*", - "expo-localization": "*", - "react-native": "*", - "tslib": "^2.6.2" - }, - "peerDependenciesMeta": { - "expo-constants": { - "optional": true - }, - "expo-localization": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/@trezor/protobuf": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-1.4.4.tgz", - "integrity": "sha512-+DwcXkio4qlMkPu6KxnEfhXv5PHTkKh2n6Fo88i5zishUHpYD3NhCS0pouzti8PIPyJE73HQ9MoisG44KQjbtg==", - "license": "See LICENSE.md in repo root", - "peer": true, - "dependencies": { - "@trezor/schema-utils": "1.3.4", - "long": "5.2.5", - "protobufjs": "7.4.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/protocol": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@trezor/protocol/-/protocol-1.2.10.tgz", - "integrity": "sha512-Ek5bHu2s4OAWOaJU5ksd1kcpe/STyLWOtUVTq6Vn4oMT3++qtrjWRQx/aTN/UaTfNoZlKvFXCC/STGlgBv9CKQ==", - "license": "See LICENSE.md in repo root", - "peer": true, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/schema-utils": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@trezor/schema-utils/-/schema-utils-1.3.4.tgz", - "integrity": "sha512-guP5TKjQEWe6c5HGx+7rhM0SAdEL5gylpkvk9XmJXjZDnl1Ew81nmLHUs2ghf8Od3pKBe4qjBIMBHUQNaOqWUg==", - "license": "See LICENSE.md in repo root", - "dependencies": { - "@sinclair/typebox": "^0.33.7", - "ts-mixer": "^6.0.3" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/transport": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@trezor/transport/-/transport-1.5.4.tgz", - "integrity": "sha512-3vGn2IEofbzhKMyLjzmTCwVTE5Wj0gkncLCNc66DU95IEW5WlwNGt/nXSJCg9TMBHK6qtlbY1HOBFuUzEW2Q7w==", - "license": "SEE LICENSE IN LICENSE.md", - "peer": true, - "dependencies": { - "@trezor/protobuf": "1.4.4", - "@trezor/protocol": "1.2.10", - "@trezor/type-utils": "1.1.9", - "@trezor/utils": "9.4.4", - "cross-fetch": "^4.0.0", - "usb": "^2.15.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/type-utils": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@trezor/type-utils/-/type-utils-1.1.9.tgz", - "integrity": "sha512-/Ug5pmVEpT5OVrf007kEvDj+zOdedHV0QcToUHG/WpVAKH9IsOssOAYIfRr8lDDgT+mDHuArZk/bYa1qvVz8Hw==", - "license": "See LICENSE.md in repo root", - "peer": true - }, - "node_modules/@trezor/utils": { - "version": "9.4.4", - "resolved": "https://registry.npmjs.org/@trezor/utils/-/utils-9.4.4.tgz", - "integrity": "sha512-08ciafbBqhApn58q3KkewdLQ3dCA71MsK/BOUfD43EB2GpB420zzky7REilXhOONc3giD0fBbTG3Zdt3HNL0/Q==", - "license": "SEE LICENSE IN LICENSE.md", - "peer": true, - "dependencies": { - "bignumber.js": "^9.3.1" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/utxo-lib": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@trezor/utxo-lib/-/utxo-lib-2.4.4.tgz", - "integrity": "sha512-rccdH3+iqvBL/Nkso/wGCdIXAQY+M/ubLIf/i/hBbcpRH6JoOg8oyaoaHzegsYNE6yHKnykTOZWz5Q4MTG02Bw==", - "license": "SEE LICENSE IN LICENSE.md", - "peer": true, - "dependencies": { - "@trezor/utils": "9.4.4", - "bech32": "^2.0.0", - "bip66": "^2.0.0", - "bitcoin-ops": "^1.4.1", - "blake-hash": "^2.0.0", - "blakejs": "^1.2.1", - "bn.js": "^5.2.2", - "bs58": "^6.0.0", - "bs58check": "^4.0.0", - "cashaddrjs": "0.4.4", - "create-hmac": "^1.1.7", - "int64-buffer": "^1.1.0", - "pushdata-bitcoin": "^1.0.1", - "tiny-secp256k1": "^1.1.7", - "typeforce": "^1.18.0", - "varuint-bitcoin": "2.0.0", - "wif": "^5.0.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/utxo-lib/node_modules/base-x": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", - "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", - "license": "MIT", - "peer": true - }, - "node_modules/@trezor/utxo-lib/node_modules/bs58": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", - "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", - "license": "MIT", - "peer": true, - "dependencies": { - "base-x": "^5.0.0" - } - }, - "node_modules/@trezor/websocket-client": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@trezor/websocket-client/-/websocket-client-1.2.4.tgz", - "integrity": "sha512-UgU31gFX8gY0abeI5DjRVnH4RfbXqHcOb019ogkR51KlfjkiWXTvUWKRLLqwslWiUIMEAI3ZFeXQds84b7Uw/Q==", - "license": "SEE LICENSE IN LICENSE.md", - "peer": true, - "dependencies": { - "@trezor/utils": "9.4.4", - "ws": "^8.18.0" - }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", - "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", - "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/react-router": { - "version": "5.1.20", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", - "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*" - } - }, - "node_modules/@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, - "node_modules/@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", - "license": "MIT" - }, - "node_modules/@types/w3c-web-usb": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.13.tgz", - "integrity": "sha512-N2nSl3Xsx8mRHZBvMSdNGtzMyeleTvtlEw+ujujgXalPqOjIA6UtrqcB6OzyUjkTbDm3J7P1RNK1lgoO7jxtsw==", - "license": "MIT" - }, - "node_modules/@types/web": { - "version": "0.0.197", - "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.197.tgz", - "integrity": "sha512-V4sOroWDADFx9dLodWpKm298NOJ1VJ6zoDVgaP+WBb/utWxqQ6gnMzd9lvVDAr/F3ibiKaxH9i45eS0gQPSTaQ==", - "license": "Apache-2.0" - }, - "node_modules/@types/ws": { - "version": "7.4.7", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", - "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", - "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/type-utils": "8.48.1", - "@typescript-eslint/utils": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.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.48.1", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", - "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "debug": "^4.3.4" - }, - "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", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", - "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.1", - "@typescript-eslint/types": "^8.48.1", - "debug": "^4.3.4" - }, - "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.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", - "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.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/tsconfig-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", - "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", - "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.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", - "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.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", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", - "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/typescript-estree": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", - "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.48.1", - "@typescript-eslint/tsconfig-utils": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.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.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", - "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.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", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", - "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.48.1", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vitejs/plugin-react": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", - "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.5", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.53", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitest/eslint-plugin": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.6.6.tgz", - "integrity": "sha512-bwgQxQWRtnTVzsUHK824tBmHzjV0iTx3tZaiQIYDjX3SA7TsQS8CuDVqxXrRY3FaOUMgbGavesCxI9MOfFLm7Q==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "^8.51.0", - "@typescript-eslint/utils": "^8.51.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": ">=8.57.0", - "typescript": ">=5.0.0", - "vitest": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "vitest": { - "optional": true - } - } - }, - "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/project-service": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz", - "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.53.0", - "@typescript-eslint/types": "^8.53.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.0.0" - } - }, - "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", - "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz", - "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==", - "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.0.0" - } - }, - "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz", - "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==", - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz", - "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.53.0", - "@typescript-eslint/tsconfig-utils": "8.53.0", - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0", - "debug": "^4.4.3", - "minimatch": "^9.0.5", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.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.0.0" - } - }, - "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz", - "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.53.0", - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/typescript-estree": "8.53.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", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", - "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.53.0", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitest/eslint-plugin/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@vitest/eslint-plugin/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@wallet-standard/base": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", - "integrity": "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16" - } - }, - "node_modules/@wallet-standard/features": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.0.tgz", - "integrity": "sha512-hiEivWNztx73s+7iLxsuD1sOJ28xtRix58W7Xnz4XzzA/pF0+aicnWgjOdA10doVDEDZdUuZCIIqG96SFNlDUg==", - "license": "Apache-2.0", - "dependencies": { - "@wallet-standard/base": "^1.1.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@walletconnect/core": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.11.2.tgz", - "integrity": "sha512-bB4SiXX8hX3/hyBfVPC5gwZCXCl+OPj+/EDVM71iAO3TDsh78KPbrVAbDnnsbHzZVHlsMohtXX3j5XVsheN3+g==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.1", - "@walletconnect/jsonrpc-provider": "1.0.13", - "@walletconnect/jsonrpc-types": "1.0.3", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.14", - "@walletconnect/keyvaluestorage": "^1.1.1", - "@walletconnect/logger": "^2.0.1", - "@walletconnect/relay-api": "^1.0.9", - "@walletconnect/relay-auth": "^1.0.4", - "@walletconnect/safe-json": "^1.0.2", - "@walletconnect/time": "^1.0.2", - "@walletconnect/types": "2.11.2", - "@walletconnect/utils": "2.11.2", - "events": "^3.3.0", - "isomorphic-unfetch": "3.1.0", - "lodash.isequal": "4.5.0", - "uint8arrays": "^3.1.0" - } - }, - "node_modules/@walletconnect/environment": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/environment/-/environment-1.0.1.tgz", - "integrity": "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/environment/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/events": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", - "integrity": "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==", - "license": "MIT", - "dependencies": { - "keyvaluestorage-interface": "^1.0.0", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/events/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/heartbeat": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.1.tgz", - "integrity": "sha512-yVzws616xsDLJxuG/28FqtZ5rzrTA4gUjdEMTbWB5Y8V1XHRmqq4efAxCw5ie7WjbXFSUyBHaWlMR+2/CpQC5Q==", - "license": "MIT", - "dependencies": { - "@walletconnect/events": "^1.0.1", - "@walletconnect/time": "^1.0.2", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/heartbeat/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/jsonrpc-provider": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.13.tgz", - "integrity": "sha512-K73EpThqHnSR26gOyNEL+acEex3P7VWZe6KE12ZwKzAt2H4e5gldZHbjsu2QR9cLeJ8AXuO7kEMOIcRv1QEc7g==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.8", - "@walletconnect/safe-json": "^1.0.2", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/jsonrpc-provider/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/jsonrpc-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.3.tgz", - "integrity": "sha512-iIQ8hboBl3o5ufmJ8cuduGad0CQm3ZlsHtujv9Eu16xq89q+BG7Nh5VLxxUgmtpnrePgFkTwXirCTkwJH1v+Yw==", - "license": "MIT", - "dependencies": { - "keyvaluestorage-interface": "^1.0.0", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/jsonrpc-types/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/jsonrpc-utils": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz", - "integrity": "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==", - "license": "MIT", - "dependencies": { - "@walletconnect/environment": "^1.0.1", - "@walletconnect/jsonrpc-types": "^1.0.3", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/jsonrpc-utils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/jsonrpc-ws-connection": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.14.tgz", - "integrity": "sha512-Jsl6fC55AYcbkNVkwNM6Jo+ufsuCQRqViOQ8ZBPH9pRREHH9welbBiszuTLqEJiQcO/6XfFDl6bzCJIkrEi8XA==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.6", - "@walletconnect/safe-json": "^1.0.2", - "events": "^3.3.0", - "ws": "^7.5.1" - } - }, - "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@walletconnect/keyvaluestorage": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", - "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", - "license": "MIT", - "dependencies": { - "@walletconnect/safe-json": "^1.0.1", - "idb-keyval": "^6.2.1", - "unstorage": "^1.9.0" - }, - "peerDependencies": { - "@react-native-async-storage/async-storage": "1.x" - }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - } - } - }, - "node_modules/@walletconnect/logger": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-2.1.3.tgz", - "integrity": "sha512-wRsD0eDQSajj8YMM/jpxoH1yeSLyS7FPkh0VKCQ1BWrERTy1Z7/DmOE8FYm/gmd7Cg6BNXVWiymhGq6wnmlq8w==", - "license": "MIT", - "dependencies": { - "@walletconnect/safe-json": "^1.0.2", - "pino": "7.11.0" - } - }, - "node_modules/@walletconnect/modal": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@walletconnect/modal/-/modal-2.6.2.tgz", - "integrity": "sha512-eFopgKi8AjKf/0U4SemvcYw9zlLpx9njVN8sf6DAkowC2Md0gPU/UNEbH1Wwj407pEKnEds98pKWib1NN1ACoA==", - "deprecated": "Please follow the migration guide on https://docs.reown.com/appkit/upgrade/wcm", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/modal-core": "2.6.2", - "@walletconnect/modal-ui": "2.6.2" - } - }, - "node_modules/@walletconnect/modal-core": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@walletconnect/modal-core/-/modal-core-2.6.2.tgz", - "integrity": "sha512-cv8ibvdOJQv2B+nyxP9IIFdxvQznMz8OOr/oR/AaUZym4hjXNL/l1a2UlSQBXrVjo3xxbouMxLb3kBsHoYP2CA==", - "license": "Apache-2.0", - "dependencies": { - "valtio": "1.11.2" - } - }, - "node_modules/@walletconnect/modal-ui": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@walletconnect/modal-ui/-/modal-ui-2.6.2.tgz", - "integrity": "sha512-rbdstM1HPGvr7jprQkyPggX7rP4XiCG85ZA+zWBEX0dVQg8PpAgRUqpeub4xQKDgY7pY/xLRXSiCVdWGqvG2HA==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/modal-core": "2.6.2", - "lit": "2.8.0", - "motion": "10.16.2", - "qrcode": "1.5.3" - } - }, - "node_modules/@walletconnect/modal-ui/node_modules/@lit/reactive-element": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", - "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.0.0" - } - }, - "node_modules/@walletconnect/modal-ui/node_modules/lit": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", - "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^1.6.0", - "lit-element": "^3.3.0", - "lit-html": "^2.8.0" - } - }, - "node_modules/@walletconnect/modal-ui/node_modules/lit-element": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", - "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.1.0", - "@lit/reactive-element": "^1.3.0", - "lit-html": "^2.8.0" - } - }, - "node_modules/@walletconnect/modal-ui/node_modules/lit-html": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", - "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, - "node_modules/@walletconnect/relay-api": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", - "integrity": "sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-types": "^1.0.2" - } - }, - "node_modules/@walletconnect/relay-auth": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz", - "integrity": "sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==", - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.0", - "@noble/hashes": "1.7.0", - "@walletconnect/safe-json": "^1.0.1", - "@walletconnect/time": "^1.0.2", - "uint8arrays": "^3.0.0" - } - }, - "node_modules/@walletconnect/relay-auth/node_modules/@noble/curves": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", - "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/relay-auth/node_modules/@noble/hashes": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", - "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/safe-json": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz", - "integrity": "sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/safe-json/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/sign-client": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.11.2.tgz", - "integrity": "sha512-MfBcuSz2GmMH+P7MrCP46mVE5qhP0ZyWA0FyIH6/WuxQ6G+MgKsGfaITqakpRPsykWOJq8tXMs3XvUPDU413OQ==", - "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.11.2", - "@walletconnect/events": "^1.0.1", - "@walletconnect/heartbeat": "1.2.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "^2.0.1", - "@walletconnect/time": "^1.0.2", - "@walletconnect/types": "2.11.2", - "@walletconnect/utils": "2.11.2", - "events": "^3.3.0" - } - }, - "node_modules/@walletconnect/time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", - "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/time/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/types": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.11.2.tgz", - "integrity": "sha512-p632MFB+lJbip2cvtXPBQslpUdiw1sDtQ5y855bOlAGquay+6fZ4h1DcDePeKQDQM3P77ax2a9aNPZxV6y/h1Q==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "^1.0.1", - "@walletconnect/heartbeat": "1.2.1", - "@walletconnect/jsonrpc-types": "1.0.3", - "@walletconnect/keyvaluestorage": "^1.1.1", - "@walletconnect/logger": "^2.0.1", - "events": "^3.3.0" - } - }, - "node_modules/@walletconnect/utils": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.11.2.tgz", - "integrity": "sha512-LyfdmrnZY6dWqlF4eDrx5jpUwsB2bEPjoqR5Z6rXPiHJKUOdJt7az+mNOn5KTSOlRpd1DmozrBrWr+G9fFLYVw==", - "license": "Apache-2.0", - "dependencies": { - "@stablelib/chacha20poly1305": "1.0.1", - "@stablelib/hkdf": "1.0.1", - "@stablelib/random": "^1.0.2", - "@stablelib/sha256": "1.0.1", - "@stablelib/x25519": "^1.0.3", - "@walletconnect/relay-api": "^1.0.9", - "@walletconnect/safe-json": "^1.0.2", - "@walletconnect/time": "^1.0.2", - "@walletconnect/types": "2.11.2", - "@walletconnect/window-getters": "^1.0.1", - "@walletconnect/window-metadata": "^1.0.1", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "^3.1.0" - } - }, - "node_modules/@walletconnect/window-getters": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", - "integrity": "sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/window-getters/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/window-metadata": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", - "integrity": "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==", - "license": "MIT", - "dependencies": { - "@walletconnect/window-getters": "^1.0.1", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/window-metadata/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@xrplf/isomorphic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@xrplf/isomorphic/-/isomorphic-1.0.1.tgz", - "integrity": "sha512-0bIpgx8PDjYdrLFeC3csF305QQ1L7sxaWnL5y71mCvhenZzJgku9QsA+9QCXBC1eNYtxWO/xR91zrXJy2T/ixg==", - "license": "ISC", - "dependencies": { - "@noble/hashes": "^1.0.0", - "eventemitter3": "5.0.1", - "ws": "^8.13.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@xrplf/secret-numbers": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@xrplf/secret-numbers/-/secret-numbers-2.0.0.tgz", - "integrity": "sha512-z3AOibRTE9E8MbjgzxqMpG1RNaBhQ1jnfhNCa1cGf2reZUJzPMYs4TggQTc7j8+0WyV3cr7y/U8Oz99SXIkN5Q==", - "license": "ISC", - "dependencies": { - "@xrplf/isomorphic": "^1.0.1", - "ripple-keypairs": "^2.0.0" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", - "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "license": "MIT" - }, - "node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axios": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", - "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/bare-addon-resolve": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.9.6.tgz", - "integrity": "sha512-hvOQY1zDK6u0rSr27T6QlULoVLwi8J2k8HHHJlxSfT7XQdQ/7bsS+AnjYkHtu/TkL+gm3aMXAKucJkJAbrDG/g==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-module-resolve": "^1.10.0", - "bare-semver": "^1.0.0" - }, - "peerDependencies": { - "bare-url": "*" - }, - "peerDependenciesMeta": { - "bare-url": { - "optional": true - } - } - }, - "node_modules/bare-module-resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.0.tgz", - "integrity": "sha512-JrzrqlC3Tds0iKRwQs8xIIJ+FRieKA9ll0jaqpotDLZtjJPVevzRoeuUYZ5GIo1t1z7/pIRdk85Q3i/2xQLfEQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-semver": "^1.0.0" - }, - "peerDependencies": { - "bare-url": "*" - }, - "peerDependenciesMeta": { - "bare-url": { - "optional": true - } - } - }, - "node_modules/bare-semver": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz", - "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/base-x": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", - "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/base32.js": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", - "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.32", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", - "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/bchaddrjs": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/bchaddrjs/-/bchaddrjs-0.5.2.tgz", - "integrity": "sha512-OO7gIn3m7ea4FVx4cT8gdlWQR2+++EquhdpWQJH9BQjK63tJJ6ngB3QMZDO6DiBoXiIGUsTPHjlrHVxPGcGxLQ==", - "license": "MIT", - "dependencies": { - "bs58check": "2.1.2", - "buffer": "^6.0.3", - "cashaddrjs": "0.4.4", - "stream-browserify": "^3.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/bchaddrjs/node_modules/bs58check": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", - "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", - "license": "MIT", - "dependencies": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/bech32": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", - "license": "MIT" - }, - "node_modules/big-integer": { - "version": "1.6.36", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz", - "integrity": "sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg==", - "license": "Unlicense", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bip32-path": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/bip32-path/-/bip32-path-0.4.2.tgz", - "integrity": "sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ==", - "license": "MIT" - }, - "node_modules/bip66": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bip66/-/bip66-2.0.0.tgz", - "integrity": "sha512-kBG+hSpgvZBrkIm9dt5T1Hd/7xGCPEX2npoxAWZfsK1FvjgaxySEh2WizjyIstWXriKo9K9uJ4u0OnsyLDUPXQ==", - "license": "MIT" - }, - "node_modules/bitcoin-ops": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", - "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==", - "license": "MIT" - }, - "node_modules/blake-hash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/blake-hash/-/blake-hash-2.0.0.tgz", - "integrity": "sha512-Igj8YowDu1PRkRsxZA7NVkdFNxH5rKv5cpLxQ0CVXSIA77pVYwCPRQJ2sMew/oneUpfuYRyjG6r8SmmmnbZb1w==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^3.0.0", - "node-gyp-build": "^4.2.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/blakejs": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", - "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", - "license": "MIT" - }, - "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", - "license": "MIT" - }, - "node_modules/borsh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-2.0.0.tgz", - "integrity": "sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg==", - "license": "Apache-2.0" - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "license": "MIT" - }, - "node_modules/browser-resolve": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", - "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.17.0" - } - }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "license": "MIT", - "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "license": "MIT", - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", - "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", - "license": "MIT", - "dependencies": { - "bn.js": "^5.2.1", - "randombytes": "^2.1.0", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", - "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", - "license": "ISC", - "dependencies": { - "bn.js": "^5.2.2", - "browserify-rsa": "^4.1.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.6.1", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.9", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-sign/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/browserify-sign/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pako": "~1.0.5" - } - }, - "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", - "license": "MIT", - "dependencies": { - "base-x": "^3.0.2" - } - }, - "node_modules/bs58check": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz", - "integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.2.0", - "bs58": "^6.0.0" - } - }, - "node_modules/bs58check/node_modules/base-x": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", - "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", - "license": "MIT" - }, - "node_modules/bs58check/node_modules/bs58": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", - "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", - "license": "MIT", - "dependencies": { - "base-x": "^5.0.0" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "license": "MIT" - }, - "node_modules/bufferutil": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", - "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001757", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", - "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/cashaddrjs": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cashaddrjs/-/cashaddrjs-0.4.4.tgz", - "integrity": "sha512-xZkuWdNOh0uq/mxJIng6vYWfTowZLd9F4GMAlp2DwFHlcCqCm91NtuAc47RuV4L7r4PYcY5p6Cr2OKNb4hnkWA==", - "license": "MIT", - "dependencies": { - "big-integer": "1.6.36" - } - }, - "node_modules/cbor": { - "version": "10.0.11", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.11.tgz", - "integrity": "sha512-vIwORDd/WyB8Nc23o2zNN5RrtFGlR6Fca61TtjkUXueI3Jf2DOZDl1zsshvBntZ3wZHBM9ztjnkXSmzQDaq3WA==", - "license": "MIT", - "peer": true, - "dependencies": { - "nofilter": "^3.0.2" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/cipher-base": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", - "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", - "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/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/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/comment-parser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", - "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", - "license": "MIT", - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/concurrently": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "4.1.2", - "rxjs": "7.8.2", - "shell-quote": "1.8.3", - "supports-color": "8.1.1", - "tree-kill": "1.2.2", - "yargs": "17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" - } - }, - "node_modules/concurrently/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/concurrently/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/concurrently/node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "node_modules/constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cookie-es": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", - "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", - "license": "MIT" - }, - "node_modules/copy-to-clipboard": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", - "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", - "license": "MIT", - "dependencies": { - "toggle-selection": "^1.0.6" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "license": "MIT" - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-fetch": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", - "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crossws": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", - "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", - "license": "MIT", - "dependencies": { - "uncrypto": "^0.1.3" - } - }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "license": "MIT", - "peer": true, - "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - }, - "engines": { - "node": "*" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "license": "MIT" - }, - "node_modules/delay": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", - "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/des.js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", - "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/destr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "license": "MIT" - }, - "node_modules/detect-browser": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", - "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", - "license": "MIT" - }, - "node_modules/detect-europe-js": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz", - "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - } - ], - "license": "MIT" - }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "license": "MIT" - }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", - "license": "MIT" - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/domain-browser": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", - "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.263", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz", - "integrity": "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==", - "license": "ISC" - }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "license": "MIT" - }, - "node_modules/encode-utf8": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", - "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.1", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.1.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.5", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "license": "MIT" - }, - "node_modules/es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", - "license": "MIT", - "dependencies": { - "es6-promise": "^4.0.3" - } - }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-import-context": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", - "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", - "license": "MIT", - "dependencies": { - "get-tsconfig": "^4.10.1", - "stable-hash-x": "^0.2.0" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-import-context" - }, - "peerDependencies": { - "unrs-resolver": "^1.0.0" - }, - "peerDependenciesMeta": { - "unrs-resolver": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-import-x": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.1.tgz", - "integrity": "sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "^8.35.0", - "comment-parser": "^1.4.1", - "debug": "^4.4.1", - "eslint-import-context": "^0.1.9", - "is-glob": "^4.0.3", - "minimatch": "^9.0.3 || ^10.0.1", - "semver": "^7.7.2", - "stable-hash-x": "^0.2.0", - "unrs-resolver": "^1.9.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-import-x" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "eslint-import-resolver-node": "*" - }, - "peerDependenciesMeta": { - "@typescript-eslint/utils": { - "optional": true - }, - "eslint-import-resolver-node": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-import-x/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint-plugin-jest-dom": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-5.5.0.tgz", - "integrity": "sha512-CRlXfchTr7EgC3tDI7MGHY6QjdJU5Vv2RPaeeGtkXUHnKZf04kgzMPIJUXt4qKCvYWVVIEo9ut9Oq1vgXAykEA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.16.3", - "requireindex": "^1.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0", - "npm": ">=6", - "yarn": ">=1" - }, - "peerDependencies": { - "@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0", - "eslint": "^6.8.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "@testing-library/dom": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-playwright": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.5.0.tgz", - "integrity": "sha512-1ckFw7Abdz+l23wtw5Tg4GTK3Y+MgEQQNjEr7FTJP3wwmIOj8DkbJ6G655aPc09c0Kfn/NoGA4xpMZzeSO4NWw==", - "license": "MIT", - "dependencies": { - "globals": "^16.4.0" - }, - "engines": { - "node": ">=16.9.0" - }, - "peerDependencies": { - "eslint": ">=8.40.0" - } - }, - "node_modules/eslint-plugin-playwright/node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-testing-library": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-7.15.4.tgz", - "integrity": "sha512-qP0ZPWAvDrS3oxZJErUfn3SZiIzj5Zh2EWuyWxjR5Bsk84ntxpquh4D0USorfyw5MzECURQ8OcEeBQdspHatzQ==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "^8.51.0", - "@typescript-eslint/utils": "^8.51.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/project-service": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz", - "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.53.0", - "@typescript-eslint/types": "^8.53.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.0.0" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/scope-manager": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", - "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz", - "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==", - "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.0.0" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/types": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz", - "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==", - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz", - "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.53.0", - "@typescript-eslint/tsconfig-utils": "8.53.0", - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0", - "debug": "^4.4.3", - "minimatch": "^9.0.5", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.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.0.0" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/utils": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz", - "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.53.0", - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/typescript-estree": "8.53.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", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", - "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.53.0", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ethereum-cryptography": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-3.2.0.tgz", - "integrity": "sha512-Urr5YVsalH+Jo0sYkTkv1MyI9bLYZwW8BENZCeE1QYaTHETEYx0Nv/SVsWkSqpYrzweg6d8KMY1wTjH/1m/BIg==", - "license": "MIT", - "dependencies": { - "@noble/ciphers": "1.3.0", - "@noble/curves": "1.9.0", - "@noble/hashes": "1.8.0", - "@scure/bip32": "1.7.0", - "@scure/bip39": "1.6.0" - }, - "engines": { - "node": "^14.21.3 || >=16", - "npm": ">=9" - } - }, - "node_modules/ethereum-cryptography/node_modules/@noble/curves": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.0.tgz", - "integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "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/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/eventsource": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "license": "MIT", - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/eyes": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", - "engines": { - "node": "> 0.1.90" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "license": "MIT" - }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-stable-stringify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", - "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", - "license": "MIT" - }, - "node_modules/fastestsmallesttextencoderdecoder": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", - "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", - "license": "CC0-1.0", - "peer": true - }, - "node_modules/feaxios": { - "version": "0.0.23", - "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.23.tgz", - "integrity": "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==", - "license": "MIT", - "dependencies": { - "is-retry-allowed": "^3.0.0" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-property": "^1.0.2" - } - }, - "node_modules/generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-property": "^1.0.0" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", - "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", - "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "license": "MIT" - }, - "node_modules/h3": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.4.tgz", - "integrity": "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==", - "license": "MIT", - "dependencies": { - "cookie-es": "^1.2.2", - "crossws": "^0.3.5", - "defu": "^6.1.4", - "destr": "^2.0.5", - "iron-webcrypto": "^1.2.1", - "node-mock-http": "^1.0.2", - "radix3": "^1.1.2", - "ufo": "^1.6.1", - "uncrypto": "^0.1.3" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hash-base": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", - "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/hash-base/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/hash-base/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hash-base/node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/hash-base/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/hash-base/node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/hey-listen": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", - "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", - "license": "MIT" - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "license": "MIT", - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "license": "MIT", - "peer": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-errors/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-errors/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "license": "ISC", - "peer": true - }, - "node_modules/https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/idb-keyval": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", - "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", - "license": "Apache-2.0" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/int64-buffer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-1.1.0.tgz", - "integrity": "sha512-94smTCQOvigN4d/2R/YDjz8YVG0Sufvv2aAh8P5m42gwhCsDAJqnbNOrxJsrADuAFAA69Q/ptGzxvNcNuIJcvw==", - "license": "MIT" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/iron-webcrypto": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", - "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/brc-dd" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-my-ip-valid": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz", - "integrity": "sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==", - "license": "MIT", - "peer": true - }, - "node_modules/is-my-json-valid": { - "version": "2.20.6", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", - "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", - "license": "MIT", - "peer": true, - "dependencies": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^5.0.0", - "xtend": "^4.0.0" - } - }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "license": "MIT", - "peer": true - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-retry-allowed": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-3.0.0.tgz", - "integrity": "sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-standalone-pwa": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", - "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - } - ], - "license": "MIT" - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/isomorphic-timers-promises": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", - "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/isomorphic-unfetch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", - "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "unfetch": "^4.2.0" - } - }, - "node_modules/isomorphic-ws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", - "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/jayson": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.2.0.tgz", - "integrity": "sha512-VfJ9t1YLwacIubLhONk0KFeosUBwstRWQ0IRT1KDjEjnVnSOVHC3uwugyV7L0c7R9lpVyrUGT2XWiBA1UTtpyg==", - "license": "MIT", - "dependencies": { - "@types/connect": "^3.4.33", - "@types/node": "^12.12.54", - "@types/ws": "^7.4.4", - "commander": "^2.20.3", - "delay": "^5.0.0", - "es6-promisify": "^5.0.0", - "eyes": "^0.1.8", - "isomorphic-ws": "^4.0.1", - "json-stringify-safe": "^5.0.1", - "stream-json": "^1.9.1", - "uuid": "^8.3.2", - "ws": "^7.5.10" - }, - "bin": { - "jayson": "bin/jayson.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jayson/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "license": "MIT" - }, - "node_modules/jayson/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/jayson/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/js-sha256": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz", - "integrity": "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==", - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "license": "MIT" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "license": "ISC" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/keyvaluestorage-interface": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", - "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lint-staged": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", - "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^14.0.2", - "listr2": "^9.0.5", - "micromatch": "^4.0.8", - "nano-spawn": "^2.0.0", - "pidtree": "^0.6.0", - "string-argv": "^0.3.2", - "yaml": "^2.8.1" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=20.17" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/lit": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.0.tgz", - "integrity": "sha512-s6tI33Lf6VpDu7u4YqsSX78D28bYQulM+VAzsGch4fx2H0eLZnJsUBsPWmGYSGoKDNbjtRv02rio1o+UdPVwvw==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.0.4", - "lit-element": "^4.1.0", - "lit-html": "^3.2.0" - } - }, - "node_modules/lit-element": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.1.tgz", - "integrity": "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.4.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-html": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.1.tgz", - "integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "license": "MIT" - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/long": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.5.tgz", - "integrity": "sha512-e0r9YBBgNCq1D1o5Dp8FMH0N5hsFtXDBiVa0qoJPHpakvZkmDKPRoGffZJII/XsHvj9An9blm+cRJ01yQqU+Dw==", - "license": "Apache-2.0" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lossless-json": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.3.0.tgz", - "integrity": "sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g==", - "license": "MIT" - }, - "node_modules/lru_map": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.4.1.tgz", - "integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==", - "license": "MIT", - "peer": true - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "license": "MIT", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "license": "MIT" - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC" - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "license": "MIT" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/motion": { - "version": "10.16.2", - "resolved": "https://registry.npmjs.org/motion/-/motion-10.16.2.tgz", - "integrity": "sha512-p+PurYqfUdcJZvtnmAqu5fJgV2kR0uLFQuBKtLeFVTrYEVllI99tiOTSefVNYuip9ELTEkepIIDftNdze76NAQ==", - "license": "MIT", - "dependencies": { - "@motionone/animation": "^10.15.1", - "@motionone/dom": "^10.16.2", - "@motionone/svelte": "^10.16.2", - "@motionone/types": "^10.15.1", - "@motionone/utils": "^10.15.1", - "@motionone/vue": "^10.16.2" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, - "node_modules/mustache": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.0.0.tgz", - "integrity": "sha512-FJgjyX/IVkbXBXYUwH+OYwQKqWpFPLaLVESd70yHjSDunwzV2hZOoTBvPf4KLoxesUzzyfTH6F784Uqd7Wm5yA==", - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - }, - "engines": { - "npm": ">=1.4.0" - } - }, - "node_modules/nan": { - "version": "2.23.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.1.tgz", - "integrity": "sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw==", - "license": "MIT" - }, - "node_modules/nano-spawn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", - "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "license": "MIT" - }, - "node_modules/near-abi": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/near-abi/-/near-abi-0.2.0.tgz", - "integrity": "sha512-kCwSf/3fraPU2zENK18sh+kKG4uKbEUEQdyWQkmW8ZofmLarObIz2+zAYjA1teDZLeMvEQew3UysnPDXgjneaA==", - "license": "(MIT AND Apache-2.0)", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.11" - } - }, - "node_modules/near-api-js": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/near-api-js/-/near-api-js-5.1.1.tgz", - "integrity": "sha512-h23BGSKxNv8ph+zU6snicstsVK1/CTXsQz4LuGGwoRE24Hj424nSe4+/1tzoiC285Ljf60kPAqRCmsfv9etF2g==", - "license": "(MIT AND Apache-2.0)", - "peer": true, - "dependencies": { - "@near-js/accounts": "1.4.1", - "@near-js/crypto": "1.4.2", - "@near-js/keystores": "0.2.2", - "@near-js/keystores-browser": "0.2.2", - "@near-js/keystores-node": "0.1.2", - "@near-js/providers": "1.0.3", - "@near-js/signers": "0.2.2", - "@near-js/transactions": "1.3.3", - "@near-js/types": "0.3.1", - "@near-js/utils": "1.1.0", - "@near-js/wallet-account": "1.3.3", - "@noble/curves": "1.8.1", - "borsh": "1.0.0", - "depd": "2.0.0", - "http-errors": "1.7.2", - "near-abi": "0.2.0", - "node-fetch": "2.6.7" - } - }, - "node_modules/near-api-js/node_modules/borsh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", - "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/near-api-js/node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "license": "MIT" - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-mock-http": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.3.tgz", - "integrity": "sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==", - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "license": "MIT" - }, - "node_modules/node-stdlib-browser": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz", - "integrity": "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert": "^2.0.0", - "browser-resolve": "^2.0.0", - "browserify-zlib": "^0.2.0", - "buffer": "^5.7.1", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "create-require": "^1.1.1", - "crypto-browserify": "^3.12.1", - "domain-browser": "4.22.0", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "isomorphic-timers-promises": "^1.0.1", - "os-browserify": "^0.3.0", - "path-browserify": "^1.0.1", - "pkg-dir": "^5.0.0", - "process": "^0.11.10", - "punycode": "^1.4.1", - "querystring-es3": "^0.2.1", - "readable-stream": "^3.6.0", - "stream-browserify": "^3.0.0", - "stream-http": "^3.2.0", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.1", - "url": "^0.11.4", - "util": "^0.12.4", - "vm-browserify": "^1.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-stdlib-browser/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/node-stdlib-browser/node_modules/crypto-browserify": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", - "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserify-cipher": "^1.0.1", - "browserify-sign": "^4.2.3", - "create-ecdh": "^4.0.4", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "diffie-hellman": "^5.0.3", - "hash-base": "~3.0.4", - "inherits": "^2.0.4", - "pbkdf2": "^3.1.2", - "public-encrypt": "^4.0.3", - "randombytes": "^2.1.0", - "randomfill": "^1.0.4" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/node-stdlib-browser/node_modules/hash-base": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/node-stdlib-browser/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/nofilter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", - "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.19" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ofetch": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", - "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", - "license": "MIT", - "dependencies": { - "destr": "^2.0.5", - "node-fetch-native": "^1.6.7", - "ufo": "^1.6.1" - } - }, - "node_modules/on-exit-leak-free": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", - "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==", - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, - "license": "(MIT AND Zlib)" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-asn1": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", - "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", - "license": "ISC", - "dependencies": { - "asn1.js": "^4.10.1", - "browserify-aes": "^1.2.0", - "evp_bytestokey": "^1.0.3", - "pbkdf2": "^3.1.5", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/pbkdf2": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", - "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", - "license": "MIT", - "dependencies": { - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "ripemd160": "^2.0.3", - "safe-buffer": "^5.2.1", - "sha.js": "^2.4.12", - "to-buffer": "^1.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/pino": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", - "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.0.0", - "on-exit-leak-free": "^0.2.0", - "pino-abstract-transport": "v0.5.0", - "pino-std-serializers": "^4.0.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.1.0", - "safe-stable-stringify": "^2.1.0", - "sonic-boom": "^2.2.1", - "thread-stream": "^0.15.1" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", - "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", - "license": "MIT", - "dependencies": { - "duplexify": "^4.1.2", - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", - "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==", - "license": "MIT" - }, - "node_modules/pkg-dir": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", - "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^5.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", - "license": "MIT" - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/protobufjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", - "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-compare": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.5.1.tgz", - "integrity": "sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA==", - "license": "MIT" - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pushdata-bitcoin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", - "integrity": "sha512-hw7rcYTJRAl4olM8Owe8x0fBuJJ+WGbMhQuLWOXEMN3PxPCKQHRkhfL+XG0+iXUmSHjkMmb3Ba55Mt21cZc9kQ==", - "license": "MIT", - "dependencies": { - "bitcoin-ops": "^1.3.0" - } - }, - "node_modules/qrcode": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", - "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/qrcode/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/qrcode/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/qrcode/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/qrcode/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/qrcode/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/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==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/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==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "node_modules/qrcode/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/radix3": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", - "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "license": "MIT", - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-copy-to-clipboard-ts": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/react-copy-to-clipboard-ts/-/react-copy-to-clipboard-ts-1.3.0.tgz", - "integrity": "sha512-3z+WgPH5lT84m2ayucrPU3z/gRfWn+ADq/HTPWZqSq4iZNRiUBhpg78jDLIjtrDtgOH7iaGr5PXUbdnr3TQ3tg==", - "license": "MIT", - "dependencies": { - "copy-to-clipboard": "^3.3.3" - }, - "peerDependencies": { - "react": ">=18.0.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", - "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", - "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", - "license": "MIT", - "dependencies": { - "react-router": "7.13.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/real-require": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", - "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/require-addon": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", - "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-addon-resolve": "^1.3.0" - }, - "engines": { - "bare": ">=1.10.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" - }, - "node_modules/requireindex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", - "license": "MIT", - "engines": { - "node": ">=0.10.5" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ripemd160": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", - "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", - "license": "MIT", - "dependencies": { - "hash-base": "^3.1.2", - "inherits": "^2.0.4" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ripple-address-codec": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-5.0.0.tgz", - "integrity": "sha512-de7osLRH/pt5HX2xw2TRJtbdLLWHu0RXirpQaEeCnWKY5DYHykh3ETSkofvm0aX0LJiV7kwkegJxQkmbO94gWw==", - "license": "ISC", - "dependencies": { - "@scure/base": "^1.1.3", - "@xrplf/isomorphic": "^1.0.0" - }, - "engines": { - "node": ">= 16" - } - }, - "node_modules/ripple-binary-codec": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-2.5.1.tgz", - "integrity": "sha512-rzN4GTorLRH0bQD7Tccgn6Eq4aunMhZaUTXDEUJ+3xrjo0m1Av5AY1Doc/jsCIaxPIAnyoVg5rWlmI93U7pGdg==", - "license": "ISC", - "dependencies": { - "@xrplf/isomorphic": "^1.0.1", - "bignumber.js": "^9.0.0", - "ripple-address-codec": "^5.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/ripple-keypairs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-2.0.0.tgz", - "integrity": "sha512-b5rfL2EZiffmklqZk1W+dvSy97v3V/C7936WxCCgDynaGPp7GE6R2XO7EU9O2LlM/z95rj870IylYnOQs+1Rag==", - "license": "ISC", - "dependencies": { - "@noble/curves": "^1.0.0", - "@xrplf/isomorphic": "^1.0.0", - "ripple-address-codec": "^5.0.0" - }, - "engines": { - "node": ">= 16" - } - }, - "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", - "fsevents": "~2.3.2" - } - }, - "node_modules/rpc-websockets": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.2.tgz", - "integrity": "sha512-VuW2xJDnl1k8n8kjbdRSWawPRkwaVqUQNjE1TdeTawf0y0abGhtVJFTXCLfgpgGDBkO/Fj6kny8Dc/nvOW78MA==", - "license": "LGPL-3.0-only", - "dependencies": { - "@swc/helpers": "^0.5.11", - "@types/uuid": "^8.3.4", - "@types/ws": "^8.2.2", - "buffer": "^6.0.3", - "eventemitter3": "^5.0.1", - "uuid": "^8.3.2", - "ws": "^8.5.0" - }, - "funding": { - "type": "paypal", - "url": "https://paypal.me/kozjak" - }, - "optionalDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - } - }, - "node_modules/rpc-websockets/node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/secp256k1": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.1.tgz", - "integrity": "sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "elliptic": "^6.5.7", - "node-addon-api": "^5.0.0", - "node-gyp-build": "^4.2.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/secp256k1/node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT" - }, - "node_modules/setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "license": "ISC", - "peer": true - }, - "node_modules/sha.js": { - "version": "2.4.12", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", - "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", - "license": "(MIT AND BSD-3-Clause)", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.0" - }, - "bin": { - "sha.js": "bin.js" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sha1": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", - "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", - "license": "BSD-3-Clause", - "dependencies": { - "charenc": ">= 0.0.1", - "crypt": ">= 0.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/sodium-native": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", - "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==", - "license": "MIT", - "optional": true, - "dependencies": { - "require-addon": "^1.1.0" - } - }, - "node_modules/sonic-boom": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", - "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/stable-hash-x": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", - "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/stream-browserify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.4", - "readable-stream": "^3.5.0" - } - }, - "node_modules/stream-chain": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", - "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", - "license": "BSD-3-Clause" - }, - "node_modules/stream-http": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", - "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", - "dev": true, - "license": "MIT", - "dependencies": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "xtend": "^4.0.2" - } - }, - "node_modules/stream-json": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", - "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", - "license": "BSD-3-Clause", - "dependencies": { - "stream-chain": "^2.2.5" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "license": "MIT" - }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/superstruct": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", - "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/text-encoding-utf-8": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", - "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" - }, - "node_modules/thread-stream": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", - "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", - "license": "MIT", - "dependencies": { - "real-require": "^0.1.0" - } - }, - "node_modules/timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "setimmediate": "^1.0.4" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/tiny-secp256k1": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.7.tgz", - "integrity": "sha512-eb+F6NabSnjbLwNoC+2o5ItbmP1kg7HliWue71JgLegQt6A5mTN8YbvTLCazdlg6e5SV6A+r8OGvZYskdlmhqQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.3.0", - "bn.js": "^4.11.8", - "create-hmac": "^1.1.7", - "elliptic": "^6.4.0", - "nan": "^2.13.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/tiny-secp256k1/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-buffer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", - "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", - "license": "MIT", - "dependencies": { - "isarray": "^2.0.5", - "safe-buffer": "^5.2.1", - "typed-array-buffer": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toggle-selection": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", - "license": "MIT" - }, - "node_modules/toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", - "license": "MIT" - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-mixer": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", - "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense" - }, - "node_modules/tweetnacl-util": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", - "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==", - "license": "Unlicense" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typeforce": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", - "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==", - "license": "MIT" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz", - "integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.48.1", - "@typescript-eslint/parser": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.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", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/ua-is-frozen": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz", - "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - } - ], - "license": "MIT" - }, - "node_modules/ua-parser-js": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.6.tgz", - "integrity": "sha512-EmaxXfltJaDW75SokrY4/lXMrVyXomE/0FpIIqP2Ctic93gK7rlme55Cwkz8l3YZ6gqf94fCU7AnIkidd/KXPg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "AGPL-3.0-or-later", - "dependencies": { - "detect-europe-js": "^0.1.2", - "is-standalone-pwa": "^0.1.1", - "ua-is-frozen": "^0.1.2" - }, - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "license": "MIT" - }, - "node_modules/uint8array-tools": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz", - "integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/uint8arrays": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz", - "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", - "license": "MIT", - "dependencies": { - "multiformats": "^9.4.2" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" - }, - "node_modules/unfetch": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", - "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", - "license": "MIT" - }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.0" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" - } - }, - "node_modules/unstorage": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.3.tgz", - "integrity": "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==", - "license": "MIT", - "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^4.0.3", - "destr": "^2.0.5", - "h3": "^1.15.4", - "lru-cache": "^10.4.3", - "node-fetch-native": "^1.6.7", - "ofetch": "^1.5.1", - "ufo": "^1.6.1" - }, - "peerDependencies": { - "@azure/app-configuration": "^1.8.0", - "@azure/cosmos": "^4.2.0", - "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.26.0", - "@capacitor/preferences": "^6.0.3 || ^7.0.0", - "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", - "@vercel/functions": "^2.2.12 || ^3.0.0", - "@vercel/kv": "^1.0.1", - "aws4fetch": "^1.0.20", - "db0": ">=0.2.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" - }, - "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@deno/kv": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/blob": { - "optional": true - }, - "@vercel/functions": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "aws4fetch": { - "optional": true - }, - "db0": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "uploadthing": { - "optional": true - } - } - }, - "node_modules/unstorage/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/urijs": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", - "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", - "license": "MIT" - }, - "node_modules/url": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", - "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^1.4.1", - "qs": "^6.12.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/usb": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/usb/-/usb-2.16.0.tgz", - "integrity": "sha512-jD88fvzDViMDH5KmmNJgzMBDj/95bDTt6+kBNaNxP4G98xUTnDMiLUY2CYmToba6JAFhM9VkcaQuxCNRLGR7zg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@types/w3c-web-usb": "^1.0.6", - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.5.0" - }, - "engines": { - "node": ">=12.22.0 <13.0 || >=14.17.0" - } - }, - "node_modules/usb/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/uuid4": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid4/-/uuid4-2.0.3.tgz", - "integrity": "sha512-CTpAkEVXMNJl2ojgtpLXHgz23dh8z81u6/HEPiQFOvBc/c2pde6TVHmH4uwY0d/GLF3tb7+VDAj4+2eJaQSdZQ==", - "license": "ISC" - }, - "node_modules/valtio": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.11.2.tgz", - "integrity": "sha512-1XfIxnUXzyswPAPXo1P3Pdx2mq/pIqZICkWN60Hby0d9Iqb+MEIpqgYVlbflvHdrp2YR/q3jyKWRPJJ100yxaw==", - "license": "MIT", - "dependencies": { - "proxy-compare": "2.5.1", - "use-sync-external-store": "1.2.0" - }, - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/valtio/node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/varuint-bitcoin": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-2.0.0.tgz", - "integrity": "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==", - "license": "MIT", - "dependencies": { - "uint8array-tools": "^0.0.8" - } - }, - "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-plugin-node-polyfills": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.25.0.tgz", - "integrity": "sha512-rHZ324W3LhfGPxWwQb2N048TThB6nVvnipsqBUJEzh3R9xeK9KI3si+GMQxCuAcpPJBVf0LpDtJ+beYzB3/chg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/plugin-inject": "^5.0.5", - "node-stdlib-browser": "^1.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/davidmyersdev" - }, - "peerDependencies": { - "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/vite-plugin-wasm": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.5.0.tgz", - "integrity": "sha512-X5VWgCnqiQEGb+omhlBVsvTfxikKtoOgAzQ95+BZ8gQ+VfMHIjSHr0wyvXFQCa0eKQ0fKyaL0kWcEnYqBac4lQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wif": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/wif/-/wif-5.0.0.tgz", - "integrity": "sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA==", - "license": "MIT", - "dependencies": { - "bs58check": "^4.0.0" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xrpl": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-4.4.3.tgz", - "integrity": "sha512-vi2OjuNkiaP8nv1j+nqHp8GZwwEjO6Y8+j/OuVMg6M4LwXEwyHdIj33dlg7cyY1Lw5+jb9HqFOQvABhaywVbTQ==", - "license": "ISC", - "dependencies": { - "@scure/bip32": "^1.3.1", - "@scure/bip39": "^1.2.1", - "@xrplf/isomorphic": "^1.0.1", - "@xrplf/secret-numbers": "^2.0.0", - "bignumber.js": "^9.0.0", - "eventemitter3": "^5.0.1", - "fast-json-stable-stringify": "^2.1.0", - "ripple-address-codec": "^5.0.0", - "ripple-binary-codec": "^2.5.0", - "ripple-keypairs": "^2.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/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/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "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", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, - "packages/fungible_allowlist_example": { - "version": "0.0.0", - "extraneous": true, - "dependencies": { - "@stellar/stellar-sdk": "^14.1.1", - "buffer": "6.0.3" - }, - "devDependencies": { - "typescript": "^5.6.2" - } - }, - "packages/fungible_token_interface_example": { - "version": "0.0.0", - "extraneous": true, - "dependencies": { - "@stellar/stellar-sdk": "^14.0.0-rc.3", - "buffer": "6.0.3" - }, - "devDependencies": { - "typescript": "^5.6.2" - } - }, - "packages/guess_the_number": { - "version": "0.0.0", - "extraneous": true, - "dependencies": { - "@stellar/stellar-sdk": "^14.1.1", - "buffer": "6.0.3" - }, - "devDependencies": { - "typescript": "^5.6.2" - } - }, - "packages/nft_enumerable_example": { - "version": "0.0.0", - "extraneous": true, - "dependencies": { - "@stellar/stellar-sdk": "^14.1.1", - "buffer": "6.0.3" - }, - "devDependencies": { - "typescript": "^5.6.2" - } - }, - "packages/stellar_hello_world_contract": { - "version": "0.0.0", - "extraneous": true, - "dependencies": { - "@stellar/stellar-sdk": "^14.0.0-rc.3", - "buffer": "6.0.3" - }, - "devDependencies": { - "typescript": "^5.6.2" - } - } - } + "name": "learnvault-frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "learnvault-frontend", + "version": "0.0.1", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@creit.tech/stellar-wallets-kit": "^2.0.1", + "@stellar/design-system": "^3.2.7", + "@stellar/stellar-sdk": "^14.4.3", + "@stellar/stellar-xdr-json": "^23.0.0", + "@tailwindcss/vite": "^4.2.2", + "@tanstack/react-query": "^5.90.17", + "@theahaco/contract-explorer": "^1.1.0", + "@theahaco/ts-config": "^1.2.0", + "canvas-confetti": "^1.9.4", + "date-fns": "^4.1.0", + "framer-motion": "^12.38.0", + "i18next": "^25.10.5", + "i18next-browser-languagedetector": "^8.2.1", + "lossless-json": "^4.3.0", + "lucide-react": "^1.7.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-helmet": "^6.1.0", + "react-i18next": "^16.6.2", + "react-is": "^19.2.4", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.13.2", + "recharts": "^3.8.0", + "resend": "^6.9.4", + "sonner": "^2.0.7", + "tailwindcss": "^4.2.2", + "utf-8-validate": "^5.0.10", + "zod": "^4.3.5" + }, + "devDependencies": { + "@playwright/test": "^1.55.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/canvas-confetti": "^1.9.0", + "@types/lodash": "^4.17.23", + "@types/react": "^19.2.10", + "@types/react-dom": "^19.2.3", + "@types/react-helmet": "^6.1.11", + "@types/react-router-dom": "^5.3.3", + "@vitejs/plugin-react": "^5.2.0", + "@vitest/coverage-v8": "^4.1.1", + "concurrently": "^9.2.1", + "dotenv": "^17.2.3", + "eslint": "^9.39.2", + "glob": "^13.0.0", + "globals": "^17.0.0", + "husky": "^9.1.7", + "jsdom": "^29.0.1", + "lint-staged": "^16.2.7", + "prettier": "^3.8.0", + "typescript": "~5.9.3", + "vite": "^8.0.3", + "vite-plugin-node-polyfills": "^0.26.0", + "vite-plugin-wasm": "^3.5.0", + "vitest": "^4.1.1" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@albedo-link/intent": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@albedo-link/intent/-/intent-0.12.0.tgz", + "integrity": "sha512-UlGBhi0qASDYOjLrOL4484vQ26Ee3zTK2oAgvPMClOs+1XNk3zbs3dECKZv+wqeSI8SkHow8mXLTa16eVh+dQA==", + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@base-org/account": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@base-org/account/-/account-2.4.0.tgz", + "integrity": "sha512-A4Umpi8B9/pqR78D1Yoze4xHyQaujioVRqqO3d6xuDFw9VRtjg6tK3bPlwE0aW+nVH/ntllCpPa2PbI8Rnjcug==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@coinbase/cdp-sdk": "^1.0.0", + "@noble/hashes": "1.4.0", + "clsx": "1.2.1", + "eventemitter3": "5.0.1", + "idb-keyval": "6.2.1", + "ox": "0.6.9", + "preact": "10.24.2", + "viem": "^2.31.7", + "zustand": "5.0.3" + } + }, + "node_modules/@base-org/account/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@base-org/account/node_modules/preact": { + "version": "10.24.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz", + "integrity": "sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==", + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@coinbase/cdp-sdk": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/@coinbase/cdp-sdk/-/cdp-sdk-1.48.2.tgz", + "integrity": "sha512-phsHxF9q4CvF8H1b//aepxy8J/pdORT+btdqv7wbQ1YOi44QYfenima15N8Ok9lZE/XqY81BebsaBkyjqBJgig==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana-program/system": "^0.10.0", + "@solana-program/token": "^0.9.0", + "@solana/kit": "^5.5.1", + "abitype": "1.0.6", + "axios": "1.13.6", + "axios-retry": "^4.5.0", + "jose": "^6.2.0", + "md5": "^2.3.0", + "uncrypto": "^0.1.3", + "viem": "^2.47.0", + "zod": "^3.25.76" + } + }, + "node_modules/@coinbase/cdp-sdk/node_modules/abitype": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.6.tgz", + "integrity": "sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@coinbase/cdp-sdk/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@creit.tech/stellar-wallets-kit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@creit.tech/stellar-wallets-kit/-/stellar-wallets-kit-2.1.0.tgz", + "integrity": "sha512-DYOJXoH/SAE74prb4DqVvxzGfSBHaRdFa9Dz9BpnXQJPB63CJejcAcgyILurQcBwitRmFQd1bPg/9h+WiRgb/w==", + "license": "MIT", + "dependencies": { + "@albedo-link/intent": "0.12.0", + "@creit.tech/xbull-wallet-connect": "0.4.0", + "@hot-wallet/sdk": "1.0.11", + "@ledgerhq/hw-app-str": "7.2.8", + "@ledgerhq/hw-transport": "6.31.12", + "@ledgerhq/hw-transport-webusb": "6.29.12", + "@lobstrco/signer-extension-api": "2.0.0", + "@preact/signals": "2.9.0", + "@reown/appkit": "1.8.19", + "@stellar/freighter-api": "6.0.0", + "@stellar/stellar-base": "14.0.1", + "@trezor/connect-plugin-stellar": "9.2.6", + "@trezor/connect-web": "9.7.2", + "@twind/core": "1.1.3", + "@twind/preset-autoprefix": "1.0.7", + "@twind/preset-tailwind": "1.1.4", + "@walletconnect/sign-client": "2.23.0", + "@walletconnect/types": "2.23.9", + "htm": "3.1.1", + "preact": "^10.29.0" + } + }, + "node_modules/@creit.tech/stellar-wallets-kit/node_modules/@trezor/connect-plugin-stellar": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@trezor/connect-plugin-stellar/-/connect-plugin-stellar-9.2.6.tgz", + "integrity": "sha512-RA0Q4GHaf1mFxgSX183yyH+5tWEgS1j+sfe9KiUyIn/VnpXeUnaCpQuKMomAjGXQ5oNuvikTOdHYOqsvuEdOyw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@trezor/utils": "9.5.0" + }, + "peerDependencies": { + "@stellar/stellar-sdk": "^13.3.0", + "@trezor/connect": "9.x.x", + "tslib": "^2.6.2" + } + }, + "node_modules/@creit.tech/stellar-wallets-kit/node_modules/@twind/core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@twind/core/-/core-1.1.3.tgz", + "integrity": "sha512-/B/aNFerMb2IeyjSJy3SJxqVxhrT77gBDknLMiZqXIRr4vNJqiuhx7KqUSRzDCwUmyGuogkamz+aOLzN6MeSLw==", + "funding": [ + { + "type": "Open Collective", + "url": "https://opencollective.com/twind" + }, + { + "type": "Github Sponsor", + "url": "https://github.com/sponsors/tw-in-js" + }, + { + "type": "Ko-fi", + "url": "https://ko-fi.com/twind" + } + ], + "license": "MIT", + "dependencies": { + "csstype": "^3.1.1" + }, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "typescript": "^4.8.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@creit.tech/stellar-wallets-kit/node_modules/@twind/preset-autoprefix": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@twind/preset-autoprefix/-/preset-autoprefix-1.0.7.tgz", + "integrity": "sha512-3wmHO0pG/CVxYBNZUV0tWcL7CP0wD5KpyWAQE/KOalWmOVBj+nH6j3v6Y3I3pRuMFaG5DC78qbYbhA1O11uG3w==", + "funding": [ + { + "type": "Open Collective", + "url": "https://opencollective.com/twind" + }, + { + "type": "Github Sponsor", + "url": "https://github.com/sponsors/tw-in-js" + }, + { + "type": "Ko-fi", + "url": "https://ko-fi.com/twind" + } + ], + "license": "MIT", + "dependencies": { + "style-vendorizer": "^2.2.3" + }, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "@twind/core": "^1.1.0", + "typescript": "^4.8.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@creit.tech/stellar-wallets-kit/node_modules/@twind/preset-tailwind": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@twind/preset-tailwind/-/preset-tailwind-1.1.4.tgz", + "integrity": "sha512-zv85wrP/DW4AxgWrLfH7kyGn/KJF3K04FMLVl2AjoxZGYdCaoZDkL8ma3hzaKQ+WGgBFRubuB/Ku2Rtv/wjzVw==", + "funding": [ + { + "type": "Open Collective", + "url": "https://opencollective.com/twind" + }, + { + "type": "Github Sponsor", + "url": "https://github.com/sponsors/tw-in-js" + }, + { + "type": "Ko-fi", + "url": "https://ko-fi.com/twind" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "@twind/core": "^1.1.0", + "typescript": "^4.8.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@creit.tech/xbull-wallet-connect": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@creit.tech/xbull-wallet-connect/-/xbull-wallet-connect-0.4.0.tgz", + "integrity": "sha512-LrCUIqUz50SkZ4mv2hTqSmwews8CNRYVoZ9+VjLsK/1U8PByzXTxv1vZyenj6avRTG86ifpoeihz7D3D5YIDrQ==", + "dependencies": { + "rxjs": "^7.5.5", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "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==", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emurgo/cardano-serialization-lib-browser": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-browser/-/cardano-serialization-lib-browser-13.2.1.tgz", + "integrity": "sha512-7RfX1gI16Vj2DgCp/ZoXqyLAakWo6+X95ku/rYGbVzuS/1etrlSiJmdbmdm+eYmszMlGQjrtOJQeVLXoj4L/Ag==", + "license": "MIT" + }, + "node_modules/@emurgo/cardano-serialization-lib-nodejs": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-nodejs/-/cardano-serialization-lib-nodejs-13.2.0.tgz", + "integrity": "sha512-Bz1zLGEqBQ0BVkqt1OgMxdBOE3BdUWUd7Ly9Ecr/aUwkA8AV1w1XzBMe4xblmJHnB1XXNlPH4SraXCvO+q0Mig==", + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@ethereumjs/common": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-10.1.1.tgz", + "integrity": "sha512-NefPzPlrJ9w+NWVe06P+sHZQU98E1AEU9vhiHJEVT2wEcNBC1YX6hON9+smrfbn86C4U1pb2zbvjhkF+n/LKBw==", + "license": "MIT", + "dependencies": { + "@ethereumjs/util": "^10.1.1", + "eventemitter3": "^5.0.1" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-10.1.1.tgz", + "integrity": "sha512-jbnWTEwcpoY+gE0r+wxfDG9zgiu54DcTcwnc9sX3DsqKR4l5K7x2V8mQL3Et6hURa4DuT9g7z6ukwpBLFchszg==", + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp.cjs" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@ethereumjs/tx": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-10.1.1.tgz", + "integrity": "sha512-Kz8GWIKQjEQB60ko9hsYDX3rZMHZZOTcmm6OFl855Lu3padVnf5ZactUKM6nmWPsumHED5bWDjO32novZd1zyw==", + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/common": "^10.1.1", + "@ethereumjs/rlp": "^10.1.1", + "@ethereumjs/util": "^10.1.1", + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@ethereumjs/tx/node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/tx/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-10.1.1.tgz", + "integrity": "sha512-r2EhaeEmLZXVs1dT2HJFQysAkr63ZWATu/9tgYSp1IlvjvwyC++DLg5kCDwMM49HBq3sOAhrPnXkoqf9DV2gbw==", + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/rlp": "^10.1.1", + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@fivebinaries/coin-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fivebinaries/coin-selection/-/coin-selection-3.0.0.tgz", + "integrity": "sha512-h25Pn1ZA7oqQBQDodGAgIsQt66T2wDge9onBKNqE66WNWL0KJiKJbpij8YOLo5AAlEIg5IS7EB1QjBgDOIg6DQ==", + "license": "Apache-2.0", + "dependencies": { + "@emurgo/cardano-serialization-lib-browser": "^13.2.0", + "@emurgo/cardano-serialization-lib-nodejs": "13.2.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@hot-wallet/sdk": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@hot-wallet/sdk/-/sdk-1.0.11.tgz", + "integrity": "sha512-qRDH/4yqnRCnk7L/Qd0/LDOKDUKWcFgvf6eRELJkP0OgxIe65i/iXaG+u2lL0mLbTGkiWYk67uAvEerNUv2gzA==", + "dependencies": { + "@near-js/crypto": "^1.4.0", + "@near-js/utils": "^1.0.0", + "@near-wallet-selector/core": "^8.9.13", + "@solana/wallet-adapter-base": "^0.9.23", + "@solana/web3.js": "^1.95.0", + "borsh": "^2.0.0", + "js-sha256": "^0.11.0", + "sha1": "^1.1.1", + "uuid4": "^2.0.3" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ledgerhq/devices": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.6.1.tgz", + "integrity": "sha512-PQR2fyWz7P/wMFHY9ZLz17WgFdxC/Im0RVDcWXpp24+iRQRyxhQeX2iG4mBKUzfaAW6pOIEiWt+vmJh88QP9rQ==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/errors": "^6.26.0", + "@ledgerhq/logs": "^6.13.0", + "rxjs": "^7.8.1", + "semver": "^7.3.5" + } + }, + "node_modules/@ledgerhq/errors": { + "version": "6.34.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.34.0.tgz", + "integrity": "sha512-l16K56FzPoXBMT5J4EpnIBmRLTYkpSyYj3z4er+rmbwq0t9dDG/UaRvFeBpwB2gqGcYWcue14qF9Wkuos/43eA==", + "license": "Apache-2.0" + }, + "node_modules/@ledgerhq/hw-app-str": { + "version": "7.2.8", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-app-str/-/hw-app-str-7.2.8.tgz", + "integrity": "sha512-VHICY9jyZW5LM/8zc/mSbW7fS2bAC1OTVOtRwdQLEDn6Gv9UaNcCWjaHI1UKAnDUqYX7DUQuIPiTP1b4O+mtUQ==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/errors": "^6.26.0", + "@ledgerhq/hw-transport": "^6.31.12", + "bip32-path": "^0.4.2" + } + }, + "node_modules/@ledgerhq/hw-transport": { + "version": "6.31.12", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.31.12.tgz", + "integrity": "sha512-FO5LRIXYC8ELtaohlO8qK0b3TfHUNBZ3+CXKPHiHj2jJwrxPf4s5kcgBYrmzuf1C/1vfrMOjzyty6OgrMIbU6Q==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.6.1", + "@ledgerhq/errors": "^6.26.0", + "@ledgerhq/logs": "^6.13.0", + "events": "^3.3.0" + } + }, + "node_modules/@ledgerhq/hw-transport-webusb": { + "version": "6.29.12", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.29.12.tgz", + "integrity": "sha512-mMGKPYAUz9MNcURe+hSTSHwqPwCli6D0lCl15Z4hDOpcqhZ26vwoeWVKeQp53NNCetHOl0lauPkN43Gt9pIggg==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.6.1", + "@ledgerhq/errors": "^6.26.0", + "@ledgerhq/hw-transport": "^6.31.12", + "@ledgerhq/logs": "^6.13.0" + } + }, + "node_modules/@ledgerhq/logs": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.17.0.tgz", + "integrity": "sha512-yra33g5q/AU7+PwAws+GaVpQGUuxnDREjVBnviJjcaJLVKuLzI4pnj8Bd3nY3fypM5k1yZEYKEXfUuGFUjP2+w==", + "license": "Apache-2.0" + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/react": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.8.tgz", + "integrity": "sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw==", + "license": "BSD-3-Clause", + "optional": true, + "peerDependencies": { + "@types/react": "17 || 18 || 19" + } + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@lobstrco/signer-extension-api": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@lobstrco/signer-extension-api/-/signer-extension-api-2.0.0.tgz", + "integrity": "sha512-jwlVyzMFF296iaNgMWn1lu+EU6BeUD4mgPurEsy8EygYNCrjA8igLpsDlXhfvXhst9tX5w4wRuTDX+FZtpfCug==", + "license": "GPL-3.0" + }, + "node_modules/@mobily/ts-belt": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@mobily/ts-belt/-/ts-belt-3.13.1.tgz", + "integrity": "sha512-K5KqIhPI/EoCTbA6CGbrenM9s41OouyK8A03fGJJcla/zKucsgLbz8HNbeseoLarRPgyWJsUyCYqFhI7t3Ra9Q==", + "license": "MIT", + "engines": { + "node": ">= 10.*" + } + }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.2.tgz", + "integrity": "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@near-js/crypto": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@near-js/crypto/-/crypto-1.4.2.tgz", + "integrity": "sha512-GRfchsyfWvSAPA1gI9hYhw5FH94Ac1BUo+Cmp5rSJt/V0K3xVzCWgOQxvv4R3kDnWjaXJEuAmpEEnr4Bp3FWrA==", + "license": "ISC", + "dependencies": { + "@near-js/types": "0.3.1", + "@near-js/utils": "1.1.0", + "@noble/curves": "1.8.1", + "borsh": "1.0.0", + "randombytes": "2.1.0", + "secp256k1": "5.0.1" + } + }, + "node_modules/@near-js/crypto/node_modules/borsh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", + "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", + "license": "Apache-2.0" + }, + "node_modules/@near-js/types": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@near-js/types/-/types-0.3.1.tgz", + "integrity": "sha512-8qIA7ynAEAuVFNAQc0cqz2xRbfyJH3PaAG5J2MgPPhD18lu/tCGd6pzYg45hjhtiJJRFDRjh/FUWKS+ZiIIxUw==", + "license": "ISC" + }, + "node_modules/@near-js/utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@near-js/utils/-/utils-1.1.0.tgz", + "integrity": "sha512-5XWRq7xpu8Wud9pRXe2U347KXyi0mXofedUY2DQ9TaqiZUcMIaN9xj7DbCs2v6dws3pJyYrT1KWxeNp5fSaY3w==", + "license": "ISC", + "dependencies": { + "@near-js/types": "0.3.1", + "@scure/base": "^1.2.0", + "depd": "2.0.0", + "mustache": "4.0.0" + } + }, + "node_modules/@near-wallet-selector/core": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/@near-wallet-selector/core/-/core-8.10.2.tgz", + "integrity": "sha512-MH8sg6XHyylq2ZXxnOjrKHMCmuRgFfpfdC816fW0R8hctZiXZ0lmfLvgG1xfA2BAxrVytiU1g3dcE97/P5cZqg==", + "dependencies": { + "borsh": "1.0.0", + "events": "3.3.0", + "js-sha256": "0.9.0", + "rxjs": "7.8.1" + }, + "peerDependencies": { + "near-api-js": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/@near-wallet-selector/core/node_modules/borsh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", + "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", + "license": "Apache-2.0" + }, + "node_modules/@near-wallet-selector/core/node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==", + "license": "MIT" + }, + "node_modules/@near-wallet-selector/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.1" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@package-json/types": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@package-json/types/-/types-0.0.12.tgz", + "integrity": "sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==", + "license": "MIT" + }, + "node_modules/@phosphor-icons/webcomponents": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@phosphor-icons/webcomponents/-/webcomponents-2.1.5.tgz", + "integrity": "sha512-JcvQkZxvcX2jK+QCclm8+e8HXqtdFW9xV4/kk2aL9Y3dJA2oQVt+pzbv1orkumz3rfx4K9mn9fDoMr1He1yr7Q==", + "license": "MIT", + "dependencies": { + "lit": "^3" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@preact/signals": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-2.9.0.tgz", + "integrity": "sha512-hYrY0KyUqkDgOl1qba/JGn6y81pXnurn21PMaxfcMwdncdZ3M/oVdmpTvEnsGjh48dIwDVc7bjWHqIsngSjYug==", + "license": "MIT", + "dependencies": { + "@preact/signals-core": "^1.14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact": ">= 10.25.0 || >=11.0.0-0" + } + }, + "node_modules/@preact/signals-core": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.1.tgz", + "integrity": "sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@reown/appkit": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit/-/appkit-1.8.19.tgz", + "integrity": "sha512-wB+xatkRbOy0AY1cZxxtcKzzPk3l3CTFulDbaISLVmZI6ZnQrOFuLnYc285zGsC6DB4d6bmwYUh89zcMLa4PvQ==", + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.19", + "@reown/appkit-controllers": "1.8.19", + "@reown/appkit-pay": "1.8.19", + "@reown/appkit-polyfills": "1.8.19", + "@reown/appkit-scaffold-ui": "1.8.19", + "@reown/appkit-ui": "1.8.19", + "@reown/appkit-utils": "1.8.19", + "@reown/appkit-wallet": "1.8.19", + "@walletconnect/universal-provider": "2.23.7", + "bs58": "6.0.0", + "semver": "7.7.2", + "valtio": "2.1.7", + "viem": ">=2.45.0" + }, + "optionalDependencies": { + "@lit/react": "1.0.8" + } + }, + "node_modules/@reown/appkit-common": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-common/-/appkit-common-1.8.19.tgz", + "integrity": "sha512-z5wDrYjUGY7YbM4b14NHVo54WKZ5++PQtGkcsXhiOP39yAVijubBQD8BfHs/Pu2fSFqnqLIFoCVvIEfNWWccRw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "big.js": "6.2.2", + "dayjs": "1.11.13", + "viem": ">=2.45.0" + } + }, + "node_modules/@reown/appkit-controllers": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.8.19.tgz", + "integrity": "sha512-JFNT8CfAVit9FJXh596Ye4U8A/oIapW+Y0KQqjB59DXyTCDZbxZDB32rULBQrSkZ6PufTEa239Dil4kABCQKtg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.19", + "@reown/appkit-wallet": "1.8.19", + "@walletconnect/universal-provider": "2.23.7", + "valtio": "2.1.7", + "viem": ">=2.45.0" + } + }, + "node_modules/@reown/appkit-pay": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.8.19.tgz", + "integrity": "sha512-HO/tQT0TbTQO3eONxNNPJAOZAOzUiHvjM0Mty1rFFeRBH68auiqQxQi2YFNMs014gNkRN+cb84VYau7+MCC0fQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.19", + "@reown/appkit-controllers": "1.8.19", + "@reown/appkit-ui": "1.8.19", + "@reown/appkit-utils": "1.8.19", + "lit": "3.3.0", + "valtio": "2.1.7" + } + }, + "node_modules/@reown/appkit-polyfills": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.8.19.tgz", + "integrity": "sha512-PSoetRSuZg7f2YFPzdfs4BayQl51zcGqYr7frwOe6td0XEsspLrrVFn/zk5QFbFHZVsMdfRZ+TTunt84ozRdnQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "buffer": "6.0.3" + } + }, + "node_modules/@reown/appkit-scaffold-ui": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.8.19.tgz", + "integrity": "sha512-Ak767x0VzeDIXb0wbzkl19kx6udw7vkb1EU0SAweG3iKc9BunW87Rfcd48/YimzMZycJaYmlbtfmqQQDYs6Few==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.19", + "@reown/appkit-controllers": "1.8.19", + "@reown/appkit-pay": "1.8.19", + "@reown/appkit-ui": "1.8.19", + "@reown/appkit-utils": "1.8.19", + "@reown/appkit-wallet": "1.8.19", + "lit": "3.3.0" + } + }, + "node_modules/@reown/appkit-ui": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.8.19.tgz", + "integrity": "sha512-fCAwW8yyyC3JcgKLBPvCtYuDGC4H8anO7u4LTaAXGEzdcU5H+IrCgNFSPNK7NuTSmgXm1TnoYxPxRFKNiNwFdA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@phosphor-icons/webcomponents": "2.1.5", + "@reown/appkit-common": "1.8.19", + "@reown/appkit-controllers": "1.8.19", + "@reown/appkit-wallet": "1.8.19", + "lit": "3.3.0", + "qrcode": "1.5.3" + } + }, + "node_modules/@reown/appkit-utils": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.8.19.tgz", + "integrity": "sha512-VQPgUMTFqoh4UD3EDZSw9wyMkyZsmIVmu8CdQ2FUxIuqYW4fLd0VIpkDeO64MMhSv8b0X8Vd6m4+eGcqSwlUAg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.19", + "@reown/appkit-controllers": "1.8.19", + "@reown/appkit-polyfills": "1.8.19", + "@reown/appkit-wallet": "1.8.19", + "@wallet-standard/wallet": "1.1.0", + "@walletconnect/logger": "3.0.2", + "@walletconnect/universal-provider": "2.23.7", + "valtio": "2.1.7", + "viem": ">=2.45.0" + }, + "optionalDependencies": { + "@base-org/account": "2.4.0", + "@safe-global/safe-apps-provider": "0.18.6", + "@safe-global/safe-apps-sdk": "9.1.0" + }, + "peerDependencies": { + "valtio": "2.1.7" + } + }, + "node_modules/@reown/appkit-wallet": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.8.19.tgz", + "integrity": "sha512-NVdIKceUhkXYtsG32925ctmVn0QJFNyDlr+mWheMLCEZ/IUPn+6aA53vTVaSUquhyeFxUXtrCOh3ln6v1tup5w==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@reown/appkit-common": "1.8.19", + "@reown/appkit-polyfills": "1.8.19", + "@walletconnect/logger": "3.0.2", + "zod": "3.22.4" + } + }, + "node_modules/@reown/appkit-wallet/node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@reown/appkit/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.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", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-inject": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", + "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-inject/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@safe-global/safe-apps-provider": { + "version": "0.18.6", + "resolved": "https://registry.npmjs.org/@safe-global/safe-apps-provider/-/safe-apps-provider-0.18.6.tgz", + "integrity": "sha512-4LhMmjPWlIO8TTDC2AwLk44XKXaK6hfBTWyljDm0HQ6TWlOEijVWNrt2s3OCVMSxlXAcEzYfqyu1daHZooTC2Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@safe-global/safe-apps-sdk": "^9.1.0", + "events": "^3.3.0" + } + }, + "node_modules/@safe-global/safe-apps-sdk": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@safe-global/safe-apps-sdk/-/safe-apps-sdk-9.1.0.tgz", + "integrity": "sha512-N5p/ulfnnA2Pi2M3YeWjULeWbjo7ei22JwU/IXnhoHzKq3pYCN6ynL9mJBOlvDVv892EgLPCWCOwQk/uBT2v0Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@safe-global/safe-gateway-typescript-sdk": "^3.5.3", + "viem": "^2.1.1" + } + }, + "node_modules/@safe-global/safe-gateway-typescript-sdk": { + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.23.1.tgz", + "integrity": "sha512-6ORQfwtEJYpalCeVO21L4XXGSdbEMfyp2hEv6cP82afKXSwvse6d3sdelgaPWUxHIsFRkWvHDdzh8IyyKHZKxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.33.22", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.33.22.tgz", + "integrity": "sha512-auUj4k+f4pyrIVf4GW5UKquSZFHJWri06QgARy9C0t9ZTjJLIuNIrr1yl9bWcJWJ1Gz1vOvYN1D+QPaIlNMVkQ==", + "license": "MIT" + }, + "node_modules/@solana-program/system": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@solana-program/system/-/system-0.10.0.tgz", + "integrity": "sha512-Go+LOEZmqmNlfr+Gjy5ZWAdY5HbYzk2RBewD9QinEU/bBSzpFfzqDRT55JjFRBGJUvMgf3C2vfXEGT4i8DSI4g==", + "license": "Apache-2.0", + "optional": true, + "peerDependencies": { + "@solana/kit": "^5.0" + } + }, + "node_modules/@solana-program/token": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@solana-program/token/-/token-0.9.0.tgz", + "integrity": "sha512-vnZxndd4ED4Fc56sw93cWZ2djEeeOFxtaPS8SPf5+a+JZjKA/EnKqzbE1y04FuMhIVrLERQ8uR8H2h72eZzlsA==", + "license": "Apache-2.0", + "optional": true, + "peerDependencies": { + "@solana/kit": "^5.0" + } + }, + "node_modules/@solana/accounts": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/accounts/-/accounts-5.5.1.tgz", + "integrity": "sha512-TfOY9xixg5rizABuLVuZ9XI2x2tmWUC/OoN556xwfDlhBHBjKfszicYYOyD6nbFmwTGYarCmyGIdteXxTXIdhQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/rpc-spec": "5.5.1", + "@solana/rpc-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/addresses": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/addresses/-/addresses-5.5.1.tgz", + "integrity": "sha512-5xoah3Q9G30HQghu/9BiHLb5pzlPKRC3zydQDmE3O9H//WfayxTFppsUDCL6FjYUHqj/wzK6CWHySglc2RkpdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/assertions": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/nominal-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/assertions": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/assertions/-/assertions-5.5.1.tgz", + "integrity": "sha512-YTCSWAlGwSlVPnWtWLm3ukz81wH4j2YaCveK+TjpvUU88hTy6fmUqxi0+hvAMAe4zKXpJyj3Az7BrLJRxbIm4Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "license": "MIT", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/codecs": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-5.5.1.tgz", + "integrity": "sha512-Vea29nJub/bXjfzEV7ZZQ/PWr1pYLZo3z0qW0LQL37uKKVzVFRQlwetd7INk3YtTD3xm9WUYr7bCvYUk3uKy2g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/codecs-data-structures": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/options": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/codecs-core": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-5.5.1.tgz", + "integrity": "sha512-TgBt//bbKBct0t6/MpA8ElaOA3sa8eYVvR7LGslCZ84WiAwwjCY0lW/lOYsFHJQzwREMdUyuEyy5YWBKtdh8Rw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/codecs-data-structures": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-5.5.1.tgz", + "integrity": "sha512-97bJWGyUY9WvBz3mX1UV3YPWGDTez6btCfD0ip3UVEXJbItVuUiOkzcO5iFDUtQT5riKT6xC+Mzl+0nO76gd0w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-5.5.1.tgz", + "integrity": "sha512-rllMIZAHqmtvC0HO/dc/21wDuWaD0B8Ryv8o+YtsICQBuiL/0U4AGwH7Pi5GNFySYk0/crSuwfIqQFtmxNSPFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/codecs-strings": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-5.5.1.tgz", + "integrity": "sha512-7klX4AhfHYA+uKKC/nxRGP2MntbYQCR3N6+v7bk1W/rSxYuhNmt+FN8aoThSZtWIKwN6BEyR1167ka8Co1+E7A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "fastestsmallesttextencoderdecoder": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/errors": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-5.5.1.tgz", + "integrity": "sha512-vFO3p+S7HoyyrcAectnXbdsMfwUzY2zYFUc2DEe5BwpiE9J1IAxPBGjOWO6hL1bbYdBrlmjNx8DXCslqS+Kcmg==", + "license": "MIT", + "optional": true, + "dependencies": { + "chalk": "5.6.2", + "commander": "14.0.2" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/fast-stable-stringify": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/fast-stable-stringify/-/fast-stable-stringify-5.5.1.tgz", + "integrity": "sha512-Ni7s2FN33zTzhTFgRjEbOVFO+UAmK8qi3Iu0/GRFYK4jN696OjKHnboSQH/EacQ+yGqS54bfxf409wU5dsLLCw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/functional": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/functional/-/functional-5.5.1.tgz", + "integrity": "sha512-tTHoJcEQq3gQx5qsdsDJ0LEJeFzwNpXD80xApW9o/PPoCNimI3SALkZl+zNW8VnxRrV3l3yYvfHWBKe/X3WG3w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/instruction-plans": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/instruction-plans/-/instruction-plans-5.5.1.tgz", + "integrity": "sha512-7z3CB7YMcFKuVvgcnNY8bY6IsZ8LG61Iytbz7HpNVGX2u1RthOs1tRW8luTzSG1MPL0Ox7afyAVMYeFqSPHnaQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/instructions": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/promises": "5.5.1", + "@solana/transaction-messages": "5.5.1", + "@solana/transactions": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/instructions": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/instructions/-/instructions-5.5.1.tgz", + "integrity": "sha512-h0G1CG6S+gUUSt0eo6rOtsaXRBwCq1+Js2a+Ps9Bzk9q7YHNFA75/X0NWugWLgC92waRp66hrjMTiYYnLBoWOQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/keys": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/keys/-/keys-5.5.1.tgz", + "integrity": "sha512-KRD61cL7CRL+b4r/eB9dEoVxIf/2EJ1Pm1DmRYhtSUAJD2dJ5Xw8QFuehobOGm9URqQ7gaQl+Fkc1qvDlsWqKg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/assertions": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/nominal-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/kit": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/kit/-/kit-5.5.1.tgz", + "integrity": "sha512-irKUGiV2yRoyf+4eGQ/ZeCRxa43yjFEL1DUI5B0DkcfZw3cr0VJtVJnrG8OtVF01vT0OUfYOcUn6zJW5TROHvQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/accounts": "5.5.1", + "@solana/addresses": "5.5.1", + "@solana/codecs": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/instruction-plans": "5.5.1", + "@solana/instructions": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/offchain-messages": "5.5.1", + "@solana/plugin-core": "5.5.1", + "@solana/programs": "5.5.1", + "@solana/rpc": "5.5.1", + "@solana/rpc-api": "5.5.1", + "@solana/rpc-parsed-types": "5.5.1", + "@solana/rpc-spec-types": "5.5.1", + "@solana/rpc-subscriptions": "5.5.1", + "@solana/rpc-types": "5.5.1", + "@solana/signers": "5.5.1", + "@solana/sysvars": "5.5.1", + "@solana/transaction-confirmation": "5.5.1", + "@solana/transaction-messages": "5.5.1", + "@solana/transactions": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/nominal-types": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/nominal-types/-/nominal-types-5.5.1.tgz", + "integrity": "sha512-I1ImR+kfrLFxN5z22UDiTWLdRZeKtU0J/pkWkO8qm/8WxveiwdIv4hooi8pb6JnlR4mSrWhq0pCIOxDYrL9GIQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/offchain-messages": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/offchain-messages/-/offchain-messages-5.5.1.tgz", + "integrity": "sha512-g+xHH95prTU+KujtbOzj8wn+C7ZNoiLhf3hj6nYq3MTyxOXtBEysguc97jJveUZG0K97aIKG6xVUlMutg5yxhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-data-structures": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/nominal-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/options": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/options/-/options-5.5.1.tgz", + "integrity": "sha512-eo971c9iLNLmk+yOFyo7yKIJzJ/zou6uKpy6mBuyb/thKtS/haiKIc3VLhyTXty3OH2PW8yOlORJnv4DexJB8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/codecs-data-structures": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/plugin-core": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/plugin-core/-/plugin-core-5.5.1.tgz", + "integrity": "sha512-VUZl30lDQFJeiSyNfzU1EjYt2QZvoBFKEwjn1lilUJw7KgqD5z7mbV7diJhT+dLFs36i0OsjXvq5kSygn8YJ3A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/programs": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/programs/-/programs-5.5.1.tgz", + "integrity": "sha512-7U9kn0Jsx1NuBLn5HRTFYh78MV4XN145Yc3WP/q5BlqAVNlMoU9coG5IUTJIG847TUqC1lRto3Dnpwm6T4YRpA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/promises": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/promises/-/promises-5.5.1.tgz", + "integrity": "sha512-T9lfuUYkGykJmppEcssNiCf6yiYQxJkhiLPP+pyAc2z84/7r3UVIb2tNJk4A9sucS66pzJnVHZKcZVGUUp6wzA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc/-/rpc-5.5.1.tgz", + "integrity": "sha512-ku8zTUMrkCWci66PRIBC+1mXepEnZH/q1f3ck0kJZ95a06bOTl5KU7HeXWtskkyefzARJ5zvCs54AD5nxjQJ+A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/fast-stable-stringify": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/rpc-api": "5.5.1", + "@solana/rpc-spec": "5.5.1", + "@solana/rpc-spec-types": "5.5.1", + "@solana/rpc-transformers": "5.5.1", + "@solana/rpc-transport-http": "5.5.1", + "@solana/rpc-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-api": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-api/-/rpc-api-5.5.1.tgz", + "integrity": "sha512-XWOQQPhKl06Vj0xi3RYHAc6oEQd8B82okYJ04K7N0Vvy3J4PN2cxeK7klwkjgavdcN9EVkYCChm2ADAtnztKnA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/rpc-parsed-types": "5.5.1", + "@solana/rpc-spec": "5.5.1", + "@solana/rpc-transformers": "5.5.1", + "@solana/rpc-types": "5.5.1", + "@solana/transaction-messages": "5.5.1", + "@solana/transactions": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-parsed-types": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-parsed-types/-/rpc-parsed-types-5.5.1.tgz", + "integrity": "sha512-HEi3G2nZqGEsa3vX6U0FrXLaqnUCg4SKIUrOe8CezD+cSFbRTOn3rCLrUmJrhVyXlHoQVaRO9mmeovk31jWxJg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-spec": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-spec/-/rpc-spec-5.5.1.tgz", + "integrity": "sha512-m3LX2bChm3E3by4mQrH4YwCAFY57QBzuUSWqlUw7ChuZ+oLLOq7b2czi4i6L4Vna67j3eCmB3e+4tqy1j5wy7Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/rpc-spec-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-spec-types": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-spec-types/-/rpc-spec-types-5.5.1.tgz", + "integrity": "sha512-6OFKtRpIEJQs8Jb2C4OO8KyP2h2Hy1MFhatMAoXA+0Ik8S3H+CicIuMZvGZ91mIu/tXicuOOsNNLu3HAkrakrw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions/-/rpc-subscriptions-5.5.1.tgz", + "integrity": "sha512-CTMy5bt/6mDh4tc6vUJms9EcuZj3xvK0/xq8IQ90rhkpYvate91RjBP+egvjgSayUg9yucU9vNuUpEjz4spM7w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/fast-stable-stringify": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/promises": "5.5.1", + "@solana/rpc-spec-types": "5.5.1", + "@solana/rpc-subscriptions-api": "5.5.1", + "@solana/rpc-subscriptions-channel-websocket": "5.5.1", + "@solana/rpc-subscriptions-spec": "5.5.1", + "@solana/rpc-transformers": "5.5.1", + "@solana/rpc-types": "5.5.1", + "@solana/subscribable": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions-api": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-api/-/rpc-subscriptions-api-5.5.1.tgz", + "integrity": "sha512-5Oi7k+GdeS8xR2ly1iuSFkAv6CZqwG0Z6b1QZKbEgxadE1XGSDrhM2cn59l+bqCozUWCqh4c/A2znU/qQjROlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/rpc-subscriptions-spec": "5.5.1", + "@solana/rpc-transformers": "5.5.1", + "@solana/rpc-types": "5.5.1", + "@solana/transaction-messages": "5.5.1", + "@solana/transactions": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions-channel-websocket": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-channel-websocket/-/rpc-subscriptions-channel-websocket-5.5.1.tgz", + "integrity": "sha512-7tGfBBrYY8TrngOyxSHoCU5shy86iA9SRMRrPSyBhEaZRAk6dnbdpmUTez7gtdVo0BCvh9nzQtUycKWSS7PnFQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/rpc-subscriptions-spec": "5.5.1", + "@solana/subscribable": "5.5.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions-spec": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-spec/-/rpc-subscriptions-spec-5.5.1.tgz", + "integrity": "sha512-iq+rGq5fMKP3/mKHPNB6MC8IbVW41KGZg83Us/+LE3AWOTWV1WT20KT2iH1F1ik9roi42COv/TpoZZvhKj45XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/promises": "5.5.1", + "@solana/rpc-spec-types": "5.5.1", + "@solana/subscribable": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-transformers": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-transformers/-/rpc-transformers-5.5.1.tgz", + "integrity": "sha512-OsWqLCQdcrRJKvHiMmwFhp9noNZ4FARuMkHT5us3ustDLXaxOjF0gfqZLnMkulSLcKt7TGXqMhBV+HCo7z5M8Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/nominal-types": "5.5.1", + "@solana/rpc-spec-types": "5.5.1", + "@solana/rpc-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-transport-http": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-transport-http/-/rpc-transport-http-5.5.1.tgz", + "integrity": "sha512-yv8GoVSHqEV0kUJEIhkdOVkR2SvJ6yoWC51cJn2rSV7plr6huLGe0JgujCmB7uZhhaLbcbP3zxXxu9sOjsi7Fg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1", + "@solana/rpc-spec": "5.5.1", + "@solana/rpc-spec-types": "5.5.1", + "undici-types": "^7.19.2" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-types": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/rpc-types/-/rpc-types-5.5.1.tgz", + "integrity": "sha512-bibTFQ7PbHJJjGJPmfYC2I+/5CRFS4O2p9WwbFraX1Keeel+nRrt/NBXIy8veP5AEn2sVJIyJPpWBRpCx1oATA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/nominal-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/signers": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/signers/-/signers-5.5.1.tgz", + "integrity": "sha512-FY0IVaBT2kCAze55vEieR6hag4coqcuJ31Aw3hqRH7mv6sV8oqwuJmUrx+uFwOp1gwd5OEAzlv6N4hOOple4sQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/instructions": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/nominal-types": "5.5.1", + "@solana/offchain-messages": "5.5.1", + "@solana/transaction-messages": "5.5.1", + "@solana/transactions": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/subscribable": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/subscribable/-/subscribable-5.5.1.tgz", + "integrity": "sha512-9K0PsynFq0CsmK1CDi5Y2vUIJpCqkgSS5yfDN0eKPgHqEptLEaia09Kaxc90cSZDZU5mKY/zv1NBmB6Aro9zQQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/sysvars": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/sysvars/-/sysvars-5.5.1.tgz", + "integrity": "sha512-k3Quq87Mm+geGUu1GWv6knPk0ALsfY6EKSJGw9xUJDHzY/RkYSBnh0RiOrUhtFm2TDNjOailg8/m0VHmi3reFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/accounts": "5.5.1", + "@solana/codecs": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/rpc-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/transaction-confirmation": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/transaction-confirmation/-/transaction-confirmation-5.5.1.tgz", + "integrity": "sha512-j4mKlYPHEyu+OD7MBt3jRoX4ScFgkhZC6H65on4Fux6LMScgivPJlwnKoZMnsgxFgWds0pl+BYzSiALDsXlYtw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/promises": "5.5.1", + "@solana/rpc": "5.5.1", + "@solana/rpc-subscriptions": "5.5.1", + "@solana/rpc-types": "5.5.1", + "@solana/transaction-messages": "5.5.1", + "@solana/transactions": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/transaction-messages": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/transaction-messages/-/transaction-messages-5.5.1.tgz", + "integrity": "sha512-aXyhMCEaAp3M/4fP0akwBBQkFPr4pfwoC5CLDq999r/FUwDax2RE/h4Ic7h2Xk+JdcUwsb+rLq85Y52hq84XvQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-data-structures": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/instructions": "5.5.1", + "@solana/nominal-types": "5.5.1", + "@solana/rpc-types": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/transactions": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/transactions/-/transactions-5.5.1.tgz", + "integrity": "sha512-8hHtDxtqalZ157pnx6p8k10D7J/KY/biLzfgh9R09VNLLY3Fqi7kJvJCr7M2ik3oRll56pxhraAGCC9yIT6eOA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@solana/addresses": "5.5.1", + "@solana/codecs-core": "5.5.1", + "@solana/codecs-data-structures": "5.5.1", + "@solana/codecs-numbers": "5.5.1", + "@solana/codecs-strings": "5.5.1", + "@solana/errors": "5.5.1", + "@solana/functional": "5.5.1", + "@solana/instructions": "5.5.1", + "@solana/keys": "5.5.1", + "@solana/nominal-types": "5.5.1", + "@solana/rpc-types": "5.5.1", + "@solana/transaction-messages": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/wallet-adapter-base": { + "version": "0.9.27", + "resolved": "https://registry.npmjs.org/@solana/wallet-adapter-base/-/wallet-adapter-base-0.9.27.tgz", + "integrity": "sha512-kXjeNfNFVs/NE9GPmysBRKQ/nf+foSaq3kfVSeMcO/iVgigyRmB551OjU3WyAolLG/1jeEfKLqF9fKwMCRkUqg==", + "license": "Apache-2.0", + "dependencies": { + "@solana/wallet-standard-features": "^1.3.0", + "@wallet-standard/base": "^1.1.0", + "@wallet-standard/features": "^1.1.0", + "eventemitter3": "^5.0.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@solana/web3.js": "^1.98.0" + } + }, + "node_modules/@solana/wallet-standard-features": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@solana/wallet-standard-features/-/wallet-standard-features-1.3.0.tgz", + "integrity": "sha512-ZhpZtD+4VArf6RPitsVExvgkF+nGghd1rzPjd97GmBximpnt1rsUxMOEyoIEuH3XBxPyNB6Us7ha7RHWQR+abg==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.0", + "@wallet-standard/features": "^1.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@solana/web3.js": { + "version": "1.98.4", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", + "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^2.1.0", + "agentkeepalive": "^4.5.0", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@solana/web3.js/node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@stellar/design-system": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@stellar/design-system/-/design-system-3.2.8.tgz", + "integrity": "sha512-h2AaxQNjUl3DaaydJAYHKfqIuojBEjHNuxzSiQdCZXz9FEp2Ohm0evsgS4GsPu0wxqAFKzDFbqGM2AOJxkXyMw==", + "license": "Apache-2.0", + "dependencies": { + "@floating-ui/dom": "^1.7.4", + "bignumber.js": "^9.3.1", + "lodash": "^4.17.21", + "react-copy-to-clipboard-ts": "^1.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "react": ">=18.x", + "react-dom": ">=18.x" + } + }, + "node_modules/@stellar/freighter-api": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@stellar/freighter-api/-/freighter-api-6.0.0.tgz", + "integrity": "sha512-8CTQcKQmTq/wL715ZUzn1x1POpR0eYhYPKEiaeA7AT0WYBOauOGTxfWPFtSidX3ohAlJZP5HFXy1kG29cVjqxw==", + "license": "Apache-2.0", + "dependencies": { + "buffer": "6.0.3", + "semver": "7.7.1" + } + }, + "node_modules/@stellar/freighter-api/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@stellar/js-xdr": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", + "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", + "license": "Apache-2.0" + }, + "node_modules/@stellar/stellar-base": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-14.0.1.tgz", + "integrity": "sha512-mI6Kjh9hGWDA1APawQTtCbR7702dNT/8Te1uuRFPqqdoAKBk3WpXOQI3ZSZO+5olW7BSHpmVG5KBPZpIpQxIvw==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.9.6", + "@stellar/js-xdr": "^3.1.2", + "base32.js": "^0.1.0", + "bignumber.js": "^9.3.1", + "buffer": "^6.0.3", + "sha.js": "^2.4.12" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stellar/stellar-base/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@stellar/stellar-base/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@stellar/stellar-sdk": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-14.6.1.tgz", + "integrity": "sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==", + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-base": "^14.1.0", + "axios": "^1.13.3", + "bignumber.js": "^9.3.1", + "commander": "^14.0.2", + "eventsource": "^2.0.2", + "feaxios": "^0.0.23", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + }, + "bin": { + "stellar-js": "bin/stellar-js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stellar/stellar-sdk/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@stellar/stellar-sdk/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@stellar/stellar-sdk/node_modules/@stellar/stellar-base": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-14.1.0.tgz", + "integrity": "sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.9.6", + "@stellar/js-xdr": "^3.1.2", + "base32.js": "^0.1.0", + "bignumber.js": "^9.3.1", + "buffer": "^6.0.3", + "sha.js": "^2.4.12" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stellar/stellar-xdr-json": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-xdr-json/-/stellar-xdr-json-23.0.1.tgz", + "integrity": "sha512-tFY2Oz+AD5/VF2cjnfbrHC3D+WIHgPHCORVjMEcZN2ah+aRGY3WVb++Lc9qrPYDwZMfQVTqlTEs02OEEJLTS0Q==" + }, + "node_modules/@swc/helpers": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", + "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", + "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-x64": "4.2.4", + "@tailwindcss/oxide-freebsd-x64": "4.2.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-x64-musl": "4.2.4", + "@tailwindcss/oxide-wasm32-wasi": "4.2.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", + "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", + "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", + "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", + "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", + "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", + "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", + "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", + "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", + "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", + "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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", + "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", + "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", + "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.4", + "@tailwindcss/oxide": "4.2.4", + "tailwindcss": "4.2.4" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.3", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.3.tgz", + "integrity": "sha512-oMO1imV4qStH+GqddafkI7Q7r2ktPL7/0Mu74W1XEhfHHd3oTIrwP3OOIsbtpnnbe8y/IU+8Lm7Bi2LlMhVdNA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.3.tgz", + "integrity": "sha512-8Fgb4vKmBHllRHUjz3ZOwgV0v9b7cxCdN5T0iFQvvWJJVs6xvaxHERO1BclTL03bbK8vZAuXVKN3IeVS1sUdeQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@theahaco/contract-explorer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@theahaco/contract-explorer/-/contract-explorer-1.2.0.tgz", + "integrity": "sha512-RL62v5H2OtH2XxtTuglzTp5k9+om/OvD/b3UUkv4vAb43+NDJmJ14ECy8CWopDYne8mx7uPRFDG2kjS9nN/XMg==", + "license": "Apache-2.0", + "dependencies": { + "@stellar/design-system": "^3.2.2", + "@stellar/stellar-sdk": "^14.2.0", + "@stellar/stellar-xdr-json": "^23.0.0", + "@tanstack/react-query": "^5.90.5", + "@theahaco/ts-config": "^1.2.0", + "json-schema": "^0.4.0", + "lossless-json": "^4.3.0" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/@theahaco/ts-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@theahaco/ts-config/-/ts-config-1.2.0.tgz", + "integrity": "sha512-792O8Pox7m2I8p0xCsUQjaAK7XBkzkpongJcSh/vkZDnBH/lau+z3HE9TTp9IqApxEqmy/H9Wm4OIBvoDj7K1w==", + "license": "MIT", + "dependencies": { + "@total-typescript/ts-reset": "^0.6.1", + "@vitest/eslint-plugin": "^1.3.4", + "eslint-plugin-import-x": "^4.16.1", + "eslint-plugin-jest-dom": "^5.5.0", + "eslint-plugin-playwright": "^2.2.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.0", + "eslint-plugin-testing-library": "^7.6.1", + "globals": "^17.0.0", + "tslib": "^2.8.1", + "typescript-eslint": "^8.38.0" + } + }, + "node_modules/@total-typescript/ts-reset": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.6.1.tgz", + "integrity": "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==", + "license": "MIT" + }, + "node_modules/@trezor/analytics": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@trezor/analytics/-/analytics-1.5.0.tgz", + "integrity": "sha512-evILW5XJEmfPlf0TY1duOLtGJ47pdGeSKVE3P75ODEUsRNxtPVqlkOUBPmYpCxPnzS8XDmkatT8lf9/DF0G6nA==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/env-utils": "1.5.0", + "@trezor/utils": "9.5.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/blockchain-link-types": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-types/-/blockchain-link-types-1.5.1.tgz", + "integrity": "sha512-Idavz6LwLBW8sXc69fh5AJEnl666EDl2Nt3io7updvBgOR0/P12I900DgjNhCKtiWuv66A33/5RE7zLcj3lfnw==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/utils": "9.5.0", + "@trezor/utxo-lib": "2.5.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/blockchain-link-utils": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-utils/-/blockchain-link-utils-1.5.2.tgz", + "integrity": "sha512-OSS5OEE98FMnYfjoEALPjBt7ebjC/FKnq3HOolHdEWXBpVlXZNN2+Vo1R9J6WbZUU087sHuUTJJy/GJYWY13Tg==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@mobily/ts-belt": "^3.13.1", + "@stellar/stellar-sdk": "14.2.0", + "@trezor/env-utils": "1.5.0", + "@trezor/protobuf": "1.5.2", + "@trezor/utils": "9.5.0", + "xrpl": "4.4.3" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/blockchain-link-utils/node_modules/@stellar/stellar-sdk": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-14.2.0.tgz", + "integrity": "sha512-7nh2ogzLRMhfkIC0fGjn1LHUzk3jqVw8tjAuTt5ADWfL9CSGBL18ILucE9igz2L/RU2AZgeAvhujAnW91Ut/oQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-base": "^14.0.1", + "axios": "^1.12.2", + "bignumber.js": "^9.3.1", + "eventsource": "^2.0.2", + "feaxios": "^0.0.23", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@trezor/blockchain-link-utils/node_modules/@trezor/protobuf": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-1.5.2.tgz", + "integrity": "sha512-zViaL1jKue8DUTVEDg0C/lMipqNMd/Z3kr29/+MeZOoupjaXIQ2Lqp3WAMe8hvNTKKX8aNQH9JrbapJ6w9FMXw==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/schema-utils": "1.4.0", + "long": "5.2.5", + "protobufjs": "7.4.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-analytics": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@trezor/connect-analytics/-/connect-analytics-1.4.0.tgz", + "integrity": "sha512-hy2J2oeIhRC/e1bOWXo5dsVMVnDwO2UKnxhR6FD8PINR3jgM6PWAXc6k33WJsBcyiTzwMP7/xPysLcgNJH5o4w==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/analytics": "1.5.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-common": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@trezor/connect-common/-/connect-common-0.5.1.tgz", + "integrity": "sha512-wdpVCwdylBh4SBO5Ys40tB/d59UlfjmxgBHDkkLgaR+JcqkthCfiw5VlUrV9wu65lquejAZhA5KQL4mUUUhCow==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@trezor/env-utils": "1.5.0", + "@trezor/type-utils": "1.2.0", + "@trezor/utils": "9.5.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-web": { + "version": "9.7.2", + "resolved": "https://registry.npmjs.org/@trezor/connect-web/-/connect-web-9.7.2.tgz", + "integrity": "sha512-r4wMnQ51KO1EaMpO8HLB95E+4s+aaZE9Vjx1dHYaD+Xj40LR7OJmR6DyDKuF0Ioji3Jxx1MwZCaFfvA+0JW+Sg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@trezor/connect": "9.7.2", + "@trezor/connect-common": "0.5.1", + "@trezor/utils": "9.5.0", + "@trezor/websocket-client": "1.3.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana-program/compute-budget": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@solana-program/compute-budget/-/compute-budget-0.8.0.tgz", + "integrity": "sha512-qPKxdxaEsFxebZ4K5RPuy7VQIm/tfJLa1+Nlt3KNA8EYQkz9Xm8htdoEaXVrer9kpgzzp9R3I3Bh6omwCM06tQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@solana/kit": "^2.1.0" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana-program/stake": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@solana-program/stake/-/stake-0.2.1.tgz", + "integrity": "sha512-ssNPsJv9XHaA+L7ihzmWGYcm/+XYURQ8UA3wQMKf6ccEHyHOUgoglkkDU/BoA0+wul6HxZUN0tHFymC0qFw6sg==", + "license": "MIT", + "peerDependencies": { + "@solana/kit": "^2.1.0" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana-program/system": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@solana-program/system/-/system-0.7.0.tgz", + "integrity": "sha512-FKTBsKHpvHHNc1ATRm7SlC5nF/VdJtOSjldhcyfMN9R7xo712Mo2jHIzvBgn8zQO5Kg0DcWuKB7268Kv1ocicw==", + "license": "Apache-2.0", + "peerDependencies": { + "@solana/kit": "^2.1.0" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana-program/token": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@solana-program/token/-/token-0.5.1.tgz", + "integrity": "sha512-bJvynW5q9SFuVOZ5vqGVkmaPGA0MCC+m9jgJj1nk5m20I389/ms69ASnhWGoOPNcie7S9OwBX0gTj2fiyWpfag==", + "license": "Apache-2.0", + "peerDependencies": { + "@solana/kit": "^2.1.0" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana-program/token-2022": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@solana-program/token-2022/-/token-2022-0.4.2.tgz", + "integrity": "sha512-zIpR5t4s9qEU3hZKupzIBxJ6nUV5/UVyIT400tu9vT1HMs5JHxaTTsb5GUhYjiiTvNwU0MQavbwc4Dl29L0Xvw==", + "license": "Apache-2.0", + "peerDependencies": { + "@solana/kit": "^2.1.0", + "@solana/sysvars": "^2.1.0" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/accounts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/accounts/-/accounts-2.3.0.tgz", + "integrity": "sha512-QgQTj404Z6PXNOyzaOpSzjgMOuGwG8vC66jSDB+3zHaRcEPRVRd2sVSrd1U6sHtnV3aiaS6YyDuPQMheg4K2jw==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "2.3.0", + "@solana/codecs-core": "2.3.0", + "@solana/codecs-strings": "2.3.0", + "@solana/errors": "2.3.0", + "@solana/rpc-spec": "2.3.0", + "@solana/rpc-types": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/addresses": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/addresses/-/addresses-2.3.0.tgz", + "integrity": "sha512-ypTNkY2ZaRFpHLnHAgaW8a83N0/WoqdFvCqf4CQmnMdFsZSdC7qOwcbd7YzdaQn9dy+P2hybewzB+KP7LutxGA==", + "license": "MIT", + "dependencies": { + "@solana/assertions": "2.3.0", + "@solana/codecs-core": "2.3.0", + "@solana/codecs-strings": "2.3.0", + "@solana/errors": "2.3.0", + "@solana/nominal-types": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/assertions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/assertions/-/assertions-2.3.0.tgz", + "integrity": "sha512-Ekoet3khNg3XFLN7MIz8W31wPQISpKUGDGTylLptI+JjCDWx3PIa88xjEMqFo02WJ8sBj2NLV64Xg1sBcsHjZQ==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/codecs": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.3.0.tgz", + "integrity": "sha512-JVqGPkzoeyU262hJGdH64kNLH0M+Oew2CIPOa/9tR3++q2pEd4jU2Rxdfye9sd0Ce3XJrR5AIa8ZfbyQXzjh+g==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/codecs-data-structures": "2.3.0", + "@solana/codecs-numbers": "2.3.0", + "@solana/codecs-strings": "2.3.0", + "@solana/options": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/codecs-data-structures": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.3.0.tgz", + "integrity": "sha512-qvU5LE5DqEdYMYgELRHv+HMOx73sSoV1ZZkwIrclwUmwTbTaH8QAJURBj0RhQ/zCne7VuLLOZFFGv6jGigWhSw==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/codecs-numbers": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/codecs-strings": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.3.0.tgz", + "integrity": "sha512-y5pSBYwzVziXu521hh+VxqUtp0hYGTl1eWGoc1W+8mdvBdC1kTqm/X7aYQw33J42hw03JjryvYOvmGgk3Qz/Ug==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/codecs-numbers": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22", + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/fast-stable-stringify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/fast-stable-stringify/-/fast-stable-stringify-2.3.0.tgz", + "integrity": "sha512-KfJPrMEieUg6D3hfQACoPy0ukrAV8Kio883llt/8chPEG3FVTX9z/Zuf4O01a15xZmBbmQ7toil2Dp0sxMJSxw==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/functional": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/functional/-/functional-2.3.0.tgz", + "integrity": "sha512-AgsPh3W3tE+nK3eEw/W9qiSfTGwLYEvl0rWaxHht/lRcuDVwfKRzeSa5G79eioWFFqr+pTtoCr3D3OLkwKz02Q==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/instructions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/instructions/-/instructions-2.3.0.tgz", + "integrity": "sha512-PLMsmaIKu7hEAzyElrk2T7JJx4D+9eRwebhFZpy2PXziNSmFF929eRHKUsKqBFM3cYR1Yy3m6roBZfA+bGE/oQ==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/keys": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/keys/-/keys-2.3.0.tgz", + "integrity": "sha512-ZVVdga79pNH+2pVcm6fr2sWz9HTwfopDVhYb0Lh3dh+WBmJjwkabXEIHey2rUES7NjFa/G7sV8lrUn/v8LDCCQ==", + "license": "MIT", + "dependencies": { + "@solana/assertions": "2.3.0", + "@solana/codecs-core": "2.3.0", + "@solana/codecs-strings": "2.3.0", + "@solana/errors": "2.3.0", + "@solana/nominal-types": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/kit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/kit/-/kit-2.3.0.tgz", + "integrity": "sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==", + "license": "MIT", + "dependencies": { + "@solana/accounts": "2.3.0", + "@solana/addresses": "2.3.0", + "@solana/codecs": "2.3.0", + "@solana/errors": "2.3.0", + "@solana/functional": "2.3.0", + "@solana/instructions": "2.3.0", + "@solana/keys": "2.3.0", + "@solana/programs": "2.3.0", + "@solana/rpc": "2.3.0", + "@solana/rpc-parsed-types": "2.3.0", + "@solana/rpc-spec-types": "2.3.0", + "@solana/rpc-subscriptions": "2.3.0", + "@solana/rpc-types": "2.3.0", + "@solana/signers": "2.3.0", + "@solana/sysvars": "2.3.0", + "@solana/transaction-confirmation": "2.3.0", + "@solana/transaction-messages": "2.3.0", + "@solana/transactions": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/nominal-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/nominal-types/-/nominal-types-2.3.0.tgz", + "integrity": "sha512-uKlMnlP4PWW5UTXlhKM8lcgIaNj8dvd8xO4Y9l+FVvh9RvW2TO0GwUO6JCo7JBzCB0PSqRJdWWaQ8pu1Ti/OkA==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/options": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.3.0.tgz", + "integrity": "sha512-PPnnZBRCWWoZQ11exPxf//DRzN2C6AoFsDI/u2AsQfYih434/7Kp4XLpfOMT/XESi+gdBMFNNfbES5zg3wAIkw==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/codecs-data-structures": "2.3.0", + "@solana/codecs-numbers": "2.3.0", + "@solana/codecs-strings": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/programs": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/programs/-/programs-2.3.0.tgz", + "integrity": "sha512-UXKujV71VCI5uPs+cFdwxybtHZAIZyQkqDiDnmK+DawtOO9mBn4Nimdb/6RjR2CXT78mzO9ZCZ3qfyX+ydcB7w==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/promises": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/promises/-/promises-2.3.0.tgz", + "integrity": "sha512-GjVgutZKXVuojd9rWy1PuLnfcRfqsaCm7InCiZc8bqmJpoghlyluweNc7ml9Y5yQn1P2IOyzh9+p/77vIyNybQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/rpc": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/rpc/-/rpc-2.3.0.tgz", + "integrity": "sha512-ZWN76iNQAOCpYC7yKfb3UNLIMZf603JckLKOOLTHuy9MZnTN8XV6uwvDFhf42XvhglgUjGCEnbUqWtxQ9pa/pQ==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0", + "@solana/fast-stable-stringify": "2.3.0", + "@solana/functional": "2.3.0", + "@solana/rpc-api": "2.3.0", + "@solana/rpc-spec": "2.3.0", + "@solana/rpc-spec-types": "2.3.0", + "@solana/rpc-transformers": "2.3.0", + "@solana/rpc-transport-http": "2.3.0", + "@solana/rpc-types": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/rpc-api": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-api/-/rpc-api-2.3.0.tgz", + "integrity": "sha512-UUdiRfWoyYhJL9PPvFeJr4aJ554ob2jXcpn4vKmRVn9ire0sCbpQKYx6K8eEKHZWXKrDW8IDspgTl0gT/aJWVg==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "2.3.0", + "@solana/codecs-core": "2.3.0", + "@solana/codecs-strings": "2.3.0", + "@solana/errors": "2.3.0", + "@solana/keys": "2.3.0", + "@solana/rpc-parsed-types": "2.3.0", + "@solana/rpc-spec": "2.3.0", + "@solana/rpc-transformers": "2.3.0", + "@solana/rpc-types": "2.3.0", + "@solana/transaction-messages": "2.3.0", + "@solana/transactions": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/rpc-parsed-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-parsed-types/-/rpc-parsed-types-2.3.0.tgz", + "integrity": "sha512-B5pHzyEIbBJf9KHej+zdr5ZNAdSvu7WLU2lOUPh81KHdHQs6dEb310LGxcpCc7HVE8IEdO20AbckewDiAN6OCg==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/rpc-spec": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-spec/-/rpc-spec-2.3.0.tgz", + "integrity": "sha512-fA2LMX4BMixCrNB2n6T83AvjZ3oUQTu7qyPLyt8gHQaoEAXs8k6GZmu6iYcr+FboQCjUmRPgMaABbcr9j2J9Sw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0", + "@solana/rpc-spec-types": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/rpc-spec-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-spec-types/-/rpc-spec-types-2.3.0.tgz", + "integrity": "sha512-xQsb65lahjr8Wc9dMtP7xa0ZmDS8dOE2ncYjlvfyw/h4mpdXTUdrSMi6RtFwX33/rGuztQ7Hwaid5xLNSLvsFQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/rpc-subscriptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions/-/rpc-subscriptions-2.3.0.tgz", + "integrity": "sha512-Uyr10nZKGVzvCOqwCZgwYrzuoDyUdwtgQRefh13pXIrdo4wYjVmoLykH49Omt6abwStB0a4UL5gX9V4mFdDJZg==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0", + "@solana/fast-stable-stringify": "2.3.0", + "@solana/functional": "2.3.0", + "@solana/promises": "2.3.0", + "@solana/rpc-spec-types": "2.3.0", + "@solana/rpc-subscriptions-api": "2.3.0", + "@solana/rpc-subscriptions-channel-websocket": "2.3.0", + "@solana/rpc-subscriptions-spec": "2.3.0", + "@solana/rpc-transformers": "2.3.0", + "@solana/rpc-types": "2.3.0", + "@solana/subscribable": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/rpc-subscriptions-api": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-api/-/rpc-subscriptions-api-2.3.0.tgz", + "integrity": "sha512-9mCjVbum2Hg9KGX3LKsrI5Xs0KX390lS+Z8qB80bxhar6MJPugqIPH8uRgLhCW9GN3JprAfjRNl7our8CPvsPQ==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "2.3.0", + "@solana/keys": "2.3.0", + "@solana/rpc-subscriptions-spec": "2.3.0", + "@solana/rpc-transformers": "2.3.0", + "@solana/rpc-types": "2.3.0", + "@solana/transaction-messages": "2.3.0", + "@solana/transactions": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/rpc-subscriptions-channel-websocket": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-channel-websocket/-/rpc-subscriptions-channel-websocket-2.3.0.tgz", + "integrity": "sha512-2oL6ceFwejIgeWzbNiUHI2tZZnaOxNTSerszcin7wYQwijxtpVgUHiuItM/Y70DQmH9sKhmikQp+dqeGalaJxw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0", + "@solana/functional": "2.3.0", + "@solana/rpc-subscriptions-spec": "2.3.0", + "@solana/subscribable": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3", + "ws": "^8.18.0" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/rpc-subscriptions-spec": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-spec/-/rpc-subscriptions-spec-2.3.0.tgz", + "integrity": "sha512-rdmVcl4PvNKQeA2l8DorIeALCgJEMSu7U8AXJS1PICeb2lQuMeaR+6cs/iowjvIB0lMVjYN2sFf6Q3dJPu6wWg==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0", + "@solana/promises": "2.3.0", + "@solana/rpc-spec-types": "2.3.0", + "@solana/subscribable": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/rpc-transformers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-transformers/-/rpc-transformers-2.3.0.tgz", + "integrity": "sha512-UuHYK3XEpo9nMXdjyGKkPCOr7WsZsxs7zLYDO1A5ELH3P3JoehvrDegYRAGzBS2VKsfApZ86ZpJToP0K3PhmMA==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0", + "@solana/functional": "2.3.0", + "@solana/nominal-types": "2.3.0", + "@solana/rpc-spec-types": "2.3.0", + "@solana/rpc-types": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/rpc-transport-http": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-transport-http/-/rpc-transport-http-2.3.0.tgz", + "integrity": "sha512-HFKydmxGw8nAF5N+S0NLnPBDCe5oMDtI2RAmW8DMqP4U3Zxt2XWhvV1SNkAldT5tF0U1vP+is6fHxyhk4xqEvg==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0", + "@solana/rpc-spec": "2.3.0", + "@solana/rpc-spec-types": "2.3.0", + "undici-types": "^7.11.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/rpc-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-types/-/rpc-types-2.3.0.tgz", + "integrity": "sha512-O09YX2hED2QUyGxrMOxQ9GzH1LlEwwZWu69QbL4oYmIf6P5dzEEHcqRY6L1LsDVqc/dzAdEs/E1FaPrcIaIIPw==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "2.3.0", + "@solana/codecs-core": "2.3.0", + "@solana/codecs-numbers": "2.3.0", + "@solana/codecs-strings": "2.3.0", + "@solana/errors": "2.3.0", + "@solana/nominal-types": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/signers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/signers/-/signers-2.3.0.tgz", + "integrity": "sha512-OSv6fGr/MFRx6J+ZChQMRqKNPGGmdjkqarKkRzkwmv7v8quWsIRnJT5EV8tBy3LI4DLO/A8vKiNSPzvm1TdaiQ==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "2.3.0", + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0", + "@solana/instructions": "2.3.0", + "@solana/keys": "2.3.0", + "@solana/nominal-types": "2.3.0", + "@solana/transaction-messages": "2.3.0", + "@solana/transactions": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/subscribable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/subscribable/-/subscribable-2.3.0.tgz", + "integrity": "sha512-DkgohEDbMkdTWiKAoatY02Njr56WXx9e/dKKfmne8/Ad6/2llUIrax78nCdlvZW9quXMaXPTxZvdQqo9N669Og==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/sysvars": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/sysvars/-/sysvars-2.3.0.tgz", + "integrity": "sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==", + "license": "MIT", + "dependencies": { + "@solana/accounts": "2.3.0", + "@solana/codecs": "2.3.0", + "@solana/errors": "2.3.0", + "@solana/rpc-types": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/transaction-confirmation": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/transaction-confirmation/-/transaction-confirmation-2.3.0.tgz", + "integrity": "sha512-UiEuiHCfAAZEKdfne/XljFNJbsKAe701UQHKXEInYzIgBjRbvaeYZlBmkkqtxwcasgBTOmEaEKT44J14N9VZDw==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "2.3.0", + "@solana/codecs-strings": "2.3.0", + "@solana/errors": "2.3.0", + "@solana/keys": "2.3.0", + "@solana/promises": "2.3.0", + "@solana/rpc": "2.3.0", + "@solana/rpc-subscriptions": "2.3.0", + "@solana/rpc-types": "2.3.0", + "@solana/transaction-messages": "2.3.0", + "@solana/transactions": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/transaction-messages": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/transaction-messages/-/transaction-messages-2.3.0.tgz", + "integrity": "sha512-bgqvWuy3MqKS5JdNLH649q+ngiyOu5rGS3DizSnWwYUd76RxZl1kN6CoqHSrrMzFMvis6sck/yPGG3wqrMlAww==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "2.3.0", + "@solana/codecs-core": "2.3.0", + "@solana/codecs-data-structures": "2.3.0", + "@solana/codecs-numbers": "2.3.0", + "@solana/errors": "2.3.0", + "@solana/functional": "2.3.0", + "@solana/instructions": "2.3.0", + "@solana/nominal-types": "2.3.0", + "@solana/rpc-types": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@solana/transactions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/transactions/-/transactions-2.3.0.tgz", + "integrity": "sha512-LnTvdi8QnrQtuEZor5Msje61sDpPstTVwKg4y81tNxDhiyomjuvnSNLAq6QsB9gIxUqbNzPZgOG9IU4I4/Uaug==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "2.3.0", + "@solana/codecs-core": "2.3.0", + "@solana/codecs-data-structures": "2.3.0", + "@solana/codecs-numbers": "2.3.0", + "@solana/codecs-strings": "2.3.0", + "@solana/errors": "2.3.0", + "@solana/functional": "2.3.0", + "@solana/instructions": "2.3.0", + "@solana/keys": "2.3.0", + "@solana/nominal-types": "2.3.0", + "@solana/rpc-types": "2.3.0", + "@solana/transaction-messages": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@trezor/connect-web/node_modules/@stellar/stellar-sdk": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-14.2.0.tgz", + "integrity": "sha512-7nh2ogzLRMhfkIC0fGjn1LHUzk3jqVw8tjAuTt5ADWfL9CSGBL18ILucE9igz2L/RU2AZgeAvhujAnW91Ut/oQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-base": "^14.0.1", + "axios": "^1.12.2", + "bignumber.js": "^9.3.1", + "eventsource": "^2.0.2", + "feaxios": "^0.0.23", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@trezor/connect-web/node_modules/@trezor/blockchain-link": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@trezor/blockchain-link/-/blockchain-link-2.6.1.tgz", + "integrity": "sha512-SPwxkihOMI0o79BOy0RkfgVL2meuJhIe1yWHCeR8uoqf5KGblUyeXxvNCy6w8ckJ9LRpM1+bZhsUODuNs3083Q==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@solana-program/compute-budget": "^0.8.0", + "@solana-program/stake": "^0.2.1", + "@solana-program/token": "^0.5.1", + "@solana-program/token-2022": "^0.4.2", + "@solana/kit": "^2.3.0", + "@solana/rpc-types": "^2.3.0", + "@stellar/stellar-sdk": "14.2.0", + "@trezor/blockchain-link-types": "1.5.0", + "@trezor/blockchain-link-utils": "1.5.1", + "@trezor/env-utils": "1.5.0", + "@trezor/utils": "9.5.0", + "@trezor/utxo-lib": "2.5.0", + "@trezor/websocket-client": "1.3.0", + "@types/web": "^0.0.197", + "crypto-browserify": "3.12.0", + "socks-proxy-agent": "8.0.5", + "stream-browserify": "^3.0.0", + "xrpl": "4.4.3" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-web/node_modules/@trezor/blockchain-link/node_modules/@trezor/blockchain-link-types": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-types/-/blockchain-link-types-1.5.0.tgz", + "integrity": "sha512-wD6FKKxNr89MTWYL+NikRkBcWXhiWNFR0AuDHW6GHmlCEHhKu/hAvQtcER8X5jt/Wd0hSKNZqtHBXJ1ZkpJ6rg==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/utils": "9.5.0", + "@trezor/utxo-lib": "2.5.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-web/node_modules/@trezor/blockchain-link/node_modules/@trezor/blockchain-link-utils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-utils/-/blockchain-link-utils-1.5.1.tgz", + "integrity": "sha512-2tDGLEj5jzydjsJQONGTWVmCDDy6FTZ4ytr1/2gE6anyYEJU8MbaR+liTt3UvcP5jwZTNutwYLvZixRfrb8JpA==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@mobily/ts-belt": "^3.13.1", + "@stellar/stellar-sdk": "14.2.0", + "@trezor/env-utils": "1.5.0", + "@trezor/protobuf": "1.5.1", + "@trezor/utils": "9.5.0", + "xrpl": "4.4.3" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-web/node_modules/@trezor/blockchain-link/node_modules/@trezor/protobuf": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-1.5.1.tgz", + "integrity": "sha512-nAkaCCAqLpErBd+IuKeG5MpbyLR/2RMgCw18TWc80m1Ws/XgQirhHY9Jbk6gLImTXb9GTrxP0+MDSahzd94rSA==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/schema-utils": "1.4.0", + "long": "5.2.5", + "protobufjs": "7.4.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-web/node_modules/@trezor/connect": { + "version": "9.7.2", + "resolved": "https://registry.npmjs.org/@trezor/connect/-/connect-9.7.2.tgz", + "integrity": "sha512-Sn6F4mNH+yi2vAHy29kwhs50bRLn92drg3znm3pkY+8yEBxI4MmuP8sKYjdgUEJnQflWh80KlcvEDeVa4olVRA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@ethereumjs/common": "^10.1.0", + "@ethereumjs/tx": "^10.1.0", + "@fivebinaries/coin-selection": "3.0.0", + "@mobily/ts-belt": "^3.13.1", + "@noble/hashes": "^1.6.1", + "@scure/bip39": "^1.5.1", + "@solana-program/compute-budget": "^0.8.0", + "@solana-program/system": "^0.7.0", + "@solana-program/token": "^0.5.1", + "@solana-program/token-2022": "^0.4.2", + "@solana/kit": "^2.3.0", + "@trezor/blockchain-link": "2.6.1", + "@trezor/blockchain-link-types": "1.5.1", + "@trezor/blockchain-link-utils": "1.5.2", + "@trezor/connect-analytics": "1.4.0", + "@trezor/connect-common": "0.5.1", + "@trezor/crypto-utils": "1.2.0", + "@trezor/device-authenticity": "1.1.2", + "@trezor/device-utils": "1.2.0", + "@trezor/env-utils": "^1.5.0", + "@trezor/protobuf": "1.5.2", + "@trezor/protocol": "1.3.0", + "@trezor/schema-utils": "1.4.0", + "@trezor/transport": "1.6.2", + "@trezor/type-utils": "1.2.0", + "@trezor/utils": "9.5.0", + "@trezor/utxo-lib": "2.5.0", + "blakejs": "^1.2.1", + "bs58": "^6.0.0", + "bs58check": "^4.0.0", + "cbor": "^10.0.10", + "cross-fetch": "^4.0.0", + "jws": "^4.0.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-web/node_modules/@trezor/protobuf": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-1.5.2.tgz", + "integrity": "sha512-zViaL1jKue8DUTVEDg0C/lMipqNMd/Z3kr29/+MeZOoupjaXIQ2Lqp3WAMe8hvNTKKX8aNQH9JrbapJ6w9FMXw==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/schema-utils": "1.4.0", + "long": "5.2.5", + "protobufjs": "7.4.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-web/node_modules/@trezor/protocol": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@trezor/protocol/-/protocol-1.3.0.tgz", + "integrity": "sha512-rmrxbDrdgxTouBPbZcSeqU7ba/e5WVT1dxvxxEntHqRdTiDl7d3VK+BErCrlyol8EH5YCqEF3/rXt0crSOfoFw==", + "license": "See LICENSE.md in repo root", + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-web/node_modules/@trezor/transport": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@trezor/transport/-/transport-1.6.2.tgz", + "integrity": "sha512-w0HlD1fU+qTGO3tefBGHF/YS/ts/TWFja9FGIJ4+7+Z9NphvIG06HGvy2HzcD9AhJy9pvDeIsyoM2TTZTiyjkQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@trezor/protobuf": "1.5.2", + "@trezor/protocol": "1.3.0", + "@trezor/type-utils": "1.2.0", + "@trezor/utils": "9.5.0", + "cross-fetch": "^4.0.0", + "usb": "^2.15.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/crypto-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@trezor/crypto-utils/-/crypto-utils-1.2.0.tgz", + "integrity": "sha512-9i1NrfW1IE6JO910ut7xrx4u5LxE++GETbpJhWLj4P5xpuGDDSDLEn/MXaYisls2DpE897aOrGPaa1qyt8V6tw==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/device-authenticity": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@trezor/device-authenticity/-/device-authenticity-1.1.2.tgz", + "integrity": "sha512-313uSXYR4XKDv3CjtCpgHA+yEe9xxqN7EFl/D68FEn70SPsuWI0+2zUvjPPh6TIOh/EcLv7hCO/QTHUAGd7ZWQ==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@noble/curves": "^2.0.1", + "@trezor/crypto-utils": "1.2.0", + "@trezor/protobuf": "1.5.2", + "@trezor/schema-utils": "1.4.0", + "@trezor/utils": "9.5.0" + } + }, + "node_modules/@trezor/device-authenticity/node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@trezor/device-authenticity/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@trezor/device-authenticity/node_modules/@trezor/protobuf": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-1.5.2.tgz", + "integrity": "sha512-zViaL1jKue8DUTVEDg0C/lMipqNMd/Z3kr29/+MeZOoupjaXIQ2Lqp3WAMe8hvNTKKX8aNQH9JrbapJ6w9FMXw==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/schema-utils": "1.4.0", + "long": "5.2.5", + "protobufjs": "7.4.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/device-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@trezor/device-utils/-/device-utils-1.2.0.tgz", + "integrity": "sha512-Aqp7pIooFTx21zRUtTI6i1AS4d9Lrx7cclvksh2nJQF9WJvbzuCXshEGkLoOsHwhQrCl3IXfbGuMdA12yDenPA==", + "license": "See LICENSE.md in repo root" + }, + "node_modules/@trezor/env-utils": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@trezor/env-utils/-/env-utils-1.5.0.tgz", + "integrity": "sha512-u1TN7dMQ5Qhpbae08Z4JJmI9fQrbbJ4yj8eIAsuzMQn6vb+Sg9vbntl+IDsZ1G9WeI73uHTLu1wWMmAgiujH8w==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "ua-parser-js": "^2.0.4" + }, + "peerDependencies": { + "expo-constants": "*", + "expo-localization": "*", + "react-native": "*", + "tslib": "^2.6.2" + }, + "peerDependenciesMeta": { + "expo-constants": { + "optional": true + }, + "expo-localization": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@trezor/schema-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@trezor/schema-utils/-/schema-utils-1.4.0.tgz", + "integrity": "sha512-K7upSeh7VDrORaIC4KAxYVW93XNlohmUnH5if/5GKYmTdQSRp1nBkO6Jm+Z4hzIthdnz/1aLgnbeN3bDxWLRxA==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@sinclair/typebox": "^0.33.7", + "ts-mixer": "^6.0.3" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/type-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@trezor/type-utils/-/type-utils-1.2.0.tgz", + "integrity": "sha512-+E2QntxkyQuYfQQyl8RvT01tq2i5Dp/LFUOXuizF+KVOqsZBjBY43j5hewcCO3+MokD7deDiPyekbUEN5/iVlw==", + "license": "See LICENSE.md in repo root" + }, + "node_modules/@trezor/utils": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@trezor/utils/-/utils-9.5.0.tgz", + "integrity": "sha512-kdyMyDbxzvOZmwBNvTjAK+C/kzyOz8T4oUbFvq+KaXn5mBFf1uf8rq5X2HkxgdYRPArtHS3PxLKsfkNCdhCYtQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "bignumber.js": "^9.3.1" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/utxo-lib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@trezor/utxo-lib/-/utxo-lib-2.5.0.tgz", + "integrity": "sha512-Fa2cZh0037oX6AHNLfpFIj65UR/OoX0ZJTocFuQASe77/1PjZHysf6BvvGfmzuFToKfrAQ+DM/1Sx+P/vnyNmA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@trezor/utils": "9.5.0", + "bech32": "^2.0.0", + "bip66": "^2.0.0", + "bitcoin-ops": "^1.4.1", + "blake-hash": "^2.0.0", + "blakejs": "^1.2.1", + "bn.js": "^5.2.2", + "bs58": "^6.0.0", + "bs58check": "^4.0.0", + "cashaddrjs": "0.4.4", + "create-hmac": "^1.1.7", + "int64-buffer": "^1.1.0", + "pushdata-bitcoin": "^1.0.1", + "tiny-secp256k1": "^1.1.7", + "typeforce": "^1.18.0", + "varuint-bitcoin": "2.0.0", + "wif": "^5.0.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/websocket-client": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@trezor/websocket-client/-/websocket-client-1.3.0.tgz", + "integrity": "sha512-9KQSaVc3NtmM6rFFj1e+9bM0C5mVKVidbnxlfzuBJu7G2YMRdIdLPcAXhvmRZjs40uzDuBeApK+p547kODz2ug==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@trezor/utils": "9.5.0", + "ws": "^8.18.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-helmet": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz", + "integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/@types/w3c-web-usb": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.14.tgz", + "integrity": "sha512-Qu3Nn6JFuF4+sHKYl+IcX9vYiI40ogleXzFFSxoE1W94rG98o/kXs8uJ0QSfFzuwBCZWlGfUGpPkgwuuX4PchA==", + "license": "MIT" + }, + "node_modules/@types/web": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.197.tgz", + "integrity": "sha512-V4sOroWDADFx9dLodWpKm298NOJ1VJ6zoDVgaP+WBb/utWxqQ6gnMzd9lvVDAr/F3ibiKaxH9i45eS0gQPSTaQ==", + "license": "Apache-2.0" + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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==", + "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/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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==", + "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/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==", + "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/scope-manager": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", + "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==", + "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.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", + "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/types": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", + "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/typescript-estree": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.0", + "@typescript-eslint/tsconfig-utils": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "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/utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.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/visitor-keys": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "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/visitor-keys/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==", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz", + "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.5", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.5", + "vitest": "4.1.5" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/eslint-plugin": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.6.16.tgz", + "integrity": "sha512-2pBN1F1JXq6zTSaYC58CMJa7pGxXIRsLfOioeZM4cPE3pRdSh1ySTSoHPQlOTEF5WgoVzWZQxhGQ3ygT78hOVg==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "^8.58.0", + "@typescript-eslint/utils": "^8.58.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "*", + "eslint": ">=8.57.0", + "typescript": ">=5.0.0", + "vitest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.5", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@wallet-standard/base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", + "integrity": "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@wallet-standard/features": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.0.tgz", + "integrity": "sha512-hiEivWNztx73s+7iLxsuD1sOJ28xtRix58W7Xnz4XzzA/pF0+aicnWgjOdA10doVDEDZdUuZCIIqG96SFNlDUg==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@wallet-standard/wallet": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/wallet/-/wallet-1.1.0.tgz", + "integrity": "sha512-Gt8TnSlDZpAl+RWOOAB/kuvC7RpcdWAlFbHNoi4gsXsfaWa1QCT6LBcfIYTPdOZC9OVZUDwqGuGAcqZejDmHjg==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@walletconnect/core": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.23.0.tgz", + "integrity": "sha512-W++xuXf+AsMPrBWn1It8GheIbCTp1ynTQP+aoFB86eUwyCtSiK7UQsn/+vJZdwElrn+Ptp2A0RqQx2onTMVHjQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/jsonrpc-ws-connection": "1.0.16", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.0", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.0", + "@walletconnect/utils": "2.23.0", + "@walletconnect/window-getters": "1.0.1", + "es-toolkit": "1.39.3", + "events": "3.3.0", + "uint8arrays": "3.1.1" + }, + "engines": { + "node": ">=18.20.8" + } + }, + "node_modules/@walletconnect/core/node_modules/@walletconnect/logger": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.0.tgz", + "integrity": "sha512-DDktPBFdmt5d7U3sbp4e3fQHNS1b6amsR8FmtOnt6L2SnV7VfcZr8VmAGL12zetAR+4fndegbREmX0P8Mw6eDg==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" + } + }, + "node_modules/@walletconnect/core/node_modules/@walletconnect/types": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.0.tgz", + "integrity": "sha512-9ZEOJyx/kNVCRncDHh3Qr9eH7Ih1dXBFB4k1J8iEudkv3t4GhYpXhqIt2kNdQWluPb1BBB4wEuckAT96yKuA8g==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.0", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/environment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/environment/-/environment-1.0.1.tgz", + "integrity": "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/environment/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/events": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", + "integrity": "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==", + "license": "MIT", + "dependencies": { + "keyvaluestorage-interface": "^1.0.0", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/events/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/heartbeat": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", + "integrity": "sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==", + "license": "MIT", + "dependencies": { + "@walletconnect/events": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-http-connection": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.8.tgz", + "integrity": "sha512-+B7cRuaxijLeFDJUq5hAzNyef3e3tBDIxyaCNmFtjwnod5AGis3RToNqzFU33vpVcxFhofkpE7Cx+5MYejbMGw==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.6", + "@walletconnect/safe-json": "^1.0.1", + "cross-fetch": "^3.1.4", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-http-connection/node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/@walletconnect/jsonrpc-provider": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz", + "integrity": "sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.8", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-types": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz", + "integrity": "sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==", + "license": "MIT", + "dependencies": { + "events": "^3.3.0", + "keyvaluestorage-interface": "^1.0.0" + } + }, + "node_modules/@walletconnect/jsonrpc-utils": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz", + "integrity": "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==", + "license": "MIT", + "dependencies": { + "@walletconnect/environment": "^1.0.1", + "@walletconnect/jsonrpc-types": "^1.0.3", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/jsonrpc-utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/jsonrpc-ws-connection": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz", + "integrity": "sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.6", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0", + "ws": "^7.5.1" + } + }, + "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/logger": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.2.tgz", + "integrity": "sha512-7wR3wAwJTOmX4gbcUZcFMov8fjftY05+5cO/d4cpDD8wDzJ+cIlKdYOXaXfxHLSYeDazMXIsxMYjHYVDfkx+nA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" + } + }, + "node_modules/@walletconnect/relay-api": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", + "integrity": "sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-types": "^1.0.2" + } + }, + "node_modules/@walletconnect/relay-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz", + "integrity": "sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "1.8.0", + "@noble/hashes": "1.7.0", + "@walletconnect/safe-json": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "uint8arrays": "^3.0.0" + } + }, + "node_modules/@walletconnect/relay-auth/node_modules/@noble/curves": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", + "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/relay-auth/node_modules/@noble/hashes": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", + "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/safe-json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz", + "integrity": "sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/safe-json/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/sign-client": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.23.0.tgz", + "integrity": "sha512-Nzf5x/LnQgC0Yjk0NmkT8kdrIMcScpALiFm9gP0n3CulL+dkf3HumqWzdoTmQSqGPxwHu/TNhGOaRKZLGQXSqw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/core": "2.23.0", + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "3.0.0", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.0", + "@walletconnect/utils": "2.23.0", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/sign-client/node_modules/@walletconnect/logger": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.0.tgz", + "integrity": "sha512-DDktPBFdmt5d7U3sbp4e3fQHNS1b6amsR8FmtOnt6L2SnV7VfcZr8VmAGL12zetAR+4fndegbREmX0P8Mw6eDg==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" + } + }, + "node_modules/@walletconnect/sign-client/node_modules/@walletconnect/types": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.0.tgz", + "integrity": "sha512-9ZEOJyx/kNVCRncDHh3Qr9eH7Ih1dXBFB4k1J8iEudkv3t4GhYpXhqIt2kNdQWluPb1BBB4wEuckAT96yKuA8g==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.0", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", + "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/time/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/types": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.9.tgz", + "integrity": "sha512-IUl1PpD/Dig8IE2OZ9XtjbPohEyOZJ73xs92EDUzoIyzRtfm36g2D340pY3iu3AAdLv1yFiaZafB8Hf8RFze8A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/universal-provider": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.23.7.tgz", + "integrity": "sha512-6UicU/Mhr/1bh7MNoajypz7BhigORbHpP1LFTf8FYLQGDqzmqHMqmMH2GDAImtaY2sFTi2jBvc22tLl8VMze/A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/jsonrpc-http-connection": "1.0.8", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/sign-client": "2.23.7", + "@walletconnect/types": "2.23.7", + "@walletconnect/utils": "2.23.7", + "es-toolkit": "1.44.0", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/@walletconnect/core": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.23.7.tgz", + "integrity": "sha512-yTyymn9mFaDZkUfLfZ3E9VyaSDPeHAXlrPxQRmNx2zFsEt/25GmTU2A848aomimLxZnAG2jNLhxbJ8I0gyNV+w==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/jsonrpc-ws-connection": "1.0.16", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.7", + "@walletconnect/utils": "2.23.7", + "@walletconnect/window-getters": "1.0.1", + "es-toolkit": "1.44.0", + "events": "3.3.0", + "uint8arrays": "3.1.1" + }, + "engines": { + "node": ">=18.20.8" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/@walletconnect/sign-client": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.23.7.tgz", + "integrity": "sha512-SX61lzb1bTl/LijlcHQttnoHPBzzoY5mW9ArR6qhFtDNDTS7yr2rcH7rCngxHlYeb4rAYcWLHgbiGSrdKxl/mg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/core": "2.23.7", + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "3.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.7", + "@walletconnect/utils": "2.23.7", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/@walletconnect/types": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.7.tgz", + "integrity": "sha512-6PAKK+iR2IntmlkCFLMAHjYeIaerCJJYRDmdRimhon0u+aNmQT+HyGM6zxDAth0rdpBD7qEvKP5IXZTE7KFUhw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/@walletconnect/utils": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.7.tgz", + "integrity": "sha512-3p38gNrkVcIiQixVrlsWSa66Gjs5PqHOug2TxDgYUVBW5NcKjwQA08GkC6CKBQUfr5iaCtbfy6uZJW1LKSIvWQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@msgpack/msgpack": "3.1.3", + "@noble/ciphers": "1.3.0", + "@noble/curves": "1.9.7", + "@noble/hashes": "1.8.0", + "@scure/base": "1.2.6", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.7", + "@walletconnect/window-getters": "1.0.1", + "@walletconnect/window-metadata": "1.0.1", + "blakejs": "1.2.1", + "detect-browser": "5.3.0", + "ox": "0.9.3", + "uint8arrays": "3.1.1" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/@walletconnect/universal-provider/node_modules/ox": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz", + "integrity": "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.0.tgz", + "integrity": "sha512-bVyv4Hl+/wVGueZ6rEO0eYgDy5deSBA4JjpJHAMOdaNoYs05NTE1HymV2lfPQQHuqc7suYexo9jwuW7i3JLuAA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@msgpack/msgpack": "3.1.2", + "@noble/ciphers": "1.3.0", + "@noble/curves": "1.9.7", + "@noble/hashes": "1.8.0", + "@scure/base": "1.2.6", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.0", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.0", + "@walletconnect/window-getters": "1.0.1", + "@walletconnect/window-metadata": "1.0.1", + "blakejs": "1.2.1", + "bs58": "6.0.0", + "detect-browser": "5.3.0", + "ox": "0.9.3", + "uint8arrays": "3.1.1" + } + }, + "node_modules/@walletconnect/utils/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/@walletconnect/logger": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.0.tgz", + "integrity": "sha512-DDktPBFdmt5d7U3sbp4e3fQHNS1b6amsR8FmtOnt6L2SnV7VfcZr8VmAGL12zetAR+4fndegbREmX0P8Mw6eDg==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" + } + }, + "node_modules/@walletconnect/utils/node_modules/@walletconnect/types": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.0.tgz", + "integrity": "sha512-9ZEOJyx/kNVCRncDHh3Qr9eH7Ih1dXBFB4k1J8iEudkv3t4GhYpXhqIt2kNdQWluPb1BBB4wEuckAT96yKuA8g==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.0", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/utils/node_modules/ox": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz", + "integrity": "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils/node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/window-getters": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", + "integrity": "sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/window-getters/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/window-metadata": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", + "integrity": "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==", + "license": "MIT", + "dependencies": { + "@walletconnect/window-getters": "^1.0.1", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/window-metadata/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@xrplf/isomorphic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@xrplf/isomorphic/-/isomorphic-1.0.1.tgz", + "integrity": "sha512-0bIpgx8PDjYdrLFeC3csF305QQ1L7sxaWnL5y71mCvhenZzJgku9QsA+9QCXBC1eNYtxWO/xR91zrXJy2T/ixg==", + "license": "ISC", + "dependencies": { + "@noble/hashes": "^1.0.0", + "eventemitter3": "5.0.1", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@xrplf/secret-numbers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@xrplf/secret-numbers/-/secret-numbers-2.0.0.tgz", + "integrity": "sha512-z3AOibRTE9E8MbjgzxqMpG1RNaBhQ1jnfhNCa1cGf2reZUJzPMYs4TggQTc7j8+0WyV3cr7y/U8Oz99SXIkN5Q==", + "license": "ISC", + "dependencies": { + "@xrplf/isomorphic": "^1.0.1", + "ripple-keypairs": "^2.0.0" + } + }, + "node_modules/abitype": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz", + "integrity": "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/base32.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", + "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", + "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT" + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/big-integer": { + "version": "1.6.36", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz", + "integrity": "sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/big.js": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", + "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bigjs" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bip32-path": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/bip32-path/-/bip32-path-0.4.2.tgz", + "integrity": "sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ==", + "license": "MIT" + }, + "node_modules/bip66": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-2.0.0.tgz", + "integrity": "sha512-kBG+hSpgvZBrkIm9dt5T1Hd/7xGCPEX2npoxAWZfsK1FvjgaxySEh2WizjyIstWXriKo9K9uJ4u0OnsyLDUPXQ==", + "license": "MIT" + }, + "node_modules/bitcoin-ops": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", + "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==", + "license": "MIT" + }, + "node_modules/blake-hash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/blake-hash/-/blake-hash-2.0.0.tgz", + "integrity": "sha512-Igj8YowDu1PRkRsxZA7NVkdFNxH5rKv5cpLxQ0CVXSIA77pVYwCPRQJ2sMew/oneUpfuYRyjG6r8SmmmnbZb1w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "license": "MIT" + }, + "node_modules/borsh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-2.0.0.tgz", + "integrity": "sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg==", + "license": "Apache-2.0" + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, + "node_modules/browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.17.0" + } + }, + "node_modules/browser-resolve/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.6.1", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/bs58check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz", + "integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^6.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "license": "MIT" + }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, + "node_modules/cashaddrjs": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cashaddrjs/-/cashaddrjs-0.4.4.tgz", + "integrity": "sha512-xZkuWdNOh0uq/mxJIng6vYWfTowZLd9F4GMAlp2DwFHlcCqCm91NtuAc47RuV4L7r4PYcY5p6Cr2OKNb4hnkWA==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.36" + } + }, + "node_modules/cbor": { + "version": "10.0.12", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.12.tgz", + "integrity": "sha512-exQDevYd7ZQLP4moMQcZkKCVZsXLAtUSflObr3xTh4xzFIv/xBCdvCd6L259kQOUP2kcTC0jvC6PpZIf/WmRXA==", + "license": "MIT", + "dependencies": { + "nofilter": "^3.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/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/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/comment-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.6.tgz", + "integrity": "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==", + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-browser": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", + "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", + "license": "MIT" + }, + "node_modules/detect-europe-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz", + "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/domain-browser": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "license": "ISC" + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", + "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-toolkit": { + "version": "1.39.3", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.3.tgz", + "integrity": "sha512-Qb/TCFCldgOy8lZ5uC7nLGdqJwSabkQiYQShmw4jyiPk1pZzaYWTwaYKYP7EgLccWYgZocMrtItrwh683voaww==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-import-x": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.2.tgz", + "integrity": "sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==", + "license": "MIT", + "dependencies": { + "@package-json/types": "^0.0.12", + "@typescript-eslint/types": "^8.56.0", + "comment-parser": "^1.4.1", + "debug": "^4.4.1", + "eslint-import-context": "^0.1.9", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3 || ^10.1.2", + "semver": "^7.7.2", + "stable-hash-x": "^0.2.0", + "unrs-resolver": "^1.9.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-import-x" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint-import-resolver-node": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/utils": { + "optional": true + }, + "eslint-import-resolver-node": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest-dom": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-5.5.0.tgz", + "integrity": "sha512-CRlXfchTr7EgC3tDI7MGHY6QjdJU5Vv2RPaeeGtkXUHnKZf04kgzMPIJUXt4qKCvYWVVIEo9ut9Oq1vgXAykEA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.3", + "requireindex": "^1.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0", + "eslint": "^6.8.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "@testing-library/dom": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-playwright": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.10.2.tgz", + "integrity": "sha512-0N+2OWc3NZbOZ0gK8mp2TK6Qu3UWcJTQ9rqU0UM2yRJXgT758pvpY0lsOLIySfbyFrLqn3TcXjixbmcK90VnuQ==", + "license": "MIT", + "dependencies": { + "globals": "^17.3.0" + }, + "engines": { + "node": ">=16.9.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-testing-library": { + "version": "7.16.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-7.16.2.tgz", + "integrity": "sha512-8gleGnQXK2ZA3hHwjCwpYTZvM+9VsrJ+/9kDI8CjqAQGAdMQOdn/rJNu7ZySENuiWlGKQWyZJ4ZjEg2zamaRHw==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "^8.56.0", + "@typescript-eslint/utils": "^8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/feaxios": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.23.tgz", + "integrity": "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==", + "license": "MIT", + "dependencies": { + "is-retry-allowed": "^3.0.0" + } + }, + "node_modules/feaxios/node_modules/is-retry-allowed": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-3.0.0.tgz", + "integrity": "sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/hash-base/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hash-base/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hash-base/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hash-base/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/hash-base/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", + "license": "Apache-2.0" + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-encoding-sniffer/node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/i18next": { + "version": "25.10.10", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.10.tgz", + "integrity": "sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/idb-keyval": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", + "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==", + "license": "Apache-2.0" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/int64-buffer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-1.1.0.tgz", + "integrity": "sha512-94smTCQOvigN4d/2R/YDjz8YVG0Sufvv2aAh8P5m42gwhCsDAJqnbNOrxJsrADuAFAA69Q/ptGzxvNcNuIJcvw==", + "license": "MIT" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT", + "optional": true + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-standalone-pwa": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", + "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic-timers-promises": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", + "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jayson": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz", + "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==", + "license": "MIT", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-sha256": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz", + "integrity": "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", + "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.5", + "@asamuzakjp/dom-selector": "^7.0.6", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/keyvaluestorage-interface": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", + "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lint-staged": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", + "string-argv": "^0.3.2", + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/lit": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", + "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/long": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.5.tgz", + "integrity": "sha512-e0r9YBBgNCq1D1o5Dp8FMH0N5hsFtXDBiVa0qoJPHpakvZkmDKPRoGffZJII/XsHvj9An9blm+cRJ01yQqU+Dw==", + "license": "Apache-2.0" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lossless-json": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.3.0.tgz", + "integrity": "sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.11.0.tgz", + "integrity": "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "license": "(Apache-2.0 AND MIT)" + }, + "node_modules/mustache": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.0.0.tgz", + "integrity": "sha512-FJgjyX/IVkbXBXYUwH+OYwQKqWpFPLaLVESd70yHjSDunwzV2hZOoTBvPf4KLoxesUzzyfTH6F784Uqd7Wm5yA==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + }, + "engines": { + "npm": ">=1.4.0" + } + }, + "node_modules/nan": { + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", + "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "license": "MIT" + }, + "node_modules/node-stdlib-browser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz", + "integrity": "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert": "^2.0.0", + "browser-resolve": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^5.7.1", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "create-require": "^1.1.1", + "crypto-browserify": "^3.12.1", + "domain-browser": "4.22.0", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "isomorphic-timers-promises": "^1.0.1", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "pkg-dir": "^5.0.0", + "process": "^0.11.10", + "punycode": "^1.4.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^3.6.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.1", + "url": "^0.11.4", + "util": "^0.12.4", + "vm-browserify": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-stdlib-browser/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/node-stdlib-browser/node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-stdlib-browser/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/node-stdlib-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "license": "MIT", + "engines": { + "node": ">=12.19" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ox": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", + "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pbkdf2": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", + "license": "MIT", + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz", + "integrity": "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "slow-redact": "^0.3.0", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postal-mime": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz", + "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", + "license": "MIT-0" + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/protobufjs": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.1.tgz", + "integrity": "sha512-NWWCCscLjs+cOKF/s/XVNFRW7Yih0fdH+9brffR5NZCy8k42yRdl5KlWKMVXuI1vfCoy4o1z80XR/W/QUb3V3w==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", + "integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pushdata-bitcoin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", + "integrity": "sha512-hw7rcYTJRAl4olM8Owe8x0fBuJJ+WGbMhQuLWOXEMN3PxPCKQHRkhfL+XG0+iXUmSHjkMmb3Ba55Mt21cZc9kQ==", + "license": "MIT", + "dependencies": { + "bitcoin-ops": "^1.3.0" + } + }, + "node_modules/qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/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==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "license": "MIT", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-copy-to-clipboard-ts": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard-ts/-/react-copy-to-clipboard-ts-1.3.1.tgz", + "integrity": "sha512-qA4J3vHAdkdrqmaYvFCt5dbATtG8mQPl4Ar8HNrfrPvdaUqAQiwyQSg8fqSw8QYkH5KVUspNyvP6o/7kwZLw8Q==", + "license": "MIT", + "dependencies": { + "copy-to-clipboard": "^3.3.3" + }, + "peerDependencies": { + "react": ">=18.0.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/react-helmet/node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-i18next": { + "version": "16.6.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.6.6.tgz", + "integrity": "sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.10.9", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", + "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", + "integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz", + "integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.5" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resend": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.12.2.tgz", + "integrity": "sha512-xwgmU4b0OqoabJsIoK/x0Whk0Fcs3bpbK4i/DEWPiE5hYJHyHl0TbB6QbI3gIr+bLdLUJ1GYm/fe41aVFuHXgw==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.4", + "svix": "1.90.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripple-address-codec": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-5.0.0.tgz", + "integrity": "sha512-de7osLRH/pt5HX2xw2TRJtbdLLWHu0RXirpQaEeCnWKY5DYHykh3ETSkofvm0aX0LJiV7kwkegJxQkmbO94gWw==", + "license": "ISC", + "dependencies": { + "@scure/base": "^1.1.3", + "@xrplf/isomorphic": "^1.0.0" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/ripple-binary-codec": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-2.7.0.tgz", + "integrity": "sha512-gEBqan5muVp+q7jgZ6aUniSyN+e4FKRzn9uFAeFSIW7IgvkezP1cUolNtpahQ+jvaSK/33hxZA7wNmn1mc330g==", + "license": "ISC", + "dependencies": { + "@xrplf/isomorphic": "^1.0.1", + "bignumber.js": "^9.0.0", + "ripple-address-codec": "^5.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ripple-keypairs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-2.0.0.tgz", + "integrity": "sha512-b5rfL2EZiffmklqZk1W+dvSy97v3V/C7936WxCCgDynaGPp7GE6R2XO7EU9O2LlM/z95rj870IylYnOQs+1Rag==", + "license": "ISC", + "dependencies": { + "@noble/curves": "^1.0.0", + "@xrplf/isomorphic": "^1.0.0", + "ripple-address-codec": "^5.0.0" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/rpc-websockets": { + "version": "9.3.8", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.8.tgz", + "integrity": "sha512-7r+fm4tSJmLf9GvZfL1DJ1SJwpagpp6AazqM0FUaeV7CA+7+NYINSk1syWa4tU/6OF2CyBicLtzENGmXRJH6wQ==", + "license": "LGPL-3.0-only", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^11.0.0", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^6.0.0" + } + }, + "node_modules/rpc-websockets/node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/rpc-websockets/node_modules/utf-8-validate": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz", + "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/rpc-websockets/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/secp256k1": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.1.tgz", + "integrity": "sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "elliptic": "^6.5.7", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/secp256k1/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sha1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", + "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", + "license": "BSD-3-Clause", + "dependencies": { + "charenc": ">= 0.0.1", + "crypt": ">= 0.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slow-redact": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/slow-redact/-/slow-redact-0.3.2.tgz", + "integrity": "sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw==", + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/style-vendorizer": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/style-vendorizer/-/style-vendorizer-2.2.3.tgz", + "integrity": "sha512-/VDRsWvQAgspVy9eATN3z6itKTuyg+jW1q6UoTCQCFRqPDw8bi3E1hXIKnGw5LvXS2AQPuJ7Af4auTLYeBOLEg==", + "license": "MIT" + }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svix": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.90.0.tgz", + "integrity": "sha512-ljkZuyy2+IBEoESkIpn8sLM+sxJHQcPxlZFxU+nVDhltNfUMisMBzWX/UR8SjEnzoI28ZjCzMbmYAPwSTucoMw==", + "license": "MIT", + "dependencies": { + "standardwebhooks": "1.0.0", + "uuid": "^10.0.0" + } + }, + "node_modules/svix/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tiny-secp256k1": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.7.tgz", + "integrity": "sha512-eb+F6NabSnjbLwNoC+2o5ItbmP1kg7HliWue71JgLegQt6A5mTN8YbvTLCazdlg6e5SV6A+r8OGvZYskdlmhqQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.3.0", + "bn.js": "^4.11.8", + "create-hmac": "^1.1.7", + "elliptic": "^6.4.0", + "nan": "^2.13.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/tiny-secp256k1/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz", + "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.28" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz", + "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, + "node_modules/tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==", + "license": "Unlicense" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", + "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.0", + "@typescript-eslint/parser": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.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/ua-is-frozen": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz", + "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/ua-parser-js": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.9.tgz", + "integrity": "sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "AGPL-3.0-or-later", + "dependencies": { + "detect-europe-js": "^0.1.2", + "is-standalone-pwa": "^0.1.1", + "ua-is-frozen": "^0.1.2" + }, + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/uint8array-tools": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz", + "integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/uint8arrays": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz", + "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.4.2" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.25.0.tgz", + "integrity": "sha512-AXNgS1Byr27fTI+2bsPEkV9CxkT8H6xNyRI68b3TatlZo3RkzlqQBLL+w7SmGPVpokjHbcuNVQUWE7FRTg+LRA==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/unstorage/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/usb": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.17.0.tgz", + "integrity": "sha512-UuFgrlglgDn5ll6d5l7kl3nDb2Yx43qLUGcDq+7UNLZLtbNug0HZBb2Xodhgx2JZB1LqvU+dOGqLEeYUeZqsHg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.5.0" + }, + "engines": { + "node": ">=12.22.0 <13.0 || >=14.17.0" + } + }, + "node_modules/usb/node_modules/node-addon-api": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", + "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uuid4": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid4/-/uuid4-2.0.3.tgz", + "integrity": "sha512-CTpAkEVXMNJl2ojgtpLXHgz23dh8z81u6/HEPiQFOvBc/c2pde6TVHmH4uwY0d/GLF3tb7+VDAj4+2eJaQSdZQ==", + "license": "ISC" + }, + "node_modules/valtio": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.7.tgz", + "integrity": "sha512-DwJhCDpujuQuKdJ2H84VbTjEJJteaSmqsuUltsfbfdbotVfNeTE4K/qc/Wi57I9x8/2ed4JNdjEna7O6PfavRg==", + "license": "MIT", + "dependencies": { + "proxy-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/varuint-bitcoin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-2.0.0.tgz", + "integrity": "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/viem": { + "version": "2.48.4", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.48.4.tgz", + "integrity": "sha512-mReP/rgY2P+WeeRSG4sUvccCLKfyAW1C73Y3KkobAqgzYmVna9qyUMNE44xIUkDtfvRuC33r24UhF4baBYovsg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.20", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/ox": { + "version": "0.14.20", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.20.tgz", + "integrity": "sha512-rby38C3nDn8eQkf29Zgw4hkCZJ64Qqi0zRPWL8ENUQ7JVuoITqrVtwWQgM/He19SCMUEc7hS/Sjw0jIOSLJhOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-node-polyfills": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.26.0.tgz", + "integrity": "sha512-BAe5YzJf368XGev02hDvioidx4uVH8dqEJlG73bjQSxM26/AQnGcKFomq9n3vGq5yqpSHKN4h1XQNxx9l98mBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-inject": "^5.0.5", + "node-stdlib-browser": "^1.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/davidmyersdev" + }, + "peerDependencies": { + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.6.0.tgz", + "integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/whatwg-url/node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wif": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/wif/-/wif-5.0.0.tgz", + "integrity": "sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA==", + "license": "MIT", + "dependencies": { + "bs58check": "^4.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xrpl": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-4.4.3.tgz", + "integrity": "sha512-vi2OjuNkiaP8nv1j+nqHp8GZwwEjO6Y8+j/OuVMg6M4LwXEwyHdIj33dlg7cyY1Lw5+jb9HqFOQvABhaywVbTQ==", + "license": "ISC", + "dependencies": { + "@scure/bip32": "^1.3.1", + "@scure/bip39": "^1.2.1", + "@xrplf/isomorphic": "^1.0.1", + "@xrplf/secret-numbers": "^2.0.0", + "bignumber.js": "^9.0.0", + "eventemitter3": "^5.0.1", + "fast-json-stable-stringify": "^2.1.0", + "ripple-address-codec": "^5.0.0", + "ripple-binary-codec": "^2.5.0", + "ripple-keypairs": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "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", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "license": "MIT", + "optional": true, + "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/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } } diff --git a/package.json b/package.json index f672893d..4f0be07f 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,94 @@ { - "name": "scaffold-stellar-frontend", + "name": "learnvault-frontend", "type": "module", "version": "0.0.1", "private": true, + "overrides": { + "protobufjs": ">=7.5.5" + }, "scripts": { "dev": "npm start", - "start": "concurrently --kill-others-on-fail --names stellar,vite -c gray,green --pad-prefix \"stellar scaffold watch --build-clients\" \"vite\"", + "dev:server": "npm run dev --prefix server", + "dev:ui": "vite", + "start": "node scripts/start-dev.mjs", + "start:full": "concurrently --kill-others-on-fail --names stellar,vite -c gray,green --pad-prefix \"stellar scaffold watch --build-clients\" \"vite\"", "build": "tsc -b && vite build", - "install:contracts": "if ls packages/*/package.json > /dev/null 2>&1; then npm install --workspaces && npm run build --workspaces --if-present; else echo 'No contract client packages found. Run: stellar-scaffold build --build-clients'; fi", + "generate:clients": "bash scripts/generate-clients.sh", + "install:contracts": "if ls packages/*/package.json > /dev/null 2>&1; then npm install --workspaces && npm run build --workspaces --if-present; else echo 'No contract client packages found. Run: npm run generate:clients'; fi", "preview": "vite preview", "lint": "eslint .", + "test:e2e": "playwright test", "format": "prettier . --write", + "docs:generate": "npm run docs:generate --prefix server", "prepare": "husky", - "typecheck": "tsc" + "typecheck": "tsc", + "test": "cargo test --workspace", + "test:frontend": "vitest run", + "test:contracts": "cargo test --workspace", + "test:watch": "cargo watch -x 'test --workspace'", + "test:coverage": "vitest run --coverage" }, "workspaces": [ "packages/*" ], "dependencies": { - "@creit.tech/stellar-wallets-kit": "^1.9.5", + "@creit.tech/stellar-wallets-kit": "^2.0.1", "@stellar/design-system": "^3.2.7", "@stellar/stellar-sdk": "^14.4.3", "@stellar/stellar-xdr-json": "^23.0.0", + "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.90.17", "@theahaco/contract-explorer": "^1.1.0", "@theahaco/ts-config": "^1.2.0", + "canvas-confetti": "^1.9.4", + "date-fns": "^4.1.0", + "framer-motion": "^12.38.0", + "i18next": "^25.10.5", + "i18next-browser-languagedetector": "^8.2.1", "lossless-json": "^4.3.0", + "lucide-react": "^1.7.0", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-router-dom": "^7.12.0", + "react-helmet": "^6.1.0", + "react-i18next": "^16.6.2", + "react-is": "^19.2.4", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.13.2", + "recharts": "^3.8.0", + "resend": "^6.9.4", + "sonner": "^2.0.7", + "tailwindcss": "^4.2.2", + "utf-8-validate": "^5.0.10", "zod": "^4.3.5" }, "devDependencies": { + "@playwright/test": "^1.55.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/canvas-confetti": "^1.9.0", "@types/lodash": "^4.17.23", "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", + "@types/react-helmet": "^6.1.11", "@types/react-router-dom": "^5.3.3", - "@vitejs/plugin-react": "^5.1.2", + "@vitejs/plugin-react": "^5.2.0", + "@vitest/coverage-v8": "^4.1.1", "concurrently": "^9.2.1", "dotenv": "^17.2.3", "eslint": "^9.39.2", "glob": "^13.0.0", "globals": "^17.0.0", "husky": "^9.1.7", + "jsdom": "^29.0.1", "lint-staged": "^16.2.7", "prettier": "^3.8.0", "typescript": "~5.9.3", - "vite": "^7.3.1", - "vite-plugin-node-polyfills": "^0.25.0", - "vite-plugin-wasm": "^3.5.0" + "vite": "^8.0.3", + "vite-plugin-node-polyfills": "^0.26.0", + "vite-plugin-wasm": "^3.5.0", + "vitest": "^4.1.1" }, "lint-staged": { "**/*": [ @@ -56,5 +96,6 @@ "prettier --write --ignore-unknown" ] }, - "prettier": "@theahaco/ts-config/prettier" + "prettier": "@theahaco/ts-config/prettier", + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..ef8e45f3 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,31 @@ +import { defineConfig, devices } from "@playwright/test" + +export default defineConfig({ + testDir: "./e2e", + timeout: 60_000, + expect: { timeout: 10_000 }, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 2 : undefined, + reporter: process.env.CI ? [["github"], ["html"]] : [["list"], ["html"]], + use: { + baseURL: "http://127.0.0.1:4173", + trace: "retain-on-failure", + video: "retain-on-failure", + screenshot: "only-on-failure", + }, + outputDir: "test-results", + webServer: { + command: + "npm run build && npm run preview -- --host 127.0.0.1 --port 4173 --strictPort", + url: "http://127.0.0.1:4173", + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}) diff --git a/public/assets/brand/covers/cover-defi.svg b/public/assets/brand/covers/cover-defi.svg new file mode 100644 index 00000000..680a82bd --- /dev/null +++ b/public/assets/brand/covers/cover-defi.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $ + + + + + + + + + TRACK 03 + + + DeFi + Fundamentals + + Understand decentralized finance: AMMs, liquidity + pools, stablecoins, and yield strategies. + + + 10 Modules + · + Intermediate + · + Earn LRN + + + + + LEARNVAULT + learnvault.xyz + diff --git a/public/assets/brand/covers/cover-intro-stellar.svg b/public/assets/brand/covers/cover-intro-stellar.svg new file mode 100644 index 00000000..06584a1b --- /dev/null +++ b/public/assets/brand/covers/cover-intro-stellar.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TRACK 01 + + + Introduction + to Stellar + + + Learn the fundamentals of the Stellar network, + accounts, assets, and transactions. + + + + 8 Modules + · + Beginner + · + Earn LRN + + + + + + LEARNVAULT + learnvault.xyz + diff --git a/public/assets/brand/covers/cover-soroban.svg b/public/assets/brand/covers/cover-soroban.svg new file mode 100644 index 00000000..c02310b4 --- /dev/null +++ b/public/assets/brand/covers/cover-soroban.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + fn mint_reward(env: Env, learner: Address) { + learner.require_auth(); + let balance = get_balance(&env, &learner); + set_balance(&env, &learner, balance + reward); + } + pub fn verify_milestone( + env: Env, + course_id: u32, + learner: Address, + ) -> bool { + // on-chain verification + } + + + + + + + + + + + + + + TRACK 02 + + + Soroban Smart + Contracts + + Build and deploy smart contracts on Stellar + using Rust and the Soroban SDK. + + + 12 Modules + · + Intermediate + · + Earn LRN + + + + + LEARNVAULT + learnvault.xyz + diff --git a/public/assets/brand/logos/favicon-16.svg b/public/assets/brand/logos/favicon-16.svg new file mode 100644 index 00000000..10164116 --- /dev/null +++ b/public/assets/brand/logos/favicon-16.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/public/assets/brand/logos/favicon-32.svg b/public/assets/brand/logos/favicon-32.svg new file mode 100644 index 00000000..a407c987 --- /dev/null +++ b/public/assets/brand/logos/favicon-32.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/public/assets/brand/logos/learnvault-icon-dark.svg b/public/assets/brand/logos/learnvault-icon-dark.svg new file mode 100644 index 00000000..e0e9b828 --- /dev/null +++ b/public/assets/brand/logos/learnvault-icon-dark.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + LRN + + diff --git a/public/assets/brand/logos/learnvault-icon-light.svg b/public/assets/brand/logos/learnvault-icon-light.svg new file mode 100644 index 00000000..6286d727 --- /dev/null +++ b/public/assets/brand/logos/learnvault-icon-light.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + LRN + + diff --git a/public/assets/brand/logos/learnvault-logo-dark.svg b/public/assets/brand/logos/learnvault-logo-dark.svg new file mode 100644 index 00000000..ba8e222d --- /dev/null +++ b/public/assets/brand/logos/learnvault-logo-dark.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + LRN + + + + LearnVault + + + LEARN · EARN · GET FUNDED + diff --git a/public/assets/brand/logos/learnvault-logo-light.svg b/public/assets/brand/logos/learnvault-logo-light.svg new file mode 100644 index 00000000..a2205daf --- /dev/null +++ b/public/assets/brand/logos/learnvault-logo-light.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + LRN + + + + LearnVault + + + LEARN · EARN · GET FUNDED + diff --git a/public/assets/brand/nft/scholar-nft-base.png b/public/assets/brand/nft/scholar-nft-base.png new file mode 100644 index 00000000..a80f9908 Binary files /dev/null and b/public/assets/brand/nft/scholar-nft-base.png differ diff --git a/public/assets/brand/nft/scholar-nft-base.svg b/public/assets/brand/nft/scholar-nft-base.svg new file mode 100644 index 00000000..62c07e42 --- /dev/null +++ b/public/assets/brand/nft/scholar-nft-base.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LRN + + + ScholarNFT + + + + VERIFIED CREDENTIAL + + + {{TRACK_NAME}} + + + {{SCHOLAR_ADDRESS}} + + + {{DATE}} + + + LEARNVAULT + + + POWERED BY STELLAR + diff --git a/public/assets/brand/nft/scholar-nft-defi.png b/public/assets/brand/nft/scholar-nft-defi.png new file mode 100644 index 00000000..2050c02e Binary files /dev/null and b/public/assets/brand/nft/scholar-nft-defi.png differ diff --git a/public/assets/brand/nft/scholar-nft-defi.svg b/public/assets/brand/nft/scholar-nft-defi.svg new file mode 100644 index 00000000..97303e8a --- /dev/null +++ b/public/assets/brand/nft/scholar-nft-defi.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LRN + + ScholarNFT + + + VERIFIED CREDENTIAL + + {{TRACK_NAME}} + + + {{SCHOLAR_ADDRESS}} + + + {{DATE}} + + LEARNVAULT + POWERED BY STELLAR + diff --git a/public/assets/brand/nft/scholar-nft-soroban.png b/public/assets/brand/nft/scholar-nft-soroban.png new file mode 100644 index 00000000..fa11e789 Binary files /dev/null and b/public/assets/brand/nft/scholar-nft-soroban.png differ diff --git a/public/assets/brand/nft/scholar-nft-soroban.svg b/public/assets/brand/nft/scholar-nft-soroban.svg new file mode 100644 index 00000000..35b4b311 --- /dev/null +++ b/public/assets/brand/nft/scholar-nft-soroban.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + } + + + + + + + + LRN + + ScholarNFT + + + VERIFIED CREDENTIAL + + {{TRACK_NAME}} + + + {{SCHOLAR_ADDRESS}} + + + {{DATE}} + + LEARNVAULT + POWERED BY STELLAR + diff --git a/public/assets/brand/nft/scholar-nft-stellar.png b/public/assets/brand/nft/scholar-nft-stellar.png new file mode 100644 index 00000000..a5d5ee36 Binary files /dev/null and b/public/assets/brand/nft/scholar-nft-stellar.png differ diff --git a/public/assets/brand/nft/scholar-nft-stellar.svg b/public/assets/brand/nft/scholar-nft-stellar.svg new file mode 100644 index 00000000..c432622d --- /dev/null +++ b/public/assets/brand/nft/scholar-nft-stellar.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LRN + + ScholarNFT + + + VERIFIED CREDENTIAL + + {{TRACK_NAME}} + + + {{SCHOLAR_ADDRESS}} + + + {{DATE}} + + LEARNVAULT + POWERED BY STELLAR + diff --git a/public/assets/brand/og/og-fallback.svg b/public/assets/brand/og/og-fallback.svg new file mode 100644 index 00000000..7fc1a484 --- /dev/null +++ b/public/assets/brand/og/og-fallback.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LearnVault + + + LEARN · EARN · GET FUNDED + + POWERED BY STELLAR + diff --git a/public/assets/brand/og/og-homepage.svg b/public/assets/brand/og/og-homepage.svg new file mode 100644 index 00000000..b09fed5e --- /dev/null +++ b/public/assets/brand/og/og-homepage.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LearnVault + + + + + + Learning is the proof of work. + The community is the bank. + + + + LEARN + + + EARN + + + GET FUNDED + + + POWERED BY STELLAR · BUILT FOR AFRICAN LEARNERS + + + learnvault.xyz + diff --git a/rust-toolchain.toml b/rust-toolchain.toml index aca06de6..e25fb766 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.89.0" +channel = "stable" targets = ["wasm32v1-none"] diff --git a/scripts/convert-nft-svg-to-png.cjs b/scripts/convert-nft-svg-to-png.cjs new file mode 100644 index 00000000..ae8940c2 --- /dev/null +++ b/scripts/convert-nft-svg-to-png.cjs @@ -0,0 +1,35 @@ +const fs = require("fs") +const path = require("path") +const { Resvg } = require("@resvg/resvg-js") + +const projectRoot = path.join(__dirname, "..") +const nftDir = path.join(projectRoot, "public/assets/brand/nft") +const svgs = [ + "scholar-nft-base.svg", + "scholar-nft-stellar.svg", + "scholar-nft-soroban.svg", + "scholar-nft-defi.svg", +] + +async function convertSvgs() { + for (const svgFile of svgs) { + const svgPath = path.join(nftDir, svgFile) + const pngFile = svgFile.replace(".svg", ".png") + const pngPath = path.join(nftDir, pngFile) + + const svgData = fs.readFileSync(svgPath) + const resvg = new Resvg(svgData, { + fitTo: { + mode: "width", + value: 2000, + }, + }) + + const pngData = resvg.render() + fs.writeFileSync(pngPath, pngData.asPng()) + + console.log(`Created: ${pngFile} (${pngData.width}x${pngData.height})`) + } +} + +convertSvgs().catch(console.error) diff --git a/scripts/deploy-testnet.sh b/scripts/deploy-testnet.sh new file mode 100644 index 00000000..c3fa6e31 --- /dev/null +++ b/scripts/deploy-testnet.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +# Deploy All V1 Contracts to Stellar Testnet +# This script deploys all six V1 contracts to Stellar Testnet +# and stores their addresses in environments.toml + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Network configuration +NETWORK="testnet" +NETWORK_PASSPHRASE="Test SDF Network ; September 2015 Future Net" + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}Deploying V1 Contracts to Testnet${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" + +# Check if stellar CLI is available +if ! command -v stellar &> /dev/null; then + echo -e "${RED}Error: stellar CLI not found${NC}" + echo "Please install stellar CLI: npm install -g @stellar/stellar-cli" + exit 1 +fi + +# Function to deploy a contract +deploy_contract() { + local contract_name=$1 + local contract_path=$2 + + echo -e "${YELLOW}Deploying $contract_name...${NC}" + + # Deploy contract + DEPLOY_RESULT=$(stellar contract deploy \ + --wasm target/wasm32v1/release/$contract_name.wasm \ + --source deployer \ + --network $NETWORK \ + --rpc-url https://soroban-testnet.stellar.org:443) + + if [ $? -eq 0 ]; then + # Extract contract ID from output + CONTRACT_ID=$(echo "$DEPLOY_RESULT" | grep -o 'Contract ID: [A-Z0-9]*' | grep -o '[A-Z0-9]*') + echo -e "${GREEN}✓ $contract_name deployed successfully!${NC}" + echo -e "${GREEN} Contract ID: $CONTRACT_ID${NC}" + echo "" + + # Store contract ID in environments.toml + echo "[$contract_name]" >> environments.toml + echo "contract_id = \"$CONTRACT_ID\"" >> environments.toml + echo "network = \"$NETWORK\"" >> environments.toml + echo "" >> environments.toml + + return $CONTRACT_ID + else + echo -e "${RED}✗ Failed to deploy $contract_name${NC}" + exit 1 + fi +} + +# Build all contracts first +echo -e "${YELLOW}Building all contracts...${NC}" +if ! cargo build --target wasm32v1-none --release; then + echo -e "${RED}✗ Build failed${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ Build completed${NC}" +echo "" + +# Clear existing environments.toml +> environments.toml + +# Deploy all six contracts +echo -e "${YELLOW}Starting deployment...${NC}" +echo "" + +LEARN_TOKEN_ID=$(deploy_contract "learn_token" "contracts/learn_token") +GOVERNANCE_TOKEN_ID=$(deploy_contract "governance_token" "contracts/governance_token") +COURSE_MILESTONE_ID=$(deploy_contract "course_milestone" "contracts/course_milestone") +MILESTONE_ESCROW_ID=$(deploy_contract "milestone_escrow" "contracts/milestone_escrow") +SCHOLARSHIP_TREASURY_ID=$(deploy_contract "scholarship_treasury" "contracts/scholarship_treasury") +SCHOLAR_NFT_ID=$(deploy_contract "scholar_nft" "contracts/scholar_nft") + +echo "" +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}Deployment Summary${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" +echo -e "${GREEN}All contracts deployed successfully!${NC}" +echo "" +echo -e "${YELLOW}Contract Addresses:${NC}" +echo -e "${BLUE}LearnToken:${NC} $LEARN_TOKEN_ID" +echo -e "${BLUE}GovernanceToken:${NC} $GOVERNANCE_TOKEN_ID" +echo -e "${BLUE}CourseMilestone:${NC} $COURSE_MILESTONE_ID" +echo -e "${BLUE}MilestoneEscrow:${NC} $MILESTONE_ESCROW_ID" +echo -e "${BLUE}ScholarshipTreasury:${NC} $SCHOLARSHIP_TREASURY_ID" +echo -e "${BLUE}ScholarNFT:${NC} $SCHOLAR_NFT_ID" +echo "" +echo -e "${GREEN}environments.toml updated with contract addresses${NC}" +echo "" +echo -e "${YELLOW}Next steps:${NC}" +echo "1. Update .env.example with contract IDs" +echo "2. Run smoke tests: stellar contract invoke --id --network testnet ..." +echo "3. Update frontend integration" diff --git a/scripts/generate-clients.sh b/scripts/generate-clients.sh new file mode 100755 index 00000000..cd8d3448 --- /dev/null +++ b/scripts/generate-clients.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# generate-clients.sh +# Generates TypeScript contract bindings for all six LearnVault Soroban contracts. +# Run this after deploying contracts and populating contract IDs in .env. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Load .env from project root if it exists +if [[ -f "$ROOT_DIR/.env" ]]; then + # Export only non-empty, non-comment lines + set -o allexport + # shellcheck disable=SC1090 + source <(grep -v '^#' "$ROOT_DIR/.env" | grep '=') + set +o allexport +fi + +NETWORK="${STELLAR_NETWORK:-testnet}" + +echo "Generating Soroban TypeScript clients (network: $NETWORK) ..." +echo "" + +generate_client() { + local name="$1" + local contract_id="${2:-}" + local out_dir="$ROOT_DIR/packages/$name" + + if [[ -z "$contract_id" ]]; then + echo " ⚠️ Skipping $name — contract ID not set in .env" + return + fi + + echo " → $name ($contract_id)" + stellar contract bindings typescript \ + --contract-id "$contract_id" \ + --network "$NETWORK" \ + --output-dir "$out_dir" \ + --overwrite +} + +generate_client "learn_token" "${VITE_LEARN_TOKEN_CONTRACT_ID:-}" +generate_client "governance_token" "${VITE_GOVERNANCE_TOKEN_CONTRACT_ID:-}" +generate_client "course_milestone" "${VITE_COURSE_MILESTONE_CONTRACT_ID:-}" +generate_client "milestone_escrow" "${VITE_MILESTONE_ESCROW_CONTRACT_ID:-}" +generate_client "scholarship_treasury" "${VITE_SCHOLARSHIP_TREASURY_CONTRACT_ID:-}" +generate_client "scholar_nft" "${VITE_SCHOLAR_NFT_CONTRACT_ID:-}" + +echo "" +echo "✅ Done. Run 'npm run install:contracts' to build the generated packages." diff --git a/scripts/mint-test-usdc.sh b/scripts/mint-test-usdc.sh new file mode 100644 index 00000000..e6974df7 --- /dev/null +++ b/scripts/mint-test-usdc.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# Mint Test USDC Script +# Usage: ./scripts/mint-test-usdc.sh [amount] +# +# This script mints test USDC tokens to a specified address on Stellar Testnet. +# Default amount is 1000 USDC (with 7 decimals = 10000000000 stroops) + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if recipient address is provided +if [ -z "$1" ]; then + echo -e "${RED}Error: Recipient address is required${NC}" + echo "Usage: $0 [amount]" + echo "Example: $0 GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 1000" + exit 1 +fi + +RECIPIENT=$1 +AMOUNT=${2:-1000} + +# Convert amount to stroops (7 decimals for USDC) +AMOUNT_STROOPS=$((AMOUNT * 10000000)) + +echo -e "${YELLOW}Minting Test USDC...${NC}" +echo "Recipient: $RECIPIENT" +echo "Amount: $AMOUNT USDC ($AMOUNT_STROOPS stroops)" +echo "" + +# Check if we're in development or staging environment +if [ -f ".env" ]; then + source .env + NETWORK=${STELLAR_SCAFFOLD_ENV:-development} +else + NETWORK="development" +fi + +echo -e "${YELLOW}Environment: $NETWORK${NC}" + +# Get the USDC contract ID from the environment +if [ "$NETWORK" == "development" ]; then + # For local development, use the deployed test token + echo -e "${YELLOW}Looking up USDC test token contract ID...${NC}" + + # Try to get the contract ID from the stellar CLI + USDC_CONTRACT_ID=$(stellar contract id asset --asset native --network $NETWORK 2>/dev/null || echo "") + + if [ -z "$USDC_CONTRACT_ID" ]; then + echo -e "${RED}Error: Could not find USDC contract ID${NC}" + echo "Make sure you have deployed the contracts first by running: npm start" + exit 1 + fi +else + # For testnet/staging, you need to set the contract ID manually + if [ -z "$PUBLIC_USDC_CONTRACT_ID" ]; then + echo -e "${RED}Error: PUBLIC_USDC_CONTRACT_ID not set in .env${NC}" + echo "Please set PUBLIC_USDC_CONTRACT_ID in your .env file" + exit 1 + fi + USDC_CONTRACT_ID=$PUBLIC_USDC_CONTRACT_ID +fi + +echo "USDC Contract ID: $USDC_CONTRACT_ID" +echo "" + +# Mint tokens using the stellar CLI +echo -e "${YELLOW}Invoking mint function...${NC}" + +stellar contract invoke \ + --id $USDC_CONTRACT_ID \ + --source me \ + --network $NETWORK \ + -- \ + mint \ + --to $RECIPIENT \ + --amount $AMOUNT_STROOPS + +if [ $? -eq 0 ]; then + echo "" + echo -e "${GREEN}✓ Successfully minted $AMOUNT USDC to $RECIPIENT${NC}" + echo "" + echo "You can verify the balance by running:" + echo "stellar contract invoke --id $USDC_CONTRACT_ID --network $NETWORK -- balance --id $RECIPIENT" +else + echo -e "${RED}✗ Failed to mint USDC${NC}" + exit 1 +fi diff --git a/scripts/smoke-test-contracts.sh b/scripts/smoke-test-contracts.sh new file mode 100644 index 00000000..c8a4a940 --- /dev/null +++ b/scripts/smoke-test-contracts.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +# Smoke Test All V1 Contracts +# This script performs basic smoke tests on all deployed contracts +# to verify they're working correctly on Stellar Testnet + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Load contract addresses from environments.toml +if [ ! -f "environments.toml" ]; then + echo -e "${RED}Error: environments.toml not found${NC}" + echo "Please run ./scripts/deploy-testnet.sh first" + exit 1 +fi + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}V1 Contract Smoke Tests${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" + +# Function to test a contract +test_contract() { + local contract_name=$1 + local contract_id=$2 + local test_function=$3 + local test_description=$4 + + echo -e "${YELLOW}Testing $contract_name...${NC}" + echo -e "${YELLOW}Contract ID: $contract_id${NC}" + + if [ -z "$contract_id" ]; then + echo -e "${RED}✗ Contract ID not found for $contract_name${NC}" + return 1 + fi + + echo -e "${YELLOW}Function: $test_function${NC}" + + # Execute the test + stellar contract invoke \ + --id "$contract_id" \ + --network testnet \ + -- \ + "$test_function" \ + 2>/dev/null + + if [ $? -eq 0 ]; then + echo -e "${GREEN}✓ $contract_name smoke test passed${NC}" + else + echo -e "${RED}✗ $contract_name smoke test failed${NC}" + return 1 + fi + + echo "" +} + +# Read contract IDs from environments.toml +LEARN_TOKEN_ID=$(grep -A1 "learn_token = " environments.toml | grep -o 'learn_token = .*' | sed 's/learn_token = "//' | sed 's/"//.*$//') +GOVERNANCE_TOKEN_ID=$(grep -A1 "governance_token = " environments.toml | grep -o 'governance_token = .*' | sed 's/governance_token = "//' | sed 's/"//.*$//') +COURSE_MILESTONE_ID=$(grep -A1 "course_milestone = " environments.toml | grep -o 'course_milestone = .*' | sed 's/course_milestone = "//' | sed 's/"//.*$//') +MILESTONE_ESCROW_ID=$(grep -A1 "milestone_escrow = " environments.toml | grep -o 'milestone_escrow = .*' | sed 's/milestone_escrow = "//' | sed 's/"//.*$//') +SCHOLARSHIP_TREASURY_ID=$(grep -A1 "scholarship_treasury = " environments.toml | grep -o 'scholarship_treasury = .*' | sed 's/scholarship_treasury = "//' | sed 's/"//.*$//') +SCHOLAR_NFT_ID=$(grep -A1 "scholar_nft = " environments.toml | grep -o 'scholar_nft = .*' | sed 's/scholar_nft = "//' | sed 's/"//.*$//') + +# Check if all contracts are deployed +if [ -z "$LEARN_TOKEN_ID" ] || [ -z "$GOVERNANCE_TOKEN_ID" ] || [ -z "$COURSE_MILESTONE_ID" ] || [ -z "$MILESTONE_ESCROW_ID" ] || [ -z "$SCHOLARSHIP_TREASURY_ID" ] || [ -z "$SCHOLAR_NFT_ID" ]; then + echo -e "${RED}Error: One or more contract IDs not found in environments.toml${NC}" + echo "Please ensure all contracts are deployed first:" + echo "Run: ./scripts/deploy-testnet.sh" + exit 1 +fi + +echo -e "${YELLOW}Starting smoke tests...${NC}" +echo "" + +# Test LearnToken +test_contract "LearnToken" "$LEARN_TOKEN_ID" "metadata" "Get token metadata" + +# Test GovernanceToken +test_contract "GovernanceToken" "$GOVERNANCE_TOKEN_ID" "balance" "Get token balance for deployer" + +# Test CourseMilestone +test_contract "CourseMilestone" "$COURSE_MILESTONE_ID" "get_milestone" "Get milestone details" + +# Test MilestoneEscrow +test_contract "MilestoneEscrow" "$MILESTONE_ESCROW_ID" "get_escrow_balance" "Get escrow balance" + +# Test ScholarshipTreasury +test_contract "ScholarshipTreasury" "$SCHOLARSHIP_TREASURY_ID" "get_treasury_balance" "Get treasury balance" + +# Test ScholarNFT +test_contract "ScholarNFT" "$SCHOLAR_NFT_ID" "token_uri" "Get NFT metadata" + +echo "" +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}Smoke Tests Summary${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" +echo -e "${GREEN}✓ All smoke tests completed!${NC}" +echo "" +echo -e "${YELLOW}Next steps:${NC}" +echo "1. Fund deployer address for testing:" +echo " stellar friendbot fund testnet $DEPLOYER_ADDRESS" +echo "" +echo "2. Test contract functions with real parameters:" +echo " stellar contract invoke --id CONTRACT_ID --network testnet --FUNCTION_NAME --PARAMS" diff --git a/scripts/start-dev.mjs b/scripts/start-dev.mjs new file mode 100644 index 00000000..0deebe21 --- /dev/null +++ b/scripts/start-dev.mjs @@ -0,0 +1,51 @@ +import { spawn, spawnSync } from "node:child_process" + +function run(command, args, name) { + const child = spawn(command, args, { + stdio: "inherit", + shell: true, + windowsHide: false, + }) + + child.on("exit", (code) => { + if (code !== 0) { + process.exit(code ?? 1) + } + }) + + child.on("error", (error) => { + console.error(`[learnvault] Failed to start ${name}:`, error.message) + process.exit(1) + }) + + return child +} + +function hasStellarCli() { + const result = spawnSync("stellar", ["--version"], { + shell: true, + stdio: "ignore", + windowsHide: true, + }) + + return result.status === 0 +} + +const vite = run("vite", [], "vite") + +if (hasStellarCli()) { + run("stellar", ["scaffold", "watch", "--build-clients"], "stellar") +} else { + console.warn( + "[learnvault] Stellar CLI not found. Starting frontend only.\n" + + "[learnvault] Install with: npm install -g @stellar/stellar-cli", + ) +} + +process.on("SIGINT", () => { + vite.kill("SIGINT") +}) + +process.on("SIGTERM", () => { + vite.kill("SIGTERM") +}) diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 00000000..bc020591 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.env +.env.* +*.log +coverage +.DS_Store diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 00000000..b05122c6 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,88 @@ +# Server Configuration +PORT=4000 +NODE_ENV=development + +# CORS Configuration +# Frontend URL for CORS origin validation +# In development, defaults to http://localhost:5173 +# In production, set to your deployed frontend domain +FRONTEND_URL=http://localhost:5173 + +# Legacy CORS_ORIGIN (deprecated, use FRONTEND_URL instead) +# CORS_ORIGIN=http://localhost:5173 + +# Database +DATABASE_URL=postgresql://user:pass@localhost:5432/learnvault?sslmode=disable + +# Redis (optional, for rate limiting and nonce storage) +REDIS_URL=redis://localhost:6379 + +# Comments spam protection +# Global max number of comments allowed per address in a rolling 24h window +MAX_COMMENTS_PER_DAY=50 + +# JWT Authentication (REQUIRED) +# RS256 JWT cryptographic keys (PEM format). +# REQUIRED: These keys MUST be set in production. The server will refuse to start without them. +# In development, keys are auto-generated if omitted (tokens reset on each restart). +# +# Generate RSA keys with: +# openssl genrsa -out private.pem 2048 +# openssl rsa -in private.pem -pubout -out public.pem +# +# Copy the entire contents (including BEGIN/END lines, with literal \n for newlines): +JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" +JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" + +# Admin Authorization +# Comma-separated list of Stellar wallet addresses allowed to approve/reject milestones. +# If unset, any valid JWT is accepted (dev only). +ADMIN_ADDRESSES=GABC...,GDEF... +ADMIN_API_KEY=replace_with_secure_admin_api_key + +# Stellar/Soroban Configuration +STELLAR_NETWORK=testnet +SOROBAN_RPC_URL=https://soroban-testnet.stellar.org +# Required for on-chain transaction submission (approving milestones, minting NFTs) +# When missing, the server will return 503 Service Unavailable for on-chain actions. +STELLAR_SECRET_KEY=S... + +# Smart Contract Addresses +COURSE_MILESTONE_CONTRACT_ID=C... +LEARN_TOKEN_CONTRACT_ID=C... +SCHOLARSHIP_TREASURY_CONTRACT_ID=C... +MILESTONE_ESCROW_CONTRACT_ID=C... + +# Badge image CIDs (IPFS) +# Used by POST /api/credentials/metadata when generating NFT metadata image URLs +BADGE_CID_STELLAR=bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi +BADGE_CID_SOROBAN=bafybeihvvlkvjkbxy6qxzjzqxzqxzqxzqxzqxzqxzqxzqxzqxzqxzqxzqx +BADGE_CID_DEFI=bafybeidefi123456789abcdefghijklmnopqrstuvwxyz1234567890abc +BADGE_CID_BASE=bafybeiabc123456789defghijklmnopqrstuvwxyz1234567890abcdef + +# Event Indexer +STARTING_LEDGER=460000000 +POLL_INTERVAL_MS=5000 + +# Pinata IPFS Pinning +# Required for POST /api/upload and POST /api/credentials/metadata +# Get keys at https://app.pinata.cloud/keys +PINATA_API_KEY=your_pinata_api_key +PINATA_SECRET=your_pinata_secret_api_key + +# Optional: override the IPFS HTTP gateway used to construct public URLs. +# Defaults to https://gateway.pinata.cloud/ipfs +IPFS_GATEWAY_URL=https://your-dedicated-gateway.mypinata.cloud/ipfs + +# SendGrid Email Integration +EMAIL_API_KEY=SG.... +EMAIL_FROM=notifications@learnvault.xyz + + +# Resend Email Configuration (alternative to SendGrid) +# Email — Resend (https://resend.com) +RESEND_API_KEY=re_... +EMAIL_FROM=notifications@learnvault.xyz + +# Frontend URL (used to build links inside email templates) +FRONTEND_URL=http://localhost:5173 diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 00000000..978a614b --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,43 @@ +# Stage 1: Build +FROM node:22-alpine AS builder + +WORKDIR /app + +# Copy package metadata +COPY package*.json ./ + +# Install all dependencies (including dev dependencies) +RUN npm ci + +# Copy the rest of the source code +COPY tsconfig.json ./ +COPY src/ ./src/ +COPY scripts/ ./scripts/ + +# Build the TypeScript code +RUN npm run build + +# Stage 2: Production +FROM node:22-alpine AS runner + +WORKDIR /app + +# Copy package metadata +COPY package*.json ./ + +# Install only production dependencies +RUN npm ci --omit=dev + +# Copy compiled source and scripts from builder +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/scripts ./scripts + +# Set environment to production +ENV NODE_ENV=production +ENV PORT=4000 + +# Expose port +EXPOSE 4000 + +# Start the application +CMD ["npm", "start"] diff --git a/server/content/courses/README.md b/server/content/courses/README.md new file mode 100644 index 00000000..2222f1bb --- /dev/null +++ b/server/content/courses/README.md @@ -0,0 +1,18 @@ +# LearnVault Course Content + +This directory contains seed curriculum content for LearnVault demos and future +database seeding. + +## Structure + +- `index.json` — master catalog of published courses and their lesson files. +- `/course.json` — per-course metadata (title, level, lesson list). +- `/lessons/*.md` — lesson content in Markdown. +- `/lessons/*.quiz.json` — quiz questions for each lesson. +- `/milestone.quiz.json` — end-of-track milestone quiz. + +## Notes + +- Lesson quizzes follow the shape expected by `src/components/QuizEngine.tsx`. +- `reviewedBy` is intentionally left blank until a technical contributor signs + off on the lesson accuracy. diff --git a/server/content/courses/defi-fundamentals/course.json b/server/content/courses/defi-fundamentals/course.json new file mode 100644 index 00000000..fb5b9969 --- /dev/null +++ b/server/content/courses/defi-fundamentals/course.json @@ -0,0 +1,45 @@ +{ + "id": "defi-fundamentals", + "title": "DeFi Fundamentals", + "level": "beginner", + "published": true, + "description": "Understand DeFi primitives (tokens, AMMs, yield, risk) and how DeFi concepts map onto Stellar and Soroban.", + "lessons": [ + { + "id": "defi-fundamentals-1", + "title": "What is DeFi?", + "kind": "theory", + "markdown": "lessons/01-what-is-defi.md", + "quiz": "lessons/01-what-is-defi.quiz.json", + "reviewedBy": null + }, + { + "id": "defi-fundamentals-2", + "title": "Tokens and AMMs", + "kind": "theory", + "markdown": "lessons/02-tokens-and-amms.md", + "quiz": "lessons/02-tokens-and-amms.quiz.json", + "reviewedBy": null + }, + { + "id": "defi-fundamentals-3", + "title": "Yield and risk", + "kind": "theory", + "markdown": "lessons/03-yield-and-risk.md", + "quiz": "lessons/03-yield-and-risk.quiz.json", + "reviewedBy": null + }, + { + "id": "defi-fundamentals-4", + "title": "DeFi on Stellar (SDEX, liquidity pools)", + "kind": "theory", + "markdown": "lessons/04-defi-on-stellar.md", + "quiz": "lessons/04-defi-on-stellar.quiz.json", + "reviewedBy": null + } + ], + "milestoneQuiz": { + "passingScore": 80, + "quiz": "milestone.quiz.json" + } +} diff --git a/server/content/courses/defi-fundamentals/lessons/01-what-is-defi.md b/server/content/courses/defi-fundamentals/lessons/01-what-is-defi.md new file mode 100644 index 00000000..ac85b8e0 --- /dev/null +++ b/server/content/courses/defi-fundamentals/lessons/01-what-is-defi.md @@ -0,0 +1,201 @@ +# Lesson 1 — What is DeFi? + +## Summary + +**DeFi** (Decentralized Finance) is a set of financial services built on open +blockchain networks. Instead of relying on banks, brokers, or any middleman, +DeFi uses software — called smart contracts — to let people lend, borrow, trade, +and save money directly with each other. + +No application forms. No waiting days for approval. No asking permission. + +## Learning Objectives + +- Explain what DeFi is in plain language +- Understand why DeFi matters especially in emerging markets +- Identify the core building blocks of DeFi +- Recognize the real risks — and not underestimate them +- Know how DeFi works on Stellar, and where LearnVault fits in + +--- + +## Why Traditional Finance Leaves People Out + +Imagine you are a 22-year-old developer in Lagos. You want to save money, send +money to your family in another state, and maybe get a small loan to buy a +laptop for your freelance work. Here is what traditional finance offers you: + +- **Savings account**: requires a formal address, ID documents, and a minimum + balance you may not have +- **Sending money home**: a remittance fee of 8–12% per transfer, plus a 2–3 day + wait +- **Loan**: rejected without a credit history or collateral, or offered at 30%+ + interest rates + +This is not a Lagos-only problem. Hundreds of millions of people across Africa, +Southeast Asia, and Latin America face the same barriers. The global financial +system was not designed for them. + +DeFi was. + +--- + +## What DeFi Actually Is + +DeFi is financial infrastructure built on public blockchains. The rules are +encoded in smart contracts — programs that run automatically when conditions are +met, without any human in the middle deciding whether you qualify. + +Think of it this way: a traditional bank account is a promise from an +institution that they will keep your money safe and give it back when you ask. A +DeFi protocol is a public program — anyone can read its code, anyone can use it, +and it cannot discriminate. + +The most important characteristic of DeFi: **the rules apply equally to everyone +who meets them**. + +--- + +## The Key Building Blocks of DeFi + +### Stablecoins + +Regular cryptocurrencies like Bitcoin fluctuate in price wildly. Stablecoins are +cryptocurrencies pegged to a stable currency — usually the US dollar. USDC (USD +Coin) is a stablecoin: 1 USDC always equals approximately $1. + +Stablecoins are the foundation of DeFi. They let you participate in financial +activities without the volatility of regular crypto. + +### DEXes — Decentralized Exchanges + +A DEX lets you swap one token for another without a centralized company in the +middle. On Stellar, this is called the **SDEX** (Stellar Decentralized +Exchange). You can trade XLM for USDC, or any other supported asset pair, +directly from your wallet. + +### Liquidity Pools + +When you use a DEX, you might wonder: where does the other side of the trade +come from? The answer is liquidity pools. + +A liquidity pool is a shared pot of two tokens — for example, XLM and USDC — +that people deposit to earn a share of trading fees. When you make a swap, you +are trading against the pool, not against another person. People who deposit are +called **liquidity providers (LPs)**, and they earn a portion of every trade +fee. + +Stellar supports liquidity pools natively, allowing anyone to become a liquidity +provider with minimal fees. + +### Yield + +Yield is the return you earn by putting your assets to work in DeFi — providing +liquidity, lending to protocols, or staking. It is typically expressed as APY +(Annual Percentage Yield). Yield comes from real activity: trading fees, loan +interest, protocol incentives. + +--- + +## The Risks — Being Honest + +DeFi has genuine risks. Understanding them is part of becoming a responsible +participant. + +**Smart contract bugs**: The code is public, but that does not mean it is +perfect. Bugs in smart contracts have led to hundreds of millions of dollars in +losses. Protocols that have been audited by reputable security firms are safer, +but no audit is a guarantee. + +**Impermanent loss**: If you provide liquidity to a pool and the price of one +asset changes significantly relative to the other, you can end up with less +value than if you had simply held the assets. This is called impermanent loss — +it becomes permanent if you withdraw during that imbalance. + +**Rug pulls**: Some projects launch tokens, attract investment, then the +founders disappear with the funds. This is a scam. Signs of a rug pull include +anonymous teams, no audits, promises of unrealistic yields, and locked liquidity +that is actually controlled by insiders. + +**User error**: DeFi does not have a "forgot my password" button. If you send +funds to the wrong address, approve a malicious contract, or lose your seed +phrase, there is no customer service line. You are responsible for your own +security. + +The rule of thumb: **never put in more than you can afford to lose while you are +still learning**. + +--- + +## DeFi on Stellar + +Stellar was built specifically for financial access and cross-border payments, +making it one of the most practical blockchains for DeFi in emerging markets. + +**SDEX — Stellar Decentralized Exchange**: The order book DEX built directly +into the Stellar protocol. Fast, cheap (fractions of a cent per transaction), +and accessible from any Stellar wallet including Freighter. + +**Liquidity Pools**: Stellar's AMM (Automated Market Maker) pools allow anyone +to provide liquidity for asset pairs. Fees are low, and the process is +accessible to anyone with a Freighter wallet. + +**USDC on Stellar**: Stellar supports native USDC issued by Circle. This means +you can hold, send, and use dollar-equivalent stablecoins with near-zero fees +and near-instant settlement — a direct answer to the expensive, slow remittance +problem described above. + +A Nigerian freelancer receiving payment in USDC on Stellar pays almost nothing +in fees and receives funds in seconds, compared to several days and 8–12% fees +through a traditional money transfer service. + +--- + +## How LearnVault Fits Into DeFi + +LearnVault is itself a DeFi application — not for trading, but for education +funding. + +- **On-chain credentials**: Your LearnTokens are soulbound tokens on Stellar. + They are your verifiable proof of learning — a reputation score no one can + fake or take away. +- **Scholarship treasury**: Donor funds flow into a smart contract treasury. No + single person controls it; the community governs it through DAO voting. +- **Milestone disbursements**: Approved scholarship funds are released in stages + via escrow contracts, only as learners complete verified milestones. + +This is DeFi applied to education: transparent, permissionless, and governed by +the community rather than a central authority. + +--- + +## Key Takeaways + +- DeFi is financial software built on public blockchains — no middlemen, no + gatekeepers, equal rules for everyone +- It matters most for people excluded from traditional finance: high remittance + fees, no credit history, no formal documentation +- Core primitives: stablecoins (stable value), DEXes (trading), liquidity pools + (shared market-making), yield (returns on deployed assets) +- Real risks exist: smart contract bugs, impermanent loss, scams, user error — + never invest more than you can afford to lose +- Stellar is particularly well-suited for DeFi in emerging markets: fast, cheap, + USDC-native, with a built-in DEX and liquidity pool support +- LearnVault uses DeFi mechanics — on-chain credentials, DAO treasury, escrow + disbursements — to fund education transparently + +--- + +## Further Reading + +- [Stellar Decentralized Exchange (SDEX) — Stellar Docs](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/dex) +- [Stellar Liquidity Pools — Stellar Docs](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/liquidity-pools) +- [What is DeFi? — Investopedia](https://www.investopedia.com/decentralized-finance-defi-5113835) +- [What is Impermanent Loss? — Investopedia](https://www.investopedia.com/impermanent-loss-7104019) +- [USDC on Stellar — Circle](https://www.circle.com/en/usdc/stellar) + +--- + +## Reviewer Sign-off + +Reviewed by: _TBD_ diff --git a/server/content/courses/defi-fundamentals/lessons/01-what-is-defi.quiz.json b/server/content/courses/defi-fundamentals/lessons/01-what-is-defi.quiz.json new file mode 100644 index 00000000..eae12bc0 --- /dev/null +++ b/server/content/courses/defi-fundamentals/lessons/01-what-is-defi.quiz.json @@ -0,0 +1,34 @@ +{ + "lessonId": "defi-fundamentals-1", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "DeFi primarily replaces which role with code and open protocols?", + "options": [ + "Internet routers", + "Financial intermediaries (some parts of them)", + "Operating systems", + "Mobile phones" + ], + "correctIndex": 1 + }, + { + "id": 2, + "text": "Which component provides external data (like prices) that many DeFi protocols rely on?", + "options": ["Oracles", "Memes", "Seed phrases", "Browser cookies"], + "correctIndex": 0 + }, + { + "id": 3, + "text": "Which is a realistic DeFi risk even when code is open?", + "options": [ + "Smart contract bugs", + "Free guaranteed profit", + "No need for wallets", + "Infinite liquidity" + ], + "correctIndex": 0 + } + ] +} diff --git a/server/content/courses/defi-fundamentals/lessons/02-tokens-and-amms.md b/server/content/courses/defi-fundamentals/lessons/02-tokens-and-amms.md new file mode 100644 index 00000000..643944a3 --- /dev/null +++ b/server/content/courses/defi-fundamentals/lessons/02-tokens-and-amms.md @@ -0,0 +1,59 @@ +# Lesson 2 — Tokens and AMMs (Theory) + +## Summary + +Two building blocks show up everywhere in DeFi: + +1. **Tokens**: the units of value +2. **AMMs** (Automated Market Makers): a way to trade tokens using liquidity + pools + +## Learning objectives + +- Explain what a fungible token is +- Understand how AMM liquidity pools enable swaps +- Define slippage and why it happens + +## Tokens (quick refresher) + +A fungible token is interchangeable: 1 unit is the same as another unit. +Examples include stablecoins, wrapped assets, or protocol tokens. + +Tokens are used for: + +- Payments and settlement +- Collateral in lending +- Governance votes + +## AMMs in plain language + +An AMM is a set of rules that sets a price based on the pool’s balances. + +A simple mental model: + +- A pool holds Token A and Token B +- Traders swap A for B (or vice versa) +- The price moves based on the pool ratio + +### Slippage + +**Slippage** is the difference between the expected price and the executed +price. It happens because your trade changes the pool ratio. + +Bigger trade vs pool size ⇒ more slippage. + +### Liquidity providers (LPs) + +Liquidity providers deposit two assets into a pool and receive an LP position. +In return, they may earn fees from swaps. + +LPs also face risk (e.g., impermanent loss) when prices move. + +## Knowledge check + +- What causes slippage in an AMM? +- Why might someone provide liquidity to a pool? + +## Reviewer sign-off + +Reviewed by: _TBD_ diff --git a/server/content/courses/defi-fundamentals/lessons/02-tokens-and-amms.quiz.json b/server/content/courses/defi-fundamentals/lessons/02-tokens-and-amms.quiz.json new file mode 100644 index 00000000..eed9b870 --- /dev/null +++ b/server/content/courses/defi-fundamentals/lessons/02-tokens-and-amms.quiz.json @@ -0,0 +1,39 @@ +{ + "lessonId": "defi-fundamentals-2", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "In an AMM, why does a large swap often experience more slippage?", + "options": [ + "Because it changes the pool’s ratio more", + "Because wallets disable small trades", + "Because oracles refuse to update", + "Because fees are always zero" + ], + "correctIndex": 0 + }, + { + "id": 2, + "text": "Who are liquidity providers (LPs)?", + "options": [ + "Users who deposit assets into a pool to enable swaps", + "Validators that run Stellar Core", + "People who write wallet seed phrases", + "A type of oracle" + ], + "correctIndex": 0 + }, + { + "id": 3, + "text": "A fungible token means…", + "options": [ + "Every token is unique like an NFT", + "Units are interchangeable (1 unit equals another)", + "Tokens cannot be transferred", + "Tokens must be physical" + ], + "correctIndex": 1 + } + ] +} diff --git a/server/content/courses/defi-fundamentals/lessons/03-yield-and-risk.md b/server/content/courses/defi-fundamentals/lessons/03-yield-and-risk.md new file mode 100644 index 00000000..f1576675 --- /dev/null +++ b/server/content/courses/defi-fundamentals/lessons/03-yield-and-risk.md @@ -0,0 +1,66 @@ +# Lesson 3 — Yield and risk (Theory) + +## Summary + +Yield is attractive, but it’s never “free.” In DeFi, yield often comes with +smart contract risk, market risk, and sometimes complex incentive systems. + +## Learning objectives + +- Identify common sources of yield +- Understand APR vs APY +- Describe major categories of DeFi risk + +## Where yield comes from + +Common sources: + +- **Fees**: trading fees in AMMs +- **Borrow interest**: lenders earn interest paid by borrowers +- **Incentives**: protocols distribute tokens to bootstrap usage + +## APR vs APY + +- **APR**: simple annual rate (not accounting for compounding) +- **APY**: includes compounding effects + +Always ask: “How is this yield produced? Fees? Borrowing? Subsidies?” + +## Major risk categories + +### Smart contract risk + +Bugs can lead to loss of funds. Audits help, but they don’t guarantee safety. + +### Market risk + +Token prices change. Collateral can fall. Positions can be liquidated. + +### Oracle risk + +If the price feed is wrong or manipulated, protocols can behave incorrectly. + +### Liquidity risk + +If markets are thin, exiting a position can be expensive (slippage) or +impossible. + +### Operational risk + +Phishing, malicious approvals, wrong network, wrong address. + +## Practical safety checklist + +- Use trusted sources and verified links +- Start with small amounts +- Understand what you’re approving and why +- Prefer battle-tested protocols for larger positions + +## Knowledge check + +- Name two sources of DeFi yield. +- Name two categories of DeFi risk. + +## Reviewer sign-off + +Reviewed by: _TBD_ diff --git a/server/content/courses/defi-fundamentals/lessons/03-yield-and-risk.quiz.json b/server/content/courses/defi-fundamentals/lessons/03-yield-and-risk.quiz.json new file mode 100644 index 00000000..42fefcc8 --- /dev/null +++ b/server/content/courses/defi-fundamentals/lessons/03-yield-and-risk.quiz.json @@ -0,0 +1,39 @@ +{ + "lessonId": "defi-fundamentals-3", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "Which is a common source of yield in DeFi?", + "options": [ + "AMM trading fees", + "Airplane tickets", + "Browser cache", + "Sequence numbers" + ], + "correctIndex": 0 + }, + { + "id": 2, + "text": "APR vs APY: APY typically includes…", + "options": [ + "Compounding", + "Seed phrases", + "Ledger pruning", + "Network upgrades" + ], + "correctIndex": 0 + }, + { + "id": 3, + "text": "Which is an example of operational risk?", + "options": [ + "Phishing links", + "Perfect audits", + "Unlimited liquidity", + "Guaranteed slippage" + ], + "correctIndex": 0 + } + ] +} diff --git a/server/content/courses/defi-fundamentals/lessons/04-defi-on-stellar.md b/server/content/courses/defi-fundamentals/lessons/04-defi-on-stellar.md new file mode 100644 index 00000000..c9e8f96c --- /dev/null +++ b/server/content/courses/defi-fundamentals/lessons/04-defi-on-stellar.md @@ -0,0 +1,57 @@ +# Lesson 4 — DeFi on Stellar (SDEX, liquidity pools) (Theory) + +## Summary + +Stellar supports asset issuance and exchange natively, and Soroban expands the +design space for DeFi-style applications. + +This lesson connects DeFi concepts to the Stellar ecosystem: assets, trustlines, +order books, liquidity pools, and smart contracts. + +## Learning objectives + +- Explain how Stellar assets and trustlines map to tokens +- Understand the difference between order books and liquidity pools +- Describe what role Soroban can play in DeFi on Stellar + +## Tokens on Stellar + +On Stellar, many “tokens” are issued assets. + +To hold an issued asset, users create a trustline. This is part of the protocol +and is an important safety/consent mechanism. + +## Exchange on Stellar: SDEX + +Stellar’s built-in exchange supports: + +- Offers (order book) +- Path payments (routing through markets) + +Order books match buyers and sellers directly. Prices come from outstanding +offers. + +## Liquidity pools + +Liquidity pools provide an AMM-like experience. Traders swap against pooled +liquidity, and LPs may earn fees. + +## Where Soroban fits + +Soroban enables: + +- Custom AMMs and routing logic +- Lending/borrowing logic +- Vaults, staking, and more complex stateful protocols + +Even with smart contracts, the basics remain: understand the asset model, +liquidity constraints, and risk. + +## Knowledge check + +- What protocol feature lets users consent to hold an issued asset? +- What’s one difference between an order book and a liquidity pool? + +## Reviewer sign-off + +Reviewed by: _TBD_ diff --git a/server/content/courses/defi-fundamentals/lessons/04-defi-on-stellar.quiz.json b/server/content/courses/defi-fundamentals/lessons/04-defi-on-stellar.quiz.json new file mode 100644 index 00000000..740ab595 --- /dev/null +++ b/server/content/courses/defi-fundamentals/lessons/04-defi-on-stellar.quiz.json @@ -0,0 +1,34 @@ +{ + "lessonId": "defi-fundamentals-4", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "On Stellar, what mechanism typically allows an account to hold an issued asset?", + "options": ["A trustline", "A browser cookie", "A QR code", "A miner"], + "correctIndex": 0 + }, + { + "id": 2, + "text": "Which best describes an order book exchange?", + "options": [ + "Prices come from matching buy/sell offers", + "Prices are fixed forever", + "It requires no users", + "It removes transaction fees" + ], + "correctIndex": 0 + }, + { + "id": 3, + "text": "Which is a realistic way Soroban can contribute to DeFi on Stellar?", + "options": [ + "Enable custom protocol logic like AMMs or lending", + "Replace wallets entirely", + "Eliminate market risk", + "Stop slippage from existing" + ], + "correctIndex": 0 + } + ] +} diff --git a/server/content/courses/defi-fundamentals/milestone.quiz.json b/server/content/courses/defi-fundamentals/milestone.quiz.json new file mode 100644 index 00000000..d598647d --- /dev/null +++ b/server/content/courses/defi-fundamentals/milestone.quiz.json @@ -0,0 +1,51 @@ +{ + "courseId": "defi-fundamentals", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "Which is a core DeFi primitive?", + "options": ["Tokens", "Fax machines", "Airports", "Hard drives"], + "correctIndex": 0 + }, + { + "id": 2, + "text": "Slippage is primarily caused by…", + "options": [ + "A trade changing the pool ratio / market depth", + "Wallet passwords", + "Memo fields", + "Sequence numbers" + ], + "correctIndex": 0 + }, + { + "id": 3, + "text": "Which is a common DeFi yield source?", + "options": [ + "Trading fees", + "Keyboard shortcuts", + "Phone wallpapers", + "Network passphrases" + ], + "correctIndex": 0 + }, + { + "id": 4, + "text": "Which is a real DeFi risk category?", + "options": [ + "Smart contract bugs", + "Guaranteed profit", + "No wallets needed", + "Infinite liquidity" + ], + "correctIndex": 0 + }, + { + "id": 5, + "text": "On Stellar, users typically opt-in to hold issued assets by creating a…", + "options": ["Trustline", "Browser tab", "Mining rig", "Seed email"], + "correctIndex": 0 + } + ] +} diff --git a/server/content/courses/index.json b/server/content/courses/index.json new file mode 100644 index 00000000..51a8ef1c --- /dev/null +++ b/server/content/courses/index.json @@ -0,0 +1,38 @@ +[ + { + "id": "stellar-basics", + "title": "Introduction to Stellar & Soroban", + "description": "Learn the fundamentals of the Stellar blockchain, accounts, transactions, and your first Soroban smart contract.", + "difficulty": "beginner", + "estimated_hours": 4, + "lesson_count": 4, + "lrn_reward": 100, + "cover_image": "/assets/courses/stellar-basics.png", + "tags": ["stellar", "soroban", "beginner"], + "on_chain_course_id": 1 + }, + { + "id": "soroban-contracts", + "title": "Soroban Smart Contract Development", + "description": "Master Soroban smart contract development with advanced patterns, testing strategies, and deployment best practices.", + "difficulty": "intermediate", + "estimated_hours": 4, + "lesson_count": 4, + "lrn_reward": 200, + "cover_image": "/assets/courses/soroban-contracts.png", + "tags": ["soroban", "smart-contracts", "intermediate", "rust"], + "on_chain_course_id": 2 + }, + { + "id": "defi-fundamentals", + "title": "DeFi Fundamentals on Stellar", + "description": "Explore decentralized finance on Stellar including token swaps, liquidity pools, and yield strategies.", + "difficulty": "intermediate", + "estimated_hours": 4, + "lesson_count": 4, + "lrn_reward": 150, + "cover_image": "/assets/courses/defi-fundamentals.png", + "tags": ["defi", "stellar", "intermediate", "liquidity", "tokens"], + "on_chain_course_id": 3 + } +] diff --git a/server/content/courses/soroban-contracts/lesson-02-first-contract.md b/server/content/courses/soroban-contracts/lesson-02-first-contract.md new file mode 100644 index 00000000..6f39f6e5 --- /dev/null +++ b/server/content/courses/soroban-contracts/lesson-02-first-contract.md @@ -0,0 +1,439 @@ +# Lesson 2: Your First Soroban Contract - A Simple Counter + +Welcome to your first hands-on Soroban smart contract development! In this +lesson, you'll write a simple counter contract that demonstrates the fundamental +patterns used in Soroban development. These same patterns are used throughout +the LearnVault project, especially in the CourseMilestone contract. + +## Project Structure — What a Soroban Contract Directory Looks Like + +A typical Soroban contract follows a standard Rust project structure with some +specific conventions: + +``` +my-counter/ +├── Cargo.toml # Rust dependencies and project metadata +├── src/ +│ ├── lib.rs # Main contract implementation +│ └── test.rs # Unit tests for the contract +└── .gitignore # Git ignore file +``` + +Let's look at the essential `Cargo.toml` for a Soroban contract: + +```toml +[package] +name = "my-counter" +version = "0.1.0" +edition = "2021" + +[dependencies] +soroban-sdk = "23.0.1" + +[dev-dependencies] +soroban-sdk = { version = "23.0.1", features = ["testutils"] } + +[lib] +crate-type = ["cdylib"] + +[profile.release] +opt-level = "z" +lto = true +``` + +Key points: + +- `soroban-sdk` is the main dependency for Soroban development +- `testutils` feature enables testing utilities +- `crate-type = ["cdylib"]` tells Rust to compile as a dynamic library suitable + for WASM + +## The Contract Struct — #[contract] and #[contractimpl] + +Every Soroban contract starts with a struct that represents your contract. The +`#[contract]` and `#[contractimpl]` attributes are crucial markers that tell the +Soroban SDK how to treat your code. + +```rust +#![no_std] + +use soroban_sdk::{contract, contractimpl, Address, Env}; + +#[contract] +pub struct Counter; + +#[contractimpl] +impl Counter { + pub fn new(env: Env, initializer: Address) -> i32 { + // Constructor logic here + 0 + } + + pub fn increment(env: Env) -> i32 { + // Increment logic here + 0 + } + + pub fn get(env: Env) -> i32 { + // Get current value logic here + 0 + } +} +``` + +Breaking this down: + +- `#![no_std]` tells Rust not to use the standard library (smart contracts run + in constrained environments) +- `#[contract]` marks `Counter` as a contract struct +- `#[contractimpl]` marks the implementation block as containing callable + contract functions +- Every function receives an `Env` parameter - the Soroban environment providing + access to storage, crypto, and other blockchain features + +## Storage — Instance vs Persistent Storage, Contracttype Keys + +Soroban provides two types of storage, each with different characteristics: + +### Instance Storage + +- Lives as long as the contract instance exists +- Gets reset when contract code is upgraded +- Good for configuration data that should reset on upgrades + +### Persistent Storage + +- Survives contract upgrades +- Good for user data that must persist across upgrades +- More expensive gas costs than instance storage + +### Storage Keys + +Storage keys need to be serializable and unique. The `#[contracttype]` attribute +makes a type usable as a storage key: + +```rust +use soroban_sdk::{contracttype, Symbol}; + +// Simple symbol key for instance storage +const COUNTER_KEY: Symbol = soroban_sdk::symbol_short!("COUNTER"); + +// Enum key for more complex data organization +#[derive(Clone, contracttype)] +pub enum DataKey { + Counter, + LastUpdated, + User(Address), +} +``` + +## Writing a Counter — Increment, Get Functions with Real Rust Code Snippets + +Now let's build our complete counter contract with proper storage management: + +```rust +#![no_std] + +use soroban_sdk::{ + contract, contractimpl, Address, Env, Symbol, panic_with_error, +}; + +// Storage key for our counter value +const COUNTER_KEY: Symbol = soroban_sdk::symbol_short!("COUNTER"); +const OWNER_KEY: Symbol = soroban_sdk::symbol_short!("OWNER"); + +// Error types for better error handling +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + NotAuthorized = 1, + NotInitialized = 2, +} + +#[contract] +pub struct Counter; + +#[contractimpl] +impl Counter { + /// Initialize the contract with an owner + pub fn initialize(env: Env, owner: Address) { + // Check if already initialized + if env.storage().instance().has(&OWNER_KEY) { + panic_with_error!(&env, Error::NotInitialized); + } + + // Set the owner + env.storage().instance().set(&OWNER_KEY, &owner); + + // Initialize counter to 0 + env.storage().instance().set(&COUNTER_KEY, &0_i32); + } + + /// Increment the counter by 1 + pub fn increment(env: Env) -> i32 { + // Get current value, default to 0 if not set + let current = env.storage() + .instance() + .get::<_, i32>(&COUNTER_KEY) + .unwrap_or(0); + + let new_value = current + 1; + + // Store the new value + env.storage().instance().set(&COUNTER_KEY, &new_value); + + new_value + } + + /// Get the current counter value + pub fn get(env: Env) -> i32 { + env.storage() + .instance() + .get::<_, i32>(&COUNTER_KEY) + .unwrap_or(0) + } + + /// Get the contract owner + pub fn get_owner(env: Env) -> Address { + env.storage() + .instance() + .get::<_, Address>(&OWNER_KEY) + .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)) + } +} +``` + +Key concepts in this implementation: + +1. **Storage Access**: `env.storage().instance()` provides access to instance + storage +2. **Error Handling**: `panic_with_error!` provides structured error reporting +3. **Default Values**: `unwrap_or(0)` provides sensible defaults +4. **Type Safety**: All storage operations are type-safe at compile time + +## Writing a Test — Using Env::default(), mock_all_auths(), Assertions + +Testing is crucial for smart contract development. Soroban provides excellent +testing utilities: + +```rust +#[cfg(test)] +mod test { + use soroban_sdk::{Address, Env}; + use crate::{Counter, CounterClient, Error}; + + fn setup_test() -> (Env, CounterClient, Address) { + let env = Env::default(); + let contract_id = env.register(Counter, ()); + let client = CounterClient::new(&env, &contract_id); + let owner = Address::generate(&env); + + // Mock all authentication for setup + env.mock_all_auths(); + client.initialize(&owner); + env.set_auths(&[]); // Clear auths for actual tests + + (env, client, owner) + } + + #[test] + fn test_counter_initialization() { + let env = Env::default(); + let contract_id = env.register(Counter, ()); + let client = CounterClient::new(&env, &contract_id); + let owner = Address::generate(&env); + + // Test initialization + client.initialize(&owner); + + // Verify initial state + assert_eq!(client.get(), 0); + assert_eq!(client.get_owner(), owner); + + // Test double initialization fails + let result = client.try_initialize(&owner); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::NotInitialized as u32 + ))) + ); + } + + #[test] + fn test_increment_functionality() { + let (env, client, _owner) = setup_test(); + + // Initial value should be 0 + assert_eq!(client.get(), 0); + + // Increment and check + assert_eq!(client.increment(), 1); + assert_eq!(client.get(), 1); + + // Multiple increments + assert_eq!(client.increment(), 2); + assert_eq!(client.increment(), 3); + assert_eq!(client.get(), 3); + } + + #[test] + fn test_persistence_across_calls() { + let (env, client, _owner) = setup_test(); + + // Increment several times + for i in 1..=5 { + assert_eq!(client.increment(), i); + } + + // Value persists + assert_eq!(client.get(), 5); + + // New increments continue from saved value + assert_eq!(client.increment(), 6); + assert_eq!(client.get(), 6); + } +} +``` + +Testing patterns demonstrated: + +1. **Setup Function**: Reusable test setup with `Env::default()` and + `env.register()` +2. **Authentication Mocking**: `mock_all_auths()` for setup, `set_auths(&[])` + for actual tests +3. **Error Testing**: Using `try_*` methods and checking error results +4. **State Verification**: Assertions to verify contract behavior + +## Building to WASM — cargo build --target wasm32v1-none + +Soroban contracts compile to WebAssembly (WASM) for blockchain execution. Here's +how to build your contract: + +```bash +# Install the WASM target if you haven't already +rustup target add wasm32v1-none + +# Build the contract +cargo build --target wasm32v1-none --release + +# The WASM file will be at: +# target/wasm32v1-none/release/my_counter.wasm +``` + +For development, you can also use the Soroban CLI: + +```bash +# Install soroban-cli (if not already installed) +cargo install soroban-cli + +# Build using soroban-cli (handles target automatically) +soroban contract build + +# This creates a .wasm file in the target/wasm32v1-none/release directory +``` + +The WASM file is what gets deployed to the Stellar network. It contains your +compiled contract logic in a format that the Stellar runtime can execute. + +## How This Relates to LearnVault — CourseMilestone Uses the Same Patterns + +The patterns you've learned here are directly applicable to the LearnVault +project. Let's examine how the CourseMilestone contract uses these same +concepts: + +**Contract Structure**: Like our counter, CourseMilestone uses `#[contract]` and +`#[contractimpl]` attributes: + +```rust +// From CourseMilestone contract +#[contract] +pub struct CourseMilestone; + +#[contractimpl] +impl CourseMilestone { + pub fn initialize(env: Env, admin: Address, learn_token: Address) { + // Similar initialization pattern + } +} +``` + +**Storage Management**: CourseMilestone uses both instance and persistent +storage: + +```rust +// Instance storage for contract configuration +const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); +const LEARN_TOKEN_KEY: Symbol = symbol_short!("LEARN_TOKEN"); + +// Persistent storage for user data +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Milestone(u32), + StudentProgress(Address), +} +``` + +**Error Handling**: CourseMilestone defines comprehensive error types just like +our counter: + +```rust +#[contracterror] +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + NotInitialized = 1, + Unauthorized = 2, + AlreadyCompleted = 3, + // ... more error types +} +``` + +**Testing Patterns**: The CourseMilestone tests follow the same structure we +used: + +```rust +fn setup(env: &Env) -> (Address, Address, CourseMilestoneClient) { + let admin = Address::generate(env); + let learn_token = Address::generate(env); + let contract_id = env.register(CourseMilestone, ()); + let client = CourseMilestoneClient::new(env, &contract_id); + + env.mock_all_auths(); + client.initialize(&admin, &learn_token); + + (contract_id, admin, client) +} +``` + +The key difference is complexity - CourseMilestone manages course progress, +token rewards, and multiple user interactions, while our counter focuses on a +single value. But the fundamental patterns are identical. + +## Challenge Exercise + +Now it's your turn to apply what you've learned! Modify the counter contract to +only allow the owner to increment the counter. + +**Requirements:** + +1. The `increment` function should check that the caller is the contract owner +2. If an unauthorized user tries to increment, return an appropriate error +3. Add a test case that verifies the authorization works correctly +4. Add a test case that verifies unauthorized access is rejected + +**Hints:** + +- You'll need to use `require_auth()` to check the caller +- The `get_owner()` function we wrote will be helpful +- Look at how CourseMilestone handles authorization in its functions +- Remember to mock authentication in your tests + +This exercise will reinforce the authentication patterns that are critical in +real-world smart contracts, especially in the LearnVault ecosystem where +controlling who can perform actions is essential for maintaining course +integrity and token distribution fairness. + +Good luck, and enjoy building your Soroban contracts! diff --git a/server/content/courses/soroban-fundamentals/course.json b/server/content/courses/soroban-fundamentals/course.json new file mode 100644 index 00000000..57676781 --- /dev/null +++ b/server/content/courses/soroban-fundamentals/course.json @@ -0,0 +1,45 @@ +{ + "id": "soroban-fundamentals", + "title": "Soroban Smart Contract Basics", + "level": "intermediate", + "published": true, + "description": "Build and test your first Soroban smart contracts using Rust, storage patterns, and practical testing workflows.", + "lessons": [ + { + "id": "soroban-fundamentals-1", + "title": "Rust basics for smart contract devs", + "kind": "theory", + "markdown": "lessons/01-rust-basics-for-smart-contract-devs.md", + "quiz": "lessons/01-rust-basics-for-smart-contract-devs.quiz.json", + "reviewedBy": null + }, + { + "id": "soroban-fundamentals-2", + "title": "Writing your first Soroban contract", + "kind": "practical", + "markdown": "lessons/02-writing-your-first-soroban-contract.md", + "quiz": "lessons/02-writing-your-first-soroban-contract.quiz.json", + "reviewedBy": null + }, + { + "id": "soroban-fundamentals-3", + "title": "Contract storage and state", + "kind": "theory", + "markdown": "lessons/03-contract-storage-and-state.md", + "quiz": "lessons/03-contract-storage-and-state.quiz.json", + "reviewedBy": null + }, + { + "id": "soroban-fundamentals-4", + "title": "Testing your contract", + "kind": "practical", + "markdown": "lessons/04-testing-your-contract.md", + "quiz": "lessons/04-testing-your-contract.quiz.json", + "reviewedBy": null + } + ], + "milestoneQuiz": { + "passingScore": 80, + "quiz": "milestone.quiz.json" + } +} diff --git a/server/content/courses/soroban-fundamentals/lessons/01-rust-basics-for-smart-contract-devs.md b/server/content/courses/soroban-fundamentals/lessons/01-rust-basics-for-smart-contract-devs.md new file mode 100644 index 00000000..e6675c97 --- /dev/null +++ b/server/content/courses/soroban-fundamentals/lessons/01-rust-basics-for-smart-contract-devs.md @@ -0,0 +1,72 @@ +# Lesson 1 — Rust basics for smart contract devs (Theory) + +## Summary + +Soroban contracts are commonly written in Rust and compiled to WebAssembly. That +means you need a solid grasp of Rust’s safety model and a few patterns that +matter in deterministic, resource-bounded smart contract environments. + +## Learning objectives + +- Explain ownership and borrowing in practical terms +- Use `Option` and `Result` to model “maybe” and “error” outcomes +- Recognize common Rust data types and patterns used in contracts +- Understand why determinism and resource limits affect your code choices + +## Rust concepts you’ll use constantly + +### Ownership & borrowing (the core idea) + +Rust prevents entire classes of bugs by enforcing clear rules about who “owns” a +value and how it can be referenced. + +- **Ownership**: exactly one owner controls a value at a time. +- **Borrowing**: you can temporarily reference a value without taking ownership. +- **Mutability**: mutation is explicit (`mut`). + +In smart contracts, this matters because you’ll build state transitions that are +safe, explicit, and testable. + +### `Option` and `Result` + +Contracts often need to handle absent values and validation errors. + +- `Option`: a value may exist (`Some`) or not (`None`). +- `Result`: an operation may succeed (`Ok`) or fail (`Err`). + +Treat these as first-class control flow tools. + +### Structs, enums, and pattern matching + +- Use **structs** to bundle related fields. +- Use **enums** to model variants (e.g., states, commands, error types). +- Use `match` to handle each variant explicitly. + +### Avoiding foot-guns in contract code + +Smart contracts execute in a deterministic environment and typically have +resource limits. + +Practical guidelines: + +- Prefer integers over floating-point math. +- Keep loops bounded (avoid unbounded iteration over user-controlled sizes). +- Be explicit about failure cases (don’t panic on user input). + +## Mini exercise + +Write a function that returns the next value for a counter. + +- Input: current counter value (or `None` if unset) +- Output: incremented value + +Think about whether this should return `u32`, `i64`, or another integer type. + +## Knowledge check + +- What’s the difference between owning a value and borrowing it? +- When would you choose `Option` vs `Result`? + +## Reviewer sign-off + +Reviewed by: _TBD_ diff --git a/server/content/courses/soroban-fundamentals/lessons/01-rust-basics-for-smart-contract-devs.quiz.json b/server/content/courses/soroban-fundamentals/lessons/01-rust-basics-for-smart-contract-devs.quiz.json new file mode 100644 index 00000000..5b63e248 --- /dev/null +++ b/server/content/courses/soroban-fundamentals/lessons/01-rust-basics-for-smart-contract-devs.quiz.json @@ -0,0 +1,34 @@ +{ + "lessonId": "soroban-fundamentals-1", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "In Rust, which type best represents “this value might be missing”?", + "options": ["Result", "Option", "Vec", "HashMap"], + "correctIndex": 1 + }, + { + "id": 2, + "text": "Why are unbounded loops risky in smart contract code?", + "options": [ + "They can exceed resource limits and fail unpredictably", + "They make keys easier to steal", + "They disable signature checks", + "They force the network into maintenance mode" + ], + "correctIndex": 0 + }, + { + "id": 3, + "text": "Which statement about ownership is most accurate?", + "options": [ + "A value can have unlimited owners by default", + "Exactly one owner controls a value at a time", + "Only mutable references can own values", + "Ownership only applies to strings" + ], + "correctIndex": 1 + } + ] +} diff --git a/server/content/courses/soroban-fundamentals/lessons/02-writing-your-first-soroban-contract.md b/server/content/courses/soroban-fundamentals/lessons/02-writing-your-first-soroban-contract.md new file mode 100644 index 00000000..793b049c --- /dev/null +++ b/server/content/courses/soroban-fundamentals/lessons/02-writing-your-first-soroban-contract.md @@ -0,0 +1,70 @@ +# Lesson 2 — Writing your first Soroban contract (Practical) + +## Summary + +In this lesson you’ll create a minimal Soroban contract, build it to WebAssembly +(Wasm), and understand the lifecycle: write → build → deploy → invoke. + +> The exact CLI commands can vary by tooling and network. Focus on the flow and +> the moving parts. + +## Learning objectives + +- Identify the parts of a Soroban contract project +- Understand what “build to Wasm” means +- Explain deploy vs invoke + +## The minimal contract shape + +Most Soroban contracts have: + +- A contract type (the “contract”) +- One or more public functions (the “entrypoints”) +- Optional storage (state) + +Conceptual example: + +```rust +// Pseudo-code for structure: +// - define a contract type +// - implement a function callable from the network +``` + +## Build → deploy → invoke + +### 1) Build + +Building compiles your Rust contract to a `.wasm` artifact. + +Things to verify after a build: + +- The build succeeds without warnings that indicate logic issues +- A Wasm artifact exists in your target/build output + +### 2) Deploy + +Deploying uploads your Wasm and creates a **contract instance** on the network. +You typically get back a **contract ID**. + +### 3) Invoke + +Invoking calls one contract function with inputs. The call may: + +- Read/write storage +- Require authorization +- Emit events + +## Common mistakes + +- Deploying to the wrong network (testnet vs public) +- Re-using an outdated contract ID after redeploying +- Assuming you can do non-deterministic things (like HTTP calls) + +## Knowledge check + +- What artifact is produced when you build a Soroban contract? +- What identifier do you typically need after deployment to invoke functions? + +## Reviewer sign-off + +Reviewed by: _TBD_ diff --git a/server/content/courses/soroban-fundamentals/lessons/02-writing-your-first-soroban-contract.quiz.json b/server/content/courses/soroban-fundamentals/lessons/02-writing-your-first-soroban-contract.quiz.json new file mode 100644 index 00000000..d7911e27 --- /dev/null +++ b/server/content/courses/soroban-fundamentals/lessons/02-writing-your-first-soroban-contract.quiz.json @@ -0,0 +1,39 @@ +{ + "lessonId": "soroban-fundamentals-2", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "When you build a Soroban contract, what is the primary output artifact?", + "options": [ + "A PNG file", + "A WebAssembly (.wasm) file", + "A PDF", + "A seed phrase" + ], + "correctIndex": 1 + }, + { + "id": 2, + "text": "What does “deploy” generally mean in the Soroban contract lifecycle?", + "options": [ + "Calling a function", + "Uploading Wasm and creating a contract instance (contract ID)", + "Changing a trustline limit", + "Minting XLM" + ], + "correctIndex": 1 + }, + { + "id": 3, + "text": "Which action typically requires a contract ID?", + "options": [ + "Viewing an explorer", + "Invoking a contract entrypoint", + "Creating a wallet", + "Exporting keys" + ], + "correctIndex": 1 + } + ] +} diff --git a/server/content/courses/soroban-fundamentals/lessons/03-contract-storage-and-state.md b/server/content/courses/soroban-fundamentals/lessons/03-contract-storage-and-state.md new file mode 100644 index 00000000..29b1c9cf --- /dev/null +++ b/server/content/courses/soroban-fundamentals/lessons/03-contract-storage-and-state.md @@ -0,0 +1,62 @@ +# Lesson 3 — Contract storage and state (Theory) + +## Summary + +State is what turns a smart contract from “a pure function” into an application. +In Soroban, storage is explicit and lives in the Stellar ledger. + +This lesson covers the kinds of storage you’ll see and the practical design +tradeoffs to keep contracts predictable and efficient. + +## Learning objectives + +- Explain what contract state is and where it lives +- Describe key/value storage patterns +- Identify common pitfalls (unbounded growth, upgrade incompatibility) + +## Storage as key/value + +A common pattern is: + +1. Choose a storage key (often a symbol or bytes) +2. Store a value for that key (often a number, struct, or map) +3. Read/modify/write during contract calls + +Design tips: + +- Keep keys stable and well-named +- Prefer storing small, well-structured values +- Avoid storing data that can be derived cheaply + +## State design patterns + +### Counters and simple config + +- Counter: `count = count + 1` +- Config: admin address, fee rate, paused flag + +### Maps (per-user state) + +Use a composite key like `(user, key)` to store per-user values. + +### Versioning for upgrades + +If you may upgrade a contract later, plan a versioning strategy: + +- Store a `schema_version` +- Write migration logic (or deploy a new contract and migrate state) + +## Pitfalls + +- **Unbounded storage**: if users can create unlimited keys, storage can bloat. +- **Unbounded loops**: avoid iterating over large user-controlled collections. +- **Ambiguous encoding**: be consistent about how you encode keys and values. + +## Knowledge check + +- Why is “unbounded growth” a contract design risk? +- What is one strategy to prepare for upgrades? + +## Reviewer sign-off + +Reviewed by: _TBD_ diff --git a/server/content/courses/soroban-fundamentals/lessons/03-contract-storage-and-state.quiz.json b/server/content/courses/soroban-fundamentals/lessons/03-contract-storage-and-state.quiz.json new file mode 100644 index 00000000..91ec96f0 --- /dev/null +++ b/server/content/courses/soroban-fundamentals/lessons/03-contract-storage-and-state.quiz.json @@ -0,0 +1,39 @@ +{ + "lessonId": "soroban-fundamentals-3", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "Where does Soroban contract state live when written to storage?", + "options": [ + "In the user’s browser", + "In the Stellar ledger", + "Only in RAM", + "Only in Horizon logs" + ], + "correctIndex": 1 + }, + { + "id": 2, + "text": "Which is a common pitfall when users can create unlimited storage keys?", + "options": [ + "Unbounded storage growth", + "Instant transaction finality", + "Automatic profit", + "Infinite free bandwidth" + ], + "correctIndex": 0 + }, + { + "id": 3, + "text": "What is one simple upgrade strategy for long-lived contracts?", + "options": [ + "Store a schema version and plan migrations", + "Delete the ledger", + "Disable signatures", + "Use floating point for balances" + ], + "correctIndex": 0 + } + ] +} diff --git a/server/content/courses/soroban-fundamentals/lessons/04-testing-your-contract.md b/server/content/courses/soroban-fundamentals/lessons/04-testing-your-contract.md new file mode 100644 index 00000000..0d562584 --- /dev/null +++ b/server/content/courses/soroban-fundamentals/lessons/04-testing-your-contract.md @@ -0,0 +1,56 @@ +# Lesson 4 — Testing your contract (Practical) + +## Summary + +Good tests are what separates “it compiles” from “it’s safe to ship.” In this +lesson you’ll learn a practical testing approach for Soroban contracts. + +## Learning objectives + +- Explain the difference between unit and integration tests +- Identify what to test: happy paths, auth, edge cases +- Build confidence before deploying on-chain + +## What to test + +### 1) Happy path + +- Valid inputs +- Expected state updates +- Expected return values + +### 2) Authorization rules + +If a function should only be callable by an admin or the user, tests should +prove unauthorized calls fail. + +### 3) Edge cases + +- Missing state (`None` / unset storage) +- Boundary values (0, max limits) +- Repeated calls + +## Unit testing mindset + +A unit test should be: + +- Deterministic +- Small +- Focused on one behavior + +## Integration testing mindset + +An integration test validates: + +- Deploy + invoke end-to-end +- Network configuration +- Real transaction signing and submission + +## Knowledge check + +- Name two categories of behaviors you should always test. +- Why are deterministic tests especially important for smart contracts? + +## Reviewer sign-off + +Reviewed by: _TBD_ diff --git a/server/content/courses/soroban-fundamentals/lessons/04-testing-your-contract.quiz.json b/server/content/courses/soroban-fundamentals/lessons/04-testing-your-contract.quiz.json new file mode 100644 index 00000000..d0c70672 --- /dev/null +++ b/server/content/courses/soroban-fundamentals/lessons/04-testing-your-contract.quiz.json @@ -0,0 +1,39 @@ +{ + "lessonId": "soroban-fundamentals-4", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "Which is an example of an “edge case” worth testing?", + "options": [ + "Only testing the main success path", + "Missing state in storage (unset value)", + "Ignoring authorization rules", + "Using random outcomes" + ], + "correctIndex": 1 + }, + { + "id": 2, + "text": "What is a key goal of unit tests for smart contracts?", + "options": [ + "Non-deterministic behavior", + "Deterministic verification of a small behavior", + "Replacing the network consensus", + "Automatically upgrading contracts" + ], + "correctIndex": 1 + }, + { + "id": 3, + "text": "Integration tests are most useful for validating which workflow?", + "options": [ + "Deploy + invoke end-to-end on a network", + "Generating seed phrases", + "Designing UI mockups", + "Creating trustlines manually" + ], + "correctIndex": 0 + } + ] +} diff --git a/server/content/courses/soroban-fundamentals/milestone.quiz.json b/server/content/courses/soroban-fundamentals/milestone.quiz.json new file mode 100644 index 00000000..0522a027 --- /dev/null +++ b/server/content/courses/soroban-fundamentals/milestone.quiz.json @@ -0,0 +1,51 @@ +{ + "courseId": "soroban-fundamentals", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "Which Rust type is best for modeling “success or error”?", + "options": ["Option", "Result", "Vec", "String"], + "correctIndex": 1 + }, + { + "id": 2, + "text": "What is the typical output of building a Soroban contract?", + "options": ["A .wasm artifact", "A PDF", "A JWT", "A PNG"], + "correctIndex": 0 + }, + { + "id": 3, + "text": "Where is Soroban contract state stored when persisted?", + "options": [ + "In the Stellar ledger", + "In the wallet", + "In the browser", + "In DNS" + ], + "correctIndex": 0 + }, + { + "id": 4, + "text": "Why are unbounded loops risky in contracts?", + "options": [ + "They can exceed resource limits", + "They remove signatures", + "They mint XLM", + "They change the network passphrase" + ], + "correctIndex": 0 + }, + { + "id": 5, + "text": "Which category should always be covered by tests?", + "options": [ + "Authorization rules", + "Logo colors", + "Browser bookmarks", + "Chat messages" + ], + "correctIndex": 0 + } + ] +} diff --git a/server/content/courses/stellar-basics/course.json b/server/content/courses/stellar-basics/course.json new file mode 100644 index 00000000..21eca9e4 --- /dev/null +++ b/server/content/courses/stellar-basics/course.json @@ -0,0 +1,45 @@ +{ + "id": "stellar-basics", + "title": "Introduction to Stellar & Soroban", + "level": "beginner", + "published": true, + "description": "Learn the basics of Stellar (accounts, assets, payments) and get a first look at Soroban smart contracts.", + "lessons": [ + { + "id": "stellar-basics-1", + "title": "What is Stellar?", + "kind": "theory", + "markdown": "lessons/01-what-is-stellar.md", + "quiz": "lessons/01-what-is-stellar.quiz.json", + "reviewedBy": null + }, + { + "id": "stellar-basics-2", + "title": "Setting up a Freighter wallet", + "kind": "practical", + "markdown": "lessons/02-setting-up-freighter-wallet.md", + "quiz": "lessons/02-setting-up-freighter-wallet.quiz.json", + "reviewedBy": null + }, + { + "id": "stellar-basics-3", + "title": "Your first XLM transaction", + "kind": "practical", + "markdown": "lessons/03-your-first-xlm-transaction.md", + "quiz": "lessons/03-your-first-xlm-transaction.quiz.json", + "reviewedBy": null + }, + { + "id": "stellar-basics-4", + "title": "Introduction to Soroban smart contracts", + "kind": "theory", + "markdown": "lessons/04-introduction-to-soroban-smart-contracts.md", + "quiz": "lessons/04-introduction-to-soroban-smart-contracts.quiz.json", + "reviewedBy": null + } + ], + "milestoneQuiz": { + "passingScore": 80, + "quiz": "milestone.quiz.json" + } +} diff --git a/server/content/courses/stellar-basics/lessons/01-what-is-stellar.md b/server/content/courses/stellar-basics/lessons/01-what-is-stellar.md new file mode 100644 index 00000000..eae5185f --- /dev/null +++ b/server/content/courses/stellar-basics/lessons/01-what-is-stellar.md @@ -0,0 +1,85 @@ +# Lesson 1 — What is Stellar? (Theory) + +## Summary + +Stellar is an open network for issuing assets and moving value quickly and +cheaply. It’s designed for payments, remittances, and on-chain asset exchange. + +By the end of this lesson, you’ll understand the core building blocks of the +Stellar network and the vocabulary you’ll see across wallets, explorers, and +developer tools. + +## Learning objectives + +- Explain what Stellar is and what it’s used for +- Describe accounts, keypairs, balances, and transactions +- Explain assets, trustlines, and why they matter +- Identify the difference between Stellar Core and Horizon + +## Core concepts (the “mental model”) + +### 1) Accounts & keys + +On Stellar, an **account** is controlled by one or more **public/private keys** +(keypairs). The public key is your address. The private key (or seed phrase in a +wallet) proves you own the account and can sign transactions. + +**Rule of thumb:** if someone gets your secret key, they can control your funds. + +### 2) XLM (the native asset) + +**XLM** is Stellar’s native asset. It’s used for fees and also as a bridge asset +for path payments. + +Most accounts must maintain a small **minimum balance** in XLM to exist on the +network and to hold additional ledger entries (like trustlines). + +### 3) Transactions & operations + +A **transaction** is a signed package submitted to the network. It contains one +or more **operations** (e.g., “send payment”, “change trust”, “manage offer”). + +Key details you’ll see in most transactions: + +- **Fee**: a small amount paid in XLM +- **Sequence number**: prevents replay and enforces ordering +- **Memo**: optional metadata (often used for exchange deposits) + +### 4) Assets & trustlines + +Stellar supports custom assets (e.g., “USDC” issued by an issuer). To hold a +non-native asset, an account must create a **trustline**. + +Think of a trustline as: “I agree to hold up to X units of this asset.” + +### 5) On-chain exchange (SDEX) + +Stellar has a built-in decentralized exchange (often called the **SDEX**). Users +can place offers to trade assets. Path payments can automatically route through +these offers to find the best conversion. + +### 6) Network components: Core vs Horizon + +- **Stellar Core**: the software validators run to reach consensus and apply + transactions to the ledger. +- **Horizon**: the API layer that most apps use to query accounts, submit + transactions, and stream events. + +## Quick exercise (5 minutes) + +1. Find a Stellar explorer you like (any public explorer is fine). +2. Search for a public account address (you can use one from a wallet later). +3. Identify: + - XLM balance + - Trustlines (if any) + - Recent transactions and operations + +## Knowledge check + +- What is the difference between a transaction and an operation? +- Why do you need a trustline to hold a non-native asset? +- Which component do most apps talk to: Stellar Core or Horizon? + +## Reviewer sign-off + +Reviewed by: _TBD_ diff --git a/server/content/courses/stellar-basics/lessons/01-what-is-stellar.quiz.json b/server/content/courses/stellar-basics/lessons/01-what-is-stellar.quiz.json new file mode 100644 index 00000000..11346de3 --- /dev/null +++ b/server/content/courses/stellar-basics/lessons/01-what-is-stellar.quiz.json @@ -0,0 +1,34 @@ +{ + "lessonId": "stellar-basics-1", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "On Stellar, which item proves you control an account and can authorize transactions?", + "options": [ + "A memo", + "A trustline", + "A secret key / seed phrase", + "A Horizon URL" + ], + "correctIndex": 2 + }, + { + "id": 2, + "text": "What is the main reason you create a trustline?", + "options": [ + "To increase your transaction fee", + "To hold a non-native asset issued on Stellar", + "To deploy a Soroban contract", + "To generate a new sequence number" + ], + "correctIndex": 1 + }, + { + "id": 3, + "text": "Most applications query data and submit transactions through which Stellar component?", + "options": ["Horizon", "Stellar Core", "Freighter", "Soroban VM"], + "correctIndex": 0 + } + ] +} diff --git a/server/content/courses/stellar-basics/lessons/02-setting-up-freighter-wallet.md b/server/content/courses/stellar-basics/lessons/02-setting-up-freighter-wallet.md new file mode 100644 index 00000000..c38ade6f --- /dev/null +++ b/server/content/courses/stellar-basics/lessons/02-setting-up-freighter-wallet.md @@ -0,0 +1,66 @@ +# Lesson 2 — Setting up a Freighter wallet (Practical) + +## Summary + +Freighter is a popular wallet for the Stellar ecosystem. In this lesson you’ll +install it, create an account, switch to a test network, and fund your account +using Friendbot. + +## Learning objectives + +- Install Freighter and create a wallet safely +- Switch between public network and test networks +- Fund a test account and confirm balances + +## Step-by-step + +### 1) Install Freighter + +1. Install the Freighter browser extension from the official store. +2. Double-check you’re installing the official wallet (publisher and reviews). + +### 2) Create a new wallet + +1. Choose **Create new wallet**. +2. Freighter will show you a **recovery phrase** (seed phrase). +3. Write it down offline and store it somewhere safe. + +**Never** share your recovery phrase. No support team will ever need it. + +### 3) Lock/unlock basics + +- Use your wallet password to unlock Freighter on your device. +- The recovery phrase is for restoring access if you lose the device. + +### 4) Switch to a test network + +For learning, use a test network so you don’t risk real funds. + +In Freighter settings, select a **test network** (e.g., “Testnet”). + +### 5) Fund your testnet account (Friendbot) + +Friendbot is a faucet that sends test XLM to new testnet accounts. + +1. Copy your public address from Freighter. +2. Request funds via Friendbot using your address. +3. Return to Freighter and confirm you now have a test XLM balance. + +If your environment provides a local Friendbot endpoint (common in dev setups), +you can use that as well. + +## Security checklist (non-negotiable) + +- ✅ Recovery phrase stored offline (paper/metal) +- ✅ No screenshots of secret keys +- ✅ Beware of “support” DMs asking for your phrase +- ✅ Use a separate test wallet for experiments + +## Knowledge check + +- What’s the difference between your wallet password and recovery phrase? +- Why should you use a test network for experiments? + +## Reviewer sign-off + +Reviewed by: _TBD_ diff --git a/server/content/courses/stellar-basics/lessons/02-setting-up-freighter-wallet.quiz.json b/server/content/courses/stellar-basics/lessons/02-setting-up-freighter-wallet.quiz.json new file mode 100644 index 00000000..e5d2c6db --- /dev/null +++ b/server/content/courses/stellar-basics/lessons/02-setting-up-freighter-wallet.quiz.json @@ -0,0 +1,39 @@ +{ + "lessonId": "stellar-basics-2", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "Which item should you never share with anyone (including “support”)?", + "options": [ + "Your public address", + "Your recovery phrase / secret key", + "A transaction hash", + "The network passphrase" + ], + "correctIndex": 1 + }, + { + "id": 2, + "text": "Why is a test network recommended for learning and experimenting?", + "options": [ + "Transactions are slower and safer", + "It lets you practice without risking real funds", + "It removes the need for signatures", + "It guarantees profits" + ], + "correctIndex": 1 + }, + { + "id": 3, + "text": "What does Friendbot typically do?", + "options": [ + "Deploys Soroban contracts", + "Sends test XLM to a testnet account", + "Stores your seed phrase", + "Converts XLM to fiat automatically" + ], + "correctIndex": 1 + } + ] +} diff --git a/server/content/courses/stellar-basics/lessons/03-your-first-xlm-transaction.md b/server/content/courses/stellar-basics/lessons/03-your-first-xlm-transaction.md new file mode 100644 index 00000000..6769bef1 --- /dev/null +++ b/server/content/courses/stellar-basics/lessons/03-your-first-xlm-transaction.md @@ -0,0 +1,68 @@ +# Lesson 3 — Your first XLM transaction (Practical) + +## Summary + +You’ll send a small payment on a test network to understand the lifecycle of a +Stellar transaction: build → sign → submit → verify on an explorer. + +## Learning objectives + +- Describe the pieces of a payment transaction +- Send XLM on a test network +- Verify the result using a transaction hash + +## Prerequisites + +- A funded testnet account in Freighter +- A second testnet address to receive funds (can be a second wallet) + +## Option A: Using a wallet + explorer (fastest) + +1. In Freighter, choose **Send**. +2. Paste the recipient address. +3. Enter a small amount of XLM (e.g., 1 XLM on testnet). +4. Confirm and sign. +5. Copy the resulting transaction hash (if shown). +6. Open an explorer and look up: + - The transaction hash + - The source account + - The destination account + +## Option B: Building a payment in code (conceptual) + +Most apps follow this pattern: + +1. Load the account (to get the current sequence number) +2. Build a transaction with one or more operations +3. Sign (wallet or secret key) +4. Submit + +Example shape (pseudo-code): + +```ts +// 1) load account +// 2) build tx with Operation.payment(...) +// 3) sign +// 4) submit +``` + +## What to observe + +- A **payment** is one operation inside a transaction +- The network charges a small **fee** (paid in XLM) +- Transactions are ordered by **sequence number** + +## Common mistakes + +- Sending on the wrong network (testnet vs public) +- Copy/paste errors in addresses +- Insufficient balance for the minimum reserve + payment + fee + +## Knowledge check + +- What is a transaction hash used for? +- Why does the sequence number matter? + +## Reviewer sign-off + +Reviewed by: _TBD_ diff --git a/server/content/courses/stellar-basics/lessons/03-your-first-xlm-transaction.quiz.json b/server/content/courses/stellar-basics/lessons/03-your-first-xlm-transaction.quiz.json new file mode 100644 index 00000000..79476994 --- /dev/null +++ b/server/content/courses/stellar-basics/lessons/03-your-first-xlm-transaction.quiz.json @@ -0,0 +1,34 @@ +{ + "lessonId": "stellar-basics-3", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "A Stellar transaction can contain multiple _____?", + "options": ["Operations", "Accounts", "Networks", "Explorers"], + "correctIndex": 0 + }, + { + "id": 2, + "text": "What is the purpose of a transaction hash?", + "options": [ + "It stores your secret key", + "It uniquely identifies a submitted transaction for lookup/verification", + "It increases your account’s minimum balance", + "It is required to create a trustline" + ], + "correctIndex": 1 + }, + { + "id": 3, + "text": "If you send on testnet but check the public network explorer, what will happen?", + "options": [ + "The transaction appears instantly", + "The explorer won’t find the transaction", + "The network auto-migrates your funds", + "Your account gets deleted" + ], + "correctIndex": 1 + } + ] +} diff --git a/server/content/courses/stellar-basics/lessons/04-introduction-to-soroban-smart-contracts.md b/server/content/courses/stellar-basics/lessons/04-introduction-to-soroban-smart-contracts.md new file mode 100644 index 00000000..f989ff70 --- /dev/null +++ b/server/content/courses/stellar-basics/lessons/04-introduction-to-soroban-smart-contracts.md @@ -0,0 +1,87 @@ +# Lesson 4 — Introduction to Soroban smart contracts (Theory) + +## Summary + +Soroban is Stellar’s smart contract platform. It enables on-chain programs that +can manage state, enforce rules, and power DeFi and other applications. + +This lesson introduces the “shape” of a Soroban contract without requiring you +to write production-ready Rust yet. + +## Learning objectives + +- Define what a smart contract is (in the Stellar + Soroban context) +- Explain contract state and why storage is explicit +- Recognize common Soroban building blocks: `Env`, auth, events, storage + +## What a Soroban contract is + +At a high level, a Soroban contract is: + +- **Code** (compiled to WebAssembly) +- **Functions** that can be invoked by transactions +- **State** stored in the Stellar ledger (when the contract chooses to store it) + +Soroban contracts run deterministically. They don’t “reach out” to the internet +or read files. Everything they need must be provided via inputs or ledger state. + +## Key building blocks + +### `Env` + +Soroban’s `Env` is the gateway to: + +- Reading/writing storage +- Emitting events +- Verifying authorization +- Accessing ledger context (like timestamps/sequence) + +### Storage (state) + +Soroban makes storage explicit. Contract state is typically stored as key/value +pairs in the ledger. + +You’ll often see patterns like: + +- “Get the current value from storage” +- “Compute a new value” +- “Write the new value back to storage” + +### Authorization (who is allowed to do what) + +Most real contracts need to enforce rules like: + +- Only an admin can upgrade settings +- A user must sign to move their funds +- A contract can only spend what it was approved to spend + +Soroban provides primitives to validate authorization for contract calls. + +## A tiny example (conceptual) + +Below is a simplified counter contract shape: + +```rust +// Pseudo-code to illustrate the idea: +// - read `count` from storage +// - increment it +// - write it back +// - return the updated value +``` + +## Where Soroban fits in this track + +In the next track (Soroban Smart Contract Basics), you’ll: + +- Learn Rust essentials for contract development +- Write and test a real contract +- Understand storage patterns and best practices + +## Knowledge check + +- Why can’t a Soroban contract call an external API directly? +- What is the role of `Env` in Soroban? + +## Reviewer sign-off + +Reviewed by: _TBD_ diff --git a/server/content/courses/stellar-basics/lessons/04-introduction-to-soroban-smart-contracts.quiz.json b/server/content/courses/stellar-basics/lessons/04-introduction-to-soroban-smart-contracts.quiz.json new file mode 100644 index 00000000..67be63e9 --- /dev/null +++ b/server/content/courses/stellar-basics/lessons/04-introduction-to-soroban-smart-contracts.quiz.json @@ -0,0 +1,39 @@ +{ + "lessonId": "stellar-basics-4", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "Soroban contracts are compiled to which format for execution?", + "options": [ + "Native x86", + "WebAssembly (Wasm)", + "JVM bytecode", + "Solidity source" + ], + "correctIndex": 1 + }, + { + "id": 2, + "text": "Which statement best describes Soroban contract storage?", + "options": [ + "Storage is implicit and unlimited", + "State is stored explicitly in the Stellar ledger when the contract writes it", + "State is stored in the user’s browser localStorage", + "State is stored on Horizon servers only" + ], + "correctIndex": 1 + }, + { + "id": 3, + "text": "In Soroban, what is `Env` primarily used for?", + "options": [ + "Mining blocks", + "Accessing storage, auth, and ledger context", + "Generating seed phrases", + "Replacing Stellar Core" + ], + "correctIndex": 1 + } + ] +} diff --git a/server/content/courses/stellar-basics/milestone.quiz.json b/server/content/courses/stellar-basics/milestone.quiz.json new file mode 100644 index 00000000..493ac9d1 --- /dev/null +++ b/server/content/courses/stellar-basics/milestone.quiz.json @@ -0,0 +1,56 @@ +{ + "courseId": "stellar-basics", + "passingScore": 80, + "questions": [ + { + "id": 1, + "text": "What is the main purpose of a trustline on Stellar?", + "options": [ + "To hold a non-native asset", + "To increase your transaction fee", + "To upgrade a Soroban contract", + "To generate a new keypair automatically" + ], + "correctIndex": 0 + }, + { + "id": 2, + "text": "Which Stellar component is most commonly used by apps to submit transactions and query account data?", + "options": ["Freighter", "Horizon", "Stellar Core", "Soroban VM"], + "correctIndex": 1 + }, + { + "id": 3, + "text": "Why should you practice transactions on a test network first?", + "options": [ + "Test networks guarantee profits", + "You can practice without risking real funds", + "You don’t need to sign transactions on test networks", + "Fees are always zero on test networks" + ], + "correctIndex": 1 + }, + { + "id": 4, + "text": "Which statement about Soroban is accurate?", + "options": [ + "Soroban contracts can read files from the server disk", + "Soroban contracts are deterministic programs that run against ledger state", + "Soroban replaces Stellar accounts", + "Soroban is required to create trustlines" + ], + "correctIndex": 1 + }, + { + "id": 5, + "text": "A Stellar transaction is ordered and protected from replay primarily by the _____?", + "options": [ + "Memo field", + "Sequence number", + "Trustline limit", + "Offer price" + ], + "correctIndex": 1 + } + ] +} diff --git a/server/content/courses/stellar-basics/quiz.json b/server/content/courses/stellar-basics/quiz.json new file mode 100644 index 00000000..4a5b5ca8 --- /dev/null +++ b/server/content/courses/stellar-basics/quiz.json @@ -0,0 +1,57 @@ +[ + { + "id": 1, + "question": "Which language do developers use to implement Soroban smart contracts?", + "options": ["Solidity", "Rust", "Go", "Python"], + "correct": 1, + "explanation": "Soroban contracts are written in Rust and compiled to WebAssembly (WASM), which the Soroban runtime executes on Stellar." + }, + { + "id": 2, + "question": "On LearnVault, what do LRN (LearnToken) balances primarily represent?", + "options": [ + "Transferable tokens meant for open market trading", + "Fees you pay to validators for every quiz submission", + "Non-transferable on-chain reputation from verified course milestone completion", + "The same thing as governance voting weight for every holder" + ], + "correct": 2, + "explanation": "LRN is soulbound (non-transferable) SEP-41 tokens minted when milestones are verified; your balance reflects verified learning and on-chain academic reputation, not freely tradable value." + }, + { + "id": 3, + "question": "About how long does it usually take for a Stellar transaction to reach finality once included in a ledger?", + "options": [ + "Less than one second", + "Roughly five seconds", + "Around ten minutes", + "Typically one hour or more" + ], + "correct": 1, + "explanation": "Stellar closes ledgers about every five seconds, so once your operation is in a ledger, settlement is fast and predictable compared to many proof-of-work chains." + }, + { + "id": 4, + "question": "What is SEP-41 in the Stellar ecosystem?", + "options": [ + "A hardware wallet pairing protocol", + "The Soroban standard for fungible token interfaces", + "A specification for Stellar account recovery phrases", + "The naming scheme for public Horizon API endpoints" + ], + "correct": 1, + "explanation": "SEP-41 defines a standard fungible token interface for Soroban; tokens like LearnVault’s LRN follow this pattern so wallets and dApps can interact with them consistently." + }, + { + "id": 5, + "question": "What is Friendbot commonly used for on Stellar?", + "options": [ + "Issuing mainnet stablecoins for merchants", + "Funding testnet accounts with free test lumens (XLM)", + "Auditing Soroban contract source code", + "Registering .stellar domain names on production" + ], + "correct": 1, + "explanation": "Friendbot is a testnet faucet: you give it a new testnet address and it sends a small amount of test XLM so you can pay fees and try transactions without real money." + } +] diff --git a/server/docker-compose.test.yml b/server/docker-compose.test.yml new file mode 100644 index 00000000..443bf001 --- /dev/null +++ b/server/docker-compose.test.yml @@ -0,0 +1,37 @@ +version: "3.8" + +services: + app: + build: + context: . + target: builder + environment: + - NODE_ENV=test + - DATABASE_URL=postgresql://learnvault:learnvault@postgres:5432/learnvault_test?sslmode=disable + - REDIS_URL=redis://redis:6379 + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + command: sh -c "npm run migrate && npm test" + + postgres: + image: postgres:16 + environment: + POSTGRES_USER: learnvault + POSTGRES_PASSWORD: learnvault + POSTGRES_DB: learnvault_test + healthcheck: + test: ["CMD-SHELL", "pg_isready -U learnvault -d learnvault_test"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7 + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 00000000..bb642883 --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,57 @@ +version: "3.8" + +services: + app: + build: + context: . + target: builder + ports: + - "4000:4000" + environment: + - NODE_ENV=development + - PORT=4000 + - DATABASE_URL=postgresql://learnvault:learnvault@postgres:5432/learnvault?sslmode=disable + - REDIS_URL=redis://redis:6379 + - FRONTEND_URL=http://localhost:5173 + volumes: + - .:/app + # Exclude the host's node_modules by mounting an anonymous volume over it + - /app/node_modules + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + command: npm run dev + + postgres: + image: postgres:16 + ports: + - "5432:5432" + environment: + POSTGRES_USER: learnvault + POSTGRES_PASSWORD: learnvault + POSTGRES_DB: learnvault + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U learnvault -d learnvault"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7 + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + redis_data: diff --git a/server/jest.config.js b/server/jest.config.js new file mode 100644 index 00000000..e36544b8 --- /dev/null +++ b/server/jest.config.js @@ -0,0 +1,38 @@ +/** @type {import('jest').Config} */ +module.exports = { + testEnvironment: "node", + testMatch: ["**/tests/**/*.test.ts", "**/controllers/**/*.test.ts"], + moduleFileExtensions: ["ts", "js", "json"], + transform: { + "^.+\\.ts$": [ + "ts-jest", + { + tsconfig: { + esModuleInterop: true, + module: "commonjs", + types: ["node", "jest"], + }, + }, + ], + }, + coverageProvider: "v8", + collectCoverageFrom: [ + "src/**/*.ts", + "!src/tests/**", + "!src/**/*.test.ts", + "!src/index.ts", + "!src/openapi.ts", + "!src/types/**", + "!src/types.d.ts", + "!src/templates/**", + ], + coverageReporters: ["text", "lcov", "json-summary"], + coverageThreshold: { + global: { + statements: 80, + branches: 80, + functions: 80, + lines: 80, + }, + }, +} diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 00000000..15a6d5f7 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,8647 @@ +{ + "name": "learnvault-server", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "learnvault-server", + "version": "0.0.1", + "dependencies": { + "@pinata/sdk": "^2.1.0", + "@sendgrid/mail": "^8.1.6", + "@stellar/stellar-sdk": "^14.4.3", + "compression": "^1.8.1", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "express-rate-limit": "^8.3.1", + "helmet": "^8.1.0", + "ioredis": "^5.6.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "multer": "^2.0.0", + "nodemailer": "^8.0.4", + "pg": "^8.20.0", + "resend": "^6.9.4", + "sanitize-html": "^2.17.3", + "side-channel": "^1.1.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "yaml": "^2.8.1", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/compression": "^1.8.1", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.7", + "@types/morgan": "^1.9.9", + "@types/multer": "^2.1.0", + "@types/node": "^22.10.6", + "@types/nodemailer": "^7.0.11", + "@types/pg": "^8.20.0", + "@types/sanitize-html": "^2.16.0", + "@types/supertest": "^7.2.0", + "@types/swagger-ui-express": "^4.1.8", + "jest": "^30.3.0", + "side-channel": "^1.1.0", + "supertest": "^7.2.2", + "ts-jest": "^29.4.6", + "ts-node-dev": "^2.0.0", + "typescript": "^5.7.2" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/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/@babel/core/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/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/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/@babel/traverse/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/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "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/@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/@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/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.3.0", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/test-result": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pinata/sdk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@pinata/sdk/-/sdk-2.1.0.tgz", + "integrity": "sha512-hkS0tcKtsjf9xhsEBs2Nbey5s+Db7x5rlOH9TaWHBXkJ7IwwOs2xnEDigNaxAHKjYAwcw+m2hzpO5QgOfeF7Zw==", + "deprecated": "Please install the new IPFS SDK at pinata-web3. More information at https://docs.pinata.cloud/web3/sdk", + "license": "MIT", + "dependencies": { + "axios": "^0.21.1", + "form-data": "^2.3.3", + "is-ipfs": "^0.6.0", + "path": "^0.12.7" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@sendgrid/client": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.6.tgz", + "integrity": "sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA==", + "license": "MIT", + "dependencies": { + "@sendgrid/helpers": "^8.0.0", + "axios": "^1.12.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/@sendgrid/client/node_modules/axios": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/@sendgrid/client/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@sendgrid/helpers": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz", + "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/@sendgrid/mail": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.6.tgz", + "integrity": "sha512-/ZqxUvKeEztU9drOoPC/8opEPOk+jLlB2q4+xpx6HVLq6aFu3pMpalkTpAQz8XfRfpLp8O25bh6pGPcHDCYpqg==", + "license": "MIT", + "dependencies": { + "@sendgrid/client": "^8.1.5", + "@sendgrid/helpers": "^8.0.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", + "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@stellar/js-xdr": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", + "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", + "license": "Apache-2.0" + }, + "node_modules/@stellar/stellar-base": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-14.1.0.tgz", + "integrity": "sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.9.6", + "@stellar/js-xdr": "^3.1.2", + "base32.js": "^0.1.0", + "bignumber.js": "^9.3.1", + "buffer": "^6.0.3", + "sha.js": "^2.4.12" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stellar/stellar-sdk": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-14.6.1.tgz", + "integrity": "sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==", + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-base": "^14.1.0", + "axios": "^1.13.3", + "bignumber.js": "^9.3.1", + "commander": "^14.0.2", + "eventsource": "^2.0.2", + "feaxios": "^0.0.23", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + }, + "bin": { + "stellar-js": "bin/stellar-js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stellar/stellar-sdk/node_modules/axios": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/@stellar/stellar-sdk/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "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", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", + "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sanitize-html": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.1.tgz", + "integrity": "sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^10.1" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/superagent/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz", + "integrity": "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/babel-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/base32.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", + "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", + "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cids": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/cids/-/cids-0.7.5.tgz", + "integrity": "sha512-zT7mPeghoWAu+ppn8+BS1tQ5qGmbMfB4AregnQjA/qHY3GC1m1ptI9GkWNlgeu38r7CuRdXB47uY2XgAYt6QVA==", + "deprecated": "This module has been superseded by the multiformats module", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "class-is": "^1.1.0", + "multibase": "~0.6.0", + "multicodec": "^1.0.0", + "multihashes": "~0.4.15" + }, + "engines": { + "node": ">=4.0.0", + "npm": ">=3.0.0" + } + }, + "node_modules/cids/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-is": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/class-is/-/class-is-1.1.0.tgz", + "integrity": "sha512-rhjH9AG1fvabIDoGRVH587413LPjTZgmDF9fOFCbFJQV4yuocX1mHxxvXI4g3cGwbVY9wAYIoKlg1N79frJKQw==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.0.tgz", + "integrity": "sha512-gDK8yiqKxrGta+3WtON59arrrw6GLmadA1qoFgYXzdcch8fmKDID2XqO8itsi3f1wufXYPT51387dN6cvVBS3Q==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/feaxios": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.23.tgz", + "integrity": "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==", + "license": "MIT", + "dependencies": { + "is-retry-allowed": "^3.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "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/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ioredis/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==", + "license": "MIT" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "license": "MIT", + "dependencies": { + "ip-regex": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ipfs": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/is-ipfs/-/is-ipfs-0.6.3.tgz", + "integrity": "sha512-HyRot1dvLcxImtDqPxAaY1miO6WsiP/z7Yxpg2qpaLWv5UdhAPtLvHJ4kMLM0w8GSl8AFsVF23PHe1LzuWrUlQ==", + "license": "MIT", + "dependencies": { + "bs58": "^4.0.1", + "cids": "~0.7.0", + "mafmt": "^7.0.0", + "multiaddr": "^7.2.1", + "multibase": "~0.6.0", + "multihashes": "~0.4.13" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-retry-allowed": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-3.0.0.tgz", + "integrity": "sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/istanbul-lib-source-maps/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/istanbul-lib-source-maps/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/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", + "import-local": "^3.2.0", + "jest-cli": "30.3.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.3.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "p-limit": "^3.1.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-haste-map/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.3.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/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==", + "license": "MIT" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/mafmt": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/mafmt/-/mafmt-7.1.0.tgz", + "integrity": "sha512-vpeo9S+hepT3k2h5iFxzEHvvR0GPBx9uKaErmnRzYNcaKb03DgOArjEMlgG4a9LcuZZ89a3I8xbeto487n26eA==", + "license": "MIT", + "dependencies": { + "multiaddr": "^7.3.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multiaddr": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/multiaddr/-/multiaddr-7.5.0.tgz", + "integrity": "sha512-GvhHsIGDULh06jyb6ev+VfREH9evJCFIRnh3jUt9iEZ6XDbyoisZRFEI9bMvK/AiR6y66y6P+eoBw9mBYMhMvw==", + "deprecated": "This module is deprecated, please upgrade to @multiformats/multiaddr", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "cids": "~0.8.0", + "class-is": "^1.1.0", + "is-ip": "^3.1.0", + "multibase": "^0.7.0", + "varint": "^5.0.0" + } + }, + "node_modules/multiaddr/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/multiaddr/node_modules/cids": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/cids/-/cids-0.8.3.tgz", + "integrity": "sha512-yoXTbV3llpm+EBGWKeL9xKtksPE/s6DPoDSY4fn8I8TEW1zehWXPSB0pwAXVDlLaOlrw+sNynj995uD9abmPhA==", + "deprecated": "This module has been superseded by the multiformats module", + "license": "MIT", + "dependencies": { + "buffer": "^5.6.0", + "class-is": "^1.1.0", + "multibase": "^1.0.0", + "multicodec": "^1.0.1", + "multihashes": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0", + "npm": ">=3.0.0" + } + }, + "node_modules/multiaddr/node_modules/cids/node_modules/multibase": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/multibase/-/multibase-1.0.1.tgz", + "integrity": "sha512-KcCxpBVY8fdVKu4dJMAahq4F/2Z/9xqEjIiR7PiMe7LRGeorFn2NLmicN6nLBCqQvft6MG2Lc9X5P0IdyvnxEw==", + "deprecated": "This module has been superseded by the multiformats module", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.8", + "buffer": "^5.5.0" + }, + "engines": { + "node": ">=10.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/multiaddr/node_modules/multibase": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/multibase/-/multibase-0.7.0.tgz", + "integrity": "sha512-TW8q03O0f6PNFTQDvh3xxH03c8CjGaaYrjkl9UQPG6rz53TQzzxJVCIWVjzcbN/Q5Y53Zd0IBQBMVktVgNx4Fg==", + "deprecated": "This module has been superseded by the multiformats module", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.8", + "buffer": "^5.5.0" + } + }, + "node_modules/multiaddr/node_modules/multihashes": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/multihashes/-/multihashes-1.0.1.tgz", + "integrity": "sha512-S27Tepg4i8atNiFaU5ZOm3+gl3KQlUanLs/jWcBxQHFttgq+5x1OgbQmf2d8axJ/48zYGBd/wT9d723USMFduw==", + "license": "MIT", + "dependencies": { + "buffer": "^5.6.0", + "multibase": "^1.0.1", + "varint": "^5.0.0" + }, + "engines": { + "node": ">=10.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/multiaddr/node_modules/multihashes/node_modules/multibase": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/multibase/-/multibase-1.0.1.tgz", + "integrity": "sha512-KcCxpBVY8fdVKu4dJMAahq4F/2Z/9xqEjIiR7PiMe7LRGeorFn2NLmicN6nLBCqQvft6MG2Lc9X5P0IdyvnxEw==", + "deprecated": "This module has been superseded by the multiformats module", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.8", + "buffer": "^5.5.0" + }, + "engines": { + "node": ">=10.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/multibase": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/multibase/-/multibase-0.6.1.tgz", + "integrity": "sha512-pFfAwyTjbbQgNc3G7D48JkJxWtoJoBMaR4xQUOuB8RnCgRqaYmWNFeJTTvrJ2w51bjLq2zTby6Rqj9TQ9elSUw==", + "deprecated": "This module has been superseded by the multiformats module", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.8", + "buffer": "^5.5.0" + } + }, + "node_modules/multibase/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/multicodec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/multicodec/-/multicodec-1.0.4.tgz", + "integrity": "sha512-NDd7FeS3QamVtbgfvu5h7fd1IlbaC4EQ0/pgU4zqE2vdHCmBGsUa0TiM8/TdSeG6BMPC92OOCf8F1ocE/Wkrrg==", + "deprecated": "This module has been superseded by the multiformats module", + "license": "MIT", + "dependencies": { + "buffer": "^5.6.0", + "varint": "^5.0.0" + } + }, + "node_modules/multicodec/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/multihashes": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/multihashes/-/multihashes-0.4.21.tgz", + "integrity": "sha512-uVSvmeCWf36pU2nB4/1kzYZjsXD9vofZKpgudqkceYY5g2aZZXJ5r9lxuzoRLl1OAp28XljXsEJ/X/85ZsKmKw==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "multibase": "^0.7.0", + "varint": "^5.0.0" + } + }, + "node_modules/multihashes/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/multihashes/node_modules/multibase": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/multibase/-/multibase-0.7.0.tgz", + "integrity": "sha512-TW8q03O0f6PNFTQDvh3xxH03c8CjGaaYrjkl9UQPG6rz53TQzzxJVCIWVjzcbN/Q5Y53Zd0IBQBMVktVgNx4Fg==", + "deprecated": "This module has been superseded by the multiformats module", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.8", + "buffer": "^5.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postal-mime": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz", + "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", + "license": "MIT-0" + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resend": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.12.2.tgz", + "integrity": "sha512-xwgmU4b0OqoabJsIoK/x0Whk0Fcs3bpbK4i/DEWPiE5hYJHyHl0TbB6QbI3gIr+bLdLUJ1GYm/fe41aVFuHXgw==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.4", + "svix": "1.90.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sanitize-html": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.3.tgz", + "integrity": "sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^10.1.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/strip-ansi-cjs": { + "name": "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/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/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/superagent/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/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/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svix": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.90.0.tgz", + "integrity": "sha512-ljkZuyy2+IBEoESkIpn8sLM+sxJHQcPxlZFxU+nVDhltNfUMisMBzWX/UR8SjEnzoI28ZjCzMbmYAPwSTucoMw==", + "license": "MIT", + "dependencies": { + "standardwebhooks": "1.0.0", + "uuid": "^10.0.0" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.32.4", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.4.tgz", + "integrity": "sha512-0AADFFQNJzExEN49SrD/34Nn9cxNxVLiydYl2MBwSZFPVXNkVwC/EFAjoezGGqE8oDegiDC+p47t8lKObCinMQ==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-jest": { + "version": "29.4.9", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", + "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.4", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "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 + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/varint": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz", + "integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 00000000..80e2e84f --- /dev/null +++ b/server/package.json @@ -0,0 +1,64 @@ +{ + "name": "learnvault-server", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js", + "docs:generate": "node scripts/generate-openapi.cjs", + "test": "jest --runInBand", + "test:coverage": "jest --runInBand --coverage", + "migrate": "ts-node scripts/migrate.ts up", + "migrate:rollback": "ts-node scripts/migrate.ts down", + "migrate:verify": "ts-node scripts/verify-migrations.ts", + "db:migrate": "npm run migrate", + "db:seed": "ts-node scripts/seed.ts", + "db:query:analyze": "ts-node scripts/query-analysis.ts" + }, + "dependencies": { + "@pinata/sdk": "^2.1.0", + "@sendgrid/mail": "^8.1.6", + "@stellar/stellar-sdk": "^14.4.3", + "compression": "^1.8.1", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "express-rate-limit": "^8.3.1", + "helmet": "^8.1.0", + "ioredis": "^5.6.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "multer": "^2.0.0", + "nodemailer": "^8.0.4", + "pg": "^8.20.0", + "resend": "^6.9.4", + "sanitize-html": "^2.17.3", + "side-channel": "^1.1.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "yaml": "^2.8.1", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/compression": "^1.8.1", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.7", + "@types/morgan": "^1.9.9", + "@types/multer": "^2.1.0", + "@types/node": "^22.10.6", + "@types/nodemailer": "^7.0.11", + "@types/pg": "^8.20.0", + "@types/sanitize-html": "^2.16.0", + "@types/supertest": "^7.2.0", + "@types/swagger-ui-express": "^4.1.8", + "jest": "^30.3.0", + "side-channel": "^1.1.0", + "supertest": "^7.2.2", + "ts-jest": "^29.4.6", + "ts-node-dev": "^2.0.0", + "typescript": "^5.7.2" + } +} diff --git a/server/scripts/generate-openapi.cjs b/server/scripts/generate-openapi.cjs new file mode 100644 index 00000000..d824b102 --- /dev/null +++ b/server/scripts/generate-openapi.cjs @@ -0,0 +1,150 @@ +const fs = require("node:fs") +const path = require("node:path") +const swaggerJSDoc = require("swagger-jsdoc") +const YAML = require("yaml") + +const routesGlob = path.resolve(__dirname, "../src/routes/*.ts") + +const spec = swaggerJSDoc({ + definition: { + openapi: "3.0.3", + info: { + title: "LearnVault API", + version: "1.0.0", + description: "Backend API for LearnVault frontend and integrations.", + }, + servers: [ + { + url: "http://localhost:4000", + description: "Local development server", + }, + ], + tags: [ + { name: "Health", description: "Server status endpoints" }, + { name: "Courses", description: "Course catalog endpoints" }, + { name: "Validator", description: "Milestone validation endpoints" }, + { name: "Events", description: "Event stream endpoints" }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + schemas: { + ErrorResponse: { + type: "object", + properties: { + error: { + type: "string", + }, + }, + required: ["error"], + }, + HealthResponse: { + type: "object", + properties: { + status: { type: "string", example: "ok" }, + timestamp: { type: "string", format: "date-time" }, + }, + required: ["status", "timestamp"], + }, + Course: { + type: "object", + properties: { + id: { type: "string" }, + title: { type: "string" }, + level: { type: "string" }, + published: { type: "boolean" }, + }, + required: ["id", "title", "level", "published"], + }, + Event: { + type: "object", + properties: { + id: { type: "string" }, + type: { type: "string" }, + entityId: { type: "string" }, + timestamp: { type: "string", format: "date-time" }, + }, + required: ["id", "type", "entityId", "timestamp"], + }, + ValidatorRequest: { + type: "object", + properties: { + courseId: { type: "string" }, + learnerAddress: { type: "string" }, + milestoneId: { type: "integer", minimum: 0 }, + }, + required: ["courseId", "learnerAddress", "milestoneId"], + }, + ValidatorResult: { + allOf: [ + { $ref: "#/components/schemas/ValidatorRequest" }, + { + type: "object", + properties: { + approved: { type: "boolean" }, + validator: { type: "string" }, + }, + required: ["approved", "validator"], + }, + ], + }, + }, + responses: { + BadRequestError: { + description: "Bad request", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/ErrorResponse", + }, + }, + }, + }, + UnauthorizedError: { + description: "Unauthorized", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/ErrorResponse", + }, + }, + }, + }, + NotFoundError: { + description: "Resource not found", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/ErrorResponse", + }, + }, + }, + }, + InternalServerError: { + description: "Internal server error", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/ErrorResponse", + }, + }, + }, + }, + }, + }, + }, + apis: [routesGlob], +}) + +const yaml = YAML.stringify(spec) +const outputPath = path.resolve(__dirname, "../../docs/openapi.yaml") + +fs.mkdirSync(path.dirname(outputPath), { recursive: true }) +fs.writeFileSync(outputPath, yaml, "utf8") + +console.log(`OpenAPI spec written to ${outputPath}`) diff --git a/server/scripts/migrate.ts b/server/scripts/migrate.ts new file mode 100644 index 00000000..0bfb9f87 --- /dev/null +++ b/server/scripts/migrate.ts @@ -0,0 +1,148 @@ +#!/usr/bin/env ts-node +/** + * Migration runner — executes *.sql files in src/db/migrations/ in order. + * Tracks applied migrations in a `schema_migrations` table so each file runs + * exactly once. + * + * Usage: + * npm run migrate — apply all pending migrations + * npm run migrate:rollback — revert the last applied migration (requires a + * matching *.undo.sql file alongside the migration) + */ + +import fs from "node:fs" +import path from "node:path" +import dotenv from "dotenv" +import { Pool, type PoolClient } from "pg" + +dotenv.config({ path: path.resolve(__dirname, "../.env") }) + +const DATABASE_URL = process.env.DATABASE_URL +if (!DATABASE_URL) { + console.error("ERROR: DATABASE_URL is not set in server/.env") + process.exit(1) +} + +const pool = new Pool({ connectionString: DATABASE_URL }) +const MIGRATIONS_DIR = path.resolve(__dirname, "../src/db/migrations") +const command = process.argv[2] ?? "up" + +async function ensureTrackingTable(client: PoolClient): Promise { + await client.query(` + CREATE TABLE IF NOT EXISTS schema_migrations ( + filename TEXT PRIMARY KEY, + applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + `) +} + +async function migrateUp(): Promise { + const client = await pool.connect() + try { + await ensureTrackingTable(client) + + const { rows: applied } = await client.query<{ filename: string }>( + "SELECT filename FROM schema_migrations ORDER BY filename", + ) + const appliedSet = new Set( + applied.map((r: { filename: string }) => r.filename), + ) + + const files = fs + .readdirSync(MIGRATIONS_DIR) + .filter((f: string) => f.endsWith(".sql") && !f.endsWith(".undo.sql")) + .sort() + + let ran = 0 + for (const file of files) { + if (appliedSet.has(file)) { + console.log(` skip ${file}`) + continue + } + + const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, file), "utf8") + console.log(` apply ${file}`) + + await client.query("BEGIN") + try { + // Non-parameterized query uses the simple query protocol, + // which supports multiple statements in a single call. + await client.query(sql) + await client.query( + "INSERT INTO schema_migrations (filename) VALUES ($1)", + [file], + ) + await client.query("COMMIT") + ran++ + } catch (err) { + await client.query("ROLLBACK") + console.error(` FAILED ${file}:`, err) + process.exit(1) + } + } + + console.log(`\nMigrations complete. ${ran} new migration(s) applied.`) + } finally { + client.release() + await pool.end() + } +} + +async function migrateDown(): Promise { + const client: PoolClient = await pool.connect() + try { + await ensureTrackingTable(client) + + const { rows } = await client.query<{ filename: string }>( + "SELECT filename FROM schema_migrations ORDER BY filename DESC LIMIT 1", + ) + + if (rows.length === 0) { + console.log("Nothing to roll back.") + return + } + + const last = rows[0].filename + const undoFile = last.replace(/\.sql$/, ".undo.sql") + const undoPath = path.join(MIGRATIONS_DIR, undoFile) + + if (!fs.existsSync(undoPath)) { + console.error( + ` ERROR: No undo file found for ${last} (expected ${undoFile})`, + ) + process.exit(1) + } + + const sql = fs.readFileSync(undoPath, "utf8") + console.log(` rollback ${last}`) + + await client.query("BEGIN") + try { + await client.query(sql) + await client.query("DELETE FROM schema_migrations WHERE filename = $1", [ + last, + ]) + await client.query("COMMIT") + console.log(`\nRolled back: ${last}`) + } catch (err) { + await client.query("ROLLBACK") + console.error(` FAILED rollback of ${last}:`, err) + process.exit(1) + } + } finally { + client.release() + await pool.end() + } +} + +if (command === "down") { + migrateDown().catch((err) => { + console.error(err) + process.exit(1) + }) +} else { + migrateUp().catch((err) => { + console.error(err) + process.exit(1) + }) +} diff --git a/server/scripts/query-analysis.ts b/server/scripts/query-analysis.ts new file mode 100644 index 00000000..0a7c30b4 --- /dev/null +++ b/server/scripts/query-analysis.ts @@ -0,0 +1,197 @@ +#!/usr/bin/env ts-node +import fs from "node:fs" +import path from "node:path" +import dotenv from "dotenv" +import { Pool } from "pg" + +dotenv.config({ path: path.resolve(__dirname, "../.env") }) + +const DATABASE_URL = process.env.DATABASE_URL +if (!DATABASE_URL) { + console.error("ERROR: DATABASE_URL is not set in server/.env") + process.exit(1) +} + +const OUTPUT_PATH = path.resolve( + __dirname, + "../../docs/database/query-analysis.md", +) + +const queries: Array<{ name: string; sql: string; params: unknown[] }> = [ + { + name: "Courses list with enrollments", + sql: `SELECT c.id, c.slug, c.title, COUNT(DISTINCT e.learner_address)::int AS students_count + FROM courses c + LEFT JOIN enrollments e ON e.course_id = c.slug + WHERE c.published_at IS NOT NULL + GROUP BY c.id, c.slug, c.title + ORDER BY c.created_at DESC + LIMIT $1 OFFSET $2`, + params: [12, 0], + }, + { + name: "Course lessons with quiz payload", + sql: `SELECT l.id, l.course_id, l.title, l.order_index, + BOOL_OR(m.id IS NOT NULL) AS is_milestone + FROM lessons l + LEFT JOIN milestones m ON m.lesson_id = l.id + LEFT JOIN quizzes q ON q.lesson_id = l.id + LEFT JOIN quiz_questions qq ON qq.quiz_id = q.id + WHERE l.course_id = $1 + GROUP BY l.id + ORDER BY l.order_index ASC`, + params: [1], + }, + { + name: "Milestone reports by scholar + status", + sql: `SELECT * + FROM milestone_reports + WHERE scholar_address = $1 AND status = $2 + ORDER BY submitted_at DESC`, + params: [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pending", + ], + }, + { + name: "Latest audit decision per report", + sql: `SELECT DISTINCT ON (report_id) report_id, decided_at, contract_tx_hash + FROM milestone_audit_log + WHERE report_id = ANY($1::int[]) + ORDER BY report_id, decided_at DESC`, + params: [[1, 2, 3]], + }, + { + name: "Governance proposals listing", + sql: `SELECT p.id, p.status, p.deadline, p.created_at + FROM proposals p + WHERE p.status = $1 + ORDER BY p.created_at DESC + LIMIT $2 OFFSET $3`, + params: ["pending", 20, 0], + }, + { + name: "Single proposal vote lookup", + sql: `SELECT id FROM votes WHERE proposal_id = $1 AND voter_address = $2`, + params: [1, "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"], + }, + { + name: "Leaderboard page", + sql: `SELECT address, lrn_balance, courses_completed + FROM scholar_balances + ORDER BY lrn_balance DESC, address ASC + LIMIT $1 OFFSET $2`, + params: [50, 0], + }, + { + name: "Enrollments by learner", + sql: `SELECT id, learner_address, course_id, tx_hash, enrolled_at + FROM enrollments + WHERE learner_address = $1 + ORDER BY enrolled_at DESC`, + params: ["GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"], + }, + { + name: "Comments by proposal", + sql: `SELECT id, author_address, content, created_at + FROM comments + WHERE proposal_id = $1 + ORDER BY is_pinned DESC, created_at DESC + LIMIT $2 OFFSET $3`, + params: ["1", 20, 0], + }, + { + name: "Recent events by contract", + sql: `SELECT id, event_type, created_at + FROM events + WHERE contract = $1 + ORDER BY created_at DESC + LIMIT $2`, + params: ["sample_contract", 25], + }, +] + +async function main(): Promise { + const pool = new Pool({ connectionString: DATABASE_URL }) + const client = await pool.connect() + + try { + const lines: string[] = [] + lines.push("# Database Query Analysis") + lines.push("") + lines.push( + `Generated: ${new Date().toISOString()} via \`npm run db:query:analyze\`.`, + ) + lines.push("") + + for (const queryDef of queries) { + lines.push(`## ${queryDef.name}`) + lines.push("") + lines.push("```sql") + lines.push(queryDef.sql.trim()) + lines.push("```") + lines.push("") + + try { + const explainResult = await client.query( + `EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${queryDef.sql}`, + queryDef.params, + ) + lines.push("```text") + for (const row of explainResult.rows) { + lines.push(String(row["QUERY PLAN"])) + } + lines.push("```") + } catch (err) { + lines.push( + "> Explain unavailable for this query in current environment.", + ) + lines.push(`> Error: ${String(err)}`) + } + + lines.push("") + } + + try { + const stats = await client.query( + `SELECT + LEFT(REGEXP_REPLACE(query, '\\s+', ' ', 'g'), 300) AS query, + calls::int AS calls, + total_exec_time::float8 AS total_exec_time_ms, + mean_exec_time::float8 AS mean_exec_time_ms + FROM pg_stat_statements + ORDER BY mean_exec_time DESC + LIMIT 10`, + ) + lines.push("## pg_stat_statements Top 10") + lines.push("") + lines.push("| mean_exec_time_ms | calls | total_exec_time_ms | query |") + lines.push("| ---: | ---: | ---: | --- |") + for (const row of stats.rows) { + lines.push( + `| ${Number(row.mean_exec_time_ms).toFixed(2)} | ${row.calls} | ${Number(row.total_exec_time_ms).toFixed(2)} | ${String(row.query).replace(/\|/g, "\\|")} |`, + ) + } + lines.push("") + } catch { + lines.push("## pg_stat_statements Top 10") + lines.push("") + lines.push( + "> pg_stat_statements is not enabled on this PostgreSQL instance.", + ) + lines.push("") + } + + fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true }) + fs.writeFileSync(OUTPUT_PATH, lines.join("\n")) + console.log(`Wrote query analysis report to ${OUTPUT_PATH}`) + } finally { + client.release() + await pool.end() + } +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/server/scripts/seed.ts b/server/scripts/seed.ts new file mode 100644 index 00000000..bb3ea55c --- /dev/null +++ b/server/scripts/seed.ts @@ -0,0 +1,43 @@ +#!/usr/bin/env ts-node +/** + * Seed script — loads src/db/seed.sql into the dev database. + * + * Usage: npm run db:seed + * + * Requires DATABASE_URL in server/.env and migrations to have been run first. + */ + +import fs from "node:fs" +import path from "node:path" +import dotenv from "dotenv" +import { Pool } from "pg" + +dotenv.config({ path: path.resolve(__dirname, "../.env") }) + +const DATABASE_URL = process.env.DATABASE_URL +if (!DATABASE_URL) { + console.error("ERROR: DATABASE_URL is not set in server/.env") + process.exit(1) +} + +const pool = new Pool({ connectionString: DATABASE_URL }) + +const SEED_FILE = path.resolve(__dirname, "../src/db/seed.sql") + +async function run(): Promise { + const sql = fs.readFileSync(SEED_FILE, "utf8") + const client = await pool.connect() + try { + console.log("Seeding database...") + await client.query(sql) + console.log("Seed complete.") + } finally { + client.release() + await pool.end() + } +} + +run().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/server/scripts/verify-migrations.ts b/server/scripts/verify-migrations.ts new file mode 100644 index 00000000..77a72d17 --- /dev/null +++ b/server/scripts/verify-migrations.ts @@ -0,0 +1,233 @@ +#!/usr/bin/env ts-node +import fs from "node:fs" +import path from "node:path" +import dotenv from "dotenv" +import { Pool, type PoolClient } from "pg" + +dotenv.config({ path: path.resolve(__dirname, "../.env") }) + +const DATABASE_URL = process.env.DATABASE_URL +if (!DATABASE_URL) { + console.error("ERROR: DATABASE_URL is not set in server/.env") + process.exit(1) +} + +const pool = new Pool({ connectionString: DATABASE_URL }) +const MIGRATIONS_DIR = path.resolve(__dirname, "../src/db/migrations") +const schemaName = `migration_verify_${Date.now()}_${Math.floor(Math.random() * 10_000)}` + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(message) + } +} + +function listUpMigrations(): string[] { + return fs + .readdirSync(MIGRATIONS_DIR) + .filter((f) => f.endsWith(".sql") && !f.endsWith(".undo.sql")) + .sort() +} + +function listDownMigrations(): string[] { + return fs + .readdirSync(MIGRATIONS_DIR) + .filter((f) => f.endsWith(".undo.sql")) + .sort() +} + +async function setSearchPath(client: PoolClient): Promise { + await client.query(`SET search_path TO "${schemaName}", public`) +} + +async function applyMigration( + client: PoolClient, + filename: string, +): Promise { + const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, filename), "utf8") + await client.query("BEGIN") + try { + await setSearchPath(client) + await client.query(sql) + await client.query("COMMIT") + console.log(` apply ${filename}`) + } catch (err) { + await client.query("ROLLBACK") + throw new Error(`Failed applying ${filename}: ${String(err)}`) + } +} + +async function rollbackMigration( + client: PoolClient, + filename: string, +): Promise { + const undoFile = filename.replace(/\.sql$/, ".undo.sql") + const undoPath = path.join(MIGRATIONS_DIR, undoFile) + if (!fs.existsSync(undoPath)) { + throw new Error(`Missing undo file for ${filename} (${undoFile})`) + } + + const sql = fs.readFileSync(undoPath, "utf8") + await client.query("BEGIN") + try { + await setSearchPath(client) + await client.query(sql) + await client.query("COMMIT") + console.log(` rollback ${filename}`) + } catch (err) { + await client.query("ROLLBACK") + throw new Error(`Failed rollback for ${filename}: ${String(err)}`) + } +} + +async function verifyConstraints(client: PoolClient): Promise { + await setSearchPath(client) + + const notNullResult = await client.query( + `SELECT is_nullable + FROM information_schema.columns + WHERE table_schema = $1 + AND table_name = 'lessons' + AND column_name = 'estimated_minutes'`, + [schemaName], + ) + assert( + notNullResult.rows[0]?.is_nullable === "NO", + "lessons.estimated_minutes must be NOT NULL", + ) + + const uniqueResult = await client.query( + `SELECT 1 + FROM pg_constraint c + JOIN pg_class t ON t.oid = c.conrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = $1 + AND t.relname = 'enrollments' + AND c.contype = 'u' + AND pg_get_constraintdef(c.oid) ILIKE '%(learner_address, course_id)%'`, + [schemaName], + ) + assert( + uniqueResult.rows.length > 0, + "Expected UNIQUE(learner_address, course_id) on enrollments", + ) + + const fkResult = await client.query( + `SELECT 1 + FROM pg_constraint c + JOIN pg_class t ON t.oid = c.conrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = $1 + AND t.relname = 'lessons' + AND c.contype = 'f' + AND pg_get_constraintdef(c.oid) ILIKE '%REFERENCES courses(id)%'`, + [schemaName], + ) + assert( + fkResult.rows.length > 0, + "Expected lessons.course_id foreign key to courses(id)", + ) + + console.log(" constraints validated") +} + +async function verifyIndexes(client: PoolClient): Promise { + await setSearchPath(client) + const expectedIndexes = [ + "idx_lessons_course_id", + "idx_milestones_course_id", + "idx_quiz_questions_quiz_id", + "idx_proposals_status_created_at", + "idx_votes_proposal_id", + "idx_enrollments_learner_address", + "idx_events_contract_event_ledger", + "idx_milestone_reports_scholar_status_submitted", + "idx_milestone_audit_report_decided_at", + ] + + const result = await client.query( + `SELECT indexname + FROM pg_indexes + WHERE schemaname = $1`, + [schemaName], + ) + const indexSet = new Set( + result.rows.map((row) => String(row.indexname)), + ) + + for (const indexName of expectedIndexes) { + assert(indexSet.has(indexName), `Missing expected index: ${indexName}`) + } + console.log(" indexes validated") +} + +async function verifyRollbackReachedCleanSchema( + client: PoolClient, +): Promise { + await setSearchPath(client) + const result = await client.query( + `SELECT COUNT(*)::int AS count + FROM information_schema.tables + WHERE table_schema = $1`, + [schemaName], + ) + const tableCount = Number(result.rows[0]?.count ?? 0) + assert( + tableCount === 0, + `Expected empty schema after full rollback, found ${tableCount} table(s)`, + ) + console.log(" rollback validated (schema clean)") +} + +async function main(): Promise { + const client = await pool.connect() + try { + console.log(`Creating verification schema: ${schemaName}`) + await client.query(`CREATE SCHEMA "${schemaName}"`) + await setSearchPath(client) + + const upMigrations = listUpMigrations() + const downMigrations = listDownMigrations() + assert(upMigrations.length > 0, "No migration files found") + assert(downMigrations.length > 0, "No undo migration files found") + + console.log("\nStep 1: apply all migrations") + for (const file of upMigrations) { + await applyMigration(client, file) + } + + console.log("\nStep 2: idempotency check (re-apply all migrations)") + for (const file of upMigrations) { + await applyMigration(client, file) + } + + console.log("\nStep 3: validate constraints and indexes") + await verifyConstraints(client) + await verifyIndexes(client) + + console.log("\nStep 4: rollback each migration in reverse order") + for (const file of [...upMigrations].reverse()) { + await rollbackMigration(client, file) + } + await verifyRollbackReachedCleanSchema(client) + + console.log("\nStep 5: re-apply all migrations after rollback") + for (const file of upMigrations) { + await applyMigration(client, file) + } + + console.log("\nMigration verification complete.") + } finally { + try { + await client.query(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`) + } finally { + client.release() + await pool.end() + } + } +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/server/src/controllers/admin-milestones.controller.test.ts b/server/src/controllers/admin-milestones.controller.test.ts new file mode 100644 index 00000000..4c37dd1d --- /dev/null +++ b/server/src/controllers/admin-milestones.controller.test.ts @@ -0,0 +1,475 @@ +/** + * Unit tests for admin-milestones controller. + * + * All external dependencies (DB, Stellar, credential service) are mocked so + * tests run in isolation without a database or Stellar SDK. + * + * Covers: + * POST /api/admin/milestones/:id/approve + * - happy path: pending → approved, real txHash returned + * - 404 report not found + * - 409 already approved + * - 503 missing Stellar credentials + * POST /api/admin/milestones/:id/reject + * - happy path: pending → rejected, reason stored + * - 400 missing reason field + * GET /api/admin/milestones/pending + * - returns only pending reports + */ + +// Must be declared before any imports so Jest hoisting works correctly. +jest.mock("../db/index", () => ({ + pool: { query: jest.fn(), connect: jest.fn() }, +})) +jest.mock("../db/milestone-store") +jest.mock("../services/stellar-contract.service") +jest.mock("../services/credential.service") + +jest.mock("../services/email.service", () => ({ + createEmailService: jest.fn().mockReturnValue({ + sendNotification: jest.fn().mockResolvedValue(undefined), + }), +})) + +jest.mock("../services/escrow-timeout.service", () => ({ + markEscrowActivity: jest.fn().mockResolvedValue(undefined), +})) + +import express from "express" +import jwt from "jsonwebtoken" +import request from "supertest" + +import { milestoneStore } from "../db/milestone-store" +import { errorHandler } from "../middleware/error.middleware" +import { adminMilestonesRouter } from "../routes/admin-milestones.routes" +import { credentialService } from "../services/credential.service" +import { stellarContractService } from "../services/stellar-contract.service" + +// ── Typed mock helpers ─────────────────────────────────────────────────────── + +const mockStore = milestoneStore as jest.Mocked +const mockStellar = stellarContractService as jest.Mocked< + typeof stellarContractService +> +const mockCredential = credentialService as jest.Mocked< + typeof credentialService +> + +// ── Shared fixtures ────────────────────────────────────────────────────────── + +const JWT_SECRET = "learnvault-secret" + +const pendingReport = { + id: 1, + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_github: null, + evidence_ipfs_cid: null, + evidence_description: "Completed all exercises", + status: "pending" as const, + submitted_at: new Date().toISOString(), + scholar_email: "scholar@example.com", + scholar_name: "Test Scholar", + course_title: "Test Course", + milestone_title: "Test Milestone", + milestone_number: 1, + lrn_reward: 100, + resubmission_count: 0, +} + +const approvedAuditEntry = { + id: 1, + report_id: 1, + validator_address: "GADMIN123", + decision: "approved" as const, + rejection_reason: null, + contract_tx_hash: "real_tx_hash_abc", + decided_at: new Date().toISOString(), +} + +function makeAdminToken(address = "GADMIN123") { + return jwt.sign({ address }, JWT_SECRET, { expiresIn: "1h" }) +} + +function buildApp() { + const app = express() + app.use(express.json()) + app.use("/api", adminMilestonesRouter) + app.use(errorHandler) + return app +} + +// ── Test lifecycle ─────────────────────────────────────────────────────────── + +beforeEach(() => { + jest.clearAllMocks() + + // Default happy-path stubs — individual tests override as needed. + mockStellar.callVerifyMilestone.mockResolvedValue({ + txHash: "real_tx_hash_abc", + simulated: false, + }) + mockStellar.emitRejectionEvent.mockResolvedValue({ + txHash: "reject_tx_hash_xyz", + simulated: false, + }) + mockStore.updateReportStatus.mockResolvedValue({ + ...pendingReport, + status: "approved", + }) + mockStore.addAuditEntry.mockResolvedValue(approvedAuditEntry) + mockCredential.mintCertificateIfComplete.mockResolvedValue({ minted: false }) + + // Provide Stellar credentials so the controller's 503 guard passes by + // default. The 503 test removes them explicitly. + process.env.STELLAR_SECRET_KEY = "FAKE_TEST_SECRET" + process.env.COURSE_MILESTONE_CONTRACT_ID = "FAKE_CONTRACT_ID" + process.env.FRONTEND_URL = "http://localhost:3000" +}) + +afterEach(() => { + delete process.env.STELLAR_SECRET_KEY + delete process.env.COURSE_MILESTONE_CONTRACT_ID + delete process.env.FRONTEND_URL +}) + +// ── GET /api/admin/milestones/pending ──────────────────────────────────────── + +describe("GET /api/admin/milestones/pending", () => { + it("returns only pending reports for an authenticated admin", async () => { + mockStore.getPendingReports.mockResolvedValue([pendingReport]) + + const res = await request(buildApp()) + .get("/api/admin/milestones/pending") + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(200) + expect(res.body.data).toHaveLength(1) + expect(res.body.data[0].status).toBe("pending") + expect(res.body.data[0].id).toBe(1) + }) + + it("returns an empty array when no pending reports exist", async () => { + mockStore.getPendingReports.mockResolvedValue([]) + + const res = await request(buildApp()) + .get("/api/admin/milestones/pending") + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(200) + expect(res.body.data).toHaveLength(0) + }) + + it("returns 401 without an auth token", async () => { + const res = await request(buildApp()).get("/api/admin/milestones/pending") + + expect(res.status).toBe(401) + expect(mockStore.getPendingReports).not.toHaveBeenCalled() + }) +}) + +// ── POST /api/admin/milestones/:id/approve ─────────────────────────────────── + +describe("POST /api/admin/milestones/:id/approve", () => { + it("happy path: transitions pending → approved and returns real txHash", async () => { + mockStore.getReportById.mockResolvedValue(pendingReport) + + const res = await request(buildApp()) + .post("/api/admin/milestones/1/approve") + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(200) + expect(res.body.data.status).toBe("approved") + expect(res.body.data.contractTxHash).toBe("real_tx_hash_abc") + expect(res.body.data.simulated).toBe(false) + expect(res.body.data.auditEntry.decision).toBe("approved") + expect(res.body.data.auditEntry.validator_address).toBe("GADMIN123") + + expect(mockStore.updateReportStatus).toHaveBeenCalledWith(1, "approved") + expect(mockStore.addAuditEntry).toHaveBeenCalledWith( + expect.objectContaining({ + report_id: 1, + decision: "approved", + contract_tx_hash: "real_tx_hash_abc", + }), + ) + }) + + it("returns 404 when the milestone report does not exist", async () => { + mockStore.getReportById.mockResolvedValue(null) + + const res = await request(buildApp()) + .post("/api/admin/milestones/999/approve") + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(404) + expect(res.body.error).toBe("Milestone report not found") + expect(mockStellar.callVerifyMilestone).not.toHaveBeenCalled() + }) + + it("returns 409 when the report is already approved", async () => { + mockStore.getReportById.mockResolvedValue({ + ...pendingReport, + status: "approved", + }) + + const res = await request(buildApp()) + .post("/api/admin/milestones/1/approve") + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(409) + expect(res.body.error).toMatch(/already approved/i) + expect(mockStellar.callVerifyMilestone).not.toHaveBeenCalled() + }) + + it("returns 409 when the report is already rejected", async () => { + mockStore.getReportById.mockResolvedValue({ + ...pendingReport, + status: "rejected", + }) + + const res = await request(buildApp()) + .post("/api/admin/milestones/1/approve") + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(409) + expect(mockStellar.callVerifyMilestone).not.toHaveBeenCalled() + }) + + it("returns 503 when Stellar credentials are not configured", async () => { + delete process.env.STELLAR_SECRET_KEY + delete process.env.COURSE_MILESTONE_CONTRACT_ID + + mockStore.getReportById.mockResolvedValue(pendingReport) + + const res = await request(buildApp()) + .post("/api/admin/milestones/1/approve") + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(503) + expect(res.body.error).toBe("Stellar credentials not configured") + // Must not proceed to the on-chain call or DB write + expect(mockStellar.callVerifyMilestone).not.toHaveBeenCalled() + expect(mockStore.updateReportStatus).not.toHaveBeenCalled() + }) + + it("includes certificate data when all milestones are complete", async () => { + mockStore.getReportById.mockResolvedValue(pendingReport) + mockCredential.mintCertificateIfComplete.mockResolvedValue({ + minted: true, + tokenUri: "ipfs://Qm...", + mintTxHash: "mint_hash_abc", + simulated: false, + }) + + const res = await request(buildApp()) + .post("/api/admin/milestones/1/approve") + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(200) + expect(res.body.data.certificate).toMatchObject({ + minted: true, + mintTxHash: "mint_hash_abc", + }) + }) +}) + +describe("POST /api/admin/milestones/batch-approve", () => { + it("approves every requested pending report and returns per-report results", async () => { + mockStore.getReportById + .mockResolvedValueOnce(pendingReport) + .mockResolvedValueOnce({ + ...pendingReport, + id: 2, + milestone_id: 2, + }) + mockStore.addAuditEntry + .mockResolvedValueOnce(approvedAuditEntry) + .mockResolvedValueOnce({ + ...approvedAuditEntry, + id: 2, + report_id: 2, + }) + + const res = await request(buildApp()) + .post("/api/admin/milestones/batch-approve") + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({ milestoneIds: [1, 2] }) + + expect(res.status).toBe(200) + expect(res.body.data.succeeded).toBe(2) + expect(res.body.data.failed).toBe(0) + expect(res.body.data.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + reportId: 1, + success: true, + status: "approved", + }), + expect.objectContaining({ + reportId: 2, + success: true, + status: "approved", + }), + ]), + ) + expect(mockStore.updateReportStatus).toHaveBeenNthCalledWith( + 1, + 1, + "approved", + ) + expect(mockStore.updateReportStatus).toHaveBeenNthCalledWith( + 2, + 2, + "approved", + ) + }) + + it("returns 404 when any milestone in the batch does not exist", async () => { + mockStore.getReportById + .mockResolvedValueOnce(pendingReport) + .mockResolvedValueOnce(null) + + const res = await request(buildApp()) + .post("/api/admin/milestones/batch-approve") + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({ milestoneIds: [1, 999] }) + + expect(res.status).toBe(404) + expect(res.body.error).toBe("One or more milestone reports were not found") + expect(res.body.data.results).toEqual([ + expect.objectContaining({ + reportId: 999, + success: false, + status: "not_found", + }), + ]) + expect(mockStellar.callVerifyMilestone).not.toHaveBeenCalled() + }) +}) + +// ── POST /api/admin/milestones/:id/reject ──────────────────────────────────── + +describe("POST /api/admin/milestones/:id/reject", () => { + it("happy path: transitions pending → rejected and stores the reason", async () => { + mockStore.getReportById.mockResolvedValue(pendingReport) + mockStore.updateReportStatus.mockResolvedValue({ + ...pendingReport, + status: "rejected", + }) + mockStore.addAuditEntry.mockResolvedValue({ + ...approvedAuditEntry, + decision: "rejected", + rejection_reason: "Insufficient evidence", + contract_tx_hash: "reject_tx_hash_xyz", + }) + + const res = await request(buildApp()) + .post("/api/admin/milestones/1/reject") + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({ reason: "Insufficient evidence" }) + + expect(res.status).toBe(200) + expect(res.body.data.status).toBe("rejected") + expect(res.body.data.reason).toBe("Insufficient evidence") + expect(res.body.data.auditEntry.rejection_reason).toBe( + "Insufficient evidence", + ) + expect(res.body.data.contractTxHash).toBe("reject_tx_hash_xyz") + + expect(mockStore.updateReportStatus).toHaveBeenCalledWith(1, "rejected") + expect(mockStore.addAuditEntry).toHaveBeenCalledWith( + expect.objectContaining({ + report_id: 1, + decision: "rejected", + rejection_reason: "Insufficient evidence", + }), + ) + }) + + it("returns 400 when the reason field is missing", async () => { + mockStore.getReportById.mockResolvedValue(pendingReport) + + const res = await request(buildApp()) + .post("/api/admin/milestones/1/reject") + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({}) + + expect(res.status).toBe(400) + expect(res.body.details).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + field: "reason", + message: "reason is required", + }), + ]), + ) + expect(mockStore.updateReportStatus).not.toHaveBeenCalled() + }) + + it("returns 404 when the milestone report does not exist", async () => { + mockStore.getReportById.mockResolvedValue(null) + + const res = await request(buildApp()) + .post("/api/admin/milestones/999/reject") + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({ reason: "Does not qualify" }) + + expect(res.status).toBe(404) + expect(res.body.error).toBe("Milestone report not found") + }) + + it("returns 409 when the report is already rejected", async () => { + mockStore.getReportById.mockResolvedValue({ + ...pendingReport, + status: "rejected", + }) + + const res = await request(buildApp()) + .post("/api/admin/milestones/1/reject") + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({ reason: "Re-rejection attempt" }) + + expect(res.status).toBe(409) + }) + + it("returns 401 without an auth token", async () => { + const res = await request(buildApp()) + .post("/api/admin/milestones/1/reject") + .send({ reason: "No auth" }) + + expect(res.status).toBe(401) + expect(mockStore.getReportById).not.toHaveBeenCalled() + }) +}) + +describe("POST /api/admin/milestones/batch-reject", () => { + it("returns 409 when any report is already processed before the batch starts", async () => { + mockStore.getReportById + .mockResolvedValueOnce(pendingReport) + .mockResolvedValueOnce({ + ...pendingReport, + id: 2, + status: "approved", + }) + + const res = await request(buildApp()) + .post("/api/admin/milestones/batch-reject") + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({ milestoneIds: [1, 2], reason: "Batch reject" }) + + expect(res.status).toBe(409) + expect(res.body.error).toBe( + "All milestone reports must be pending before batch processing", + ) + expect(res.body.data.results).toEqual([ + expect.objectContaining({ + reportId: 2, + success: false, + status: "approved", + }), + ]) + expect(mockStellar.emitRejectionEvent).not.toHaveBeenCalled() + }) +}) diff --git a/server/src/controllers/admin-milestones.controller.ts b/server/src/controllers/admin-milestones.controller.ts new file mode 100644 index 00000000..8b75ba9f --- /dev/null +++ b/server/src/controllers/admin-milestones.controller.ts @@ -0,0 +1,521 @@ +import { type Request, type Response } from "express" +import sanitizeHtml from "sanitize-html" +import { milestoneStore } from "../db/milestone-store" +import { type AdminRequest } from "../middleware/admin.middleware" +import { credentialService } from "../services/credential.service" +import { createEmailService } from "../services/email.service" +import { markEscrowActivity } from "../services/escrow-timeout.service" +import { stellarContractService } from "../services/stellar-contract.service" +import { templates, toPlainText } from "../templates/email-templates" + +const emailService = createEmailService( + process.env.RESEND_API_KEY || process.env.EMAIL_API_KEY || "", +) + +type MilestoneStatusFilter = "pending" | "approved" | "rejected" + +function hasStellarMilestoneCredentials(): boolean { + return Boolean( + process.env.STELLAR_SECRET_KEY && process.env.COURSE_MILESTONE_CONTRACT_ID, + ) +} + +// ── GET /api/admin/milestones/pending ──────────────────────────────────────── + +export async function listMilestones( + req: Request, + res: Response, +): Promise { + const page = + typeof req.query.page === "string" ? Number.parseInt(req.query.page, 10) : 1 + const pageSize = + typeof req.query.pageSize === "string" + ? Number.parseInt(req.query.pageSize, 10) + : 10 + const courseId = + typeof req.query.course === "string" ? req.query.course : undefined + const status = + typeof req.query.status === "string" + ? (req.query.status as MilestoneStatusFilter) + : undefined + + if ( + status && + status !== "pending" && + status !== "approved" && + status !== "rejected" + ) { + res.status(400).json({ error: "Invalid milestone status filter" }) + return + } + + try { + const safePage = Number.isFinite(page) && page > 0 ? page : 1 + const safePageSize = + Number.isFinite(pageSize) && pageSize > 0 ? Math.min(pageSize, 100) : 10 + const result = await milestoneStore.listReports( + { + courseId, + status, + }, + safePage, + safePageSize, + ) + + res.status(200).json({ + data: result.data, + total: result.total, + page: safePage, + pageSize: safePageSize, + }) + } catch (err) { + console.error("[admin] listMilestones error:", err) + res.status(500).json({ error: "Failed to fetch milestones" }) + } +} + +export async function getPendingMilestones( + _req: Request, + res: Response, +): Promise { + try { + const reports = await milestoneStore.getPendingReports() + res.status(200).json({ data: reports }) + } catch (err) { + console.error("[admin] getPendingMilestones error:", err) + res.status(500).json({ error: "Failed to fetch pending milestones" }) + } +} + +export async function getMilestoneById( + req: Request, + res: Response, +): Promise { + const id = Number(req.params.id) + if (!Number.isInteger(id) || id <= 0) { + res.status(400).json({ error: "Invalid milestone report id" }) + return + } + + try { + const report = await milestoneStore.getReportById(id) + if (!report) { + res.status(404).json({ error: "Milestone report not found" }) + return + } + const auditLog = await milestoneStore.getAuditForReport(id) + res.status(200).json({ data: { ...report, auditLog } }) + } catch (err) { + console.error("[admin] getMilestoneById error:", err) + res.status(500).json({ error: "Failed to fetch milestone report" }) + } +} + +export async function approveMilestone( + req: AdminRequest, + res: Response, +): Promise { + const id = Number(req.params.id) + if (!Number.isInteger(id) || id <= 0) { + res.status(400).json({ error: "Invalid milestone report id" }) + return + } + + const validatorAddress = req.adminAddress ?? "unknown" + + try { + const report = await milestoneStore.getReportById(id) + if (!report) { + res.status(404).json({ error: "Milestone report not found" }) + return + } + if (report.status !== "pending") { + res.status(409).json({ error: `Report already ${report.status}` }) + return + } + if (!hasStellarMilestoneCredentials()) { + res.status(503).json({ error: "Stellar credentials not configured" }) + return + } + + // Trigger on-chain verify_milestone() call + const contractResult = await stellarContractService.callVerifyMilestone( + report.scholar_address, + report.course_id, + report.milestone_id, + { requestId: req.requestId }, + ) + + // Persist decision + await milestoneStore.updateReportStatus(id, "approved") + try { + await markEscrowActivity(report.scholar_address, report.course_id) + } catch (trackingErr) { + console.error("[admin] escrow activity update failed:", trackingErr) + } + const auditEntry = await milestoneStore.addAuditEntry({ + report_id: id, + validator_address: validatorAddress, + decision: "approved", + rejection_reason: null, + contract_tx_hash: contractResult.txHash, + }) + + try { + if (report.scholar_email) { + await emailService.sendNotification({ + to: report.scholar_email, + subject: "Milestone Approved ", + template: "milestone-approved-admin", + data: { + name: report.scholar_name || "Scholar", + courseTitle: report.course_title || `Course ${report.course_id}`, + milestoneTitle: + report.milestone_title || + `Milestone ${report.milestone_number ?? report.milestone_id}`, + milestoneNumber: String( + report.milestone_number ?? report.milestone_id, + ), + reward: String(report.lrn_reward ?? 0), + dashboardUrl: `${process.env.FRONTEND_URL || ""}/dashboard`, + unsubscribeUrl: "#", + }, + }) + } + } catch (emailErr) { + console.error("[admin] approval email failed (non-blocking):", emailErr) + } + + let certificate = null + try { + const mintResult = await credentialService.mintCertificateIfComplete( + report.scholar_address, + report.course_id, + ) + if (mintResult.minted) { + certificate = mintResult + console.info( + `[admin] ScholarNFT minted for ${report.scholar_address} — course ${report.course_id} (tx: ${mintResult.mintTxHash})`, + ) + } + } catch (mintErr) { + console.error("[admin] Certificate mint failed (non-blocking):", mintErr) + } + + res.status(200).json({ + data: { + reportId: id, + status: "approved", + contractTxHash: contractResult.txHash, + simulated: contractResult.simulated, + auditEntry, + certificate, + }, + }) + } catch (err) { + console.error("[admin] approveMilestone error:", err) + const msg = err instanceof Error ? err.message : String(err) + if (msg.includes("not configured")) { + res.status(503).json({ error: "Stellar credentials not configured" }) + return + } + res.status(500).json({ error: "Failed to approve milestone" }) + } +} + +export async function rejectMilestone( + req: AdminRequest, + res: Response, +): Promise { + const id = Number(req.params.id) + if (!Number.isInteger(id) || id <= 0) { + res.status(400).json({ error: "Invalid milestone report id" }) + return + } + + const { reason } = req.body as { reason: string } + const validatorAddress = req.adminAddress ?? "unknown" + + // Validate and sanitize rejection reason + if (!reason || typeof reason !== "string") { + res.status(400).json({ error: "Rejection reason is required" }) + return + } + if (reason.length > 1000) { + res + .status(400) + .json({ error: "Rejection reason must be 1000 characters or fewer" }) + return + } + const sanitizedReason = sanitizeHtml(reason, { + allowedTags: [], + allowedAttributes: {}, + }) + + try { + const report = await milestoneStore.getReportById(id) + if (!report) { + res.status(404).json({ error: "Milestone report not found" }) + return + } + if (report.status !== "pending") { + res.status(409).json({ error: `Report already ${report.status}` }) + return + } + if (!hasStellarMilestoneCredentials()) { + res.status(503).json({ error: "Stellar credentials not configured" }) + return + } + + // Emit on-chain rejection event + const contractResult = await stellarContractService.emitRejectionEvent( + report.scholar_address, + report.course_id, + report.milestone_id, + reason, + { requestId: req.requestId }, + ) + + // Persist decision + await milestoneStore.updateReportStatus(id, "rejected") + try { + await markEscrowActivity(report.scholar_address, report.course_id) + } catch (trackingErr) { + console.error("[admin] escrow activity update failed:", trackingErr) + } + const auditEntry = await milestoneStore.addAuditEntry({ + report_id: id, + validator_address: validatorAddress, + decision: "rejected", + rejection_reason: sanitizedReason, + contract_tx_hash: contractResult.txHash, + }) + + try { + if (report.scholar_email) { + await emailService.sendNotification({ + to: report.scholar_email, + subject: "Milestone Rejected", + template: "milestone-rejected-admin", + data: { + name: report.scholar_name || "Scholar", + courseTitle: report.course_title || `Course ${report.course_id}`, + milestoneTitle: + report.milestone_title || + `Milestone ${report.milestone_number ?? report.milestone_id}`, + milestoneNumber: String( + report.milestone_number ?? report.milestone_id, + ), + rejectionReason: sanitizedReason, + milestoneUrl: `${process.env.FRONTEND_URL || ""}/milestones`, + unsubscribeUrl: "#", + }, + }) + } + } catch (emailErr) { + console.error("[admin] rejection email failed (non-blocking):", emailErr) + } + + console.info( + `[admin] Scholar ${report.scholar_address} notified of rejection for milestone ${report.milestone_id} in course ${report.course_id}`, + ) + + res.status(200).json({ + data: { + reportId: id, + status: "rejected", + reason: sanitizedReason, + contractTxHash: contractResult.txHash, + simulated: contractResult.simulated, + auditEntry, + }, + }) + } catch (err) { + console.error("[admin] rejectMilestone error:", err) + const msg = err instanceof Error ? err.message : String(err) + if (msg.includes("not configured")) { + res.status(503).json({ error: "Stellar credentials not configured" }) + return + } + res.status(500).json({ error: "Failed to reject milestone" }) + } +} + +export async function batchApproveMilestones( + req: AdminRequest, + res: Response, +): Promise { + const { milestoneIds } = req.body as { milestoneIds: number[] } + if (!Array.isArray(milestoneIds) || milestoneIds.length === 0) { + res.status(400).json({ error: "milestoneIds must be a non-empty array" }) + return + } + + const validatorAddress = req.adminAddress ?? "unknown" + + // Pre-flight: fetch all reports and check existence + const reports = await Promise.all( + milestoneIds.map((id) => milestoneStore.getReportById(id)), + ) + const notFound = milestoneIds.filter((id, i) => !reports[i]) + if (notFound.length > 0) { + res.status(404).json({ + error: "One or more milestone reports were not found", + data: { + results: notFound.map((id) => ({ + reportId: id, + success: false, + status: "not_found", + })), + }, + }) + return + } + + if (!hasStellarMilestoneCredentials()) { + res.status(503).json({ error: "Stellar credentials not configured" }) + return + } + + const results = [] + let succeeded = 0 + let failed = 0 + + for (let i = 0; i < milestoneIds.length; i++) { + const id = milestoneIds[i] + const report = reports[i]! + try { + if (report.status !== "pending") { + results.push({ reportId: id, success: false, status: report.status }) + failed++ + continue + } + const contractResult = await stellarContractService.callVerifyMilestone( + report.scholar_address, + report.course_id, + report.milestone_id, + { requestId: req.requestId }, + ) + await milestoneStore.updateReportStatus(id, "approved") + await milestoneStore.addAuditEntry({ + report_id: id, + validator_address: validatorAddress, + decision: "approved", + rejection_reason: null, + contract_tx_hash: contractResult.txHash, + }) + results.push({ + reportId: id, + success: true, + status: "approved", + contractTxHash: contractResult.txHash, + }) + succeeded++ + } catch { + results.push({ reportId: id, success: false, status: "failed" }) + failed++ + } + } + + res.status(200).json({ + data: { + action: "approve", + totalRequested: milestoneIds.length, + processed: milestoneIds.length, + succeeded, + failed, + results, + }, + }) +} + +export async function batchRejectMilestones( + req: AdminRequest, + res: Response, +): Promise { + const { milestoneIds, reason = "Rejected from the admin panel" } = + req.body as { milestoneIds: number[]; reason?: string } + + if (!Array.isArray(milestoneIds) || milestoneIds.length === 0) { + res.status(400).json({ error: "milestoneIds must be a non-empty array" }) + return + } + + const validatorAddress = req.adminAddress ?? "unknown" + + const reports = await Promise.all( + milestoneIds.map((id) => milestoneStore.getReportById(id)), + ) + + // Check all are pending before processing any + const nonPending = reports + .map((r, i) => ({ report: r, id: milestoneIds[i] })) + .filter(({ report }) => report && report.status !== "pending") + + if (nonPending.length > 0) { + res.status(409).json({ + error: "All milestone reports must be pending before batch processing", + data: { + results: nonPending.map(({ report, id }) => ({ + reportId: id, + success: false, + status: report!.status, + })), + }, + }) + return + } + + if (!hasStellarMilestoneCredentials()) { + res.status(503).json({ error: "Stellar credentials not configured" }) + return + } + + const results = [] + let succeeded = 0 + let failed = 0 + + for (let i = 0; i < milestoneIds.length; i++) { + const id = milestoneIds[i] + const report = reports[i]! + try { + const contractResult = await stellarContractService.emitRejectionEvent( + report.scholar_address, + report.course_id, + report.milestone_id, + reason, + { requestId: req.requestId }, + ) + await milestoneStore.updateReportStatus(id, "rejected") + await milestoneStore.addAuditEntry({ + report_id: id, + validator_address: validatorAddress, + decision: "rejected", + rejection_reason: reason, + contract_tx_hash: contractResult.txHash, + }) + results.push({ + reportId: id, + success: true, + status: "rejected", + reason, + contractTxHash: contractResult.txHash, + }) + succeeded++ + } catch { + results.push({ reportId: id, success: false, status: "failed" }) + failed++ + } + } + + res.status(200).json({ + data: { + action: "reject", + totalRequested: milestoneIds.length, + processed: milestoneIds.length, + succeeded, + failed, + results, + }, + }) +} diff --git a/server/src/controllers/admin.controller.ts b/server/src/controllers/admin.controller.ts new file mode 100644 index 00000000..6925a887 --- /dev/null +++ b/server/src/controllers/admin.controller.ts @@ -0,0 +1,96 @@ +import { type Request, type Response } from "express" +import { pool } from "../db/index" + +const STELLAR_NETWORK = process.env.STELLAR_NETWORK ?? "testnet" +const STELLAR_SECRET_KEY = process.env.STELLAR_SECRET_KEY ?? "" +const LEARN_TOKEN_CONTRACT_ID = process.env.LEARN_TOKEN_CONTRACT_ID ?? "" +const SCHOLARSHIP_TREASURY_CONTRACT_ID = + process.env.SCHOLARSHIP_TREASURY_CONTRACT_ID ?? "" + +async function queryContractI128( + contractId: string, + method: string, +): Promise { + if (!contractId || !STELLAR_SECRET_KEY) return "0" + + try { + const { + Keypair, + Contract, + TransactionBuilder, + Networks, + BASE_FEE, + rpc, + scValToNative, + } = await import("@stellar/stellar-sdk") + + const server = new rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + + const keypair = Keypair.fromSecret(STELLAR_SECRET_KEY) + const account = await server.getAccount(keypair.publicKey()) + const contract = new Contract(contractId) + + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: + STELLAR_NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET, + }) + .addOperation(contract.call(method)) + .setTimeout(30) + .build() + + const simulation = await server.simulateTransaction(tx) + if (rpc.Api.isSimulationError(simulation) || !simulation.result?.retval) { + return "0" + } + + const value = scValToNative(simulation.result.retval) + if (typeof value === "bigint") return value.toString() + if (typeof value === "number") return Math.trunc(value).toString() + if (typeof value === "string") return value + return "0" + } catch (err) { + console.warn(`[admin] Failed to query contract method ${method}:`, err) + return "0" + } +} + +export async function getAdminStats( + _req: Request, + res: Response, +): Promise { + try { + const statsResult = await pool.query( + `SELECT + (SELECT COUNT(*)::int FROM milestone_reports WHERE status = 'pending') AS pending_milestones, + (SELECT COUNT(*)::int FROM milestone_audit_log WHERE decision = 'approved' AND decided_at::date = CURRENT_DATE) AS approved_milestones_today, + (SELECT COUNT(*)::int FROM milestone_audit_log WHERE decision = 'rejected' AND decided_at::date = CURRENT_DATE) AS rejected_milestones_today, + (SELECT COUNT(DISTINCT scholar_address)::int FROM milestone_reports) AS total_scholars, + (SELECT COUNT(*)::int FROM proposals WHERE status = 'pending') AS open_proposals`, + ) + + const row = statsResult.rows[0] ?? {} + + const [totalLrnMinted, treasuryBalanceUsdc] = await Promise.all([ + queryContractI128(LEARN_TOKEN_CONTRACT_ID, "total_supply"), + queryContractI128(SCHOLARSHIP_TREASURY_CONTRACT_ID, "treasury_balance"), + ]) + + res.status(200).json({ + pending_milestones: Number(row.pending_milestones ?? 0), + approved_milestones_today: Number(row.approved_milestones_today ?? 0), + rejected_milestones_today: Number(row.rejected_milestones_today ?? 0), + total_scholars: Number(row.total_scholars ?? 0), + total_lrn_minted: totalLrnMinted, + open_proposals: Number(row.open_proposals ?? 0), + treasury_balance_usdc: treasuryBalanceUsdc, + }) + } catch (err) { + console.error("[admin] getAdminStats error:", err) + res.status(500).json({ error: "Failed to fetch admin stats" }) + } +} diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts new file mode 100644 index 00000000..c02e405a --- /dev/null +++ b/server/src/controllers/auth.controller.ts @@ -0,0 +1,147 @@ +import { type Request, type Response } from "express" + +import { type AuthService } from "../services/auth.service" + +export function createAuthControllers(authService: AuthService) { + return { + async getChallenge(req: Request, res: Response): Promise { + const address = + typeof req.query.address === "string" ? req.query.address.trim() : "" + + if (!address) { + res.status(400).json({ error: "Missing query parameter: address" }) + return + } + + try { + const challenge = await authService.createChallenge(address) + res.status(200).json(challenge) + } catch (err) { + const message = err instanceof Error ? err.message : "Bad request" + res.status(400).json({ error: message }) + } + }, + + async postChallengeVerify(req: Request, res: Response): Promise { + const body = req.body as { signed_transaction?: unknown } + const signedTransaction = + typeof body.signed_transaction === "string" + ? body.signed_transaction.trim() + : "" + + if (!signedTransaction) { + res + .status(400) + .json({ error: "Missing required field: signed_transaction" }) + return + } + + try { + const token = + await authService.verifySignedTransaction(signedTransaction) + res.status(200).json({ + token, + tokenType: "Bearer", + expiresIn: "24h", + }) + } catch (err) { + const message = err instanceof Error ? err.message : "Unauthorized" + if (message.includes("Invalid") || message.includes("Missing")) { + res.status(400).json({ error: message }) + return + } + if (message.includes("expired")) { + res.status(401).json({ error: message }) + return + } + res.status(401).json({ error: message }) + } + }, + + async getNonce(req: Request, res: Response): Promise { + const address = + typeof req.query.address === "string" ? req.query.address.trim() : "" + + if (!address) { + res.status(400).json({ error: "Missing query parameter: address" }) + return + } + + try { + const { nonce } = await authService.getOrCreateNonce(address) + res.status(200).json({ nonce }) + } catch (err) { + const message = err instanceof Error ? err.message : "Bad request" + if (message === "Invalid Stellar public key") { + res.status(400).json({ error: message }) + return + } + res.status(400).json({ error: message }) + } + }, + + async postVerify(req: Request, res: Response): Promise { + const body = req.body as { address?: unknown; signature?: unknown } + const address = + typeof body.address === "string" ? body.address.trim() : "" + const signature = + typeof body.signature === "string" ? body.signature.trim() : "" + + if (!address || !signature) { + res + .status(400) + .json({ error: "Missing required fields: address, signature" }) + return + } + + try { + const token = await authService.verifyAndIssueToken(address, signature) + res.status(200).json({ + token, + tokenType: "Bearer", + expiresIn: "24h", + }) + } catch (err) { + const message = err instanceof Error ? err.message : "Unauthorized" + if ( + message === "Invalid Stellar public key" || + message === "Invalid signature encoding" + ) { + res.status(400).json({ error: message }) + return + } + if (message === "Invalid signature") { + res.status(401).json({ error: message }) + return + } + if (message.startsWith("Nonce expired")) { + res.status(401).json({ error: message }) + return + } + res.status(401).json({ error: message }) + } + }, + + async postLogout(req: Request, res: Response): Promise { + const authHeader = req.headers.authorization + if (!authHeader || !authHeader.startsWith("Bearer ")) { + res.status(401).json({ error: "Missing authorization header" }) + return + } + + const token = authHeader.split(" ")[1] + if (!token) { + res.status(401).json({ error: "Missing token" }) + return + } + + try { + await authService.logout(token) + res.status(200).json({ message: "Logged out successfully" }) + } catch (err) { + const message = err instanceof Error ? err.message : "Logout failed" + res.status(400).json({ error: message }) + } + }, + } +} diff --git a/server/src/controllers/community.controller.ts b/server/src/controllers/community.controller.ts new file mode 100644 index 00000000..85002a41 --- /dev/null +++ b/server/src/controllers/community.controller.ts @@ -0,0 +1,51 @@ +import { type Request, type Response } from "express" + +export type CommunityEvent = { + id: string + title: string + description: string + date: string + type: "hackathon" | "study_group" | "workshop" + link: string +} + +// In-memory storage for events (since DB is a stub) +const events: CommunityEvent[] = [ + { + id: "1", + title: "Stellar Soroban Hackathon", + description: "Build the future of decentralized finance on Stellar.", + date: "2026-05-15T10:00:00Z", + type: "hackathon", + link: "https://stellar.org/hackathon", + }, + { + id: "2", + title: "Rust for Soroban Workshop", + description: "Learn how to write secure smart contracts with Rust.", + date: "2026-05-20T14:00:00Z", + type: "workshop", + link: "https://learnvault.io/workshop/rust", + }, +] + +export const getEvents = (req: Request, res: Response) => { + res.json(events) +} + +export const createEvent = (req: Request, res: Response) => { + const { title, description, date, type, link } = req.body + if (!title || !description || !date || !type || !link) { + return res.status(400).json({ error: "Missing required fields" }) + } + const newEvent: CommunityEvent = { + id: (events.length + 1).toString(), + title, + description, + date, + type, + link, + } + events.push(newEvent) + res.status(201).json(newEvent) +} diff --git a/server/src/controllers/courses.controller.ts b/server/src/controllers/courses.controller.ts new file mode 100644 index 00000000..6db9db0f --- /dev/null +++ b/server/src/controllers/courses.controller.ts @@ -0,0 +1,518 @@ +import { type Request, type Response } from "express" +import sanitizeHtml from "sanitize-html" +import { pool } from "../db" + +type CourseRow = { + id: number + slug: string + title: string + description: string + cover_image_url: string | null + track: string + difficulty: "beginner" | "intermediate" | "advanced" + published_at: string | null + created_at: string + updated_at: string + students_count: number +} + +type LessonRow = { + id: number + course_id: number + title: string + content_markdown: string + order_index: number + estimated_minutes: number + is_milestone: boolean + created_at: string + updated_at: string + quiz: Array<{ + question: string + options: string[] + correctIndex: number + }> +} + +const toCourse = (row: CourseRow) => ({ + id: row.id, + slug: row.slug, + title: row.title, + description: row.description, + coverImage: row.cover_image_url, + track: row.track, + difficulty: row.difficulty, + published: Boolean(row.published_at), + createdAt: row.created_at, + updatedAt: row.updated_at, + studentsCount: Number(row.students_count ?? 0), +}) + +const toLesson = (row: LessonRow) => ({ + id: row.id, + courseId: row.course_id, + title: row.title, + content: row.content_markdown, + order: row.order_index, + estimatedMinutes: Number(row.estimated_minutes ?? 10), + isMilestone: row.is_milestone, + quiz: row.quiz ?? [], + createdAt: row.created_at, + updatedAt: row.updated_at, +}) + +const difficultyValues = new Set(["beginner", "intermediate", "advanced"]) + +export const getCourses = async ( + req: Request, + res: Response, +): Promise => { + try { + const track = + typeof req.query.track === "string" ? req.query.track.trim() : undefined + const search = + typeof req.query.search === "string" ? req.query.search.trim() : undefined + const includeUnpublished = + typeof req.query.includeUnpublished === "string" && + ["1", "true", "yes"].includes( + req.query.includeUnpublished.trim().toLowerCase(), + ) + const difficulty = + typeof req.query.difficulty === "string" + ? req.query.difficulty.trim().toLowerCase() + : undefined + + const pageParam = + typeof req.query.page === "string" + ? Number.parseInt(req.query.page, 10) + : 1 + + const limitParam = + typeof req.query.limit === "string" + ? Number.parseInt(req.query.limit, 10) + : 12 + + const offsetParam = + typeof req.query.offset === "string" + ? Number.parseInt(req.query.offset, 10) + : undefined + + const limit = + Number.isFinite(limitParam) && limitParam > 0 + ? Math.min(limitParam, 50) + : 12 + + let offset = 0 + let page = 1 + + if ( + offsetParam !== undefined && + Number.isFinite(offsetParam) && + offsetParam >= 0 + ) { + offset = offsetParam + page = Math.floor(offset / limit) + 1 + } else { + page = Number.isFinite(pageParam) && pageParam > 0 ? pageParam : 1 + offset = (page - 1) * limit + } + + const conditions: string[] = [] + const params: unknown[] = [] + + if (!includeUnpublished) { + params.push(true) + conditions.push("c.published_at IS NOT NULL") + } + + if (track) { + params.push(track) + conditions.push(`LOWER(c.track) = LOWER($${params.length})`) + } + + if (search) { + params.push(`%${search}%`) + conditions.push( + `(c.title ILIKE $${params.length} OR c.description ILIKE $${params.length})`, + ) + } + + if (difficulty) { + if (!difficultyValues.has(difficulty)) { + res.status(200).json({ + data: [], + page, + limit, + total: 0, + totalPages: 0, + }) + return + } + params.push(difficulty) + conditions.push(`c.difficulty = $${params.length}`) + } + + const whereClause = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "" + + const totalResult = (await pool.query( + `SELECT COUNT(*) AS count FROM courses c ${whereClause}`, + params, + )) as { rows: Array<{ count: string }> } + const total = Number.parseInt(totalResult.rows[0]?.count ?? "0", 10) + const totalPages = total === 0 ? 0 : Math.ceil(total / limit) + + params.push(limit) + params.push(offset) + const rowsResult = (await pool.query( + `SELECT + c.id, + c.slug, + c.title, + c.description, + c.cover_image_url, + c.track, + c.difficulty, + c.published_at, + c.created_at, + c.updated_at, + COUNT(DISTINCT e.learner_address)::int AS students_count + FROM courses c + LEFT JOIN enrollments e ON e.course_id = c.slug + ${whereClause} + GROUP BY c.id, c.slug, c.title, c.description, c.cover_image_url, c.track, c.difficulty, c.published_at, c.created_at, c.updated_at + ORDER BY c.created_at DESC + LIMIT $${params.length - 1} OFFSET $${params.length}`, + params, + )) as { rows: CourseRow[] } + + res.status(200).json({ + data: rowsResult.rows.map(toCourse), + page, + limit, + total, + totalPages, + }) + } catch { + res.status(500).json({ error: "Internal server error" }) + } +} + +export const getCourse = async (req: Request, res: Response): Promise => { + try { + const idOrSlug = req.params.idOrSlug + const isNumericId = /^\d+$/.test(idOrSlug) + + const query = isNumericId + ? `SELECT id, slug, title, description, cover_image_url, track, difficulty, published_at, created_at, updated_at + FROM courses + WHERE id = $1 AND published_at IS NOT NULL + LIMIT 1` + : `SELECT id, slug, title, description, cover_image_url, track, difficulty, published_at, created_at, updated_at + FROM courses + WHERE slug = $1 AND published_at IS NOT NULL + LIMIT 1` + + const courseResult = (await pool.query(query, [ + isNumericId ? Number.parseInt(idOrSlug, 10) : idOrSlug, + ])) as { rows: CourseRow[] } + + const course = courseResult.rows[0] + if (!course) { + res.status(404).json({ error: "Course not found" }) + return + } + + const lessonResult = (await pool.query( + `SELECT + l.id, + l.course_id, + l.title, + l.content_markdown, + l.order_index, + l.estimated_minutes, + BOOL_OR(m.id IS NOT NULL) AS is_milestone, + l.created_at, + l.updated_at, + COALESCE( + json_agg( + json_build_object( + 'question', qq.question_text, + 'options', qq.options, + 'correctIndex', qq.correct_index + ) + ORDER BY qq.id + ) FILTER (WHERE qq.id IS NOT NULL), + '[]'::json + ) AS quiz + FROM lessons l + LEFT JOIN milestones m ON m.lesson_id = l.id + LEFT JOIN quizzes q ON q.lesson_id = l.id + LEFT JOIN quiz_questions qq ON qq.quiz_id = q.id + WHERE l.course_id = $1 + GROUP BY l.id + ORDER BY l.order_index ASC`, + [course.id], + )) as { rows: LessonRow[] } + + res.status(200).json({ + ...toCourse(course), + lessons: lessonResult.rows.map(toLesson), + }) + } catch { + res.status(500).json({ error: "Internal server error" }) + } +} + +export const getCourseLessonById = async ( + req: Request, + res: Response, +): Promise => { + try { + const lessonId = Number.parseInt(req.params.id, 10) + if (!Number.isInteger(lessonId) || lessonId <= 0) { + res.status(404).json({ error: "Lesson not found" }) + return + } + + const idOrSlug = req.params.idOrSlug + const isNumericId = /^\d+$/.test(idOrSlug) + + const result = (await pool.query( + `SELECT + l.id, + l.course_id, + l.title, + l.content_markdown, + l.order_index, + l.estimated_minutes, + BOOL_OR(m.id IS NOT NULL) AS is_milestone, + l.created_at, + l.updated_at, + COALESCE( + json_agg( + json_build_object( + 'question', qq.question_text, + 'options', qq.options, + 'correctIndex', qq.correct_index + ) + ORDER BY qq.id + ) FILTER (WHERE qq.id IS NOT NULL), + '[]'::json + ) AS quiz + FROM lessons l + INNER JOIN courses c ON c.id = l.course_id + LEFT JOIN milestones m ON m.lesson_id = l.id + LEFT JOIN quizzes q ON q.lesson_id = l.id + LEFT JOIN quiz_questions qq ON qq.quiz_id = q.id + WHERE ${isNumericId ? "c.id" : "c.slug"} = $1 + AND c.published_at IS NOT NULL + AND l.id = $2 + GROUP BY l.id + LIMIT 1`, + [isNumericId ? Number.parseInt(idOrSlug, 10) : idOrSlug, lessonId], + )) as { rows: LessonRow[] } + + const lesson = result.rows[0] + if (!lesson) { + res.status(404).json({ error: "Lesson not found" }) + return + } + + res.status(200).json(toLesson(lesson)) + } catch { + res.status(500).json({ error: "Internal server error" }) + } +} + +export const createCourse = async ( + req: Request, + res: Response, +): Promise => { + try { + const body = req.body as { + title?: unknown + slug?: unknown + description?: unknown + coverImage?: unknown + track?: unknown + difficulty?: unknown + } + + for (const field of ["title", "slug", "track", "difficulty"] as const) { + const value = body[field] + if (typeof value !== "string" || value.trim().length === 0) { + res.status(400).json({ error: `${field} is required`, field }) + return + } + } + + // Validate and sanitize description + let description = "" + if (body.description) { + if (typeof body.description !== "string") { + res + .status(400) + .json({ error: "description must be a string", field: "description" }) + return + } + if (body.description.length > 2000) { + res.status(400).json({ + error: "description must be 2000 characters or fewer", + field: "description", + }) + return + } + description = sanitizeHtml(body.description, { + allowedTags: ["p", "br", "strong", "em", "ul", "ol", "li"], + allowedAttributes: {}, + }) + } + + // Sanitize title + const title = sanitizeHtml(String(body.title).trim(), { + allowedTags: [], + allowedAttributes: {}, + }) + + const difficulty = String(body.difficulty).toLowerCase() + if (!difficultyValues.has(difficulty)) { + res.status(400).json({ error: "Invalid difficulty", field: "difficulty" }) + return + } + + const insert = (await pool.query( + `INSERT INTO courses (title, slug, description, cover_image_url, track, difficulty, published_at) + VALUES ($1, $2, $3, $4, $5, $6, NULL) + RETURNING id, slug, title, description, cover_image_url, track, difficulty, published_at, created_at, updated_at`, + [ + title, + String(body.slug).trim(), + description, + typeof body.coverImage === "string" ? body.coverImage : null, + String(body.track).trim(), + difficulty, + ], + )) as { rows: CourseRow[] } + + res.status(201).json(toCourse(insert.rows[0])) + } catch (error) { + if (typeof error === "object" && error && "code" in error) { + const code = (error as { code?: string }).code + if (code === "23505") { + res.status(409).json({ error: "Slug already exists" }) + return + } + } + res.status(500).json({ error: "Internal server error" }) + } +} + +export const updateCourse = async ( + req: Request, + res: Response, +): Promise => { + try { + const id = Number.parseInt(req.params.id, 10) + if (!Number.isInteger(id) || id <= 0) { + res.status(404).json({ error: "Course not found" }) + return + } + + const existing = (await pool.query( + `SELECT id FROM courses WHERE id = $1 LIMIT 1`, + [id], + )) as { rowCount: number; rows: Array<{ id: number }> } + if (existing.rowCount === 0) { + res.status(404).json({ error: "Course not found" }) + return + } + + const body = req.body as Record + const values: unknown[] = [] + const setClauses: string[] = [] + + const addField = (column: string, value: unknown) => { + values.push(value) + setClauses.push(`${column} = $${values.length}`) + } + + if ("title" in body && typeof body.title === "string") { + const sanitizedTitle = sanitizeHtml(body.title.trim(), { + allowedTags: [], + allowedAttributes: {}, + }) + addField("title", sanitizedTitle) + } + if ("slug" in body && typeof body.slug === "string") { + addField("slug", body.slug.trim()) + } + if ("description" in body && typeof body.description === "string") { + if (body.description.length > 2000) { + res.status(400).json({ + error: "description must be 2000 characters or fewer", + field: "description", + }) + return + } + const sanitizedDescription = sanitizeHtml(body.description, { + allowedTags: ["p", "br", "strong", "em", "ul", "ol", "li"], + allowedAttributes: {}, + }) + addField("description", sanitizedDescription) + } + if ("coverImage" in body) { + if (typeof body.coverImage === "string") { + addField("cover_image_url", body.coverImage) + } else if (body.coverImage === null) { + addField("cover_image_url", null) + } + } + if ("track" in body && typeof body.track === "string") { + addField("track", body.track.trim()) + } + if ("difficulty" in body && typeof body.difficulty === "string") { + const difficulty = body.difficulty.toLowerCase() + if (!difficultyValues.has(difficulty)) { + res + .status(400) + .json({ error: "Invalid difficulty", field: "difficulty" }) + return + } + addField("difficulty", difficulty) + } + if ("published" in body && typeof body.published === "boolean") { + if (body.published) { + setClauses.push( + `published_at = COALESCE(published_at, CURRENT_TIMESTAMP)`, + ) + } else { + setClauses.push(`published_at = NULL`) + } + } + + if (setClauses.length === 0) { + res.status(400).json({ error: "No valid fields provided" }) + return + } + + values.push(id) + const result = (await pool.query( + `UPDATE courses + SET ${setClauses.join(", ")} + WHERE id = $${values.length} + RETURNING id, slug, title, description, cover_image_url, track, difficulty, published_at, created_at, updated_at`, + values, + )) as { rows: CourseRow[] } + + res.status(200).json(toCourse(result.rows[0])) + } catch (error) { + if (typeof error === "object" && error && "code" in error) { + const code = (error as { code?: string }).code + if (code === "23505") { + res.status(409).json({ error: "Slug already exists" }) + return + } + } + res.status(500).json({ error: "Internal server error" }) + } +} diff --git a/server/src/controllers/credentials.controller.ts b/server/src/controllers/credentials.controller.ts new file mode 100644 index 00000000..1dc9e351 --- /dev/null +++ b/server/src/controllers/credentials.controller.ts @@ -0,0 +1,255 @@ +import fs from "fs/promises" +import path from "path" +import { type Request, type Response } from "express" + +import { pool } from "../db/index" +import { pinJsonToIPFS, getGatewayUrl } from "../services/pinata.service" + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface CourseMetadata { + id: string + title: string + difficulty: string +} + +interface NFTAttribute { + trait_type: string + value: string +} + +interface NFTMetadata { + name: string + description: string + image: string + attributes: NFTAttribute[] +} + +interface CreateMetadataRequest { + course_id: string + learner_address: string + completed_at: string +} + +// --------------------------------------------------------------------------- +// Course Data +// --------------------------------------------------------------------------- + +let coursesCache: CourseMetadata[] | null = null + +async function loadCourses(): Promise { + if (coursesCache) return coursesCache + + const coursesPath = path.resolve( + __dirname, + "../../content/courses/index.json", + ) + const coursesData = await fs.readFile(coursesPath, "utf-8") + const courses = JSON.parse(coursesData) as Array<{ + id: string + title: string + difficulty: string + }> + + coursesCache = courses.map((c) => ({ + id: c.id, + title: c.title, + difficulty: c.difficulty, + })) + + return coursesCache +} + +// --------------------------------------------------------------------------- +// Image Mapping +// --------------------------------------------------------------------------- + +const COURSE_IMAGE_MAP: Record = { + "stellar-basics": "scholar-nft-stellar.png", + "soroban-fundamentals": "scholar-nft-soroban.png", + "soroban-contracts": "scholar-nft-soroban.png", + "defi-fundamentals": "scholar-nft-defi.png", +} + +const DEFAULT_IMAGE = "scholar-nft-base.png" + +const IMAGE_CID_MAP: Record = { + "scholar-nft-stellar.png": + process.env.BADGE_CID_STELLAR ?? + "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + "scholar-nft-soroban.png": + process.env.BADGE_CID_SOROBAN ?? + "bafybeihvvlkvjkbxy6qxzjzqxzqxzqxzqxzqxzqxzqxzqxzqxzqxzqxzqx", + "scholar-nft-defi.png": + process.env.BADGE_CID_DEFI ?? + "bafybeidefi123456789abcdefghijklmnopqrstuvwxyz1234567890abc", + "scholar-nft-base.png": + process.env.BADGE_CID_BASE ?? + "bafybeiabc123456789defghijklmnopqrstuvwxyz1234567890abcdef", +} + +function getImageCID(courseId: string): string { + const imageName = COURSE_IMAGE_MAP[courseId] || DEFAULT_IMAGE + return IMAGE_CID_MAP[imageName] || IMAGE_CID_MAP[DEFAULT_IMAGE] +} + +// --------------------------------------------------------------------------- +// Metadata Generation +// --------------------------------------------------------------------------- + +function generateMetadata( + course: CourseMetadata, + learnerAddress: string, + completedAt: string, +): NFTMetadata { + const imageCID = getImageCID(course.id) + + return { + name: `${course.title} — Course Completion`, + description: `Issued to learners who complete all milestones in ${course.title}`, + image: `ipfs://${imageCID}`, + attributes: [ + { + trait_type: "Course", + value: course.id, + }, + { + trait_type: "Course Title", + value: course.title, + }, + { + trait_type: "Completed At", + value: completedAt, + }, + { + trait_type: "Learner", + value: learnerAddress, + }, + { + trait_type: "Difficulty", + value: course.difficulty, + }, + ], + } +} + +// --------------------------------------------------------------------------- +// Controller +// --------------------------------------------------------------------------- + +/** + * POST /api/credentials/metadata + * + * Generate NFT metadata for a course completion credential and upload to IPFS. + * Returns the ipfs:// URI for use in scholar_nft.mint(). + */ +export async function createCredentialMetadata( + req: Request, + res: Response, +): Promise { + try { + const { course_id, learner_address, completed_at } = + req.body as CreateMetadataRequest + + // Load course data + const courses = await loadCourses() + const course = courses.find((c) => c.id === course_id) + + if (!course) { + res.status(404).json({ + error: "Course not found", + message: `No course found with id: ${course_id}`, + }) + return + } + + // Generate metadata + const metadata = generateMetadata(course, learner_address, completed_at) + + // Upload to IPFS via Pinata + const metadataName = `${course_id}-${learner_address}-${Date.now()}` + const cid = await pinJsonToIPFS( + metadata as unknown as Record, + metadataName, + ) + + // Build response + const metadataUri = `ipfs://${cid}` + const gatewayUrl = getGatewayUrl(cid) + + res.status(201).json({ + data: { + metadata_uri: metadataUri, + gateway_url: gatewayUrl, + metadata, + }, + }) + } catch (error) { + console.error("Error creating credential metadata:", error) + + if ( + error instanceof Error && + error.message.includes("Pinata is not configured") + ) { + res.status(503).json({ + error: "Service unavailable", + message: + "IPFS pinning service is not configured. Please contact the administrator.", + }) + return + } + + res.status(500).json({ + error: "Internal server error", + message: "Failed to create credential metadata", + }) + } +} + +type CredentialRow = { + token_id: string | number + course_id: string + metadata_uri: string | null + minted_at: Date + revoked: boolean +} + +export async function getCredentialsByAddress( + req: Request, + res: Response, +): Promise { + const { address } = req.params + + if (!address) { + res.status(400).json({ error: "Scholar address is required" }) + return + } + + try { + const result = await pool.query( + `SELECT token_id, course_id, metadata_uri, minted_at, revoked + FROM scholar_nfts + WHERE scholar_address = $1 + ORDER BY minted_at DESC`, + [address], + ) + + const data = result.rows.map((row: CredentialRow) => ({ + token_id: Number(row.token_id), + course_id: row.course_id, + metadata_uri: row.metadata_uri, + minted_at: row.minted_at.toISOString(), + revoked: row.revoked, + })) + + res.status(200).json({ data }) + } catch (error) { + console.error("Error fetching credentials by address:", error) + res.status(500).json({ + error: "Internal server error", + message: "Failed to fetch credentials", + }) + } +} diff --git a/server/src/controllers/enrollments.controller.ts b/server/src/controllers/enrollments.controller.ts new file mode 100644 index 00000000..964c5a44 --- /dev/null +++ b/server/src/controllers/enrollments.controller.ts @@ -0,0 +1,137 @@ +import { type Request, type Response } from "express" +import { pool } from "../db/index" +import { stellarContractService } from "../services/stellar-contract.service" + +const COURSE_MILESTONE_CONTRACT_ID = + process.env.COURSE_MILESTONE_CONTRACT_ID ?? "" + +/** + * Create a new enrollment for a learner in a course. + * Validates on-chain enrollment first. + */ +export const createEnrollment = async ( + req: Request, + res: Response, +): Promise => { + try { + const { learner_address, course_id, tx_hash } = req.body + + if (!learner_address || !course_id || !tx_hash) { + res.status(400).json({ + error: "learner_address, course_id, and tx_hash are required", + }) + return + } + + // Validate on-chain enrollment if contract ID is configured + // Only perform on-chain check if course_id is a numeric string + // (the contract uses u32 for course IDs, not string slugs) + if (COURSE_MILESTONE_CONTRACT_ID) { + // Try to parse course_id as a number + const courseIdNum = parseInt(course_id, 10) + + if (!isNaN(courseIdNum) && courseIdNum > 0) { + // It's a valid numeric course ID, check on-chain + const isEnrolledOnChain = await stellarContractService.isEnrolled( + learner_address, + courseIdNum, + { requestId: req.requestId }, + ) + + if (!isEnrolledOnChain) { + res.status(400).json({ + error: "Learner is not enrolled in this course on-chain", + }) + return + } + } else { + // course_id is a string slug (e.g., "stellar-basics") + // Skip on-chain validation - mapping from slug to contract ID + // would require additional database logic + console.warn( + `[enrollments] course_id "${course_id}" is not numeric, skipping on-chain validation`, + ) + } + } else { + // If no contract configured, allow enrollment (development mode) + console.warn( + "[enrollments] No COURSE_MILESTONE_CONTRACT_ID configured, skipping on-chain validation", + ) + } + + // Check if already enrolled in DB + const existingCheck = await pool.query( + "SELECT id FROM enrollments WHERE learner_address = $1 AND course_id = $2", + [learner_address, course_id], + ) + + if (existingCheck.rows.length > 0) { + res.status(409).json({ + error: "Already enrolled in this course", + }) + return + } + + // Insert enrollment record + const result = await pool.query( + `INSERT INTO enrollments (learner_address, course_id, tx_hash) + VALUES ($1, $2, $3) + RETURNING id, enrolled_at`, + [learner_address, course_id, tx_hash], + ) + + const enrollment = result.rows[0] + + res.status(201).json({ + enrollment_id: enrollment.id, + enrolled_at: enrollment.enrolled_at, + }) + } catch (error) { + console.error("[enrollments] Error creating enrollment:", error) + res.status(500).json({ + error: "Failed to create enrollment", + }) + } +} + +/** + * Get all enrollments for a learner. + * Query param: learner_address + */ +export const getEnrollments = async ( + req: Request, + res: Response, +): Promise => { + try { + const { learner_address } = req.query + + if (!learner_address || typeof learner_address !== "string") { + res.status(400).json({ + error: "learner_address query parameter is required", + }) + return + } + + const result = await pool.query( + `SELECT id, learner_address, course_id, tx_hash, enrolled_at + FROM enrollments + WHERE learner_address = $1 + ORDER BY enrolled_at DESC`, + [learner_address], + ) + + res.status(200).json({ + data: result.rows.map((row) => ({ + enrollment_id: row.id, + course_id: row.course_id, + tx_hash: row.tx_hash, + enrolled_at: row.enrolled_at, + })), + }) + } catch (error) { + console.error("[enrollments] Error fetching enrollments:", error) + res.status(500).json({ + error: "Failed to fetch enrollments", + }) + } +} diff --git a/server/src/controllers/events.controller.ts b/server/src/controllers/events.controller.ts new file mode 100644 index 00000000..be10517d --- /dev/null +++ b/server/src/controllers/events.controller.ts @@ -0,0 +1,105 @@ +import { type Request, type Response } from "express" +import { pool } from "../db/index" + +function parsePositiveInt(value: unknown, fallback: number): number { + if (typeof value !== "string") return fallback + const parsed = Number.parseInt(value, 10) + if (Number.isNaN(parsed) || parsed < 0) return fallback + return parsed +} + +function extractTxHash(data: unknown): string | null { + if (!data || typeof data !== "object") return null + + const queue: unknown[] = [data] + const txKeys = new Set([ + "txhash", + "tx_hash", + "transactionhash", + "transaction_hash", + ]) + + while (queue.length > 0) { + const current = queue.shift() + if (Array.isArray(current)) { + queue.push(...current) + continue + } + + if (!current || typeof current !== "object") { + continue + } + + for (const [rawKey, value] of Object.entries(current)) { + const key = rawKey.toLowerCase() + if (txKeys.has(key) && typeof value === "string" && value.length > 0) { + return value + } + if (value && typeof value === "object") { + queue.push(value) + } + } + } + + return null +} + +export const getEvents = async (req: Request, res: Response): Promise => { + const contractFilter = + typeof req.query.contract === "string" + ? req.query.contract.trim() + : undefined + const typeFilter = + typeof req.query.type === "string" ? req.query.type.trim() : undefined + const addressFilter = + typeof req.query.address === "string" ? req.query.address.trim() : undefined + + const limit = Math.max( + 1, + Math.min(parsePositiveInt(req.query.limit, 50), 100), + ) + const offset = Math.max(0, parsePositiveInt(req.query.offset, 0)) + + let query = ` + SELECT id, contract, event_type, data, ledger_sequence, created_at + FROM events + ` + const conditions: string[] = [] + const params: unknown[] = [] + + if (contractFilter) { + params.push(contractFilter) + conditions.push(`contract = $${params.length}`) + } + + if (typeFilter) { + params.push(typeFilter) + conditions.push(`event_type = $${params.length}`) + } + + if (addressFilter) { + params.push(`%${addressFilter.toLowerCase()}%`) + conditions.push(`LOWER(data::text) LIKE $${params.length}`) + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}` + } + + const limitParam = params.length + 1 + const offsetParam = params.length + 2 + query += ` ORDER BY created_at DESC LIMIT $${limitParam} OFFSET $${offsetParam}` + params.push(limit, offset) + + try { + const result = await pool.query(query, params) + const data = result.rows.map((row) => ({ + ...row, + tx_hash: extractTxHash(row.data), + })) + res.status(200).json({ data }) + } catch (err) { + console.error("[events] Query failed:", err) + res.status(500).json({ error: "Failed to fetch events" }) + } +} diff --git a/server/src/controllers/flag-content.controller.ts b/server/src/controllers/flag-content.controller.ts new file mode 100644 index 00000000..a40e0e06 --- /dev/null +++ b/server/src/controllers/flag-content.controller.ts @@ -0,0 +1,96 @@ +import { type Request, type Response } from "express" +import { flaggedContentStore } from "../db/flagged-content-store" +import { pool } from "../db/index" +import { createEmailService } from "../services/email.service" + +const emailService = createEmailService(process.env.EMAIL_API_KEY || "") + +interface FlagContentRequestBody { + contentType: "comment" | "proposal" + contentId: number + reason: string +} + +export async function flagContent(req: Request, res: Response): Promise { + const body = req.body as FlagContentRequestBody + const { contentType, contentId, reason } = body + + if (!contentType || !contentId || !reason) { + res.status(400).json({ error: "Missing required fields" }) + return + } + + if (!["comment", "proposal"].includes(contentType)) { + res.status(400).json({ error: "Invalid content type" }) + return + } + + if (reason.length < 10) { + res.status(400).json({ error: "Reason must be at least 10 characters" }) + return + } + + const reporterAddress = (req as any).user?.address + + if (!reporterAddress) { + res.status(401).json({ error: "Authentication required" }) + return + } + + try { + // Verify content exists + let contentExists = false + if (contentType === "comment") { + const result = await pool.query( + `SELECT id FROM comments WHERE id = $1 AND deleted_at IS NULL`, + [contentId], + ) + contentExists = result.rows.length > 0 + } else if (contentType === "proposal") { + const result = await pool.query( + `SELECT id FROM proposals WHERE id = $1`, + [contentId], + ) + contentExists = result.rows.length > 0 + } + + if (!contentExists) { + res.status(404).json({ error: "Content not found" }) + return + } + + // Create or update flag + const flag = await flaggedContentStore.createOrUpdateFlag( + contentType, + contentId, + reporterAddress, + reason, + ) + + // Check if content should be hidden (3+ flags) + const flags = await flaggedContentStore.getFlagsForContent( + contentType, + contentId, + ) + if (flags.length >= 3 && !flag.is_hidden) { + await flaggedContentStore.deleteContent(contentType, contentId) + } + + // Send email to admin + emailService + .sendAdminFlagNotification( + contentType, + contentId, + reason, + reporterAddress, + ) + .catch((err) => + console.error("[EmailService] Admin flag alert failed:", err), + ) + + res.status(201).json({ data: flag }) + } catch (err) { + console.error("[flagContent] error:", err) + res.status(500).json({ error: "Failed to flag content" }) + } +} diff --git a/server/src/controllers/forum.controller.ts b/server/src/controllers/forum.controller.ts new file mode 100644 index 00000000..93e15bab --- /dev/null +++ b/server/src/controllers/forum.controller.ts @@ -0,0 +1,260 @@ +import { type Request, type Response } from "express" +import { pool } from "../db" +import { createEmailService } from "../services/email.service" + +export const listForumThreads = async ( + req: Request, + res: Response, +): Promise => { + try { + const idOrSlug = req.params.idOrSlug + const isNumericId = /^\d+$/.test(idOrSlug) + + // First try to find the course + const courseQuery = isNumericId + ? `SELECT id FROM courses WHERE id = $1 AND published_at IS NOT NULL` + : `SELECT id, slug FROM courses WHERE slug = $1 AND published_at IS NOT NULL` + + const courseResult = await pool.query(courseQuery, [ + isNumericId ? Number.parseInt(idOrSlug, 10) : idOrSlug, + ]) + + if ((courseResult as any).rowCount === 0) { + res.status(404).json({ error: "Course not found" }) + return + } + + // List threads + const course_id = isNumericId ? idOrSlug : courseResult.rows[0].slug + + const threadsResult = await pool.query( + `SELECT t.id, t.course_id, t.author_address, t.title, t.content, t.created_at, t.updated_at, + (SELECT COUNT(*)::int FROM forum_replies r WHERE r.thread_id = t.id) as reply_count + FROM forum_threads t + WHERE t.course_id = $1 + ORDER BY t.created_at DESC`, + [course_id], + ) + + res.status(200).json({ data: threadsResult.rows }) + } catch (error) { + console.error("[forum] listForumThreads error:", error) + res.status(500).json({ error: "Internal server error" }) + } +} + +export const createForumThread = async ( + req: Request, + res: Response, +): Promise => { + try { + const idOrSlug = req.params.idOrSlug + const isNumericId = /^\d+$/.test(idOrSlug) + const authorAddress = + (req as any).walletAddress || (req as any).user?.address + + if (!authorAddress) { + res.status(401).json({ error: "Unauthorized" }) + return + } + + const courseResult = await pool.query( + isNumericId + ? `SELECT id, slug FROM courses WHERE id = $1 AND published_at IS NOT NULL` + : `SELECT id, slug FROM courses WHERE slug = $1 AND published_at IS NOT NULL`, + [isNumericId ? Number.parseInt(idOrSlug, 10) : idOrSlug], + ) + + if ((courseResult as any).rowCount === 0) { + res.status(404).json({ error: "Course not found" }) + return + } + + const course_id = courseResult.rows[0].slug + const { title, content } = req.body + + if (!title || typeof title !== "string" || title.trim().length === 0) { + res.status(400).json({ error: "title is required" }) + return + } + + if ( + !content || + typeof content !== "string" || + content.trim().length === 0 + ) { + res.status(400).json({ error: "content is required" }) + return + } + + const result = await pool.query( + `INSERT INTO forum_threads (course_id, author_address, title, content) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [course_id, authorAddress, title.trim(), content.trim()], + ) + + res.status(201).json(result.rows[0]) + } catch (error) { + console.error("[forum] createForumThread error:", error) + res.status(500).json({ error: "Internal server error" }) + } +} + +export const getForumThread = async ( + req: Request, + res: Response, +): Promise => { + try { + const threadId = Number.parseInt(req.params.threadId, 10) + if (!Number.isInteger(threadId) || threadId <= 0) { + res.status(400).json({ error: "Invalid thread ID" }) + return + } + + const threadResult = await pool.query( + `SELECT * FROM forum_threads WHERE id = $1`, + [threadId], + ) + + if ((threadResult as any).rowCount === 0) { + res.status(404).json({ error: "Thread not found" }) + return + } + + const repliesResult = await pool.query( + `SELECT * FROM forum_replies WHERE thread_id = $1 ORDER BY created_at ASC`, + [threadId], + ) + + res.status(200).json({ + ...threadResult.rows[0], + replies: repliesResult.rows, + }) + } catch (error) { + console.error("[forum] getForumThread error:", error) + res.status(500).json({ error: "Internal server error" }) + } +} + +export const replyToForumThread = async ( + req: Request, + res: Response, +): Promise => { + try { + const threadId = Number.parseInt(req.params.threadId, 10) + if (!Number.isInteger(threadId) || threadId <= 0) { + res.status(400).json({ error: "Invalid thread ID" }) + return + } + + const authorAddress = + (req as any).walletAddress || (req as any).user?.address + if (!authorAddress) { + res.status(401).json({ error: "Unauthorized" }) + return + } + + const { content } = req.body + + if ( + !content || + typeof content !== "string" || + content.trim().length === 0 + ) { + res.status(400).json({ error: "content is required" }) + return + } + + const threadResult = await pool.query( + `SELECT * FROM forum_threads WHERE id = $1`, + [threadId], + ) + + if ((threadResult as any).rowCount === 0) { + res.status(404).json({ error: "Thread not found" }) + return + } + + const thread = threadResult.rows[0] + + const result = await pool.query( + `INSERT INTO forum_replies (thread_id, author_address, content) + VALUES ($1, $2, $3) + RETURNING *`, + [threadId, authorAddress, content.trim()], + ) + + // Email notification using mock mechanism if email isn't directly known or mapped + // Real implementation would look up scholar_email by author_address but we don't have a reliable email mapping table currently + try { + // Wait, we can fetch email if it was stored, but since it's not uniformly stored, + // we will simulate the EmailService call to log it. + const emailService = createEmailService(process.env.RESEND_API_KEY || "") + console.log( + `[Forum] Sending reply notification to thread owner: ${thread.author_address}`, + ) + + // For now, let's use a dummy email based on wallet to simulate since we don't store actual emails universally + const targetEmail = `${thread.author_address}@example.com` // Mock email + + await emailService.sendNotification({ + to: targetEmail, + subject: "New Reply to your Thread", + template: "forum-reply", + data: { + name: thread.author_address.slice(0, 6) + "...", // Short wallet as name + threadTitle: thread.title, + replyPreview: + content.trim().slice(0, 100) + (content.length > 100 ? "..." : ""), + threadUrl: `${process.env.FRONTEND_URL || "http://localhost:3000"}/courses/${thread.course_id}?tab=forum&thread=${thread.id}`, + }, + }) + } catch (emailErr) { + console.error("[forum] email notification failed:", emailErr) + } + + res.status(201).json(result.rows[0]) + } catch (error) { + console.error("[forum] replyToForumThread error:", error) + res.status(500).json({ error: "Internal server error" }) + } +} + +export const deleteForumThread = async ( + req: Request, + res: Response, +): Promise => { + try { + const threadId = Number.parseInt(req.params.threadId, 10) + if (!Number.isInteger(threadId) || threadId <= 0) { + res.status(400).json({ error: "Invalid thread ID" }) + return + } + + await pool.query(`DELETE FROM forum_threads WHERE id = $1`, [threadId]) + res.status(200).json({ success: true }) + } catch (error) { + console.error("[forum] deleteForumThread error:", error) + res.status(500).json({ error: "Internal server error" }) + } +} + +export const deleteForumReply = async ( + req: Request, + res: Response, +): Promise => { + try { + const replyId = Number.parseInt(req.params.replyId, 10) + if (!Number.isInteger(replyId) || replyId <= 0) { + res.status(400).json({ error: "Invalid reply ID" }) + return + } + + await pool.query(`DELETE FROM forum_replies WHERE id = $1`, [replyId]) + res.status(200).json({ success: true }) + } catch (error) { + console.error("[forum] deleteForumReply error:", error) + res.status(500).json({ error: "Internal server error" }) + } +} diff --git a/server/src/controllers/governance.controller.ts b/server/src/controllers/governance.controller.ts new file mode 100644 index 00000000..34f1c1bf --- /dev/null +++ b/server/src/controllers/governance.controller.ts @@ -0,0 +1,591 @@ +import { type Request, type Response } from "express" +import sanitizeHtml from "sanitize-html" +import { z } from "zod" + +import { pool } from "../db/index" +import { trackEscrowTimeout } from "../services/escrow-timeout.service" +import { stellarContractService } from "../services/stellar-contract.service" + +type ProposalStatus = "pending" | "approved" | "rejected" +type ProposalPublicState = "open" | "closed" | "cancelled" | "executed" + +const stellarAddressSchema = z.string().min(56).max(56).startsWith("G") + +function parseStatus(value: unknown): ProposalStatus | undefined { + if (typeof value !== "string") return undefined + const normalized = value.trim().toLowerCase() + if ( + normalized === "pending" || + normalized === "approved" || + normalized === "rejected" + ) { + return normalized + } + return undefined +} + +function parsePositiveInt(value: unknown, fallback: number): number { + if (typeof value !== "string") return fallback + const parsed = Number.parseInt(value, 10) + if (Number.isNaN(parsed) || parsed < 1) return fallback + return parsed +} + +function parseProposalId(value: unknown): number | null { + if (typeof value !== "string") return null + const parsed = Number.parseInt(value, 10) + if (Number.isNaN(parsed) || parsed < 1) return null + return parsed +} + +function deriveProposalState(proposal: { + status: string + cancelled?: boolean | null + deadline?: Date | string | null +}): ProposalPublicState { + if (proposal.cancelled) return "cancelled" + if (proposal.status === "approved") return "executed" + if (proposal.status === "rejected") return "closed" + + if (proposal.deadline) { + const deadline = new Date(proposal.deadline) + if (!Number.isNaN(deadline.getTime()) && deadline.getTime() <= Date.now()) { + return "closed" + } + } + + return "open" +} + +function parseViewerAddress(value: unknown): string | null { + if (typeof value !== "string") return null + const trimmed = value.trim() + const validation = stellarAddressSchema.safeParse(trimmed) + return validation.success ? validation.data : null +} + +function buildProposalSelect(viewerParamIndex?: number) { + const viewerVoteSelect = viewerParamIndex + ? ", uv.support AS user_vote_support" + : ", NULL::boolean AS user_vote_support" + const viewerJoin = viewerParamIndex + ? ` LEFT JOIN votes uv + ON uv.proposal_id = p.id + AND uv.voter_address = $${viewerParamIndex}` + : "" + + return `SELECT + p.id, + p.author_address, + p.title, + p.description, + p.amount, + p.votes_for, + p.votes_against, + p.status, + p.deadline, + p.created_at${viewerVoteSelect} + FROM proposals p${viewerJoin}` +} + +export async function getGovernanceProposals( + req: Request, + res: Response, +): Promise { + const status = parseStatus(req.query.status) + const viewerAddress = parseViewerAddress(req.query.viewer_address) + const page = parsePositiveInt(req.query.page, 1) + const limit = Math.min(parsePositiveInt(req.query.limit, 20), 100) + const offset = (page - 1) * limit + + const conditions: string[] = [] + const values: unknown[] = [] + + if (status) { + conditions.push(`p.status = $${values.length + 1}`) + values.push(status) + } + + const whereClause = conditions.length + ? `WHERE ${conditions.join(" AND ")}` + : "" + + try { + const totalResult = await pool.query( + `SELECT COUNT(*)::int AS total FROM proposals p ${whereClause}`, + values, + ) + + const total = Number(totalResult.rows[0]?.total ?? 0) + + const proposalValues = viewerAddress + ? [...values, limit, offset, viewerAddress] + : [...values, limit, offset] + const proposalsResult = await pool.query( + `${buildProposalSelect(viewerAddress ? values.length + 3 : undefined)} + ${whereClause} + ORDER BY p.created_at DESC + LIMIT $${values.length + 1} + OFFSET $${values.length + 2}`, + proposalValues, + ) + + res.status(200).json({ + proposals: proposalsResult.rows, + total, + page, + totalPages: Math.ceil(total / limit), + }) + } catch { + res.status(500).json({ error: "Failed to fetch governance proposals" }) + } +} + +export async function getGovernanceProposalById( + req: Request, + res: Response, +): Promise { + const proposalId = Number.parseInt(req.params.id, 10) + const viewerAddress = parseViewerAddress(req.query.viewer_address) + const values: unknown[] = viewerAddress + ? [proposalId, viewerAddress] + : [proposalId] + + if (Number.isNaN(proposalId) || proposalId < 1) { + res.status(400).json({ error: "Invalid proposal id" }) + return + } + + try { + const result = await pool.query( + `${buildProposalSelect(viewerAddress ? 2 : undefined)} + WHERE p.id = $1 + LIMIT 1`, + values, + ) + + if (!result?.rows || result.rows.length === 0) { + res.status(404).json({ error: "Proposal not found" }) + return + } + + res.status(200).json(result.rows[0]) + } catch { + res.status(500).json({ error: "Failed to fetch governance proposal" }) + } +} + +const GOV_DECIMALS = 7 +const GOV_DIVISOR = 10n ** BigInt(GOV_DECIMALS) + +export async function getVotingPower( + req: Request, + res: Response, +): Promise { + const { address } = req.params + if (!address || address.length < 50) { + res.status(400).json({ error: "Invalid Stellar address" }) + return + } + + try { + const rawBalance = + await stellarContractService.getGovernanceTokenBalance(address) + const balanceBigInt = BigInt(rawBalance) + const whole = balanceBigInt / GOV_DIVISOR + const frac = balanceBigInt % GOV_DIVISOR + const formatted = `${whole}.${frac.toString().padStart(GOV_DECIMALS, "0").slice(0, 2)}` + + res.status(200).json({ + address, + gov_balance: rawBalance, + formatted, + can_vote: balanceBigInt > 0n, + }) + } catch (err) { + console.error("[governance] getVotingPower error:", err) + res.status(500).json({ error: "Failed to fetch voting power" }) + } +} + +const createProposalSchema = z.object({ + author_address: z.string().min(50).max(56), + title: z.string().min(5).max(200), + description: z.string().min(10).max(5000), + requested_amount: z.string().regex(/^\d+(\.\d+)?$/, "Must be a valid number"), + evidence_url: z.string().url().optional(), +}) + +const castVoteSchema = z.object({ + proposal_id: z + .number() + .int() + .positive("proposal_id must be a positive integer"), + voter_address: z + .string() + .min(56, "voter_address must be a valid Stellar address") + .max(56, "voter_address must be a valid Stellar address") + .startsWith("G", "voter_address must be a valid Stellar address"), + support: z.boolean(), + signature: z.string().optional(), +}) + +export async function createGovernanceProposal( + req: Request, + res: Response, +): Promise { + const validation = createProposalSchema.safeParse(req.body) + if (!validation.success) { + res.status(400).json({ + error: "Invalid proposal data", + details: validation.error.flatten().fieldErrors, + }) + return + } + + const { author_address, title, description, requested_amount, evidence_url } = + validation.data + + // Sanitize HTML content + const sanitizedTitle = sanitizeHtml(title, { + allowedTags: [], + allowedAttributes: {}, + }) + const sanitizedDescription = sanitizeHtml(description, { + allowedTags: ["p", "br", "strong", "em", "ul", "ol", "li"], + allowedAttributes: {}, + }) + + const programUrl = evidence_url ?? "https://learnvault.app/dao/proposals" + + try { + // Parse the requested amount + const amount = Number.parseFloat(requested_amount) + + // Prepare contract parameters for ScholarshipTreasury.submit_proposal() + const today = new Date() + const startDate = new Date(today) + startDate.setDate(startDate.getDate() + 7) // Start 1 week from now + + const milestone1 = new Date(startDate) + milestone1.setMonth(milestone1.getMonth() + 1) + + const milestone2 = new Date(startDate) + milestone2.setMonth(milestone2.getMonth() + 2) + + const milestone3 = new Date(startDate) + milestone3.setMonth(milestone3.getMonth() + 3) + + // Convert to atomic units (USDC has 7 decimals on Stellar) + const atomicAmount = Math.floor(amount * 10 ** 7) + + const params = { + applicant: author_address, + amount: atomicAmount, + programName: title, + programUrl, + programDescription: description, + startDate: startDate.toISOString().split("T")[0], + milestoneTitles: [ + "Phase 1: Initial Progress", + "Phase 2: Mid-term Completion", + "Phase 3: Final Delivery", + ], + milestoneDates: [ + milestone1.toISOString().split("T")[0], + milestone2.toISOString().split("T")[0], + milestone3.toISOString().split("T")[0], + ], + } + + // 1. Call the on-chain contract first + const contractResult = + await stellarContractService.submitScholarshipProposal(params, { + requestId: req.requestId, + }) + + // 2. Only write to DB if contract call succeeded + const dbResult = await pool.query( + `INSERT INTO proposals ( + author_address, + title, + description, + amount, + status, + deadline, + created_at + ) VALUES ($1, $2, $3, $4, 'pending', NOW() + INTERVAL '7 days', NOW()) + RETURNING id`, + [author_address, sanitizedTitle, sanitizedDescription, amount], + ) + + const proposal_id = dbResult.rows[0]?.id + if (proposal_id) { + try { + await trackEscrowTimeout({ + proposalId: proposal_id, + scholarAddress: author_address, + }) + } catch (trackingErr) { + console.error("[governance] escrow tracking failed:", trackingErr) + } + } + + res.status(201).json({ + proposal_id, + tx_hash: contractResult.txHash, + }) + } catch (err) { + console.error("[governance] Proposal creation failed:", err) + res.status(500).json({ + error: "Failed to create governance proposal", + message: err instanceof Error ? err.message : String(err), + }) + } +} + +export async function castVote(req: Request, res: Response): Promise { + const validation = castVoteSchema.safeParse(req.body) + if (!validation.success) { + res.status(400).json({ + error: "Invalid vote data", + details: validation.error.flatten().fieldErrors, + }) + return + } + + const { proposal_id, voter_address, support } = validation.data + + try { + // 1. Check if proposal exists + const proposalResult = await pool.query( + "SELECT id, status, deadline, cancelled FROM proposals WHERE id = $1", + [proposal_id], + ) + + if (!proposalResult?.rows || proposalResult.rows.length === 0) { + res.status(404).json({ error: "Proposal not found" }) + return + } + + if (proposalResult.rows[0].cancelled) { + res.status(400).json({ + error: "Voting is closed for this proposal", + }) + return + } + + // 2. Check if proposal is still pending + if (proposalResult.rows[0].status !== "pending") { + res.status(400).json({ + error: "Voting is closed for this proposal", + }) + return + } + + if ( + proposalResult.rows[0].deadline && + new Date(proposalResult.rows[0].deadline).getTime() <= Date.now() + ) { + res.status(400).json({ + error: "Voting is closed for this proposal", + }) + return + } + + // 3. Check if voter already voted + const existingVote = await pool.query( + "SELECT id FROM votes WHERE proposal_id = $1 AND voter_address = $2", + [proposal_id, voter_address], + ) + + if ((existingVote?.rows ?? []).length > 0) { + res.status(409).json({ error: "You have already voted on this proposal" }) + return + } + + // 4. Check voter's effective voting power (own balance + any delegated-to-them) + const rawBalance = + await stellarContractService.getGovernanceTokenBalance(voter_address) + const balanceBigInt = BigInt(rawBalance) + + if (balanceBigInt <= 0n) { + res.status(400).json({ + error: "You have no voting power", + details: "Voter has no GOV tokens", + }) + return + } + + // 5. Call the on-chain vote contract + const contractResult = await stellarContractService.castVote( + { + voter: voter_address, + proposalId: proposal_id, + support, + }, + { requestId: req.requestId }, + ) + + // 6. Write to DB after successful contract call + const votingPower = balanceBigInt + const dbResult = await pool.query( + `INSERT INTO votes (proposal_id, voter_address, support, voting_power, tx_hash) + VALUES ($1, $2, $3, $4, $5) + RETURNING id`, + [ + proposal_id, + voter_address, + support, + votingPower.toString(), + contractResult.txHash, + ], + ) + + // 7. Update proposal vote counts + const updateColumn = support ? "votes_for" : "votes_against" + await pool.query( + `UPDATE proposals SET ${updateColumn} = ${updateColumn} + $1 WHERE id = $2`, + [votingPower.toString(), proposal_id], + ) + + // 8. Fetch updated vote counts for response + const updatedProposal = await pool.query( + "SELECT votes_for, votes_against FROM proposals WHERE id = $1", + [proposal_id], + ) + + res.status(201).json({ + tx_hash: contractResult.txHash, + votes_for: updatedProposal.rows[0]?.votes_for ?? "0", + votes_against: updatedProposal.rows[0]?.votes_against ?? "0", + }) + } catch (err) { + console.error("[governance] Vote casting failed:", err) + res.status(500).json({ + error: "Failed to cast vote", + message: err instanceof Error ? err.message : String(err), + }) + } +} + +export async function getProposalStatus( + req: Request, + res: Response, +): Promise { + const proposalId = parseProposalId(req.params.id) + if (!proposalId) { + res.status(400).json({ error: "Invalid proposal id" }) + return + } + + try { + const result = await pool.query( + "SELECT id, status, deadline, cancelled FROM proposals WHERE id = $1", + [proposalId], + ) + + if (result.rows.length === 0) { + res.status(404).json({ error: "Proposal not found" }) + return + } + + const proposal = result.rows[0] + res.status(200).json({ + id: proposal.id, + state: deriveProposalState(proposal), + status: proposal.status, + cancelled: Boolean(proposal.cancelled), + deadline: proposal.deadline ?? null, + }) + } catch (err) { + console.error("[governance] Get proposal status failed:", err) + res.status(500).json({ error: "Failed to fetch proposal status" }) + } +} + +export async function cancelProposal( + req: Request, + res: Response, +): Promise { + const proposalId = parseProposalId(req.params.id) + if (!proposalId) { + res.status(400).json({ error: "Invalid proposal id" }) + return + } + + try { + const proposalResult = await pool.query( + "SELECT id, status, deadline, cancelled FROM proposals WHERE id = $1", + [proposalId], + ) + + if (proposalResult.rows.length === 0) { + res.status(404).json({ error: "Proposal not found" }) + return + } + + const proposal = proposalResult.rows[0] + + if (proposal.cancelled) { + res.status(409).json({ error: "Proposal is already cancelled" }) + return + } + + if (deriveProposalState(proposal) !== "open") { + res.status(409).json({ error: "Only open proposals can be cancelled" }) + return + } + + await stellarContractService.cancelProposal( + { proposalId }, + { requestId: req.requestId }, + ) + await pool.query("UPDATE proposals SET cancelled = TRUE WHERE id = $1", [ + proposalId, + ]) + + res.status(204).end() + } catch (err) { + console.error("[governance] Cancel proposal failed:", err) + res.status(500).json({ + error: "Failed to cancel proposal", + message: err instanceof Error ? err.message : String(err), + }) + } +} + +export async function getDelegation( + req: Request, + res: Response, +): Promise { + const { address } = req.params + if (!address || address.length < 50) { + res.status(400).json({ error: "Invalid Stellar address" }) + return + } + + try { + const [rawVotingPower, rawOwnBalance, delegatee] = await Promise.all([ + stellarContractService.getGovernanceTokenBalance(address), + stellarContractService.getGovernanceTokenBalance(address), + stellarContractService.getGovernanceDelegation(address), + ]) + + const ownBalance = BigInt(rawOwnBalance) + const votingPower = BigInt(rawVotingPower) + const delegatedToMe = delegatee ? 0n : votingPower - ownBalance + + res.status(200).json({ + address, + delegatee, + is_delegating: delegatee !== null, + own_balance: rawOwnBalance, + delegated_to_me: delegatedToMe > 0n ? delegatedToMe.toString() : "0", + voting_power: rawVotingPower, + }) + } catch (err) { + console.error("[governance] getDelegation error:", err) + res.status(500).json({ error: "Failed to fetch delegation state" }) + } +} diff --git a/server/src/controllers/health.controller.ts b/server/src/controllers/health.controller.ts new file mode 100644 index 00000000..4c4dc3eb --- /dev/null +++ b/server/src/controllers/health.controller.ts @@ -0,0 +1,8 @@ +import { type Request, type Response } from "express" + +export const getHealth = (_req: Request, res: Response): void => { + res.status(200).json({ + status: "ok", + timestamp: new Date().toISOString(), + }) +} diff --git a/server/src/controllers/leaderboard.controller.ts b/server/src/controllers/leaderboard.controller.ts new file mode 100644 index 00000000..e2ee8c7c --- /dev/null +++ b/server/src/controllers/leaderboard.controller.ts @@ -0,0 +1,67 @@ +import { type Request, type Response } from "express" + +/** + * Mock data for the leaderboard. + * In a real production app, this would be fetched from an indexer or + * by aggregating on-chain events. + */ +const MOCK_LEADERS = [ + { + rank: 1, + address: "GDOW...7890", + fullAddress: "GDOWK76XRPX7PFM7W4ZREXV6XOPD6VHY6G6G6G6G6G6G6G6G6G6G6G6G", + balance: "1250", + completedCourses: 5, + }, + { + rank: 2, + address: "GBAV...1234", + fullAddress: "GBAV76XRPX7PFM7W4ZREXV6XOPD6VHY6G6G6G6G6G6G6G6G6G6G6G6G", + balance: "980", + completedCourses: 4, + }, + { + rank: 3, + address: "GCTY...5678", + fullAddress: "GCTY76XRPX7PFM7W4ZREXV6XOPD6VHY6G6G6G6G6G6G6G6G6G6G6G6G", + balance: "750", + completedCourses: 3, + }, + { + rank: 4, + address: "GDQK...4321", + fullAddress: "GDQK76XRPX7PFM7W4ZREXV6XOPD6VHY6G6G6G6G6G6G6G6G6G6G6G6G", + balance: "420", + completedCourses: 2, + }, + { + rank: 5, + address: "GBNZ...8765", + fullAddress: "GBNZ76XRPX7PFM7W4ZREXV6XOPD6VHY6G6G6G6G6G6G6G6G6G6G6G6G", + balance: "150", + completedCourses: 1, + }, +] + +export const getLeaderboard = (req: Request, res: Response): void => { + const limit = Number.parseInt(String(req.query.limit ?? "10"), 10) + const offset = Number.parseInt(String(req.query.offset ?? "0"), 10) + + const normalizedLimit = Number.isNaN(limit) + ? 10 + : Math.max(1, Math.min(limit, 50)) + const normalizedOffset = Number.isNaN(offset) ? 0 : Math.max(0, offset) + + // Return a slice of mock data + const data = MOCK_LEADERS.slice( + normalizedOffset, + normalizedOffset + normalizedLimit, + ) + + res.status(200).json({ + data, + total: MOCK_LEADERS.length, + limit: normalizedLimit, + offset: normalizedOffset, + }) +} diff --git a/server/src/controllers/me.controller.ts b/server/src/controllers/me.controller.ts new file mode 100644 index 00000000..237df743 --- /dev/null +++ b/server/src/controllers/me.controller.ts @@ -0,0 +1,13 @@ +import { type Request, type Response } from "express" + +export function getMe(req: Request, res: Response): void { + const address = req.walletAddress + if (!address) { + res.status(401).json({ error: "Unauthorized" }) + return + } + + res.status(200).json({ + address, + }) +} diff --git a/server/src/controllers/milestone-resubmit.controller.ts b/server/src/controllers/milestone-resubmit.controller.ts new file mode 100644 index 00000000..116322af --- /dev/null +++ b/server/src/controllers/milestone-resubmit.controller.ts @@ -0,0 +1,55 @@ +import { type Request, type Response } from "express" +import { milestoneStore } from "../db/milestone-store" + +interface MilestoneResubmitRequestBody { + id: number + evidenceGithub?: string + evidenceIpfsCid?: string + evidenceDescription?: string +} + +export async function resubmitMilestoneReport( + req: Request, + res: Response, +): Promise { + const body = req.body as MilestoneResubmitRequestBody + + const { id, evidenceGithub, evidenceIpfsCid, evidenceDescription } = body + + if (!id) { + res.status(400).json({ error: "Milestone report ID is required" }) + return + } + + try { + // Get the existing report + const existing = await milestoneStore.getReportById(id) + if (!existing) { + res.status(404).json({ error: "Milestone report not found" }) + return + } + + if (existing.status !== "rejected") { + res + .status(400) + .json({ error: "Only rejected milestones can be resubmitted" }) + return + } + + // Update the report + const updated = await milestoneStore.createReport({ + scholar_address: existing.scholar_address, + course_id: existing.course_id, + milestone_id: existing.milestone_id, + evidence_github: evidenceGithub ?? existing.evidence_github, + evidence_ipfs_cid: evidenceIpfsCid ?? existing.evidence_ipfs_cid, + evidence_description: + evidenceDescription ?? existing.evidence_description, + }) + + res.status(200).json({ data: updated }) + } catch (err) { + console.error("[milestones] resubmitMilestoneReport error:", err) + res.status(500).json({ error: "Failed to resubmit milestone report" }) + } +} diff --git a/server/src/controllers/milestone-submit.controller.ts b/server/src/controllers/milestone-submit.controller.ts new file mode 100644 index 00000000..152ca57a --- /dev/null +++ b/server/src/controllers/milestone-submit.controller.ts @@ -0,0 +1,89 @@ +import { type Request, type Response } from "express" +import sanitizeHtml from "sanitize-html" +import { milestoneStore } from "../db/milestone-store" +import { createEmailService } from "../services/email.service" +import { markEscrowActivity } from "../services/escrow-timeout.service" + +interface MilestoneSubmitRequestBody { + scholarAddress?: string + learner_address?: string + courseId?: string + course_id?: string + milestoneId?: number + milestone_id?: number + evidenceGithub?: string + evidenceIpfsCid?: string + evidenceDescription?: string + evidence_url?: string +} +const emailService = createEmailService(process.env.EMAIL_API_KEY || "") + +export async function submitMilestoneReport( + req: Request, + res: Response, +): Promise { + const body = req.body as MilestoneSubmitRequestBody + + const scholarAddress = body.scholarAddress ?? body.learner_address + const courseId = body.courseId ?? body.course_id + const milestoneId = body.milestoneId ?? body.milestone_id + const evidenceGithub = body.evidenceGithub ?? body.evidence_url + const evidenceIpfsCid = body.evidenceIpfsCid + let evidenceDescription = body.evidenceDescription + + // Validate required fields + if (!scholarAddress || !courseId || milestoneId === undefined) { + res.status(400).json({ error: "Invalid request body" }) + return + } + + // Validate evidence description length + if (evidenceDescription && evidenceDescription.length > 2000) { + res + .status(400) + .json({ error: "Evidence description must be 2000 characters or fewer" }) + return + } + + // Sanitize evidence description + if (evidenceDescription) { + evidenceDescription = sanitizeHtml(evidenceDescription, { + allowedTags: ["p", "br", "strong", "em", "ul", "ol", "li"], + allowedAttributes: {}, + }) + } + + try { + const report = await milestoneStore.createReport({ + scholar_address: scholarAddress, + course_id: courseId, + milestone_id: milestoneId, + evidence_github: evidenceGithub ?? null, + evidence_ipfs_cid: evidenceIpfsCid ?? null, + evidence_description: evidenceDescription ?? null, + }) + try { + await markEscrowActivity(scholarAddress, courseId) + } catch (trackingErr) { + console.error("[milestones] escrow activity update failed:", trackingErr) + } + + emailService + .sendAdminMilestoneNotification( + scholarAddress, // Using address as name since name wasn't in the body + courseId, + milestoneId.toString(), + ) + .catch((err) => console.error("[EmailService] Admin alert failed:", err)) + res.status(201).json({ data: report }) + } catch (err) { + if (err instanceof Error && err.message === "DUPLICATE_REPORT") { + res.status(409).json({ + error: "A report for this milestone has already been submitted", + }) + return + } + console.error("[milestones] submitMilestoneReport error:", err) + res.status(500).json({ error: "Failed to submit milestone report" }) + } +} diff --git a/server/src/controllers/moderation.controller.ts b/server/src/controllers/moderation.controller.ts new file mode 100644 index 00000000..c7e36e19 --- /dev/null +++ b/server/src/controllers/moderation.controller.ts @@ -0,0 +1,139 @@ +import { type Request, type Response } from "express" +import { flaggedContentStore } from "../db/flagged-content-store" +import { pool } from "../db/index" + +interface ModerationActionRequest { + action: "delete" | "dismiss" | "warn" + adminNotes?: string +} + +export async function listFlaggedContent( + req: Request, + res: Response, +): Promise { + try { + const status = (req.query.status as string) || "pending" + const flags = await flaggedContentStore.getFlaggedContent( + status as "pending" | "reviewed" | "dismissed", + ) + + res.json({ data: flags }) + } catch (err) { + console.error("[listFlaggedContent] error:", err) + res.status(500).json({ error: "Failed to fetch flagged content" }) + } +} + +export async function getFlagDetails( + req: Request, + res: Response, +): Promise { + const { flagId } = req.params + + try { + const flag = await flaggedContentStore.getFlagById(Number(flagId)) + if (!flag) { + res.status(404).json({ error: "Flag not found" }) + return + } + + // Get the actual content + let content: any = null + if (flag.content_type === "comment") { + const result = await pool.query(`SELECT * FROM comments WHERE id = $1`, [ + flag.content_id, + ]) + content = result.rows[0] + } else if (flag.content_type === "proposal") { + const result = await pool.query(`SELECT * FROM proposals WHERE id = $1`, [ + flag.content_id, + ]) + content = result.rows[0] + } + + // Get audit log + const auditLog = await flaggedContentStore.getAuditForFlag(Number(flagId)) + + res.json({ data: { flag, content, auditLog } }) + } catch (err) { + console.error("[getFlagDetails] error:", err) + res.status(500).json({ error: "Failed to fetch flag details" }) + } +} + +export async function actionOnFlag(req: Request, res: Response): Promise { + const { flagId } = req.params + const body = req.body as ModerationActionRequest + const adminAddress = (req as any).user?.address + + const { action, adminNotes } = body + + if (!["delete", "dismiss", "warn"].includes(action)) { + res.status(400).json({ error: "Invalid action" }) + return + } + + try { + const flag = await flaggedContentStore.getFlagById(Number(flagId)) + if (!flag) { + res.status(404).json({ error: "Flag not found" }) + return + } + + // Perform the action + if (action === "delete") { + // Soft delete the content + if (flag.content_type === "comment") { + await pool.query( + `UPDATE comments SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1`, + [flag.content_id], + ) + } + // For proposals, we might want to archive them instead + } + + // Update flag status + const updatedFlag = await flaggedContentStore.updateFlagStatus( + Number(flagId), + "reviewed", + adminAddress, + action as "deleted" | "dismissed" | "warned", + adminNotes, + ) + + // Add audit entry + await flaggedContentStore.addAuditEntry( + Number(flagId), + action, + adminAddress, + adminNotes, + ) + + res.json({ data: updatedFlag }) + } catch (err) { + console.error("[actionOnFlag] error:", err) + res.status(500).json({ error: "Failed to take action on flag" }) + } +} + +export async function getAdminModerationStats( + req: Request, + res: Response, +): Promise { + try { + const pendingResult = await flaggedContentStore.getFlaggedContent("pending") + const reviewedResult = + await flaggedContentStore.getFlaggedContent("reviewed") + + const stats = { + pendingCount: pendingResult.length, + reviewedCount: reviewedResult.length, + topReports: pendingResult.slice(0, 5), + } + + res.json(stats) + } catch (err) { + console.error("[getAdminModerationStats] error:", err) + res.status(500).json({ error: "Failed to fetch moderation stats" }) + } +} diff --git a/server/src/controllers/scholars.controller.ts b/server/src/controllers/scholars.controller.ts new file mode 100644 index 00000000..f1182549 --- /dev/null +++ b/server/src/controllers/scholars.controller.ts @@ -0,0 +1,281 @@ +import { type Request, type Response } from "express" + +import { pool } from "../db/index" +import { milestoneStore } from "../db/milestone-store" +import { listEscrowTimeoutsForScholar } from "../services/escrow-timeout.service" +import { stellarContractService } from "../services/stellar-contract.service" + +type ApiMilestoneStatus = "pending" | "verified" | "rejected" +type InternalMilestoneStatus = "pending" | "approved" | "rejected" + +function mapInternalStatus( + status: InternalMilestoneStatus, +): ApiMilestoneStatus { + if (status === "approved") return "verified" + return status +} + +function mapQueryStatus( + status: string | undefined, +): InternalMilestoneStatus | undefined { + if (!status) return undefined + + if (status === "verified") return "approved" + if (status === "approved") return "approved" + if (status === "pending") return "pending" + if (status === "rejected") return "rejected" + + return undefined +} + +function toIsoDateTime(value: unknown): string | null { + if (!value) return null + if (value instanceof Date) return value.toISOString() + if (typeof value === "string") { + const asDate = new Date(value) + return Number.isNaN(asDate.getTime()) ? value : asDate.toISOString() + } + return String(value) +} + +export async function getScholarMilestones( + req: Request, + res: Response, +): Promise { + const address = req.params.address + const courseId = + typeof req.query.course_id === "string" ? req.query.course_id : undefined + const rawStatus = + typeof req.query.status === "string" ? req.query.status : undefined + const internalStatus = mapQueryStatus(rawStatus) + + if (rawStatus && !internalStatus) { + res.status(400).json({ error: "Validation failed" }) + return + } + + try { + const reports = await milestoneStore.getReportsForScholar(address, { + courseId, + status: internalStatus, + }) + const reportIds = reports.map((report) => report.id) + let lastDecisionByReportId: Record< + number, + { decided_at: unknown; contract_tx_hash: string | null } + > = {} + + if (reportIds.length > 0) { + const auditResult = await pool.query( + `SELECT DISTINCT ON (report_id) + report_id, + decided_at, + contract_tx_hash + FROM milestone_audit_log + WHERE report_id = ANY($1::int[]) + ORDER BY report_id, decided_at DESC`, + [reportIds], + ) + lastDecisionByReportId = Object.fromEntries( + (auditResult?.rows ?? []).map((row) => [ + Number(row.report_id), + { + decided_at: row.decided_at, + contract_tx_hash: + typeof row.contract_tx_hash === "string" + ? row.contract_tx_hash + : null, + }, + ]), + ) + } + + const milestones = reports.map((report) => { + const lastDecision = lastDecisionByReportId[report.id] + const evidenceUrl = + report.evidence_github ?? + (report.evidence_ipfs_cid ? `ipfs://${report.evidence_ipfs_cid}` : null) + + return { + id: String(report.id), + course_id: report.course_id, + milestone_id: report.milestone_id, + status: mapInternalStatus(report.status), + evidence_url: evidenceUrl, + submitted_at: toIsoDateTime(report.submitted_at), + verified_at: lastDecision + ? toIsoDateTime(lastDecision.decided_at) + : null, + tx_hash: lastDecision?.contract_tx_hash ?? null, + } + }) + + res.status(200).json({ milestones }) + } catch (err) { + console.error("[scholars] getScholarMilestones error:", err) + res.status(500).json({ error: "Failed to fetch scholar milestones" }) + } +} + +function parsePositiveInt(value: unknown, fallback: number): number { + if (typeof value !== "string") return fallback + const parsed = Number.parseInt(value, 10) + if (Number.isNaN(parsed) || parsed < 1) return fallback + return parsed +} + +export async function getScholarsLeaderboard( + req: Request, + res: Response, +): Promise { + const page = parsePositiveInt(req.query.page, 1) + const limit = Math.min(parsePositiveInt(req.query.limit, 50), 100) + const search = + typeof req.query.search === "string" ? req.query.search.trim() : "" + const offset = (page - 1) * limit + + const whereClause = search ? "WHERE address ILIKE $1" : "" + const whereValues: unknown[] = search ? [`%${search}%`] : [] + + try { + const totalResult = await pool.query( + `SELECT COUNT(*)::int AS total FROM scholar_balances ${whereClause}`, + whereValues, + ) + const total = Number(totalResult.rows[0]?.total ?? 0) + + const rankingsValues = [...whereValues, limit, offset] + const rankingsResult = await pool.query( + `SELECT + ROW_NUMBER() OVER (ORDER BY lrn_balance DESC, address ASC) AS rank, + address, + lrn_balance, + courses_completed + FROM scholar_balances + ${whereClause} + ORDER BY lrn_balance DESC, address ASC + LIMIT $${whereValues.length + 1} + OFFSET $${whereValues.length + 2}`, + rankingsValues, + ) + + const currentAddress = req.walletAddress + let yourRank: number | null = null + + if (currentAddress) { + const rankResult = await pool.query( + `SELECT rank FROM ( + SELECT ROW_NUMBER() OVER (ORDER BY lrn_balance DESC, address ASC) AS rank, address + FROM scholar_balances + ) ranked + WHERE address = $1`, + [currentAddress], + ) + yourRank = rankResult.rows[0]?.rank ?? null + } + + res.status(200).json({ + rankings: rankingsResult.rows, + total, + your_rank: yourRank, + }) + } catch { + res.status(500).json({ error: "Failed to fetch scholars leaderboard" }) + } +} + +export async function getScholarProfile( + req: Request, + res: Response, +): Promise { + const { address } = req.params + + if (!address) { + res.status(400).json({ error: "Scholar address is required" }) + return + } + + try { + // 1. Fetch on-chain data + const lrn_balance = + await stellarContractService.getLearnTokenBalance(address) + const enrolled_courses = + await stellarContractService.getEnrolledCourses(address) + const credentials = + await stellarContractService.getScholarCredentials(address) + + // 2. Fetch database data + const milestoneStatsResult = await pool.query( + `SELECT + COUNT(*) FILTER (WHERE status = 'approved') AS completed, + COUNT(*) FILTER (WHERE status = 'pending') AS pending + FROM milestone_reports + WHERE scholar_address = $1`, + [address], + ) + const stats = milestoneStatsResult.rows[0] + + const joinedAtResult = await pool.query( + `SELECT MIN(enrolled_at) AS joined_at + FROM enrollments + WHERE learner_address = $1`, + [address], + ) + // Fallback to current time if no enrollments yet + const joinedAt = + joinedAtResult.rows[0]?.joined_at ?? new Date().toISOString() + + res.status(200).json({ + address, + lrn_balance, + enrolled_courses, + completed_milestones: Number(stats?.completed ?? 0), + pending_milestones: Number(stats?.pending ?? 0), + credentials, + joined_at: joinedAt, + }) + } catch (error) { + console.error("[scholars] Error fetching scholar profile:", error) + res.status(500).json({ error: "Failed to fetch scholar profile" }) + } +} + +export async function getScholarCredentials( + req: Request, + res: Response, +): Promise { + const { address } = req.params + + if (!address) { + res.status(400).json({ error: "Scholar address is required" }) + return + } + + try { + const credentials = + await stellarContractService.getScholarCredentials(address) + res.status(200).json({ credentials }) + } catch (error) { + console.error("[scholars] Error fetching scholar credentials:", error) + res.status(500).json({ error: "Failed to fetch scholar credentials" }) + } +} + +export async function getScholarEscrowTimeouts( + req: Request, + res: Response, +): Promise { + const { address } = req.params + if (!address) { + res.status(400).json({ error: "Scholar address is required" }) + return + } + + try { + const escrows = await listEscrowTimeoutsForScholar(address) + res.status(200).json({ escrows }) + } catch (error) { + console.error("[scholars] Error fetching escrow timeout status:", error) + res.status(500).json({ error: "Failed to fetch escrow timeout status" }) + } +} diff --git a/server/src/controllers/scholarships.controller.ts b/server/src/controllers/scholarships.controller.ts new file mode 100644 index 00000000..0da8665f --- /dev/null +++ b/server/src/controllers/scholarships.controller.ts @@ -0,0 +1,190 @@ +import { type Request, type Response } from "express" +import { z } from "zod" + +import { pool } from "../db/index" +import { trackEscrowTimeout } from "../services/escrow-timeout.service" +import { stellarContractService } from "../services/stellar-contract.service" + +const applySchema = z.object({ + applicant_address: z.string().min(50).max(56), + full_name: z.string().min(2), + course_id: z.string().min(2), + motivation: z.string().min(10), + evidence_url: z.string().url(), + amount: z.number().positive().optional(), +}) + +export async function applyForScholarship( + req: Request, + res: Response, +): Promise { + const validation = applySchema.safeParse(req.body) + if (!validation.success) { + res.status(400).json({ + error: "Invalid application data", + details: validation.error.flatten().fieldErrors, + }) + return + } + + const { + applicant_address, + full_name, + course_id, + motivation, + evidence_url, + amount, + } = validation.data + + try { + // 1. Prepare contract parameters + // Mapping simplified backend request to detailed on-chain proposal + const today = new Date() + const tomorrow = new Date(today) + tomorrow.setDate(tomorrow.getDate() + 1) + + const month1 = new Date(today) + month1.setMonth(month1.getMonth() + 1) + + const month2 = new Date(today) + month2.setMonth(month2.getMonth() + 2) + + const month3 = new Date(today) + month3.setMonth(month3.getMonth() + 3) + + const requestedAmount = amount || 1000 // Default 1000 USDC + const atomicAmount = requestedAmount * 10 ** 7 // USDC has 7 decimals on Stellar + + const params = { + applicant: applicant_address, + amount: atomicAmount, + programName: `${full_name} - ${course_id}`, + programUrl: evidence_url, + programDescription: motivation, + startDate: tomorrow.toISOString().split("T")[0], + milestoneTitles: [ + "Phase 1: Course Onboarding & Initial Progress", + "Phase 2: Core Curriculum Completion", + "Phase 3: Final Project Submission & Certification", + ], + milestoneDates: [ + month1.toISOString().split("T")[0], + month2.toISOString().split("T")[0], + month3.toISOString().split("T")[0], + ], + } + + // 2. Call the on-chain contract + const result = await stellarContractService.submitScholarshipProposal( + params, + { requestId: req.requestId }, + ) + + // 3. Store in the database + const dbResult = await pool.query( + `INSERT INTO proposals ( + author_address, + title, + description, + amount, + status, + created_at + ) VALUES ($1, $2, $3, $4, 'pending', NOW()) + RETURNING id`, + [ + applicant_address, + `${full_name} - ${course_id}`, + `Motivation: ${motivation}\n\nEvidence: ${evidence_url}`, + requestedAmount, + ], + ) + + const proposal_id = dbResult.rows[0]?.id + if (proposal_id) { + try { + await trackEscrowTimeout({ + proposalId: proposal_id, + scholarAddress: applicant_address, + courseId: course_id, + }) + } catch (trackingErr) { + console.error("[scholarships] escrow tracking failed:", trackingErr) + } + } + + res.status(201).json({ + proposal_id, + tx_hash: result.txHash, + simulated: result.simulated, + }) + } catch (err) { + console.error("[scholarships] Application failed:", err) + res.status(500).json({ + error: "Failed to submit scholarship application", + message: err instanceof Error ? err.message : String(err), + }) + } +} + +export async function contributeToScholarship( + req: Request, + res: Response, +): Promise { + const contributionSchema = z.object({ + proposal_id: z.number(), + donor_address: z.string().min(50).max(56), + amount: z.number().positive(), + tx_hash: z.string().min(64), + }) + + const validation = contributionSchema.safeParse(req.body) + if (!validation.success) { + res.status(400).json({ error: "Invalid contribution data" }) + return + } + + const { proposal_id, donor_address, amount, tx_hash } = validation.data + + try { + const client = await pool.connect() + try { + await client.query("BEGIN") + + // 1. Record the contribution + await client.query( + "INSERT INTO scholarship_contributions (proposal_id, donor_address, amount, tx_hash) VALUES (, , , )", + [proposal_id, donor_address, amount, tx_hash], + ) + + // 2. Update the proposal's current funding + const updateResult = await client.query( + "UPDATE proposals SET current_funding = current_funding + WHERE id = RETURNING current_funding, amount", + [amount, proposal_id], + ) + + const { current_funding, amount: target_amount } = updateResult.rows[0] + + // 3. Check if fully funded + if (parseFloat(current_funding) >= parseFloat(target_amount)) { + await client.query( + "UPDATE proposals SET status = 'funded' WHERE id = ", + [proposal_id], + ) + } + + await client.query("COMMIT") + res.status(200).json({ + message: "Contribution recorded successfully", + current_funding, + }) + } catch (err) { + await client.query("ROLLBACK") + throw err + } finally { + client.release() + } + } catch (err) { + console.error("[scholarships] Contribution failed:", err) + res.status(500).json({ error: "Internal server error" }) + } +} diff --git a/server/src/controllers/treasury.controller.ts b/server/src/controllers/treasury.controller.ts new file mode 100644 index 00000000..44e4a695 --- /dev/null +++ b/server/src/controllers/treasury.controller.ts @@ -0,0 +1,181 @@ +import { rpc } from "@stellar/stellar-sdk" +import { type Request, type Response } from "express" + +const STELLAR_NETWORK = process.env.STELLAR_NETWORK ?? "testnet" +const SCHOLARSHIP_TREASURY_CONTRACT_ID = + process.env.SCHOLARSHIP_TREASURY_CONTRACT_ID ?? "" + +function parsePositiveInt(value: unknown, fallback: number): number { + if (typeof value !== "string") return fallback + const parsed = Number.parseInt(value, 10) + if (Number.isNaN(parsed) || parsed < 0) return fallback + return parsed +} + +/** + * GET /api/treasury/stats + * Returns aggregated treasury statistics + */ +export const getTreasuryStats = async ( + _req: Request, + res: Response, +): Promise => { + if (!SCHOLARSHIP_TREASURY_CONTRACT_ID) { + res.status(503).json({ + error: "Treasury contract not configured", + }) + return + } + + try { + const server = new rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + + // Fetch events from the ScholarshipTreasury contract + const response = await server.getEvents({ + filters: [ + { type: "contract", contractIds: [SCHOLARSHIP_TREASURY_CONTRACT_ID] }, + ], + startLedger: Number(process.env.STARTING_LEDGER ?? "460000000"), + limit: 1000, + }) + + let totalDeposited = BigInt(0) + let totalDisbursed = BigInt(0) + const scholars = new Set() + const donors = new Set() + let activeProposals = 0 + + // Parse events to calculate stats + for (const event of response.events) { + const { scValToNative } = await import("@stellar/stellar-sdk") + const eventData = scValToNative(event.value) + + // Identify event type from topics + const topics = event.topic.map((t: any) => scValToNative(t)) + const eventType = topics[0] + + if (eventType === "deposit" || eventType === "Deposit") { + const amount = BigInt(eventData.amount || 0) + totalDeposited += amount + if (eventData.donor) donors.add(eventData.donor) + } else if (eventType === "disburse" || eventType === "Disburse") { + const amount = BigInt(eventData.amount || 0) + totalDisbursed += amount + if (eventData.scholar) scholars.add(eventData.scholar) + } else if (eventType === "proposal_submitted") { + activeProposals++ + } + } + + res.status(200).json({ + total_deposited_usdc: totalDeposited.toString(), + total_disbursed_usdc: totalDisbursed.toString(), + scholars_funded: scholars.size, + active_proposals: activeProposals, + donors_count: donors.size, + }) + } catch (err) { + console.error("[treasury] Failed to fetch stats:", err) + res.status(500).json({ + error: "Failed to fetch treasury statistics", + }) + } +} + +/** + * GET /api/treasury/activity + * Returns recent treasury activity (deposits and disbursements) + */ +export const getTreasuryActivity = async ( + req: Request, + res: Response, +): Promise => { + if (!SCHOLARSHIP_TREASURY_CONTRACT_ID) { + res.status(503).json({ + error: "Treasury contract not configured", + }) + return + } + + const limit = Math.max( + 1, + Math.min(parsePositiveInt(req.query.limit, 20), 100), + ) + const offset = Math.max(0, parsePositiveInt(req.query.offset, 0)) + + try { + const server = new rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + + // Fetch events from the ScholarshipTreasury contract + const response = await server.getEvents({ + filters: [ + { type: "contract", contractIds: [SCHOLARSHIP_TREASURY_CONTRACT_ID] }, + ], + startLedger: Number(process.env.STARTING_LEDGER ?? "460000000"), + limit: 1000, + }) + + const events: Array<{ + type: string + amount?: string + address?: string + scholar?: string + tx_hash: string + created_at: string + }> = [] + + // Parse and format events + for (const event of response.events) { + const { scValToNative } = await import("@stellar/stellar-sdk") + const eventData = scValToNative(event.value) + + // Identify event type from topics + const topics = event.topic.map((t: any) => scValToNative(t)) + const eventType = topics[0] + + if (eventType === "deposit" || eventType === "Deposit") { + events.push({ + type: "deposit", + amount: eventData.amount?.toString() || "0", + address: eventData.donor || "unknown", + tx_hash: event.txHash || "", + created_at: event.ledgerClosedAt || new Date().toISOString(), + }) + } else if (eventType === "disburse" || eventType === "Disburse") { + events.push({ + type: "disburse", + scholar: eventData.scholar || "unknown", + amount: eventData.amount?.toString() || "0", + tx_hash: event.txHash || "", + created_at: event.ledgerClosedAt || new Date().toISOString(), + }) + } + } + + // Sort by created_at descending (most recent first) + events.sort( + (a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), + ) + + // Apply pagination + const paginatedEvents = events.slice(offset, offset + limit) + + res.status(200).json({ + events: paginatedEvents, + }) + } catch (err) { + console.error("[treasury] Failed to fetch activity:", err) + res.status(500).json({ + error: "Failed to fetch treasury activity", + }) + } +} diff --git a/server/src/controllers/upload.controller.ts b/server/src/controllers/upload.controller.ts new file mode 100644 index 00000000..7ebe1b35 --- /dev/null +++ b/server/src/controllers/upload.controller.ts @@ -0,0 +1,103 @@ +import { type NextFunction, type Request, type Response } from "express" +import { z } from "zod" +import { AppError } from "../errors/app-error-handler" +import { + getGatewayUrl, + pinFileToIPFS, + pinJsonToIPFS, +} from "../services/pinata.service" + +// --------------------------------------------------------------------------- +// POST /api/upload +// --------------------------------------------------------------------------- + +/** + * Pin a file to IPFS via Pinata and return the CID + gateway URL. + * The file is expected as multipart/form-data under the field name "file". + */ +export async function uploadFile( + req: Request, + res: Response, + next: NextFunction, +): Promise { + try { + if (!req.file) { + throw new AppError("No file provided", 400, { + file: 'A file is required under the field name "file"', + }) + } + + const cid = await pinFileToIPFS(req.file.buffer, req.file.originalname) + const gatewayUrl = getGatewayUrl(cid) + + res.status(201).json({ cid, gatewayUrl }) + } catch (err) { + next(err) + } +} + +// --------------------------------------------------------------------------- +// POST /api/upload/nft-metadata +// --------------------------------------------------------------------------- + +const nftMetadataSchema = z.object({ + name: z.string().min(1, "name is required"), + description: z.string().min(1, "description is required"), + // CID of the image already pinned via POST /api/upload + image: z.string().min(1, "image CID is required"), + attributes: z + .array( + z.object({ + trait_type: z.string(), + value: z.union([z.string(), z.number()]), + }), + ) + .optional(), +}) + +/** + * Pin ScholarNFT JSON metadata to IPFS. + * The image must already be uploaded via POST /api/upload. + * Returns the metadata CID and an ipfs:// URI suitable for the NFT contract's + * tokenURI / baseURI. + */ +export async function pinNftMetadata( + req: Request, + res: Response, + next: NextFunction, +): Promise { + try { + const parsed = nftMetadataSchema.safeParse(req.body) + if (!parsed.success) { + throw new AppError( + "Invalid metadata", + 400, + parsed.error.flatten().fieldErrors, + ) + } + + const { name, description, image, attributes } = parsed.data + + // Normalise image to an ipfs:// URI so the metadata is self-contained + const imageUri = image.startsWith("ipfs://") ? image : `ipfs://${image}` + + const metadata: Record = { + name, + description, + image: imageUri, + ...(attributes ? { attributes } : {}), + } + + const cid = await pinJsonToIPFS(metadata, `${name}-metadata.json`) + const gatewayUrl = getGatewayUrl(cid) + + res.status(201).json({ + cid, + gatewayUrl, + // ipfs:// URI for use directly in the ScholarNFT contract + tokenUri: `ipfs://${cid}`, + }) + } catch (err) { + next(err) + } +} diff --git a/server/src/controllers/validator.controller.ts b/server/src/controllers/validator.controller.ts new file mode 100644 index 00000000..68eb88ac --- /dev/null +++ b/server/src/controllers/validator.controller.ts @@ -0,0 +1,85 @@ +import { type Request, type Response } from "express" +import { milestoneStore } from "../db/milestone-store" + +interface ValidationRequestBody { + report_id?: number + scholar_address?: string + course_id?: string + milestone_id?: number + evidence_url?: string + evidence_description?: string +} + +export const validateMilestone = async ( + req: Request, + res: Response, +): Promise => { + const body = req.body as ValidationRequestBody + + const errors: string[] = [] + + // 1. Check required fields + if (!body.report_id && !body.milestone_id) { + errors.push("Either report_id or milestone_id is required") + } + if (!body.scholar_address) { + errors.push("scholar_address is required") + } + if (!body.evidence_url && !body.evidence_description) { + errors.push( + "At least one evidence field is required (evidence_url or evidence_description)", + ) + } + + if (errors.length > 0) { + res.status(400).json({ + data: { + approved: false, + validator: "learnvault-validator", + reasons: errors, + }, + }) + return + } + + // 2. If report_id is provided, verify the report exists and is pending + if (body.report_id) { + try { + const report = await milestoneStore.getReportById(body.report_id) + if (!report) { + res.status(404).json({ + data: { + approved: false, + validator: "learnvault-validator", + reasons: [`Report ${body.report_id} not found`], + }, + }) + return + } + if (report.status !== "pending") { + res.status(409).json({ + data: { + approved: false, + validator: "learnvault-validator", + reasons: [`Report ${body.report_id} is already ${report.status}`], + }, + }) + return + } + } catch { + // Database unavailable — log and continue with field validation only + console.warn( + "[validator] Could not query milestone store, proceeding with field validation only", + ) + } + } + + // 3. All checks passed + res.status(200).json({ + data: { + approved: true, + validator: "learnvault-validator", + data: req.body, + }, + }) +} diff --git a/server/src/controllers/wiki.controller.ts b/server/src/controllers/wiki.controller.ts new file mode 100644 index 00000000..047c26d9 --- /dev/null +++ b/server/src/controllers/wiki.controller.ts @@ -0,0 +1,171 @@ +import { type Request, type Response } from "express" +import { pool } from "../db" + +type WikiPageRow = { + id: number + slug: string + title: string + content: string + category: string + is_published: boolean + created_at: string + updated_at: string +} + +const toWikiPage = (row: WikiPageRow) => ({ + id: row.id, + slug: row.slug, + title: row.title, + content: row.content, + category: row.category, + isPublished: row.is_published, + createdAt: row.created_at, + updatedAt: row.updated_at, +}) + +export const getWikiPages = async ( + req: Request, + res: Response, +): Promise => { + try { + const includeUnpublished = req.query.includeUnpublished === "true" + const category = + typeof req.query.category === "string" ? req.query.category : undefined + + const conditions: string[] = [] + const params: unknown[] = [] + + if (!includeUnpublished) { + conditions.push("is_published = TRUE") + } + + if (category) { + params.push(category) + conditions.push(`category = $${params.length}`) + } + + const whereClause = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "" + const result = await pool.query( + `SELECT * FROM wiki_pages ${whereClause} ORDER BY category, title ASC`, + params, + ) + + res.status(200).json(result.rows.map(toWikiPage)) + } catch (error) { + console.error("Error fetching wiki pages:", error) + res.status(500).json({ error: "Internal server error" }) + } +} + +export const getWikiPageBySlug = async ( + req: Request, + res: Response, +): Promise => { + try { + const { slug } = req.params + const result = await pool.query( + "SELECT * FROM wiki_pages WHERE slug = $1 LIMIT 1", + [slug], + ) + + if ((result as any).rowCount === 0) { + res.status(404).json({ error: "Wiki page not found" }) + return + } + + res.status(200).json(toWikiPage(result.rows[0])) + } catch (error) { + console.error("Error fetching wiki page:", error) + res.status(500).json({ error: "Internal server error" }) + } +} + +export const createWikiPage = async ( + req: Request, + res: Response, +): Promise => { + try { + const { title, slug, content, category, isPublished } = req.body + + if (!title || !slug || !content || !category) { + res.status(400).json({ error: "Missing required fields" }) + return + } + + const result = await pool.query( + `INSERT INTO wiki_pages (title, slug, content, category, is_published) + VALUES ($1, $2, $3, $4, $5) + RETURNING *`, + [title, slug, content, category, isPublished !== false], + ) + + res.status(201).json(toWikiPage(result.rows[0])) + } catch (error: any) { + if (error.code === "23505") { + res.status(409).json({ error: "Slug already exists" }) + return + } + console.error("Error creating wiki page:", error) + res.status(500).json({ error: "Internal server error" }) + } +} + +export const updateWikiPage = async ( + req: Request, + res: Response, +): Promise => { + try { + const { id } = req.params + const { title, slug, content, category, isPublished } = req.body + + const result = await pool.query( + `UPDATE wiki_pages + SET title = COALESCE($1, title), + slug = COALESCE($2, slug), + content = COALESCE($3, content), + category = COALESCE($4, category), + is_published = COALESCE($5, is_published), + updated_at = CURRENT_TIMESTAMP + WHERE id = $6 + RETURNING *`, + [title, slug, content, category, isPublished, id], + ) + + if ((result as any).rowCount === 0) { + res.status(404).json({ error: "Wiki page not found" }) + return + } + + res.status(200).json(toWikiPage(result.rows[0])) + } catch (error: any) { + if (error.code === "23505") { + res.status(409).json({ error: "Slug already exists" }) + return + } + console.error("Error updating wiki page:", error) + res.status(500).json({ error: "Internal server error" }) + } +} + +export const deleteWikiPage = async ( + req: Request, + res: Response, +): Promise => { + try { + const { id } = req.params + const result = await pool.query("DELETE FROM wiki_pages WHERE id = $1", [ + id, + ]) + + if ((result as any).rowCount === 0) { + res.status(404).json({ error: "Wiki page not found" }) + return + } + + res.status(204).send() + } catch (error) { + console.error("Error deleting wiki page:", error) + res.status(500).json({ error: "Internal server error" }) + } +} diff --git a/server/src/db/flagged-content-store.ts b/server/src/db/flagged-content-store.ts new file mode 100644 index 00000000..35178a4f --- /dev/null +++ b/server/src/db/flagged-content-store.ts @@ -0,0 +1,296 @@ +import { pool } from "./index" + +export interface FlaggedContent { + id: number + content_type: "comment" | "proposal" + content_id: number + reporter_address: string + reason: string + flag_count: number + status: "pending" | "reviewed" | "dismissed" + admin_action?: "deleted" | "dismissed" | "warned" + admin_address?: string + admin_notes?: string + is_hidden: boolean + created_at: string + reviewed_at?: string +} + +export interface FlagAuditEntry { + id: number + flagged_id: number + action: string + actor_address: string + notes?: string + created_at: string +} + +class InMemoryFlaggedContentStore { + private flags: FlaggedContent[] = [] + private auditLog: FlagAuditEntry[] = [] + private flagSeq = 1 + private auditSeq = 1 + + async createOrUpdateFlag( + contentType: "comment" | "proposal", + contentId: number, + reporterAddress: string, + reason: string, + ): Promise { + const existing = this.flags.find( + (f) => + f.content_type === contentType && + f.content_id === contentId && + f.reporter_address === reporterAddress, + ) + + if (existing) { + existing.flag_count += 1 + return existing + } + + const flag: FlaggedContent = { + id: this.flagSeq++, + content_type: contentType, + content_id: contentId, + reporter_address: reporterAddress, + reason, + flag_count: 1, + status: "pending", + is_hidden: false, + created_at: new Date().toISOString(), + } + + this.flags.push(flag) + return flag + } + + async getFlaggedContent( + status?: "pending" | "reviewed" | "dismissed", + ): Promise { + return this.flags + .filter((f) => (status ? f.status === status : true)) + .sort((a, b) => b.created_at.localeCompare(a.created_at)) + } + + async getFlagById(id: number): Promise { + return this.flags.find((f) => f.id === id) ?? null + } + + async getFlagsForContent( + contentType: "comment" | "proposal", + contentId: number, + ): Promise { + return this.flags.filter( + (f) => f.content_type === contentType && f.content_id === contentId, + ) + } + + async updateFlagStatus( + id: number, + status: "pending" | "reviewed" | "dismissed", + adminAddress?: string, + adminAction?: "deleted" | "dismissed" | "warned", + adminNotes?: string, + ): Promise { + const flag = this.flags.find((f) => f.id === id) + if (!flag) return null + + flag.status = status + flag.reviewed_at = new Date().toISOString() + if (adminAddress) flag.admin_address = adminAddress + if (adminAction) flag.admin_action = adminAction + if (adminNotes) flag.admin_notes = adminNotes + + return flag + } + + async deleteContent( + contentType: "comment" | "proposal", + contentId: number, + ): Promise { + // Mark all flags for this content as hidden + this.flags.forEach((f) => { + if (f.content_type === contentType && f.content_id === contentId) { + f.is_hidden = true + } + }) + } + + async addAuditEntry( + flaggedId: number, + action: string, + actorAddress: string, + notes?: string, + ): Promise { + const entry: FlagAuditEntry = { + id: this.auditSeq++, + flagged_id: flaggedId, + action, + actor_address: actorAddress, + notes, + created_at: new Date().toISOString(), + } + this.auditLog.push(entry) + return entry + } + + async getAuditForFlag(flaggedId: number): Promise { + return this.auditLog.filter((e) => e.flagged_id === flaggedId) + } +} + +const inMemoryStore = new InMemoryFlaggedContentStore() + +function isRealPool(): boolean { + return typeof (pool as any).totalCount !== "undefined" +} + +export const flaggedContentStore = { + async createOrUpdateFlag( + contentType: "comment" | "proposal", + contentId: number, + reporterAddress: string, + reason: string, + ): Promise { + if (!isRealPool()) { + return inMemoryStore.createOrUpdateFlag( + contentType, + contentId, + reporterAddress, + reason, + ) + } + + // Check for existing flag + const existingResult = await pool.query( + `SELECT * FROM flagged_content WHERE content_type = $1 AND content_id = $2 AND reporter_address = $3`, + [contentType, contentId, reporterAddress], + ) + + if (existingResult.rows.length > 0) { + const existing = existingResult.rows[0] + // Update flag count + const updateResult = await pool.query( + `UPDATE flagged_content SET flag_count = flag_count + 1 WHERE id = $1 RETURNING *`, + [existing.id], + ) + return updateResult.rows[0] + } + + // Create new flag + const result = await pool.query( + `INSERT INTO flagged_content (content_type, content_id, reporter_address, reason) + VALUES ($1, $2, $3, $4) RETURNING *`, + [contentType, contentId, reporterAddress, reason], + ) + return result.rows[0] + }, + + async getFlaggedContent( + status?: "pending" | "reviewed" | "dismissed", + ): Promise { + if (!isRealPool()) return inMemoryStore.getFlaggedContent(status) + + const result = await pool.query( + `SELECT * FROM flagged_content ${status ? "WHERE status = $1" : ""} ORDER BY flag_count DESC, created_at DESC`, + status ? [status] : [], + ) + return result.rows + }, + + async getFlagById(id: number): Promise { + if (!isRealPool()) return inMemoryStore.getFlagById(id) + + const result = await pool.query( + `SELECT * FROM flagged_content WHERE id = $1`, + [id], + ) + return result.rows[0] ?? null + }, + + async getFlagsForContent( + contentType: "comment" | "proposal", + contentId: number, + ): Promise { + if (!isRealPool()) + return inMemoryStore.getFlagsForContent(contentType, contentId) + + const result = await pool.query( + `SELECT * FROM flagged_content WHERE content_type = $1 AND content_id = $2 ORDER BY created_at DESC`, + [contentType, contentId], + ) + return result.rows + }, + + async updateFlagStatus( + id: number, + status: "pending" | "reviewed" | "dismissed", + adminAddress?: string, + adminAction?: "deleted" | "dismissed" | "warned", + adminNotes?: string, + ): Promise { + if (!isRealPool()) { + return inMemoryStore.updateFlagStatus( + id, + status, + adminAddress, + adminAction, + adminNotes, + ) + } + + const result = await pool.query( + `UPDATE flagged_content SET status = $1, reviewed_at = NOW(), admin_address = $2, admin_action = $3, admin_notes = $4 WHERE id = $5 RETURNING *`, + [ + status, + adminAddress ?? null, + adminAction ?? null, + adminNotes ?? null, + id, + ], + ) + return result.rows[0] ?? null + }, + + async deleteContent( + contentType: "comment" | "proposal", + contentId: number, + ): Promise { + if (!isRealPool()) { + return inMemoryStore.deleteContent(contentType, contentId) + } + + await pool.query( + `UPDATE flagged_content SET is_hidden = TRUE WHERE content_type = $1 AND content_id = $2`, + [contentType, contentId], + ) + }, + + async addAuditEntry( + flaggedId: number, + action: string, + actorAddress: string, + notes?: string, + ): Promise { + if (!isRealPool()) { + return inMemoryStore.addAuditEntry(flaggedId, action, actorAddress, notes) + } + + const result = await pool.query( + `INSERT INTO flag_audit_log (flagged_id, action, actor_address, notes) VALUES ($1, $2, $3, $4) RETURNING *`, + [flaggedId, action, actorAddress, notes ?? null], + ) + return result.rows[0] + }, + + async getAuditForFlag(flaggedId: number): Promise { + if (!isRealPool()) return inMemoryStore.getAuditForFlag(flaggedId) + + const result = await pool.query( + `SELECT * FROM flag_audit_log WHERE flagged_id = $1 ORDER BY created_at ASC`, + [flaggedId], + ) + return result.rows + }, +} diff --git a/server/src/db/index.ts b/server/src/db/index.ts new file mode 100644 index 00000000..aefa8ec9 --- /dev/null +++ b/server/src/db/index.ts @@ -0,0 +1,130 @@ +import { Pool } from "pg" + +class MockPool { + async connect() { + return { + query: async () => ({ rows: [] }), + release: () => {}, + } + } + async query(_text: string, _params?: any[]) { + return { rows: [] } + } +} + +let activePool: Pool | MockPool + +try { + activePool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: + process.env.NODE_ENV === "production" + ? { rejectUnauthorized: false } + : false, + }) +} catch { + console.warn("[db] Failed to create postgres pool, using mock") + activePool = new MockPool() +} + +export const pool = activePool + +/** + * Verifies the database connection on startup. + * Schema is managed exclusively via migrations (`npm run migrate`). + * No DDL is executed here. + */ +export const initDb = async () => { + try { + if (activePool instanceof Pool) { + const client = await activePool.connect() + await client.query("SELECT 1") + client.release() + console.log("[db] Postgres connection verified") + await logPgStatStatementsSnapshot() + } else { + console.log("[db] In-memory mock database initialized") + } + } catch (err) { + console.error("[db] Connection check failed, falling back to mock:", err) + activePool = new MockPool() + } +} + +export const db = { + query: (text: string, params?: any[]) => activePool.query(text, params), + connected: true, +} + +export async function getPgStatStatementsSnapshot(limit = 5): Promise<{ + enabled: boolean + rows: Array<{ + query: string + calls: number + total_exec_time_ms: number + mean_exec_time_ms: number + rows: number + }> +}> { + if (!(activePool instanceof Pool)) { + return { enabled: false, rows: [] } + } + + try { + const extensionCheck = await activePool.query( + `SELECT EXISTS ( + SELECT 1 + FROM pg_extension + WHERE extname = 'pg_stat_statements' + ) AS enabled`, + ) + const enabled = Boolean(extensionCheck.rows[0]?.enabled) + if (!enabled) return { enabled: false, rows: [] } + + const statsResult = await activePool.query( + `SELECT + LEFT(REGEXP_REPLACE(query, '\\s+', ' ', 'g'), 300) AS query, + calls::int AS calls, + total_exec_time::float8 AS total_exec_time_ms, + mean_exec_time::float8 AS mean_exec_time_ms, + rows::bigint AS rows + FROM pg_stat_statements + ORDER BY mean_exec_time DESC + LIMIT $1`, + [Math.max(1, Math.min(limit, 20))], + ) + + return { + enabled: true, + rows: statsResult.rows.map((row) => ({ + query: String(row.query), + calls: Number(row.calls ?? 0), + total_exec_time_ms: Number(row.total_exec_time_ms ?? 0), + mean_exec_time_ms: Number(row.mean_exec_time_ms ?? 0), + rows: Number(row.rows ?? 0), + })), + } + } catch { + return { enabled: false, rows: [] } + } +} + +async function logPgStatStatementsSnapshot(): Promise { + const snapshot = await getPgStatStatementsSnapshot(3) + if (!snapshot.enabled) { + console.log("[db] pg_stat_statements not enabled") + return + } + + if (snapshot.rows.length === 0) { + console.log("[db] pg_stat_statements enabled (no rows yet)") + return + } + + console.log("[db] Slow query snapshot (pg_stat_statements):") + for (const row of snapshot.rows) { + console.log( + ` mean=${row.mean_exec_time_ms.toFixed(2)}ms calls=${row.calls} query=${row.query}`, + ) + } +} diff --git a/server/src/db/migrations/001_milestone_reports.sql b/server/src/db/migrations/001_milestone_reports.sql new file mode 100644 index 00000000..c9a47ad4 --- /dev/null +++ b/server/src/db/migrations/001_milestone_reports.sql @@ -0,0 +1,24 @@ +-- Milestone reports submitted by scholars +CREATE TABLE IF NOT EXISTS milestone_reports ( + id SERIAL PRIMARY KEY, + scholar_address TEXT NOT NULL, + course_id TEXT NOT NULL, + milestone_id INTEGER NOT NULL, + evidence_github TEXT, + evidence_ipfs_cid TEXT, + evidence_description TEXT, + status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')), + submitted_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE(scholar_address, course_id, milestone_id) +); + +-- Audit log for every validator decision +CREATE TABLE IF NOT EXISTS milestone_audit_log ( + id SERIAL PRIMARY KEY, + report_id INTEGER NOT NULL REFERENCES milestone_reports(id) ON DELETE CASCADE, + validator_address TEXT NOT NULL, + decision TEXT NOT NULL CHECK (decision IN ('approved', 'rejected')), + rejection_reason TEXT, + contract_tx_hash TEXT, + decided_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); diff --git a/server/src/db/migrations/001_milestone_reports.undo.sql b/server/src/db/migrations/001_milestone_reports.undo.sql new file mode 100644 index 00000000..c1756df4 --- /dev/null +++ b/server/src/db/migrations/001_milestone_reports.undo.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS milestone_audit_log; +DROP TABLE IF EXISTS milestone_reports; diff --git a/server/src/db/migrations/002_ipfs_cids.sql b/server/src/db/migrations/002_ipfs_cids.sql new file mode 100644 index 00000000..19257b9e --- /dev/null +++ b/server/src/db/migrations/002_ipfs_cids.sql @@ -0,0 +1,44 @@ +-- IPFS upload registry +-- Tracks every file pinned to IPFS so admins can audit uploads and so the +-- frontend can list attachments for a given proposal or course. +CREATE TABLE IF NOT EXISTS ipfs_uploads ( + id SERIAL PRIMARY KEY, + uploader_address TEXT NOT NULL, + cid TEXT NOT NULL UNIQUE, + gateway_url TEXT NOT NULL, + original_filename TEXT NOT NULL, + mimetype TEXT NOT NULL, + -- Loose tag for the call-site: 'proposal_document' | 'course_cover' | + -- 'milestone_evidence' | 'nft_image' | 'nft_metadata' + context TEXT, + -- Optional foreign-key hints (nullable; not all uploads are tied to a row) + ref_id TEXT, -- e.g. proposal on-chain ID or course ID + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Course assets — stores the IPFS CID for each course's cover image so the +-- courses controller can serve it alongside the course metadata. +CREATE TABLE IF NOT EXISTS course_assets ( + course_id TEXT NOT NULL, + asset_type TEXT NOT NULL DEFAULT 'cover_image', + cid TEXT NOT NULL, + gateway_url TEXT NOT NULL, + uploaded_by TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (course_id, asset_type) +); + +-- Proposal documents — stores off-chain CIDs for supporting documents +-- attached to on-chain scholarship proposals. +CREATE TABLE IF NOT EXISTS proposal_documents ( + id SERIAL PRIMARY KEY, + proposal_id TEXT NOT NULL, -- on-chain proposal identifier + uploader_address TEXT NOT NULL, + cid TEXT NOT NULL, + gateway_url TEXT NOT NULL, + original_filename TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_proposal_documents_proposal_id + ON proposal_documents (proposal_id); diff --git a/server/src/db/migrations/002_ipfs_cids.undo.sql b/server/src/db/migrations/002_ipfs_cids.undo.sql new file mode 100644 index 00000000..6f3d8b24 --- /dev/null +++ b/server/src/db/migrations/002_ipfs_cids.undo.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS idx_proposal_documents_proposal_id; +DROP TABLE IF EXISTS proposal_documents; +DROP TABLE IF EXISTS course_assets; +DROP TABLE IF EXISTS ipfs_uploads; diff --git a/server/src/db/migrations/003_course_content_schema.sql b/server/src/db/migrations/003_course_content_schema.sql new file mode 100644 index 00000000..93ce2703 --- /dev/null +++ b/server/src/db/migrations/003_course_content_schema.sql @@ -0,0 +1,99 @@ +-- ============================================================ +-- Migration 003: Course content relational schema +-- ============================================================ + +-- Courses +CREATE TABLE IF NOT EXISTS courses ( + id SERIAL PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + title TEXT NOT NULL, + description TEXT NOT NULL, + difficulty TEXT NOT NULL CHECK (difficulty IN ('beginner', 'intermediate', 'advanced')), + track TEXT NOT NULL, + cover_image_url TEXT, + lrn_reward NUMERIC(18, 7) NOT NULL DEFAULT 0, + published_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Lessons +CREATE TABLE IF NOT EXISTS lessons ( + id SERIAL PRIMARY KEY, + course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE, + order_index INTEGER NOT NULL, + title TEXT NOT NULL, + content_markdown TEXT NOT NULL DEFAULT '', + estimated_minutes INTEGER NOT NULL DEFAULT 10, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (course_id, order_index) +); + +-- Milestones (on-chain linkage per lesson) +CREATE TABLE IF NOT EXISTS milestones ( + id SERIAL PRIMARY KEY, + course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE, + lesson_id INTEGER REFERENCES lessons(id) ON DELETE SET NULL, + on_chain_milestone_id INTEGER NOT NULL, + lrn_amount NUMERIC(18, 7) NOT NULL DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (course_id, on_chain_milestone_id) +); + +-- Quizzes (one per lesson) +CREATE TABLE IF NOT EXISTS quizzes ( + id SERIAL PRIMARY KEY, + lesson_id INTEGER NOT NULL UNIQUE REFERENCES lessons(id) ON DELETE CASCADE, + passing_score INTEGER NOT NULL DEFAULT 70, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Quiz questions +CREATE TABLE IF NOT EXISTS quiz_questions ( + id SERIAL PRIMARY KEY, + quiz_id INTEGER NOT NULL REFERENCES quizzes(id) ON DELETE CASCADE, + question_text TEXT NOT NULL, + options JSONB NOT NULL, + correct_index INTEGER NOT NULL, + explanation TEXT, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_lessons_course_id ON lessons (course_id); +CREATE INDEX IF NOT EXISTS idx_milestones_course_id ON milestones (course_id); +CREATE INDEX IF NOT EXISTS idx_milestones_lesson_id ON milestones (lesson_id); +CREATE INDEX IF NOT EXISTS idx_quiz_questions_quiz_id ON quiz_questions (quiz_id); + +-- updated_at trigger function (shared) +CREATE OR REPLACE FUNCTION set_updated_at() +RETURNS TRIGGER LANGUAGE plpgsql AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$; + +CREATE OR REPLACE TRIGGER trg_courses_updated_at + BEFORE UPDATE ON courses + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +CREATE OR REPLACE TRIGGER trg_lessons_updated_at + BEFORE UPDATE ON lessons + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +CREATE OR REPLACE TRIGGER trg_milestones_updated_at + BEFORE UPDATE ON milestones + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +CREATE OR REPLACE TRIGGER trg_quizzes_updated_at + BEFORE UPDATE ON quizzes + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +CREATE OR REPLACE TRIGGER trg_quiz_questions_updated_at + BEFORE UPDATE ON quiz_questions + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); diff --git a/server/src/db/migrations/003_course_content_schema.undo.sql b/server/src/db/migrations/003_course_content_schema.undo.sql new file mode 100644 index 00000000..e8b8c15b --- /dev/null +++ b/server/src/db/migrations/003_course_content_schema.undo.sql @@ -0,0 +1,15 @@ +DROP TRIGGER IF EXISTS trg_quiz_questions_updated_at ON quiz_questions; +DROP TRIGGER IF EXISTS trg_quizzes_updated_at ON quizzes; +DROP TRIGGER IF EXISTS trg_milestones_updated_at ON milestones; +DROP TRIGGER IF EXISTS trg_lessons_updated_at ON lessons; +DROP TRIGGER IF EXISTS trg_courses_updated_at ON courses; +DROP FUNCTION IF EXISTS set_updated_at(); +DROP INDEX IF EXISTS idx_quiz_questions_quiz_id; +DROP INDEX IF EXISTS idx_milestones_lesson_id; +DROP INDEX IF EXISTS idx_milestones_course_id; +DROP INDEX IF EXISTS idx_lessons_course_id; +DROP TABLE IF EXISTS quiz_questions; +DROP TABLE IF EXISTS quizzes; +DROP TABLE IF EXISTS milestones; +DROP TABLE IF EXISTS lessons; +DROP TABLE IF EXISTS courses; diff --git a/server/src/db/migrations/004_events.sql b/server/src/db/migrations/004_events.sql new file mode 100644 index 00000000..5de4072d --- /dev/null +++ b/server/src/db/migrations/004_events.sql @@ -0,0 +1,34 @@ +-- ============================================================ +-- Migration 004: On-chain events table for indexing contract events +-- ============================================================ + +-- Events table: stores historical Soroban contract events +CREATE TABLE IF NOT EXISTS events ( + id SERIAL PRIMARY KEY, + contract TEXT NOT NULL, + event_type TEXT NOT NULL, + data JSONB NOT NULL, + ledger_sequence BIGINT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(contract, ledger_sequence) +); + +-- Index for fast lookups by contract + event type + ledger (idempotency checks) +CREATE INDEX IF NOT EXISTS idx_events_contract_event_ledger + ON events (contract, event_type, ledger_sequence); + +-- GIN index for JSONB data queries (address in data.address) +CREATE INDEX IF NOT EXISTS idx_events_data_gin + ON events USING GIN (data); + +-- Composite index for API queries +CREATE INDEX IF NOT EXISTS idx_events_contract_type + ON events (contract, event_type); + +-- Index for recent events +CREATE INDEX IF NOT EXISTS idx_events_created_at + ON events (created_at DESC); + +COMMENT ON TABLE events IS 'Stores historical Soroban contract events for leaderboards/activity feeds'; +COMMENT ON COLUMN events.data IS 'Parsed event data as JSONB (address, amount, etc)'; + diff --git a/server/src/db/migrations/004_events.undo.sql b/server/src/db/migrations/004_events.undo.sql new file mode 100644 index 00000000..3937f9d2 --- /dev/null +++ b/server/src/db/migrations/004_events.undo.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS idx_events_created_at; +DROP INDEX IF EXISTS idx_events_contract_type; +DROP INDEX IF EXISTS idx_events_data_gin; +DROP INDEX IF EXISTS idx_events_contract_event_ledger; +DROP TABLE IF EXISTS events; diff --git a/server/src/db/migrations/005_governance_and_comments.sql b/server/src/db/migrations/005_governance_and_comments.sql new file mode 100644 index 00000000..67d4f653 --- /dev/null +++ b/server/src/db/migrations/005_governance_and_comments.sql @@ -0,0 +1,50 @@ +-- ============================================================ +-- Migration 005: Governance proposals, comments, and scholar balances +-- ============================================================ + +CREATE TABLE IF NOT EXISTS comments ( + id SERIAL PRIMARY KEY, + proposal_id TEXT NOT NULL, + author_address TEXT NOT NULL, + parent_id INTEGER REFERENCES comments(id) ON DELETE CASCADE, + content TEXT NOT NULL, + upvotes INTEGER DEFAULT 0, + downvotes INTEGER DEFAULT 0, + is_pinned BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE TABLE IF NOT EXISTS comment_votes ( + id SERIAL PRIMARY KEY, + comment_id INTEGER REFERENCES comments(id) ON DELETE CASCADE, + voter_address TEXT NOT NULL, + vote_type TEXT CHECK (vote_type IN ('upvote', 'downvote')), + UNIQUE(comment_id, voter_address) +); + +CREATE TABLE IF NOT EXISTS proposals ( + id SERIAL PRIMARY KEY, + author_address TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL, + amount NUMERIC(18, 7) NOT NULL DEFAULT 0, + votes_for BIGINT NOT NULL DEFAULT 0, + votes_against BIGINT NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')), + deadline TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_proposals_status_created_at + ON proposals (status, created_at DESC); + +CREATE TABLE IF NOT EXISTS scholar_balances ( + address TEXT PRIMARY KEY, + lrn_balance NUMERIC(30, 0) NOT NULL DEFAULT 0, + courses_completed INTEGER NOT NULL DEFAULT 0, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_scholar_balances_lrn_desc + ON scholar_balances (lrn_balance DESC, address ASC); diff --git a/server/src/db/migrations/005_governance_and_comments.undo.sql b/server/src/db/migrations/005_governance_and_comments.undo.sql new file mode 100644 index 00000000..dea79da3 --- /dev/null +++ b/server/src/db/migrations/005_governance_and_comments.undo.sql @@ -0,0 +1,6 @@ +DROP INDEX IF EXISTS idx_scholar_balances_lrn_desc; +DROP TABLE IF EXISTS scholar_balances; +DROP INDEX IF EXISTS idx_proposals_status_created_at; +DROP TABLE IF EXISTS proposals; +DROP TABLE IF EXISTS comment_votes; +DROP TABLE IF EXISTS comments; diff --git a/server/src/db/migrations/006_scholar_nfts.sql b/server/src/db/migrations/006_scholar_nfts.sql new file mode 100644 index 00000000..ef0fdcf5 --- /dev/null +++ b/server/src/db/migrations/006_scholar_nfts.sql @@ -0,0 +1,36 @@ +-- ============================================================ +-- Migration 005: ScholarNFT credentials table +-- ============================================================ + +-- ScholarNFT table: stores minted ScholarNFT credentials +CREATE TABLE IF NOT EXISTS scholar_nfts ( + token_id BIGINT PRIMARY KEY, + scholar_address TEXT NOT NULL, + course_id TEXT NOT NULL, + metadata_uri TEXT NOT NULL, + minted_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + revoked BOOLEAN NOT NULL DEFAULT FALSE, + revoked_reason TEXT, + revoked_at TIMESTAMP WITH TIME ZONE, + + FOREIGN KEY (course_id) REFERENCES courses(slug) ON DELETE CASCADE +); + +-- Index for fast lookups by scholar address +CREATE INDEX IF NOT EXISTS idx_scholar_nfts_scholar_address + ON scholar_nfts (scholar_address); + +-- Index for course lookups +CREATE INDEX IF NOT EXISTS idx_scholar_nfts_course_id + ON scholar_nfts (course_id); + +-- Index for revocation status +CREATE INDEX IF NOT EXISTS idx_scholar_nfts_revoked + ON scholar_nfts (revoked); + +COMMENT ON TABLE scholar_nfts IS 'Stores ScholarNFT credential metadata and ownership information'; +COMMENT ON COLUMN scholar_nfts.token_id IS 'Unique token ID from the ScholarNFT contract'; +COMMENT ON COLUMN scholar_nfts.scholar_address IS 'Stellar address of the scholar who owns this NFT'; +COMMENT ON COLUMN scholar_nfts.course_id IS 'Course slug this credential is for'; +COMMENT ON COLUMN scholar_nfts.metadata_uri IS 'IPFS URI containing the NFT metadata'; +COMMENT ON COLUMN scholar_nfts.revoked IS 'Whether this credential has been revoked'; \ No newline at end of file diff --git a/server/src/db/migrations/006_scholar_nfts.undo.sql b/server/src/db/migrations/006_scholar_nfts.undo.sql new file mode 100644 index 00000000..0c7449fe --- /dev/null +++ b/server/src/db/migrations/006_scholar_nfts.undo.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS idx_scholar_nfts_revoked; +DROP INDEX IF EXISTS idx_scholar_nfts_course_id; +DROP INDEX IF EXISTS idx_scholar_nfts_scholar_address; +DROP TABLE IF EXISTS scholar_nfts; diff --git a/server/src/db/migrations/007_votes_enrollments_platform_events.sql b/server/src/db/migrations/007_votes_enrollments_platform_events.sql new file mode 100644 index 00000000..eae9cb50 --- /dev/null +++ b/server/src/db/migrations/007_votes_enrollments_platform_events.sql @@ -0,0 +1,44 @@ +-- ============================================================ +-- Migration 007: Votes, enrollments, and platform events +-- ============================================================ + +-- Votes on governance proposals +CREATE TABLE IF NOT EXISTS votes ( + id SERIAL PRIMARY KEY, + proposal_id INTEGER NOT NULL REFERENCES proposals(id), + voter_address TEXT NOT NULL, + support BOOLEAN NOT NULL, + voting_power NUMERIC NOT NULL, + tx_hash TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE(proposal_id, voter_address) +); + +CREATE INDEX IF NOT EXISTS idx_votes_proposal_id ON votes (proposal_id); +CREATE INDEX IF NOT EXISTS idx_votes_voter_address ON votes (voter_address); + +-- Course enrollments +CREATE TABLE IF NOT EXISTS enrollments ( + id SERIAL PRIMARY KEY, + learner_address TEXT NOT NULL, + course_id TEXT NOT NULL, + tx_hash TEXT NOT NULL, + enrolled_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE(learner_address, course_id) +); + +CREATE INDEX IF NOT EXISTS idx_enrollments_learner_address ON enrollments (learner_address); +CREATE INDEX IF NOT EXISTS idx_enrollments_course_id ON enrollments (course_id); + +-- Platform-wide event log (activity feed, analytics) +CREATE TABLE IF NOT EXISTS platform_events ( + id SERIAL PRIMARY KEY, + event_type TEXT NOT NULL, + data JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_platform_events_type_created_at + ON platform_events (event_type, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_platform_events_created_at + ON platform_events (created_at DESC); diff --git a/server/src/db/migrations/007_votes_enrollments_platform_events.undo.sql b/server/src/db/migrations/007_votes_enrollments_platform_events.undo.sql new file mode 100644 index 00000000..8ffe34bf --- /dev/null +++ b/server/src/db/migrations/007_votes_enrollments_platform_events.undo.sql @@ -0,0 +1,9 @@ +DROP INDEX IF EXISTS idx_platform_events_created_at; +DROP INDEX IF EXISTS idx_platform_events_type_created_at; +DROP TABLE IF EXISTS platform_events; +DROP INDEX IF EXISTS idx_enrollments_course_id; +DROP INDEX IF EXISTS idx_enrollments_learner_address; +DROP TABLE IF EXISTS enrollments; +DROP INDEX IF EXISTS idx_votes_voter_address; +DROP INDEX IF EXISTS idx_votes_proposal_id; +DROP TABLE IF EXISTS votes; diff --git a/server/src/db/migrations/008_proposal_cancellation.sql b/server/src/db/migrations/008_proposal_cancellation.sql new file mode 100644 index 00000000..e50661c4 --- /dev/null +++ b/server/src/db/migrations/008_proposal_cancellation.sql @@ -0,0 +1,6 @@ +-- ============================================================ +-- Migration 008: Proposal cancellation support +-- ============================================================ + +ALTER TABLE proposals +ADD COLUMN IF NOT EXISTS cancelled BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/server/src/db/migrations/008_proposal_cancellation.undo.sql b/server/src/db/migrations/008_proposal_cancellation.undo.sql new file mode 100644 index 00000000..dd6a7b3b --- /dev/null +++ b/server/src/db/migrations/008_proposal_cancellation.undo.sql @@ -0,0 +1,6 @@ +-- ============================================================ +-- Undo Migration 008: Proposal cancellation support +-- ============================================================ + +ALTER TABLE proposals +DROP COLUMN IF EXISTS cancelled; diff --git a/server/src/db/migrations/009_delegation_events.sql b/server/src/db/migrations/009_delegation_events.sql new file mode 100644 index 00000000..7f403dad --- /dev/null +++ b/server/src/db/migrations/009_delegation_events.sql @@ -0,0 +1,15 @@ +-- Indexes on-chain DelegateChanged and DelegateRemoved events for the governance token. +-- delegatee IS NULL means the row records an undelegation (DelegateRemoved). + +CREATE TABLE IF NOT EXISTS delegation_events ( + id SERIAL PRIMARY KEY, + delegator TEXT NOT NULL, + delegatee TEXT, + tx_hash TEXT, + ledger_sequence BIGINT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_delegation_delegator ON delegation_events (delegator); +CREATE INDEX IF NOT EXISTS idx_delegation_delegatee ON delegation_events (delegatee) + WHERE delegatee IS NOT NULL; diff --git a/server/src/db/migrations/009_delegation_events.undo.sql b/server/src/db/migrations/009_delegation_events.undo.sql new file mode 100644 index 00000000..a16962c0 --- /dev/null +++ b/server/src/db/migrations/009_delegation_events.undo.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS delegation_events; diff --git a/server/src/db/migrations/009_escrow_timeouts.sql b/server/src/db/migrations/009_escrow_timeouts.sql new file mode 100644 index 00000000..9addedb3 --- /dev/null +++ b/server/src/db/migrations/009_escrow_timeouts.sql @@ -0,0 +1,40 @@ +-- ============================================================ +-- Migration 009: Escrow timeout tracking and reminders +-- ============================================================ + +CREATE TABLE IF NOT EXISTS escrow_timeouts ( + id SERIAL PRIMARY KEY, + proposal_id INTEGER NOT NULL UNIQUE REFERENCES proposals(id) ON DELETE CASCADE, + scholar_address TEXT NOT NULL, + scholar_email TEXT, + course_id TEXT, + inactivity_window_days INTEGER NOT NULL DEFAULT 30, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_activity_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'reclaimed')), + reminder_sent_at TIMESTAMP WITH TIME ZONE, + reclaimed_at TIMESTAMP WITH TIME ZONE, + last_check_at TIMESTAMP WITH TIME ZONE, + reclaim_tx_hash TEXT, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_escrow_timeouts_status_last_activity + ON escrow_timeouts (status, last_activity_at ASC); + +CREATE INDEX IF NOT EXISTS idx_escrow_timeouts_scholar_course + ON escrow_timeouts (scholar_address, course_id); + +CREATE OR REPLACE FUNCTION set_escrow_timeouts_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_escrow_timeouts_updated_at ON escrow_timeouts; +CREATE TRIGGER trg_escrow_timeouts_updated_at + BEFORE UPDATE ON escrow_timeouts + FOR EACH ROW + EXECUTE FUNCTION set_escrow_timeouts_updated_at(); diff --git a/server/src/db/migrations/009_escrow_timeouts.undo.sql b/server/src/db/migrations/009_escrow_timeouts.undo.sql new file mode 100644 index 00000000..87132d8d --- /dev/null +++ b/server/src/db/migrations/009_escrow_timeouts.undo.sql @@ -0,0 +1,5 @@ +DROP TRIGGER IF EXISTS trg_escrow_timeouts_updated_at ON escrow_timeouts; +DROP FUNCTION IF EXISTS set_escrow_timeouts_updated_at; +DROP INDEX IF EXISTS idx_escrow_timeouts_scholar_course; +DROP INDEX IF EXISTS idx_escrow_timeouts_status_last_activity; +DROP TABLE IF EXISTS escrow_timeouts; diff --git a/server/src/db/migrations/009_flagged_content.sql b/server/src/db/migrations/009_flagged_content.sql new file mode 100644 index 00000000..71de2d2a --- /dev/null +++ b/server/src/db/migrations/009_flagged_content.sql @@ -0,0 +1,38 @@ +-- ============================================================ +-- Migration 009: Content moderation and flagging system +-- ============================================================ + +CREATE TABLE IF NOT EXISTS flagged_content ( + id SERIAL PRIMARY KEY, + content_type TEXT NOT NULL CHECK (content_type IN ('comment', 'proposal')), + content_id INTEGER NOT NULL, + reporter_address TEXT NOT NULL, + reason TEXT NOT NULL, + flag_count INTEGER DEFAULT 1, + status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'reviewed', 'dismissed')), + admin_action TEXT CHECK (admin_action IN ('deleted', 'dismissed', 'warned')), + admin_address TEXT, + admin_notes TEXT, + is_hidden BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + reviewed_at TIMESTAMP WITH TIME ZONE, + UNIQUE(content_type, content_id, reporter_address) +); + +CREATE INDEX IF NOT EXISTS idx_flagged_content_status_created_at + ON flagged_content (status, created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_flagged_content_hidden + ON flagged_content (is_hidden) WHERE is_hidden = TRUE; + +CREATE TABLE IF NOT EXISTS flag_audit_log ( + id SERIAL PRIMARY KEY, + flagged_id INTEGER REFERENCES flagged_content(id) ON DELETE CASCADE, + action TEXT NOT NULL, + actor_address TEXT NOT NULL, + notes TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_flag_audit_log_flagged_id + ON flag_audit_log (flagged_id); diff --git a/server/src/db/migrations/009_flagged_content.undo.sql b/server/src/db/migrations/009_flagged_content.undo.sql new file mode 100644 index 00000000..f138e451 --- /dev/null +++ b/server/src/db/migrations/009_flagged_content.undo.sql @@ -0,0 +1,10 @@ +-- ============================================================ +-- Migration 009 Undo: Content moderation and flagging system +-- ============================================================ + +DROP INDEX IF EXISTS idx_flag_audit_log_flagged_id; +DROP TABLE IF EXISTS flag_audit_log; + +DROP INDEX IF EXISTS idx_flagged_content_hidden; +DROP INDEX IF EXISTS idx_flagged_content_status_created_at; +DROP TABLE IF EXISTS flagged_content; diff --git a/server/src/db/migrations/009_wiki_pages.sql b/server/src/db/migrations/009_wiki_pages.sql new file mode 100644 index 00000000..3eb43942 --- /dev/null +++ b/server/src/db/migrations/009_wiki_pages.sql @@ -0,0 +1,32 @@ +CREATE TABLE IF NOT EXISTS wiki_pages ( + id SERIAL PRIMARY KEY, + slug VARCHAR(255) UNIQUE NOT NULL, + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + category VARCHAR(100) NOT NULL, + is_published BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Seed initial wiki content +INSERT INTO wiki_pages (slug, title, content, category) VALUES +( + 'stellar-basics', + 'Stellar Basics', + '# Stellar Basics\n\nStellar is a decentralized, fast, and energy-efficient network for currencies and payments. It allows you to create, send, and trade digital representations of all forms of money.\n\n## Key Concepts\n\n### Accounts\nEvery account on Stellar is identified by a public key (starting with G) and controlled by a secret key (starting with S).\n\n### Assets\nStellar can handle any type of asset, from XLM (Lumens) to stablecoins like USDC.\n\n### Transactions\nTransactions are atomic and take 3-5 seconds to confirm. Fees are extremely low (0.00001 XLM default).', + 'Stellar' +), +( + 'soroban-intro', + 'Introduction to Soroban', + '# Introduction to Soroban\n\nSoroban is the smart contract platform for the Stellar network. It is designed to be batteries-included, high-performance, and developer-friendly.\n\n## Why Soroban?\n\n- **Rust-based**: Use the power and safety of Rust.\n- **WASM**: Contracts compile to WebAssembly.\n- **Batteries-included**: Built-in support for events, multi-auth, and more.\n- **Scalable**: Designed to handle high throughput with predictable costs.', + 'Soroban' +), +( + 'learnvault-how-to', + 'How to use LearnVault', + '# How to use LearnVault\n\nLearnVault is a decentralized learning platform where your progress is your proof of work.\n\n## Getting Started\n\n1. **Connect Wallet**: Use a Stellar wallet like Freighter.\n2. **Enroll in Courses**: Browse our catalog and start a track.\n3. **Complete Milestones**: Finish lessons and quizzes to earn LRN tokens.\n4. **Governance**: Use your LRN tokens to vote on DAO proposals.', + 'Platform' +) +ON CONFLICT (slug) DO NOTHING; diff --git a/server/src/db/migrations/009_wiki_pages.undo.sql b/server/src/db/migrations/009_wiki_pages.undo.sql new file mode 100644 index 00000000..dfdc0302 --- /dev/null +++ b/server/src/db/migrations/009_wiki_pages.undo.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS wiki_pages CASCADE; diff --git a/server/src/db/migrations/010_query_optimization_indexes.sql b/server/src/db/migrations/010_query_optimization_indexes.sql new file mode 100644 index 00000000..a175d224 --- /dev/null +++ b/server/src/db/migrations/010_query_optimization_indexes.sql @@ -0,0 +1,36 @@ +-- ============================================================ +-- Migration 010: Query optimization indexes +-- ============================================================ + +-- Milestone report listing and filtering +CREATE INDEX IF NOT EXISTS idx_milestone_reports_scholar_status_submitted + ON milestone_reports (scholar_address, status, submitted_at DESC); + +CREATE INDEX IF NOT EXISTS idx_milestone_reports_course_status_submitted + ON milestone_reports (course_id, status, submitted_at DESC); + +CREATE INDEX IF NOT EXISTS idx_milestone_reports_status_submitted + ON milestone_reports (status, submitted_at DESC); + +-- Fetch latest audit decision per report efficiently +CREATE INDEX IF NOT EXISTS idx_milestone_audit_report_decided_at + ON milestone_audit_log (report_id, decided_at DESC); + +-- Governance proposal listings and status checks +CREATE INDEX IF NOT EXISTS idx_proposals_created_at + ON proposals (created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_proposals_cancelled_status_deadline + ON proposals (cancelled, status, deadline); + +-- Comment feed loading by proposal +CREATE INDEX IF NOT EXISTS idx_comments_proposal_created_at + ON comments (proposal_id, created_at DESC); + +-- Event feed filters by contract and recency +CREATE INDEX IF NOT EXISTS idx_events_contract_created_at + ON events (contract, created_at DESC); + +-- Enrollments query by learner ordered by most recent +CREATE INDEX IF NOT EXISTS idx_enrollments_learner_enrolled_at + ON enrollments (learner_address, enrolled_at DESC); diff --git a/server/src/db/migrations/010_query_optimization_indexes.undo.sql b/server/src/db/migrations/010_query_optimization_indexes.undo.sql new file mode 100644 index 00000000..ff6909b5 --- /dev/null +++ b/server/src/db/migrations/010_query_optimization_indexes.undo.sql @@ -0,0 +1,9 @@ +DROP INDEX IF EXISTS idx_enrollments_learner_enrolled_at; +DROP INDEX IF EXISTS idx_events_contract_created_at; +DROP INDEX IF EXISTS idx_comments_proposal_created_at; +DROP INDEX IF EXISTS idx_proposals_cancelled_status_deadline; +DROP INDEX IF EXISTS idx_proposals_created_at; +DROP INDEX IF EXISTS idx_milestone_audit_report_decided_at; +DROP INDEX IF EXISTS idx_milestone_reports_status_submitted; +DROP INDEX IF EXISTS idx_milestone_reports_course_status_submitted; +DROP INDEX IF EXISTS idx_milestone_reports_scholar_status_submitted; diff --git a/server/src/db/migrations/011_forum.sql b/server/src/db/migrations/011_forum.sql new file mode 100644 index 00000000..33781663 --- /dev/null +++ b/server/src/db/migrations/011_forum.sql @@ -0,0 +1,34 @@ +-- ============================================================ +-- Migration 011: Forum Threads and Replies +-- ============================================================ + +CREATE TABLE IF NOT EXISTS forum_threads ( + id SERIAL PRIMARY KEY, + course_id TEXT NOT NULL, + author_address TEXT NOT NULL, + title TEXT NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_forum_threads_course_id ON forum_threads (course_id); + +CREATE TABLE IF NOT EXISTS forum_replies ( + id SERIAL PRIMARY KEY, + thread_id INTEGER NOT NULL REFERENCES forum_threads(id) ON DELETE CASCADE, + author_address TEXT NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_forum_replies_thread_id ON forum_replies (thread_id); + +CREATE OR REPLACE TRIGGER trg_forum_threads_updated_at + BEFORE UPDATE ON forum_threads + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +CREATE OR REPLACE TRIGGER trg_forum_replies_updated_at + BEFORE UPDATE ON forum_replies + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); diff --git a/server/src/db/migrations/011_forum.undo.sql b/server/src/db/migrations/011_forum.undo.sql new file mode 100644 index 00000000..e40c3554 --- /dev/null +++ b/server/src/db/migrations/011_forum.undo.sql @@ -0,0 +1,5 @@ +DROP TRIGGER IF EXISTS trg_forum_replies_updated_at ON forum_replies; +DROP TRIGGER IF EXISTS trg_forum_threads_updated_at ON forum_threads; + +DROP TABLE IF EXISTS forum_replies CASCADE; +DROP TABLE IF EXISTS forum_threads CASCADE; diff --git a/server/src/db/migrations/011_multi_donor_contributions.sql b/server/src/db/migrations/011_multi_donor_contributions.sql new file mode 100644 index 00000000..c4ed9281 --- /dev/null +++ b/server/src/db/migrations/011_multi_donor_contributions.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS scholarship_contributions ( + id SERIAL PRIMARY KEY, + proposal_id INTEGER REFERENCES proposals(id) ON DELETE CASCADE, + donor_address VARCHAR(56) NOT NULL, + amount NUMERIC(20, 7) NOT NULL, + tx_hash VARCHAR(64) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Add a column to proposals to track current funding if not exists +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='proposals' AND COLUMN_NAME='current_funding') THEN + ALTER TABLE proposals ADD COLUMN current_funding NUMERIC(20, 7) DEFAULT 0; + END IF; +END $$; diff --git a/server/src/db/migrations/011_multi_donor_contributions.undo.sql b/server/src/db/migrations/011_multi_donor_contributions.undo.sql new file mode 100644 index 00000000..6713d04f --- /dev/null +++ b/server/src/db/migrations/011_multi_donor_contributions.undo.sql @@ -0,0 +1,3 @@ +ALTER TABLE proposals DROP COLUMN IF EXISTS current_funding; + +DROP TABLE IF EXISTS scholarship_contributions CASCADE; diff --git a/server/src/db/milestone-store.ts b/server/src/db/milestone-store.ts new file mode 100644 index 00000000..6cffda9a --- /dev/null +++ b/server/src/db/milestone-store.ts @@ -0,0 +1,383 @@ +import { pool } from "./index" + +export interface MilestoneReport { + id: number + scholar_address: string + course_id: string + milestone_id: number + evidence_github?: string | null + evidence_ipfs_cid?: string | null + evidence_description?: string | null + status: "pending" | "approved" | "rejected" + submitted_at: string + resubmission_count: number + scholar_email?: string + scholar_name?: string + course_title?: string + milestone_title?: string + milestone_number?: number + lrn_reward?: number +} + +export interface MilestoneAuditEntry { + id: number + report_id: number + validator_address: string + decision: "approved" | "rejected" + rejection_reason?: string | null + contract_tx_hash?: string | null + decided_at: string +} + +export interface MilestoneReportFilters { + courseId?: string + status?: "pending" | "approved" | "rejected" +} + +export interface PaginatedMilestoneReports { + data: MilestoneReport[] + total: number +} + +// In-memory fallback store (used when Postgres is unavailable) +class InMemoryMilestoneStore { + private reports: MilestoneReport[] = [] + private auditLog: MilestoneAuditEntry[] = [] + private reportSeq = 1 + private auditSeq = 1 + + async getPendingReports(): Promise { + return this.reports.filter((r) => r.status === "pending") + } + + async listReports( + filters: MilestoneReportFilters = {}, + page: number = 1, + pageSize: number = 10, + ): Promise { + const { courseId, status } = filters + const filtered = this.reports + .filter((report) => (courseId ? report.course_id === courseId : true)) + .filter((report) => (status ? report.status === status : true)) + .sort((a, b) => b.submitted_at.localeCompare(a.submitted_at)) + + const offset = (page - 1) * pageSize + return { + data: filtered.slice(offset, offset + pageSize), + total: filtered.length, + } + } + + async getReportById(id: number): Promise { + return this.reports.find((r) => r.id === id) ?? null + } + + async getReportsForScholar( + scholarAddress: string, + filters: MilestoneReportFilters = {}, + ): Promise { + const { courseId, status } = filters + return this.reports + .filter((r) => r.scholar_address === scholarAddress) + .filter((r) => (courseId ? r.course_id === courseId : true)) + .filter((r) => (status ? r.status === status : true)) + .sort((a, b) => b.submitted_at.localeCompare(a.submitted_at)) + } + + async createReport( + data: Omit< + MilestoneReport, + "id" | "status" | "submitted_at" | "resubmission_count" + >, + ): Promise { + const existing = this.reports.find( + (r) => + r.scholar_address === data.scholar_address && + r.course_id === data.course_id && + r.milestone_id === data.milestone_id, + ) + if (existing) { + if (existing.status !== "rejected") { + throw new Error("DUPLICATE_REPORT") + } + // Resubmit: update existing + existing.evidence_github = data.evidence_github + existing.evidence_ipfs_cid = data.evidence_ipfs_cid + existing.evidence_description = data.evidence_description + existing.status = "pending" + existing.submitted_at = new Date().toISOString() + existing.resubmission_count += 1 + return existing + } + const report: MilestoneReport = { + id: this.reportSeq++, + status: "pending", + submitted_at: new Date().toISOString(), + resubmission_count: 0, + ...data, + } + this.reports.push(report) + return report + } + + async updateReportStatus( + id: number, + status: "approved" | "rejected", + ): Promise { + const report = this.reports.find((r) => r.id === id) + if (!report) return null + report.status = status + return report + } + + async addAuditEntry( + entry: Omit, + ): Promise { + const log: MilestoneAuditEntry = { + id: this.auditSeq++, + decided_at: new Date().toISOString(), + ...entry, + } + this.auditLog.push(log) + return log + } + + async getAuditForReport(reportId: number): Promise { + return this.auditLog.filter((e) => e.report_id === reportId) + } + + async getMilestoneProgress( + scholarAddress: string, + courseId: string, + ): Promise<{ totalMilestones: number; approvedCount: number }> { + const allForCourse = this.reports.filter( + (r) => r.scholar_address === scholarAddress && r.course_id === courseId, + ) + const approvedCount = allForCourse.filter( + (r) => r.status === "approved", + ).length + const totalMilestones = allForCourse.length + return { totalMilestones, approvedCount } + } +} + +export const inMemoryMilestoneStore = new InMemoryMilestoneStore() + +// Detect whether we're using real Postgres +function isRealPool(): boolean { + return typeof (pool as any).totalCount !== "undefined" +} + +export const milestoneStore = { + async getPendingReports(): Promise { + if (!isRealPool()) return inMemoryMilestoneStore.getPendingReports() + const result = await pool.query( + `SELECT * FROM milestone_reports WHERE status = 'pending' ORDER BY submitted_at ASC`, + ) + return result.rows + }, + + async listReports( + filters: MilestoneReportFilters = {}, + page: number = 1, + pageSize: number = 10, + ): Promise { + if (!isRealPool()) { + return inMemoryMilestoneStore.listReports(filters, page, pageSize) + } + + const values: Array = [] + const conditions: string[] = [] + + if (filters.courseId) { + values.push(filters.courseId) + conditions.push(`course_id = $${values.length}`) + } + + if (filters.status) { + values.push(filters.status) + conditions.push(`status = $${values.length}`) + } + + const whereClause = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "" + const totalResult = await pool.query( + `SELECT COUNT(*) AS total FROM milestone_reports ${whereClause}`, + values, + ) + const total = Number(totalResult.rows[0]?.total ?? 0) + const offset = (page - 1) * pageSize + const rowValues = [...values, pageSize, offset] + const dataResult = await pool.query( + `SELECT * + FROM milestone_reports + ${whereClause} + ORDER BY submitted_at DESC + LIMIT $${rowValues.length - 1} + OFFSET $${rowValues.length}`, + rowValues, + ) + + return { + data: dataResult.rows, + total, + } + }, + + async getReportById(id: number): Promise { + if (!isRealPool()) return inMemoryMilestoneStore.getReportById(id) + const result = await pool.query( + `SELECT * FROM milestone_reports WHERE id = $1`, + [id], + ) + return result.rows[0] ?? null + }, + + async getReportsForScholar( + scholarAddress: string, + filters: MilestoneReportFilters = {}, + ): Promise { + if (!isRealPool()) { + return inMemoryMilestoneStore.getReportsForScholar( + scholarAddress, + filters, + ) + } + + const values: Array = [scholarAddress] + let sql = `SELECT * FROM milestone_reports WHERE scholar_address = $1` + + if (filters.courseId) { + values.push(filters.courseId) + sql += ` AND course_id = $${values.length}` + } + + if (filters.status) { + values.push(filters.status) + sql += ` AND status = $${values.length}` + } + + sql += ` ORDER BY submitted_at DESC` + + const result = await pool.query(sql, values) + return result.rows + }, + + async createReport( + data: Omit< + MilestoneReport, + "id" | "status" | "submitted_at" | "resubmission_count" + >, + ): Promise { + if (!isRealPool()) return inMemoryMilestoneStore.createReport(data) + // Check for existing + const existingResult = await pool.query( + `SELECT id, status, resubmission_count FROM milestone_reports WHERE scholar_address = $1 AND course_id = $2 AND milestone_id = $3`, + [data.scholar_address, data.course_id, data.milestone_id], + ) + const existing = existingResult.rows[0] + if (existing) { + if (existing.status !== "rejected") { + throw new Error("DUPLICATE_REPORT") + } + // Resubmit: update + const updateResult = await pool.query( + `UPDATE milestone_reports SET evidence_github = $1, evidence_ipfs_cid = $2, evidence_description = $3, status = 'pending', submitted_at = NOW(), resubmission_count = resubmission_count + 1 WHERE id = $4 RETURNING *`, + [ + data.evidence_github ?? null, + data.evidence_ipfs_cid ?? null, + data.evidence_description ?? null, + existing.id, + ], + ) + return updateResult.rows[0] + } + // Insert new + try { + const result = await pool.query( + `INSERT INTO milestone_reports + (scholar_address, course_id, milestone_id, evidence_github, evidence_ipfs_cid, evidence_description, resubmission_count) + VALUES ($1, $2, $3, $4, $5, $6, 0) + RETURNING *`, + [ + data.scholar_address, + data.course_id, + data.milestone_id, + data.evidence_github ?? null, + data.evidence_ipfs_cid ?? null, + data.evidence_description ?? null, + ], + ) + return result.rows[0] + } catch (err: any) { + if (err?.code === "23505") throw new Error("DUPLICATE_REPORT") + throw err + } + }, + + async updateReportStatus( + id: number, + status: "approved" | "rejected", + ): Promise { + if (!isRealPool()) + return inMemoryMilestoneStore.updateReportStatus(id, status) + const result = await pool.query( + `UPDATE milestone_reports SET status = $1 WHERE id = $2 RETURNING *`, + [status, id], + ) + return result.rows[0] ?? null + }, + + async addAuditEntry( + entry: Omit, + ): Promise { + if (!isRealPool()) return inMemoryMilestoneStore.addAuditEntry(entry) + const result = await pool.query( + `INSERT INTO milestone_audit_log + (report_id, validator_address, decision, rejection_reason, contract_tx_hash) + VALUES ($1, $2, $3, $4, $5) + RETURNING *`, + [ + entry.report_id, + entry.validator_address, + entry.decision, + entry.rejection_reason ?? null, + entry.contract_tx_hash ?? null, + ], + ) + return result.rows[0] + }, + + async getMilestoneProgress( + scholarAddress: string, + courseId: string, + ): Promise<{ totalMilestones: number; approvedCount: number }> { + if (!isRealPool()) { + return inMemoryMilestoneStore.getMilestoneProgress( + scholarAddress, + courseId, + ) + } + const totalResult = await pool.query( + `SELECT COUNT(*) AS total FROM milestones WHERE course_id = (SELECT id FROM courses WHERE slug = $1)`, + [courseId], + ) + const approvedResult = await pool.query( + `SELECT COUNT(*) AS approved FROM milestone_reports WHERE scholar_address = $1 AND course_id = $2 AND status = 'approved'`, + [scholarAddress, courseId], + ) + return { + totalMilestones: Number(totalResult.rows[0]?.total ?? 0), + approvedCount: Number(approvedResult.rows[0]?.approved ?? 0), + } + }, + + async getAuditForReport(reportId: number): Promise { + if (!isRealPool()) return inMemoryMilestoneStore.getAuditForReport(reportId) + const result = await pool.query( + `SELECT * FROM milestone_audit_log WHERE report_id = $1 ORDER BY decided_at ASC`, + [reportId], + ) + return result.rows + }, +} diff --git a/server/src/db/nonce-store.ts b/server/src/db/nonce-store.ts new file mode 100644 index 00000000..12246634 --- /dev/null +++ b/server/src/db/nonce-store.ts @@ -0,0 +1,99 @@ +import Redis from "ioredis" + +const NONCE_PREFIX = "learnvault:nonce:" +const DEFAULT_TTL_SECONDS = 300 // 5 minutes + +export type NonceStore = { + getNonce(address: string): Promise + /** Sets nonce only if missing; returns the effective nonce (existing or new). */ + getOrSetNonce( + address: string, + nonce: string, + ttlSeconds?: number, + ): Promise + deleteNonce(address: string): Promise +} + +type MemoryEntry = { nonce: string; expiresAt: number } + +function createMemoryStore(): NonceStore { + const map = new Map() + + const sweep = (address: string): void => { + const e = map.get(address) + if (e && Date.now() >= e.expiresAt) { + map.delete(address) + } + } + + return { + async getNonce(address: string): Promise { + sweep(address) + return map.get(address)?.nonce ?? null + }, + + async getOrSetNonce( + address: string, + nonce: string, + ttlSeconds = DEFAULT_TTL_SECONDS, + ): Promise { + sweep(address) + const existing = map.get(address) + if (existing && Date.now() < existing.expiresAt) { + return existing.nonce + } + const expiresAt = Date.now() + ttlSeconds * 1000 + map.set(address, { nonce, expiresAt }) + return nonce + }, + + async deleteNonce(address: string): Promise { + map.delete(address) + }, + } +} + +function createRedisStore(redisUrl: string): NonceStore { + const client = new Redis(redisUrl, { + maxRetriesPerRequest: 2, + lazyConnect: false, + }) + + const key = (address: string): string => `${NONCE_PREFIX}${address}` + + return { + async getNonce(address: string): Promise { + const v = await client.get(key(address)) + return v + }, + + async getOrSetNonce( + address: string, + nonce: string, + ttlSeconds = DEFAULT_TTL_SECONDS, + ): Promise { + const k = key(address) + const existing = await client.get(k) + if (existing !== null) { + const ttl = await client.ttl(k) + // ttl > 0: key has expiry; ttl === -1: exists with no TTL (still valid) + if (ttl > 0 || ttl === -1) { + return existing + } + } + await client.set(k, nonce, "EX", ttlSeconds) + return nonce + }, + + async deleteNonce(address: string): Promise { + await client.del(key(address)) + }, + } +} + +export function createNonceStore(redisUrl: string | undefined): NonceStore { + if (redisUrl && redisUrl.trim().length > 0) { + return createRedisStore(redisUrl.trim()) + } + return createMemoryStore() +} diff --git a/server/src/db/seed.sql b/server/src/db/seed.sql new file mode 100644 index 00000000..cb88c170 --- /dev/null +++ b/server/src/db/seed.sql @@ -0,0 +1,359 @@ +-- ============================================================ +-- Seed data: 2 sample courses with lessons, milestones & quizzes +-- ============================================================ + +-- Courses +INSERT INTO courses (slug, title, description, difficulty, track, cover_image_url, lrn_reward, published_at) +VALUES + ( + 'intro-to-stellar', + 'Introduction to Stellar', + 'Learn the fundamentals of the Stellar network: accounts, assets, transactions, and the Stellar Consensus Protocol.', + 'beginner', + 'stellar', + '/assets/brand/covers/cover-intro-stellar.svg', + 50.0000000, + NOW() + ), + ( + 'soroban-smart-contracts', + 'Soroban Smart Contracts', + 'Build and deploy smart contracts on Stellar using Soroban. Covers Rust basics, contract storage, and cross-contract calls.', + 'intermediate', + 'soroban', + '/assets/brand/covers/cover-soroban.svg', + 120.0000000, + NOW() + ), + ( + 'defi-fundamentals', + 'DeFi Fundamentals', + 'Understand the core concepts of decentralized finance: swaps, liquidity pools, yield farming, and protocol risks.', + 'beginner', + 'defi', + '/assets/brand/covers/cover-defi.svg', + 75.0000000, + NOW() + ) +ON CONFLICT (slug) DO NOTHING; + +-- Lessons for course 1 (intro-to-stellar) +INSERT INTO lessons (course_id, order_index, title, content_markdown, estimated_minutes) +SELECT c.id, 1, 'What is Stellar?', + E'## What is Stellar?\n\nStellar is an open-source, decentralized payment protocol that enables fast, low-cost cross-border transactions.\n\n### Key Concepts\n- **Lumens (XLM)**: The native asset used to pay transaction fees.\n- **Accounts**: Identified by a public key; must hold a minimum XLM balance.\n- **Anchors**: Entities that bridge fiat and crypto on the network.', + 15 +FROM courses c WHERE c.slug = 'intro-to-stellar' +ON CONFLICT (course_id, order_index) DO NOTHING; + +INSERT INTO lessons (course_id, order_index, title, content_markdown, estimated_minutes) +SELECT c.id, 2, 'Accounts and Keypairs', + E'## Accounts and Keypairs\n\nEvery Stellar account is identified by a **public key** (G...) and controlled by a **secret key** (S...).\n\n```bash\n# Generate a keypair with the Stellar CLI\nstellar keys generate --global alice\n```\n\nAccounts must be **funded** with at least 1 XLM (the base reserve) before they can transact.', + 20 +FROM courses c WHERE c.slug = 'intro-to-stellar' +ON CONFLICT (course_id, order_index) DO NOTHING; + +INSERT INTO lessons (course_id, order_index, title, content_markdown, estimated_minutes) +SELECT c.id, 3, 'Sending Your First Transaction', + E'## Sending Your First Transaction\n\nTransactions on Stellar are atomic bundles of **operations**. The most common operation is `Payment`.\n\n### Steps\n1. Build the transaction with the SDK.\n2. Sign it with your secret key.\n3. Submit to Horizon.\n\nFees are tiny — typically 100 stroops (0.00001 XLM).', + 25 +FROM courses c WHERE c.slug = 'intro-to-stellar' +ON CONFLICT (course_id, order_index) DO NOTHING; + +-- Lessons for course 2 (soroban-smart-contracts) +INSERT INTO lessons (course_id, order_index, title, content_markdown, estimated_minutes) +SELECT c.id, 1, 'Soroban Overview', + E'## Soroban Overview\n\nSoroban is Stellar''s smart contract platform, built with Rust and WebAssembly.\n\n### Why Soroban?\n- Predictable, metered execution costs.\n- First-class support for Stellar assets.\n- Designed for real-world financial use cases.', + 20 +FROM courses c WHERE c.slug = 'soroban-smart-contracts' +ON CONFLICT (course_id, order_index) DO NOTHING; + +INSERT INTO lessons (course_id, order_index, title, content_markdown, estimated_minutes) +SELECT c.id, 2, 'Writing Your First Contract', + E'## Writing Your First Contract\n\n```rust\n#![no_std]\nuse soroban_sdk::{contract, contractimpl, Env, Symbol, symbol_short};\n\n#[contract]\npub struct HelloContract;\n\n#[contractimpl]\nimpl HelloContract {\n pub fn hello(env: Env, to: Symbol) -> Symbol {\n symbol_short!("Hello")\n }\n}\n```\n\nCompile with `stellar contract build` and deploy to testnet.', + 30 +FROM courses c WHERE c.slug = 'soroban-smart-contracts' +ON CONFLICT (course_id, order_index) DO NOTHING; + +INSERT INTO lessons (course_id, order_index, title, content_markdown, estimated_minutes) +SELECT c.id, 3, 'Contract Storage', + E'## Contract Storage\n\nSoroban provides three storage tiers:\n\n| Tier | TTL | Use case |\n|------|-----|----------|\n| **Persistent** | Long-lived | User balances, config |\n| **Temporary** | Short-lived | Nonces, session data |\n| **Instance** | Contract lifetime | Contract metadata |\n\nAlways choose the cheapest tier that meets your durability needs.', + 25 +FROM courses c WHERE c.slug = 'soroban-smart-contracts' +ON CONFLICT (course_id, order_index) DO NOTHING; + +-- Lessons for course 3 (defi-fundamentals) +INSERT INTO lessons (course_id, order_index, title, content_markdown, estimated_minutes) +SELECT c.id, 1, 'What is DeFi?', + E'## What is DeFi?\n\nDecentralized Finance (DeFi) refers to financial services built on blockchain networks without intermediaries.\n\n### Key Characteristics\n- **Transparent**: All transactions are visible on-chain.\n- **Permissionless**: Anyone can participate without KYC.\n- **Composable**: Protocols can interact with each other (money legos).\n- **Non-custodial**: Users control their own assets.', + 18 +FROM courses c WHERE c.slug = 'defi-fundamentals' +ON CONFLICT (course_id, order_index) DO NOTHING; + +INSERT INTO lessons (course_id, order_index, title, content_markdown, estimated_minutes) +SELECT c.id, 2, 'Automated Market Makers (AMMs)', + E'## Automated Market Makers (AMMs)\n\nAMMs are smart contracts that enable peer-to-peer trading without order books.\n\n### How They Work\n- Users deposit token pairs into liquidity pools.\n- Traders swap tokens against the pool using the formula: x * y = k.\n- Liquidity providers earn a portion of trading fees.\n\n### Popular AMMs\n- Uniswap (Ethereum)\n- Curve (Stablecoins)\n- Stellar Swap (Stellar)', + 22 +FROM courses c WHERE c.slug = 'defi-fundamentals' +ON CONFLICT (course_id, order_index) DO NOTHING; + +INSERT INTO lessons (course_id, order_index, title, content_markdown, estimated_minutes) +SELECT c.id, 3, 'Yield Farming and Risk', + E'## Yield Farming and Risk\n\nYield farming involves depositing assets into DeFi protocols to earn returns.\n\n### Common Risks\n- **Impermanent Loss**: Price divergence between pooled assets.\n- **Smart Contract Risk**: Bugs or exploits in protocol code.\n- **Liquidation Risk**: Collateral value drops below loan threshold.\n- **Regulatory Risk**: Changing legal landscape.\n\nAlways do your own research (DYOR) before participating.', + 25 +FROM courses c WHERE c.slug = 'defi-fundamentals' +ON CONFLICT (course_id, order_index) DO NOTHING; + +-- Milestones for course 1 +INSERT INTO milestones (course_id, lesson_id, on_chain_milestone_id, lrn_amount) +SELECT c.id, l.id, 1, 15.0000000 +FROM courses c +JOIN lessons l ON l.course_id = c.id AND l.order_index = 1 +WHERE c.slug = 'intro-to-stellar' +ON CONFLICT (course_id, on_chain_milestone_id) DO NOTHING; + +INSERT INTO milestones (course_id, lesson_id, on_chain_milestone_id, lrn_amount) +SELECT c.id, l.id, 2, 15.0000000 +FROM courses c +JOIN lessons l ON l.course_id = c.id AND l.order_index = 2 +WHERE c.slug = 'intro-to-stellar' +ON CONFLICT (course_id, on_chain_milestone_id) DO NOTHING; + +INSERT INTO milestones (course_id, lesson_id, on_chain_milestone_id, lrn_amount) +SELECT c.id, l.id, 3, 20.0000000 +FROM courses c +JOIN lessons l ON l.course_id = c.id AND l.order_index = 3 +WHERE c.slug = 'intro-to-stellar' +ON CONFLICT (course_id, on_chain_milestone_id) DO NOTHING; + +-- Milestones for course 2 +INSERT INTO milestones (course_id, lesson_id, on_chain_milestone_id, lrn_amount) +SELECT c.id, l.id, 1, 30.0000000 +FROM courses c +JOIN lessons l ON l.course_id = c.id AND l.order_index = 1 +WHERE c.slug = 'soroban-smart-contracts' +ON CONFLICT (course_id, on_chain_milestone_id) DO NOTHING; + +INSERT INTO milestones (course_id, lesson_id, on_chain_milestone_id, lrn_amount) +SELECT c.id, l.id, 2, 45.0000000 +FROM courses c +JOIN lessons l ON l.course_id = c.id AND l.order_index = 2 +WHERE c.slug = 'soroban-smart-contracts' +ON CONFLICT (course_id, on_chain_milestone_id) DO NOTHING; + +INSERT INTO milestones (course_id, lesson_id, on_chain_milestone_id, lrn_amount) +SELECT c.id, l.id, 3, 45.0000000 +FROM courses c +JOIN lessons l ON l.course_id = c.id AND l.order_index = 3 +WHERE c.slug = 'soroban-smart-contracts' +ON CONFLICT (course_id, on_chain_milestone_id) DO NOTHING; + +-- Milestones for course 3 (defi-fundamentals) +INSERT INTO milestones (course_id, lesson_id, on_chain_milestone_id, lrn_amount) +SELECT c.id, l.id, 1, 20.0000000 +FROM courses c +JOIN lessons l ON l.course_id = c.id AND l.order_index = 1 +WHERE c.slug = 'defi-fundamentals' +ON CONFLICT (course_id, on_chain_milestone_id) DO NOTHING; + +INSERT INTO milestones (course_id, lesson_id, on_chain_milestone_id, lrn_amount) +SELECT c.id, l.id, 2, 25.0000000 +FROM courses c +JOIN lessons l ON l.course_id = c.id AND l.order_index = 2 +WHERE c.slug = 'defi-fundamentals' +ON CONFLICT (course_id, on_chain_milestone_id) DO NOTHING; + +INSERT INTO milestones (course_id, lesson_id, on_chain_milestone_id, lrn_amount) +SELECT c.id, l.id, 3, 30.0000000 +FROM courses c +JOIN lessons l ON l.course_id = c.id AND l.order_index = 3 +WHERE c.slug = 'defi-fundamentals' +ON CONFLICT (course_id, on_chain_milestone_id) DO NOTHING; + +-- Quizzes +INSERT INTO quizzes (lesson_id, passing_score) +SELECT l.id, 70 +FROM lessons l +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'intro-to-stellar' AND l.order_index = 1 +ON CONFLICT (lesson_id) DO NOTHING; + +INSERT INTO quizzes (lesson_id, passing_score) +SELECT l.id, 70 +FROM lessons l +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'intro-to-stellar' AND l.order_index = 2 +ON CONFLICT (lesson_id) DO NOTHING; + +INSERT INTO quizzes (lesson_id, passing_score) +SELECT l.id, 70 +FROM lessons l +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'soroban-smart-contracts' AND l.order_index = 1 +ON CONFLICT (lesson_id) DO NOTHING; + +INSERT INTO quizzes (lesson_id, passing_score) +SELECT l.id, 80 +FROM lessons l +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'soroban-smart-contracts' AND l.order_index = 2 +ON CONFLICT (lesson_id) DO NOTHING; + +INSERT INTO quizzes (lesson_id, passing_score) +SELECT l.id, 70 +FROM lessons l +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'defi-fundamentals' AND l.order_index = 1 +ON CONFLICT (lesson_id) DO NOTHING; + +INSERT INTO quizzes (lesson_id, passing_score) +SELECT l.id, 75 +FROM lessons l +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'defi-fundamentals' AND l.order_index = 2 +ON CONFLICT (lesson_id) DO NOTHING; + +INSERT INTO quizzes (lesson_id, passing_score) +SELECT l.id, 75 +FROM lessons l +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'defi-fundamentals' AND l.order_index = 3 +ON CONFLICT (lesson_id) DO NOTHING; + +-- Quiz questions: intro-to-stellar lesson 1 +INSERT INTO quiz_questions (quiz_id, question_text, options, correct_index, explanation) +SELECT q.id, + 'What is the native asset of the Stellar network?', + '["Bitcoin (BTC)", "Ether (ETH)", "Lumens (XLM)", "USDC"]'::jsonb, + 2, + 'Lumens (XLM) is the native asset of Stellar, used to pay transaction fees and maintain minimum account balances.' +FROM quizzes q +JOIN lessons l ON l.id = q.lesson_id +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'intro-to-stellar' AND l.order_index = 1; + +INSERT INTO quiz_questions (quiz_id, question_text, options, correct_index, explanation) +SELECT q.id, + 'What role do Anchors play on the Stellar network?', + '["They validate transactions", "They bridge fiat currency and crypto assets", "They store private keys", "They mine new XLM"]'::jsonb, + 1, + 'Anchors are trusted entities that issue assets on Stellar and allow users to deposit/withdraw fiat or other off-chain assets.' +FROM quizzes q +JOIN lessons l ON l.id = q.lesson_id +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'intro-to-stellar' AND l.order_index = 1; + +-- Quiz questions: intro-to-stellar lesson 2 +INSERT INTO quiz_questions (quiz_id, question_text, options, correct_index, explanation) +SELECT q.id, + 'What prefix does a Stellar public key start with?', + '["S", "G", "0x", "pk_"]'::jsonb, + 1, + 'Stellar public keys (account IDs) always start with "G". Secret keys start with "S".' +FROM quizzes q +JOIN lessons l ON l.id = q.lesson_id +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'intro-to-stellar' AND l.order_index = 2; + +-- Quiz questions: soroban lesson 1 +INSERT INTO quiz_questions (quiz_id, question_text, options, correct_index, explanation) +SELECT q.id, + 'What language is used to write Soroban smart contracts?', + '["Solidity", "Go", "Rust", "TypeScript"]'::jsonb, + 2, + 'Soroban contracts are written in Rust and compiled to WebAssembly (WASM).' +FROM quizzes q +JOIN lessons l ON l.id = q.lesson_id +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'soroban-smart-contracts' AND l.order_index = 1; + +INSERT INTO quiz_questions (quiz_id, question_text, options, correct_index, explanation) +SELECT q.id, + 'What runtime format does Soroban use to execute contracts?', + '["EVM bytecode", "JVM bytecode", "WebAssembly (WASM)", "LLVM IR"]'::jsonb, + 2, + 'Soroban compiles Rust contracts to WebAssembly, which runs in a sandboxed, metered environment on the Stellar network.' +FROM quizzes q +JOIN lessons l ON l.id = q.lesson_id +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'soroban-smart-contracts' AND l.order_index = 1; + +-- Quiz questions: soroban lesson 2 +INSERT INTO quiz_questions (quiz_id, question_text, options, correct_index, explanation) +SELECT q.id, + 'Which macro marks a Soroban struct as a deployable contract?', + '["#[contractimpl]", "#[contract]", "#[soroban_contract]", "#[deploy]"]'::jsonb, + 1, + 'The #[contract] attribute macro marks a struct as a Soroban contract entry point. #[contractimpl] is used on the impl block.' +FROM quizzes q +JOIN lessons l ON l.id = q.lesson_id +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'soroban-smart-contracts' AND l.order_index = 2; + +-- Quiz questions: defi-fundamentals lesson 1 +INSERT INTO quiz_questions (quiz_id, question_text, options, correct_index, explanation) +SELECT q.id, + 'What does DeFi stand for?', + '["Decentralized Finance", "Digital Finance", "Distributed Financial Services", "Decentralized Funding"]'::jsonb, + 0, + 'DeFi stands for Decentralized Finance, referring to financial services built on blockchain without intermediaries.' +FROM quizzes q +JOIN lessons l ON l.id = q.lesson_id +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'defi-fundamentals' AND l.order_index = 1; + +INSERT INTO quiz_questions (quiz_id, question_text, options, correct_index, explanation) +SELECT q.id, + 'Which of the following is NOT a key characteristic of DeFi?', + '["Transparent", "Permissionless", "Centralized governance", "Non-custodial"]'::jsonb, + 2, + 'DeFi is decentralized and permissionless. Centralized governance is contrary to DeFi principles.' +FROM quizzes q +JOIN lessons l ON l.id = q.lesson_id +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'defi-fundamentals' AND l.order_index = 1; + +-- Quiz questions: defi-fundamentals lesson 2 +INSERT INTO quiz_questions (quiz_id, question_text, options, correct_index, explanation) +SELECT q.id, + 'What is the core formula used by AMMs?', + '["x + y = k", "x * y = k", "x / y = k", "x - y = k"]'::jsonb, + 1, + 'The constant product formula x * y = k is the foundation of most AMMs like Uniswap.' +FROM quizzes q +JOIN lessons l ON l.id = q.lesson_id +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'defi-fundamentals' AND l.order_index = 2; + +INSERT INTO quiz_questions (quiz_id, question_text, options, correct_index, explanation) +SELECT q.id, + 'Who earns fees in an AMM?', + '["Traders", "Liquidity providers", "Protocol developers", "Validators"]'::jsonb, + 1, + 'Liquidity providers earn a portion of trading fees proportional to their share of the pool.' +FROM quizzes q +JOIN lessons l ON l.id = q.lesson_id +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'defi-fundamentals' AND l.order_index = 2; + +-- Quiz questions: defi-fundamentals lesson 3 +INSERT INTO quiz_questions (quiz_id, question_text, options, correct_index, explanation) +SELECT q.id, + 'What is impermanent loss?', + '["Loss from trading fees", "Price divergence between pooled assets", "Loss from smart contract bugs", "Loss from liquidation"]'::jsonb, + 1, + 'Impermanent loss occurs when the price ratio of pooled assets diverges, resulting in lower returns than holding the assets separately.' +FROM quizzes q +JOIN lessons l ON l.id = q.lesson_id +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'defi-fundamentals' AND l.order_index = 3; + +INSERT INTO quiz_questions (quiz_id, question_text, options, correct_index, explanation) +SELECT q.id, + 'Which risk is specific to lending protocols?', + '["Impermanent loss", "Liquidation risk", "Slippage", "Front-running"]'::jsonb, + 1, + 'Liquidation risk occurs when collateral value drops below the loan threshold, triggering forced asset sales.' +FROM quizzes q +JOIN lessons l ON l.id = q.lesson_id +JOIN courses c ON c.id = l.course_id +WHERE c.slug = 'defi-fundamentals' AND l.order_index = 3; diff --git a/server/src/db/token-store.ts b/server/src/db/token-store.ts new file mode 100644 index 00000000..4df53def --- /dev/null +++ b/server/src/db/token-store.ts @@ -0,0 +1,71 @@ +import crypto from "node:crypto" +import Redis from "ioredis" + +const BLOCKLIST_PREFIX = "learnvault:blocklist:" + +export type TokenStore = { + isRevoked(token: string): Promise + revoke(token: string, ttlSeconds: number): Promise +} + +function hashToken(token: string): string { + return crypto.createHash("sha256").update(token).digest("hex") +} + +type MemoryEntry = { expiresAt: number } + +function createMemoryStore(): TokenStore { + const map = new Map() + + const sweep = (hash: string): void => { + const e = map.get(hash) + if (e && Date.now() >= e.expiresAt) { + map.delete(hash) + } + } + + return { + async isRevoked(token: string): Promise { + const hash = hashToken(token) + sweep(hash) + return map.has(hash) + }, + + async revoke(token: string, ttlSeconds: number): Promise { + const hash = hashToken(token) + const expiresAt = Date.now() + ttlSeconds * 1000 + map.set(hash, { expiresAt }) + }, + } +} + +function createRedisStore(redisUrl: string): TokenStore { + const client = new Redis(redisUrl, { + maxRetriesPerRequest: 2, + lazyConnect: false, + }) + + const key = (hash: string): string => `${BLOCKLIST_PREFIX}${hash}` + + return { + async isRevoked(token: string): Promise { + const hash = hashToken(token) + const v = await client.get(key(hash)) + return v !== null + }, + + async revoke(token: string, ttlSeconds: number): Promise { + const hash = hashToken(token) + if (ttlSeconds > 0) { + await client.set(key(hash), "1", "EX", ttlSeconds) + } + }, + } +} + +export function createTokenStore(redisUrl: string | undefined): TokenStore { + if (redisUrl && redisUrl.trim().length > 0) { + return createRedisStore(redisUrl.trim()) + } + return createMemoryStore() +} diff --git a/server/src/errors/app-error-handler.ts b/server/src/errors/app-error-handler.ts new file mode 100644 index 00000000..7c094614 --- /dev/null +++ b/server/src/errors/app-error-handler.ts @@ -0,0 +1,11 @@ +export class AppError extends Error { + statusCode: number + details?: unknown + + constructor(message: string, statusCode: number, details?: unknown) { + super(message) + this.statusCode = statusCode + this.details = details + Error.captureStackTrace(this, this.constructor) + } +} diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 00000000..3ac56a26 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,270 @@ +import path from "path" +// eslint-disable-next-line import/order +import dotenv from "dotenv" +import compression from "compression" + +// Load server/.env whether you run from repo root or from server/ +dotenv.config({ path: path.resolve(__dirname, "..", ".env") }) + +import cors from "cors" +import express, { + type Request, + type Response, + type NextFunction, +} from "express" +import helmet from "helmet" +import morgan from "morgan" +import swaggerUi from "swagger-ui-express" +import YAML from "yaml" +import { z } from "zod" + +import { initDb } from "./db/index" +import { createNonceStore } from "./db/nonce-store" +import { createTokenStore } from "./db/token-store" +import { setupConsoleRequestTracing } from "./lib/request-context" +import { createRequireTrustedOrigin } from "./middleware/csrf.middleware" +import { errorHandler } from "./middleware/error.middleware" +import { globalLimiter } from "./middleware/rate-limit.middleware" +import { requestLogger } from "./middleware/request-logger.middleware" +import { buildOpenApiSpec } from "./openapi" +import { adminMilestonesRouter } from "./routes/admin-milestones.routes" +import { adminRouter } from "./routes/admin.routes" +import { createAuthRouter } from "./routes/auth.routes" +import { createCommentsRouter } from "./routes/comments.routes" +import { communityRouter } from "./routes/community.routes" +import { coursesRouter } from "./routes/courses.routes" +import { createCredentialsRouter } from "./routes/credentials.routes" +import { enrollmentsRouter } from "./routes/enrollments.routes" +import { eventsRouter } from "./routes/events.routes" +import { createForumRouter } from "./routes/forum.routes" +import { governanceRouter } from "./routes/governance.routes" +import { healthRouter } from "./routes/health.routes" +import { leaderboardRouter } from "./routes/leaderboard.routes" +import { createMeRouter } from "./routes/me.routes" +import { moderationRouter } from "./routes/moderation.routes" +import { scholarsRouter } from "./routes/scholars.routes" +import { scholarshipsRouter } from "./routes/scholarships.routes" +import { treasuryRouter } from "./routes/treasury.routes" +import { createUploadRouter } from "./routes/upload.routes" +import { validatorRouter } from "./routes/validator.routes" +import { wikiRouter } from "./routes/wiki.routes" +import { createAuthService } from "./services/auth.service" +import { + createJwtService, + generateEphemeralDevJwtKeys, +} from "./services/jwt.service" + +const _ignoredPemString = z + .string() + .min(1) + .transform((s) => s.replace(/\\n/g, "\n").trim()) + +const envSchema = z.object({ + PORT: z.coerce.number().int().positive().default(4000), + CORS_ORIGIN: z.string().default("http://localhost:5173"), + FRONTEND_URL: z.string().optional(), + NODE_ENV: z.string().default("development"), + REDIS_URL: z.string().optional(), + JWT_PRIVATE_KEY: z.string().optional(), + JWT_PUBLIC_KEY: z.string().optional(), +}) + +const env = envSchema.parse(process.env) +setupConsoleRequestTracing() + +const isProduction = env.NODE_ENV === "production" + +// Configure allowed CORS origins +const allowedOrigins = [ + env.FRONTEND_URL || env.CORS_ORIGIN || "http://localhost:5173", + "https://learnvault.app", + "https://www.learnvault.app", +] + +// In development, also allow common local dev ports +if (!isProduction) { + allowedOrigins.push( + "http://localhost:5173", + "http://localhost:3000", + "http://localhost:5174", + "http://127.0.0.1:5173", + ) +} + +let jwtPrivateKey = env.JWT_PRIVATE_KEY +let jwtPublicKey = env.JWT_PUBLIC_KEY + +// Generate ephemeral keys in dev if not provided +if (!jwtPrivateKey || !jwtPublicKey) { + if (isProduction) { + throw new Error( + "JWT_PRIVATE_KEY and JWT_PUBLIC_KEY environment variables are required in production", + ) + } + console.warn( + "⚠️ JWT keys not found in .env — generating ephemeral keys (tokens will reset on restart)", + ) + const ephemeral = generateEphemeralDevJwtKeys() + jwtPrivateKey = ephemeral.privateKeyPem + jwtPublicKey = ephemeral.publicKeyPem +} + +if (!jwtPrivateKey || !jwtPublicKey) { + throw new Error( + "JWT_PRIVATE_KEY and JWT_PUBLIC_KEY must be configured to start the server", + ) +} + +const nonceStore = createNonceStore(env.REDIS_URL) +const tokenStore = createTokenStore(env.REDIS_URL) +const jwtService = createJwtService(jwtPrivateKey, jwtPublicKey, tokenStore) +const authService = createAuthService(nonceStore, jwtService) + +const app = express() + +// ✅ compression must be added immediately after express init +// Skip compression for already-compressed content types (images, IPFS data, video, etc.) +app.use( + compression({ + filter: (req, res) => { + const contentType = res.getHeader("Content-Type") as string | undefined + if (contentType) { + if (/^image\//i.test(contentType)) return false + if (/^video\//i.test(contentType)) return false + if (/^audio\//i.test(contentType)) return false + if (/application\/octet-stream/i.test(contentType)) return false + } + // Skip IPFS gateway passthrough responses + const url = req.url ?? "" + if (url.includes("/ipfs/") || url.includes("ipfs.io")) return false + return compression.filter(req, res) + }, + level: 6, // balanced speed vs ratio (default is 6, explicit for clarity) + }) as any, +) + +export { app } +const openApiSpec = buildOpenApiSpec() +const _ignoredOpenApiYaml = YAML.stringify(openApiSpec) + +app.set("trust proxy", 1) + +// Log request latency: METHOD URL - Xms +app.use((req: Request, res: Response, next: NextFunction) => { + const start = Date.now() + res.on("finish", () => { + console.log(`${req.method} ${req.url} - ${Date.now() - start}ms`) + }) + next() +}) + +// Cache-Control: API responses must never be cached +app.use("/api", (_req: Request, res: Response, next: NextFunction) => { + res.setHeader("Cache-Control", "no-store") + next() +}) + +app.use(requestLogger) + +app.use( + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"], + connectSrc: [ + "'self'", + "https://horizon-testnet.stellar.org", + "https://horizon.stellar.org", + "https://ipfs.io", + "https://*.stellar.org", + ], + imgSrc: ["'self'", "data:", "https://ipfs.io"], + upgradeInsecureRequests: [], + }, + }, + xContentTypeOptions: true, + hsts: true, + }), +) + +app.use( + cors({ + origin: (origin: any, callback: any) => { + if (!origin) { + return callback(null, true) + } + + if (allowedOrigins.includes(origin)) { + callback(null, true) + } else { + console.warn(`CORS blocked request from origin: ${origin}`) + callback(new Error("Not allowed by CORS")) + } + }, + credentials: true, + methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + exposedHeaders: ["X-Request-ID"], + }), +) + +app.use(createRequireTrustedOrigin(allowedOrigins)) +app.use(express.json()) +app.use(globalLimiter) + +// Routes +app.use("/api", healthRouter) +app.use("/api/auth", createAuthRouter(authService)) +app.use("/api", createMeRouter(jwtService)) +app.use("/api", coursesRouter) +app.use("/api", enrollmentsRouter) +app.use("/api", scholarsRouter) +app.use("/api", scholarshipsRouter) +app.use("/api", createForumRouter(jwtService)) +app.use("/api", createCredentialsRouter(jwtService)) +app.use("/api", validatorRouter) +app.use("/api", eventsRouter) +app.use("/api/community", communityRouter) +app.use("/api", createCommentsRouter(jwtService)) +app.use("/api", leaderboardRouter) +app.use("/api", governanceRouter) +app.use("/api", treasuryRouter) +app.use("/api", wikiRouter) +app.use("/api", adminRouter) +app.use("/api", adminMilestonesRouter) +app.use("/api", moderationRouter) +app.use("/api", createUploadRouter(jwtService)) + +app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)) + +app.use(morgan("dev")) +app.use(errorHandler) + +// ── Startup ────────────────────────────────────────────────────────────────── + +async function start() { + const skipDb = process.env.SKIP_DB === "true" + + if (skipDb) { + console.warn( + "⚠️ SKIP_DB=true — skipping database initialization (dev/test mode only)", + ) + } else { + console.log("🔌 Initializing database...") + try { + await initDb() + console.log("✅ Database initialized successfully") + } catch (err) { + console.error("❌ Database initialization failed:") + console.error(err) + process.exit(1) + } + } + + app.listen(env.PORT, () => { + console.log(`🚀 Server listening on http://localhost:${env.PORT}`) + }) +} + +void start() diff --git a/server/src/lib/event-config.ts b/server/src/lib/event-config.ts new file mode 100644 index 00000000..36c63fa2 --- /dev/null +++ b/server/src/lib/event-config.ts @@ -0,0 +1,50 @@ +// Event configuration and helpers +// Import types for reuse +import { + type ContractName, + type EventTopic, + type EventTopicValue, + type ApiEvent, + CONTRACT_IDS, + EVENTS_TO_INDEX, + EVENT_DATA_SCHEMAS, + DB_EVENT_SCHEMA, +} from "../types/events" + +export { + type ContractName, + type EventTopic, + type EventTopicValue, + type ApiEvent, + CONTRACT_IDS, + EVENTS_TO_INDEX, + EVENT_DATA_SCHEMAS, + DB_EVENT_SCHEMA, +} + +// Soroban RPC endpoints +export const SOROBAN_RPC_URL = + process.env.SOROBAN_RPC_URL ?? + (process.env.STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org") + +// Indexer config +export const INDEXER_CONFIG = { + startingLedger: Number(process.env.STARTING_LEDGER ?? "0"), + pollIntervalMs: Number(process.env.POLL_INTERVAL_MS ?? "5000"), + batchSize: 100, // ledgers per poll +} as const + +// Helper to get flat list of {contractId, topics[]} for polling +export function getPollingTargets(): Array<{ + contractId: string + topics: string[] +}> { + return Object.entries(CONTRACT_IDS) + .map(([name, id]) => ({ + contractId: id, + topics: (EVENTS_TO_INDEX as any)[name as ContractName] || [], + })) + .filter((t) => t.topics.length > 0 && t.contractId) +} diff --git a/server/src/lib/request-context.ts b/server/src/lib/request-context.ts new file mode 100644 index 00000000..ccf0d7ba --- /dev/null +++ b/server/src/lib/request-context.ts @@ -0,0 +1,53 @@ +import { AsyncLocalStorage } from "node:async_hooks" + +type RequestContext = { + requestId: string +} + +const requestContextStorage = new AsyncLocalStorage() + +let consolePatched = false + +export function runWithRequestContext( + context: RequestContext, + fn: () => T, +): T { + return requestContextStorage.run(context, fn) +} + +export function getRequestContext(): RequestContext | undefined { + return requestContextStorage.getStore() +} + +export function getRequestId(): string | undefined { + return getRequestContext()?.requestId +} + +const methods = ["log", "info", "warn", "error", "debug"] as const + +export function setupConsoleRequestTracing(): void { + if (consolePatched) { + return + } + + for (const method of methods) { + const original = console[method].bind(console) + console[method] = ((...args: unknown[]) => { + const requestId = getRequestId() + if (!requestId) { + original(...args) + return + } + + const tag = `[requestId=${requestId}]` + if (typeof args[0] === "string") { + original(`${tag} ${args[0]}`, ...args.slice(1)) + return + } + + original(tag, ...args) + }) as (typeof console)[typeof method] + } + + consolePatched = true +} diff --git a/server/src/lib/zod-schemas.ts b/server/src/lib/zod-schemas.ts new file mode 100644 index 00000000..58132729 --- /dev/null +++ b/server/src/lib/zod-schemas.ts @@ -0,0 +1,272 @@ +import { z } from "zod" + +const requiredString = (field: string, maxLength?: number) => { + const schema = z + .string({ + required_error: `${field} is required`, + invalid_type_error: `${field} must be a string`, + }) + .trim() + .min(1, `${field} is required`) + + if (maxLength) { + return schema.max( + maxLength, + `${field} must be ${maxLength} characters or fewer`, + ) + } + + return schema +} + +const optionalTrimmedString = (field: string, maxLength?: number) => { + const schema = z + .string({ + invalid_type_error: `${field} must be a string`, + }) + .trim() + .min(1, `${field} cannot be empty`) + + if (maxLength) { + return schema + .max(maxLength, `${field} must be ${maxLength} characters or fewer`) + .optional() + } + + return schema.optional() +} + +const requiredInteger = (field: string) => + z + .number({ + required_error: `${field} is required`, + invalid_type_error: `${field} must be a number`, + }) + .int(`${field} must be an integer`) + .nonnegative(`${field} must be a non-negative integer`) + +export const courseIdParamSchema = z.object({ + courseId: z + .string({ message: "Course ID is required" }) + .cuid({ message: "Invalid course ID format" }), +}) + +export const milestoneReportIdParamSchema = z + .object({ + id: z + .string({ + required_error: "id is required", + invalid_type_error: "id must be a string", + }) + .regex(/^[1-9]\d*$/, "id must be a positive integer"), + }) + .strict() + +export const validateMilestoneSchema = z.object({ + courseId: z.string().cuid({ message: "Invalid course ID format" }), + learnerAddress: z.string().min(1), + milestoneId: z.number().int().nonnegative(), +}) + +export const legacyMilestoneSubmitBodySchema = z + .object({ + scholarAddress: requiredString("scholarAddress").max(100), + courseId: requiredString("courseId").max(100), + milestoneId: requiredInteger("milestoneId"), + evidenceGithub: z + .string({ + invalid_type_error: "evidenceGithub must be a string", + }) + .url("evidenceGithub must be a valid URL") + .max(500, "evidenceGithub must be 500 characters or fewer") + .optional(), + evidenceIpfsCid: optionalTrimmedString("evidenceIpfsCid", 100), + evidenceDescription: optionalTrimmedString("evidenceDescription", 2000), + }) + .strict() + .superRefine((data, ctx) => { + if ( + data.evidenceGithub !== undefined || + data.evidenceIpfsCid !== undefined || + data.evidenceDescription !== undefined + ) { + return + } + + for (const field of [ + "evidenceGithub", + "evidenceIpfsCid", + "evidenceDescription", + ]) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [field], + message: + "At least one evidence field is required (evidenceGithub, evidenceIpfsCid, or evidenceDescription)", + }) + } + }) + +export const milestoneSubmitBodySchema = z + .object({ + learner_address: requiredString("learner_address", 100), + course_id: requiredString("course_id", 100), + milestone_id: requiredInteger("milestone_id"), + evidence_url: z + .string({ + required_error: "evidence_url is required", + invalid_type_error: "evidence_url must be a string", + }) + .trim() + .url("evidence_url must be a valid URL") + .max(500, "evidence_url must be 500 characters or fewer"), + }) + .strict() + +export const approveMilestoneBodySchema = z + .object({ + note: optionalTrimmedString("note", 1000), + }) + .strict() + +const milestoneIdsSchema = z + .array(requiredInteger("milestoneIds")) + .min(1, "milestoneIds must include at least one milestone id") + .superRefine((ids, ctx) => { + const seen = new Set() + ids.forEach((id, index) => { + if (id <= 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [index], + message: "milestoneIds entries must be positive integers", + }) + } + + if (seen.has(id)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [index], + message: "milestoneIds must not contain duplicates", + }) + return + } + + seen.add(id) + }) + }) + +export const batchApproveMilestonesBodySchema = z + .object({ + milestoneIds: milestoneIdsSchema, + }) + .strict() + +export const rejectMilestoneBodySchema = z + .object({ + reason: requiredString("reason", 1000), + }) + .strict() + +export const batchRejectMilestonesBodySchema = z + .object({ + milestoneIds: milestoneIdsSchema, + reason: optionalTrimmedString("reason"), + }) + .strict() + +export const createCommentBodySchema = z + .object({ + proposalId: optionalTrimmedString("proposalId", 100), + proposal_id: optionalTrimmedString("proposal_id", 100), + content: optionalTrimmedString("content"), + body: optionalTrimmedString("body"), + author_address: optionalTrimmedString("author_address", 100), + parentId: z + .number({ + invalid_type_error: "parentId must be a number", + }) + .int("parentId must be an integer") + .positive("parentId must be a positive integer") + .optional(), + parent_id: z + .number({ + invalid_type_error: "parent_id must be a number", + }) + .int("parent_id must be an integer") + .positive("parent_id must be a positive integer") + .optional(), + }) + .strict() + .superRefine((data, ctx) => { + const usesSnakeCase = + data.proposal_id !== undefined || + data.body !== undefined || + data.author_address !== undefined || + data.parent_id !== undefined + + if (usesSnakeCase) { + if (data.proposal_id === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["proposal_id"], + message: "proposal_id is required", + }) + } + + if (data.body === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["body"], + message: "body is required", + }) + } + + if (data.author_address === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["author_address"], + message: "author_address is required", + }) + } + + return + } + + if (data.proposalId === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["proposalId"], + message: "proposalId is required", + }) + } + + if (data.content === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["content"], + message: "content is required", + }) + } + }) + +export const createCredentialMetadataBodySchema = z + .object({ + course_id: requiredString("course_id", 100), + learner_address: requiredString("learner_address", 100), + completed_at: z + .string({ + required_error: "completed_at is required", + invalid_type_error: "completed_at must be a string", + }) + .datetime({ message: "completed_at must be a valid ISO 8601 datetime" }), + }) + .strict() + +export const enrollmentBodySchema = z + .object({ + learner_address: requiredString("learner_address", 100), + course_id: requiredString("course_id", 100), + tx_hash: requiredString("tx_hash", 200), + }) + .strict() diff --git a/server/src/middleware/admin.middleware.ts b/server/src/middleware/admin.middleware.ts new file mode 100644 index 00000000..82526f18 --- /dev/null +++ b/server/src/middleware/admin.middleware.ts @@ -0,0 +1,87 @@ +import { type NextFunction, type Request, type Response } from "express" +import jwt from "jsonwebtoken" + +const DEFAULT_NON_PROD_JWT_SECRET = "learnvault-secret" + +function getAdminAddresses(): string[] { + return (process.env.ADMIN_ADDRESSES ?? "") + .split(",") + .map((a) => a.trim()) + .filter(Boolean) +} + +function getJwtPublicKey(): string | undefined { + return process.env.JWT_PUBLIC_KEY?.replace(/\\n/g, "\n").trim() +} + +function getJwtSecret(): string | undefined { + const secret = process.env.JWT_SECRET?.trim() + if (secret) return secret + if (process.env.NODE_ENV === "production") return undefined + + return DEFAULT_NON_PROD_JWT_SECRET +} + +export interface AdminRequest extends Request { + adminAddress?: string +} + +/** + * Middleware that verifies the Bearer JWT and checks the wallet address + * is in the ADMIN_ADDRESSES allowlist. + * + * In dev mode (no ADMIN_ADDRESSES set) any valid JWT is accepted so the + * API remains usable without extra config. + */ +export function requireAdmin( + req: AdminRequest, + res: Response, + next: NextFunction, +): void { + const header = req.headers.authorization + if (!header?.startsWith("Bearer ")) { + res.status(401).json({ error: "Unauthorized" }) + return + } + + const token = header.slice("Bearer ".length).trim() + let decoded: { address?: string; sub?: string } + const jwtPublicKey = getJwtPublicKey() + const jwtSecret = getJwtSecret() + + if (!jwtPublicKey && !jwtSecret) { + res.status(500).json({ error: "JWT verification not configured" }) + return + } + + try { + decoded = ( + jwtPublicKey + ? jwt.verify(token, jwtPublicKey, { + algorithms: ["RS256"], + }) + : jwt.verify(token, jwtSecret!) + ) as { address?: string; sub?: string } + } catch { + res.status(401).json({ error: "Invalid or expired token" }) + return + } + + const address = decoded.address ?? decoded.sub ?? "" + if (!address) { + res.status(401).json({ error: "Token missing address claim" }) + return + } + + const adminAddresses = getAdminAddresses() + + // If ADMIN_ADDRESSES is configured, enforce the allowlist + if (adminAddresses.length > 0 && !adminAddresses.includes(address)) { + res.status(403).json({ error: "Forbidden: not an admin address" }) + return + } + + req.adminAddress = address + req.walletAddress = address + next() +} diff --git a/server/src/middleware/auth.middleware.ts b/server/src/middleware/auth.middleware.ts new file mode 100644 index 00000000..ccbc3a28 --- /dev/null +++ b/server/src/middleware/auth.middleware.ts @@ -0,0 +1,86 @@ +import { type NextFunction, type Request, type Response } from "express" +import jwt from "jsonwebtoken" +import { type JwtService } from "../services/jwt.service" + +// --------------------------------------------------------------------------- +// Factory-based auth (used by routes that receive jwtService via DI) +// --------------------------------------------------------------------------- + +export function createRequireAuth(jwtService: JwtService) { + return async function requireAuth( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + const header = req.headers.authorization + if (!header?.startsWith("Bearer ")) { + res.status(401).json({ error: "Unauthorized" }) + return + } + + const token = header.slice("Bearer ".length).trim() + if (!token) { + res.status(401).json({ error: "Unauthorized" }) + return + } + + try { + const { sub } = await jwtService.verifyWalletToken(token) + req.walletAddress = sub + ;(req as AuthRequest).user = { address: sub } + next() + } catch (err) { + const message = + err instanceof Error ? err.message : "Invalid or expired token" + res.status(401).json({ error: message }) + } + } +} + +// --------------------------------------------------------------------------- +// Standalone auth (used by self-contained routers, e.g. upload, comments) +// --------------------------------------------------------------------------- + +const JWT_SECRET = process.env.JWT_SECRET || "learnvault-secret" +const JWT_PUBLIC_KEY = process.env.JWT_PUBLIC_KEY?.replace(/\\n/g, "\n").trim() + +export interface AuthRequest extends Request { + user?: { + address: string + } +} + +export const authMiddleware = ( + req: AuthRequest, + res: Response, + next: NextFunction, +) => { + const authHeader = req.headers.authorization + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ error: "Unauthorized" }) + } + + const token = authHeader.split(" ")[1] + try { + let decoded: { sub?: string; address?: string } + if (JWT_PUBLIC_KEY) { + decoded = jwt.verify(token, JWT_PUBLIC_KEY, { + algorithms: ["RS256"], + }) as { sub?: string; address?: string } + } else { + decoded = jwt.verify(token, JWT_SECRET) as { + sub?: string + address?: string + } + } + + const address = decoded.sub ?? decoded.address + if (!address) { + return res.status(401).json({ error: "Invalid token" }) + } + req.user = { address } + next() + } catch { + return res.status(401).json({ error: "Invalid token" }) + } +} diff --git a/server/src/middleware/course-admin.middleware.ts b/server/src/middleware/course-admin.middleware.ts new file mode 100644 index 00000000..01f691b5 --- /dev/null +++ b/server/src/middleware/course-admin.middleware.ts @@ -0,0 +1,114 @@ +import { type NextFunction, type Request, type Response } from "express" +import jwt from "jsonwebtoken" + +const DEFAULT_NON_PROD_JWT_SECRET = "learnvault-secret" + +type TokenPayload = { + sub?: string + address?: string + role?: string + isAdmin?: boolean +} + +function getJwtPublicKey(): string | undefined { + return process.env.JWT_PUBLIC_KEY?.replace(/\\n/g, "\n").trim() +} + +function getJwtSecret(): string | undefined { + const secret = process.env.JWT_SECRET?.trim() + if (secret) return secret + if (process.env.NODE_ENV === "production") return undefined + + return DEFAULT_NON_PROD_JWT_SECRET +} + +function getAdminApiKey(): string | undefined { + const apiKey = process.env.ADMIN_API_KEY?.trim() + return apiKey || undefined +} + +function getAdminAddresses(): string[] { + return (process.env.ADMIN_ADDRESSES ?? "") + .split(",") + .map((value) => value.trim()) + .filter(Boolean) +} + +function wantsUnpublishedCourses(req: Request): boolean { + const rawValue = req.query.includeUnpublished + if (typeof rawValue !== "string") return false + + return ["1", "true", "yes"].includes(rawValue.trim().toLowerCase()) +} + +export function requireCourseAdmin( + req: Request, + res: Response, + next: NextFunction, +): void { + const jwtPublicKey = getJwtPublicKey() + const jwtSecret = getJwtSecret() + const adminApiKey = getAdminApiKey() + const adminAddresses = getAdminAddresses() + const apiKey = req.header("x-api-key") + if (adminApiKey && apiKey && apiKey === adminApiKey) { + next() + return + } + + const authHeader = req.headers.authorization + if (!authHeader?.startsWith("Bearer ")) { + res.status(401).json({ error: "Unauthorized" }) + return + } + + const token = authHeader.slice("Bearer ".length).trim() + if (!token) { + res.status(401).json({ error: "Unauthorized" }) + return + } + + if (!jwtPublicKey && !jwtSecret) { + res.status(500).json({ error: "JWT verification not configured" }) + return + } + + let decoded: TokenPayload + try { + if (jwtPublicKey) { + decoded = jwt.verify(token, jwtPublicKey, { + algorithms: ["RS256"], + }) as TokenPayload + } else { + decoded = jwt.verify(token, jwtSecret!) as TokenPayload + } + } catch { + res.status(401).json({ error: "Unauthorized" }) + return + } + + const address = decoded.sub ?? decoded.address ?? "" + const isAdminRole = decoded.role === "admin" || decoded.isAdmin === true + const isAllowedAddress = + address.length > 0 && adminAddresses.includes(address) + + if (!isAdminRole && !isAllowedAddress) { + res.status(403).json({ error: "Forbidden" }) + return + } + + next() +} + +export function requireCourseAdminIfRequested( + req: Request, + res: Response, + next: NextFunction, +): void { + if (!wantsUnpublishedCourses(req)) { + next() + return + } + + requireCourseAdmin(req, res, next) +} diff --git a/server/src/middleware/csrf.middleware.ts b/server/src/middleware/csrf.middleware.ts new file mode 100644 index 00000000..8f40e220 --- /dev/null +++ b/server/src/middleware/csrf.middleware.ts @@ -0,0 +1,50 @@ +import { type NextFunction, type Request, type Response } from "express" + +const STATE_CHANGING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]) + +/** + * Rejects state-changing requests whose Origin or Referer do not match the + * allowlist. Browsers cannot set these headers from page JS, so this blocks + * classical (browser-mediated) CSRF — including against endpoints that do + * not require authentication. + * + * Requests with neither Origin nor Referer are allowed through. This keeps + * server-to-server clients (curl, Postman, workers) working and matches the + * posture of the CORS middleware; it is not a trust decision, just a + * recognition that those requests have no browser fingerprint to validate. + * Bearer-token auth is the load-bearing defense for that path. + */ +export function createRequireTrustedOrigin(allowedOrigins: readonly string[]) { + const trusted = new Set(allowedOrigins) + + return function requireTrustedOrigin( + req: Request, + res: Response, + next: NextFunction, + ): void { + if (!STATE_CHANGING_METHODS.has(req.method)) { + return next() + } + + const origin = req.headers.origin + if (origin) { + if (trusted.has(origin)) return next() + res.status(403).json({ error: "Forbidden: untrusted origin" }) + return + } + + const referer = req.headers.referer + if (referer) { + try { + const refererOrigin = new URL(referer).origin + if (trusted.has(refererOrigin)) return next() + } catch { + // malformed Referer — fall through to reject + } + res.status(403).json({ error: "Forbidden: untrusted referer" }) + return + } + + next() + } +} diff --git a/server/src/middleware/error.middleware.ts b/server/src/middleware/error.middleware.ts new file mode 100644 index 00000000..f009a565 --- /dev/null +++ b/server/src/middleware/error.middleware.ts @@ -0,0 +1,25 @@ +import { type NextFunction, type Request, type Response } from "express" +import { AppError } from "../errors/app-error-handler" + +export const errorHandler = ( + err: unknown, + _req: Request, + res: Response, + _next: NextFunction, +): void => { + if (err instanceof AppError) { + res.status(err.statusCode).json({ + error: err.message, + message: err.message, + ...(err.details ? { details: err.details } : {}), + }) + return + } + + const message = err instanceof Error ? err.message : "Internal Server Error" + + res.status(500).json({ + error: message, + message, + }) +} diff --git a/server/src/middleware/milestone-rate-limit.middleware.ts b/server/src/middleware/milestone-rate-limit.middleware.ts new file mode 100644 index 00000000..5c4c6bc0 --- /dev/null +++ b/server/src/middleware/milestone-rate-limit.middleware.ts @@ -0,0 +1,19 @@ +import rateLimit from "express-rate-limit" + +/** + * Rate limiter for milestone report submissions. + * Allows 1 report per scholar per milestone (enforced at DB level via UNIQUE constraint), + * but also caps burst submissions to 10 per IP per 15 minutes to prevent abuse. + */ +export const milestoneSubmitRateLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => { + // Key by wallet address when available, fall back to IP + const body = req.body as { scholarAddress?: string } + return body?.scholarAddress ?? req.ip ?? "unknown" + }, + message: { error: "Too many milestone submissions; try again later" }, +}) diff --git a/server/src/middleware/nonce-rate-limit.middleware.ts b/server/src/middleware/nonce-rate-limit.middleware.ts new file mode 100644 index 00000000..6e499ba1 --- /dev/null +++ b/server/src/middleware/nonce-rate-limit.middleware.ts @@ -0,0 +1,10 @@ +import rateLimit from "express-rate-limit" + +/** 10 nonce requests per IP per minute */ +export const nonceRateLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 10, + standardHeaders: true, + legacyHeaders: false, + message: { error: "Too many nonce requests; try again later" }, +}) diff --git a/server/src/middleware/rate-limit.middleware.ts b/server/src/middleware/rate-limit.middleware.ts new file mode 100644 index 00000000..670d7c2a --- /dev/null +++ b/server/src/middleware/rate-limit.middleware.ts @@ -0,0 +1,134 @@ +import { type Request, type Response, type NextFunction } from "express" +import rateLimit, { ipKeyGenerator } from "express-rate-limit" +import { AppError } from "../errors/app-error-handler" + +const createRateLimitHandler = + (message: string) => (req: Request, res: Response, next: NextFunction) => { + next(new AppError(message, 429)) + } + +const getBodyWalletValue = ( + req: Request, + keys: string[], +): string | undefined => { + const body = req.body as Record | undefined + if (!body || typeof body !== "object") return undefined + + for (const key of keys) { + const value = body[key] + if (typeof value === "string" && value.trim().length > 0) { + return value + } + } + + return undefined +} + +const createWalletKeyGenerator = + (bodyKeys: string[]) => + (req: Request): string => { + const headerWallet = req.headers["x-wallet-address"] + if (typeof headerWallet === "string" && headerWallet.trim().length > 0) { + return headerWallet + } + + return ( + getBodyWalletValue(req, bodyKeys) ?? + ipKeyGenerator(req.ip ?? "unknown") ?? + "unknown" + ) + } + +export const globalLimiter = rateLimit({ + windowMs: 60 * 1000, + limit: 100, + standardHeaders: "draft-7", + legacyHeaders: false, + handler: createRateLimitHandler("Too many requests, please try again later."), +}) + +export const uploadLimiter = rateLimit({ + windowMs: 60 * 1000, + limit: 5, + standardHeaders: "draft-7", + legacyHeaders: false, + handler: createRateLimitHandler( + "Upload limit reached. You can upload 5 times per minute.", + ), +}) + +export const milestoneReportLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, + limit: 3, + keyGenerator: (req: Request) => + (req.headers["x-wallet-address"] as string) ?? + ipKeyGenerator(req.ip ?? "unknown") ?? + "unknown", + standardHeaders: "draft-7", + legacyHeaders: false, + handler: createRateLimitHandler( + "Milestone report limit reached. You can submit 3 reports per hour.", + ), +}) + +export const proposalSubmissionLimiter = rateLimit({ + windowMs: 24 * 60 * 60 * 1000, + limit: 1, + keyGenerator: (req: Request) => + (req.headers["x-wallet-address"] as string) ?? + ipKeyGenerator(req.ip ?? "unknown") ?? + "unknown", + standardHeaders: "draft-7", + legacyHeaders: false, + handler: createRateLimitHandler( + "Proposal limit reached. You can submit 1 proposal per day.", + ), +}) + +export const authVerifyLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + limit: 10, + standardHeaders: "draft-7", + legacyHeaders: false, + handler: createRateLimitHandler( + "Verification limit reached. You can verify up to 10 times every 15 minutes.", + ), +}) + +export const scholarshipApplyLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, + limit: 3, + keyGenerator: createWalletKeyGenerator(["applicant_address"]), + standardHeaders: "draft-7", + legacyHeaders: false, + handler: createRateLimitHandler( + "Application limit reached. You can submit 3 scholarship applications per hour.", + ), +}) + +export const governanceVoteLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, + limit: 20, + keyGenerator: createWalletKeyGenerator([ + "walletAddress", + "wallet_address", + "voterAddress", + "voter_address", + ]), + standardHeaders: "draft-7", + legacyHeaders: false, + handler: createRateLimitHandler( + "Voting limit reached. You can submit 20 governance votes per hour.", + ), +}) + +export const milestoneSubmissionLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, + limit: 10, + keyGenerator: createWalletKeyGenerator(["scholarAddress", "scholar_address"]), + standardHeaders: "draft-7", + legacyHeaders: false, + handler: createRateLimitHandler( + "Milestone limit reached. You can submit 10 milestone reports per hour.", + ), +}) diff --git a/server/src/middleware/request-logger.middleware.ts b/server/src/middleware/request-logger.middleware.ts new file mode 100644 index 00000000..2aa583bd --- /dev/null +++ b/server/src/middleware/request-logger.middleware.ts @@ -0,0 +1,66 @@ +import { randomUUID } from "crypto" +import { type NextFunction, type Request, type Response } from "express" +import { runWithRequestContext } from "../lib/request-context" + +type LogPayload = { + requestId: string + method: string + path: string + statusCode: number + durationMs: number +} + +type Logger = { + info: (payload: LogPayload) => void +} + +type RequestLoggerOptions = { + logger?: Logger + enabled?: boolean +} + +const jsonLogger: Logger = { + info(payload) { + process.stdout.write(`${JSON.stringify(payload)}\n`) + }, +} + +export function createRequestLogger(options: RequestLoggerOptions = {}) { + const enabled = options.enabled ?? process.env.NODE_ENV !== "test" + const logger = options.logger ?? jsonLogger + + return function requestLogger( + req: Request, + res: Response, + next: NextFunction, + ) { + const requestId = randomUUID() + runWithRequestContext({ requestId }, () => { + const startedAt = process.hrtime.bigint() + + req.requestId = requestId + res.setHeader("X-Request-ID", requestId) + + res.on("finish", () => { + if (!enabled) { + return + } + + const durationMs = + Number(process.hrtime.bigint() - startedAt) / 1_000_000 + + logger.info({ + requestId, + method: req.method, + path: req.originalUrl || req.path, + statusCode: res.statusCode, + durationMs: Number(durationMs.toFixed(3)), + }) + }) + + next() + }) + } +} + +export const requestLogger = createRequestLogger() diff --git a/server/src/middleware/upload.middleware.ts b/server/src/middleware/upload.middleware.ts new file mode 100644 index 00000000..b8bd8fdc --- /dev/null +++ b/server/src/middleware/upload.middleware.ts @@ -0,0 +1,33 @@ +import multer from "multer" +import { AppError } from "../errors/app-error-handler" + +const ALLOWED_TYPES = [ + "image/png", + "image/jpeg", + "image/jpg", + "application/pdf", + "video/mp4", +] as const + +type AllowedMimeType = (typeof ALLOWED_TYPES)[number] + +const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB + +const fileFilter: multer.Options["fileFilter"] = (_req, file, cb) => { + if (!ALLOWED_TYPES.includes(file.mimetype as AllowedMimeType)) { + return cb( + new AppError("Invalid file type", 400, { + file: `${file.mimetype} is not allowed. Accepted: PNG, JPEG, PDF, MP4`, + }), + ) + } + cb(null, true) +} + +// Files are kept in memory so the controller can pass the buffer directly to +// Pinata without writing anything to disk. +export const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: MAX_FILE_SIZE }, + fileFilter, +}) diff --git a/server/src/middleware/validate.middleware.ts b/server/src/middleware/validate.middleware.ts new file mode 100644 index 00000000..03a46486 --- /dev/null +++ b/server/src/middleware/validate.middleware.ts @@ -0,0 +1,61 @@ +import { type NextFunction, type Request, type Response } from "express" +import { type ZodError, type ZodTypeAny } from "zod" +import { AppError } from "../errors/app-error-handler" + +type SchemaMap = { + body?: ZodTypeAny + query?: ZodTypeAny + params?: ZodTypeAny +} + +const formatErrors = (error: ZodError, location: "body" | "query" | "params") => + error.issues.map((issue) => ({ + field: issue.path.join(".") || location, + message: issue.message, + })) + +export const validate = + (schemas: SchemaMap) => + (req: Request, _res: Response, next: NextFunction) => { + try { + if (schemas.body) { + const result = schemas.body.safeParse(req.body) + if (!result.success) { + throw new AppError( + "Validation failed", + 400, + formatErrors(result.error, "body"), + ) + } + req.body = result.data + } + + if (schemas.query) { + const result = schemas.query.safeParse(req.query) + if (!result.success) { + throw new AppError( + "Validation failed", + 400, + formatErrors(result.error, "query"), + ) + } + req.query = result.data + } + + if (schemas.params) { + const result = schemas.params.safeParse(req.params) + if (!result.success) { + throw new AppError( + "Validation failed", + 400, + formatErrors(result.error, "params"), + ) + } + req.params = result.data as Request["params"] + } + + next() + } catch (error) { + next(error) + } + } diff --git a/server/src/middleware/validation.middleware.ts b/server/src/middleware/validation.middleware.ts new file mode 100644 index 00000000..0de711e4 --- /dev/null +++ b/server/src/middleware/validation.middleware.ts @@ -0,0 +1 @@ +export { validate } from "./validate.middleware" diff --git a/server/src/openapi.ts b/server/src/openapi.ts new file mode 100644 index 00000000..73cd5618 --- /dev/null +++ b/server/src/openapi.ts @@ -0,0 +1,407 @@ +import path from "node:path" + +import swaggerJSDoc from "swagger-jsdoc" + +export const buildOpenApiSpec = () => { + const sourceGlob = path.resolve(__dirname, "./routes/*.ts") + const transpiledGlob = path.resolve(__dirname, "./routes/*.js") + const rootSourceGlob = path.resolve(__dirname, "../src/routes/*.ts") + + return swaggerJSDoc({ + definition: { + openapi: "3.0.3", + info: { + title: "LearnVault API", + version: "1.0.0", + description: "Backend API for LearnVault frontend and integrations.", + }, + servers: [ + { + url: "http://localhost:4000", + description: "Local development server", + }, + ], + tags: [ + { name: "Health", description: "Server status endpoints" }, + { name: "Auth", description: "Wallet authentication endpoints" }, + { name: "Courses", description: "Course catalog endpoints" }, + { name: "Enrollments", description: "Course enrollment endpoints" }, + { name: "Governance", description: "Governance proposal endpoints" }, + { + name: "Scholarships", + description: "Scholarship application endpoints", + }, + { name: "Scholars", description: "Scholar leaderboard endpoints" }, + { name: "Validator", description: "Milestone validation endpoints" }, + { name: "Admin", description: "Admin milestone management endpoints" }, + { name: "Credentials", description: "Scholar credential endpoints" }, + { name: "Events", description: "Event stream endpoints" }, + { name: "Leaderboard", description: "Learner ranking endpoints" }, + { name: "Comments", description: "Proposal comment endpoints" }, + { + name: "Treasury", + description: "Treasury statistics and activity endpoints", + }, + { name: "Upload", description: "IPFS file upload endpoints" }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + schemas: { + ErrorResponse: { + type: "object", + properties: { + error: { + type: "string", + }, + }, + required: ["error"], + }, + HealthResponse: { + type: "object", + properties: { + status: { type: "string", example: "ok" }, + timestamp: { type: "string", format: "date-time" }, + }, + required: ["status", "timestamp"], + }, + Course: { + type: "object", + properties: { + id: { type: "string" }, + title: { type: "string" }, + level: { type: "string" }, + published: { type: "boolean" }, + }, + required: ["id", "title", "level", "published"], + }, + Event: { + type: "object", + properties: { + id: { type: "string" }, + type: { type: "string" }, + entityId: { type: "string" }, + timestamp: { type: "string", format: "date-time" }, + }, + required: ["id", "type", "entityId", "timestamp"], + }, + ValidatorRequest: { + type: "object", + properties: { + courseId: { type: "string" }, + learnerAddress: { type: "string" }, + milestoneId: { type: "integer", minimum: 0 }, + }, + required: ["courseId", "learnerAddress", "milestoneId"], + }, + ValidatorResult: { + allOf: [ + { $ref: "#/components/schemas/ValidatorRequest" }, + { + type: "object", + properties: { + approved: { type: "boolean" }, + validator: { type: "string" }, + }, + required: ["approved", "validator"], + }, + ], + }, + Proposal: { + type: "object", + properties: { + id: { type: "integer" }, + author_address: { type: "string", example: "GABCD123456789..." }, + title: { type: "string" }, + description: { type: "string" }, + amount: { type: "number" }, + votes_for: { type: "integer" }, + votes_against: { type: "integer" }, + status: { + type: "string", + enum: ["pending", "approved", "rejected"], + }, + cancelled: { type: "boolean" }, + deadline: { type: "string", format: "date-time" }, + }, + required: ["id", "author_address", "title", "status"], + }, + ScholarRanking: { + type: "object", + properties: { + rank: { type: "integer" }, + address: { type: "string" }, + lrn_balance: { type: "number" }, + courses_completed: { type: "integer" }, + }, + required: ["rank", "address", "lrn_balance", "courses_completed"], + }, + ScholarshipApplication: { + type: "object", + properties: { + applicant_address: { + type: "string", + minLength: 50, + maxLength: 56, + }, + full_name: { type: "string", minLength: 2 }, + course_id: { type: "string", minLength: 2 }, + motivation: { type: "string", minLength: 10 }, + evidence_url: { type: "string", format: "uri" }, + amount: { + type: "number", + description: "Requested USDC amount (default: 1000)", + }, + }, + required: [ + "applicant_address", + "full_name", + "course_id", + "motivation", + "evidence_url", + ], + }, + CourseDetail: { + type: "object", + properties: { + id: { type: "integer" }, + slug: { type: "string" }, + title: { type: "string" }, + description: { type: "string" }, + coverImage: { type: "string", nullable: true }, + track: { type: "string" }, + difficulty: { + type: "string", + enum: ["beginner", "intermediate", "advanced"], + }, + published: { type: "boolean" }, + createdAt: { type: "string", format: "date-time" }, + updatedAt: { type: "string", format: "date-time" }, + }, + required: [ + "id", + "slug", + "title", + "track", + "difficulty", + "published", + ], + }, + Lesson: { + type: "object", + properties: { + id: { type: "integer" }, + courseId: { type: "integer" }, + title: { type: "string" }, + content: { type: "string" }, + order: { type: "integer" }, + quiz: { + type: "array", + items: { + type: "object", + properties: { + question: { type: "string" }, + options: { type: "array", items: { type: "string" } }, + correctIndex: { type: "integer" }, + }, + }, + }, + createdAt: { type: "string", format: "date-time" }, + updatedAt: { type: "string", format: "date-time" }, + }, + required: ["id", "courseId", "title", "content", "order"], + }, + GovernanceProposalInput: { + type: "object", + properties: { + author_address: { + type: "string", + minLength: 50, + maxLength: 56, + example: "GABCD123456789...", + }, + title: { type: "string", minLength: 5, maxLength: 200 }, + description: { type: "string", minLength: 10 }, + requested_amount: { + type: "string", + pattern: "^\\d+(\\.\\d+)?$", + description: + "Numeric string representing requested USDC amount", + }, + evidence_url: { type: "string", format: "uri" }, + }, + required: [ + "author_address", + "title", + "description", + "requested_amount", + "evidence_url", + ], + }, + GovernanceProposalCreated: { + type: "object", + properties: { + proposal_id: { type: "integer" }, + tx_hash: { type: "string" }, + }, + required: ["proposal_id", "tx_hash"], + }, + VotingPower: { + type: "object", + properties: { + address: { + type: "string", + example: "GABCD123456789...", + }, + gov_balance: { + type: "string", + description: "Raw governance token balance", + }, + formatted: { + type: "string", + description: "Human-readable balance", + example: "100.50", + }, + can_vote: { type: "boolean" }, + }, + required: ["address", "gov_balance", "formatted", "can_vote"], + }, + ScholarProfile: { + type: "object", + properties: { + address: { type: "string" }, + lrn_balance: { + type: "string", + description: "Raw LRN token balance", + }, + enrolled_courses: { + type: "array", + items: { type: "string" }, + }, + completed_milestones: { type: "integer" }, + pending_milestones: { type: "integer" }, + credentials: { + type: "array", + items: { + $ref: "#/components/schemas/Credential", + }, + }, + joined_at: { type: "string", format: "date-time" }, + }, + required: [ + "address", + "lrn_balance", + "enrolled_courses", + "completed_milestones", + "pending_milestones", + "credentials", + "joined_at", + ], + }, + ScholarMilestone: { + type: "object", + properties: { + id: { type: "string" }, + course_id: { type: "string" }, + milestone_id: { type: "integer" }, + status: { + type: "string", + enum: ["pending", "verified", "rejected"], + }, + evidence_url: { type: "string", nullable: true }, + submitted_at: { + type: "string", + format: "date-time", + nullable: true, + }, + verified_at: { + type: "string", + format: "date-time", + nullable: true, + }, + tx_hash: { type: "string", nullable: true }, + }, + required: ["id", "course_id", "milestone_id", "status"], + }, + Credential: { + type: "object", + properties: { + token_id: { type: "integer" }, + course_id: { type: "string" }, + course_title: { type: "string" }, + issued_at: { type: "string", format: "date-time" }, + metadata_uri: { type: "string" }, + revoked: { type: "boolean" }, + }, + required: [ + "token_id", + "course_id", + "course_title", + "issued_at", + "metadata_uri", + "revoked", + ], + }, + }, + responses: { + BadRequestError: { + description: "Bad request", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/ErrorResponse", + }, + }, + }, + }, + UnauthorizedError: { + description: "Unauthorized", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/ErrorResponse", + }, + }, + }, + }, + NotFoundError: { + description: "Resource not found", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/ErrorResponse", + }, + }, + }, + }, + ForbiddenError: { + description: "Forbidden", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/ErrorResponse", + }, + }, + }, + }, + InternalServerError: { + description: "Internal server error", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/ErrorResponse", + }, + }, + }, + }, + }, + }, + }, + apis: [sourceGlob, transpiledGlob, rootSourceGlob], + }) +} diff --git a/server/src/routes/admin-milestones.routes.ts b/server/src/routes/admin-milestones.routes.ts new file mode 100644 index 00000000..20551b36 --- /dev/null +++ b/server/src/routes/admin-milestones.routes.ts @@ -0,0 +1,237 @@ +// Admin milestones routes - handles approval/rejection of milestone submissions +// Last updated: 2025-01-24 to resolve CI caching issues +import { Router } from "express" +import { + listMilestones, + getPendingMilestones, + getMilestoneById, + approveMilestone, + rejectMilestone, + batchApproveMilestones, + batchRejectMilestones, +} from "../controllers/admin-milestones.controller" +import { submitMilestoneReport } from "../controllers/milestone-submit.controller" +import { resubmitMilestoneReport } from "../controllers/milestone-resubmit.controller" +import { + approveMilestoneBodySchema, + batchApproveMilestonesBodySchema, + batchRejectMilestonesBodySchema, + legacyMilestoneSubmitBodySchema, + milestoneReportIdParamSchema, + milestoneSubmitBodySchema, + rejectMilestoneBodySchema, +} from "../lib/zod-schemas" +import { requireAdmin } from "../middleware/admin.middleware" +import { milestoneSubmissionLimiter } from "../middleware/rate-limit.middleware" +import { validate } from "../middleware/validate.middleware" + +export const adminMilestonesRouter = Router() + +adminMilestonesRouter.get("/admin/milestones", requireAdmin, listMilestones) + +/** + * @openapi + * /api/admin/milestones/pending: + * get: + * tags: [Admin] + * summary: List all unverified milestone reports + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: List of pending milestone reports + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + */ +adminMilestonesRouter.get( + "/admin/milestones/pending", + requireAdmin, + getPendingMilestones, +) + +/** + * @openapi + * /api/admin/milestones/{id}: + * get: + * tags: [Admin] + * summary: Get milestone report details and evidence + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: { type: integer } + * responses: + * 200: + * description: Milestone report with audit log + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * $ref: '#/components/responses/NotFoundError' + */ +adminMilestonesRouter.get( + "/admin/milestones/:id", + requireAdmin, + validate({ + params: milestoneReportIdParamSchema, + }), + getMilestoneById, +) + +/** + * @openapi + * /api/admin/milestones/{id}/approve: + * post: + * tags: [Admin] + * summary: Approve a milestone report and trigger contract call + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: { type: integer } + * responses: + * 200: + * description: Milestone approved + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * $ref: '#/components/responses/NotFoundError' + * 409: + * description: Report already processed + */ +adminMilestonesRouter.post( + "/admin/milestones/:id/approve", + requireAdmin, + validate({ + params: milestoneReportIdParamSchema, + body: approveMilestoneBodySchema, + }), + approveMilestone, +) + +adminMilestonesRouter.post( + "/admin/milestones/:id/reject", + requireAdmin, + validate({ + params: milestoneReportIdParamSchema, + body: rejectMilestoneBodySchema, + }), + rejectMilestone, +) + +/** + * @openapi + * /api/milestones/submit: + * post: + * tags: [Milestones] + * summary: Scholar submits a milestone report + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [scholarAddress, course_id, milestone_id] + * properties: + * scholarAddress: + * type: string + * course_id: + * type: string + * milestone_id: + * type: integer + * evidenceGitHub: + * type: string + * evidenceIpfsCid: + * type: string + * evidenceDescription: + * type: string + * responses: + * 201: + * description: Report submitted + * 400: + * $ref: '#/components/responses/BadRequestError' + * 409: + * description: Report already submitted for this milestone + * 429: + * description: Rate limit exceeded + */ +adminMilestonesRouter.post( + "/milestones/submit", + milestoneSubmissionLimiter, + validate({ + body: legacyMilestoneSubmitBodySchema, + }), + submitMilestoneReport, +) + +adminMilestonesRouter.post( + "/milestones", + milestoneSubmissionLimiter, + validate({ + body: milestoneSubmitBodySchema, + }), + submitMilestoneReport, +) + +/** + * @openapi + * /api/milestones/resubmit: + * post: + * tags: [Milestones] + * summary: Scholar resubmits a rejected milestone report + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [id] + * properties: + * id: + * type: integer + * evidenceGithub: + * type: string + * evidenceIpfsCid: + * type: string + * evidenceDescription: + * type: string + * responses: + * 200: + * description: Report resubmitted + * 400: + * $ref: '#/components/responses/BadRequestError' + * 404: + * description: Report not found + * 429: + * description: Rate limit exceeded + */ +adminMilestonesRouter.post( + "/milestones/resubmit", + milestoneSubmissionLimiter, + resubmitMilestoneReport, +) + +adminMilestonesRouter.post( + "/admin/milestones/batch-approve", + requireAdmin, + validate({ body: batchApproveMilestonesBodySchema }), + batchApproveMilestones, +) + +adminMilestonesRouter.post( + "/admin/milestones/batch-reject", + requireAdmin, + validate({ body: batchRejectMilestonesBodySchema }), + batchRejectMilestones, +) diff --git a/server/src/routes/admin.routes.ts b/server/src/routes/admin.routes.ts new file mode 100644 index 00000000..4bbf70fc --- /dev/null +++ b/server/src/routes/admin.routes.ts @@ -0,0 +1,8 @@ +import { Router } from "express" + +import { getAdminStats } from "../controllers/admin.controller" +import { requireAdmin } from "../middleware/admin.middleware" + +export const adminRouter = Router() + +adminRouter.get("/admin/stats", requireAdmin, getAdminStats) diff --git a/server/src/routes/auth.routes.ts b/server/src/routes/auth.routes.ts new file mode 100644 index 00000000..e5418b61 --- /dev/null +++ b/server/src/routes/auth.routes.ts @@ -0,0 +1,39 @@ +import { Router } from "express" + +import { createAuthControllers } from "../controllers/auth.controller" +import { nonceRateLimiter } from "../middleware/nonce-rate-limit.middleware" +import { authVerifyLimiter } from "../middleware/rate-limit.middleware" +import { type AuthService } from "../services/auth.service" + +export function createAuthRouter(authService: AuthService): Router { + const router = Router() + const { + getNonce, + postVerify, + getChallenge, + postChallengeVerify, + postLogout, + } = createAuthControllers(authService) + + router.get("/challenge", nonceRateLimiter, (req, res) => { + void getChallenge(req, res) + }) + + router.post("/challenge/verify", (req, res) => { + void postChallengeVerify(req, res) + }) + + router.get("/nonce", nonceRateLimiter, (req, res) => { + void getNonce(req, res) + }) + + router.post("/verify", authVerifyLimiter, (req, res) => { + void postVerify(req, res) + }) + + router.post("/logout", (req, res) => { + void postLogout(req, res) + }) + + return router +} diff --git a/server/src/routes/comments.routes.test.ts b/server/src/routes/comments.routes.test.ts new file mode 100644 index 00000000..2d7f1caf --- /dev/null +++ b/server/src/routes/comments.routes.test.ts @@ -0,0 +1,361 @@ +import express, { type Express } from "express" +import jwt from "jsonwebtoken" +import request from "supertest" + +// ── Mocks must be declared before any imports that use these modules ───────── + +const mockClient = { + query: jest.fn(), + release: jest.fn(), +} + +jest.mock("../db/index", () => ({ + pool: { + query: jest.fn(), + connect: jest.fn(), + }, +})) + +import { pool } from "../db/index" +import { createCommentsRouter } from "./comments.routes" + +const mockedQuery = pool.query as jest.Mock +const mockedConnect = pool.connect as jest.Mock + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +const TEST_SECRET = "learnvault-secret" +const AUTHOR = "GABC1234567890AUTHOR" +const OTHER = "GDEF9876543210OTHER" +const PROPOSAL_AUTHOR = "GPROP_AUTHOR_ADDRESS0" + +/** Generate a test Bearer token for a given address */ +const makeToken = (address: string) => + `Bearer ${jwt.sign({ sub: address }, TEST_SECRET)}` + +const testJwtService = { + signWalletToken: (address: string) => jwt.sign({ sub: address }, TEST_SECRET), + verifyWalletToken: async (token: string) => { + const decoded = jwt.verify(token, TEST_SECRET) as { + sub?: string + address?: string + } + const sub = decoded.sub ?? decoded.address ?? "" + if (!sub) throw new Error("Invalid token") + return { sub } + }, + revokeToken: jest.fn().mockResolvedValue(undefined), +} + +const buildApp = (): Express => { + const app = express() + app.use(express.json()) + app.use("/api", createCommentsRouter(testJwtService)) + return app +} + +beforeEach(() => { + jest.clearAllMocks() + mockClient.query.mockReset() + mockClient.release.mockReset() + mockedConnect.mockResolvedValue(mockClient) +}) + +// ── GET /api/proposals/:proposalId/comments ─────────────────────────────────── + +describe("GET /api/proposals/:proposalId/comments", () => { + it("returns comments for a proposal", async () => { + const rows = [ + { + id: 1, + proposal_id: "42", + content: "Great proposal!", + author_address: AUTHOR, + is_pinned: false, + created_at: new Date().toISOString(), + deleted_at: null, + }, + ] + mockedQuery.mockResolvedValueOnce({ rows }) + + const res = await request(buildApp()).get("/api/proposals/42/comments") + + expect(res.status).toBe(200) + expect(res.body).toHaveLength(1) + expect(res.body[0].proposal_id).toBe("42") + }) + + it("returns empty array when no comments exist", async () => { + mockedQuery.mockResolvedValueOnce({ rows: [] }) + + const res = await request(buildApp()).get("/api/proposals/99/comments") + + expect(res.status).toBe(200) + expect(res.body).toEqual([]) + }) + + it("returns 500 on database error", async () => { + mockedQuery.mockRejectedValueOnce(new Error("DB connection lost")) + + const res = await request(buildApp()).get("/api/proposals/1/comments") + + expect(res.status).toBe(500) + expect(res.body.error).toBe("Failed to fetch comments") + }) + + it("respects limit and offset query params", async () => { + mockedQuery.mockResolvedValueOnce({ rows: [] }) + + const res = await request(buildApp()).get( + "/api/proposals/1/comments?limit=5&offset=10", + ) + + expect(res.status).toBe(200) + expect(mockedQuery).toHaveBeenCalledWith(expect.stringContaining("LIMIT"), [ + "1", + 5, + 10, + ]) + }) + + it("caps limit at 100 even if a larger value is sent", async () => { + mockedQuery.mockResolvedValueOnce({ rows: [] }) + + await request(buildApp()).get("/api/proposals/1/comments?limit=500") + + const [, params] = mockedQuery.mock.calls[0] + expect(params[1]).toBe(100) + }) +}) + +// ── POST /api/comments ──────────────────────────────────────────────────────── + +describe("POST /api/comments", () => { + it("creates a comment and returns 201", async () => { + const newComment = { + id: 10, + proposal_id: "5", + author_address: AUTHOR, + content: "Nice idea", + parent_id: null, + is_pinned: false, + created_at: new Date().toISOString(), + } + // spam check returns count 0, then insert + mockedQuery + .mockResolvedValueOnce({ rows: [{ count: "0" }] }) + .mockResolvedValueOnce({ rows: [{ count: "0" }] }) + .mockResolvedValueOnce({ rows: [newComment] }) + + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", makeToken(AUTHOR)) + .send({ proposal_id: "5", content: "Nice idea" }) + + expect(res.status).toBe(201) + expect(res.body.content).toBe("Nice idea") + expect(res.body.author_address).toBe(AUTHOR) + }) + + it("rejects comment content over 2,000 characters", async () => { + const tooLong = "a".repeat(2001) + + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", makeToken(AUTHOR)) + .send({ proposal_id: "5", content: tooLong }) + + expect(res.status).toBe(400) + expect(res.body.error).toBe("Comment must be 2,000 characters or fewer") + expect(mockedQuery).not.toHaveBeenCalled() + }) + + it("strips HTML tags from comment content before storage", async () => { + const insertedComment = { + id: 11, + proposal_id: "5", + author_address: AUTHOR, + content: "Hello alert(1) world", + parent_id: null, + is_pinned: false, + created_at: new Date().toISOString(), + } + + mockedQuery + .mockResolvedValueOnce({ rows: [{ count: "0" }] }) + .mockResolvedValueOnce({ rows: [{ count: "0" }] }) + .mockResolvedValueOnce({ rows: [insertedComment] }) + + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", makeToken(AUTHOR)) + .send({ + proposal_id: "5", + content: "Hello world", + }) + + expect(res.status).toBe(201) + expect(mockedQuery).toHaveBeenNthCalledWith( + 3, + expect.stringContaining("INSERT INTO comments"), + expect.arrayContaining(["5", AUTHOR, "Hello world", null]), + ) + }) + + it("returns 400 when required fields are missing", async () => { + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", makeToken(AUTHOR)) + .send({}) + + expect(res.status).toBe(400) + }) + + it("returns 429 when daily comment limit is reached", async () => { + mockedQuery.mockResolvedValueOnce({ rows: [{ count: "5" }] }) + + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", makeToken(AUTHOR)) + .send({ proposal_id: "5", content: "Spam" }) + + expect(res.status).toBe(429) + expect(res.body.error).toMatch(/limit/i) + }) + + it("returns 401 without auth token", async () => { + const res = await request(buildApp()) + .post("/api/comments") + .send({ proposal_id: "5", content: "No auth" }) + + expect(res.status).toBe(401) + }) + + it("returns 400 when author_address does not match authenticated user", async () => { + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", makeToken(AUTHOR)) + .send({ + proposal_id: "5", + content: "Impersonation", + author_address: OTHER, + }) + + expect(res.status).toBe(400) + }) + + it("returns 400 when parentId is not a positive integer", async () => { + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", makeToken(AUTHOR)) + .send({ proposal_id: "5", content: "Reply", parentId: "abc" }) + + expect(res.status).toBe(400) + expect(mockedQuery).not.toHaveBeenCalled() + }) +}) + +// ── POST /api/comments/:id/pin ──────────────────────────────────────────────── + +describe("PUT /api/comments/:id/pin", () => { + it("allows the proposal author to pin a comment", async () => { + // 1. fetch comment → proposal_id = 10 + mockedQuery.mockResolvedValueOnce({ + rows: [{ proposal_id: "10" }], + rowCount: 1, + }) + // 2. fetch proposal → author = PROPOSAL_AUTHOR + mockedQuery.mockResolvedValueOnce({ + rows: [{ author_address: PROPOSAL_AUTHOR }], + rowCount: 1, + }) + // 3. unpin all + mockedQuery.mockResolvedValueOnce({ rows: [] }) + // 4. pin this one + mockedQuery.mockResolvedValueOnce({ rows: [] }) + + const res = await request(buildApp()) + .put("/api/comments/7/pin") + .set("Authorization", makeToken(PROPOSAL_AUTHOR)) + + expect(res.status).toBe(200) + expect(res.body.message).toBe("Comment pinned") + }) + + it("returns 403 when a non-author tries to pin", async () => { + // 1. fetch comment + mockedQuery.mockResolvedValueOnce({ + rows: [{ proposal_id: "10" }], + rowCount: 1, + }) + // 2. fetch proposal → different author + mockedQuery.mockResolvedValueOnce({ + rows: [{ author_address: PROPOSAL_AUTHOR }], + rowCount: 1, + }) + + const res = await request(buildApp()) + .put("/api/comments/7/pin") + .set("Authorization", makeToken(OTHER)) + + expect(res.status).toBe(403) + expect(res.body.error).toMatch(/proposal author/i) + }) + + it("returns 404 when comment does not exist", async () => { + mockedQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 }) + + const res = await request(buildApp()) + .put("/api/comments/999/pin") + .set("Authorization", makeToken(AUTHOR)) + + expect(res.status).toBe(404) + }) + + it("returns 401 without auth token", async () => { + const res = await request(buildApp()).put("/api/comments/7/pin") + + expect(res.status).toBe(401) + }) +}) + +// ── DELETE /api/comments/:id ────────────────────────────────────────────────── + +describe("DELETE /api/comments/:id", () => { + it("soft-deletes the comment when called by the author", async () => { + // check ownership returns row + mockedQuery.mockResolvedValueOnce({ + rows: [{ id: 3, author_address: AUTHOR }], + rowCount: 1, + }) + // soft-delete update + mockedQuery.mockResolvedValueOnce({ rows: [] }) + + const res = await request(buildApp()) + .delete("/api/comments/3") + .set("Authorization", makeToken(AUTHOR)) + + expect(res.status).toBe(200) + expect(res.body.success).toBe(true) + // Verify the UPDATE sets deleted_at + const updateCall = mockedQuery.mock.calls[1] + expect(updateCall[0]).toMatch(/deleted_at/i) + }) + + it("returns 404 when comment does not exist or belongs to another user", async () => { + mockedQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 }) + + const res = await request(buildApp()) + .delete("/api/comments/3") + .set("Authorization", makeToken(OTHER)) + + expect(res.status).toBe(404) + expect(res.body.error).toMatch(/not found|unauthorized/i) + }) + + it("returns 401 without auth token", async () => { + const res = await request(buildApp()).delete("/api/comments/3") + + expect(res.status).toBe(401) + }) +}) diff --git a/server/src/routes/comments.routes.ts b/server/src/routes/comments.routes.ts new file mode 100644 index 00000000..dffb96e2 --- /dev/null +++ b/server/src/routes/comments.routes.ts @@ -0,0 +1,362 @@ +import { Router, type Response } from "express" +import sanitizeHtml from "sanitize-html" +import { pool } from "../db/index" +import { createCommentBodySchema } from "../lib/zod-schemas" +import { + createRequireAuth, + type AuthRequest, +} from "../middleware/auth.middleware" +import { validate } from "../middleware/validate.middleware" +import { type JwtService } from "../services/jwt.service" +import { flagContent } from "../controllers/flag-content.controller" + +const VOTE_COLUMN: Record = { + upvote: "upvotes", + downvote: "downvotes", +} + +export function createCommentsRouter(jwtService: JwtService): Router { + const router = Router() + const requireAuth = createRequireAuth(jwtService) + const maxCommentLength = 2000 + const maxCommentsPerDay = Number.parseInt( + process.env.MAX_COMMENTS_PER_DAY ?? "50", + 10, + ) + + /** + * @openapi + * /api/proposals/{proposalId}/comments: + * get: + * summary: Fetch comments for a proposal + * tags: [Comments] + * parameters: + * - in: path + * name: proposalId + * required: true + * schema: { type: string } + * responses: + * 200: + * description: List of comments + */ + router.get("/proposals/:proposalId/comments", async (req, res) => { + const { proposalId } = req.params + const limit = Math.min(parseInt(req.query.limit as string) || 50, 100) + const offset = Math.max(parseInt(req.query.offset as string) || 0, 0) + try { + const result = await pool.query( + `SELECT * FROM comments WHERE proposal_id = $1 AND deleted_at IS NULL + AND id NOT IN (SELECT content_id FROM flagged_content WHERE content_type = 'comment' AND is_hidden = TRUE) + ORDER BY is_pinned DESC, created_at ASC LIMIT $2 OFFSET $3`, + [proposalId, limit, offset], + ) + res.json(result.rows) + } catch (err) { + res.status(500).json({ error: "Failed to fetch comments" }) + } + }) + + /** + * @openapi + * /api/comments: + * post: + * summary: Post a new comment + * tags: [Comments] + * security: [{ bearerAuth: [] }] + */ + router.post( + "/comments", + requireAuth, + validate({ + body: createCommentBodySchema, + }), + async (req: AuthRequest, res: Response) => { + const body = req.body as { + proposalId?: string + proposal_id?: string + content?: string + body?: string + parentId?: number + parent_id?: number + author_address?: string + } + const proposalId = body.proposalId ?? body.proposal_id ?? "" + const content = body.content ?? body.body ?? "" + const parentId = body.parentId ?? body.parent_id + const tokenAddress = req.user?.address ?? "" + const authorAddress = body.author_address ?? tokenAddress + const safeContent = sanitizeHtml(content, { + allowedTags: [], + allowedAttributes: {}, + }) + + if (body.author_address && body.author_address !== tokenAddress) { + return res.status(400).json({ + error: "Validation failed", + message: "Validation failed", + details: [ + { + field: "author_address", + message: "author_address must match the authenticated user", + }, + ], + }) + } + + if (content.length > maxCommentLength) { + return res.status(400).json({ + error: "Comment must be 2,000 characters or fewer", + }) + } + + if ( + parentId !== undefined && + (parentId === null || !Number.isInteger(parentId) || parentId <= 0) + ) { + return res.status(400).json({ + error: "parentId must be a positive integer or null", + }) + } + + try { + const globalSpamCheck = await pool.query( + `SELECT COUNT(*) FROM comments WHERE author_address = $1 AND created_at > NOW() - INTERVAL '1 day'`, + [authorAddress], + ) + if (parseInt(globalSpamCheck.rows[0].count) >= maxCommentsPerDay) { + return res + .status(429) + .json({ error: "Global daily comment limit reached" }) + } + + // Spam protection: max 5 comments per address per proposal per day + const spamCheck = await pool.query( + `SELECT COUNT(*) FROM comments WHERE author_address = $1 AND proposal_id = $2 AND created_at > NOW() - INTERVAL '1 day'`, + [authorAddress, proposalId], + ) + if (parseInt(spamCheck.rows[0].count) >= 5) { + return res + .status(429) + .json({ error: "Daily comment limit reached for this proposal" }) + } + + const result = await pool.query( + `INSERT INTO comments (proposal_id, author_address, content, parent_id) VALUES ($1, $2, $3, $4) RETURNING *`, + [proposalId, authorAddress, safeContent, parentId ?? null], + ) + res.status(201).json(result.rows[0]) + } catch (err) { + res.status(500).json({ error: "Failed to post comment" }) + } + }, + ) + + /** + * @openapi + * /api/comments/{id}: + * delete: + * summary: Delete own comment (soft delete) + * tags: [Comments] + * security: [{ bearerAuth: [] }] + */ + router.delete( + "/comments/:id", + requireAuth, + async (req: AuthRequest, res: Response) => { + const { id } = req.params + const authorAddress = req.user?.address + try { + // Check if comment exists and belongs to user (and not already deleted) + const checkResult = await pool.query( + `SELECT * FROM comments WHERE id = $1 AND author_address = $2 AND deleted_at IS NULL`, + [id, authorAddress], + ) + if (checkResult.rows.length === 0) { + return res + .status(404) + .json({ error: "Comment not found or unauthorized" }) + } + + // Soft delete: set deleted_at timestamp + await pool.query( + `UPDATE comments SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1`, + [id], + ) + res.json({ success: true }) + } catch (err) { + res.status(500).json({ error: "Failed to delete comment" }) + } + }, + ) + + /** + * @openapi + * /api/comments/{id}/vote: + * put: + * summary: Upvote or downvote a comment + * tags: [Comments] + * security: [{ bearerAuth: [] }] + */ + router.put( + "/comments/:id/vote", + requireAuth, + async (req: AuthRequest, res: Response) => { + const { id } = req.params + const { type } = req.body // 'upvote' or 'downvote' + const voterAddress = req.user?.address + + if (!VOTE_COLUMN[type]) { + return res.status(400).json({ error: "Invalid vote type" }) + } + + const col = VOTE_COLUMN[type] + const client = await pool.connect() + try { + await client.query("BEGIN") + + // Check if vote already exists + const existingVote = await client.query( + `SELECT vote_type FROM comment_votes WHERE comment_id = $1 AND voter_address = $2`, + [id, voterAddress], + ) + + if (existingVote.rows.length > 0) { + if (existingVote.rows[0].vote_type === type) { + // Remove vote if clicking the same button + await client.query( + `DELETE FROM comment_votes WHERE comment_id = $1 AND voter_address = $2`, + [id, voterAddress], + ) + await client.query( + `UPDATE comments SET ${col} = ${col} - 1 WHERE id = $1`, + [id], + ) + } else { + // Change vote type + const oldType = existingVote.rows[0].vote_type + const oldCol = VOTE_COLUMN[oldType] + await client.query( + `UPDATE comment_votes SET vote_type = $1 WHERE comment_id = $2 AND voter_address = $3`, + [type, id, voterAddress], + ) + await client.query( + `UPDATE comments SET ${col} = ${col} + 1, ${oldCol} = ${oldCol} - 1 WHERE id = $1`, + [id], + ) + } + } else { + // New vote + await client.query( + `INSERT INTO comment_votes (comment_id, voter_address, vote_type) VALUES ($1, $2, $3)`, + [id, voterAddress, type], + ) + await client.query( + `UPDATE comments SET ${col} = ${col} + 1 WHERE id = $1`, + [id], + ) + } + + await client.query("COMMIT") + const updatedComment = await client.query( + `SELECT * FROM comments WHERE id = $1`, + [id], + ) + res.json(updatedComment.rows[0]) + } catch (err) { + await client.query("ROLLBACK") + res.status(500).json({ error: "Failed to vote" }) + } finally { + client.release() + } + }, + ) + + /** + * @openapi + * /api/comments/{id}/pin: + * put: + * summary: Pin a comment (proposal author only) + * tags: [Comments] + * security: [{ bearerAuth: [] }] + */ + router.put( + "/comments/:id/pin", + requireAuth, + async (req: AuthRequest, res: Response) => { + const { id } = req.params + const authorAddress = req.user?.address + try { + // Check if the user is the author of the proposal associated with this comment + // For now, we'll assume a "proposal_authors" mapping or check a proposals table + // In a real app, you'd fetch the proposal by comment.proposal_id and check its author + // MOCK: Allow anyone to pin for now if they are the "author" of the proposal (which we'll just check against a param or something) + // Actually, the user says "Proposal author can pin one comment". + // I'll need a way to verify this. + const commentRes = await pool.query( + `SELECT proposal_id FROM comments WHERE id = $1 AND deleted_at IS NULL`, + [id], + ) + if (commentRes.rows.length === 0) + return res.status(404).json({ error: "Comment not found" }) + + const proposalId = commentRes.rows[0].proposal_id + + // Verify the requesting user is the proposal author + const proposalRes = await pool.query( + `SELECT author_address FROM proposals WHERE id = $1`, + [proposalId], + ) + if (proposalRes.rows.length === 0) + return res.status(404).json({ error: "Proposal not found" }) + + const proposalAuthor = proposalRes.rows[0].author_address + if (proposalAuthor.toLowerCase() !== authorAddress?.toLowerCase()) + return res + .status(403) + .json({ error: "Only the proposal author can pin comments" }) + + // UPDATE: Reset pins for this proposal and pin this one + await pool.query( + `UPDATE comments SET is_pinned = FALSE WHERE proposal_id = $1`, + [proposalId], + ) + await pool.query(`UPDATE comments SET is_pinned = TRUE WHERE id = $1`, [ + id, + ]) + res.json({ message: "Comment pinned" }) + } catch (err) { + res.status(500).json({ error: "Failed to pin comment" }) + } + }, + ) + + /** + * @openapi + * /api/content/flag: + * post: + * summary: Flag content (comment or proposal) for moderation + * tags: [Comments] + * security: [{ bearerAuth: [] }] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [contentType, contentId, reason] + * properties: + * contentType: + * type: string + * enum: [comment, proposal] + * contentId: + * type: integer + * reason: + * type: string + * responses: + * 201: + * description: Content flagged successfully + */ + router.post("/content/flag", requireAuth, flagContent) + + return router +} diff --git a/server/src/routes/community.routes.ts b/server/src/routes/community.routes.ts new file mode 100644 index 00000000..091791b1 --- /dev/null +++ b/server/src/routes/community.routes.ts @@ -0,0 +1,9 @@ +import { Router } from "express" +import { getEvents, createEvent } from "../controllers/community.controller" + +const router = Router() + +router.get("/events", getEvents) +router.post("/events", createEvent) + +export { router as communityRouter } diff --git a/server/src/routes/courses.routes.ts b/server/src/routes/courses.routes.ts new file mode 100644 index 00000000..8f156f8e --- /dev/null +++ b/server/src/routes/courses.routes.ts @@ -0,0 +1,269 @@ +import { Router } from "express" + +import { + createCourse, + getCourse, + getCourseLessonById, + getCourses, + updateCourse, +} from "../controllers/courses.controller" +import { + requireCourseAdmin, + requireCourseAdminIfRequested, +} from "../middleware/course-admin.middleware" + +export const coursesRouter = Router() + +/** + * @openapi + * /api/courses: + * get: + * tags: [Courses] + * summary: List published courses + * description: Returns a paginated list of published courses, optionally filtered by track and difficulty. + * parameters: + * - in: query + * name: track + * schema: + * type: string + * description: Filter by course track (case-insensitive) + * - in: query + * name: difficulty + * schema: + * type: string + * enum: [beginner, intermediate, advanced] + * description: Filter by difficulty level + * - in: query + * name: search + * schema: + * type: string + * description: Case-insensitive search across course title and description + * - in: query + * name: page + * schema: + * type: integer + * minimum: 1 + * default: 1 + * description: Page number + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * maximum: 50 + * default: 12 + * description: Number of courses per page + * responses: + * 200: + * description: Paginated list of courses + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * $ref: '#/components/schemas/CourseDetail' + * page: + * type: integer + * limit: + * type: integer + * total: + * type: integer + * totalPages: + * type: integer + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +coursesRouter.get("/courses", requireCourseAdminIfRequested, getCourses) +/** + * @openapi + * /api/courses/{idOrSlug}: + * get: + * tags: [Courses] + * summary: Get a single course with lessons + * description: Returns course details and all associated lessons by numeric ID or slug. + * parameters: + * - in: path + * name: idOrSlug + * required: true + * schema: + * type: string + * description: Course numeric ID or slug + * responses: + * 200: + * description: Course details with lessons + * content: + * application/json: + * schema: + * allOf: + * - $ref: '#/components/schemas/CourseDetail' + * - type: object + * properties: + * lessons: + * type: array + * items: + * $ref: '#/components/schemas/Lesson' + * 404: + * $ref: '#/components/responses/NotFoundError' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +coursesRouter.get("/courses/:idOrSlug", getCourse) + +/** + * @openapi + * /api/courses/{idOrSlug}/lessons/{id}: + * get: + * tags: [Courses] + * summary: Get a single lesson + * description: Returns a specific lesson by ID within a published course. + * parameters: + * - in: path + * name: idOrSlug + * required: true + * schema: + * type: string + * description: Course numeric ID or slug + * - in: path + * name: id + * required: true + * schema: + * type: integer + * minimum: 1 + * description: Lesson ID + * responses: + * 200: + * description: Lesson details + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Lesson' + * 404: + * $ref: '#/components/responses/NotFoundError' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +coursesRouter.get("/courses/:idOrSlug/lessons/:id", getCourseLessonById) + +/** + * @openapi + * /api/courses: + * post: + * tags: [Courses] + * summary: Create a new course + * description: Creates an unpublished course. Requires course admin privileges. + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - title + * - slug + * - track + * - difficulty + * properties: + * title: + * type: string + * slug: + * type: string + * description: + * type: string + * coverImage: + * type: string + * nullable: true + * track: + * type: string + * difficulty: + * type: string + * enum: [beginner, intermediate, advanced] + * responses: + * 201: + * description: Course created + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CourseDetail' + * 400: + * $ref: '#/components/responses/BadRequestError' + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 409: + * description: Slug already exists + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +coursesRouter.post("/courses", requireCourseAdmin, createCourse) + +/** + * @openapi + * /api/courses/{id}: + * patch: + * tags: [Courses] + * summary: Update a course + * description: Partially updates an existing course. Requires course admin privileges. + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * minimum: 1 + * description: Course ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * slug: + * type: string + * description: + * type: string + * coverImage: + * type: string + * nullable: true + * track: + * type: string + * difficulty: + * type: string + * enum: [beginner, intermediate, advanced] + * published: + * type: boolean + * responses: + * 200: + * description: Updated course + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CourseDetail' + * 400: + * $ref: '#/components/responses/BadRequestError' + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 404: + * $ref: '#/components/responses/NotFoundError' + * 409: + * description: Slug already exists + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +coursesRouter.patch("/courses/:id", requireCourseAdmin, updateCourse) diff --git a/server/src/routes/credentials.routes.ts b/server/src/routes/credentials.routes.ts new file mode 100644 index 00000000..07ceaaad --- /dev/null +++ b/server/src/routes/credentials.routes.ts @@ -0,0 +1,187 @@ +import { Router } from "express" + +import { + createCredentialMetadata, + getCredentialsByAddress, +} from "../controllers/credentials.controller" +import * as schemas from "../lib/zod-schemas" +import { createRequireAuth } from "../middleware/auth.middleware" +import { validate } from "../middleware/validation.middleware" +import { type JwtService } from "../services/jwt.service" + +export function createCredentialsRouter(jwtService: JwtService): Router { + const credentialsRouter = Router() + const requireAuth = createRequireAuth(jwtService) + + /** + * @openapi + * /api/credentials/{address}: + * get: + * tags: [Credentials] + * summary: List credentials for a learner address + * description: Returns all ScholarNFT credentials minted for the provided Stellar address, including revoked items. + * parameters: + * - in: path + * name: address + * required: true + * schema: + * type: string + * description: Scholar's Stellar wallet address + * responses: + * 200: + * description: Credentials fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * type: object + * properties: + * token_id: + * type: integer + * example: 1 + * course_id: + * type: string + * example: "stellar-basics" + * metadata_uri: + * type: string + * nullable: true + * example: "ipfs://bafkrei..." + * minted_at: + * type: string + * format: date-time + * example: "2026-03-26T10:30:00Z" + * revoked: + * type: boolean + * example: false + * 400: + * $ref: '#/components/responses/BadRequestError' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ + credentialsRouter.get("/credentials/:address", getCredentialsByAddress) + + /** + * @openapi + * /api/credentials/metadata: + * post: + * tags: [Credentials] + * summary: Generate and upload NFT metadata for a course completion credential + * description: | + * Creates ERC-721/ERC-1155 compliant metadata for a ScholarNFT credential, + * uploads it to IPFS via Pinata, and returns the ipfs:// URI for use in + * scholar_nft.mint(). + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - course_id + * - learner_address + * - completed_at + * properties: + * course_id: + * type: string + * description: Course identifier (e.g., "stellar-basics") + * example: "stellar-basics" + * learner_address: + * type: string + * description: Stellar address of the learner + * example: "GABC123...XYZ789" + * completed_at: + * type: string + * format: date-time + * description: ISO 8601 timestamp of course completion + * example: "2026-03-26T10:30:00Z" + * responses: + * 201: + * description: Metadata created and uploaded successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + * properties: + * metadata_uri: + * type: string + * description: IPFS URI for use in scholar_nft.mint() + * example: "ipfs://bafkreiabcdef1234567890ghijklmnopqrstuvwxyz" + * gateway_url: + * type: string + * description: HTTP gateway URL for viewing metadata + * example: "https://gateway.pinata.cloud/ipfs/bafkreiabcdef1234567890ghijklmnopqrstuvwxyz" + * metadata: + * type: object + * description: The generated metadata object + * properties: + * name: + * type: string + * example: "Introduction to Stellar & Soroban — Course Completion" + * description: + * type: string + * example: "Issued to learners who complete all milestones in Introduction to Stellar & Soroban" + * image: + * type: string + * example: "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" + * attributes: + * type: array + * items: + * type: object + * properties: + * trait_type: + * type: string + * value: + * type: string + * 400: + * $ref: '#/components/responses/BadRequestError' + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 404: + * description: Course not found + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Course not found" + * message: + * type: string + * example: "No course found with id: invalid-course" + * 500: + * $ref: '#/components/responses/InternalServerError' + * 503: + * description: IPFS pinning service not configured + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Service unavailable" + * message: + * type: string + * example: "IPFS pinning service is not configured. Please contact the administrator." + */ + credentialsRouter.post( + "/credentials/metadata", + requireAuth, + validate({ + body: schemas.createCredentialMetadataBodySchema, + }), + createCredentialMetadata, + ) + + return credentialsRouter +} diff --git a/server/src/routes/enrollments.routes.ts b/server/src/routes/enrollments.routes.ts new file mode 100644 index 00000000..7c89c0f5 --- /dev/null +++ b/server/src/routes/enrollments.routes.ts @@ -0,0 +1,118 @@ +import { Router } from "express" + +import { + createEnrollment, + getEnrollments, +} from "../controllers/enrollments.controller" +import * as schemas from "../lib/zod-schemas" +import { validate } from "../middleware/validation.middleware" + +export const enrollmentsRouter = Router() + +/** + * @openapi + * /api/enrollments: + * post: + * tags: [Enrollments] + * summary: Create a new course enrollment + * description: | + * Records a course enrollment in the database after validating + * the on-chain enrollment via the CourseMilestone contract. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - learner_address + * - course_id + * - tx_hash + * properties: + * learner_address: + * type: string + * description: The learner's Stellar wallet address + * course_id: + * type: string + * description: The course identifier (e.g., "stellar-basics") + * tx_hash: + * type: string + * description: The transaction hash of the on-chain enrollment + * example: + * learner_address: "GABCD123456789..." + * course_id: "stellar-basics" + * tx_hash: "abc123def456" + * responses: + * 201: + * description: Enrollment created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * enrollment_id: + * type: integer + * enrolled_at: + * type: string + * format: date-time + * 400: + * description: Validation error or not enrolled on-chain + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 409: + * description: Already enrolled in this course + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +enrollmentsRouter.post( + "/enrollments", + validate({ + body: schemas.enrollmentBodySchema, + }), + createEnrollment, +) + +/** + * @openapi + * /api/enrollments: + * get: + * tags: [Enrollments] + * summary: Get enrollments for a learner + * description: Returns all courses a learner is enrolled in + * parameters: + * - in: query + * name: learner_address + * required: true + * schema: + * type: string + * description: The learner's Stellar wallet address + * responses: + * 200: + * description: Enrollments fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * type: object + * properties: + * enrollment_id: + * type: integer + * course_id: + * type: string + * tx_hash: + * type: string + * enrolled_at: + * type: string + * format: date-time + * 400: + * description: Missing learner_address parameter + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +enrollmentsRouter.get("/enrollments", getEnrollments) diff --git a/server/src/routes/events.routes.ts b/server/src/routes/events.routes.ts new file mode 100644 index 00000000..cbb4b17d --- /dev/null +++ b/server/src/routes/events.routes.ts @@ -0,0 +1,58 @@ +import { Router } from "express" + +import { getEvents } from "../controllers/events.controller" + +export const eventsRouter = Router() + +/** + * @openapi + * /api/events: + * get: + * tags: [Events] + * summary: List indexed on-chain events + * parameters: + * - in: query + * name: contract + * schema: + * type: string + * description: Contract ID filter + * - in: query + * name: type + * schema: + * type: string + * description: Event topic (LearnToken::Mint etc) + * - in: query + * name: address + * schema: + * type: string + * description: Address in event data + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * maximum: 100 + * default: 20 + * responses: + * 200: + * description: Success + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * type: object + * properties: + * id: { type: integer } + * contract: { type: string } + * event_type: { type: string } + * data: { type: object } + * ledger_sequence: { type: string } + * created_at: { type: string, format: date-time } + * 500: + * description: Internal error + */ +eventsRouter.get("/events", getEvents) diff --git a/server/src/routes/forum.routes.ts b/server/src/routes/forum.routes.ts new file mode 100644 index 00000000..8a918254 --- /dev/null +++ b/server/src/routes/forum.routes.ts @@ -0,0 +1,41 @@ +import { Router } from "express" + +import { + createForumThread, + deleteForumReply, + deleteForumThread, + getForumThread, + listForumThreads, + replyToForumThread, +} from "../controllers/forum.controller" +import { createRequireAuth } from "../middleware/auth.middleware" +import { requireCourseAdmin } from "../middleware/course-admin.middleware" +import { type JwtService } from "../services/jwt.service" + +export function createForumRouter(jwtService: JwtService): Router { + const router = Router() + const requireAuth = createRequireAuth(jwtService) + + router.get("/courses/:idOrSlug/forum", listForumThreads) + router.get("/courses/:idOrSlug/forum/:threadId", getForumThread) + + router.post("/courses/:idOrSlug/forum", requireAuth, createForumThread) + router.post( + "/courses/:idOrSlug/forum/:threadId/replies", + requireAuth, + replyToForumThread, + ) + + router.delete( + "/courses/:idOrSlug/forum/:threadId", + requireCourseAdmin, + deleteForumThread, + ) + router.delete( + "/courses/:idOrSlug/forum/replies/:replyId", + requireCourseAdmin, + deleteForumReply, + ) + + return router +} diff --git a/server/src/routes/governance.routes.ts b/server/src/routes/governance.routes.ts new file mode 100644 index 00000000..d4e311d4 --- /dev/null +++ b/server/src/routes/governance.routes.ts @@ -0,0 +1,213 @@ +import { Router } from "express" + +import { + cancelProposal, + castVote, + createGovernanceProposal, + getDelegation, + getProposalStatus, + getGovernanceProposalById, + getGovernanceProposals, + getVotingPower, +} from "../controllers/governance.controller" +import { requireAdmin } from "../middleware/admin.middleware" + +export const governanceRouter = Router() + +/** + * @openapi + * /api/governance/proposals: + * get: + * tags: [Governance] + * summary: List governance proposals + * description: Returns a paginated list of governance proposals, optionally filtered by status. + * parameters: + * - in: query + * name: status + * schema: + * type: string + * enum: [pending, approved, rejected] + * description: Filter proposals by status + * - in: query + * name: page + * schema: + * type: integer + * minimum: 1 + * default: 1 + * description: Page number + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * maximum: 100 + * default: 20 + * description: Number of proposals per page + * responses: + * 200: + * description: Paginated list of proposals + * content: + * application/json: + * schema: + * type: object + * properties: + * proposals: + * type: array + * items: + * $ref: '#/components/schemas/Proposal' + * total: + * type: integer + * page: + * type: integer + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +governanceRouter.get("/governance/proposals", (req, res) => { + void getGovernanceProposals(req, res) +}) + +governanceRouter.get("/proposals", (req, res) => { + void getGovernanceProposals(req, res) +}) + +/** + * @openapi + * /api/governance/proposals: + * post: + * tags: [Governance] + * summary: Create a governance proposal + * description: | + * Submits a new governance proposal on-chain via the ScholarshipTreasury contract + * and records it in the database. Generates a 3-milestone program automatically. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/GovernanceProposalInput' + * example: + * author_address: "GABCD123456789..." + * title: "Fund blockchain education program" + * description: "A comprehensive program to teach blockchain development fundamentals." + * requested_amount: "1000" + * evidence_url: "https://github.com/example/proposal" + * responses: + * 201: + * description: Proposal created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/GovernanceProposalCreated' + * 400: + * $ref: '#/components/responses/BadRequestError' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +governanceRouter.post("/governance/proposals", (req, res) => { + void createGovernanceProposal(req, res) +}) + +governanceRouter.post("/proposals", (req, res) => { + void createGovernanceProposal(req, res) +}) + +governanceRouter.get("/governance/proposals/:id", (req, res) => { + void getGovernanceProposalById(req, res) +}) + +governanceRouter.get("/proposals/:id", (req, res) => { + void getGovernanceProposalById(req, res) +}) + +/** + * @openapi + * /api/governance/voting-power/{address}: + * get: + * tags: [Governance] + * summary: Get voting power for an address + * description: Returns the governance token balance and voting eligibility for the given Stellar address. + * parameters: + * - in: path + * name: address + * required: true + * schema: + * type: string + * minLength: 50 + * description: Stellar wallet address + * responses: + * 200: + * description: Voting power details + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/VotingPower' + * 400: + * $ref: '#/components/responses/BadRequestError' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +governanceRouter.get("/governance/voting-power/:address", (req, res) => { + void getVotingPower(req, res) +}) + +governanceRouter.get("/governance/delegation/:address", (req, res) => { + void getDelegation(req, res) +}) + +governanceRouter.post("/governance/vote", (req, res) => { + void castVote(req, res) +}) + +/** + * @openapi + * /api/proposals/{id}/status: + * get: + * tags: [Governance] + * summary: Get the current public state of a proposal + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Proposal state details + * 400: + * $ref: '#/components/responses/BadRequestError' + * 404: + * $ref: '#/components/responses/NotFoundError' + */ +governanceRouter.get("/proposals/:id/status", (req, res) => { + void getProposalStatus(req, res) +}) + +/** + * @openapi + * /api/proposals/{id}: + * delete: + * tags: [Governance] + * summary: Cancel an open proposal + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 204: + * description: Proposal cancelled + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * $ref: '#/components/responses/NotFoundError' + * 409: + * description: Proposal already closed or cancelled + */ +governanceRouter.delete("/proposals/:id", requireAdmin, (req, res) => { + void cancelProposal(req, res) +}) diff --git a/server/src/routes/health.routes.ts b/server/src/routes/health.routes.ts new file mode 100644 index 00000000..eb2cf351 --- /dev/null +++ b/server/src/routes/health.routes.ts @@ -0,0 +1,75 @@ +import { Router } from "express" + +import { getPgStatStatementsSnapshot, pool } from "../db/index" + +export const healthRouter = Router() + +/** + * @openapi + * /api/health: + * get: + * tags: [Health] + * summary: Check server health status + * responses: + * 200: + * description: Database is connected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/HealthResponse' + * 503: + * description: Database connectivity is degraded + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/HealthResponse' + */ +healthRouter.get("/health", async (_req, res) => { + const uptime = process.uptime() + const timestamp = new Date().toISOString() + + try { + // pg Pool ping: keep it lightweight + const result: any = await pool.query("SELECT 1 AS one") + const hasRow = Array.isArray(result?.rows) && result.rows.length > 0 + + if (hasRow) { + res.status(200).json({ + status: "ok", + db: "connected", + uptime, + timestamp, + }) + return + } + + console.error("[health] DB ping returned no rows") + res.status(503).json({ + status: "degraded", + db: "disconnected", + uptime, + timestamp, + }) + } catch (err) { + console.error("[health] DB ping failed:", err) + res.status(503).json({ + status: "degraded", + db: "disconnected", + uptime, + timestamp, + }) + } +}) + +healthRouter.get("/health/db/performance", async (_req, res) => { + try { + const snapshot = await getPgStatStatementsSnapshot(10) + res.status(200).json(snapshot) + } catch { + res.status(500).json({ + enabled: false, + rows: [], + error: "Failed to fetch database performance stats", + }) + } +}) diff --git a/server/src/routes/leaderboard.routes.ts b/server/src/routes/leaderboard.routes.ts new file mode 100644 index 00000000..345e7ef0 --- /dev/null +++ b/server/src/routes/leaderboard.routes.ts @@ -0,0 +1,57 @@ +import { Router } from "express" + +import { getLeaderboard } from "../controllers/leaderboard.controller" + +export const leaderboardRouter = Router() + +/** + * @openapi + * /api/leaderboard: + * get: + * tags: [Leaderboard] + * summary: List top learners + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * maximum: 50 + * default: 10 + * description: Max number of learners to return + * - in: query + * name: offset + * schema: + * type: integer + * minimum: 0 + * default: 0 + * description: Pagination offset + * responses: + * 200: + * description: Leaderboard fetched successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * type: object + * properties: + * rank: { type: "integer" } + * address: { type: "string" } + * fullAddress: { type: "string" } + * balance: { type: "string" } + * completedCourses: { type: "integer" } + * total: { type: "integer" } + * limit: { type: "integer" } + * offset: { type: "integer" } + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +leaderboardRouter.get("/leaderboard", getLeaderboard) diff --git a/server/src/routes/me.routes.ts b/server/src/routes/me.routes.ts new file mode 100644 index 00000000..0a5cf33c --- /dev/null +++ b/server/src/routes/me.routes.ts @@ -0,0 +1,14 @@ +import { Router } from "express" + +import { getMe } from "../controllers/me.controller" +import { createRequireAuth } from "../middleware/auth.middleware" +import { type JwtService } from "../services/jwt.service" + +export function createMeRouter(jwtService: JwtService): Router { + const router = Router() + const requireAuth = createRequireAuth(jwtService) + + router.get("/me", requireAuth, getMe) + + return router +} diff --git a/server/src/routes/moderation.routes.ts b/server/src/routes/moderation.routes.ts new file mode 100644 index 00000000..9b4dce2d --- /dev/null +++ b/server/src/routes/moderation.routes.ts @@ -0,0 +1,114 @@ +import { Router } from "express" +import { + listFlaggedContent, + getFlagDetails, + actionOnFlag, + getAdminModerationStats, +} from "../controllers/moderation.controller" +import { requireAdmin } from "../middleware/admin.middleware" + +export const moderationRouter = Router() + +/** + * @openapi + * /api/admin/moderation: + * get: + * tags: [Admin] + * summary: List flagged content + * description: Returns flagged content queue for moderation. Admins only. + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: status + * schema: + * type: string + * enum: [pending, reviewed, dismissed] + * description: Filter by status + * responses: + * 200: + * description: Flagged content list + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + */ +moderationRouter.get("/admin/moderation", requireAdmin, listFlaggedContent) + +/** + * @openapi + * /api/admin/moderation/{flagId}: + * get: + * tags: [Admin] + * summary: Get flag details with content and audit log + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: flagId + * required: true + * schema: { type: integer } + * responses: + * 200: + * description: Flag details + * 404: + * $ref: '#/components/responses/NotFoundError' + */ +moderationRouter.get("/admin/moderation/:flagId", requireAdmin, getFlagDetails) + +/** + * @openapi + * /api/admin/moderation/{flagId}/action: + * post: + * tags: [Admin] + * summary: Take action on flagged content + * description: Delete, dismiss, or warn for flagged content + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: flagId + * required: true + * schema: { type: integer } + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [action] + * properties: + * action: + * type: string + * enum: [delete, dismiss, warn] + * adminNotes: + * type: string + * responses: + * 200: + * description: Action taken + * 404: + * $ref: '#/components/responses/NotFoundError' + */ +moderationRouter.post( + "/admin/moderation/:flagId/action", + requireAdmin, + actionOnFlag, +) + +/** + * @openapi + * /api/admin/moderation/stats: + * get: + * tags: [Admin] + * summary: Get moderation statistics + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Moderation stats + */ +moderationRouter.get( + "/admin/moderation/stats", + requireAdmin, + getAdminModerationStats, +) diff --git a/server/src/routes/scholars.routes.ts b/server/src/routes/scholars.routes.ts new file mode 100644 index 00000000..ad969f2d --- /dev/null +++ b/server/src/routes/scholars.routes.ts @@ -0,0 +1,177 @@ +import { Router } from "express" + +import { + getScholarMilestones, + getScholarsLeaderboard, + getScholarProfile, + getScholarCredentials, + getScholarEscrowTimeouts, +} from "../controllers/scholars.controller" + +export const scholarsRouter = Router() + +/** + * @openapi + * /api/scholars/leaderboard: + * get: + * tags: [Scholars] + * summary: Get scholars leaderboard + * description: Returns a paginated ranking of scholars by LRN balance, with optional search. + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * minimum: 1 + * default: 1 + * description: Page number + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * maximum: 100 + * default: 50 + * description: Number of scholars per page + * - in: query + * name: search + * schema: + * type: string + * description: Filter scholars by wallet address (partial match) + * responses: + * 200: + * description: Paginated scholars leaderboard + * content: + * application/json: + * schema: + * type: object + * properties: + * rankings: + * type: array + * items: + * $ref: '#/components/schemas/ScholarRanking' + * total: + * type: integer + * your_rank: + * type: integer + * nullable: true + * description: Current user's rank (null if not authenticated or not ranked) + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +scholarsRouter.get("/scholars/leaderboard", (req, res) => { + void getScholarsLeaderboard(req, res) +}) + +/** + * @openapi + * /api/scholars/{address}: + * get: + * tags: [Scholars] + * summary: Get scholar profile + * description: Returns a scholar's on-chain balances, enrolled courses, milestone stats, credentials, and join date. + * parameters: + * - in: path + * name: address + * required: true + * schema: + * type: string + * description: Scholar's Stellar wallet address + * responses: + * 200: + * description: Scholar profile + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ScholarProfile' + * 400: + * $ref: '#/components/responses/BadRequestError' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +scholarsRouter.get("/scholars/:address", (req, res) => { + void getScholarProfile(req, res) +}) + +/** + * @openapi + * /api/scholars/{address}/milestones: + * get: + * tags: [Scholars] + * summary: Get milestones for a scholar + * description: Returns milestone reports for a scholar, optionally filtered by course or status. + * parameters: + * - in: path + * name: address + * required: true + * schema: + * type: string + * description: Scholar's Stellar wallet address + * - in: query + * name: course_id + * schema: + * type: string + * description: Filter milestones by course ID + * - in: query + * name: status + * schema: + * type: string + * enum: [pending, verified, rejected] + * description: Filter milestones by status + * responses: + * 200: + * description: Scholar milestones + * content: + * application/json: + * schema: + * type: object + * properties: + * milestones: + * type: array + * items: + * $ref: '#/components/schemas/ScholarMilestone' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +scholarsRouter.get("/scholars/:address/milestones", (req, res) => { + void getScholarMilestones(req, res) +}) + +/** + * @openapi + * /api/scholars/{address}/credentials: + * get: + * tags: [Scholars] + * summary: Get credentials for a scholar + * description: Returns all credentials (NFTs) earned by the scholar. + * parameters: + * - in: path + * name: address + * required: true + * schema: + * type: string + * description: Scholar's Stellar wallet address + * responses: + * 200: + * description: Scholar credentials + * content: + * application/json: + * schema: + * type: object + * properties: + * credentials: + * type: array + * items: + * $ref: '#/components/schemas/Credential' + * 400: + * $ref: '#/components/responses/BadRequestError' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +scholarsRouter.get("/scholars/:address/credentials", (req, res) => { + void getScholarCredentials(req, res) +}) + +scholarsRouter.get("/scholars/:address/escrow-timeouts", (req, res) => { + void getScholarEscrowTimeouts(req, res) +}) diff --git a/server/src/routes/scholarships.routes.ts b/server/src/routes/scholarships.routes.ts new file mode 100644 index 00000000..256a22b6 --- /dev/null +++ b/server/src/routes/scholarships.routes.ts @@ -0,0 +1,94 @@ +import { Router } from "express" + +import { + applyForScholarship, + contributeToScholarship, +} from "../controllers/scholarships.controller" +import { scholarshipApplyLimiter } from "../middleware/rate-limit.middleware" + +export const scholarshipsRouter = Router() + +/** + * @openapi + * /api/scholarships/apply: + * post: + * tags: [Scholarships] + * summary: Submit a scholarship application + * description: | + * Creates a scholarship proposal on-chain via the ScholarshipTreasury contract + * and records it in the database. Generates a 3-milestone program automatically. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ScholarshipApplication' + * example: + * applicant_address: "GABCD123456789..." + * full_name: "Jane Doe" + * course_id: "stellar-basics" + * motivation: "I want to learn blockchain development to build solutions for my community." + * evidence_url: "https://github.com/janedoe/portfolio" + * amount: 1000 + * responses: + * 201: + * description: Scholarship application submitted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * proposal_id: + * type: integer + * description: Database ID of the created proposal + * tx_hash: + * type: string + * description: On-chain transaction hash + * simulated: + * type: boolean + * description: Whether the transaction was simulated (no secret key configured) + * 400: + * description: Validation error + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * details: + * type: object + * description: Field-level validation errors + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +scholarshipsRouter.post( + "/scholarships/apply", + scholarshipApplyLimiter, + (req, res) => { + void applyForScholarship(req, res) + }, +) + +/** + * @openapi + * /api/scholarships/contribute: + * post: + * tags: [Scholarships] + * summary: Record a donor contribution to a scholarship + * description: Tracks a partial or full contribution from a donor to a scholarship proposal. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * proposal_id: { type: integer } + * donor_address: { type: string } + * amount: { type: number } + * tx_hash: { type: string } + */ +scholarshipsRouter.post("/scholarships/contribute", (req, res) => { + void contributeToScholarship(req, res) +}) diff --git a/server/src/routes/treasury.routes.ts b/server/src/routes/treasury.routes.ts new file mode 100644 index 00000000..78e69774 --- /dev/null +++ b/server/src/routes/treasury.routes.ts @@ -0,0 +1,117 @@ +import { Router } from "express" + +import { + getTreasuryStats, + getTreasuryActivity, +} from "../controllers/treasury.controller" + +export const treasuryRouter = Router() + +/** + * @openapi + * /api/treasury/stats: + * get: + * tags: [Treasury] + * summary: Get treasury statistics + * description: Returns aggregated statistics including total deposits, disbursements, scholars funded, active proposals, and donor count + * responses: + * 200: + * description: Treasury statistics + * content: + * application/json: + * schema: + * type: object + * properties: + * total_deposited_usdc: + * type: string + * description: Total USDC deposited (in stroops) + * example: "125400000000" + * total_disbursed_usdc: + * type: string + * description: Total USDC disbursed (in stroops) + * example: "98200000000" + * scholars_funded: + * type: integer + * description: Number of unique scholars funded + * example: 128 + * active_proposals: + * type: integer + * description: Number of active scholarship proposals + * example: 12 + * donors_count: + * type: integer + * description: Number of unique donors + * example: 47 + * 500: + * description: Internal server error + * 503: + * description: Treasury contract not configured + */ +treasuryRouter.get("/treasury/stats", getTreasuryStats) + +/** + * @openapi + * /api/treasury/activity: + * get: + * tags: [Treasury] + * summary: Get treasury activity feed + * description: Returns recent treasury events including deposits and disbursements + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * default: 20 + * minimum: 1 + * maximum: 100 + * description: Maximum number of events to return + * - in: query + * name: offset + * schema: + * type: integer + * default: 0 + * minimum: 0 + * description: Number of events to skip for pagination + * responses: + * 200: + * description: Treasury activity events + * content: + * application/json: + * schema: + * type: object + * properties: + * events: + * type: array + * items: + * type: object + * properties: + * type: + * type: string + * enum: [deposit, disburse] + * example: "deposit" + * amount: + * type: string + * description: Amount in stroops + * example: "500000000" + * address: + * type: string + * description: Donor address (for deposits) + * example: "GABC..." + * scholar: + * type: string + * description: Scholar address (for disbursements) + * example: "GDEF..." + * tx_hash: + * type: string + * description: Transaction hash + * example: "018d4d55354a1d4f6726932712954d0f5b6797a0d58478a5e89f6a9d3451d3d8" + * created_at: + * type: string + * format: date-time + * description: Event timestamp + * 500: + * description: Internal server error + * 503: + * description: Treasury contract not configured + */ +treasuryRouter.get("/treasury/activity", getTreasuryActivity) diff --git a/server/src/routes/upload.routes.ts b/server/src/routes/upload.routes.ts new file mode 100644 index 00000000..d5c7bfab --- /dev/null +++ b/server/src/routes/upload.routes.ts @@ -0,0 +1,76 @@ +import { Router } from "express" +import { pinNftMetadata, uploadFile } from "../controllers/upload.controller" +import { createRequireAuth } from "../middleware/auth.middleware" +import { upload } from "../middleware/upload.middleware" +import { type JwtService } from "../services/jwt.service" + +export function createUploadRouter(jwtService: JwtService): Router { + const router = Router() + const requireAuth = createRequireAuth(jwtService) + + /** + * @openapi + * /api/upload: + * post: + * tags: [Upload] + * summary: Pin a file to IPFS via Pinata + * description: > + * Accepts a single file (PDF, PNG, JPEG, MP4 — max 10 MB), pins it to + * IPFS via Pinata, and returns the CID and a Pinata gateway URL. + * Use this endpoint to upload proposal attachments, course cover images, + * and ScholarNFT images before referencing their CIDs elsewhere. + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: [file] + * properties: + * file: + * type: string + * format: binary + * responses: + * 201: + * description: File pinned successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * cid: + * type: string + * example: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi + * gatewayUrl: + * type: string + * example: https://gateway.pinata.cloud/ipfs/bafybei... + * 400: + * $ref: '#/components/responses/BadRequestError' + * 401: + * $ref: '#/components/responses/UnauthorizedError' + */ + router.post("/upload", requireAuth, upload.single("file"), uploadFile) + + /** + * Pin NFT metadata (JSON) to IPFS + * @openapi + * /api/upload/nft-metadata: + * post: + * tags: [Upload] + * summary: Pin NFT metadata to IPFS + * security: + * - bearerAuth: [] + * responses: + * 201: + * description: Metadata pinned successfully + * 400: + * $ref: '#/components/responses/BadRequestError' + * 401: + * $ref: '#/components/responses/UnauthorizedError' + */ + router.post("/upload/nft-metadata", requireAuth, pinNftMetadata) + + return router +} diff --git a/server/src/routes/validator.routes.ts b/server/src/routes/validator.routes.ts new file mode 100644 index 00000000..cf425b68 --- /dev/null +++ b/server/src/routes/validator.routes.ts @@ -0,0 +1,45 @@ +import { Router } from "express" +import { validateMilestone } from "../controllers/validator.controller" +import * as schemas from "../lib/zod-schemas" +import { validate } from "../middleware/validation.middleware" + +export const validatorRouter = Router() + +/** + * @openapi + * /api/validator/validate: + * post: + * tags: [Validator] + * summary: Validate a learner milestone + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ValidatorRequest' + * responses: + * 200: + * description: Milestone validation successful + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * $ref: '#/components/schemas/ValidatorResult' + * 400: + * $ref: '#/components/responses/BadRequestError' + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +validatorRouter.post( + "/validator/validate", + validate({ + body: schemas.validateMilestoneSchema, + }), + validateMilestone, +) diff --git a/server/src/routes/wiki.routes.ts b/server/src/routes/wiki.routes.ts new file mode 100644 index 00000000..defbf5d0 --- /dev/null +++ b/server/src/routes/wiki.routes.ts @@ -0,0 +1,14 @@ +import { Router } from "express" +import * as wikiController from "../controllers/wiki.controller" +import { requireAdmin } from "../middleware/admin.middleware" + +export const wikiRouter = Router() + +// Public routes +wikiRouter.get("/", wikiController.getWikiPages) +wikiRouter.get("/:slug", wikiController.getWikiPageBySlug) + +// Admin routes +wikiRouter.post("/", requireAdmin, wikiController.createWikiPage) +wikiRouter.put("/:id", requireAdmin, wikiController.updateWikiPage) +wikiRouter.delete("/:id", requireAdmin, wikiController.deleteWikiPage) diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts new file mode 100644 index 00000000..7f44cd5f --- /dev/null +++ b/server/src/services/auth.service.ts @@ -0,0 +1,172 @@ +import crypto from "node:crypto" +import { + Account, + Keypair, + Memo, + Networks, + StrKey, + Transaction, + TransactionBuilder, +} from "@stellar/stellar-sdk" + +import { type NonceStore } from "../db/nonce-store" +import { type JwtService } from "./jwt.service" + +const NONCE_MESSAGE_PREFIX = "LearnVault sign-in: " +const CHALLENGE_TTL_SECONDS = 300 + +function getNetworkPassphrase(): string { + const network = (process.env.STELLAR_NETWORK ?? "testnet").toLowerCase() + return network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET +} + +export function isValidStellarPublicKey(address: string): boolean { + try { + return StrKey.isValidEd25519PublicKey(address) + } catch { + return false + } +} + +function randomNoncePayload(): string { + const bytes = crypto.randomBytes(24) + return `${NONCE_MESSAGE_PREFIX}${bytes.toString("hex")}` +} + +export type AuthService = { + getOrCreateNonce(address: string): Promise<{ nonce: string }> + verifyAndIssueToken(address: string, signatureBase64: string): Promise + createChallenge(address: string): Promise<{ + transaction: string + networkPassphrase: string + }> + verifySignedTransaction(signedTransactionXdr: string): Promise + logout(token: string): Promise +} + +export function createAuthService( + nonceStore: NonceStore, + jwtService: JwtService, +): AuthService { + return { + async logout(token: string): Promise { + await jwtService.revokeToken(token) + }, + + async createChallenge(address: string): Promise<{ + transaction: string + networkPassphrase: string + }> { + if (!isValidStellarPublicKey(address)) { + throw new Error("Invalid Stellar public key") + } + + const nonce = crypto.randomBytes(12).toString("hex") + await nonceStore.getOrSetNonce(address, nonce, CHALLENGE_TTL_SECONDS) + + const networkPassphrase = getNetworkPassphrase() + const account = new Account(address, "0") + const challengeTx = new TransactionBuilder(account, { + fee: "100", + networkPassphrase, + }) + .addMemo(Memo.text(nonce)) + .setTimeout(CHALLENGE_TTL_SECONDS) + .build() + + return { + transaction: challengeTx.toXDR(), + networkPassphrase, + } + }, + + async verifySignedTransaction( + signedTransactionXdr: string, + ): Promise { + const networkPassphrase = getNetworkPassphrase() + let tx: Transaction + + try { + tx = new Transaction(signedTransactionXdr, networkPassphrase) + } catch { + throw new Error("Invalid signed transaction") + } + + const address = tx.source + if (!isValidStellarPublicKey(address)) { + throw new Error("Invalid Stellar public key") + } + + if (tx.sequence !== "0") { + throw new Error("Invalid challenge sequence") + } + + if (tx.memo.type !== "text" || typeof tx.memo.value !== "string") { + throw new Error("Invalid challenge memo") + } + + const nonce = tx.memo.value.trim() + if (!nonce) { + throw new Error("Invalid challenge memo") + } + + const stored = await nonceStore.getNonce(address) + if (!stored || stored !== nonce) { + throw new Error("Challenge expired or invalid") + } + + const txHash = tx.hash() + const keypair = Keypair.fromPublicKey(address) + const hasValidSignature = tx.signatures.some((signature) => + keypair.verify(txHash, signature.signature()), + ) + + if (!hasValidSignature) { + throw new Error("Invalid transaction signature") + } + + await nonceStore.deleteNonce(address) + return jwtService.signWalletToken(address) + }, + + async getOrCreateNonce(address: string): Promise<{ nonce: string }> { + if (!isValidStellarPublicKey(address)) { + throw new Error("Invalid Stellar public key") + } + + const fresh = randomNoncePayload() + const nonce = await nonceStore.getOrSetNonce(address, fresh, 300) + return { nonce } + }, + + async verifyAndIssueToken( + address: string, + signatureBase64: string, + ): Promise { + if (!isValidStellarPublicKey(address)) { + throw new Error("Invalid Stellar public key") + } + + const stored = await nonceStore.getNonce(address) + if (stored === null) { + throw new Error("Nonce expired or missing; request a new nonce") + } + + const keypair = Keypair.fromPublicKey(address) + const messageBytes = Buffer.from(stored, "utf8") + let signatureBytes: Buffer + try { + signatureBytes = Buffer.from(signatureBase64, "base64") + } catch { + throw new Error("Invalid signature encoding") + } + + if (!keypair.verify(messageBytes, signatureBytes)) { + throw new Error("Invalid signature") + } + + await nonceStore.deleteNonce(address) + return jwtService.signWalletToken(address) + }, + } +} diff --git a/server/src/services/credential.service.ts b/server/src/services/credential.service.ts new file mode 100644 index 00000000..846a8550 --- /dev/null +++ b/server/src/services/credential.service.ts @@ -0,0 +1,82 @@ +import { pool } from "../db/index" +import { milestoneStore } from "../db/milestone-store" +import { pinJsonToIPFS } from "./pinata.service" +import { + stellarContractService, + type ContractCallResult, +} from "./stellar-contract.service" + +export interface CertificateMintResult { + minted: boolean + tokenUri?: string + mintTxHash?: string | null + simulated?: boolean +} + +async function isCourseComplete( + scholarAddress: string, + courseId: string, +): Promise { + const { totalMilestones, approvedCount } = + await milestoneStore.getMilestoneProgress(scholarAddress, courseId) + return totalMilestones > 0 && approvedCount >= totalMilestones +} + +async function generateAndPinMetadata( + scholarAddress: string, + courseId: string, +): Promise { + const metadata = { + name: `LearnVault Certificate — ${courseId}`, + description: `Soulbound certificate awarded to ${scholarAddress} for completing all milestones in course ${courseId} on LearnVault.`, + image: "ipfs://bafkreihdwdcefgh4dqkjv67uzcmw7oj5ulnmrg2ibnong2tefnifowzjte", + attributes: [ + { trait_type: "Course", value: courseId }, + { trait_type: "Scholar", value: scholarAddress }, + { trait_type: "Completed At", value: new Date().toISOString() }, + ], + } + + const cid = await pinJsonToIPFS( + metadata, + `cert-${courseId}-${scholarAddress}`, + ) + return `ipfs://${cid}` +} + +async function mintCertificateIfComplete( + scholarAddress: string, + courseId: string, +): Promise { + const complete = await isCourseComplete(scholarAddress, courseId) + if (!complete) { + return { minted: false } + } + + const tokenUri = await generateAndPinMetadata(scholarAddress, courseId) + + const mintResult: ContractCallResult = + await stellarContractService.callMintScholarNFT(scholarAddress, tokenUri) + + // Store in database + if (mintResult.tokenId) { + await pool.query( + `INSERT INTO scholar_nfts (token_id, scholar_address, course_id, metadata_uri) + VALUES ($1, $2, $3, $4)`, + [mintResult.tokenId, scholarAddress, courseId, tokenUri], + ) + } + + return { + minted: true, + tokenUri, + mintTxHash: mintResult.txHash, + simulated: mintResult.simulated, + } +} + +export const credentialService = { + isCourseComplete, + generateAndPinMetadata, + mintCertificateIfComplete, +} diff --git a/server/src/services/email.service.ts b/server/src/services/email.service.ts new file mode 100644 index 00000000..1b75c263 --- /dev/null +++ b/server/src/services/email.service.ts @@ -0,0 +1,149 @@ +import { Resend } from "resend" +import { + templates, + toPlainText, + type EmailVariables, +} from "../templates/email-templates" + +export interface EmailOptions { + to: string + template: string + subject: string + data: EmailVariables +} + +export class EmailService { + private readonly from: string + private readonly resendClient?: Resend + + constructor(apiKey?: string) { + this.from = process.env.EMAIL_FROM || "notifications@learnvault.xyz" + const resendApiKey = process.env.RESEND_API_KEY || apiKey + if (resendApiKey) { + this.resendClient = new Resend(resendApiKey) + } + } + + private async render( + templateName: string, + data: EmailVariables, + ): Promise<{ html: string; text: string }> { + const templateFn = templates[templateName] + + if (!templateFn) { + console.warn(`[EmailService] Template not found: ${templateName}`) + return { html: "", text: "" } + } + + const html = templateFn(data) + const text = toPlainText(html) + + return { html, text } + } + + async sendNotification(options: EmailOptions): Promise { + const { html, text } = await this.render(options.template, options.data) + + if (!this.resendClient) { + console.log( + `[EmailService] MOCK SEND to ${options.to}: ${options.subject}`, + ) + console.log(html) + return true + } + + try { + await this.resendClient.emails.send({ + from: this.from, + to: options.to, + subject: options.subject, + html, + text, + }) + + return true + } catch (error) { + console.error("[EmailService] Error sending email:", error) + return false + } + } + + async sendAdminMilestoneNotification( + scholarName: string, + courseSlug: string, + milestoneId: string, + ): Promise { + const adminEmails = process.env.ADMIN_EMAILS + + if (!adminEmails) { + console.warn( + "[EmailService] ADMIN_EMAILS not set, skipping notification.", + ) + return false + } + + const adminLink = `${process.env.FRONTEND_URL || "http://localhost:3000"}/admin/reviews` + + const body = `New milestone submission from ${scholarName} for course ${courseSlug}, milestone ${milestoneId}. Review it here: ${adminLink}` + + const emails = adminEmails.split(",").map((email) => email.trim()) + + let allSent = true + for (const email of emails) { + const success = await this.sendNotification({ + to: email, + subject: `New Milestone Submission`, + template: "admin-alert", + data: { + body, + adminUrl: adminLink, + unsubscribeUrl: "#", + }, + }) + if (!success) allSent = false + } + + return allSent + } + + async sendAdminFlagNotification( + contentType: string, + contentId: number, + reason: string, + reporterAddress: string, + ): Promise { + const adminEmails = process.env.ADMIN_EMAILS + + if (!adminEmails) { + console.warn( + "[EmailService] ADMIN_EMAILS not set, skipping flag notification.", + ) + return false + } + + const adminLink = `${process.env.FRONTEND_URL || "http://localhost:3000"}/admin/moderation` + + const body = `New content flag: ${contentType} #${contentId} reported by ${reporterAddress}. Reason: ${reason}. Review it here: ${adminLink}` + + const emails = adminEmails.split(",").map((email) => email.trim()) + + let allSent = true + for (const email of emails) { + const success = await this.sendNotification({ + to: email, + subject: `New Content Flag - ${contentType.toUpperCase()}`, + template: "admin-alert", + data: { + body, + adminUrl: adminLink, + unsubscribeUrl: "#", + }, + }) + if (!success) allSent = false + } + + return allSent + } +} + +export const createEmailService = (apiKey?: string) => new EmailService(apiKey) diff --git a/server/src/services/escrow-timeout.service.ts b/server/src/services/escrow-timeout.service.ts new file mode 100644 index 00000000..aa73cdc8 --- /dev/null +++ b/server/src/services/escrow-timeout.service.ts @@ -0,0 +1,260 @@ +import { pool } from "../db/index" +import { createEmailService } from "./email.service" +import { stellarContractService } from "./stellar-contract.service" + +const DAY_MS = 24 * 60 * 60 * 1000 +const REMINDER_THRESHOLD_DAYS = 7 + +export type EscrowTimeoutStatus = { + proposalId: number + scholarAddress: string + courseId: string | null + daysRemaining: number + inactivityWindowDays: number + lastActivityAt: string + deadlineAt: string + reminderSentAt: string | null + status: "active" | "reclaimed" +} + +type EscrowTimeoutRow = { + proposal_id: number + scholar_address: string + scholar_email: string | null + course_id: string | null + inactivity_window_days: number + last_activity_at: Date | string + status: "active" | "reclaimed" + reminder_sent_at: Date | string | null +} + +const emailService = createEmailService( + process.env.RESEND_API_KEY || process.env.EMAIL_API_KEY || "", +) + +function toDate(value: Date | string): Date { + return value instanceof Date ? value : new Date(value) +} + +function computeDaysRemaining( + lastActivityAt: Date, + windowDays: number, +): number { + const deadline = lastActivityAt.getTime() + windowDays * DAY_MS + return Math.ceil((deadline - Date.now()) / DAY_MS) +} + +function toStatus(row: EscrowTimeoutRow): EscrowTimeoutStatus { + const lastActivity = toDate(row.last_activity_at) + const deadline = new Date( + lastActivity.getTime() + row.inactivity_window_days * DAY_MS, + ) + return { + proposalId: row.proposal_id, + scholarAddress: row.scholar_address, + courseId: row.course_id, + daysRemaining: computeDaysRemaining( + lastActivity, + row.inactivity_window_days, + ), + inactivityWindowDays: row.inactivity_window_days, + lastActivityAt: lastActivity.toISOString(), + deadlineAt: deadline.toISOString(), + reminderSentAt: row.reminder_sent_at + ? toDate(row.reminder_sent_at).toISOString() + : null, + status: row.status, + } +} + +async function insertPlatformEvent( + eventType: string, + data: Record, +): Promise { + await pool.query( + `INSERT INTO platform_events (event_type, data) VALUES ($1, $2::jsonb)`, + [eventType, JSON.stringify(data)], + ) +} + +async function notifyEscrowTimeoutWarning( + row: EscrowTimeoutRow, +): Promise { + const status = toStatus(row) + const milestoneUrl = `${process.env.FRONTEND_URL || ""}/dashboard` + + if (row.scholar_email) { + await emailService.sendNotification({ + to: row.scholar_email, + subject: "Milestone escrow timeout approaching", + template: "inactivity-reminder", + data: { + name: row.scholar_address, + milestoneTitle: row.course_id || `Proposal ${row.proposal_id}`, + milestoneUrl, + unsubscribeUrl: "#", + }, + }) + } + + await insertPlatformEvent("escrow_timeout_warning", { + proposal_id: row.proposal_id, + scholar_address: row.scholar_address, + course_id: row.course_id, + days_remaining: status.daysRemaining, + recipient_type: "scholar", + recipient_address: row.scholar_address, + }) + + const voters = await pool.query<{ voter_address: string }>( + `SELECT voter_address FROM votes WHERE proposal_id = $1`, + [row.proposal_id], + ) + + for (const vote of voters.rows) { + await insertPlatformEvent("escrow_timeout_warning", { + proposal_id: row.proposal_id, + scholar_address: row.scholar_address, + course_id: row.course_id, + days_remaining: status.daysRemaining, + recipient_type: "donor", + recipient_address: vote.voter_address, + }) + } +} + +async function reclaimExpiredEscrow(row: EscrowTimeoutRow): Promise { + const contractResult = await stellarContractService.reclaimInactiveEscrow( + row.proposal_id, + ) + + await pool.query( + `UPDATE escrow_timeouts + SET status = 'reclaimed', + reclaimed_at = NOW(), + last_check_at = NOW(), + reclaim_tx_hash = $2 + WHERE proposal_id = $1`, + [row.proposal_id, contractResult.txHash], + ) + + await insertPlatformEvent("escrow_reclaimed", { + proposal_id: row.proposal_id, + scholar_address: row.scholar_address, + course_id: row.course_id, + tx_hash: contractResult.txHash, + }) +} + +export async function trackEscrowTimeout(input: { + proposalId: number + scholarAddress: string + scholarEmail?: string | null + courseId?: string | null + inactivityWindowDays?: number +}): Promise { + await pool.query( + `INSERT INTO escrow_timeouts ( + proposal_id, + scholar_address, + scholar_email, + course_id, + inactivity_window_days, + last_activity_at + ) VALUES ($1, $2, $3, $4, $5, NOW()) + ON CONFLICT (proposal_id) + DO UPDATE + SET scholar_address = EXCLUDED.scholar_address, + scholar_email = COALESCE(EXCLUDED.scholar_email, escrow_timeouts.scholar_email), + course_id = COALESCE(EXCLUDED.course_id, escrow_timeouts.course_id), + inactivity_window_days = EXCLUDED.inactivity_window_days, + last_activity_at = NOW(), + status = 'active', + reclaimed_at = NULL, + reclaim_tx_hash = NULL`, + [ + input.proposalId, + input.scholarAddress, + input.scholarEmail ?? null, + input.courseId ?? null, + input.inactivityWindowDays ?? 30, + ], + ) +} + +export async function markEscrowActivity( + scholarAddress: string, + courseId: string, +): Promise { + await pool.query( + `UPDATE escrow_timeouts + SET last_activity_at = NOW(), + reminder_sent_at = NULL, + status = 'active' + WHERE scholar_address = $1 + AND course_id = $2 + AND status = 'active'`, + [scholarAddress, courseId], + ) +} + +export async function listEscrowTimeoutsForScholar( + scholarAddress: string, +): Promise { + const result = await pool.query( + `SELECT proposal_id, scholar_address, scholar_email, course_id, inactivity_window_days, last_activity_at, status, reminder_sent_at + FROM escrow_timeouts + WHERE scholar_address = $1 + ORDER BY last_activity_at DESC`, + [scholarAddress], + ) + + return result.rows.map(toStatus) +} + +export async function processEscrowTimeouts(): Promise { + let result + try { + result = await pool.query( + `SELECT proposal_id, scholar_address, scholar_email, course_id, inactivity_window_days, last_activity_at, status, reminder_sent_at + FROM escrow_timeouts + WHERE status = 'active' + ORDER BY last_activity_at ASC`, + ) + } catch (err) { + console.error("[escrow-timeout] query failed:", err) + return + } + + for (const row of result.rows) { + const status = toStatus(row) + try { + if (status.daysRemaining <= 0) { + await reclaimExpiredEscrow(row) + continue + } + + const alreadyReminded = Boolean(row.reminder_sent_at) + if (status.daysRemaining <= REMINDER_THRESHOLD_DAYS && !alreadyReminded) { + await notifyEscrowTimeoutWarning(row) + await pool.query( + `UPDATE escrow_timeouts + SET reminder_sent_at = NOW(), + last_check_at = NOW() + WHERE proposal_id = $1`, + [row.proposal_id], + ) + } else { + await pool.query( + `UPDATE escrow_timeouts SET last_check_at = NOW() WHERE proposal_id = $1`, + [row.proposal_id], + ) + } + } catch (err) { + console.error("[escrow-timeout] processing failed:", { + proposalId: row.proposal_id, + err, + }) + } + } +} diff --git a/server/src/services/event-indexer.service.ts b/server/src/services/event-indexer.service.ts new file mode 100644 index 00000000..f76b65ce --- /dev/null +++ b/server/src/services/event-indexer.service.ts @@ -0,0 +1,88 @@ +import { rpc as StellarRpc } from "@stellar/stellar-sdk" +import { Pool } from "pg" +import { + SOROBAN_RPC_URL, + INDEXER_CONFIG, + getPollingTargets, +} from "../lib/event-config" + +const pool = new Pool({ connectionString: process.env.DATABASE_URL! }) + +const rpc = new StellarRpc.Server(SOROBAN_RPC_URL) + +export interface IndexedEvent { + contract: string + event_type: string + data: Record + ledger_sequence: string // RPC returns string, DB bigint +} + +/** + * Poll and index new events from target contracts + * @param startLedger - Starting ledger (config or last indexed) + * @param endLedger - Latest ledger to check + */ +export async function indexEventsBatch( + startLedger: number, + endLedger: number, +): Promise { + const targets = getPollingTargets() + let inserted = 0 + + for (const { contractId, topics } of targets) { + for (const topic of topics) { + const filters: StellarRpc.Api.EventFilter[] = [ + { + type: "contract", + contractIds: [contractId], + topics: [[topic]], + }, + ] + + try { + const response = await rpc.getEvents({ + filters, + startLedger, + endLedger, + limit: 200, + }) + + for (const ev of response.events) { + const ledger = Number(ev.ledger) + if (ledger > endLedger) continue + + // Check idempotency + const exists = await pool.query( + "SELECT 1 FROM events WHERE contract = $1 AND ledger_sequence = $2", + [contractId, ledger], + ) + if ((exists.rowCount ?? 0) > 0) continue + + const data = { id: ev.id, type: ev.type, ledger: ev.ledger } + + await pool.query( + `INSERT INTO events (contract, event_type, data, ledger_sequence) + VALUES ($1, $2, $3, $4)`, + [contractId, topic, data, ledger], + ) + inserted++ + } + } catch (err) { + console.error(`[indexer:${contractId}:${topic}] Error:`, err) + } + } + } + + console.log( + `[indexer] Inserted ${inserted} events from ${startLedger}-${endLedger}`, + ) +} + +// Get last indexed ledger per contract (for resuming) +export async function getLastIndexedLedger(contract: string): Promise { + const res = await pool.query( + "SELECT MAX(ledger_sequence) FROM events WHERE contract = $1", + [contract], + ) + return (res.rows[0]?.max as number) || INDEXER_CONFIG.startingLedger +} diff --git a/server/src/services/jwt.service.ts b/server/src/services/jwt.service.ts new file mode 100644 index 00000000..11d0e890 --- /dev/null +++ b/server/src/services/jwt.service.ts @@ -0,0 +1,75 @@ +import crypto from "node:crypto" + +import jwt from "jsonwebtoken" + +import { type TokenStore } from "../db/token-store" + +const JWT_EXPIRY = "24h" + +function normalizePem(pem: string): string { + return pem.replace(/\\n/g, "\n").trim() +} + +/** In-memory RSA pair for local dev when JWT_* env vars are unset (not for production). */ +export function generateEphemeralDevJwtKeys(): { + privateKeyPem: string + publicKeyPem: string +} { + const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", { + modulusLength: 2048, + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" }, + }) + return { privateKeyPem: privateKey, publicKeyPem: publicKey } +} + +export type JwtService = { + signWalletToken(stellarAddress: string): string + verifyWalletToken(token: string): Promise<{ sub: string }> + revokeToken(token: string): Promise +} + +export function createJwtService( + privateKeyPem: string, + publicKeyPem: string, + tokenStore: TokenStore, +): JwtService { + const privateKey = normalizePem(privateKeyPem) + const publicKey = normalizePem(publicKeyPem) + + return { + signWalletToken(stellarAddress: string): string { + return jwt.sign({ sub: stellarAddress }, privateKey, { + algorithm: "RS256", + expiresIn: JWT_EXPIRY, + }) + }, + + async verifyWalletToken(token: string): Promise<{ sub: string }> { + const isRevoked = await tokenStore.isRevoked(token) + if (isRevoked) { + throw new Error("Token has been revoked") + } + + const decoded = jwt.verify(token, publicKey, { + algorithms: ["RS256"], + }) as { sub?: string } + + if (typeof decoded.sub !== "string" || decoded.sub.length === 0) { + throw new Error("Invalid token payload") + } + + return { sub: decoded.sub } + }, + + async revokeToken(token: string): Promise { + const decoded = jwt.decode(token) as { exp?: number } + const nowSeconds = Math.floor(Date.now() / 1000) + const ttl = (decoded?.exp ?? nowSeconds + 86400) - nowSeconds + + if (ttl > 0) { + await tokenStore.revoke(token, ttl) + } + }, + } +} diff --git a/server/src/services/pinata.service.ts b/server/src/services/pinata.service.ts new file mode 100644 index 00000000..c0dff317 --- /dev/null +++ b/server/src/services/pinata.service.ts @@ -0,0 +1,91 @@ +import { Readable } from "stream" +import PinataClient from "@pinata/sdk" + +// --------------------------------------------------------------------------- +// Client +// --------------------------------------------------------------------------- + +function createClient(): PinataClient | null { + const apiKey = process.env.PINATA_API_KEY + const secret = process.env.PINATA_SECRET + if (!apiKey || !secret) return null + return new PinataClient(apiKey, secret) +} + +// Lazily created so the service can be imported even when env vars are absent +// (e.g. in tests that stub the module). +let _client: PinataClient | null | undefined +function getClient(): PinataClient { + if (_client === undefined) _client = createClient() + if (!_client) { + // Allow tests to proceed without Pinata configuration + if ( + process.env.NODE_ENV === "test" || + process.env.JWT_SECRET === "learnvault-secret" + ) { + throw new Error("Pinata not configured for test - this should be mocked") + } + throw new Error( + "Pinata is not configured. Set PINATA_API_KEY and PINATA_SECRET in server/.env", + ) + } + return _client +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Pin a file buffer to IPFS via Pinata. + * Returns the IPFS CIDv1. + */ +export async function pinFileToIPFS( + buffer: Buffer, + filename: string, +): Promise { + const client = getClient() + + // @pinata/sdk requires a Readable stream with a `path` property so it can + // infer the filename when building the multipart request. + const stream = Readable.from(buffer) as Readable & { path: string } + stream.path = filename + + const result = await client.pinFileToIPFS(stream, { + pinataMetadata: { name: filename }, + pinataOptions: { cidVersion: 1 }, + }) + + return result.IpfsHash +} + +/** + * Pin a JSON object to IPFS via Pinata. + * Intended for ScholarNFT metadata conforming to ERC-721 / ERC-1155 metadata + * standard (name, description, image, attributes). + * Returns the IPFS CIDv1. + */ +export async function pinJsonToIPFS( + json: Record, + name: string, +): Promise { + const client = getClient() + + const result = await client.pinJSONToIPFS(json, { + pinataMetadata: { name }, + pinataOptions: { cidVersion: 1 }, + }) + + return result.IpfsHash +} + +/** + * Build a public HTTP URL for a CID using the configured gateway. + * Defaults to the Pinata dedicated gateway; override with IPFS_GATEWAY_URL. + */ +export function getGatewayUrl(cid: string): string { + const base = + process.env.IPFS_GATEWAY_URL?.replace(/\/$/, "") ?? + "https://gateway.pinata.cloud/ipfs" + return `${base}/${cid}` +} diff --git a/server/src/services/stellar-contract.service.ts b/server/src/services/stellar-contract.service.ts new file mode 100644 index 00000000..65c13582 --- /dev/null +++ b/server/src/services/stellar-contract.service.ts @@ -0,0 +1,1000 @@ +/** + * Stellar contract service for triggering on-chain milestone verification. + * + * In production this calls the CourseMilestone contract via the Stellar SDK. + */ + +import { pool } from "../db/index" +import { getRequestId } from "../lib/request-context" + +const STELLAR_NETWORK = process.env.STELLAR_NETWORK ?? "testnet" +const STELLAR_SECRET_KEY = process.env.STELLAR_SECRET_KEY ?? "" +const COURSE_MILESTONE_CONTRACT_ID = + process.env.COURSE_MILESTONE_CONTRACT_ID ?? "" +const SCHOLAR_NFT_CONTRACT_ID = process.env.SCHOLAR_NFT_CONTRACT_ID ?? "" +const SCHOLARSHIP_TREASURY_CONTRACT_ID = + process.env.SCHOLARSHIP_TREASURY_CONTRACT_ID ?? "" +const MILESTONE_ESCROW_CONTRACT_ID = + process.env.MILESTONE_ESCROW_CONTRACT_ID ?? "" +const LEARN_TOKEN_CONTRACT_ID = process.env.LEARN_TOKEN_CONTRACT_ID ?? "" +const GOVERNANCE_TOKEN_CONTRACT_ID = + process.env.GOVERNANCE_TOKEN_CONTRACT_ID ?? "" + +export interface ContractCallResult { + txHash: string | null + simulated: boolean + tokenId?: number +} + +export interface ScholarshipProposalParams { + applicant: string + amount: number + programName: string + programUrl: string + programDescription: string + startDate: string + milestoneTitles: string[] + milestoneDates: string[] +} + +export interface CastVoteParams { + voter: string + proposalId: number + support: boolean +} + +export interface CancelProposalParams { + proposalId: number +} + +interface RequestTraceOptions { + requestId?: string +} + +function resolveRequestId(options?: RequestTraceOptions): string | undefined { + return options?.requestId ?? getRequestId() +} + +function buildRequestMemoValue(requestId?: string): string | null { + if (!requestId) return null + const compact = requestId.replace(/-/g, "").slice(0, 24) + if (!compact) return null + return `rid:${compact}` +} + +// --- Admin Validation Cache --- +let cachedAdminAddress: string | null = null +let lastAdminCheckTime: number = 0 +const ADMIN_CACHE_TTL = 5 * 60 * 1000 // 5 minutes in milliseconds + +async function ensureAdminRole(): Promise { + if (!STELLAR_SECRET_KEY) { + throw new Error( + "STELLAR_SECRET_KEY not configured — cannot submit on-chain transaction", + ) + } + + const { + Keypair, + Contract, + TransactionBuilder, + Networks, + BASE_FEE, + rpc, + scValToNative, + } = await import("@stellar/stellar-sdk") + + const keypair = Keypair.fromSecret(STELLAR_SECRET_KEY) + const serverPubKey = keypair.publicKey() + + // 1. Check if we have a valid cached result + if (Date.now() - lastAdminCheckTime < ADMIN_CACHE_TTL && cachedAdminAddress) { + if (serverPubKey !== cachedAdminAddress) { + throw new Error( + `Server keypair ${serverPubKey} is not the contract admin. Update STELLAR_SECRET_KEY.`, + ) + } + return + } + + // 2. Cache expired or empty: Fetch from the blockchain + const serverUrl = + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org" + const server = new rpc.Server(serverUrl) + + const account = await server.getAccount(serverPubKey) + const contract = new Contract(COURSE_MILESTONE_CONTRACT_ID) + + // Build a transaction solely to simulate the admin() getter + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: + STELLAR_NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET, + }) + .addOperation(contract.call("admin")) + .setTimeout(30) + .build() + + const simResult = await server.simulateTransaction(tx) + + if (rpc.Api.isSimulationError(simResult)) { + throw new Error(`Failed to simulate admin() check: ${simResult.error}`) + } + + if (!simResult.result || !simResult.result.retval) { + throw new Error("Contract admin() returned no value.") + } + + // 3. Update the Cache + cachedAdminAddress = scValToNative(simResult.result.retval) as string + lastAdminCheckTime = Date.now() + + // 4. Verify Authorization + if (serverPubKey !== cachedAdminAddress) { + throw new Error( + `Server keypair ${serverPubKey} is not the contract admin. Update STELLAR_SECRET_KEY.`, + ) + } +} + +async function callVerifyMilestone( + scholarAddress: string, + courseId: string, + milestoneId: number, + options: RequestTraceOptions = {}, +): Promise { + if (!STELLAR_SECRET_KEY) { + throw new Error( + "STELLAR_SECRET_KEY not configured — cannot submit on-chain transaction", + ) + } + if (!COURSE_MILESTONE_CONTRACT_ID) { + throw new Error( + "COURSE_MILESTONE_CONTRACT_ID not configured — cannot submit on-chain transaction", + ) + } + + try { + // Enforce access control before doing anything + await ensureAdminRole() + // Dynamic import so the SDK is only loaded when actually needed + const { + Keypair, + Contract, + TransactionBuilder, + Memo, + Networks, + BASE_FEE, + rpc, + xdr, + } = await import("@stellar/stellar-sdk") + + const server = new rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + + const keypair = Keypair.fromSecret(STELLAR_SECRET_KEY) + const account = await server.getAccount(keypair.publicKey()) + const contract = new Contract(COURSE_MILESTONE_CONTRACT_ID) + + const txBuilder = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: + STELLAR_NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET, + }) + const requestMemoValue = buildRequestMemoValue(resolveRequestId(options)) + if (requestMemoValue) { + txBuilder.addMemo(Memo.text(requestMemoValue)) + } + + const tx = txBuilder + .addOperation( + contract.call( + "verify_milestone", + xdr.ScVal.scvString(scholarAddress), + xdr.ScVal.scvString(courseId), + xdr.ScVal.scvU32(milestoneId), + ), + ) + .setTimeout(30) + .build() + + const prepared = await server.prepareTransaction(tx) + prepared.sign(keypair) + + const result = await server.sendTransaction(prepared) + return { txHash: result.hash, simulated: false } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + // Bubble up our specific admin error without wrapping it + if (msg.includes("is not the contract admin")) { + throw err + } + console.error("[stellar] Contract call failed:", err) + throw new Error( + "Contract call failed: " + + (err instanceof Error ? err.message : String(err)), + ) + } +} + +async function emitRejectionEvent( + scholarAddress: string, + courseId: string, + milestoneId: number, + reason: string, + options: RequestTraceOptions = {}, +): Promise { + if (!STELLAR_SECRET_KEY) { + throw new Error( + "STELLAR_SECRET_KEY not configured — cannot submit on-chain transaction", + ) + } + if (!COURSE_MILESTONE_CONTRACT_ID) { + throw new Error( + "COURSE_MILESTONE_CONTRACT_ID not configured — cannot submit on-chain transaction", + ) + } + + try { + // Enforce access control before doing anything + await ensureAdminRole() + const { + Keypair, + Contract, + TransactionBuilder, + Memo, + Networks, + BASE_FEE, + rpc, + xdr, + } = await import("@stellar/stellar-sdk") + + const server = new rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + + const keypair = Keypair.fromSecret(STELLAR_SECRET_KEY) + const account = await server.getAccount(keypair.publicKey()) + const contract = new Contract(COURSE_MILESTONE_CONTRACT_ID) + + const txBuilder = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: + STELLAR_NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET, + }) + const requestMemoValue = buildRequestMemoValue(resolveRequestId(options)) + if (requestMemoValue) { + txBuilder.addMemo(Memo.text(requestMemoValue)) + } + + const tx = txBuilder + .addOperation( + contract.call( + "reject_milestone", + xdr.ScVal.scvString(scholarAddress), + xdr.ScVal.scvString(courseId), + xdr.ScVal.scvU32(milestoneId), + xdr.ScVal.scvString(reason), + ), + ) + .setTimeout(30) + .build() + + const prepared = await server.prepareTransaction(tx) + prepared.sign(keypair) + + const result = await server.sendTransaction(prepared) + return { txHash: result.hash, simulated: false } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + // Bubble up our specific admin error without wrapping it + if (msg.includes("is not the contract admin")) { + throw err + } + console.error("[stellar] Rejection event failed:", err) + throw new Error( + "Rejection event failed: " + + (err instanceof Error ? err.message : String(err)), + ) + } +} + +async function callMintScholarNFT( + scholarAddress: string, + metadataUri: string, +): Promise { + if (!STELLAR_SECRET_KEY) { + throw new Error( + "STELLAR_SECRET_KEY not configured — cannot submit on-chain transaction", + ) + } + if (!SCHOLAR_NFT_CONTRACT_ID) { + throw new Error( + "SCHOLAR_NFT_CONTRACT_ID not configured — cannot submit on-chain transaction", + ) + } + + try { + const { + Keypair, + Contract, + TransactionBuilder, + Networks, + BASE_FEE, + rpc, + xdr, + Address, + } = await import("@stellar/stellar-sdk") + + const server = new rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + + const keypair = Keypair.fromSecret(STELLAR_SECRET_KEY) + const account = await server.getAccount(keypair.publicKey()) + const contract = new Contract(SCHOLAR_NFT_CONTRACT_ID) + + // Generate a unique token ID (simple approach: use timestamp) + const tokenId = Date.now() + + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: + STELLAR_NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET, + }) + .addOperation( + contract.call( + "mint", + new Address(scholarAddress).toScVal(), + xdr.ScVal.scvU64(new xdr.Uint64(tokenId)), + ), + ) + .setTimeout(30) + .build() + + const prepared = await server.prepareTransaction(tx) + prepared.sign(keypair) + + const result = await server.sendTransaction(prepared) + return { txHash: result.hash, simulated: false, tokenId } + } catch (err) { + console.error("[stellar] ScholarNFT mint failed:", err) + throw new Error( + `ScholarNFT mint failed: ${err instanceof Error ? err.message : String(err)}`, + ) + } +} + +/** + * Check if a learner is enrolled in a course on-chain. + */ +async function isEnrolled( + learnerAddress: string, + courseId: number, + _options: RequestTraceOptions = {}, +): Promise { + if (!COURSE_MILESTONE_CONTRACT_ID) { + console.warn( + "[stellar] COURSE_MILESTONE_CONTRACT_ID not set — simulating enrollment check", + ) + return true // In dev mode, assume enrolled + } + + try { + const { + Contract, + rpc, + xdr, + Address, + Networks, + TransactionBuilder, + Keypair, + } = await import("@stellar/stellar-sdk") + + const server = new rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + + // Get a dummy account for simulation + const dummyKeypair = Keypair.random() + const dummyAccount = await server.getAccount(dummyKeypair.publicKey()) + + const contract = new Contract(COURSE_MILESTONE_CONTRACT_ID) + + // Create address from learner address + const learnerScVal = xdr.ScVal.scvAddress( + new Address(learnerAddress).toScVal() as any, + ) + + const tx = new TransactionBuilder(dummyAccount, { + fee: "100", + networkPassphrase: + STELLAR_NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET, + }) + .addOperation( + contract.call("is_enrolled", learnerScVal, xdr.ScVal.scvU32(courseId)), + ) + .setTimeout(30) + .build() + + const simResult = await server.simulateTransaction(tx) + + if (rpc.Api.isSimulationError(simResult)) { + console.error("[stellar] is_enrolled simulation failed:", simResult.error) + return false + } + + if (simResult.result) { + const { scValToNative } = await import("@stellar/stellar-sdk") + return scValToNative(simResult.result.retval) as boolean + } + + return false + } catch (err) { + console.error("[stellar] is_enrolled check failed:", err) + return false + } +} + +async function submitScholarshipProposal( + params: ScholarshipProposalParams, + options: RequestTraceOptions = {}, +): Promise { + if (!STELLAR_SECRET_KEY) { + throw new Error( + "STELLAR_SECRET_KEY not configured — cannot submit on-chain transaction", + ) + } + if (!SCHOLARSHIP_TREASURY_CONTRACT_ID) { + throw new Error( + "SCHOLARSHIP_TREASURY_CONTRACT_ID not configured — cannot submit on-chain transaction", + ) + } + + try { + const { + Keypair, + Contract, + TransactionBuilder, + Memo, + Networks, + BASE_FEE, + rpc, + nativeToScVal, + } = await import("@stellar/stellar-sdk") + + const server = new rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + + const keypair = Keypair.fromSecret(STELLAR_SECRET_KEY) + const account = await server.getAccount(keypair.publicKey()) + const contract = new Contract(SCHOLARSHIP_TREASURY_CONTRACT_ID) + + const txBuilder = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: + STELLAR_NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET, + }) + const requestMemoValue = buildRequestMemoValue(resolveRequestId(options)) + if (requestMemoValue) { + txBuilder.addMemo(Memo.text(requestMemoValue)) + } + + const tx = txBuilder + .addOperation( + contract.call( + "submit_proposal", + nativeToScVal(params.applicant, { type: "address" }), + nativeToScVal(params.amount, { type: "i128" }), + nativeToScVal(params.programName), + nativeToScVal(params.programUrl), + nativeToScVal(params.programDescription), + nativeToScVal(params.startDate), + nativeToScVal(params.milestoneTitles), + nativeToScVal(params.milestoneDates), + ), + ) + .setTimeout(30) + .build() + + const prepared = await server.prepareTransaction(tx) + prepared.sign(keypair) + + const result = await server.sendTransaction(prepared) + + return { txHash: result.hash, proposalId: null, simulated: false } + } catch (err) { + console.error("[stellar] Scholarship proposal submission failed:", err) + throw new Error( + "Scholarship proposal submission failed: " + + (err instanceof Error ? err.message : String(err)), + ) + } +} + +async function castVote( + params: CastVoteParams, + options: RequestTraceOptions = {}, +): Promise { + if (!STELLAR_SECRET_KEY) { + throw new Error( + "STELLAR_SECRET_KEY not configured — cannot submit on-chain transaction", + ) + } + if (!SCHOLARSHIP_TREASURY_CONTRACT_ID) { + throw new Error( + "SCHOLARSHIP_TREASURY_CONTRACT_ID not configured — cannot submit on-chain transaction", + ) + } + + try { + const { + Keypair, + Contract, + TransactionBuilder, + Memo, + Networks, + BASE_FEE, + rpc, + nativeToScVal, + } = await import("@stellar/stellar-sdk") + + const server = new rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + + const keypair = Keypair.fromSecret(STELLAR_SECRET_KEY) + const account = await server.getAccount(keypair.publicKey()) + const contract = new Contract(SCHOLARSHIP_TREASURY_CONTRACT_ID) + + const txBuilder = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: + STELLAR_NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET, + }) + const requestMemoValue = buildRequestMemoValue(resolveRequestId(options)) + if (requestMemoValue) { + txBuilder.addMemo(Memo.text(requestMemoValue)) + } + + const tx = txBuilder + .addOperation( + contract.call( + "vote", + nativeToScVal(params.voter, { type: "address" }), + nativeToScVal(params.proposalId, { type: "u32" }), + nativeToScVal(params.support, { type: "bool" }), + ), + ) + .setTimeout(30) + .build() + + const prepared = await server.prepareTransaction(tx) + prepared.sign(keypair) + + const result = await server.sendTransaction(prepared) + + return { txHash: result.hash, simulated: false } + } catch (err) { + console.error("[stellar] Cast vote failed:", err) + throw new Error( + "Cast vote failed: " + (err instanceof Error ? err.message : String(err)), + ) + } +} + +async function cancelProposal( + params: CancelProposalParams, + options: RequestTraceOptions = {}, +): Promise { + if (!STELLAR_SECRET_KEY) { + throw new Error( + "STELLAR_SECRET_KEY not configured — cannot submit on-chain transaction", + ) + } + if (!SCHOLARSHIP_TREASURY_CONTRACT_ID) { + throw new Error( + "SCHOLARSHIP_TREASURY_CONTRACT_ID not configured — cannot submit on-chain transaction", + ) + } + + try { + const { + Keypair, + Contract, + TransactionBuilder, + Memo, + Networks, + BASE_FEE, + rpc, + nativeToScVal, + } = await import("@stellar/stellar-sdk") + + const server = new rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + + const keypair = Keypair.fromSecret(STELLAR_SECRET_KEY) + const account = await server.getAccount(keypair.publicKey()) + const contract = new Contract(SCHOLARSHIP_TREASURY_CONTRACT_ID) + + const txBuilder = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: + STELLAR_NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET, + }) + const requestMemoValue = buildRequestMemoValue(resolveRequestId(options)) + if (requestMemoValue) { + txBuilder.addMemo(Memo.text(requestMemoValue)) + } + + const tx = txBuilder + .addOperation( + contract.call( + "cancel_proposal", + nativeToScVal(params.proposalId, { type: "u32" }), + ), + ) + .setTimeout(30) + .build() + + const prepared = await server.prepareTransaction(tx) + prepared.sign(keypair) + + const result = await server.sendTransaction(prepared) + + return { txHash: result.hash, simulated: false } + } catch (err) { + console.error("[stellar] Cancel proposal failed:", err) + throw new Error( + "Cancel proposal failed: " + + (err instanceof Error ? err.message : String(err)), + ) + } +} + +async function reclaimInactiveEscrow( + proposalId: number, + options: RequestTraceOptions = {}, +): Promise { + if (!STELLAR_SECRET_KEY) { + throw new Error( + "STELLAR_SECRET_KEY not configured — cannot submit on-chain transaction", + ) + } + if (!MILESTONE_ESCROW_CONTRACT_ID) { + throw new Error( + "MILESTONE_ESCROW_CONTRACT_ID not configured — cannot submit on-chain transaction", + ) + } + + try { + const { + Keypair, + Contract, + TransactionBuilder, + Memo, + Networks, + BASE_FEE, + rpc, + nativeToScVal, + } = await import("@stellar/stellar-sdk") + + const server = new rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + + const keypair = Keypair.fromSecret(STELLAR_SECRET_KEY) + const account = await server.getAccount(keypair.publicKey()) + const contract = new Contract(MILESTONE_ESCROW_CONTRACT_ID) + + const txBuilder = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: + STELLAR_NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET, + }) + const requestMemoValue = buildRequestMemoValue(resolveRequestId(options)) + if (requestMemoValue) { + txBuilder.addMemo(Memo.text(requestMemoValue)) + } + + const tx = txBuilder + .addOperation( + contract.call( + "reclaim_inactive", + nativeToScVal(proposalId, { type: "u32" }), + ), + ) + .setTimeout(30) + .build() + + const prepared = await server.prepareTransaction(tx) + prepared.sign(keypair) + + const result = await server.sendTransaction(prepared) + return { txHash: result.hash, simulated: false } + } catch (err) { + console.error("[stellar] reclaim_inactive failed:", err) + throw new Error( + "reclaim_inactive failed: " + + (err instanceof Error ? err.message : String(err)), + ) + } +} + +async function getLearnTokenBalance(address: string): Promise { + if (!LEARN_TOKEN_CONTRACT_ID) { + console.warn( + "[stellar] LEARN_TOKEN_CONTRACT_ID not set — simulating balance", + ) + return "10000000000" // 1000 LRN + } + try { + const { Contract, Address } = await import("@stellar/stellar-sdk") + const server = new (await import("@stellar/stellar-sdk")).rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + const contract = new Contract(LEARN_TOKEN_CONTRACT_ID) + const tx = new (await import("@stellar/stellar-sdk")).TransactionBuilder( + new (await import("@stellar/stellar-sdk")).Account( + "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBF3UKJQ2K5RQDD", + "0", + ), + { + fee: "100", + networkPassphrase: + STELLAR_NETWORK === "mainnet" + ? (await import("@stellar/stellar-sdk")).Networks.PUBLIC + : (await import("@stellar/stellar-sdk")).Networks.TESTNET, + }, + ) + .addOperation(contract.call("balance", new Address(address).toScVal())) + .setTimeout(30) + .build() + + const simResult = await server.simulateTransaction(tx) + if ( + (await import("@stellar/stellar-sdk")).rpc.Api.isSimulationError( + simResult, + ) + ) + return "0" + const { scValToNative } = await import("@stellar/stellar-sdk") + return scValToNative(simResult.result?.retval!).toString() + } catch (err) { + console.error("[stellar] getLearnTokenBalance failed:", err) + return "0" + } +} + +async function getGovernanceTokenBalance(address: string): Promise { + if (!GOVERNANCE_TOKEN_CONTRACT_ID) { + console.warn( + "[stellar] GOVERNANCE_TOKEN_CONTRACT_ID not set — simulating balance", + ) + return "1250000000" + } + try { + const { Contract, Address } = await import("@stellar/stellar-sdk") + const server = new (await import("@stellar/stellar-sdk")).rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + const contract = new Contract(GOVERNANCE_TOKEN_CONTRACT_ID) + const tx = new (await import("@stellar/stellar-sdk")).TransactionBuilder( + new (await import("@stellar/stellar-sdk")).Account( + "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBF3UKJQ2K5RQDD", + "0", + ), + { + fee: "100", + networkPassphrase: + STELLAR_NETWORK === "mainnet" + ? (await import("@stellar/stellar-sdk")).Networks.PUBLIC + : (await import("@stellar/stellar-sdk")).Networks.TESTNET, + }, + ) + .addOperation(contract.call("balance", new Address(address).toScVal())) + .setTimeout(30) + .build() + + const simResult = await server.simulateTransaction(tx) + if ( + (await import("@stellar/stellar-sdk")).rpc.Api.isSimulationError( + simResult, + ) + ) + return "0" + const { scValToNative } = await import("@stellar/stellar-sdk") + return scValToNative(simResult.result?.retval!).toString() + } catch (err) { + console.error("[stellar] getGovernanceTokenBalance failed:", err) + return "0" + } +} + +async function getGovernanceVotingPower(address: string): Promise { + if (!GOVERNANCE_TOKEN_CONTRACT_ID) { + console.warn( + "[stellar] GOVERNANCE_TOKEN_CONTRACT_ID not set — simulating voting power", + ) + return "1250000000" + } + try { + const { Contract, Address } = await import("@stellar/stellar-sdk") + const server = new (await import("@stellar/stellar-sdk")).rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + const contract = new Contract(GOVERNANCE_TOKEN_CONTRACT_ID) + const tx = new (await import("@stellar/stellar-sdk")).TransactionBuilder( + new (await import("@stellar/stellar-sdk")).Account( + "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBF3UKJQ2K5RQDD", + "0", + ), + { + fee: "100", + networkPassphrase: + STELLAR_NETWORK === "mainnet" + ? (await import("@stellar/stellar-sdk")).Networks.PUBLIC + : (await import("@stellar/stellar-sdk")).Networks.TESTNET, + }, + ) + .addOperation( + contract.call("get_voting_power", new Address(address).toScVal()), + ) + .setTimeout(30) + .build() + + const simResult = await server.simulateTransaction(tx) + if ( + (await import("@stellar/stellar-sdk")).rpc.Api.isSimulationError( + simResult, + ) + ) + return "0" + const { scValToNative } = await import("@stellar/stellar-sdk") + return scValToNative(simResult.result?.retval!).toString() + } catch (err) { + console.error("[stellar] getGovernanceVotingPower failed:", err) + return "0" + } +} + +async function getGovernanceDelegation( + address: string, +): Promise { + if (!GOVERNANCE_TOKEN_CONTRACT_ID) return null + try { + const { Contract, Address } = await import("@stellar/stellar-sdk") + const server = new (await import("@stellar/stellar-sdk")).rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + const contract = new Contract(GOVERNANCE_TOKEN_CONTRACT_ID) + const tx = new (await import("@stellar/stellar-sdk")).TransactionBuilder( + new (await import("@stellar/stellar-sdk")).Account( + "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBF3UKJQ2K5RQDD", + "0", + ), + { + fee: "100", + networkPassphrase: + STELLAR_NETWORK === "mainnet" + ? (await import("@stellar/stellar-sdk")).Networks.PUBLIC + : (await import("@stellar/stellar-sdk")).Networks.TESTNET, + }, + ) + .addOperation( + contract.call("get_delegate", new Address(address).toScVal()), + ) + .setTimeout(30) + .build() + + const simResult = await server.simulateTransaction(tx) + if ( + (await import("@stellar/stellar-sdk")).rpc.Api.isSimulationError( + simResult, + ) + ) + return null + const { scValToNative } = await import("@stellar/stellar-sdk") + const raw = scValToNative(simResult.result?.retval!) + // Option
→ null (None) or an Address string (Some) + return typeof raw === "string" ? raw : null + } catch (err) { + console.error("[stellar] getGovernanceDelegation failed:", err) + return null + } +} + +async function getEnrolledCourses(address: string): Promise { + if (!COURSE_MILESTONE_CONTRACT_ID) { + console.warn( + "[stellar] COURSE_MILESTONE_CONTRACT_ID not set — simulating enrollments", + ) + return ["stellar-basics", "defi-101"] + } + return ["stellar-basics", "defi-101"] +} + +async function getScholarCredentials(address: string): Promise { + try { + const result = await pool.query( + `SELECT + sn.token_id, + sn.course_id, + c.title as course_title, + sn.metadata_uri, + sn.minted_at as issued_at, + sn.revoked + FROM scholar_nfts sn + LEFT JOIN courses c ON sn.course_id = c.slug + WHERE sn.scholar_address = $1 + ORDER BY sn.minted_at DESC`, + [address], + ) + + type NftRow = { + token_id: string | number + course_id: string + course_title: string | null + metadata_uri: string | null + issued_at: Date + revoked: boolean + } + return result.rows.map((row: NftRow) => ({ + token_id: Number(row.token_id), + course_id: row.course_id, + course_title: row.course_title || "Unknown Course", + issued_at: row.issued_at.toISOString(), + metadata_uri: row.metadata_uri, + revoked: row.revoked, + })) + } catch (err) { + console.error("[stellar] getScholarCredentials failed:", err) + return [] + } +} + +export const stellarContractService = { + callVerifyMilestone, + emitRejectionEvent, + callMintScholarNFT, + isEnrolled, + submitScholarshipProposal, + castVote, + cancelProposal, + reclaimInactiveEscrow, + getLearnTokenBalance, + getGovernanceTokenBalance, + getGovernanceVotingPower, + getGovernanceDelegation, + getEnrolledCourses, + getScholarCredentials, +} diff --git a/server/src/templates/email-templates.ts b/server/src/templates/email-templates.ts new file mode 100644 index 00000000..0eaf22a4 --- /dev/null +++ b/server/src/templates/email-templates.ts @@ -0,0 +1,224 @@ +export type EmailVariables = Record + +const baseLayout = (content: string, vars: EmailVariables) => ` + + + + + + +
+
+

LearnVault

+
+ ${content} + +
+ + +` + +export const templates: Record string> = { + "proposal-submitted": (vars) => + baseLayout( + ` +

Hi ${vars.name},

+

Great news! Your scholarship proposal ${vars.proposalTitle} is now live on LearnVault.

+

Donors can now see your proposal and start voting.

+

View Proposal

+

Best,
The LearnVault Team

+ `, + vars, + ), + + "proposal-approved": (vars) => + baseLayout( + ` +

Hi ${vars.name},

+

🎉 Your scholarship proposal ${vars.proposalTitle} was approved!

+

Congratulations, you can now start your learning journey and submit milestones.

+

Go to Dashboard

+

Best,
The LearnVault Team

+ `, + vars, + ), + + "proposal-rejected": (vars) => + baseLayout( + ` +

Hi ${vars.name},

+

Unfortunately, your scholarship proposal ${vars.proposalTitle} was not approved in its current form.

+

Reason: ${vars.rejectionReason}

+

You can revise and resubmit your proposal.

+

Revise Proposal

+

Best,
The LearnVault Team

+ `, + vars, + ), + + "milestone-verified": (vars) => + baseLayout( + ` +

Hi ${vars.name},

+

Milestone verified — funds released!

+

Your milestone ${vars.milestoneTitle} for the course ${vars.courseTitle} has been verified by the validator.

+

The scholarship funds have been released to your wallet.

+

View Dashboard

+

Keep up the great work!

+

Best,
The LearnVault Team

+ `, + vars, + ), + + "milestone-rejected": (vars) => + baseLayout( + ` +

Hi ${vars.name},

+

Your milestone report needs more evidence.

+

Your report for ${vars.milestoneTitle} was rejected by the validator.

+

Reason: ${vars.rejectionReason}

+

Please update your report with more evidence to get it verified.

+

Update Milestone

+

Best,
The LearnVault Team

+ `, + vars, + ), + + "inactivity-reminder": (vars) => + baseLayout( + ` +

Hi ${vars.name},

+

Action needed — update your milestone.

+

Your scholarship milestone ${vars.milestoneTitle} is approaching the inactivity timeout.

+

Please submit your progress or update the milestone within the next 7 days to avoid losing your scholarship.

+

Submit Milestone

+

Best,
The LearnVault Team

+ `, + vars, + ), + + "certificate-awarded": (vars) => + baseLayout( + ` +

Hi ${vars.name},

+

Congratulations — you earned a certificate!

+

You have completed all milestones in ${vars.courseTitle} and a ScholarNFT credential has been minted to your wallet.

+

This soulbound token is your permanent on-chain proof of completion.

+

View Certificate

+

Keep building!

+

Best,
The LearnVault Team

+ `, + vars, + ), + + "voted-on-proposal": (vars) => + baseLayout( + ` +

Hi ${vars.name},

+

You voted on ${vars.proposalTitle}!

+

Thank you for supporting this scholar. Your vote helps empower the next generation of builders on Stellar.

+

View Proposal

+

Best,
The LearnVault Team

+ `, + vars, + ), + + "admin-alert": (vars) => + baseLayout( + ` +

Attention Admin,

+

A new milestone submission requires review.

+
+

${vars.body}

+

Review in Admin Panel

+

Best,
LearnVault System

+ `, + vars, + ), + "milestone-approved-admin": (vars) => + baseLayout( + ` +

Hi ${vars.name},

+

Yayy! Your milestone has been approved!

+

Your milestone ${vars.milestoneTitle} for the course ${vars.courseTitle} has been approved by the admin.

+ +
    +
  • Milestone: ${vars.milestoneNumber}
  • +
  • Reward Earned: ${vars.reward} LRN
  • +
+ +

Keep up the great progress 🚀

+ +

View Dashboard

+ +

Best,
The LearnVault Team

+ `, + vars, + ), + + "milestone-rejected-admin": (vars) => + baseLayout( + ` +

Hi ${vars.name},

+

Your milestone was not approved

+

Your submission for ${vars.milestoneTitle} in the course ${vars.courseTitle} was reviewed by the admin and requires changes.

+ +
    +
  • Milestone: ${vars.milestoneNumber}
  • +
+ + ${ + vars.rejectionReason + ? `

Reason: ${vars.rejectionReason}

` + : "" + } + +

Please review the feedback and resubmit.

+ +

Update Milestone

+ +

Best,
The LearnVault Team

+ `, + vars, + ), + "forum-reply": (vars) => + baseLayout( + ` +

Hi ${vars.name},

+

Good news! Someone has replied to your thread ${vars.threadTitle}.

+

Reply preview:

+
+ ${vars.replyPreview} +
+

View Thread

+

Best,
The LearnVault Team

+ `, + vars, + ), +} + +/** + * Basic helper to strip HTML and provide a plain-text fallback. + */ +export const toPlainText = (html: string): string => { + return html + .replace(/]*>[\s\S]*?<\/style>/gi, "") + .replace(/<[^>]+>/g, " ") + .replace(/\s+/g, " ") + .trim() +} diff --git a/server/src/tests/admin-milestones.test.ts b/server/src/tests/admin-milestones.test.ts new file mode 100644 index 00000000..c876a195 --- /dev/null +++ b/server/src/tests/admin-milestones.test.ts @@ -0,0 +1,508 @@ +/** + * Integration tests for the admin milestone verification API. + * Uses the in-memory store so no database is required. + */ + +jest.mock("../db/index", () => ({ + pool: { + query: jest.fn(), + connect: jest.fn(), + }, +})) + +jest.mock("../services/stellar-contract.service", () => ({ + stellarContractService: { + callVerifyMilestone: jest + .fn() + .mockResolvedValue({ txHash: "test_tx_hash", simulated: false }), + emitRejectionEvent: jest + .fn() + .mockResolvedValue({ txHash: "test_tx_hash", simulated: false }), + callMintScholarNFT: jest + .fn() + .mockResolvedValue({ txHash: "test_tx_hash", simulated: false }), + }, +})) + +jest.mock("../services/email.service", () => ({ + createEmailService: jest.fn().mockReturnValue({ + sendNotification: jest.fn().mockResolvedValue(undefined), + sendAdminMilestoneNotification: jest.fn().mockResolvedValue(undefined), + }), +})) + +jest.mock("../services/escrow-timeout.service", () => ({ + markEscrowActivity: jest.fn().mockResolvedValue(undefined), +})) + +jest.mock("../services/credential.service", () => ({ + credentialService: { + mintCertificateIfComplete: jest.fn().mockResolvedValue({ minted: false }), + }, +})) + +import express from "express" +import jwt from "jsonwebtoken" +import request from "supertest" +import { inMemoryMilestoneStore } from "../db/milestone-store" +import { errorHandler } from "../middleware/error.middleware" +import { adminMilestonesRouter } from "../routes/admin-milestones.routes" +import { stellarContractService } from "../services/stellar-contract.service" + +const JWT_SECRET = "learnvault-secret" + +function makeAdminToken(address = "GADMIN123") { + return jwt.sign({ address }, JWT_SECRET, { expiresIn: "1h" }) +} + +function buildApp() { + const app = express() + app.use(express.json()) + app.use("/api", adminMilestonesRouter) + app.use(errorHandler) + return app +} + +// Reset in-memory store before each test +beforeEach(() => { + // @ts-ignore – reset private fields for test isolation + inMemoryMilestoneStore["reports"] = [] + // @ts-ignore + inMemoryMilestoneStore["auditLog"] = [] + // @ts-ignore + inMemoryMilestoneStore["reportSeq"] = 1 + // @ts-ignore + inMemoryMilestoneStore["auditSeq"] = 1 + + // Provide fake Stellar credentials so the approve/reject credential guard + // passes — the pool mock ensures no real SDK call is made. + process.env.STELLAR_SECRET_KEY = "FAKE_TEST_KEY" + process.env.COURSE_MILESTONE_CONTRACT_ID = "FAKE_TEST_CONTRACT" + process.env.FRONTEND_URL = "http://localhost:3000" + process.env.NODE_ENV = "test" +}) + +afterEach(() => { + delete process.env.STELLAR_SECRET_KEY + delete process.env.COURSE_MILESTONE_CONTRACT_ID + delete process.env.FRONTEND_URL + delete process.env.NODE_ENV +}) + +describe("POST /api/milestones/submit", () => { + it("creates a report with valid payload", async () => { + const app = buildApp() + const res = await request(app).post("/api/milestones/submit").send({ + scholarAddress: "GSCHOLAR1", + courseId: "stellar-basics", + milestoneId: 1, + evidenceDescription: "Completed all exercises", + }) + + expect(res.status).toBe(201) + expect(res.body.data.status).toBe("pending") + expect(res.body.data.scholar_address).toBe("GSCHOLAR1") + }) + + it("rejects submission with no evidence", async () => { + const app = buildApp() + const res = await request(app).post("/api/milestones/submit").send({ + scholarAddress: "GSCHOLAR1", + courseId: "stellar-basics", + milestoneId: 1, + }) + + expect(res.status).toBe(400) + }) + + it("returns 409 on duplicate submission", async () => { + const app = buildApp() + const payload = { + scholarAddress: "GSCHOLAR1", + courseId: "stellar-basics", + milestoneId: 1, + evidenceDescription: "First attempt", + } + + await request(app).post("/api/milestones/submit").send(payload) + const res = await request(app).post("/api/milestones/submit").send(payload) + + expect(res.status).toBe(409) + }) + + it("returns field-level validation errors for invalid payloads", async () => { + const app = buildApp() + const res = await request(app).post("/api/milestones/submit").send({ + scholarAddress: "", + courseId: "stellar-basics", + milestoneId: 1, + }) + + expect(res.status).toBe(400) + expect(res.body.error).toBe("Validation failed") + expect(res.body.details).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + field: "scholarAddress", + message: "scholarAddress is required", + }), + expect.objectContaining({ + field: "evidenceGithub", + }), + ]), + ) + }) +}) + +describe("POST /api/milestones", () => { + it("creates a report with the issue payload shape", async () => { + const app = buildApp() + const res = await request(app).post("/api/milestones").send({ + learner_address: "GSCHOLAR2", + course_id: "stellar-basics", + milestone_id: 2, + evidence_url: "https://example.com/evidence", + }) + + expect(res.status).toBe(201) + expect(res.body.data.scholar_address).toBe("GSCHOLAR2") + expect(res.body.data.evidence_github).toBe("https://example.com/evidence") + }) +}) + +describe("GET /api/admin/milestones/pending", () => { + it("returns 401 without token", async () => { + const app = buildApp() + const res = await request(app).get("/api/admin/milestones/pending") + expect(res.status).toBe(401) + }) + + it("returns pending reports for admin", async () => { + // Seed a report + await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + evidence_github: null, + evidence_ipfs_cid: null, + }) + + const app = buildApp() + const res = await request(app) + .get("/api/admin/milestones/pending") + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(200) + expect(res.body.data).toHaveLength(1) + expect(res.body.data[0].status).toBe("pending") + }) +}) + +describe("GET /api/admin/milestones/:id", () => { + it("returns 404 for unknown id", async () => { + const app = buildApp() + const res = await request(app) + .get("/api/admin/milestones/999") + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(404) + }) + + it("returns report with audit log", async () => { + const report = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + evidence_github: null, + evidence_ipfs_cid: null, + }) + + const app = buildApp() + const res = await request(app) + .get(`/api/admin/milestones/${report.id}`) + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(200) + expect(res.body.data.id).toBe(report.id) + expect(Array.isArray(res.body.data.auditLog)).toBe(true) + }) +}) + +describe("POST /api/admin/milestones/:id/approve", () => { + it("approves a pending report and records audit entry", async () => { + const report = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + evidence_github: null, + evidence_ipfs_cid: null, + }) + + const app = buildApp() + const res = await request(app) + .post(`/api/admin/milestones/${report.id}/approve`) + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(200) + expect(res.body.data.status).toBe("approved") + expect(res.body.data.auditEntry.decision).toBe("approved") + expect(res.body.data.auditEntry.validator_address).toBe("GADMIN123") + }) + + it("returns 409 when approving an already-approved report", async () => { + const report = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + evidence_github: null, + evidence_ipfs_cid: null, + }) + await inMemoryMilestoneStore.updateReportStatus(report.id, "approved") + + const app = buildApp() + const res = await request(app) + .post(`/api/admin/milestones/${report.id}/approve`) + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(409) + }) + + it("returns field-level validation errors when note is invalid", async () => { + const report = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + evidence_github: null, + evidence_ipfs_cid: null, + }) + + const app = buildApp() + const res = await request(app) + .post(`/api/admin/milestones/${report.id}/approve`) + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({ note: 123 }) + + expect(res.status).toBe(400) + expect(res.body.details).toEqual([ + { + field: "note", + message: "note must be a string", + }, + ]) + }) +}) + +describe("POST /api/admin/milestones/batch-approve", () => { + it("approves multiple pending reports and returns per-report results", async () => { + const reportOne = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + evidence_github: null, + evidence_ipfs_cid: null, + }) + const reportTwo = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR2", + course_id: "stellar-basics", + milestone_id: 2, + evidence_description: "Done again", + evidence_github: null, + evidence_ipfs_cid: null, + }) + + const app = buildApp() + const res = await request(app) + .post("/api/admin/milestones/batch-approve") + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({ milestoneIds: [reportOne.id, reportTwo.id] }) + + expect(res.status).toBe(200) + expect(res.body.data.succeeded).toBe(2) + expect(res.body.data.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + reportId: reportOne.id, + success: true, + status: "approved", + }), + expect.objectContaining({ + reportId: reportTwo.id, + success: true, + status: "approved", + }), + ]), + ) + }) + + it("fails validation before processing when any report is not pending", async () => { + const reportOne = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + evidence_github: null, + evidence_ipfs_cid: null, + }) + const reportTwo = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR2", + course_id: "stellar-basics", + milestone_id: 2, + evidence_description: "Done again", + evidence_github: null, + evidence_ipfs_cid: null, + }) + await inMemoryMilestoneStore.updateReportStatus(reportTwo.id, "approved") + + const app = buildApp() + const res = await request(app) + .post("/api/admin/milestones/batch-approve") + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({ milestoneIds: [reportOne.id, reportTwo.id] }) + + expect(res.status).toBe(409) + expect(res.body.data.results).toEqual([ + expect.objectContaining({ + reportId: reportTwo.id, + success: false, + status: "approved", + }), + ]) + expect(stellarContractService.callVerifyMilestone).not.toHaveBeenCalled() + }) +}) + +describe("POST /api/admin/milestones/:id/reject", () => { + it("rejects a pending report with a reason", async () => { + const report = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + evidence_github: null, + evidence_ipfs_cid: null, + }) + + const app = buildApp() + const res = await request(app) + .post(`/api/admin/milestones/${report.id}/reject`) + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({ reason: "Evidence is insufficient" }) + + expect(res.status).toBe(200) + expect(res.body.data.status).toBe("rejected") + expect(res.body.data.reason).toBe("Evidence is insufficient") + expect(res.body.data.auditEntry.rejection_reason).toBe( + "Evidence is insufficient", + ) + }) + + it("returns 400 when reason is missing", async () => { + const report = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + evidence_github: null, + evidence_ipfs_cid: null, + }) + + const app = buildApp() + const res = await request(app) + .post(`/api/admin/milestones/${report.id}/reject`) + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({}) + + expect(res.status).toBe(400) + expect(res.body.details).toEqual([ + { + field: "reason", + message: "reason is required", + }, + ]) + }) +}) + +describe("POST /api/admin/milestones/batch-reject", () => { + it("rejects multiple pending reports and returns per-report results", async () => { + const reportOne = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + evidence_github: null, + evidence_ipfs_cid: null, + }) + const reportTwo = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR2", + course_id: "stellar-basics", + milestone_id: 2, + evidence_description: "Done again", + evidence_github: null, + evidence_ipfs_cid: null, + }) + + const app = buildApp() + const res = await request(app) + .post("/api/admin/milestones/batch-reject") + .set("Authorization", `Bearer ${makeAdminToken()}`) + .send({ + milestoneIds: [reportOne.id, reportTwo.id], + reason: "Needs more evidence", + }) + + expect(res.status).toBe(200) + expect(res.body.data.succeeded).toBe(2) + expect(res.body.data.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + reportId: reportOne.id, + success: true, + status: "rejected", + reason: "Needs more evidence", + }), + expect.objectContaining({ + reportId: reportTwo.id, + success: true, + status: "rejected", + reason: "Needs more evidence", + }), + ]), + ) + }) +}) + +describe("error handling", () => { + it("returns 503 when Stellar credentials are not configured", async () => { + const report = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + evidence_github: null, + evidence_ipfs_cid: null, + }) + + // Mock the service to throw the 'not configured' error + const mockedService = + stellarContractService.callVerifyMilestone as jest.Mock + mockedService.mockRejectedValueOnce( + new Error( + "STELLAR_SECRET_KEY not configured — cannot submit on-chain transaction", + ), + ) + + const app = buildApp() + const res = await request(app) + .post(`/api/admin/milestones/${report.id}/approve`) + .set("Authorization", `Bearer ${makeAdminToken()}`) + + expect(res.status).toBe(503) + expect(res.body.error).toBe("Stellar credentials not configured") + }) +}) diff --git a/server/src/tests/adr.test.ts b/server/src/tests/adr.test.ts new file mode 100644 index 00000000..ca78b709 --- /dev/null +++ b/server/src/tests/adr.test.ts @@ -0,0 +1,23 @@ +import fs from "fs" +import path from "path" + +describe("ADR files", () => { + it("should have all 7 ADR files", () => { + const adrDir = path.join(__dirname, "../../../docs/adr") + const files = fs.readdirSync(adrDir) + + const expected = [ + "ADR-001.md", + "ADR-002.md", + "ADR-003.md", + "ADR-004.md", + "ADR-005.md", + "ADR-006.md", + "ADR-007.md", + ] + + expected.forEach((file) => { + expect(files).toContain(file) + }) + }) +}) diff --git a/server/src/tests/auth.service.test.ts b/server/src/tests/auth.service.test.ts new file mode 100644 index 00000000..eb38c273 --- /dev/null +++ b/server/src/tests/auth.service.test.ts @@ -0,0 +1,112 @@ +import jwt from "jsonwebtoken" +import { createTokenStore } from "../db/token-store" +import { + createJwtService, + generateEphemeralDevJwtKeys, +} from "../services/jwt.service" + +describe("Auth / JWT Service", () => { + const { privateKeyPem, publicKeyPem } = generateEphemeralDevJwtKeys() + const tokenStore = createTokenStore(undefined) // Use memory store + const jwtService = createJwtService(privateKeyPem, publicKeyPem, tokenStore) + const address = "GABC123..." + + describe("JWT Generation & Claims", () => { + it("generates a JWT with correct claims (sub, iat, exp)", async () => { + const token = jwtService.signWalletToken(address) + const decoded = jwt.decode(token) as any + + expect(decoded.sub).toBe(address) + expect(decoded.iat).toBeDefined() + expect(decoded.exp).toBeDefined() + // exp should be 24h after iat + expect(decoded.exp - decoded.iat).toBe(24 * 60 * 60) + }) + + it("uses RS256 algorithm", async () => { + const token = jwtService.signWalletToken(address) + const header = JSON.parse( + Buffer.from(token.split(".")[0], "base64").toString(), + ) + expect(header.alg).toBe("RS256") + }) + }) + + describe("JWT Validation", () => { + it("passes for valid tokens", async () => { + const token = jwtService.signWalletToken(address) + const result = await jwtService.verifyWalletToken(token) + expect(result.sub).toBe(address) + }) + + it("fails for expired tokens", async () => { + // Create a token that expired 1 hour ago + const expiredToken = jwt.sign( + { sub: address, exp: Math.floor(Date.now() / 1000) - 3600 }, + privateKeyPem, + { algorithm: "RS256" }, + ) + + await expect(jwtService.verifyWalletToken(expiredToken)).rejects.toThrow( + /jwt expired/i, + ) + }) + + it("fails for tampered tokens (payload changed)", async () => { + const token = jwtService.signWalletToken(address) + const [header, payload, signature] = token.split(".") + const decodedPayload = JSON.parse( + Buffer.from(payload, "base64").toString(), + ) + decodedPayload.sub = "G_EVIL_ADDRESS" + const tamperedPayload = Buffer.from(JSON.stringify(decodedPayload)) + .toString("base64url") + .replace(/=+$/, "") + const tamperedToken = `${header}.${tamperedPayload}.${signature}` + + await expect(jwtService.verifyWalletToken(tamperedToken)).rejects.toThrow( + /invalid signature|invalid token/i, + ) + }) + + it("fails when using HS256 to verify (algorithm enforcement)", async () => { + // Attempting to use a symmetric key (HS256) instead of the public key + const hs256Token = jwt.sign({ sub: address }, "some-secret", { + algorithm: "HS256", + }) + + await expect(jwtService.verifyWalletToken(hs256Token)).rejects.toThrow( + /invalid algorithm/i, + ) + }) + }) + + describe("Logout & Blocklist", () => { + it("rejects tokens after logout", async () => { + const token = jwtService.signWalletToken(address) + + // Initially valid + await expect(jwtService.verifyWalletToken(token)).resolves.toBeDefined() + + // Logout + await jwtService.revokeToken(token) + + // Now invalid + await expect(jwtService.verifyWalletToken(token)).rejects.toThrow( + /revoked/i, + ) + }) + + it("retains revocation state across multiple checks", async () => { + const token = jwtService.signWalletToken(address) + await jwtService.revokeToken(token) + + await expect(jwtService.verifyWalletToken(token)).rejects.toThrow( + /revoked/i, + ) + await expect(jwtService.verifyWalletToken(token)).rejects.toThrow( + /revoked/i, + ) + }) + }) +}) diff --git a/server/src/tests/comments.test.ts b/server/src/tests/comments.test.ts new file mode 100644 index 00000000..b0b9f23f --- /dev/null +++ b/server/src/tests/comments.test.ts @@ -0,0 +1,181 @@ +import express from "express" +import jwt from "jsonwebtoken" +import request from "supertest" +import { pool } from "../db/index" +import { errorHandler } from "../middleware/error.middleware" +import { createCommentsRouter } from "../routes/comments.routes" + +const JWT_SECRET = "learnvault-secret" + +const testJwtService = { + signWalletToken: (addr: string) => jwt.sign({ sub: addr }, JWT_SECRET), + verifyWalletToken: async (token: string) => { + const d = jwt.verify(token, JWT_SECRET) as { + sub?: string + address?: string + } + const sub = d.sub ?? d.address ?? "" + if (!sub) throw new Error("Invalid token") + return { sub } + }, + revokeToken: jest.fn().mockResolvedValue(undefined), +} + +function makeToken(address = "GUSER123") { + return jwt.sign({ address }, JWT_SECRET, { expiresIn: "1h" }) +} + +function buildApp() { + const app = express() + app.use(express.json()) + app.use("/api", createCommentsRouter(testJwtService)) + app.use(errorHandler) + return app +} + +describe("POST /api/comments", () => { + const querySpy = jest.spyOn(pool, "query") + + beforeEach(() => { + jest.clearAllMocks() + }) + + afterAll(() => { + querySpy.mockRestore() + }) + + it("returns field-level validation errors for invalid snake_case payloads", async () => { + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", `Bearer ${makeToken()}`) + .send({ + proposal_id: "proposal-1", + body: "", + author_address: "GUSER123", + }) + + expect(res.status).toBe(400) + expect(res.body.error).toBe("Validation failed") + expect(res.body.details).toEqual([ + { + field: "body", + message: "body cannot be empty", + }, + ]) + expect(querySpy).not.toHaveBeenCalled() + }) + + it("accepts the issue payload shape when the author matches the token", async () => { + querySpy + .mockResolvedValueOnce({ rows: [{ count: "0" }] } as never) + .mockResolvedValueOnce({ rows: [{ count: "0" }] } as never) + .mockResolvedValueOnce({ + rows: [ + { + id: 1, + proposal_id: "proposal-1", + author_address: "GUSER123", + content: "Nice proposal", + }, + ], + } as never) + + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", `Bearer ${makeToken()}`) + .send({ + proposal_id: "proposal-1", + body: "Nice proposal", + author_address: "GUSER123", + }) + + expect(res.status).toBe(201) + expect(res.body.author_address).toBe("GUSER123") + expect(res.body.content).toBe("Nice proposal") + }) + + it("rejects comments longer than 2,000 characters", async () => { + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", `Bearer ${makeToken()}`) + .send({ + proposal_id: "proposal-1", + body: "a".repeat(2001), + author_address: "GUSER123", + }) + + expect(res.status).toBe(400) + expect(res.body.error).toBe("Comment must be 2,000 characters or fewer") + expect(querySpy).not.toHaveBeenCalled() + }) + + it("strips HTML tags before storing comments", async () => { + querySpy + .mockResolvedValueOnce({ rows: [{ count: "0" }] } as never) + .mockResolvedValueOnce({ rows: [{ count: "0" }] } as never) + .mockResolvedValueOnce({ + rows: [ + { + id: 2, + proposal_id: "proposal-1", + author_address: "GUSER123", + content: "Hello world", + }, + ], + } as never) + + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", `Bearer ${makeToken()}`) + .send({ + proposal_id: "proposal-1", + body: "Hello world", + author_address: "GUSER123", + }) + + expect(res.status).toBe(201) + expect(querySpy).toHaveBeenCalledTimes(3) + const insertCallArgs = querySpy.mock.calls[2]?.[1] as unknown[] + expect(insertCallArgs[2]).toBe("Hello world") + }) + + it("rejects invalid parentId values", async () => { + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", `Bearer ${makeToken()}`) + .send({ + proposal_id: "proposal-1", + body: "Reply", + parent_id: 0, + author_address: "GUSER123", + }) + + expect(res.status).toBe(400) + expect(querySpy).not.toHaveBeenCalled() + }) + + it("enforces a global per-address daily comment limit", async () => { + const previousMax = process.env.MAX_COMMENTS_PER_DAY + process.env.MAX_COMMENTS_PER_DAY = "1" + + querySpy.mockResolvedValueOnce({ rows: [{ count: "1" }] } as never) + + const res = await request(buildApp()) + .post("/api/comments") + .set("Authorization", `Bearer ${makeToken()}`) + .send({ + proposal_id: "proposal-1", + body: "Another comment", + author_address: "GUSER123", + }) + + expect(res.status).toBe(429) + expect(res.body.error).toBe("Global daily comment limit reached") + + if (previousMax === undefined) { + delete process.env.MAX_COMMENTS_PER_DAY + } else { + process.env.MAX_COMMENTS_PER_DAY = previousMax + } + }) +}) diff --git a/server/src/tests/courses-api.test.ts b/server/src/tests/courses-api.test.ts new file mode 100644 index 00000000..f5b91f03 --- /dev/null +++ b/server/src/tests/courses-api.test.ts @@ -0,0 +1,370 @@ +process.env.JWT_SECRET = "learnvault-secret" + +jest.mock("../db/index", () => ({ + pool: { + query: jest.fn(), + }, +})) + +import express from "express" +import jwt from "jsonwebtoken" +import request from "supertest" +import { pool } from "../db/index" +import { errorHandler } from "../middleware/error.middleware" +import { coursesRouter } from "../routes/courses.routes" + +const mockedQuery = pool.query as jest.Mock +const JWT_SECRET = "learnvault-secret" + +const adminToken = jwt.sign({ sub: "GADMIN", role: "admin" }, JWT_SECRET, { + expiresIn: "1h", +}) +const nonAdminToken = jwt.sign({ sub: "GUSER" }, JWT_SECRET, { + expiresIn: "1h", +}) + +function buildApp() { + const app = express() + app.use(express.json()) + app.use("/api", coursesRouter) + app.use(errorHandler) + return app +} + +beforeEach(() => { + mockedQuery.mockReset() + delete process.env.ADMIN_API_KEY +}) + +describe("GET /api/courses", () => { + it("returns published courses only with pagination payload", async () => { + mockedQuery + .mockResolvedValueOnce({ rows: [{ count: "1" }] }) + .mockResolvedValueOnce({ + rows: [ + { + id: 1, + slug: "stellar-basics", + title: "Stellar Basics", + description: "Basics", + cover_image_url: null, + track: "web3", + difficulty: "beginner", + published_at: "2026-01-01T00:00:00.000Z", + created_at: "2026-01-01T00:00:00.000Z", + updated_at: "2026-01-02T00:00:00.000Z", + }, + ], + }) + + const res = await request(buildApp()).get("/api/courses") + expect(res.status).toBe(200) + expect(res.body.total).toBe(1) + expect(res.body.totalPages).toBe(1) + expect(res.body.data).toHaveLength(1) + expect(res.body.data[0].published).toBe(true) + }) + + it("applies track and difficulty filters together", async () => { + mockedQuery + .mockResolvedValueOnce({ rows: [{ count: "0" }] }) + .mockResolvedValueOnce({ + rows: [], + }) + + const res = await request(buildApp()).get( + "/api/courses?track=web3&difficulty=beginner", + ) + expect(res.status).toBe(200) + expect(res.body.data).toEqual([]) + expect(res.body.total).toBe(0) + }) + + it("applies search across course title and description", async () => { + mockedQuery + .mockResolvedValueOnce({ rows: [{ count: "1" }] }) + .mockResolvedValueOnce({ + rows: [ + { + id: 1, + slug: "stellar-basics", + title: "Stellar Basics", + description: "Learn how Stellar works", + cover_image_url: null, + track: "web3", + difficulty: "beginner", + published_at: "2026-01-01T00:00:00.000Z", + created_at: "2026-01-01T00:00:00.000Z", + updated_at: "2026-01-02T00:00:00.000Z", + }, + ], + }) + + const res = await request(buildApp()).get("/api/courses?search=stellar") + + expect(res.status).toBe(200) + expect(res.body.total).toBe(1) + expect(mockedQuery).toHaveBeenNthCalledWith( + 1, + expect.stringContaining("c.title ILIKE $2 OR c.description ILIKE $2"), + ["%stellar%"], + ) + expect(mockedQuery).toHaveBeenNthCalledWith( + 2, + expect.stringContaining("c.title ILIKE $2 OR c.description ILIKE $2"), + ["%stellar%", 12, 0], + ) + }) + + it("enforces max limit and computes pages", async () => { + mockedQuery + .mockResolvedValueOnce({ rows: [{ count: "120" }] }) + .mockResolvedValueOnce({ + rows: [], + }) + + const res = await request(buildApp()).get("/api/courses?page=2&limit=999") + expect(res.status).toBe(200) + expect(res.body.limit).toBe(50) + expect(res.body.page).toBe(2) + expect(res.body.totalPages).toBe(3) + }) + + it("supports offset parameter", async () => { + mockedQuery + .mockResolvedValueOnce({ rows: [{ count: "100" }] }) + .mockResolvedValueOnce({ + rows: [], + }) + + const res = await request(buildApp()).get("/api/courses?offset=10&limit=10") + expect(res.status).toBe(200) + expect(res.body.page).toBe(2) + expect(res.body.limit).toBe(10) + }) + + it("returns empty results for invalid difficulty", async () => { + const res = await request(buildApp()).get("/api/courses?difficulty=expert") + expect(res.status).toBe(200) + expect(res.body).toEqual({ + data: [], + page: 1, + limit: 12, + total: 0, + totalPages: 0, + }) + }) +}) + +describe("GET /api/courses/:idOrSlug", () => { + it("returns a course with nested lessons", async () => { + mockedQuery + .mockResolvedValueOnce({ + rows: [ + { + id: 1, + slug: "stellar-basics", + title: "Stellar Basics", + description: "Basics", + cover_image_url: null, + track: "web3", + difficulty: "beginner", + published_at: "2026-01-01T00:00:00.000Z", + created_at: "2026-01-01T00:00:00.000Z", + updated_at: "2026-01-02T00:00:00.000Z", + }, + ], + }) + .mockResolvedValueOnce({ + rows: [ + { + id: 10, + course_id: 1, + title: "Lesson 1", + content_markdown: "Content", + order_index: 1, + quiz: [], + created_at: "2026-01-01T00:00:00.000Z", + updated_at: "2026-01-01T00:00:00.000Z", + }, + ], + }) + + const res = await request(buildApp()).get("/api/courses/stellar-basics") + expect(res.status).toBe(200) + expect(res.body.slug).toBe("stellar-basics") + expect(res.body.lessons).toHaveLength(1) + }) + + it("returns 404 when course is missing", async () => { + mockedQuery.mockResolvedValueOnce({ rows: [] }) + const res = await request(buildApp()).get("/api/courses/missing-course") + expect(res.status).toBe(404) + expect(res.body).toEqual({ error: "Course not found" }) + }) +}) + +describe("GET /api/courses/:idOrSlug/lessons/:id", () => { + it("returns lesson including quiz", async () => { + mockedQuery.mockResolvedValueOnce({ + rows: [ + { + id: 10, + course_id: 1, + title: "Lesson 1", + content_markdown: "Content", + order_index: 1, + quiz: [{ question: "Q?", options: ["A", "B"], correctIndex: 0 }], + created_at: "2026-01-01T00:00:00.000Z", + updated_at: "2026-01-01T00:00:00.000Z", + }, + ], + }) + + const res = await request(buildApp()).get( + "/api/courses/stellar-basics/lessons/10", + ) + expect(res.status).toBe(200) + expect(res.body.id).toBe(10) + expect(res.body.quiz).toHaveLength(1) + }) + + it("returns 404 for wrong course or missing lesson", async () => { + mockedQuery.mockResolvedValueOnce({ rows: [] }) + const res = await request(buildApp()).get("/api/courses/defi/lessons/10") + expect(res.status).toBe(404) + expect(res.body).toEqual({ error: "Lesson not found" }) + }) +}) + +describe("POST /api/courses", () => { + it("creates a course for admin", async () => { + mockedQuery.mockResolvedValueOnce({ + rows: [ + { + id: 11, + slug: "new-course", + title: "New Course", + description: "", + cover_image_url: null, + track: "web3", + difficulty: "beginner", + published_at: null, + created_at: "2026-01-01T00:00:00.000Z", + updated_at: "2026-01-01T00:00:00.000Z", + }, + ], + }) + + const res = await request(buildApp()) + .post("/api/courses") + .set("Authorization", `Bearer ${adminToken}`) + .send({ + title: "New Course", + slug: "new-course", + track: "web3", + difficulty: "beginner", + }) + + expect(res.status).toBe(201) + expect(res.body.slug).toBe("new-course") + expect(res.body.published).toBe(false) + }) + + it("returns 400 for missing required fields", async () => { + const res = await request(buildApp()) + .post("/api/courses") + .set("Authorization", `Bearer ${adminToken}`) + .send({ slug: "only-slug" }) + + expect(res.status).toBe(400) + expect(res.body.field).toBe("title") + }) + + it("returns 409 for duplicate slug", async () => { + mockedQuery.mockRejectedValueOnce({ code: "23505" }) + const res = await request(buildApp()) + .post("/api/courses") + .set("Authorization", `Bearer ${adminToken}`) + .send({ + title: "Duplicate", + slug: "duplicate", + track: "web3", + difficulty: "beginner", + }) + expect(res.status).toBe(409) + }) + + it("returns 401 without auth", async () => { + const res = await request(buildApp()).post("/api/courses").send({}) + expect(res.status).toBe(401) + expect(res.body).toEqual({ error: "Unauthorized" }) + }) + + it("returns 403 for non-admin JWT", async () => { + const res = await request(buildApp()) + .post("/api/courses") + .set("Authorization", `Bearer ${nonAdminToken}`) + .send({}) + expect(res.status).toBe(403) + expect(res.body).toEqual({ error: "Forbidden" }) + }) +}) + +describe("PATCH /api/courses/:id", () => { + it("updates course fields", async () => { + mockedQuery + .mockResolvedValueOnce({ rowCount: 1, rows: [{ id: 1 }] }) + .mockResolvedValueOnce({ + rows: [ + { + id: 1, + slug: "stellar-basics", + title: "Updated Title", + description: "Desc", + cover_image_url: null, + track: "web3", + difficulty: "beginner", + published_at: null, + created_at: "2026-01-01T00:00:00.000Z", + updated_at: "2026-01-03T00:00:00.000Z", + }, + ], + }) + + const res = await request(buildApp()) + .patch("/api/courses/1") + .set("Authorization", `Bearer ${adminToken}`) + .send({ title: "Updated Title" }) + + expect(res.status).toBe(200) + expect(res.body.title).toBe("Updated Title") + }) + + it("returns 404 when course does not exist", async () => { + mockedQuery.mockResolvedValueOnce({ rowCount: 0, rows: [] }) + const res = await request(buildApp()) + .patch("/api/courses/999") + .set("Authorization", `Bearer ${adminToken}`) + .send({ title: "Nope" }) + expect(res.status).toBe(404) + }) + + it("returns 401 without auth", async () => { + const res = await request(buildApp()).patch("/api/courses/1").send({}) + expect(res.status).toBe(401) + }) + + it("returns 409 for duplicate slug", async () => { + mockedQuery + .mockResolvedValueOnce({ rowCount: 1, rows: [{ id: 1 }] }) + .mockRejectedValueOnce({ code: "23505" }) + + const res = await request(buildApp()) + .patch("/api/courses/1") + .set("Authorization", `Bearer ${adminToken}`) + .send({ slug: "taken-slug" }) + expect(res.status).toBe(409) + expect(res.body).toEqual({ error: "Slug already exists" }) + }) +}) diff --git a/server/src/tests/credential.service.test.ts b/server/src/tests/credential.service.test.ts new file mode 100644 index 00000000..da3c052b --- /dev/null +++ b/server/src/tests/credential.service.test.ts @@ -0,0 +1,138 @@ +jest.mock("../db/index", () => ({ + pool: { + query: jest.fn(), + connect: jest.fn(), + }, +})) + +jest.mock("../services/pinata.service", () => ({ + pinJsonToIPFS: jest.fn().mockResolvedValue("bafkreifakeipfscid123"), + getGatewayUrl: jest.fn( + (cid: string) => `https://gateway.pinata.cloud/ipfs/${cid}`, + ), +})) + +jest.mock("../services/stellar-contract.service", () => ({ + stellarContractService: { + callVerifyMilestone: jest.fn().mockResolvedValue({ + txHash: "sim_verify_123", + simulated: true, + }), + emitRejectionEvent: jest.fn().mockResolvedValue({ + txHash: "sim_reject_123", + simulated: true, + }), + callMintScholarNFT: jest.fn().mockResolvedValue({ + txHash: "sim_mint_123", + simulated: true, + }), + }, +})) + +import { inMemoryMilestoneStore } from "../db/milestone-store" +import { credentialService } from "../services/credential.service" + +beforeEach(() => { + // @ts-ignore + inMemoryMilestoneStore["reports"] = [] + // @ts-ignore + inMemoryMilestoneStore["auditLog"] = [] + // @ts-ignore + inMemoryMilestoneStore["reportSeq"] = 1 + // @ts-ignore + inMemoryMilestoneStore["auditSeq"] = 1 +}) + +describe("credentialService.isCourseComplete", () => { + it("returns false when no milestones exist", async () => { + const result = await credentialService.isCourseComplete( + "GSCHOLAR1", + "stellar-basics", + ) + expect(result).toBe(false) + }) + + it("returns false when not all milestones are approved", async () => { + await inMemoryMilestoneStore.createReport({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + }) + await inMemoryMilestoneStore.updateReportStatus(1, "approved") + + await inMemoryMilestoneStore.createReport({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 2, + evidence_description: "WIP", + }) + + const result = await credentialService.isCourseComplete( + "GSCHOLAR1", + "stellar-basics", + ) + expect(result).toBe(false) + }) + + it("returns true when all milestones are approved", async () => { + await inMemoryMilestoneStore.createReport({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + }) + await inMemoryMilestoneStore.updateReportStatus(1, "approved") + + await inMemoryMilestoneStore.createReport({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 2, + evidence_description: "Done", + }) + await inMemoryMilestoneStore.updateReportStatus(2, "approved") + + const result = await credentialService.isCourseComplete( + "GSCHOLAR1", + "stellar-basics", + ) + expect(result).toBe(true) + }) +}) + +describe("credentialService.mintCertificateIfComplete", () => { + it("returns minted: false when course is incomplete", async () => { + await inMemoryMilestoneStore.createReport({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "WIP", + }) + + const result = await credentialService.mintCertificateIfComplete( + "GSCHOLAR1", + "stellar-basics", + ) + expect(result.minted).toBe(false) + expect(result.tokenUri).toBeUndefined() + }) + + it("mints certificate when all milestones are approved", async () => { + await inMemoryMilestoneStore.createReport({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_description: "Done", + }) + await inMemoryMilestoneStore.updateReportStatus(1, "approved") + + const result = await credentialService.mintCertificateIfComplete( + "GSCHOLAR1", + "stellar-basics", + ) + expect(result.minted).toBe(true) + expect(result.tokenUri).toBe("ipfs://bafkreifakeipfscid123") + expect(result.mintTxHash).toBe("sim_mint_123") + expect(result.simulated).toBe(true) + }) +}) diff --git a/server/src/tests/csrf.test.ts b/server/src/tests/csrf.test.ts new file mode 100644 index 00000000..f193dd0c --- /dev/null +++ b/server/src/tests/csrf.test.ts @@ -0,0 +1,238 @@ +/** + * CSRF posture regression tests. + * + * The LearnVault API authenticates exclusively via `Authorization: Bearer` + * headers — there are no auth cookies, and therefore no ambient credentials + * that a cross-origin page could ride. See `docs/csrf-protection.md` for the + * full rationale. + * + * These tests pin the invariants that make that claim hold: + * 1. CORS is a strict allowlist (disallowed origins are not granted ACAO). + * 2. The auth middleware rejects requests without a valid Bearer token. + * 3. Protected endpoints do not issue Set-Cookie on success. + * + * If any of these regresses (e.g. someone introduces cookie-based sessions + * or loosens CORS to `origin: true`), this file should fail before the + * change ships. + */ + +import cors from "cors" +import express from "express" +import jwt from "jsonwebtoken" +import request from "supertest" + +import { createRequireAuth } from "../middleware/auth.middleware" +import { createRequireTrustedOrigin } from "../middleware/csrf.middleware" +import { errorHandler } from "../middleware/error.middleware" + +const JWT_SECRET = "learnvault-csrf-test-secret" +const ALLOWED_ORIGIN = "https://learnvault.app" +const DISALLOWED_ORIGIN = "https://malicious-site.example" +const ALLOWED_ORIGINS = [ALLOWED_ORIGIN] + +const testJwtService = { + signWalletToken: (addr: string) => jwt.sign({ sub: addr }, JWT_SECRET), + verifyWalletToken: async (token: string) => { + const d = jwt.verify(token, JWT_SECRET) as { + sub?: string + address?: string + } + const sub = d.sub ?? d.address ?? "" + if (!sub) throw new Error("Invalid token") + return { sub } + }, + revokeToken: jest.fn().mockResolvedValue(undefined), +} + +function validToken(address = "GUSER123") { + return jwt.sign({ sub: address }, JWT_SECRET, { expiresIn: "1h" }) +} + +/** + * Builds an app whose CORS + auth wiring mirrors `server/src/index.ts`. + * Uses a synthetic `/api/protected` route so we exercise the middleware + * chain in isolation, without pulling in database or external-service + * dependencies. + */ +function buildApp() { + const app = express() + app.use( + cors({ + origin: (origin, cb) => { + if (!origin) return cb(null, true) + if (origin === ALLOWED_ORIGIN) return cb(null, true) + return cb(new Error("Not allowed by CORS")) + }, + credentials: true, + methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + }), + ) + app.use(createRequireTrustedOrigin(ALLOWED_ORIGINS)) + app.use(express.json()) + + const requireAuth = createRequireAuth(testJwtService) + app.post("/api/protected", requireAuth, (_req, res) => { + res.status(200).json({ ok: true }) + }) + app.post("/api/public", (_req, res) => { + res.status(200).json({ ok: true }) + }) + app.get("/api/public", (_req, res) => { + res.status(200).json({ ok: true }) + }) + + app.use(errorHandler) + return app +} + +describe("CSRF posture — bearer-only auth model", () => { + it("OPTIONS preflight from a disallowed origin is not granted Access-Control-Allow-Origin", async () => { + const res = await request(buildApp()) + .options("/api/protected") + .set("Origin", DISALLOWED_ORIGIN) + .set("Access-Control-Request-Method", "POST") + .set("Access-Control-Request-Headers", "authorization,content-type") + + // Without ACAO echoed back for the attacker's origin, the browser will + // refuse to send the real cross-origin POST. + expect(res.headers["access-control-allow-origin"]).toBeUndefined() + }) + + it("cross-origin POST from a disallowed origin is blocked at the CORS layer", async () => { + const res = await request(buildApp()) + .post("/api/protected") + .set("Origin", DISALLOWED_ORIGIN) + .set("Authorization", `Bearer ${validToken()}`) + .send({}) + + // The cors middleware forwards the rejection to errorHandler; the + // protected handler never runs and no ACAO is set for the attacker. + expect(res.headers["access-control-allow-origin"]).toBeUndefined() + expect(res.body?.ok).toBeUndefined() + }) + + it("state-changing POST without an Authorization header is rejected with 401", async () => { + const res = await request(buildApp()) + .post("/api/protected") + .set("Origin", ALLOWED_ORIGIN) + .send({}) + + expect(res.status).toBe(401) + }) + + it("state-changing POST with an invalid Bearer token is rejected with 401", async () => { + const res = await request(buildApp()) + .post("/api/protected") + .set("Origin", ALLOWED_ORIGIN) + .set("Authorization", "Bearer not-a-real-token") + .send({}) + + expect(res.status).toBe(401) + }) + + it("state-changing POST with an empty Bearer value is rejected with 401", async () => { + const res = await request(buildApp()) + .post("/api/protected") + .set("Origin", ALLOWED_ORIGIN) + .set("Authorization", "Bearer ") + .send({}) + + expect(res.status).toBe(401) + }) + + describe("requireTrustedOrigin middleware", () => { + it("rejects a state-changing POST on an unauth endpoint when Origin is untrusted", async () => { + // Attacker owns their own server and makes a cross-origin call with + // their real Origin — browser-mediated CSRF scenario. cors also + // blocks this, but the dedicated middleware pins the behavior. + const res = await request(buildApp()) + .post("/api/public") + .set("Origin", DISALLOWED_ORIGIN) + .send({}) + + expect(res.status).toBeGreaterThanOrEqual(400) + expect(res.body?.ok).toBeUndefined() + }) + + it("rejects a state-changing POST when only Referer is set and is untrusted", async () => { + const res = await request(buildApp()) + .post("/api/public") + .set("Referer", `${DISALLOWED_ORIGIN}/some/path`) + .send({}) + + expect(res.status).toBe(403) + expect(res.body?.ok).toBeUndefined() + }) + + it("allows a state-changing POST when only Referer is set and is trusted", async () => { + const res = await request(buildApp()) + .post("/api/public") + .set("Referer", `${ALLOWED_ORIGIN}/dashboard`) + .send({}) + + expect(res.status).toBe(200) + expect(res.body?.ok).toBe(true) + }) + + it("rejects a state-changing POST when Referer is malformed", async () => { + const res = await request(buildApp()) + .post("/api/public") + .set("Referer", "not a url") + .send({}) + + expect(res.status).toBe(403) + }) + + it("allows state-changing POST with neither Origin nor Referer (server-to-server)", async () => { + // Permissive mode: curl/Postman/workers without a browser + // fingerprint pass through. Auth middleware on protected routes + // is the load-bearing defense for this path. + const res = await request(buildApp()).post("/api/public").send({}) + + expect(res.status).toBe(200) + }) + + it("does not block GET requests from a trusted origin", async () => { + // Baseline: the trusted-origin gate only applies to state-changing + // methods. GET/HEAD/OPTIONS pass through untouched. + const res = await request(buildApp()) + .get("/api/public") + .set("Origin", ALLOWED_ORIGIN) + + expect(res.status).toBe(200) + }) + + it("does not gate GETs on Referer (reads are not state-changing)", async () => { + // Even with an untrusted Referer, a GET passes — the middleware + // short-circuits on non-state-changing methods. + const { createRequireTrustedOrigin: make } = + await import("../middleware/csrf.middleware") + const mw = make(ALLOWED_ORIGINS) + const req = { + method: "GET", + headers: { referer: `${DISALLOWED_ORIGIN}/x` }, + } as unknown as import("express").Request + let called = false + const next = () => { + called = true + } + mw(req, {} as import("express").Response, next) + expect(called).toBe(true) + }) + }) + + it("protected endpoint does not issue Set-Cookie on success (bearer-only invariant)", async () => { + // If this starts failing, someone has introduced cookie-based auth — + // revisit docs/csrf-protection.md and add CSRF token validation + // before merging. + const res = await request(buildApp()) + .post("/api/protected") + .set("Origin", ALLOWED_ORIGIN) + .set("Authorization", `Bearer ${validToken()}`) + .send({}) + + expect(res.status).toBe(200) + expect(res.headers["set-cookie"]).toBeUndefined() + }) +}) diff --git a/server/src/tests/governance.test.ts b/server/src/tests/governance.test.ts new file mode 100644 index 00000000..bb3c2aa9 --- /dev/null +++ b/server/src/tests/governance.test.ts @@ -0,0 +1,595 @@ +process.env.JWT_SECRET = "learnvault-secret" +process.env.ADMIN_ADDRESSES = "GADMIN123" +process.env.NODE_ENV = "test" +process.env.STELLAR_SECRET_KEY = "test-secret-key" +process.env.PINATA_API_KEY = "test-api-key" +process.env.PINATA_SECRET = "test-secret" +process.env.FRONTEND_URL = "http://localhost:3000" + +import express from "express" +import jwt from "jsonwebtoken" +import request from "supertest" + +// Mock the dependencies before importing the router/controller +jest.mock("../db/index", () => ({ + pool: { + query: jest.fn().mockResolvedValue({ rows: [{ id: 456 }] }), + }, +})) + +jest.mock("../services/stellar-contract.service", () => ({ + stellarContractService: { + submitScholarshipProposal: jest.fn().mockResolvedValue({ + txHash: "mock_tx_hash_abc123", + proposalId: null, + simulated: false, + }), + getGovernanceTokenBalance: jest.fn().mockResolvedValue("1250000000"), + getGovernanceVotingPower: jest.fn().mockResolvedValue("1250000000"), + castVote: jest.fn().mockResolvedValue({ + txHash: "mock_vote_tx_hash", + simulated: false, + }), + cancelProposal: jest.fn().mockResolvedValue({ + txHash: "mock_cancel_tx_hash", + simulated: false, + }), + }, +})) + +jest.mock("../services/pinata.service", () => ({ + getClient: jest.fn().mockReturnValue({ + pinFileToIPFS: jest.fn().mockResolvedValue({ + IpfsHash: "mock-ipfs-hash", + }), + pinJsonToIPFS: jest.fn().mockResolvedValue({ + IpfsHash: "mock-json-hash", + }), + }), +})) + +jest.mock("../services/email.service", () => ({ + createEmailService: jest.fn().mockReturnValue({ + sendNotification: jest.fn().mockResolvedValue({}), + }), +})) + +jest.mock("../services/escrow-timeout.service", () => ({ + trackEscrowTimeout: jest.fn().mockResolvedValue(undefined), +})) + +jest.mock("../lib/request-context", () => ({ + getRequestContext: jest.fn().mockReturnValue({ + requestId: "test-request-id-123", + }), + runWithRequestContext: jest.fn((context, fn) => fn()), +})) + +import { governanceRouter } from "../routes/governance.routes" + +const app = express() +app.use(express.json()) +app.use( + require("../middleware/request-logger.middleware").createRequestLogger({ + enabled: false, + }), +) +app.use("/api", governanceRouter) + +const JWT_SECRET = "learnvault-secret" + +function makeToken(address: string) { + return jwt.sign({ address }, JWT_SECRET, { expiresIn: "1h" }) +} + +describe("POST /api/governance/proposals", () => { + it("should create a valid governance proposal", async () => { + const response = await request(app).post("/api/governance/proposals").send({ + author_address: + "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", + title: "Fund my Soroban course", + description: "I am learning Soroban and need funding for my course.", + requested_amount: "500", + evidence_url: "https://example.com/my-proposal", + }) + + expect(response.status).toBe(201) + expect(response.body).toHaveProperty("proposal_id", 456) + expect(response.body).toHaveProperty("tx_hash", "mock_tx_hash_abc123") + }) + + it("should reject proposal with missing required fields", async () => { + const response = await request(app).post("/api/governance/proposals").send({ + author_address: + "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", + title: "Fund my course", + }) + + expect(response.status).toBe(400) + expect(response.body).toHaveProperty("error", "Invalid proposal data") + expect(response.body).toHaveProperty("details") + }) + + it("should reject proposal with invalid author_address (too short)", async () => { + const response = await request(app).post("/api/governance/proposals").send({ + author_address: "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBF", + title: "Fund my Soroban course", + description: "I am learning Soroban and need funding for my course.", + requested_amount: "500", + evidence_url: "https://example.com/my-proposal", + }) + + expect(response.status).toBe(400) + expect(response.body).toHaveProperty("error", "Invalid proposal data") + expect(response.body.details).toHaveProperty("author_address") + }) + + it("should reject proposal with invalid evidence_url", async () => { + const response = await request(app).post("/api/governance/proposals").send({ + author_address: + "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", + title: "Fund my Soroban course", + description: "I am learning Soroban and need funding for my course.", + requested_amount: "500", + evidence_url: "not-a-valid-url", + }) + + expect(response.status).toBe(400) + expect(response.body).toHaveProperty("error", "Invalid proposal data") + expect(response.body.details).toHaveProperty("evidence_url") + }) + + it("should reject proposal with invalid requested_amount", async () => { + const response = await request(app).post("/api/governance/proposals").send({ + author_address: + "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", + title: "Fund my Soroban course", + description: "I am learning Soroban and need funding for my course.", + requested_amount: "not-a-number", + evidence_url: "https://example.com/my-proposal", + }) + + expect(response.status).toBe(400) + expect(response.body).toHaveProperty("error", "Invalid proposal data") + expect(response.body.details).toHaveProperty("requested_amount") + }) + + it("should handle contract call failure gracefully", async () => { + const { stellarContractService } = + await import("../services/stellar-contract.service") + ;( + stellarContractService.submitScholarshipProposal as jest.Mock + ).mockRejectedValueOnce(new Error("Contract call failed")) + + const response = await request(app).post("/api/governance/proposals").send({ + author_address: + "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", + title: "Fund my Soroban course", + description: "I am learning Soroban and need funding for my course.", + requested_amount: "500", + evidence_url: "https://example.com/my-proposal", + }) + + expect(response.status).toBe(500) + expect(response.body).toHaveProperty( + "error", + "Failed to create governance proposal", + ) + expect(response.body).toHaveProperty("message") + }) +}) + +describe("GET /api/governance/voting-power/:address", () => { + it("returns voting power for a valid address", async () => { + const response = await request(app).get( + "/api/governance/voting-power/GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", + ) + + expect(response.status).toBe(200) + expect(response.body.address).toBe( + "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", + ) + expect(response.body.gov_balance).toBe("1250000000") + expect(response.body.formatted).toBe("125.00") + expect(response.body.can_vote).toBe(true) + }) + + it("returns can_vote false for zero balance", async () => { + const { stellarContractService } = + await import("../services/stellar-contract.service") + ;( + stellarContractService.getGovernanceTokenBalance as jest.Mock + ).mockResolvedValueOnce("0") + + const response = await request(app).get( + "/api/governance/voting-power/GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", + ) + + expect(response.status).toBe(200) + expect(response.body.gov_balance).toBe("0") + expect(response.body.formatted).toBe("0.00") + expect(response.body.can_vote).toBe(false) + }) + + it("returns 400 for invalid address", async () => { + const response = await request(app).get( + "/api/governance/voting-power/short", + ) + + expect(response.status).toBe(400) + expect(response.body.error).toBe("Invalid Stellar address") + }) +}) + +describe("GET /api/proposals", () => { + it("returns proposals from the alias endpoint", async () => { + const db = require("../db/index") + db.pool.query + .mockResolvedValueOnce({ rows: [{ total: 1 }] }) + .mockResolvedValueOnce({ + rows: [ + { + id: 7, + author_address: + "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", + title: "Fund cohort", + description: "Detailed proposal", + amount: "500", + votes_for: "10", + votes_against: "2", + status: "pending", + deadline: "2026-04-10T12:00:00.000Z", + created_at: "2026-03-28T12:00:00.000Z", + user_vote_support: true, + }, + ], + }) + + const response = await request(app).get( + `/api/proposals?viewer_address=${TEST_VOTER}`, + ) + + expect(response.status).toBe(200) + expect(response.body.total).toBe(1) + expect(response.body.proposals[0]).toHaveProperty("id", 7) + expect(response.body.proposals[0]).toHaveProperty("user_vote_support", true) + }) +}) + +describe("GET /api/proposals/:id", () => { + it("returns proposal detail from the alias endpoint", async () => { + const db = require("../db/index") + db.pool.query.mockResolvedValueOnce({ + rows: [ + { + id: 9, + author_address: + "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", + title: "Fund educators", + description: "Long-form detail", + amount: "750", + votes_for: "11", + votes_against: "4", + status: "pending", + deadline: "2026-04-15T12:00:00.000Z", + created_at: "2026-03-28T12:00:00.000Z", + user_vote_support: null, + }, + ], + }) + + const response = await request(app).get("/api/proposals/9") + + expect(response.status).toBe(200) + expect(response.body).toHaveProperty("id", 9) + expect(response.body).toHaveProperty("title", "Fund educators") + }) +}) + +// Valid 56-char Stellar test address +const TEST_VOTER = "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ" + +describe("POST /api/governance/vote", () => { + let pool: any + let stellarContractService: any + + beforeEach(() => { + jest.clearAllMocks() + const db = require("../db/index") + const scs = require("../services/stellar-contract.service") + pool = db.pool + stellarContractService = scs.stellarContractService + // Default happy path mocks + pool.query + .mockResolvedValueOnce({ + rows: [ + { + id: 1, + status: "pending", + deadline: "2099-01-01T00:00:00.000Z", + cancelled: false, + }, + ], + }) // proposal check + .mockResolvedValueOnce({ rows: [] }) // no existing vote + .mockResolvedValueOnce({ rows: [{ id: 1 }] }) // insert vote + .mockResolvedValueOnce({ rows: [] }) // update proposal + .mockResolvedValueOnce({ + rows: [{ votes_for: "1250000000", votes_against: "0" }], + }) // fetch updated counts + stellarContractService.getGovernanceTokenBalance.mockResolvedValue( + "1250000000", + ) + stellarContractService.castVote.mockResolvedValue({ + txHash: "mock_vote_tx", + simulated: false, + }) + }) + + it("should cast a valid vote", async () => { + const response = await request(app).post("/api/governance/vote").send({ + proposal_id: 1, + voter_address: TEST_VOTER, + support: true, + }) + + expect(response.status).toBe(201) + expect(response.body).toHaveProperty("tx_hash", "mock_vote_tx") + expect(response.body).toHaveProperty("votes_for") + expect(response.body).toHaveProperty("votes_against") + }) + + it("should reject vote with invalid proposal_id", async () => { + const response = await request(app).post("/api/governance/vote").send({ + proposal_id: -1, + voter_address: TEST_VOTER, + support: true, + }) + + expect(response.status).toBe(400) + expect(response.body).toHaveProperty("error", "Invalid vote data") + }) + + it("should reject vote with invalid voter_address", async () => { + const response = await request(app).post("/api/governance/vote").send({ + proposal_id: 1, + voter_address: "short", + support: true, + }) + + expect(response.status).toBe(400) + expect(response.body).toHaveProperty("error", "Invalid vote data") + }) + + it("should reject vote when proposal not found", async () => { + pool.query.mockReset() + pool.query.mockResolvedValueOnce({ rows: [] }) + + const response = await request(app).post("/api/governance/vote").send({ + proposal_id: 999, + voter_address: TEST_VOTER, + support: true, + }) + + expect(response.status).toBe(404) + expect(response.body).toHaveProperty("error", "Proposal not found") + }) + + it("should reject vote when proposal is not pending", async () => { + pool.query.mockReset() + pool.query.mockResolvedValueOnce({ + rows: [{ id: 1, status: "approved", deadline: null }], + }) + + const response = await request(app).post("/api/governance/vote").send({ + proposal_id: 1, + voter_address: TEST_VOTER, + support: true, + }) + + expect(response.status).toBe(400) + expect(response.body).toHaveProperty( + "error", + "Voting is closed for this proposal", + ) + }) + + it("should reject vote when voter already voted", async () => { + pool.query.mockReset() + pool.query + .mockResolvedValueOnce({ + rows: [ + { + id: 1, + status: "pending", + deadline: "2099-01-01T00:00:00.000Z", + cancelled: false, + }, + ], + }) + .mockResolvedValueOnce({ rows: [{ id: 1 }] }) + + const response = await request(app).post("/api/governance/vote").send({ + proposal_id: 1, + voter_address: TEST_VOTER, + support: true, + }) + + expect(response.status).toBe(409) + expect(response.body).toHaveProperty( + "error", + "You have already voted on this proposal", + ) + }) + + it("should reject vote when voter has no GOV tokens", async () => { + pool.query.mockReset() + pool.query + .mockResolvedValueOnce({ + rows: [ + { + id: 1, + status: "pending", + deadline: "2099-01-01T00:00:00.000Z", + cancelled: false, + }, + ], + }) + .mockResolvedValueOnce({ rows: [] }) + stellarContractService.getGovernanceTokenBalance.mockResolvedValueOnce("0") + + const response = await request(app).post("/api/governance/vote").send({ + proposal_id: 1, + voter_address: TEST_VOTER, + support: true, + }) + + expect(response.status).toBe(400) + expect(response.body).toHaveProperty("error", "You have no voting power") + }) + + it("should handle contract call failure gracefully", async () => { + pool.query.mockReset() + pool.query + .mockResolvedValueOnce({ + rows: [ + { + id: 1, + status: "pending", + deadline: "2099-01-01T00:00:00.000Z", + cancelled: false, + }, + ], + }) + .mockResolvedValueOnce({ rows: [] }) + stellarContractService.castVote.mockRejectedValueOnce( + new Error("Contract call failed"), + ) + + const response = await request(app).post("/api/governance/vote").send({ + proposal_id: 1, + voter_address: TEST_VOTER, + support: true, + }) + + expect(response.status).toBe(500) + expect(response.body).toHaveProperty("error", "Failed to cast vote") + }) + + it("should reject vote when deadline has passed", async () => { + pool.query.mockReset() + pool.query.mockResolvedValueOnce({ + rows: [ + { id: 1, status: "pending", deadline: "2020-01-01T00:00:00.000Z" }, + ], + }) + + const response = await request(app).post("/api/governance/vote").send({ + proposal_id: 1, + voter_address: TEST_VOTER, + support: true, + }) + + expect(response.status).toBe(400) + expect(response.body).toHaveProperty( + "error", + "Voting is closed for this proposal", + ) + }) +}) + +describe("GET /api/proposals/:id/status", () => { + let pool: any + + beforeEach(() => { + jest.clearAllMocks() + pool = require("../db/index").pool + }) + + it("returns open status for a live pending proposal", async () => { + pool.query.mockResolvedValueOnce({ + rows: [{ id: 7, status: "pending", cancelled: false, deadline: null }], + }) + + const response = await request(app).get("/api/proposals/7/status") + + expect(response.status).toBe(200) + expect(response.body).toEqual({ + id: 7, + state: "open", + status: "pending", + cancelled: false, + deadline: null, + }) + }) + + it("returns cancelled state for a cancelled proposal", async () => { + pool.query.mockResolvedValueOnce({ + rows: [{ id: 7, status: "pending", cancelled: true, deadline: null }], + }) + + const response = await request(app).get("/api/proposals/7/status") + + expect(response.status).toBe(200) + expect(response.body.state).toBe("cancelled") + }) +}) + +describe("DELETE /api/proposals/:id", () => { + let pool: any + let stellarContractService: any + + beforeEach(() => { + jest.clearAllMocks() + const db = require("../db/index") + const scs = require("../services/stellar-contract.service") + pool = db.pool + stellarContractService = scs.stellarContractService + }) + + it("allows an admin to cancel an open proposal", async () => { + pool.query + .mockResolvedValueOnce({ + rows: [{ id: 12, status: "pending", cancelled: false, deadline: null }], + }) + .mockResolvedValueOnce({ rows: [] }) + + const response = await request(app) + .delete("/api/proposals/12") + .set("Authorization", `Bearer ${makeToken("GADMIN123")}`) + + expect(response.status).toBe(204) + expect(stellarContractService.cancelProposal).toHaveBeenCalledWith( + { proposalId: 12 }, + { requestId: expect.any(String) }, + ) + expect(pool.query).toHaveBeenNthCalledWith( + 2, + "UPDATE proposals SET cancelled = TRUE WHERE id = $1", + [12], + ) + }) + + it("rejects non-admin users", async () => { + const response = await request(app) + .delete("/api/proposals/12") + .set("Authorization", `Bearer ${makeToken("GNOTADMIN123")}`) + + expect(response.status).toBe(403) + expect(response.body.error).toBe("Forbidden: not an admin address") + }) + + it("returns 409 for an already-cancelled proposal", async () => { + pool.query.mockResolvedValueOnce({ + rows: [{ id: 12, status: "pending", cancelled: true, deadline: null }], + }) + + const response = await request(app) + .delete("/api/proposals/12") + .set("Authorization", `Bearer ${makeToken("GADMIN123")}`) + + expect(response.status).toBe(409) + expect(response.body.error).toBe("Proposal is already cancelled") + expect(stellarContractService.cancelProposal).not.toHaveBeenCalled() + }) +}) diff --git a/server/src/tests/request-logger.middleware.test.ts b/server/src/tests/request-logger.middleware.test.ts new file mode 100644 index 00000000..79d66c72 --- /dev/null +++ b/server/src/tests/request-logger.middleware.test.ts @@ -0,0 +1,68 @@ +import express from "express" +import request from "supertest" + +import { createRequestLogger } from "../middleware/request-logger.middleware" + +describe("requestLogger middleware", () => { + it("attaches a request id header to every response", async () => { + const app = express() + + app.use(createRequestLogger({ enabled: false })) + app.get("/api/ping", (req, res) => { + res.json({ requestId: req.requestId }) + }) + + const response = await request(app).get("/api/ping?source=test") + + expect(response.status).toBe(200) + expect(response.headers["x-request-id"]).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, + ) + expect(response.body.requestId).toBe(response.headers["x-request-id"]) + }) + + it("logs structured request data on response finish", async () => { + const info = jest.fn() + const app = express() + + app.use(createRequestLogger({ enabled: true, logger: { info } })) + app.get("/api/users", (_req, res) => { + res.status(201).json({ ok: true }) + }) + + const response = await request(app).get("/api/users?page=1") + + expect(response.status).toBe(201) + expect(info).toHaveBeenCalledTimes(1) + expect(info).toHaveBeenCalledWith( + expect.objectContaining({ + requestId: response.headers["x-request-id"], + method: "GET", + path: "/api/users?page=1", + statusCode: 201, + durationMs: expect.any(Number), + }), + ) + }) + + it("stays silent in the test environment by default", async () => { + const previousNodeEnv = process.env.NODE_ENV + process.env.NODE_ENV = "test" + + const info = jest.fn() + const app = express() + + app.use(createRequestLogger({ logger: { info } })) + app.get("/api/quiet", (_req, res) => { + res.sendStatus(204) + }) + + const response = await request(app).get("/api/quiet") + + expect(response.status).toBe(204) + expect(response.headers["x-request-id"]).toBeTruthy() + expect(info).not.toHaveBeenCalled() + + process.env.NODE_ENV = previousNodeEnv + }) +}) diff --git a/server/src/tests/scholars-milestones.test.ts b/server/src/tests/scholars-milestones.test.ts new file mode 100644 index 00000000..9551feb0 --- /dev/null +++ b/server/src/tests/scholars-milestones.test.ts @@ -0,0 +1,163 @@ +/** + * Integration tests for scholar milestone history endpoint. + * Uses the in-memory store so no database is required. + */ + +jest.mock("../db/index", () => ({ + pool: { + query: jest.fn(), + connect: jest.fn(), + }, +})) + +import express from "express" +import request from "supertest" + +import { inMemoryMilestoneStore } from "../db/milestone-store" +import { errorHandler } from "../middleware/error.middleware" +import { scholarsRouter } from "../routes/scholars.routes" + +function buildApp() { + const app = express() + app.use(express.json()) + app.use("/api", scholarsRouter) + app.use(errorHandler) + return app +} + +beforeEach(() => { + // @ts-ignore – reset private fields for test isolation + inMemoryMilestoneStore["reports"] = [] + // @ts-ignore + inMemoryMilestoneStore["auditLog"] = [] + // @ts-ignore + inMemoryMilestoneStore["reportSeq"] = 1 + // @ts-ignore + inMemoryMilestoneStore["auditSeq"] = 1 +}) + +describe("GET /api/scholars/:address/milestones", () => { + it("returns milestone history with decision metadata", async () => { + const approvedReport = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 2, + evidence_github: "https://example.com/evidence", + evidence_ipfs_cid: null, + evidence_description: null, + }) + + await inMemoryMilestoneStore.updateReportStatus( + approvedReport.id, + "approved", + ) + await inMemoryMilestoneStore.addAuditEntry({ + report_id: approvedReport.id, + validator_address: "GADMIN123", + decision: "approved", + rejection_reason: null, + contract_tx_hash: "abc123", + }) + + await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 3, + evidence_github: "https://example.com/pending", + evidence_ipfs_cid: null, + evidence_description: null, + }) + + const rejectedReport = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 4, + evidence_github: "https://example.com/rejected", + evidence_ipfs_cid: null, + evidence_description: null, + }) + + await inMemoryMilestoneStore.updateReportStatus( + rejectedReport.id, + "rejected", + ) + await inMemoryMilestoneStore.addAuditEntry({ + report_id: rejectedReport.id, + validator_address: "GADMIN123", + decision: "rejected", + rejection_reason: "No evidence", + contract_tx_hash: "tx_reject_1", + }) + + const app = buildApp() + const res = await request(app).get("/api/scholars/GSCHOLAR1/milestones") + + expect(res.status).toBe(200) + expect(res.body.milestones).toHaveLength(3) + + const approved = res.body.milestones.find((m: any) => m.milestone_id === 2) + expect(approved).toMatchObject({ + id: String(approvedReport.id), + course_id: "stellar-basics", + milestone_id: 2, + status: "verified", + evidence_url: "https://example.com/evidence", + tx_hash: "abc123", + }) + expect(typeof approved.submitted_at).toBe("string") + expect(typeof approved.verified_at).toBe("string") + + const pending = res.body.milestones.find((m: any) => m.milestone_id === 3) + expect(pending.status).toBe("pending") + expect(pending.verified_at).toBeNull() + expect(pending.tx_hash).toBeNull() + }) + + it("filters by status and course_id", async () => { + const report1 = await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "stellar-basics", + milestone_id: 1, + evidence_github: "https://example.com/1", + evidence_ipfs_cid: null, + evidence_description: null, + }) + await inMemoryMilestoneStore.updateReportStatus(report1.id, "approved") + await inMemoryMilestoneStore.addAuditEntry({ + report_id: report1.id, + validator_address: "GADMIN123", + decision: "approved", + rejection_reason: null, + contract_tx_hash: "tx1", + }) + + await inMemoryMilestoneStore["createReport"]({ + scholar_address: "GSCHOLAR1", + course_id: "soroban-fundamentals", + milestone_id: 1, + evidence_github: "https://example.com/2", + evidence_ipfs_cid: null, + evidence_description: null, + }) + + const app = buildApp() + const res = await request(app).get( + "/api/scholars/GSCHOLAR1/milestones?status=verified&course_id=stellar-basics", + ) + + expect(res.status).toBe(200) + expect(res.body.milestones).toHaveLength(1) + expect(res.body.milestones[0].status).toBe("verified") + expect(res.body.milestones[0].course_id).toBe("stellar-basics") + }) + + it("returns 400 for invalid status", async () => { + const app = buildApp() + const res = await request(app).get( + "/api/scholars/GSCHOLAR1/milestones?status=not-a-status", + ) + + expect(res.status).toBe(400) + expect(res.body.error).toBe("Validation failed") + }) +}) diff --git a/server/src/tests/scholars-profile.test.ts b/server/src/tests/scholars-profile.test.ts new file mode 100644 index 00000000..ce893df2 --- /dev/null +++ b/server/src/tests/scholars-profile.test.ts @@ -0,0 +1,84 @@ +import express, { type Express } from "express" +import request from "supertest" + +// Mock internal modules +jest.mock("../db/index", () => ({ + pool: { + query: jest.fn(), + }, +})) + +jest.mock("../services/stellar-contract.service", () => ({ + stellarContractService: { + getLearnTokenBalance: jest.fn().mockResolvedValue("10000000000"), + getEnrolledCourses: jest + .fn() + .mockResolvedValue(["stellar-basics", "defi-101"]), + getScholarCredentials: jest.fn().mockResolvedValue([ + { + token_id: 1, + course_id: "stellar-basics", + issued_at: "2026-03-26T15:00:00Z", + }, + ]), + }, +})) + +import { pool } from "../db/index" +import { scholarsRouter } from "../routes/scholars.routes" + +const mockedQuery = pool.query as jest.Mock + +// We need a helper to build the app with mocked dependencies + +const buildApp = (): Express => { + const app = express() + app.use(express.json()) + app.use("/api", scholarsRouter) + return app +} + +describe("GET /api/scholars/:address", () => { + const mockAddress = "GABC1234567890" + + beforeEach(() => { + mockedQuery.mockReset() + }) + + it("returns a complete scholar profile", async () => { + // Mock database responses + mockedQuery + .mockResolvedValueOnce({ + rows: [{ completed: "1", pending: "1" }], + }) // stats + .mockResolvedValueOnce({ + rows: [{ joined_at: "2026-01-15T10:00:00.000Z" }], + }) // joinedAt + + const res = await request(buildApp()).get(`/api/scholars/${mockAddress}`) + + expect(res.status).toBe(200) + expect(res.body).toEqual({ + address: mockAddress, + lrn_balance: "10000000000", + enrolled_courses: ["stellar-basics", "defi-101"], + completed_milestones: 1, + pending_milestones: 1, + credentials: [ + { + token_id: 1, + course_id: "stellar-basics", + issued_at: "2026-03-26T15:00:00Z", + }, + ], + joined_at: "2026-01-15T10:00:00.000Z", + }) + }) + + it("returns 400 if address is missing", async () => { + // Express usually handles this via routing, but if we call without address: + // Note: /api/scholars/ without address might 404 due to route not matching + const res = await request(buildApp()).get("/api/scholars/") + expect(res.status).toBe(404) + }) +}) diff --git a/server/src/tests/scholarships.test.ts b/server/src/tests/scholarships.test.ts new file mode 100644 index 00000000..08b8d177 --- /dev/null +++ b/server/src/tests/scholarships.test.ts @@ -0,0 +1,52 @@ +import express from "express" +import request from "supertest" + +// Mock the dependencies before importing the router/controller +jest.mock("../db/index", () => ({ + pool: { + query: jest.fn().mockResolvedValue({ rows: [{ id: 123 }] }), + }, +})) + +jest.mock("../services/stellar-contract.service", () => ({ + stellarContractService: { + submitScholarshipProposal: jest.fn().mockResolvedValue({ + txHash: "fake_tx_hash", + simulated: true, + }), + }, +})) + +import { scholarshipsRouter } from "../routes/scholarships.routes" + +const app = express() +app.use(express.json()) +app.use("/api", scholarshipsRouter) + +describe("Scholarship Application API", () => { + it("should accept valid scholarship application", async () => { + const response = await request(app).post("/api/scholarships/apply").send({ + applicant_address: + "GBX7B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2", + full_name: "John Doe", + course_id: "stellar-101", + motivation: "I want to learn about Stellar and build on it.", + evidence_url: "https://example.com/evidence", + }) + + expect(response.status).toBe(201) + expect(response.body).toHaveProperty("proposal_id", 123) + expect(response.body).toHaveProperty("tx_hash", "fake_tx_hash") + }) + + it("should reject invalid scholarship application (missing field)", async () => { + const response = await request(app).post("/api/scholarships/apply").send({ + applicant_address: + "GBX7B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2", + full_name: "John Doe", + }) + + expect(response.status).toBe(400) + expect(response.body).toHaveProperty("error", "Invalid application data") + }) +}) diff --git a/server/src/tests/upload.test.ts b/server/src/tests/upload.test.ts new file mode 100644 index 00000000..5eef5e56 --- /dev/null +++ b/server/src/tests/upload.test.ts @@ -0,0 +1,226 @@ +/** + * Tests for POST /api/upload and POST /api/upload/nft-metadata. + * + * Pinata is mocked so no real API credentials are needed. + */ + +import express from "express" +import jwt from "jsonwebtoken" +import request from "supertest" + +// --------------------------------------------------------------------------- +// Mock pinata.service BEFORE importing the router so the controller gets the +// mock, not the real Pinata client. +// --------------------------------------------------------------------------- + +jest.mock("../services/pinata.service", () => ({ + pinFileToIPFS: jest.fn().mockResolvedValue("bafybeifake123"), + pinJsonToIPFS: jest.fn().mockResolvedValue("bafybeifakejson456"), + getGatewayUrl: jest.fn( + (cid: string) => `https://gateway.pinata.cloud/ipfs/${cid}`, + ), +})) + +import { errorHandler } from "../middleware/error.middleware" +import { createUploadRouter } from "../routes/upload.routes" +import * as pinataService from "../services/pinata.service" + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const JWT_SECRET = "learnvault-secret" + +const testJwtService = { + signWalletToken: (addr: string) => jwt.sign({ sub: addr }, JWT_SECRET), + verifyWalletToken: async (token: string) => { + const d = jwt.verify(token, JWT_SECRET) as { + sub?: string + address?: string + } + const sub = d.sub ?? d.address ?? "" + if (!sub) throw new Error("Invalid token") + return { sub } + }, + revokeToken: jest.fn().mockResolvedValue(undefined), +} + +function makeToken(address = "GUSER123") { + return jwt.sign({ address }, JWT_SECRET, { expiresIn: "1h" }) +} + +function buildApp() { + const app = express() + app.use(express.json()) + app.use("/api", createUploadRouter(testJwtService)) + app.use(errorHandler) + return app +} + +const VALID_PDF = Buffer.from("%PDF-1.4 fake pdf content") +const VALID_PNG = Buffer.from("\x89PNG\r\n\x1a\n fake png") + +// --------------------------------------------------------------------------- +// POST /api/upload +// --------------------------------------------------------------------------- + +describe("POST /api/upload", () => { + beforeEach(() => jest.clearAllMocks()) + + it("returns 401 when no token is provided", async () => { + const res = await request(buildApp()) + .post("/api/upload") + .attach("file", VALID_PDF, { + filename: "doc.pdf", + contentType: "application/pdf", + }) + + expect(res.status).toBe(401) + }) + + it("returns 400 when no file is attached", async () => { + const res = await request(buildApp()) + .post("/api/upload") + .set("Authorization", `Bearer ${makeToken()}`) + + expect(res.status).toBe(400) + expect(res.body).toHaveProperty("message") + }) + + it("returns 400 for a disallowed MIME type", async () => { + const res = await request(buildApp()) + .post("/api/upload") + .set("Authorization", `Bearer ${makeToken()}`) + .attach("file", Buffer.from(""), { + filename: "evil.svg", + contentType: "image/svg+xml", + }) + + expect(res.status).toBe(400) + }) + + it("pins a PDF and returns cid + gatewayUrl", async () => { + const res = await request(buildApp()) + .post("/api/upload") + .set("Authorization", `Bearer ${makeToken()}`) + .attach("file", VALID_PDF, { + filename: "proposal.pdf", + contentType: "application/pdf", + }) + + expect(res.status).toBe(201) + expect(res.body.cid).toBe("bafybeifake123") + expect(res.body.gatewayUrl).toBe( + "https://gateway.pinata.cloud/ipfs/bafybeifake123", + ) + + expect(pinataService.pinFileToIPFS).toHaveBeenCalledWith( + expect.any(Buffer), + "proposal.pdf", + ) + }) + + it("pins a PNG and returns cid + gatewayUrl", async () => { + const res = await request(buildApp()) + .post("/api/upload") + .set("Authorization", `Bearer ${makeToken()}`) + .attach("file", VALID_PNG, { + filename: "cover.png", + contentType: "image/png", + }) + + expect(res.status).toBe(201) + expect(res.body.cid).toBe("bafybeifake123") + }) + + it("returns 500 when Pinata throws", async () => { + ;(pinataService.pinFileToIPFS as jest.Mock).mockRejectedValueOnce( + new Error("Pinata API error"), + ) + + const res = await request(buildApp()) + .post("/api/upload") + .set("Authorization", `Bearer ${makeToken()}`) + .attach("file", VALID_PDF, { + filename: "doc.pdf", + contentType: "application/pdf", + }) + + expect(res.status).toBe(500) + }) +}) + +// --------------------------------------------------------------------------- +// POST /api/upload/nft-metadata +// --------------------------------------------------------------------------- + +describe("POST /api/upload/nft-metadata", () => { + beforeEach(() => jest.clearAllMocks()) + + it("returns 401 when no token is provided", async () => { + const res = await request(buildApp()) + .post("/api/upload/nft-metadata") + .send({ name: "Test", description: "Desc", image: "bafybeifake123" }) + + expect(res.status).toBe(401) + }) + + it("returns 400 when required fields are missing", async () => { + const res = await request(buildApp()) + .post("/api/upload/nft-metadata") + .set("Authorization", `Bearer ${makeToken()}`) + .send({ name: "Test" }) // missing description + image + + expect(res.status).toBe(400) + }) + + it("pins metadata and returns cid, gatewayUrl, tokenUri", async () => { + const res = await request(buildApp()) + .post("/api/upload/nft-metadata") + .set("Authorization", `Bearer ${makeToken()}`) + .send({ + name: "LearnVault Scholar", + description: "Completed Web3 Foundations", + image: "bafybeifake123", + attributes: [{ trait_type: "Course", value: "Web3 Foundations" }], + }) + + expect(res.status).toBe(201) + expect(res.body.cid).toBe("bafybeifakejson456") + expect(res.body.tokenUri).toBe("ipfs://bafybeifakejson456") + expect(res.body.gatewayUrl).toContain("bafybeifakejson456") + + // Image CID should be normalised to an ipfs:// URI in the pinned JSON + const pinCall = (pinataService.pinJsonToIPFS as jest.Mock).mock.calls[0] + expect(pinCall[0].image).toBe("ipfs://bafybeifake123") + expect(pinCall[0].attributes).toHaveLength(1) + }) + + it("normalises a bare CID image to ipfs:// URI", async () => { + await request(buildApp()) + .post("/api/upload/nft-metadata") + .set("Authorization", `Bearer ${makeToken()}`) + .send({ + name: "Scholar NFT", + description: "Proof of completion", + image: "bafybeifake123", // no ipfs:// prefix + }) + + const pinCall = (pinataService.pinJsonToIPFS as jest.Mock).mock.calls[0] + expect(pinCall[0].image).toBe("ipfs://bafybeifake123") + }) + + it("passes through an already-prefixed ipfs:// image URI unchanged", async () => { + await request(buildApp()) + .post("/api/upload/nft-metadata") + .set("Authorization", `Bearer ${makeToken()}`) + .send({ + name: "Scholar NFT", + description: "Proof of completion", + image: "ipfs://bafybeifake123", + }) + + const pinCall = (pinataService.pinJsonToIPFS as jest.Mock).mock.calls[0] + expect(pinCall[0].image).toBe("ipfs://bafybeifake123") + }) +}) diff --git a/server/src/types.d.ts b/server/src/types.d.ts new file mode 100644 index 00000000..df77181a --- /dev/null +++ b/server/src/types.d.ts @@ -0,0 +1 @@ +declare module "swagger-jsdoc" diff --git a/server/src/types/events.ts b/server/src/types/events.ts new file mode 100644 index 00000000..243720ac --- /dev/null +++ b/server/src/types/events.ts @@ -0,0 +1,78 @@ +import { z } from "zod" + +// Event config - contract IDs from env +export const CONTRACT_IDS = { + learnToken: process.env.LEARN_TOKEN_CONTRACT_ID!, + courseMilestone: process.env.COURSE_MILESTONE_CONTRACT_ID!, + scholarshipTreasury: process.env.SCHOLARSHIP_TREASURY_CONTRACT_ID!, + milestoneEscrow: process.env.MILESTONE_ESCROW_CONTRACT_ID!, + scholarNft: process.env.SCHOLAR_NFT_CONTRACT_ID!, +} as const + +export type ContractName = keyof typeof CONTRACT_IDS + +// Event topics (contract::topic) +export const EVENT_TOPICS = { + LearnToken_Mint: "LearnToken::Mint", + CourseMilestone_MilestoneComplete: "CourseMilestone::MilestoneComplete", + ScholarshipTreasury_Deposit: "ScholarshipTreasury::Deposit", + ScholarshipTreasury_ProposalCreated: "ScholarshipTreasury::ProposalCreated", + ScholarshipTreasury_VoteCastEvent: "ScholarshipTreasury::VoteCastEvent", + MilestoneEscrow_FundsDisbursed: "MilestoneEscrow::FundsDisbursed", + ScholarNft_Minted: "ScholarNFT::minted", + ScholarNft_Revoked: "ScholarNFT::revoked", +} as const + +export type EventTopic = keyof typeof EVENT_TOPICS +export type EventTopicValue = (typeof EVENT_TOPICS)[EventTopic] + +// Events to index: contract -> topics[] +export const EVENTS_TO_INDEX: Record = { + learnToken: ["LearnToken_Mint"], + courseMilestone: ["CourseMilestone_MilestoneComplete"], + scholarshipTreasury: [ + "ScholarshipTreasury_Deposit", + "ScholarshipTreasury_ProposalCreated", + "ScholarshipTreasury_VoteCastEvent", + ], + milestoneEscrow: ["MilestoneEscrow_FundsDisbursed"], + scholarNft: ["ScholarNft_Minted", "ScholarNft_Revoked"], +} as const + +// Zod schemas for event data parsing (extend as needed) +export const EVENT_DATA_SCHEMAS: Partial> = + { + "LearnToken::Mint": z.object({ + address: z.string(), + amount: z.string().regex(/^\d+$/), // uint128 as string + }), + "CourseMilestone::MilestoneComplete": z.object({ + address: z.string(), + courseId: z.string(), + milestoneId: z.string().regex(/^\d+$/), // u32 + }), + "ScholarNFT::minted": z.object({ + token_id: z.string().regex(/^\d+$/), + owner: z.string(), + }), + "ScholarNFT::revoked": z.object({ + token_id: z.string().regex(/^\d+$/), + reason: z.string(), + }), + // Add others... + } + +// DB Event row +export const DB_EVENT_SCHEMA = z.object({ + id: z.number(), + contract: z.string(), + event_type: z.string(), + data: z.record(z.unknown()), + ledger_sequence: z.bigint(), + created_at: z.string().datetime(), +}) + +// API response +export type ApiEvent = z.infer & { + // Add computed fields if needed +} diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts new file mode 100644 index 00000000..778fed38 --- /dev/null +++ b/server/src/types/express.d.ts @@ -0,0 +1,12 @@ +declare global { + namespace Express { + interface Request { + /** Correlation ID attached to each request */ + requestId?: string + /** Stellar public key (G...) after JWT verification */ + walletAddress?: string + } + } +} + +export {} diff --git a/server/src/types/sanitize-html.d.ts b/server/src/types/sanitize-html.d.ts new file mode 100644 index 00000000..e37fbb8b --- /dev/null +++ b/server/src/types/sanitize-html.d.ts @@ -0,0 +1,11 @@ +declare module "sanitize-html" { + interface SanitizeHtmlOptions { + allowedTags?: string[] + allowedAttributes?: Record + } + + export default function sanitizeHtml( + input: string, + options?: SanitizeHtmlOptions, + ): string +} diff --git a/server/src/utils/email.ts b/server/src/utils/email.ts new file mode 100644 index 00000000..6ede13c6 --- /dev/null +++ b/server/src/utils/email.ts @@ -0,0 +1,28 @@ +import nodemailer from "nodemailer" + +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: Number(process.env.SMTP_PORT), + secure: false, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS, + }, +}) + +export const sendEmail = async ({ + to, + subject, + html, +}: { + to: string + subject: string + html: string +}) => { + await transporter.sendMail({ + from: process.env.SMTP_FROM, + to, + subject, + html, + }) +} diff --git a/server/src/workers/escrow-timeout-worker.ts b/server/src/workers/escrow-timeout-worker.ts new file mode 100644 index 00000000..2c1cc2ce --- /dev/null +++ b/server/src/workers/escrow-timeout-worker.ts @@ -0,0 +1,35 @@ +import { processEscrowTimeouts } from "../services/escrow-timeout.service" + +const DEFAULT_INTERVAL_MS = 60 * 60 * 1000 +const intervalMs = Number.parseInt( + process.env.ESCROW_TIMEOUT_CRON_INTERVAL_MS || "", + 10, +) +const pollEveryMs = + Number.isFinite(intervalMs) && intervalMs > 0 + ? intervalMs + : DEFAULT_INTERVAL_MS + +let timer: NodeJS.Timeout | null = null + +export async function startEscrowTimeoutWorker(): Promise { + if (timer) { + return + } + + console.log(`[escrow-timeout] Worker started (interval=${pollEveryMs}ms)`) + + await processEscrowTimeouts() + + timer = setInterval(() => { + void processEscrowTimeouts() + }, pollEveryMs) +} + +export function stopEscrowTimeoutWorker(): void { + if (timer) { + clearInterval(timer) + timer = null + } + console.log("[escrow-timeout] Worker stopped") +} diff --git a/server/src/workers/event-poller.ts b/server/src/workers/event-poller.ts new file mode 100644 index 00000000..150c84c1 --- /dev/null +++ b/server/src/workers/event-poller.ts @@ -0,0 +1,57 @@ +import { rpc } from "@stellar/stellar-sdk" // dynamic later +import { INDEXER_CONFIG, getPollingTargets } from "../lib/event-config" +import { + indexEventsBatch, + getLastIndexedLedger, +} from "../services/event-indexer.service" + +let pollInterval: NodeJS.Timeout | null = null + +export async function startEventPoller(): Promise { + console.log("[poller] Starting event indexer...") + + // Get global latest ledger + const network = new rpc.Server(process.env.SOROBAN_RPC_URL!) + const info = await network.getLatestLedger() + let currentLedger = Number(info.sequence) + + pollInterval = setInterval(async () => { + try { + const newInfo = await network.getLatestLedger() + const latestLedger = Number(newInfo.sequence) + + if (currentLedger >= latestLedger) return + + // Simple: poll from current to latest in batches + const batchSize = INDEXER_CONFIG.batchSize + for ( + let start = currentLedger + 1; + start <= latestLedger; + start += batchSize + ) { + const end = Math.min(start + batchSize - 1, latestLedger) + await indexEventsBatch(start, end) + } + + currentLedger = latestLedger + } catch (err) { + console.error("[poller] Poll failed:", err) + } + }, INDEXER_CONFIG.pollIntervalMs) + + console.log( + `[poller] Running - poll ${INDEXER_CONFIG.pollIntervalMs}ms, batch ${INDEXER_CONFIG.batchSize}, from ledger ${INDEXER_CONFIG.startingLedger}`, + ) +} + +export function stopEventPoller(): void { + if (pollInterval) { + clearInterval(pollInterval) + pollInterval = null + } + console.log("[poller] Stopped") +} + +// Graceful shutdown +process.on("SIGTERM", stopEventPoller) +process.on("SIGINT", stopEventPoller) diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 00000000..295e5472 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "outDir": "dist", + "types": ["node", "jest"] + }, + "include": ["src/**/*.ts", "scripts/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/src/App.module.css b/src/App.module.css index 5b1cf417..05f2d7e2 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -45,9 +45,70 @@ a[target]:has(button)::after { justify-content: start; } -/** FOOTER */ -.AppLayout :global(.Layout__footer__content) { - flex-direction: row-reverse; +/** NAV BAR */ +.NavBar { + border-bottom: 1px solid var(--sds-clr-gray-06); + padding: 1rem 3rem; + background: var(--sds-clr-white); +} + +.NavBarContent { + display: flex; + align-items: center; + justify-content: space-between; + max-width: 1200px; + margin: 0 auto; +} + +.Logo { + text-decoration: none; + color: inherit; +} + +.NavLinks { + display: flex; + gap: 2rem; + align-items: center; +} + +.NavRight { + display: flex; + align-items: center; + gap: 1rem; +} + +.Hamburger { + display: none; +} + +/* Mobile styles */ +@media (max-width: 768px) { + .NavBar { + padding: 1rem; + } + + .NavLinks { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--sds-clr-white); + flex-direction: column; + justify-content: center; + align-items: center; + gap: 2rem; + z-index: 1000; + } + + .NavLinksOpen { + display: flex; + } + + .Hamburger { + display: block; + } } /** SDS FIXES */ diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 00000000..bac63386 --- /dev/null +++ b/src/App.test.tsx @@ -0,0 +1,105 @@ +import { type ReactNode } from "react" +import { MemoryRouter } from "react-router-dom" +import { describe, expect, it, vi } from "vitest" +import App from "./App" +import appSource from "./App.tsx?raw" +import { render, screen } from "./test/setup" + +vi.mock("./components/ErrorBoundary", () => ({ + default: ({ children }: { children: ReactNode }) => children, +})) + +vi.mock("./components/Footer", () => ({ + default: () =>
, +})) + +vi.mock("./components/NavBar", () => ({ + default: () =>
, +})) + +vi.mock("./components/NetworkPreconnect", () => ({ + default: () => null, +})) + +vi.mock("./components/Toast/ToastProvider", () => ({ + ToastProvider: ({ children }: { children: ReactNode }) => children, +})) + +vi.mock("./components/WalletToastWatcher", () => ({ + WalletToastWatcher: () => null, +})) + +vi.mock("./pages/Admin", () => ({ default: () =>
Admin
})) +vi.mock("./pages/Courses", () => ({ + default: () =>
Courses Page
, +})) +vi.mock("./pages/Credential", () => ({ + default: () =>
Credential
, +})) +vi.mock("./pages/Dao", () => ({ default: () =>
Dao
})) +vi.mock("./pages/DaoProposals", () => ({ + default: () =>
Dao Proposals
, +})) +vi.mock("./pages/DaoPropose", () => ({ + default: () =>
Dao Propose
, +})) +vi.mock("./pages/Dashboard", () => ({ + default: () =>
Dashboard
, +})) +vi.mock("./pages/Debug", () => ({ default: () =>
Debug
})) +vi.mock("./pages/Donor", () => ({ default: () =>
Donor
})) +vi.mock("./pages/Home", () => ({ default: () =>
Home
})) +vi.mock("./pages/Leaderboard", () => ({ + default: () =>
Leaderboard
, +})) +vi.mock("./pages/Learn", () => ({ default: () =>
Learn
})) +vi.mock("./pages/LessonView", () => ({ + default: () =>
Lesson View
, +})) +vi.mock("./pages/NotFound", () => ({ + default: () =>
Not Found
, +})) +vi.mock("./pages/Profile", () => ({ default: () =>
Profile
})) +vi.mock("./pages/ScholarshipApply", () => ({ + default: () =>
Scholarship Apply
, +})) +vi.mock("./pages/Treasury", () => ({ + default: () =>
Treasury
, +})) + +const getStaticRoutePaths = () => { + const routePaths = Array.from( + appSource.matchAll(/ path, + ) + + return routePaths.filter((path) => path !== "*" && !path.includes(":")) +} + +describe("App route definitions", () => { + it("navigates /courses to the Courses page", async () => { + render( + + + , + ) + + expect(await screen.findByText("Courses Page")).toBeInTheDocument() + }) + + it("does not keep the dead CourseCatalog component", () => { + expect(appSource).not.toMatch(/\b(?:const|function)\s+CourseCatalog\b/) + }) + + it("does not define duplicate static route paths", () => { + const staticPaths = getStaticRoutePaths() + const duplicatePaths = staticPaths.filter( + (path, index) => staticPaths.indexOf(path) !== index, + ) + + expect(duplicatePaths).toEqual([]) + expect(staticPaths.filter((path) => path === "/courses")).toHaveLength(1) + expect(staticPaths.filter((path) => path === "/profile")).toHaveLength(1) + expect(staticPaths.filter((path) => path === "/debug")).toHaveLength(1) + }) +}) diff --git a/src/App.tsx b/src/App.tsx index 8da0e03b..9c742815 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,139 +1,116 @@ -import { Button, Icon, Layout } from "@stellar/design-system" -import { Routes, Route, Outlet, NavLink } from "react-router-dom" -import styles from "./App.module.css" -import ConnectAccount from "./components/ConnectAccount" -import CourseCard from "./components/CourseCard" -import { labPrefix } from "./contracts/util" -import Debug from "./pages/Debug" -import Home from "./pages/Home" -// NAYA IMPORT: Apna CourseCard yahan import karo +import { lazy, Suspense, type ReactNode } from "react" +import { Outlet, Route, Routes } from "react-router-dom" +import ErrorBoundary from "./components/ErrorBoundary" +import Footer from "./components/Footer" +import NavBar from "./components/NavBar" +import NetworkPreconnect from "./components/NetworkPreconnect" +import { ToastProvider } from "./components/Toast/ToastProvider" +import { WalletToastWatcher } from "./components/WalletToastWatcher" -// TESTING KE LIYE DUMMY PAGE (Sirf UI check karne ke liye) -const CourseCatalog = () => ( -
-

- Course Catalog -

- {/* CSS Grid for Responsive Cards */} -
- alert("Enrolled in Soroban!")} - /> - -
-
+const Admin = lazy(() => import("./pages/Admin")) +const Community = lazy(() => import("./pages/Community")) +const Courses = lazy(() => import("./pages/Courses")) +const Credential = lazy(() => import("./pages/Credential")) +const Dao = lazy(() => import("./pages/Dao")) +const DaoProposals = lazy(() => import("./pages/DaoProposals")) +const DaoPropose = lazy(() => import("./pages/DaoPropose")) +const Dashboard = lazy(() => import("./pages/Dashboard")) +const Debug = lazy(() => import("./pages/Debug")) +const Donor = lazy(() => import("./pages/Donor")) +const Home = lazy(() => import("./pages/Home")) +const History = lazy(() => import("./pages/History")) +const Leaderboard = lazy(() => import("./pages/Leaderboard")) +const Learn = lazy(() => import("./pages/Learn")) +const LessonView = lazy(() => import("./pages/LessonView")) +const NotFound = lazy(() => import("./pages/NotFound")) +const Profile = lazy(() => import("./pages/Profile")) +const ScholarshipApply = lazy(() => import("./pages/ScholarshipApply")) +const Treasury = lazy(() => import("./pages/Treasury")) +const Wiki = lazy(() => import("./pages/Wiki")) +const WikiPage = lazy(() => import("./pages/WikiPage")) + +const renderRoute = (element: ReactNode) => ( + + }>{element} + ) function App() { return ( - - }> - } /> - {/* NAYA ROUTE: Yahan tumhara component dikhega */} - } /> - } /> - } /> - - + + + + }> + )} /> + )} /> + )} + /> + )} /> + )} /> + )} + /> + )} /> + )} /> + )} /> + )} /> + )} /> + )} + /> + )} + /> + )} /> + )} /> + )} /> + )} /> + )} /> + )} + /> + )} /> + )} /> + )} /> + )} /> + + + ) } -const AppLayout: React.FC = () => ( -
- - {/* NAYA BUTTON NAVBAR MEIN */} - - {({ isActive }) => ( - - )} - - - - {({ isActive }) => ( - - )} - - - - - - } - contentRight={} - /> +const RouteFallback = () => ( +
+
+
+
+
+ {Array.from({ length: 3 }).map((_, index) => ( +
+ ))} +
+
+
+) -
- - - - - +const AppLayout = () => ( + // Issue #61 — Theme-aware background using CSS variables + Tailwind dark: variant + ) diff --git a/src/components/ActivityFeed.tsx b/src/components/ActivityFeed.tsx new file mode 100644 index 00000000..a24bf5c1 --- /dev/null +++ b/src/components/ActivityFeed.tsx @@ -0,0 +1,192 @@ +import { formatDistanceToNow } from "date-fns" +import React from "react" +import { stellarNetwork } from "../contracts/util" +import { + useActivityFeed, + type ActivityEvent, + type ActivityEventType, + type ActivityEventFilter, +} from "../hooks/useActivityFeed" + +export interface ActivityFeedProps { + address: string | undefined + limit?: number + filter?: ActivityEventFilter + title?: string +} + +const EVENT_CONFIG: Record< + ActivityEventType, + { icon: string; label: string; color: string } +> = { + lrn_minted: { + icon: "\u{1F3C6}", + label: "LearnToken minted", + color: "text-yellow-400", + }, + course_enrolled: { + icon: "\u{1F4DA}", + label: "Course enrolled", + color: "text-blue-400", + }, + milestone_completed: { + icon: "\u2705", + label: "Milestone completed", + color: "text-emerald-400", + }, + scholar_nft_minted: { + icon: "\u{1F393}", + label: "ScholarNFT minted", + color: "text-purple-400", + }, + vote_cast: { + icon: "\u{1F5F3}\uFE0F", + label: "Vote cast", + color: "text-cyan-400", + }, + funds_disbursed: { + icon: "\u{1F4B0}", + label: "Funds disbursed", + color: "text-green-400", + }, +} + +function getExplorerUrl(txHash: string): string { + switch (stellarNetwork) { + case "PUBLIC": + return `https://stellar.expert/explorer/public/tx/${txHash}` + case "TESTNET": + return `https://stellar.expert/explorer/testnet/tx/${txHash}` + case "FUTURENET": + return `https://stellar.expert/explorer/futurenet/tx/${txHash}` + default: + return `https://stellar.expert/explorer/testnet/tx/${txHash}` + } +} + +function formatRelativeTime(timestamp: string): string { + try { + return formatDistanceToNow(new Date(timestamp), { addSuffix: true }) + } catch { + return "recently" + } +} + +function ActivityEventRow({ event }: { event: ActivityEvent }) { + const config = EVENT_CONFIG[event.type] + + return ( +
+
+ {config.icon} +
+
+

+ {event.description} +

+
+ + {config.label} + + + {formatRelativeTime(event.timestamp)} + +
+
+ {event.txHash && ( + + View Tx → + + )} +
+ ) +} + +function ActivityFeedSkeleton() { + return ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+
+
+
+ ))} +
+ ) +} + +function EmptyState() { + return ( +
+
🚀
+

+ No activity yet — start learning! +

+
+ ) +} + +export function ActivityFeed({ + address, + limit = 10, + filter = "all", + title = "Activity Feed", +}: ActivityFeedProps) { + const { events, isLoading, error, hasMore, loadMore } = useActivityFeed( + address, + limit, + filter, + ) + + return ( +
+
+

{title}

+
+
+ +
+ {isLoading ? ( + + ) : error ? ( +
+

{error}

+
+ ) : events.length === 0 ? ( + + ) : ( + <> +
+ {events.map((event) => ( + + ))} +
+ {hasMore && ( +
+ +
+ )} + + )} +
+
+ ) +} + +export default ActivityFeed diff --git a/src/components/AddressDisplay.tsx b/src/components/AddressDisplay.tsx new file mode 100644 index 00000000..dc86ac3d --- /dev/null +++ b/src/components/AddressDisplay.tsx @@ -0,0 +1,174 @@ +import { motion, AnimatePresence } from "framer-motion" +import { useState, useId } from "react" +import { useWallet } from "../hooks/useWallet" +import { stellarNetwork } from "../contracts/util" + +export interface AddressDisplayProps { + address?: string | null + className?: string + addressClassName?: string + buttonClassName?: string + prefixLength?: number + suffixLength?: number + showCopyButton?: boolean + showExplorerLink?: boolean + fullOnHover?: boolean +} + +export function truncateAddress( + address: string, + prefixLength = 6, + suffixLength = 4, +): string { + if (!address) return "" + if (address.length <= prefixLength + suffixLength + 3) return address + return `${address.slice(0, prefixLength)}...${address.slice(-suffixLength)}` +} + +export const AddressDisplay: React.FC = ({ + address, + className = "", + addressClassName = "", + buttonClassName = "", + prefixLength = 6, + suffixLength = 4, + showCopyButton = true, + showExplorerLink = true, + fullOnHover = true, +}) => { + const [copied, setCopied] = useState(false) + const [isHovered, setIsHovered] = useState(false) + const { network: walletNetwork } = useWallet() + const tooltipId = useId() + + if (!address) return null + + const truncated = truncateAddress(address, prefixLength, suffixLength) + + const handleCopy = async (e: React.MouseEvent) => { + e.stopPropagation() + try { + await navigator.clipboard.writeText(address) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch (err) { + console.error("Failed to copy:", err) + } + } + + const getExplorerUrl = () => { + const activeNetwork = (walletNetwork || stellarNetwork).toLowerCase() + const baseUrl = + activeNetwork.includes("public") || activeNetwork.includes("mainnet") + ? "https://stellar.expert/explorer/public/account/" + : activeNetwork.includes("futurenet") + ? "https://futurenet.stellar.expert/explorer/futurenet/account/" + : "https://testnet.stellar.expert/explorer/testnet/account/" + return `${baseUrl}${address}` + } + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+ + {fullOnHover && isHovered ? address : truncated} + + + + {isHovered && !fullOnHover && ( + + {address} + + )} + +
+ + {showCopyButton && ( + + + {copied ? ( + + + + ) : ( + + + + + )} + + + )} + + {showExplorerLink && ( + + + + + + + + )} +
+ ) +} + +export default AddressDisplay diff --git a/src/components/ComingSoon.module.css b/src/components/ComingSoon.module.css new file mode 100644 index 00000000..0dadaa70 --- /dev/null +++ b/src/components/ComingSoon.module.css @@ -0,0 +1,26 @@ +.rocket { + animation: bounce 2s infinite; +} + +@keyframes bounce { + 0%, + 20%, + 50%, + 80%, + 100% { + transform: translateY(0); + } + 40% { + transform: translateY(-10px); + } + 60% { + transform: translateY(-5px); + } +} + +/* Optional: responsive utilities if not using Tailwind */ +@media (max-width: 640px) { + pre { + font-size: 1.5rem !important; + } +} diff --git a/src/components/ComingSoon.tsx b/src/components/ComingSoon.tsx new file mode 100644 index 00000000..0b0c6e4a --- /dev/null +++ b/src/components/ComingSoon.tsx @@ -0,0 +1,44 @@ +import { Link } from "react-router-dom" +import styles from "./ComingSoon.module.css" // optional + +interface ComingSoonProps { + title: string + issueUrl?: string +} + +export function ComingSoon({ title, issueUrl }: ComingSoonProps) { + return ( +
+
+ {/* LearnVault rocket 🚀 */} +
+					{"🚀\n /_\\\n |o| \n  | \n / \\"}
+				
+
+ +

+ {title} +

+ +

+ This page is under construction. We're building something awesome for + the LearnVault community! +

+ +
+

Stay tuned for updates 📚✨

+ + {issueUrl && ( + + View open issues → + + )} +
+
+ ) +} diff --git a/src/components/CommentCard.tsx b/src/components/CommentCard.tsx new file mode 100644 index 00000000..ed90fbd2 --- /dev/null +++ b/src/components/CommentCard.tsx @@ -0,0 +1,398 @@ +import { formatDistanceToNow } from "date-fns" +import React, { useId, useState } from "react" +import ReactMarkdown from "react-markdown" +import { getAuthToken } from "../util/auth" +import AddressDisplay from "./AddressDisplay" + +const API_BASE = import.meta.env.VITE_SERVER_URL ?? "http://localhost:4000" + +export interface Comment { + id: number + proposal_id: string + author_address: string + parent_id: number | null + content: string + upvotes: number + downvotes: number + is_pinned: boolean + created_at: string +} + +interface CommentCardProps { + comment: Comment + isAuthor?: boolean + isReply?: boolean + canPin?: boolean + onUpdate?: () => void +} + +const API_URL = ( + (import.meta.env.VITE_API_URL as string | undefined) ?? + (import.meta.env.VITE_SERVER_URL as string | undefined) ?? + "" +).replace(/\/$/, "") + +const CommentCard: React.FC = ({ + comment, + isAuthor, + isReply, + canPin, + onUpdate, +}) => { + const [isReplying, setIsReplying] = useState(false) + const [replyText, setReplyText] = useState("") + const [replyError, setReplyError] = useState(null) + const [isFlagging, setIsFlagging] = useState(false) + const [flagReason, setFlagReason] = useState("") + const [flagError, setFlagError] = useState(null) + const replyFieldId = useId() + const replyHintId = `${replyFieldId}-hint` + const replyErrorId = `${replyFieldId}-error` + const replySectionId = `${replyFieldId}-section` + const authorId = `comment-${comment.id}-author` + + const handleVote = async (type: "upvote" | "downvote") => { + const token = getAuthToken() + if (!token) return + try { + const res = await fetch(`${API_URL}/api/comments/${comment.id}/vote`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ type }), + }) + if (res.ok) onUpdate?.() + } catch (err) { + console.error("Vote failed", err) + } + } + + const handlePin = async () => { + const token = getAuthToken() + if (!token) return + try { + const res = await fetch(`${API_URL}/api/comments/${comment.id}/pin`, { + method: "PUT", + headers: { + Authorization: `Bearer ${token}`, + }, + }) + if (res.ok) onUpdate?.() + } catch (err) { + console.error("Pin failed", err) + } + } + + const handleFlag = async () => { + if (!flagReason.trim()) { + setFlagError("Please provide a reason for reporting this comment.") + return + } + + if (flagReason.length < 10) { + setFlagError("Reason must be at least 10 characters.") + return + } + + const token = getAuthToken() + if (!token) { + setFlagError("Sign in to report content.") + return + } + + setFlagError(null) + try { + const res = await fetch(`${API_URL}/api/content/flag`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + contentType: "comment", + contentId: comment.id, + reason: flagReason, + }), + }) + + if (res.ok) { + setIsFlagging(false) + setFlagReason("") + // Show success message + } else { + const err = await res.json().catch(() => ({})) + setFlagError(err.error || "Failed to report comment.") + } + } catch (err) { + console.error("Flag failed", err) + setFlagError("Failed to report comment.") + } + } + + const handlePostReply = async () => { + if (!replyText.trim()) { + setReplyError("Enter a reply before submitting.") + return + } + + const token = getAuthToken() + if (!token) { + setReplyError("Sign in to reply.") + return + } + setReplyError(null) + + try { + const res = await fetch(`${API_URL}/api/comments`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + proposalId: comment.proposal_id, + content: replyText, + parentId: comment.id, + }), + }) + if (res.ok) { + setReplyText("") + setIsReplying(false) + onUpdate?.() + } else { + const err = await res.json().catch(() => ({})) + setReplyError(err.error || "Reply failed.") + } + } catch (err) { + console.error("Reply failed", err) + setReplyError("Reply failed.") + } + } + + const toggleReply = () => { + setIsReplying((current) => !current) + setReplyError(null) + } + + const replyDescriptionIds = [ + replyHintId, + replyError ? replyErrorId : undefined, + ] + .filter(Boolean) + .join(" ") + + return ( +
+ {comment.is_pinned && ( +
+ Pinned by Author +
+ )} + +
+
+
+ {comment.author_address.slice(0, 2)} +
+
+
+ + {isAuthor && ( + + Author + + )} +
+

+ {formatDistanceToNow(new Date(comment.created_at))} ago +

+
+
+ +
+ {canPin && !comment.is_pinned && ( + + )} + {!isReply && ( + + )} + +
+
+ +
+ {comment.content} +
+ +
+
+ + + {comment.upvotes - comment.downvotes} + + +
+
+ + {isReplying && ( +
+ +

+ Markdown is supported. +

+