diff --git a/.env.example b/.env.example index 6c10224..ba75deb 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,22 @@ -# Supabase Configuration -# You can find these values in your Supabase Dashboard under Project Settings > API -NEXT_PUBLIC_SUPABASE_URL=your-project-url-here.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here +# ── Database (Neon Postgres) ────────────────────────────────────────────────── +# Pooled connection string from the Neon console (Connection Details → Pooled). +# Used by the app (Drizzle over Neon's serverless driver), the keeper scripts, +# and `npm run db:migrate`. SERVER-ONLY. +DATABASE_URL=postgres://user:password@ep-xxx-pooler.region.aws.neon.tech/neondb?sslmode=require + +# ── Sessions ────────────────────────────────────────────────────────────────── +# Signs the HttpOnly session cookie (HS256 JWT) issued after SEP-10 sign-in. +# At least 32 characters. Rotating it signs everyone out. +# openssl rand -base64 48 +SESSION_SECRET= + +# ── File storage (Vercel Blob) ──────────────────────────────────────────────── +# KYC documents are stored as private blobs. Create a Blob store in the Vercel +# dashboard (Storage → Blob) and copy its read/write token. +BLOB_READ_WRITE_TOKEN= # Application Configuration -# This is used for OAuth redirects and email confirmation links +# Public origin of the app (referral links, e-mail links, SIWS domain default). # Local development: http://localhost:3000 # Production: https://your-domain.com NEXT_PUBLIC_SITE_URL=http://localhost:3000 @@ -48,11 +60,6 @@ NEXT_PUBLIC_SIWS_DOMAIN=localhost:3000 # SEP-10 server signing key (S...). SERVER-ONLY — generate a dedicated key, # never reuse the platform admin key. stellar keys generate trustlend-siws --global SIWS_SERVER_SECRET= -# -# Secret used to deterministically derive each wallet's Supabase auth password -# (HMAC-SHA256 of the address). SERVER-ONLY — use a long random value and never -# rotate without a migration plan (rotating invalidates existing wallet logins). -SIWS_PASSWORD_SECRET= NEXT_PUBLIC_STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org NEXT_PUBLIC_STELLAR_FRIENDBOT_URL=https://friendbot.stellar.org @@ -60,8 +67,10 @@ NEXT_PUBLIC_STELLAR_FRIENDBOT_URL=https://friendbot.stellar.org # These keys are used internally to manage role-based redirects NEXT_PUBLIC_PENDING_ROLE_KEY=trustlend_pending_role -# Comma-separated allowlist for Trade Vault admin panel access -# Example: admin1@tradevault.com,admin2@tradevault.com +# Comma-separated allowlist for the admin panel. Accepts e-mail addresses and +# Stellar public keys (G...), since accounts are wallet-based. An allowlisted +# account must ALSO have profiles.role = 'admin'. +# Example: GABC...XYZ,admin@example.com TRADE_VAULT_ADMIN_EMAILS= # ── Soroban RPC ─────────────────────────────────────────────────────────────── @@ -122,9 +131,6 @@ ADMIN_SECRET_KEY= DEFAULT_GRACE_PERIOD_DAYS=7 DEFAULT_INSURANCE_PAYOUT_DAYS=60 -# Supabase service-role key — required by all crons for trusted DB access. -SUPABASE_SERVICE_ROLE_KEY= - # ── Decentralized Credit Oracle ─────────────────────────────────────────────── # The authorized oracle is the only account allowed to post off-chain credit # scores on-chain (via `submit_credit_score`). Register it once after deploy: @@ -140,17 +146,18 @@ NEXT_PUBLIC_ORACLE_ADDRESS= ORACLE_SECRET_KEY= # ── Liquidation Keeper (scripts/liquidation-keeper.ts) ──────────────────────── -# Automated bot that liquidates under-collateralized loans. Deployed as a -# background worker that monitors every minute: vercel.json schedules -# POST /api/cron/liquidation on "* * * * *" (authenticated with Bearer CRON_SECRET, -# same as the other crons) — or run `npm run liquidation:keeper:service` +# Automated bot that liquidates under-collateralized loans. Vercel Hobby only +# permits daily crons, so vercel.json runs POST /api/cron/liquidation once a +# day as a safety net and .github/workflows/keepers.yml hits the same endpoint +# every 5 minutes (needs the KEEPER_BASE_URL + CRON_SECRET repo secrets) — or +# run `npm run liquidation:keeper:service` # (--interval=60) self-hosted. One-shot `npm run liquidation:keeper` remains # available for cron schedulers. Requires ADMIN_SECRET_KEY (above) to sign # liquidation transactions, plus NEXT_PUBLIC_LENDING_CONTRACT_ID / # NEXT_PUBLIC_REPUTATION_CONTRACT_ID / NEXT_PUBLIC_ADMIN_ADDRESS. # -# Where to source open loans from: "db" (Supabase, default) or "chain" -# (iterate the LendingContract directly — no Supabase needed). +# Where to source open loans from: "db" (the database, default) or "chain" +# (iterate the LendingContract directly — no database needed). LIQUIDATION_SOURCE=db # Evaluate only; never submit a liquidation transaction. Useful for staging. LIQUIDATION_DRY_RUN=false @@ -287,10 +294,10 @@ ORACLE_DISCORD_WEBHOOK_URL= # In CI these are repository *secrets*, not values in this file. Restore # instructions and bucket setup live in docs/disaster-recovery.md. # -# Direct Postgres connection string. Use the DIRECT connection (port 5432), not -# the pooled/pgbouncer one — pg_dump needs session-level features the pooler -# does not provide. Supabase: Project Settings → Database → Connection string. -DATABASE_URL= +# pg_dump needs the DIRECT (non-pooled) Neon connection string — the same +# DATABASE_URL as above with the "-pooler" segment removed from the host. Set +# BACKUP_DATABASE_URL when it differs; otherwise DATABASE_URL is used. +BACKUP_DATABASE_URL= # # Passphrase used to encrypt each dump with AES-256 before upload. # ⚠️ Store this in a password manager as well as in CI. If it is lost, every diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b4b973..2d39620 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ jobs: runs-on: ubuntu-latest env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} steps: - name: Checkout Code uses: actions/checkout@v4 @@ -59,16 +60,25 @@ jobs: - name: Lint run: npm run lint + - name: Unit tests + run: npm test + - name: Build Next.js - # We set this environment variable because dummy keys are needed for the build if relying on env + # Placeholder env so the build never needs real secrets. env: - NEXT_PUBLIC_SUPABASE_URL: "https://example.supabase.co" - NEXT_PUBLIC_SUPABASE_ANON_KEY: "dummy-key" NEXT_PUBLIC_STELLAR_NETWORK: "testnet" NEXT_PUBLIC_STELLAR_HORIZON_URL: "https://horizon-testnet.stellar.org" NEXT_PUBLIC_ADMIN_ADDRESS: "GAJRNUO6HSMQG4FNHNWQVRXJZJZ7QRA7HXPYYB6H5PTA3EAAJXJNZD7U" run: npm run build + # Apply pending Drizzle migrations to the production database before the + # deploy goes live. No-op until the DATABASE_URL secret exists. + - name: Migrate database + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && env.DATABASE_URL != '' }} + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + run: npm run db:migrate + - name: Deploy to Vercel if: ${{ env.VERCEL_TOKEN != '' }} run: npx vercel --prod --yes --token=${{ env.VERCEL_TOKEN }} diff --git a/.github/workflows/e2e-playwright.yml b/.github/workflows/e2e-playwright.yml index 1bf8e6d..94465bc 100644 --- a/.github/workflows/e2e-playwright.yml +++ b/.github/workflows/e2e-playwright.yml @@ -33,8 +33,6 @@ jobs: - name: Build Next.js Application # We supply dummy configuration since tests intercept the actual calls env: - NEXT_PUBLIC_SUPABASE_URL: "https://example.supabase.co" - NEXT_PUBLIC_SUPABASE_ANON_KEY: "dummy-key" NEXT_PUBLIC_STELLAR_NETWORK: "testnet" NEXT_PUBLIC_STELLAR_HORIZON_URL: "https://horizon-testnet.stellar.org" NEXT_PUBLIC_ADMIN_ADDRESS: "GAJRNUO6HSMQG4FNHNWQVRXJZJZ7QRA7HXPYYB6H5PTA3EAAJXJNZD7U" @@ -42,8 +40,6 @@ jobs: - name: Run Playwright tests env: - NEXT_PUBLIC_SUPABASE_URL: "https://example.supabase.co" - NEXT_PUBLIC_SUPABASE_ANON_KEY: "dummy-key" NEXT_PUBLIC_STELLAR_NETWORK: "testnet" NEXT_PUBLIC_STELLAR_HORIZON_URL: "https://horizon-testnet.stellar.org" NEXT_PUBLIC_ADMIN_ADDRESS: "GAJRNUO6HSMQG4FNHNWQVRXJZJZ7QRA7HXPYYB6H5PTA3EAAJXJNZD7U" diff --git a/.github/workflows/keepers.yml b/.github/workflows/keepers.yml new file mode 100644 index 0000000..89096e3 --- /dev/null +++ b/.github/workflows/keepers.yml @@ -0,0 +1,53 @@ +name: Keepers (liquidation + price oracle) + +# Vercel Hobby only allows one cron run per day, which is far too slow for the +# liquidation keeper and the collateral price oracle. GitHub Actions can fire +# every 5 minutes, so it pings the same authenticated cron endpoints instead. +# +# Required repository secrets: +# KEEPER_BASE_URL e.g. https://trustlend-stellar.vercel.app (no trailing slash) +# CRON_SECRET must match the CRON_SECRET env var configured on Vercel +# +# The workflow is a no-op until both secrets exist, so it is safe on forks. + +on: + schedule: + - cron: "*/5 * * * *" + workflow_dispatch: + +concurrency: + group: keepers + cancel-in-progress: false + +jobs: + run: + name: Trigger cron endpoints + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + KEEPER_BASE_URL: ${{ secrets.KEEPER_BASE_URL }} + CRON_SECRET: ${{ secrets.CRON_SECRET }} + steps: + - name: Skip when secrets are not configured + id: gate + run: | + if [ -z "$KEEPER_BASE_URL" ] || [ -z "$CRON_SECRET" ]; then + echo "KEEPER_BASE_URL / CRON_SECRET not set — nothing to do." + echo "configured=false" >> "$GITHUB_OUTPUT" + else + echo "configured=true" >> "$GITHUB_OUTPUT" + fi + + - name: Liquidation keeper + if: steps.gate.outputs.configured == 'true' + run: | + curl --fail-with-body --silent --show-error --max-time 120 \ + -X POST "$KEEPER_BASE_URL/api/cron/liquidation" \ + -H "Authorization: Bearer $CRON_SECRET" + + - name: Price oracle + if: steps.gate.outputs.configured == 'true' + run: | + curl --fail-with-body --silent --show-error --max-time 120 \ + -X POST "$KEEPER_BASE_URL/api/cron/price-oracle" \ + -H "Authorization: Bearer $CRON_SECRET" diff --git a/README.md b/README.md index 28a9bcd..f6a5b51 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Next.js React TypeScript - Supabase + Neon Postgres Stellar Soroban Stellar Wave @@ -61,7 +61,7 @@ TrustLend is designed as a foundational layer for decentralized, inclusive credi ## 🏗️ Architecture & Workflow -TrustLend uses a practical hybrid architecture: **fast UX off-chain** (Supabase/Next.js) combined with **trust-critical logic on-chain** (Soroban/Stellar). The diagram below maps every component and data flow across all six layers of the platform. +TrustLend uses a practical hybrid architecture: **fast UX off-chain** (Next.js + Neon Postgres) combined with **trust-critical logic on-chain** (Soroban/Stellar). The diagram below maps every component and data flow across all six layers of the platform. ```mermaid flowchart TB @@ -84,7 +84,7 @@ flowchart TB subgraph Backend["⚙️ Backend Layer (Next.js)"] direction TB SA[("📡 Server Actions & API Routes
app/actions + app/api")] - SB[("🗄️ Supabase
PostgreSQL · Auth · RLS · Storage")] + SB[("🗄️ Neon Postgres
Drizzle ORM · sessions · Vercel Blob")] RM[("🔌 Soroban Client
lib/stellar/soroban.ts")] SC[("🔐 Server-side Contract Invoker
lib/stellar/server-contract.ts")] RC[("⚡ Redis Cache
Simulation result cache")] @@ -210,7 +210,7 @@ flowchart LR style S fill:#3b82f6,color:#fff ``` -1. **Onboarding:** User signs up via Supabase Auth, connects a Stellar wallet (Freighter / xBull / Albedo on desktop, or any WalletConnect v2 mobile wallet such as LOBSTR by scanning a QR code), completes KYC verification, and their on-chain reputation profile is initialized. +1. **Onboarding:** User signs in with their Stellar wallet (SEP-10 challenge signature; no passwords) (Freighter / xBull / Albedo on desktop, or any WalletConnect v2 mobile wallet such as LOBSTR by scanning a QR code), completes KYC verification, and their on-chain reputation profile is initialized. 2. **Borrowing:** Borrower submits a loan request. The Next.js backend calls `ReputationContract.calculate_max_loan` and `calculate_interest_rate` to determine eligibility and terms. 3. **Lending:** Lender reviews the request in the marketplace, approves it, and the `LendingContract.approve_loan` is called. Funds are locked via `EscrowContract.create_escrow_hold`. 4. **Disbursement:** After the 1-hour revocation window expires, the admin confirms disbursement. `EscrowContract.confirm_disbursement` releases funds to the borrower, and `LendingContract.activate_loan` marks the loan as active. @@ -221,7 +221,7 @@ flowchart LR | Automation | Trigger | Action | |---|---|---| -| **Payment-Due Scheduler** | Vercel Cron (hourly) | Queries Supabase for loans due within 48h → Sends webhook & email | +| **Payment-Due Scheduler** | Vercel Cron (daily) | Queries the database for loans due within 48h → Sends webhook & email | | **Default Management** | Vercel Cron (daily) | Checks overdue loans against ledger time → Marks defaulted on-chain → Proposes insurance payout via MultiSigAdmin (requires N-of-M human approval) | | **Liquidation Keeper** | Manual / cron | Monitors LTV ratios against dynamic thresholds → Liquidates under-collateralized positions → Posts Slack/Discord alerts | | **Oracle Credit Score** | Manual / cron | Posts verified off-chain credit scores to the Reputation contract | @@ -233,7 +233,7 @@ flowchart LR | Layer | Technology | |---|---| | **Frontend** | Next.js 16, React 19, TypeScript, Tailwind CSS 4, Framer Motion | -| **Backend & DB** | Supabase (Auth, Postgres RLS, Storage) | +| **Backend & DB** | Neon Postgres + Drizzle ORM, SEP-10 wallet sessions (`jose`), Vercel Blob for KYC files | | **Blockchain** | Stellar Testnet, Soroban RPC, Horizon API | | **Wallet** | Freighter Wallet, xBull, Albedo, WalletConnect v2 for mobile wallets (`@creit.tech/stellar-wallets-kit`) | | **Smart Contracts** | Rust (Soroban, `wasm32v1-none`) — 8 contracts deployed | @@ -293,7 +293,7 @@ npm run deploy:testnet:dry ### What it writes Contract IDs land directly in `.env.local`. Keys already present are updated **in -place** — your Supabase keys, API secrets and comments are left untouched, and a +place** — your database URL, API secrets and comments are left untouched, and a `.env.local.bak` is taken first. A reference copy also goes to `.env.contracts`. | Contract | Env key | @@ -368,8 +368,8 @@ TrustLend includes an automated scheduler that checks for loans with payment dea ### How It Works -1. An external scheduler (Vercel Cron or any HTTP trigger) calls `POST /api/cron/payment-due` hourly. -2. The route queries Supabase for `active` or `funded` loans with `due_at` between now and +48 hours. +1. An external scheduler (Vercel Cron or any HTTP trigger) calls `POST /api/cron/payment-due` daily. +2. The route queries the database for `active` or `funded` loans with `due_at` between now and +48 hours. 3. A POST webhook is sent to `WEBHOOK_NOTIFICATION_URL` for each qualifying loan. 4. The loan's `metadata.payment_due_notified_at` is set to prevent duplicate notifications. 5. Per-loan errors are logged without stopping the rest of the batch. @@ -380,7 +380,7 @@ TrustLend includes an automated scheduler that checks for loans with payment dea |---|---| | `WEBHOOK_NOTIFICATION_URL` | URL of the notification service that receives payment-due webhook POSTs | | `CRON_SECRET` | Secret token used to authenticate scheduler requests (`Authorization: Bearer `) | -| `SUPABASE_SERVICE_ROLE_KEY` | Supabase service-role key (required for RLS-bypassing loan queries) | +| `DATABASE_URL` | Neon Postgres connection string | | `RESEND_API_KEY` | Optional Resend API key for borrower email notifications | | `RESEND_FROM_EMAIL` | Verified sender address used for TrustLend emails | | `RESEND_REPLY_TO_EMAIL` | Optional reply-to address for support responses | diff --git a/SECURITY.md b/SECURITY.md index a96e870..070b54b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -45,7 +45,7 @@ If you discover a security vulnerability within TrustLend, **do NOT open a publi - **Frontend/UI Bugs:** Visual presentation flaws, CSS issues, or non-security UI bugs without impact on user funds or data. - **Denial of Service (DoS):** Volumetric DoS/DDoS attacks against infrastructure or public Stellar RPC endpoints not caused by application design flaws. - **Social Engineering:** Phishing, spam, or social engineering attacks targeted at TrustLend maintainers or users. -- **Third-Party Dependencies:** Vulnerabilities in underlying infrastructure (e.g. Stellar Core, Soroban SDK, Supabase platform) unless directly exploitable through TrustLend code logic. +- **Third-Party Dependencies:** Vulnerabilities in underlying infrastructure (e.g. Stellar Core, Soroban SDK, Neon, Vercel) unless directly exploitable through TrustLend code logic. - **Known Issues:** Vulnerabilities already reported, tracked in public issues/PRs, or previously disclosed in security audit reports. --- diff --git a/__tests__/api/analytics.test.ts b/__tests__/api/analytics.test.ts index d81b0f8..936932f 100644 --- a/__tests__/api/analytics.test.ts +++ b/__tests__/api/analytics.test.ts @@ -5,12 +5,14 @@ import { clearAnalyticsMemoryCacheForTests, } from "@/lib/analytics-cache"; -const mockGetServiceRoleClient = vi.fn(); +import { createFakeDb } from "../helpers/fake-db"; + +const mockGetDb = vi.fn(); const mockGetCachedPlatformAnalytics = vi.fn(); const mockSetCachedPlatformAnalytics = vi.fn(); -vi.mock("@/lib/supabase/server", () => ({ - getServiceRoleClient: () => mockGetServiceRoleClient(), +vi.mock("@/lib/db/client", () => ({ + getDb: () => mockGetDb(), })); vi.mock("@/lib/analytics-cache", () => ({ @@ -19,77 +21,27 @@ vi.mock("@/lib/analytics-cache", () => ({ clearAnalyticsMemoryCacheForTests: vi.fn(), })); -function createClientStub() { - return { - from: (table: string) => ({ - select: () => { - if (table === "loans") { - return Promise.resolve({ - data: [ - { principal_amount: 1000, status: "funded" }, - { principal_amount: 2500, status: "active" }, - { principal_amount: 999, status: "requested" }, - ], - error: null, - }); - } - - if (table === "pool_positions") { - return Promise.resolve({ - data: [ - { principal_amount: 4000, earned_interest: 120, status: "active" }, - { principal_amount: 500, earned_interest: 40, status: "closed" }, - ], - error: null, - }); - } - - if (table === "loan_repayments") { - return Promise.resolve({ - data: [ - { amount: 600 }, - { amount: 500 }, - ], - error: null, - }); - } - - if (table === "ledger_transactions") { - return Promise.resolve({ - data: [ - { - amount: 1200, - user_id: "u1", - status: "confirmed", - created_at: new Date().toISOString(), - }, - { - amount: 800, - user_id: "u2", - status: "confirmed", - created_at: new Date().toISOString(), - }, - { - amount: 300, - user_id: "u3", - status: "pending", - created_at: new Date().toISOString(), - }, - ], - error: null, - }); - } - - return Promise.resolve({ data: [], error: null }); - }, - }), - } as unknown as { - from: ( - table: string, - ) => { - select: () => Promise<{ data: unknown[]; error: null }>; - }; - }; +/** + * fetchPlatformAnalytics runs four queries in parallel (loans, pool positions, + * repayments, ledger); queue the results in that order. + */ +function createDbStub() { + const db = createFakeDb(); + db.queue([ + { principal_amount: "1000", status: "funded" }, + { principal_amount: "2500", status: "active" }, + ]); + db.queue([ + { principal_amount: "4000", earned_interest: "120", status: "active" }, + { principal_amount: "500", earned_interest: "40", status: "closed" }, + ]); + db.queue([{ amount: "600" }, { amount: "500" }]); + db.queue([ + { amount: "1200", user_id: "u1", status: "confirmed", created_at: new Date() }, + { amount: "800", user_id: "u2", status: "confirmed", created_at: new Date() }, + { amount: "300", user_id: "u3", status: "pending", created_at: new Date() }, + ]); + return db; } describe("GET /api/analytics", () => { @@ -121,7 +73,7 @@ describe("GET /api/analytics", () => { } as unknown as NextRequest); expect(response.status).toBe(200); - expect(mockGetServiceRoleClient).not.toHaveBeenCalled(); + expect(mockGetDb).not.toHaveBeenCalled(); expect(mockSetCachedPlatformAnalytics).not.toHaveBeenCalled(); expect(await response.json()).toEqual(cachedPayload); expect(response.headers.get("x-analytics-cache")).toBe("hit"); @@ -129,7 +81,7 @@ describe("GET /api/analytics", () => { it("returns aggregated platform metrics and stores them in cache", async () => { mockGetCachedPlatformAnalytics.mockResolvedValue(null); - mockGetServiceRoleClient.mockReturnValue(createClientStub()); + mockGetDb.mockReturnValue(createDbStub()); const response = await GET({ nextUrl: new URL("http://localhost/api/analytics"), @@ -154,7 +106,7 @@ describe("GET /api/analytics", () => { it("returns a service unavailable response when the client cannot be created", async () => { mockGetCachedPlatformAnalytics.mockResolvedValue(null); - mockGetServiceRoleClient.mockReturnValue(null); + mockGetDb.mockReturnValue(null); const response = await GET({ nextUrl: new URL("http://localhost/api/analytics"), diff --git a/__tests__/api/cron/liquidation.test.ts b/__tests__/api/cron/liquidation.test.ts index b032bc0..9a99d1d 100644 --- a/__tests__/api/cron/liquidation.test.ts +++ b/__tests__/api/cron/liquidation.test.ts @@ -4,7 +4,7 @@ import path from "node:path"; import { NextRequest } from "next/server"; import type { KeeperConfig } from "@/scripts/liquidation-keeper"; -// ── Mock the keeper module (no real RPC / Supabase in tests) ────────────────── +// ── Mock the keeper module (no real RPC / database in tests) ────────────────── const mockLoadConfig = vi.fn(); const mockRunLiquidationKeeper = vi.fn(); @@ -118,16 +118,25 @@ describe("POST /api/cron/liquidation", () => { }); }); -// ── Acceptance criterion: the worker monitors prices every minute ────────────── -// vercel.json must schedule the liquidation cron on a 1-minute cadence. +// ── Scheduling ───────────────────────────────────────────────────────────────── +// Vercel Hobby rejects any cron that runs more than once a day, so vercel.json +// keeps a daily safety-net schedule and the 5-minute cadence lives in +// .github/workflows/keepers.yml. Both must keep pointing at this route. -describe("vercel.json liquidation schedule", () => { - it("schedules /api/cron/liquidation every minute (* * * * *)", () => { +describe("liquidation cron scheduling", () => { + it("vercel.json schedules /api/cron/liquidation once a day (Hobby-compatible)", () => { const raw = fs.readFileSync(path.resolve(process.cwd(), "vercel.json"), "utf8"); const crons = (JSON.parse(raw) as { crons: Array<{ path: string; schedule: string }> }).crons; const liquidationCron = crons.find((c) => c.path === "/api/cron/liquidation"); expect(liquidationCron).toBeDefined(); - expect(liquidationCron?.schedule).toBe("* * * * *"); + // "m h * * *" — exactly one run per day + expect(liquidationCron?.schedule).toMatch(/^\d{1,2} \d{1,2} \* \* \*$/); + }); + + it("the GitHub Actions keeper workflow triggers the route every 5 minutes", () => { + const raw = fs.readFileSync(path.resolve(process.cwd(), ".github/workflows/keepers.yml"), "utf8"); + expect(raw).toContain(`cron: "*/5 * * * *"`); + expect(raw).toContain("/api/cron/liquidation"); }); }); diff --git a/__tests__/api/kyc/token.test.ts b/__tests__/api/kyc/token.test.ts index cc0abeb..522b67b 100644 --- a/__tests__/api/kyc/token.test.ts +++ b/__tests__/api/kyc/token.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { NextRequest } from "next/server"; import type { KycApplicantResult } from "@/lib/kyc/types"; +import { createFakeDb, type FakeDb } from "../../helpers/fake-db"; // ── Mock auth (session) ─────────────────────────────────────────────────────── const mockRequireAuthenticatedUser = vi.fn(); @@ -8,12 +9,10 @@ vi.mock("@/lib/auth/session", () => ({ requireAuthenticatedUser: (...args: unknown[]) => mockRequireAuthenticatedUser(...args), })); -// ── Mock Supabase clients ────────────────────────────────────────────────────── -const mockGetServerSupabaseClient = vi.fn(); -const mockGetServiceRoleClient = vi.fn(); -vi.mock("@/lib/supabase/server", () => ({ - getServerSupabaseClient: () => mockGetServerSupabaseClient(), - getServiceRoleClient: () => mockGetServiceRoleClient(), +// ── Mock database ───────────────────────────────────────────────────────────── +const mockGetDb = vi.fn(); +vi.mock("@/lib/db/client", () => ({ + getDb: () => mockGetDb(), })); // ── Mock the SumSub provider ─────────────────────────────────────────────────── @@ -35,30 +34,21 @@ const TOKEN_RESULT: KycApplicantResult = { }; function user(role: "borrower" | "lender" | "admin") { - return { user: { id: "user-1", email: "a@b.com" }, role }; + return { user: { id: "user-1", email: "a@b.com", fullName: "", walletAddress: "GABC" }, role }; } -/** Supabase client whose profiles query resolves to `profile`. */ -function makeProfilesClient(profile: Record | null) { - const chain = { - from: vi.fn(() => chain), - select: vi.fn(() => chain), - eq: vi.fn(() => chain), - maybeSingle: vi.fn(() => Promise.resolve({ data: profile, error: null })), - }; - mockGetServerSupabaseClient.mockReturnValue(chain); - return chain; +/** Database whose first profiles lookup resolves to `profile` (camelCase columns). */ +function makeDb(profile: Record | null): FakeDb { + const db = createFakeDb(); + db.queue(profile ? [profile] : []); + mockGetDb.mockReturnValue(db); + return db; } -/** Service-role client used to persist kyc_provider_id (bypasses RLS). */ -function makeServiceClient() { - const chain = { - from: vi.fn(() => chain), - update: vi.fn(() => chain), - eq: vi.fn(() => Promise.resolve({ error: null })), - }; - mockGetServiceRoleClient.mockReturnValue(chain); - return chain; +/** The `set({...})` payload of the first update issued on the fake db. */ +function persistedUpdate(db: FakeDb): Record { + const set = db.calls.find((c) => c.method === "set"); + return (set?.args[0] ?? {}) as Record; } function post() { @@ -72,8 +62,7 @@ describe("POST /api/kyc/token", () => { it("issues a KYC SDK token for a lender (issue #262 — AC1)", async () => { mockRequireAuthenticatedUser.mockResolvedValue(user("lender")); - makeProfilesClient({ full_name: "Jane Lender", kyc_provider_id: null, kyc_status: "pending" }); - makeServiceClient(); + const db = makeDb({ fullName: "Jane Lender", kycProviderId: null, kycStatus: "pending" }); mockGetApplicantId.mockResolvedValue(null); mockCreateApplicant.mockResolvedValue("appl-lender-1"); mockGenerateSdkToken.mockResolvedValue(TOKEN_RESULT); @@ -84,14 +73,14 @@ describe("POST /api/kyc/token", () => { expect(await response.json()).toEqual(TOKEN_RESULT); expect(mockCreateApplicant).toHaveBeenCalledWith("user-1", "a@b.com", "Jane Lender"); expect(mockGenerateSdkToken).toHaveBeenCalledWith("appl-lender-1", "user-1"); - // Provider id persisted via service role - expect(mockGetServiceRoleClient().from).toHaveBeenCalledWith("profiles"); + // Provider id persisted on the profile + expect(db.calls.some((c) => c.method === "update")).toBe(true); + expect(persistedUpdate(db).kycProviderId).toBe("appl-lender-1"); }); it("reuses an existing applicant found via the provider and persists it", async () => { mockRequireAuthenticatedUser.mockResolvedValue(user("borrower")); - makeProfilesClient({ full_name: "Bob Borrower", kyc_provider_id: null, kyc_status: "submitted" }); - makeServiceClient(); + const db = makeDb({ fullName: "Bob Borrower", kycProviderId: null, kycStatus: "submitted" }); mockGetApplicantId.mockResolvedValue("appl-existing"); mockGenerateSdkToken.mockResolvedValue(TOKEN_RESULT); @@ -101,9 +90,9 @@ describe("POST /api/kyc/token", () => { expect(mockCreateApplicant).not.toHaveBeenCalled(); expect(mockGenerateSdkToken).toHaveBeenCalledWith("appl-existing", "user-1"); // Persisted with the existing (non-pending) status preserved - const persisted = mockGetServiceRoleClient().from("profiles").update.mock.calls[0][0] as Record; - expect(persisted.kyc_provider_id).toBe("appl-existing"); - expect(persisted.kyc_status).toBe("submitted"); + const persisted = persistedUpdate(db); + expect(persisted.kycProviderId).toBe("appl-existing"); + expect(persisted.kycStatus).toBe("submitted"); }); it("redirects admins away instead of issuing a customer KYC token", async () => { @@ -113,9 +102,9 @@ describe("POST /api/kyc/token", () => { expect(mockGenerateSdkToken).not.toHaveBeenCalled(); }); - it("returns 503 when the database client is unavailable", async () => { + it("returns 503 when the database is unavailable", async () => { mockRequireAuthenticatedUser.mockResolvedValue(user("lender")); - mockGetServerSupabaseClient.mockReturnValue(null); + mockGetDb.mockReturnValue(null); const response = await post(); @@ -125,8 +114,7 @@ describe("POST /api/kyc/token", () => { it("returns 500 when the provider fails", async () => { mockRequireAuthenticatedUser.mockResolvedValue(user("lender")); - makeProfilesClient({ full_name: "Jane Lender", kyc_provider_id: null, kyc_status: "pending" }); - makeServiceClient(); + makeDb({ fullName: "Jane Lender", kycProviderId: null, kycStatus: "pending" }); mockGetApplicantId.mockResolvedValue(null); mockCreateApplicant.mockRejectedValue(new Error("SumSub API error 401")); @@ -145,13 +133,13 @@ describe("GET /api/kyc/token", () => { it("returns the current KYC status for a lender", async () => { mockRequireAuthenticatedUser.mockResolvedValue(user("lender")); - makeProfilesClient({ - kyc_status: "verified", - kyc_provider_id: "appl-lender-1", - kyc_submitted_at: "2026-08-01T00:00:00.000Z", - kyc_verified_at: "2026-08-02T00:00:00.000Z", - kyc_rejection_reason: null, - regulated_pool_access: true, + makeDb({ + kycStatus: "verified", + kycProviderId: "appl-lender-1", + kycSubmittedAt: new Date("2026-08-01T00:00:00.000Z"), + kycVerifiedAt: new Date("2026-08-02T00:00:00.000Z"), + kycRejectionReason: null, + regulatedPoolAccess: true, }); const response = await GET(new NextRequest("http://localhost/api/kyc/token")); @@ -160,13 +148,14 @@ describe("GET /api/kyc/token", () => { expect(await response.json()).toMatchObject({ kycStatus: "verified", applicantId: "appl-lender-1", + submittedAt: "2026-08-01T00:00:00.000Z", regulatedPoolAccess: true, }); }); it("defaults to pending when no profile exists", async () => { mockRequireAuthenticatedUser.mockResolvedValue(user("lender")); - makeProfilesClient(null); + makeDb(null); const response = await GET(new NextRequest("http://localhost/api/kyc/token")); @@ -176,13 +165,11 @@ describe("GET /api/kyc/token", () => { it("returns 401 when the status lookup fails", async () => { mockRequireAuthenticatedUser.mockResolvedValue(user("lender")); - const chain = { - from: vi.fn(() => chain), - select: vi.fn(() => chain), - eq: vi.fn(() => chain), - maybeSingle: vi.fn(() => Promise.reject(new Error("db down"))), + const db = createFakeDb(); + db.select = () => { + throw new Error("db down"); }; - mockGetServerSupabaseClient.mockReturnValue(chain); + mockGetDb.mockReturnValue(db); const response = await GET(new NextRequest("http://localhost/api/kyc/token")); diff --git a/__tests__/api/kyc/webhook.test.ts b/__tests__/api/kyc/webhook.test.ts index 24c55f0..c7d09ee 100644 --- a/__tests__/api/kyc/webhook.test.ts +++ b/__tests__/api/kyc/webhook.test.ts @@ -7,13 +7,14 @@ import { } from "@/lib/kyc/provider"; import type { SumSubWebhookPayload } from "@/lib/kyc/types"; -// ── Mock only Supabase — the real provider signature/status mapping runs ────── -const mockGetServiceRoleClient = vi.fn(); -const mockFrom = vi.fn(); -const mockRpc = vi.fn(); +import { createFakeDb, type FakeDb } from "../../helpers/fake-db"; -vi.mock("@/lib/supabase/server", () => ({ - getServiceRoleClient: () => mockGetServiceRoleClient(), +// ── Mock only the database — the real provider signature/status mapping runs ── +const mockGetDb = vi.fn(); +let db: FakeDb; + +vi.mock("@/lib/db/client", () => ({ + getDb: () => mockGetDb(), })); import { POST, GET } from "@/app/api/kyc/webhook/route"; @@ -44,15 +45,26 @@ function reviewedPayload(overrides: Partial = {}): SumSubW }; } -/** profiles.update(...).eq(...) chain whose eq() resolves per call. */ -function makeUpdateChain(results: Array<{ error: unknown }> = [{ error: null }]) { - const queue = [...results]; - const chain = { - update: vi.fn((_payload: Record) => chain), - eq: vi.fn(() => Promise.resolve(queue.shift() ?? { error: null })), - }; - mockFrom.mockReturnValue(chain); - return chain; +/** + * Queue the row counts the profile update(s) will report. The route updates + * by user id first and falls back to the provider id when nothing matched. + */ +function primeUpdates(...matched: number[]) { + db.reset(); + for (const n of matched) db.queue(Array.from({ length: n }, () => ({ id: "user-1" }))); +} + +/** The set({...}) payload of the first profile update. */ +function firstUpdatePayload(): Record { + return (db.calls.find((c) => c.method === "set")?.args[0] ?? {}) as Record; +} + +function updateCount(): number { + return db.calls.filter((c) => c.method === "update").length; +} + +function seededReputation(): boolean { + return db.calls.some((c) => c.method === "onConflictDoUpdate"); } describe("POST /api/kyc/webhook", () => { @@ -60,31 +72,28 @@ describe("POST /api/kyc/webhook", () => { process.env = { ...ORIGINAL_ENV }; vi.clearAllMocks(); process.env.SUMSUB_WEBHOOK_SECRET = WEBHOOK_SECRET; - mockGetServiceRoleClient.mockReturnValue({ from: mockFrom, rpc: mockRpc }); + db = createFakeDb(); + mockGetDb.mockReturnValue(db); }); it("auto-updates the profile to verified on a GREEN review (AC2)", async () => { const body = JSON.stringify(reviewedPayload()); - const chain = makeUpdateChain(); - mockRpc.mockResolvedValue({ error: null }); + primeUpdates(1); const response = await POST(webhookRequest(body, digest(body))); expect(response.status).toBe(200); expect(await response.json()).toMatchObject({ received: true, status: "verified" }); - expect(chain.update).toHaveBeenCalledOnce(); - const payload = chain.update.mock.calls[0][0]; - expect(payload.kyc_status).toBe("verified"); - expect(payload.regulated_pool_access).toBe(true); - expect(payload.kyc_provider_id).toBe("appl-1"); - expect(payload.kyc_verified_at).toBeTruthy(); - expect(payload.kyc_rejection_reason).toBeNull(); + expect(updateCount()).toBe(1); + const payload = firstUpdatePayload(); + expect(payload.kycStatus).toBe("verified"); + expect(payload.regulatedPoolAccess).toBe(true); + expect(payload.kycProviderId).toBe("appl-1"); + expect(payload.kycVerifiedAt).toBeTruthy(); + expect(payload.kycRejectionReason).toBeNull(); // Reputation snapshot seeded on first verification - expect(mockRpc).toHaveBeenCalledWith("seed_reputation_snapshot", { - p_user_id: "user-1", - p_initial_score: 100, - }); + expect(seededReputation()).toBe(true); }); it("marks the profile rejected with a reason on a FINAL RED review", async () => { @@ -98,16 +107,16 @@ describe("POST /api/kyc/webhook", () => { }, }) ); - const chain = makeUpdateChain(); + primeUpdates(1); const response = await POST(webhookRequest(body, digest(body))); expect(response.status).toBe(200); - const payload = chain.update.mock.calls[0][0]; - expect(payload.kyc_status).toBe("rejected"); - expect(payload.regulated_pool_access).toBe(false); - expect(payload.kyc_rejection_reason).toContain("DOCUMENT_MISMATCH"); - expect(mockRpc).not.toHaveBeenCalled(); + const payload = firstUpdatePayload(); + expect(payload.kycStatus).toBe("rejected"); + expect(payload.regulatedPoolAccess).toBe(false); + expect(payload.kycRejectionReason).toContain("DOCUMENT_MISMATCH"); + expect(seededReputation()).toBe(false); }); it("keeps a RETRY rejection as submitted (resubmission allowed)", async () => { @@ -116,26 +125,26 @@ describe("POST /api/kyc/webhook", () => { reviewResult: { reviewAnswer: "RED", reviewRejectType: "RETRY" }, }) ); - const chain = makeUpdateChain(); + primeUpdates(1); const response = await POST(webhookRequest(body, digest(body))); expect(response.status).toBe(200); - const payload = chain.update.mock.calls[0][0]; - expect(payload.kyc_status).toBe("submitted"); - expect(payload.regulated_pool_access).toBe(false); + const payload = firstUpdatePayload(); + expect(payload.kycStatus).toBe("submitted"); + expect(payload.regulatedPoolAccess).toBe(false); }); it("marks pending applicants as submitted", async () => { const body = JSON.stringify(reviewedPayload({ type: "applicantPending" })); - const chain = makeUpdateChain(); + primeUpdates(1); const response = await POST(webhookRequest(body, digest(body))); expect(response.status).toBe(200); - const payload = chain.update.mock.calls[0][0]; - expect(payload.kyc_status).toBe("submitted"); - expect(payload.kyc_submitted_at).toBeTruthy(); + const payload = firstUpdatePayload(); + expect(payload.kycStatus).toBe("submitted"); + expect(payload.kycSubmittedAt).toBeTruthy(); }); it("rejects requests with an invalid signature (401) and never touches the DB", async () => { @@ -144,18 +153,18 @@ describe("POST /api/kyc/webhook", () => { const response = await POST(webhookRequest(body, "deadbeef")); expect(response.status).toBe(401); - expect(mockFrom).not.toHaveBeenCalled(); + expect(updateCount()).toBe(0); }); it("falls back to the provider-id lookup when the user-id update fails", async () => { const body = JSON.stringify(reviewedPayload()); - // first eq() (by user id) errors → fallback eq() (by provider id) succeeds - const chain = makeUpdateChain([{ error: new Error("db down") }, { error: null }]); + // the update by user id matches nothing → fallback update by provider id + primeUpdates(0, 1); const response = await POST(webhookRequest(body, digest(body))); expect(response.status).toBe(200); - expect(chain.update).toHaveBeenCalledTimes(2); + expect(updateCount()).toBe(2); }); it("returns 400 when applicantId or externalUserId is missing", async () => { @@ -164,7 +173,7 @@ describe("POST /api/kyc/webhook", () => { const response = await POST(webhookRequest(body, digest(body))); expect(response.status).toBe(400); - expect(mockFrom).not.toHaveBeenCalled(); + expect(updateCount()).toBe(0); }); it("returns 400 on malformed JSON", async () => { @@ -175,7 +184,7 @@ describe("POST /api/kyc/webhook", () => { }); it("acknowledges the webhook (200) when the service client is unavailable", async () => { - mockGetServiceRoleClient.mockReturnValue(null); + mockGetDb.mockReturnValue(null); const body = JSON.stringify(reviewedPayload()); const response = await POST(webhookRequest(body, digest(body))); @@ -186,7 +195,7 @@ describe("POST /api/kyc/webhook", () => { it("accepts the webhook in dev mode when no webhook secret is configured", async () => { delete process.env.SUMSUB_WEBHOOK_SECRET; - makeUpdateChain(); + primeUpdates(1); const body = JSON.stringify(reviewedPayload()); const response = await POST(webhookRequest(body, "")); diff --git a/__tests__/api/loans/apply.test.ts b/__tests__/api/loans/apply.test.ts index 907e364..8c97e98 100644 --- a/__tests__/api/loans/apply.test.ts +++ b/__tests__/api/loans/apply.test.ts @@ -22,13 +22,14 @@ vi.mock("@/lib/notifications", () => ({ createNotification: vi.fn().mockResolvedValue({ id: "notif-1" }), })); -// ── Mock Supabase client ────────────────────────────────────────────────────── -const mockGetServerSupabaseClient = vi.fn(); -vi.mock("@/lib/supabase/server", () => ({ - getServerSupabaseClient: () => mockGetServerSupabaseClient(), +// ── Mock database ───────────────────────────────────────────────────────────── +const mockGetDb = vi.fn(); +vi.mock("@/lib/db/client", () => ({ + getDb: () => mockGetDb(), })); import { POST } from "@/app/api/loans/apply/route"; +import { createFakeDb } from "../../helpers/fake-db"; function makeMockRequest(body: Record) { return new NextRequest("http://localhost/api/loans/apply", { @@ -48,10 +49,7 @@ describe("POST /api/loans/apply - Minimum Borrow Amount Validation", () => { }); it("rejects dust loan amount below 1 XLM with 400 status", async () => { - const mockDb = { - from: vi.fn(), - }; - mockGetServerSupabaseClient.mockResolvedValue(mockDb); + mockGetDb.mockReturnValue(createFakeDb()); const req = makeMockRequest({ amount: 0.0000001, @@ -67,10 +65,7 @@ describe("POST /api/loans/apply - Minimum Borrow Amount Validation", () => { }); it("rejects zero or negative loan amounts with 400 status", async () => { - const mockDb = { - from: vi.fn(), - }; - mockGetServerSupabaseClient.mockResolvedValue(mockDb); + mockGetDb.mockReturnValue(createFakeDb()); const req = makeMockRequest({ amount: 0, diff --git a/__tests__/helpers/fake-db.ts b/__tests__/helpers/fake-db.ts new file mode 100644 index 0000000..0c23611 --- /dev/null +++ b/__tests__/helpers/fake-db.ts @@ -0,0 +1,95 @@ +/** + * A minimal stand-in for the Drizzle handle returned by lib/db/client.getDb(). + * + * Every query builder method (`select`, `from`, `where`, `orderBy`, `limit`, + * `insert`, `values`, `update`, `set`, `delete`, `returning`, `leftJoin`, + * `offset`, `onConflictDoNothing`, `onConflictDoUpdate`) returns the same + * chain, and awaiting the chain resolves with the next queued result. + * `db.execute()` also consumes the queue and resolves `{ rows }`. + * + * const db = createFakeDb(); + * db.queue([{ id: "loan-1" }]); // first query resolves to this + * db.queue([]); // second query resolves to [] + * + * Results are consumed in the order the code awaits them. Use `db.calls` to + * assert which builder methods ran and with what arguments. + */ + +type Call = { method: string; args: unknown[] }; + +export interface FakeDb { + queue(result: unknown): FakeDb; + /** Results that will be handed out, in order. */ + pending: unknown[]; + calls: Call[]; + reset(): void; + // Chain entry points + select: (...args: unknown[]) => FakeChain; + insert: (...args: unknown[]) => FakeChain; + update: (...args: unknown[]) => FakeChain; + delete: (...args: unknown[]) => FakeChain; + execute: (...args: unknown[]) => Promise<{ rows: unknown[] }>; +} + +export interface FakeChain extends PromiseLike { + [key: string]: unknown; +} + +const CHAIN_METHODS = [ + "from", + "where", + "orderBy", + "limit", + "offset", + "leftJoin", + "innerJoin", + "values", + "set", + "returning", + "onConflictDoNothing", + "onConflictDoUpdate", + "groupBy", +]; + +export function createFakeDb(): FakeDb { + const db: FakeDb = { + pending: [], + calls: [], + queue(result: unknown) { + db.pending.push(result); + return db; + }, + reset() { + db.pending = []; + db.calls = []; + }, + select: (...args) => chain("select", args), + insert: (...args) => chain("insert", args), + update: (...args) => chain("update", args), + delete: (...args) => chain("delete", args), + execute: async (...args) => { + db.calls.push({ method: "execute", args }); + const next = db.pending.shift(); + return { rows: Array.isArray(next) ? next : next === undefined ? [] : [next] }; + }, + }; + + function chain(method: string, args: unknown[]): FakeChain { + db.calls.push({ method, args }); + const c: FakeChain = { + then(onFulfilled, onRejected) { + const next = db.pending.shift(); + return Promise.resolve(next === undefined ? [] : next).then(onFulfilled, onRejected); + }, + }; + for (const m of CHAIN_METHODS) { + c[m] = (...a: unknown[]) => { + db.calls.push({ method: m, args: a }); + return c; + }; + } + return c; + } + + return db; +} diff --git a/__tests__/lib/analytics.test.ts b/__tests__/lib/analytics.test.ts index ceb71b4..4ffa3b6 100644 --- a/__tests__/lib/analytics.test.ts +++ b/__tests__/lib/analytics.test.ts @@ -30,6 +30,7 @@ describe("aggregatePlatformAnalytics", () => { ], }, 30 * 24 * 60 * 60 * 1000, + now, ); expect(metrics).toEqual({ diff --git a/__tests__/lib/email.test.ts b/__tests__/lib/email.test.ts index 54d6eab..86c0c93 100644 --- a/__tests__/lib/email.test.ts +++ b/__tests__/lib/email.test.ts @@ -1,18 +1,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -// ── Mock Supabase service role client ───────────────────────────────────────── -const mockGetUserById = vi.fn(); - -vi.mock("@/lib/supabase/server", () => ({ - getServiceRoleClient: () => ({ - auth: { - admin: { - getUserById: mockGetUserById, - }, - }, - }), +import { createFakeDb } from "../helpers/fake-db"; + +// ── Mock the database: getUserEmail() runs one select on users ─────────────── +const fakeDb = createFakeDb(); + +vi.mock("@/lib/db/client", () => ({ + getDb: () => fakeDb, })); +/** Queue the users row the next e-mail lookup will read. */ +function primeUserEmail(email: string | null) { + fakeDb.reset(); + fakeDb.queue(email === null ? [] : [{ email }]); +} + import { isResendConfigured, sendLoanApprovedEmail, @@ -102,10 +104,7 @@ describe("Resend Email Delivery", () => { }); it("sends loan funded email immediately to the borrower", async () => { - mockGetUserById.mockResolvedValue({ - data: { user: { email: "borrower@example.com" } }, - error: null, - }); + primeUserEmail("borrower@example.com"); const fetchMock = vi.fn().mockResolvedValue({ ok: true }); vi.stubGlobal("fetch", fetchMock); @@ -116,7 +115,7 @@ describe("Resend Email Delivery", () => { loanId: "loan-abc", }); - expect(mockGetUserById).toHaveBeenCalledWith("user-123"); + expect(fakeDb.calls.some((c) => c.method === "select")).toBe(true); expect(fetchMock).toHaveBeenCalledOnce(); const [url, options] = fetchMock.mock.calls[0]; @@ -132,10 +131,7 @@ describe("Resend Email Delivery", () => { }); it("sends loan approved email to borrower", async () => { - mockGetUserById.mockResolvedValue({ - data: { user: { email: "borrower@example.com" } }, - error: null, - }); + primeUserEmail("borrower@example.com"); const fetchMock = vi.fn().mockResolvedValue({ ok: true }); vi.stubGlobal("fetch", fetchMock); @@ -154,10 +150,7 @@ describe("Resend Email Delivery", () => { }); it("handles missing user email gracefully without throwing", async () => { - mockGetUserById.mockResolvedValue({ - data: { user: null }, - error: { message: "User not found" }, - }); + primeUserEmail(null); const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); @@ -174,10 +167,7 @@ describe("Resend Email Delivery", () => { }); it("catches and logs API failures without unhandled rejections", async () => { - mockGetUserById.mockResolvedValue({ - data: { user: { email: "borrower@example.com" } }, - error: null, - }); + primeUserEmail("borrower@example.com"); vi.stubGlobal( "fetch", diff --git a/__tests__/lib/sep31.test.ts b/__tests__/lib/sep31.test.ts index 3a7b82a..fb06d4f 100644 --- a/__tests__/lib/sep31.test.ts +++ b/__tests__/lib/sep31.test.ts @@ -11,7 +11,7 @@ vi.mock("@stellar/stellar-sdk", () => ({ Keypair: { fromPublicKey: (key: string) => ({ verify: (data: Buffer, sig: Buffer) => { - return sig.toString("base64") === "mockSignatureBase64"; + return sig.toString("utf8") === "mock-signature"; }, }), }, @@ -245,7 +245,7 @@ describe("Stellar SEP-31 Client", () => { it("correctly identifies valid signatures using Ed25519", async () => { const isValid = await verifyAnchorSignature( '{"status":"completed"}', - "mockSignatureBase64", + Buffer.from("mock-signature").toString("base64"), "GCSW6Y6W7QA2SV6OQNK2STU2QL2IWOJM4XNKV56A476I2V4JSU46A6N2" ); diff --git a/__tests__/scheduler/default-management.test.ts b/__tests__/scheduler/default-management.test.ts index c4de164..6ba7b78 100644 --- a/__tests__/scheduler/default-management.test.ts +++ b/__tests__/scheduler/default-management.test.ts @@ -17,10 +17,11 @@ vi.mock("@/lib/stellar/server-contract", () => ({ xlmToStroops: (xlm: number) => BigInt(Math.round(xlm * 10_000_000)), })); -// ── Mock Supabase service-role client ────────────────────────────────────────── -const mockFrom = vi.fn(); -vi.mock("@/lib/supabase/server", () => ({ - getServiceRoleClient: () => ({ from: mockFrom }), +// ── Mock the database ───────────────────────────────────────────────────────── +import { createFakeDb } from "../helpers/fake-db"; +let db = createFakeDb(); +vi.mock("@/lib/db/client", () => ({ + getDb: () => db, })); import { @@ -54,24 +55,27 @@ describe("computeDaysOverdue", () => { // ── runDefaultManagement ─────────────────────────────────────────────────────── -/** A chain mock where every builder method returns itself; reads resolve via - * maybeSingle() (sequenced) and awaits resolve via the thenable. */ -function makeSupabase(loans: unknown[], maybeSingleQueue: unknown[]) { - const queue = [...maybeSingleQueue]; - const chain: Record = {}; - const ret = () => chain; - for (const m of ["select", "in", "not", "lt", "eq", "update"]) chain[m] = vi.fn(ret); - chain.maybeSingle = vi.fn(() => - Promise.resolve(queue.length ? queue.shift() : { data: null }) +/** + * Queue the results the run will read, in order: the overdue-loans query, + * then per loan the funding ledger row and the borrower's wallet, then any + * metadata-flag update. + */ +function makeDb(loans: ReturnType[], perLoan: unknown[][]) { + db = createFakeDb(); + db.queue( + loans.map((l) => ({ + id: l.id, + borrowerId: l.borrower_id, + status: l.status, + principalAmount: String(l.principal_amount), + repaidAmount: String(l.repaid_amount), + dueAt: l.due_at ? new Date(l.due_at as string) : null, + defaultedAt: l.defaulted_at ? new Date(l.defaulted_at as string) : null, + metadata: l.metadata, + })), ); - // Awaiting the chain (the overdue-loans query) resolves to the loan list. - Object.defineProperty(chain, "then", { - get() { - return (resolve: (v: unknown) => void) => resolve({ data: loans, error: null }); - }, - }); - mockFrom.mockReturnValue(chain); - return chain; + for (const rows of perLoan) db.queue(rows); + return db; } const NOW = 1_700_000_000; @@ -98,7 +102,7 @@ describe("runDefaultManagement", () => { }); it("skips loans still within the grace period", async () => { - makeSupabase([overdueLoan(3)], []); + makeDb([overdueLoan(3)], []); const res = await runDefaultManagement(); expect(res.scanned).toBe(1); expect(res.defaulted).toBe(0); @@ -107,13 +111,13 @@ describe("runDefaultManagement", () => { }); it("marks a past-grace loan defaulted (no payout before insurance threshold)", async () => { - // maybeSingle order: ledger funding info, borrower wallet, loans metadata (for flag write) - makeSupabase( + // read order: ledger funding info, borrower wallet, then the flag update + makeDb( [overdueLoan(30)], [ - { data: { metadata: { lenderAddress: "GLENDER", onchainLoanId: 7 } } }, - { data: { wallet_address: "GBORROWER" } }, - { data: { metadata: {} } }, + [{ metadata: { lenderAddress: "GLENDER", onchainLoanId: 7 }, amount: "1000" }], + [{ walletAddress: "GBORROWER" }], + [], ] ); const res = await runDefaultManagement(); @@ -124,11 +128,11 @@ describe("runDefaultManagement", () => { }); it("does not re-default an already-defaulted loan", async () => { - makeSupabase( + makeDb( [overdueLoan(30, { defaulted_at: new Date(NOW * 1000).toISOString() })], [ - { data: { metadata: { lenderAddress: "GLENDER", onchainLoanId: 7 } } }, - { data: { wallet_address: "GBORROWER" } }, + [{ metadata: { lenderAddress: "GLENDER", onchainLoanId: 7 }, amount: "1000" }], + [{ walletAddress: "GBORROWER" }], ] ); const res = await runDefaultManagement(); @@ -137,7 +141,7 @@ describe("runDefaultManagement", () => { }); it("reports counts and never throws on a clean run", async () => { - makeSupabase([], []); + makeDb([], []); const res = await runDefaultManagement(); expect(res).toMatchObject({ scanned: 0, defaulted: 0, payoutsProposed: 0, failed: 0 }); expect(res.ledgerTime).toBe(new Date(NOW * 1000).toISOString()); diff --git a/__tests__/scheduler/payment-due.test.ts b/__tests__/scheduler/payment-due.test.ts index a943336..3afe696 100644 --- a/__tests__/scheduler/payment-due.test.ts +++ b/__tests__/scheduler/payment-due.test.ts @@ -1,17 +1,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { createFakeDb, type FakeDb } from "../helpers/fake-db"; -// ── Mock Supabase service role client ───────────────────────────────────────── -const _mockUpdate = vi.fn(); -const _mockSingle = vi.fn(); -const _mockSelect = vi.fn(); -const mockFrom = vi.fn(); -const mockRpc = vi.fn(); - -vi.mock("@/lib/supabase/server", () => ({ - getServiceRoleClient: () => ({ - from: mockFrom, - rpc: mockRpc, - }), +// ── Mock the database ───────────────────────────────────────────────────────── +let db: FakeDb | null; + +vi.mock("@/lib/db/client", () => ({ + getDb: () => db, })); import { @@ -37,25 +31,22 @@ function makeLoan(overrides: Partial = {}): DueLoan { }; } -function buildSelectChain(data: unknown, error: unknown = null) { - const chain = { - select: vi.fn().mockReturnThis(), - in: vi.fn().mockReturnThis(), - not: vi.fn().mockReturnThis(), - gt: vi.fn().mockReturnThis(), - lte: vi.fn().mockReturnThis(), - eq: vi.fn().mockReturnThis(), - single: vi.fn().mockResolvedValue({ data, error }), - update: vi.fn().mockReturnThis(), - then: undefined as unknown, +/** The camelCase row shape the loans query returns for a DueLoan. */ +function toDbRow(loan: DueLoan) { + return { + id: loan.id, + borrowerId: loan.borrower_id, + dueAt: loan.due_at ? new Date(loan.due_at) : null, + principalAmount: String(loan.principal_amount), + repaidAmount: String(loan.repaid_amount), + metadata: loan.metadata, }; - // Make the chain itself resolve like a promise (for .lte(...) which is the terminal call) - Object.defineProperty(chain, "then", { - get() { - return (resolve: (v: unknown) => void) => resolve({ data, error }); - }, - }); - return chain; +} + +function primeDueLoans(loans: DueLoan[]) { + db = createFakeDb(); + db.queue(loans.map(toDbRow)); + return db; } // ── queryDueLoans ────────────────────────────────────────────────────────────── @@ -64,22 +55,19 @@ describe("queryDueLoans", () => { beforeEach(() => vi.clearAllMocks()); it("returns loans due within 48 hours that are not yet notified", async () => { - const loan = makeLoan(); - const chain = buildSelectChain([loan]); - mockFrom.mockReturnValue(chain); + primeDueLoans([makeLoan()]); const result = await queryDueLoans(); expect(result).toHaveLength(1); expect(result[0].id).toBe("loan-1"); + expect(result[0].principal_amount).toBe(1000); }); it("filters out loans already marked as notified", async () => { - const notifiedLoan = makeLoan({ - metadata: { payment_due_notified_at: "2026-06-27T00:00:00.000Z" }, - }); - const chain = buildSelectChain([notifiedLoan]); - mockFrom.mockReturnValue(chain); + primeDueLoans([ + makeLoan({ metadata: { payment_due_notified_at: "2026-06-27T00:00:00.000Z" } }), + ]); const result = await queryDueLoans(); @@ -87,25 +75,21 @@ describe("queryDueLoans", () => { }); it("returns empty array when no loans are due", async () => { - const chain = buildSelectChain([]); - mockFrom.mockReturnValue(chain); + primeDueLoans([]); const result = await queryDueLoans(); expect(result).toHaveLength(0); }); - it("throws when Supabase returns an error", async () => { - const chain = buildSelectChain(null, { message: "DB error" }); - mockFrom.mockReturnValue(chain); + it("throws when the database is not configured", async () => { + db = null; - await expect(queryDueLoans()).rejects.toThrow("Failed to query due loans: DB error"); + await expect(queryDueLoans()).rejects.toThrow("Database unavailable"); }); it("handles multiple qualifying loans", async () => { - const loans = [makeLoan({ id: "loan-1" }), makeLoan({ id: "loan-2" })]; - const chain = buildSelectChain(loans); - mockFrom.mockReturnValue(chain); + primeDueLoans([makeLoan({ id: "loan-1" }), makeLoan({ id: "loan-2" })]); const result = await queryDueLoans(); @@ -155,30 +139,21 @@ describe("sendWebhookNotification", () => { describe("markLoanNotified", () => { beforeEach(() => vi.clearAllMocks()); - it("falls back to manual metadata merge when RPC is unavailable", async () => { - mockRpc.mockResolvedValue({ error: { message: "function not found" } }); - - const updateFn = vi.fn().mockResolvedValue({ error: null }); - const _eqFn = vi.fn().mockReturnValue({ error: null }); - const chain = { - select: vi.fn().mockReturnThis(), - eq: vi.fn().mockReturnValue({ - single: vi.fn().mockResolvedValue({ - data: { metadata: { existing_key: "value" } }, - error: null, - }), - }), - update: vi.fn().mockReturnValue({ eq: updateFn }), - }; - mockFrom.mockReturnValue(chain); + it("merges the notified timestamp into loans.metadata with an update", async () => { + db = createFakeDb(); + db.queue([]); await markLoanNotified("loan-1"); - expect(chain.update).toHaveBeenCalledWith( - expect.objectContaining({ - metadata: expect.objectContaining({ payment_due_notified_at: expect.any(String) }), - }) - ); + const methods = db.calls.map((c) => c.method); + expect(methods).toEqual(expect.arrayContaining(["update", "set", "where"])); + const set = db.calls.find((c) => c.method === "set")?.args[0] as { metadata?: unknown }; + expect(set.metadata).toBeDefined(); + }); + + it("throws when the database is not configured", async () => { + db = null; + await expect(markLoanNotified("loan-1")).rejects.toThrow("Database unavailable"); }); }); @@ -197,10 +172,7 @@ describe("runPaymentDueScheduler", () => { }); it("returns succeeded count when all notifications succeed", async () => { - const loan = makeLoan(); - const chain = buildSelectChain([loan]); - mockFrom.mockReturnValue(chain); - mockRpc.mockResolvedValue({ error: null }); + primeDueLoans([makeLoan()]); vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true })); const result = await runPaymentDueScheduler(); @@ -211,9 +183,7 @@ describe("runPaymentDueScheduler", () => { }); it("records failure without stopping other loans", async () => { - const loans = [makeLoan({ id: "loan-1" }), makeLoan({ id: "loan-2" })]; - const chain = buildSelectChain(loans); - mockFrom.mockReturnValue(chain); + primeDueLoans([makeLoan({ id: "loan-1" }), makeLoan({ id: "loan-2" })]); let callCount = 0; vi.stubGlobal( @@ -224,7 +194,6 @@ describe("runPaymentDueScheduler", () => { return Promise.resolve({ ok: true }); }) ); - mockRpc.mockResolvedValue({ error: null }); const result = await runPaymentDueScheduler(); @@ -235,8 +204,7 @@ describe("runPaymentDueScheduler", () => { }); it("returns zero processed for empty result set", async () => { - const chain = buildSelectChain([]); - mockFrom.mockReturnValue(chain); + primeDueLoans([]); const result = await runPaymentDueScheduler(); diff --git a/__tests__/scripts/liquidation-keeper.test.ts b/__tests__/scripts/liquidation-keeper.test.ts index 98446b5..e1661c9 100644 --- a/__tests__/scripts/liquidation-keeper.test.ts +++ b/__tests__/scripts/liquidation-keeper.test.ts @@ -13,10 +13,11 @@ vi.mock("@/lib/stellar/server-contract", () => ({ invokeSigned: (...args: unknown[]) => mockInvokeSigned(...args), })); -// ── Mock Supabase (used only in --source=db) ──────────────────────────────────── -const mockFrom = vi.fn(); -vi.mock("@supabase/supabase-js", () => ({ - createClient: () => ({ from: mockFrom }), +// ── Mock the database (used only in --source=db) ────────────────────────────── +import { createFakeDb, type FakeDb } from "../helpers/fake-db"; +let fakeDb: FakeDb | null = null; +vi.mock("@/lib/db/client", () => ({ + getDb: () => fakeDb, })); import { @@ -225,6 +226,8 @@ function scValLoan(overrides: Record = {}) { describe("runLiquidationKeeper", () => { beforeEach(() => { vi.clearAllMocks(); + mockInvokeReadOnly.mockReset(); + mockInvokeSigned.mockReset(); mockGetAdminKeypair.mockReturnValue({} as never); }); @@ -233,7 +236,8 @@ describe("runLiquidationKeeper", () => { .mockResolvedValueOnce(1) // get_loan_count .mockResolvedValueOnce(scValLoan()) // get_loan .mockResolvedValueOnce(500) // get_reputation_score - .mockResolvedValueOnce(8000); // calculate_liquidation_threshold + .mockResolvedValueOnce(8000) // calculate_liquidation_threshold + .mockResolvedValueOnce(true); // check_liquidation_eligibility (grace period over, #157) mockInvokeSigned.mockResolvedValueOnce({ hash: "abc123", returnValue: null }); const cfg: KeeperConfig = { ...BASE_CFG, source: "chain" }; @@ -251,7 +255,8 @@ describe("runLiquidationKeeper", () => { .mockResolvedValueOnce(1) .mockResolvedValueOnce(scValLoan()) .mockResolvedValueOnce(500) - .mockResolvedValueOnce(8000); + .mockResolvedValueOnce(8000) + .mockResolvedValueOnce(true); // grace period over const cfg: KeeperConfig = { ...BASE_CFG, source: "chain", dryRun: true }; const summary = await runLiquidationKeeper(cfg, null); @@ -312,7 +317,8 @@ describe("runLiquidationKeeper", () => { .mockResolvedValueOnce(1) .mockResolvedValueOnce(scValLoan()) .mockResolvedValueOnce(500) - .mockResolvedValueOnce(8000); + .mockResolvedValueOnce(8000) + .mockResolvedValueOnce(true); // grace period over const cfg: KeeperConfig = { ...BASE_CFG, source: "chain", dryRun: false }; const summary = await runLiquidationKeeper(cfg, null); @@ -321,34 +327,23 @@ describe("runLiquidationKeeper", () => { expect(mockInvokeSigned).not.toHaveBeenCalled(); }); - it("resolves candidate loans from Supabase when source=db", async () => { - const chain = { - select: vi.fn().mockReturnThis(), - in: vi.fn().mockReturnThis(), - eq: vi.fn().mockReturnThis(), - maybeSingle: vi.fn().mockResolvedValue({ - data: { metadata: { onchainLoanId: 7 } }, - }), - }; - // The initial `.in(...)` query resolves via awaiting the chain itself. - Object.defineProperty(chain, "then", { - get() { - return (resolve: (v: unknown) => void) => resolve({ data: [{ id: "db-loan-1" }], error: null }); - }, - }); - mockFrom.mockReturnValue(chain); + it("resolves candidate loans from the database when source=db", async () => { + fakeDb = createFakeDb(); + // open loans, then the funding ledger row that carries the on-chain id + fakeDb.queue([{ id: "db-loan-1" }]); + fakeDb.queue([{ metadata: { onchainLoanId: 7 } }]); mockInvokeReadOnly .mockResolvedValueOnce(scValLoan({ id: 7 })) .mockResolvedValueOnce(500) - .mockResolvedValueOnce(8000); + .mockResolvedValueOnce(8000) + .mockResolvedValueOnce(true); // grace period over mockInvokeSigned.mockResolvedValueOnce({ hash: "xyz", returnValue: null }); const cfg: KeeperConfig = { ...BASE_CFG, source: "db", - supabaseUrl: "https://x.supabase.co", - supabaseServiceKey: "svc", + databaseUrl: "postgres://example", }; const summary = await runLiquidationKeeper(cfg, {} as never); diff --git a/app/actions/admin-kyc.ts b/app/actions/admin-kyc.ts index 11ec3c7..8497d45 100644 --- a/app/actions/admin-kyc.ts +++ b/app/actions/admin-kyc.ts @@ -5,7 +5,10 @@ * Only admins can verify/reject user identity documents */ -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { desc, eq, inArray, sql } from "drizzle-orm"; +import { requireApiAdmin } from "@/lib/auth/session"; +import { getDb } from "@/lib/db/client"; +import { profiles, reputationSnapshots } from "@/lib/db/schema"; export async function verifyKYCDocument( userId: string, @@ -13,81 +16,57 @@ export async function verifyKYCDocument( rejectionReason?: string ): Promise<{ success: boolean; error?: string }> { try { - const supabase = await getServerSupabaseClient(); - if (!supabase) { - return { success: false, error: "Supabase not available" }; - } - - // Verify admin status - const { data: adminUser, error: authError } = await supabase.auth.getUser(); - if (authError || !adminUser?.user) { - return { success: false, error: "Not authenticated" }; - } - - // Check if requester is admin - const { data: adminProfile } = await supabase - .from("profiles") - .select("role") - .eq("id", adminUser.user.id) - .maybeSingle(); - - if (adminProfile?.role !== "admin") { + try { + await requireApiAdmin(); + } catch { return { success: false, error: "Unauthorized: Admin access required" }; } + const db = getDb(); + if (!db) { + return { success: false, error: "Database not available" }; + } - // Update KYC status - const updateData = approved - ? { - kyc_status: "verified", - kyc_verified_at: new Date().toISOString(), - kyc_rejection_reason: null, - } - : { - kyc_status: "rejected", - kyc_rejection_reason: rejectionReason || "Document does not meet requirements", - }; - - const { error: updateError } = await supabase - .from("profiles") - .update(updateData) - .eq("id", userId); - - if (updateError) throw updateError; + await db + .update(profiles) + .set( + approved + ? { kycStatus: "verified", kycVerifiedAt: new Date(), kycRejectionReason: null } + : { + kycStatus: "rejected", + kycRejectionReason: rejectionReason || "Document does not meet requirements", + }, + ) + .where(eq(profiles.id, userId)); // When KYC is approved, seed an initial reputation score from real profile fields. if (approved) { - const { data: userProfile } = await supabase - .from("profiles") - .select("full_name, phone, country_code") - .eq("id", userId) - .maybeSingle(); + const [userProfile] = await db + .select({ fullName: profiles.fullName, phone: profiles.phone, countryCode: profiles.countryCode }) + .from(profiles) + .where(eq(profiles.id, userId)) + .limit(1); let initialScore = 70; - if (userProfile?.full_name?.trim()) initialScore += 15; + if (userProfile?.fullName?.trim()) initialScore += 15; if (userProfile?.phone?.trim()) initialScore += 15; - if (userProfile?.country_code?.trim()) initialScore += 10; - - const { error: reputationError } = await supabase.rpc("seed_reputation_snapshot", { - p_user_id: userId, - p_initial_score: initialScore, - }); - - if (reputationError) { - throw reputationError; - } - - console.log( - `[TrustLend] Reputation snapshot seeded for ${userId}: score=${initialScore}` - ); + if (userProfile?.countryCode?.trim()) initialScore += 10; + const clamped = Math.max(0, Math.min(750, initialScore)); + + await db + .insert(reputationSnapshots) + .values({ userId, scoreTotal: clamped }) + .onConflictDoUpdate({ + target: reputationSnapshots.userId, + set: { scoreTotal: clamped, updatedAt: sql`now()` }, + }); + + console.log(`[TrustLend] Reputation snapshot seeded for ${userId}: score=${clamped}`); } - - console.log( - `✅ KYC ${approved ? "approved" : "rejected"} for user ${userId}` - ); + console.log(`[TrustLend] KYC ${approved ? "approved" : "rejected"} for user ${userId}`); return { success: true }; } catch (error) { - console.error("❌ KYC verification failed:", error); + console.error("[TrustLend] KYC verification failed:", error); return { success: false, error: error instanceof Error ? error.message : "Verification failed", @@ -95,76 +74,50 @@ export async function verifyKYCDocument( } } -export async function getPendingKYCDocuments(): Promise< - Array<{ - id: string; - email: string; - full_name: string; - kyc_status: string; - government_id_url: string; - submitted_at: string; - }> | null -> { - try { - const supabase = await getServerSupabaseClient(); - if (!supabase) return null; - - // Verify admin - const { data: adminUser } = await supabase.auth.getUser(); - if (!adminUser?.user) return null; - - const { data: adminProfile } = await supabase - .from("profiles") - .select("role") - .eq("id", adminUser.user.id) - .maybeSingle(); - - if (adminProfile?.role !== "admin") return null; - - const { data, error } = await supabase - .from("profiles") - .select("id, full_name, kyc_status, government_id_ipfs_hash, government_id_url, kyc_submitted_at") - .in("kyc_status", ["submitted", "verified", "rejected"]) - .order("kyc_submitted_at", { ascending: false }); +export interface PendingKycDocument { + id: string; + email: string; + full_name: string; + kyc_status: string; + government_id_url: string; + submitted_at: string; +} - if (error) { - console.error("Error fetching KYC documents:", error); +export async function getPendingKYCDocuments(): Promise { + try { + try { + await requireApiAdmin(); + } catch { return null; } - - // Generate signed URLs for each profile document path. - const docsWithEmail = await Promise.all( - (data || []).map(async (doc) => { - let viewUrl = doc.government_id_url; - if (doc.government_id_ipfs_hash) { - const { data: signedData } = await supabase.storage - .from("kyc-documents") - .createSignedUrl(doc.government_id_ipfs_hash, 3600); - - if (signedData?.signedUrl) { - viewUrl = signedData.signedUrl; - } - } - - return { - ...doc, - email: "hidden", - submitted_at: doc.kyc_submitted_at || "", - government_id_url: viewUrl || "", - }; + const db = getDb(); + if (!db) return null; + + const rows = await db + .select({ + id: profiles.id, + fullName: profiles.fullName, + kycStatus: profiles.kycStatus, + documentPath: profiles.governmentIdIpfsHash, + kycSubmittedAt: profiles.kycSubmittedAt, }) - ); - - return docsWithEmail as Array<{ - id: string; - email: string; - full_name: string; - kyc_status: string; - government_id_url: string; - submitted_at: string; - }>; + .from(profiles) + .where(inArray(profiles.kycStatus, ["submitted", "verified", "rejected"])) + .orderBy(desc(profiles.kycSubmittedAt)); + + return rows.map((doc) => ({ + id: doc.id, + email: "hidden", + full_name: doc.fullName, + kyc_status: doc.kycStatus, + // Documents are private; admins view them through the streaming route. + government_id_url: doc.documentPath + ? `/api/admin/kyc/document?path=${encodeURIComponent(doc.documentPath)}` + : "", + submitted_at: doc.kycSubmittedAt ? doc.kycSubmittedAt.toISOString() : "", + })); } catch (error) { - console.error("❌ Failed to fetch KYC documents:", error); + console.error("[TrustLend] Failed to fetch KYC documents:", error); return null; } } diff --git a/app/actions/admin-pools.ts b/app/actions/admin-pools.ts index 9985cb7..04e1381 100644 --- a/app/actions/admin-pools.ts +++ b/app/actions/admin-pools.ts @@ -1,37 +1,24 @@ +"use server"; + /** * Admin Pools Server Actions - * - * OPTIMIZATION (Issue #39): - * - Uses optimized fetchActivePoolsWithLiquidity function - * - Reduced sequential queries in runAutoMatch - * - Single lookups for pool and loan validation + * + * Creating pools, approving loans against pool liquidity, and the auto-match + * pass that funds pending loans from whichever active pool can cover them. */ -"use server"; - -import { getServerSupabaseClient } from "@/lib/supabase/server"; -import { - fetchPoolById, - fetchActivePoolsWithLiquidity, -} from "@/lib/db/pools"; +import { asc, eq, sql } from "drizzle-orm"; +import { requireApiAdmin } from "@/lib/auth/session"; +import { getDb, type Db } from "@/lib/db/client"; +import { fetchActivePoolsWithLiquidity, fetchPoolById } from "@/lib/db/pools"; +import { lendingPools, loans } from "@/lib/db/schema"; import { sendLoanApprovedEmail } from "@/lib/email/resend"; -async function requireAdmin() { - const supabase = await getServerSupabaseClient(); - if (!supabase) throw new Error("Database unavailable"); - - const { data: { user }, error } = await supabase.auth.getUser(); - if (error || !user) throw new Error("Not authenticated"); - - const { data: profile } = await supabase - .from("profiles") - .select("role") - .eq("id", user.id) - .maybeSingle(); - - if (profile?.role !== "admin") throw new Error("Unauthorized: Admin only"); - - return { user, supabase }; +async function requireAdmin(): Promise<{ db: Db }> { + await requireApiAdmin(); + const db = getDb(); + if (!db) throw new Error("Database unavailable"); + return { db }; } // ── Create a new lending pool ────────────────────────────────────────────────── @@ -39,41 +26,35 @@ export async function createLendingPool( formData: FormData ): Promise<{ success: boolean; error?: string }> { try { - const { supabase } = await requireAdmin(); + const { db } = await requireAdmin(); const name = String(formData.get("name") ?? "").trim(); const aprBps = parseInt(String(formData.get("apr_bps") ?? "0"), 10); const description = String(formData.get("description") ?? "").trim(); const borrowCapRaw = formData.get("borrow_cap"); - const borrowCap = borrowCapRaw !== null && borrowCapRaw !== "" - ? parseFloat(String(borrowCapRaw)) - : null; + const borrowCap = + borrowCapRaw !== null && borrowCapRaw !== "" ? parseFloat(String(borrowCapRaw)) : null; if (borrowCap !== null && (borrowCap <= 0 || !Number.isFinite(borrowCap))) { return { success: false, error: "Borrow cap must be a positive number" }; } - if (!name) return { success: false, error: "Pool name is required" }; if (!aprBps || aprBps <= 0 || aprBps > 10000) return { success: false, error: "APR must be between 0.01% and 100%" }; - const { error } = await supabase.from("lending_pools").insert({ + await db.insert(lendingPools).values({ name, description: description || null, status: "active", - apr_bps: aprBps, - total_liquidity: 0, - available_liquidity: 0, - borrow_cap: borrowCap ?? null, + aprBps, + totalLiquidity: "0", + availableLiquidity: "0", + borrowCap: borrowCap === null ? null : String(borrowCap), }); - if (error) return { success: false, error: error.message }; return { success: true }; } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Failed", - }; + return { success: false, error: err instanceof Error ? err.message : "Failed" }; } } @@ -83,58 +64,43 @@ export async function togglePoolStatus( newStatus: "active" | "paused" ): Promise<{ success: boolean; error?: string }> { try { - const { supabase } = await requireAdmin(); - - const { error } = await supabase - .from("lending_pools") - .update({ status: newStatus }) - .eq("id", poolId); - - if (error) return { success: false, error: error.message }; + const { db } = await requireAdmin(); + await db.update(lendingPools).set({ status: newStatus }).where(eq(lendingPools.id, poolId)); return { success: true }; } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Failed", - }; + return { success: false, error: err instanceof Error ? err.message : "Failed" }; } } -/** - * Approve a pending loan and link to pool. - * - * OPTIMIZATION: - * - Single pool lookup via optimized fetchPoolById - * - No redundant pool queries - * - Atomic update pattern - */ +/** Approve a pending loan and reserve its principal from the pool. */ export async function approveLoan( loanId: string, poolId: string ): Promise<{ success: boolean; error?: string }> { try { - const { supabase } = await requireAdmin(); - - // Fetch loan to validate - const { data: loan, error: fetchErr } = await supabase - .from("loans") - .select("id, borrower_id, status, principal_amount, pool_id") - .eq("id", loanId) - .maybeSingle(); + const { db } = await requireAdmin(); + + const [loan] = await db + .select({ + id: loans.id, + borrowerId: loans.borrowerId, + status: loans.status, + principalAmount: loans.principalAmount, + }) + .from(loans) + .where(eq(loans.id, loanId)) + .limit(1); - if (fetchErr || !loan) return { success: false, error: "Loan not found" }; + if (!loan) return { success: false, error: "Loan not found" }; if (loan.status !== "requested") return { success: false, error: `Loan is already ${loan.status}` }; - // Fetch pool to check liquidity using optimized function - const pool = await fetchPoolById(supabase, poolId); - + const pool = await fetchPoolById(db, poolId); if (!pool) return { success: false, error: "Pool not found" }; - if (pool.status !== "active") - return { success: false, error: "Pool is not active" }; + if (pool.status !== "active") return { success: false, error: "Pool is not active" }; - const loanAmount = Number(loan.principal_amount ?? 0); - const available = Number(pool.available_liquidity ?? 0); + const loanAmount = Number(loan.principalAmount ?? 0); + const available = pool.available_liquidity; if (loanAmount > available) { return { @@ -144,66 +110,32 @@ export async function approveLoan( } // Borrow cap enforcement (#153) - if (pool.borrow_cap !== null && pool.borrow_cap !== undefined) { - const currentBorrowed = Number(pool.total_borrowed ?? 0); - if (currentBorrowed + loanAmount > pool.borrow_cap) { - return { - success: false, - error: `Pool borrow cap exceeded: pool has borrowed ${currentBorrowed} XLM, cap is ${pool.borrow_cap} XLM, loan needs ${loanAmount} XLM`, - }; - } + if (pool.borrow_cap !== null && pool.total_borrowed + loanAmount > pool.borrow_cap) { + return { + success: false, + error: `Pool borrow cap exceeded: pool has borrowed ${pool.total_borrowed} XLM, cap is ${pool.borrow_cap} XLM, loan needs ${loanAmount} XLM`, + }; } - const now = new Date().toISOString(); - - // 1. Approve loan - const { error: loanErr } = await supabase - .from("loans") - .update({ - status: "approved", - pool_id: poolId, - approved_at: now, - }) - .eq("id", loanId); + const now = new Date(); + await db + .update(loans) + .set({ status: "approved", poolId, approvedAt: now }) + .where(eq(loans.id, loanId)); + await db + .update(lendingPools) + .set({ availableLiquidity: sql`${lendingPools.availableLiquidity} - ${loanAmount}` }) + .where(eq(lendingPools.id, poolId)); - if (loanErr) return { success: false, error: loanErr.message }; - - // 2. Deduct available liquidity from pool - const { error: poolErr } = await supabase - .from("lending_pools") - .update({ available_liquidity: available - loanAmount }) - .eq("id", poolId); - - if (poolErr) return { success: false, error: poolErr.message }; - - await sendLoanApprovedEmail({ - userId: String(loan.borrower_id), - amount: loanAmount, - loanId, - }); + await sendLoanApprovedEmail({ userId: loan.borrowerId, amount: loanAmount, loanId }); return { success: true }; } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Failed", - }; + return { success: false, error: err instanceof Error ? err.message : "Failed" }; } } -/** - * Run auto-matching: fund all pending loans that pools can cover. - * - * OPTIMIZATION (Issue #39): - * - Fetch active pools in single query using optimized function - * - Replaced sequential pool fetches with batch query - * - Reduced from ~4 queries to 2-3 queries total - * - * PERFORMANCE: - * - Active pools query: O(1) with index on (status, available_liquidity) - * - Pending loans query: O(1) with index on (status) - * - Matching loop: O(n*m) but with filtered, pre-sorted data - */ +/** Run auto-matching: approve every pending loan an active pool can cover. */ export async function runAutoMatch(): Promise<{ success: boolean; matched: number; @@ -211,57 +143,49 @@ export async function runAutoMatch(): Promise<{ error?: string; }> { try { - const { supabase } = await requireAdmin(); - - // Get all pending loans ordered by creation (oldest first) - const { data: pendingLoans } = await supabase - .from("loans") - .select("id, borrower_id, principal_amount, pool_id") - .eq("status", "requested") - .order("requested_at", { ascending: true }); + const { db } = await requireAdmin(); + + const pendingLoans = await db + .select({ + id: loans.id, + borrowerId: loans.borrowerId, + principalAmount: loans.principalAmount, + poolId: loans.poolId, + }) + .from(loans) + .where(eq(loans.status, "requested")) + .orderBy(asc(loans.requestedAt)); - if (!pendingLoans || pendingLoans.length === 0) { + if (pendingLoans.length === 0) { return { success: true, matched: 0, skipped: 0 }; } - // OPTIMIZED: Fetch all active pools with sufficient liquidity in ONE query - // using optimized fetchActivePoolsWithLiquidity - const activePools = await fetchActivePoolsWithLiquidity(supabase, 0); - + const activePools = await fetchActivePoolsWithLiquidity(db, 0); if (activePools.length === 0) { - return { - success: true, - matched: 0, - skipped: pendingLoans.length, - }; + return { success: true, matched: 0, skipped: pendingLoans.length }; } - // Mutable pool liquidity map for local state tracking + // Mutable liquidity map so several loans can draw on one pool in a pass. const poolLiquidity = new Map( - activePools.map((p) => [String(p.id), Number(p.available_liquidity ?? 0)]) + activePools.map((p) => [p.id, p.available_liquidity]) ); let matched = 0; let skipped = 0; - const now = new Date().toISOString(); + const now = new Date(); - // Process each pending loan for (const loan of pendingLoans) { - const amount = Number(loan.principal_amount ?? 0); + const amount = Number(loan.principalAmount ?? 0); - // Find a pool with enough liquidity (prefer the assigned pool if any) let targetPoolId: string | null = null; - const assignedPool = loan.pool_id ? String(loan.pool_id) : null; - + const assignedPool = loan.poolId ?? null; if (assignedPool && (poolLiquidity.get(assignedPool) ?? 0) >= amount) { targetPoolId = assignedPool; } else { - // Pick the pool with most liquidity that covers the loan - // Pools are already sorted by available_liquidity DESC from fetch + // Pools are already sorted by available liquidity (desc). for (const pool of activePools) { - const currentLiquidity = poolLiquidity.get(String(pool.id)) ?? 0; - if (currentLiquidity >= amount) { - targetPoolId = String(pool.id); + if ((poolLiquidity.get(pool.id) ?? 0) >= amount) { + targetPoolId = pool.id; break; } } @@ -273,44 +197,29 @@ export async function runAutoMatch(): Promise<{ } // Borrow cap enforcement (#153) - const targetPool = activePools.find((p) => String(p.id) === targetPoolId); - if (targetPool && targetPool.borrow_cap !== null && targetPool.borrow_cap !== undefined) { - const currentBorrowed = Number(targetPool.total_borrowed ?? 0); - if (currentBorrowed + amount > targetPool.borrow_cap) { - skipped++; - continue; - } + const targetPool = activePools.find((p) => p.id === targetPoolId); + if (targetPool && targetPool.borrow_cap !== null && targetPool.total_borrowed + amount > targetPool.borrow_cap) { + skipped++; + continue; } - // Approve and deduct - const [loanResult, poolResult] = await Promise.all([ - supabase - .from("loans") - .update({ status: "approved", pool_id: targetPoolId, approved_at: now }) - .eq("id", loan.id), - supabase - .from("lending_pools") - .update({ - available_liquidity: - (poolLiquidity.get(targetPoolId) ?? 0) - amount, - }) - .eq("id", targetPoolId), - ]); - - if (loanResult.error || poolResult.error) { + try { + await db + .update(loans) + .set({ status: "approved", poolId: targetPoolId, approvedAt: now }) + .where(eq(loans.id, loan.id)); + await db + .update(lendingPools) + .set({ availableLiquidity: sql`${lendingPools.availableLiquidity} - ${amount}` }) + .where(eq(lendingPools.id, targetPoolId)); + } catch { skipped++; - } else { - poolLiquidity.set( - targetPoolId, - (poolLiquidity.get(targetPoolId) ?? 0) - amount - ); - await sendLoanApprovedEmail({ - userId: String(loan.borrower_id), - amount, - loanId: String(loan.id), - }); - matched++; + continue; } + + poolLiquidity.set(targetPoolId, (poolLiquidity.get(targetPoolId) ?? 0) - amount); + await sendLoanApprovedEmail({ userId: loan.borrowerId, amount, loanId: loan.id }); + matched++; } return { success: true, matched, skipped }; @@ -325,32 +234,25 @@ export async function runAutoMatch(): Promise<{ } // ── Set pool borrow cap ────────────────────────────────────────────────────── -/** - * Set or clear the borrow cap for a lending pool. - * Pass null to remove the cap (unlimited borrowing). - */ +/** Set or clear the borrow cap for a lending pool (null = unlimited). */ export async function setPoolBorrowCap( poolId: string, borrowCap: number | null ): Promise<{ success: boolean; error?: string }> { try { - const { supabase } = await requireAdmin(); + const { db } = await requireAdmin(); if (borrowCap !== null && (borrowCap <= 0 || !Number.isFinite(borrowCap))) { return { success: false, error: "Borrow cap must be a positive number or null" }; } - const { error } = await supabase - .from("lending_pools") - .update({ borrow_cap: borrowCap }) - .eq("id", poolId); + await db + .update(lendingPools) + .set({ borrowCap: borrowCap === null ? null : String(borrowCap) }) + .where(eq(lendingPools.id, poolId)); - if (error) return { success: false, error: error.message }; return { success: true }; } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Failed", - }; + return { success: false, error: err instanceof Error ? err.message : "Failed" }; } -} \ No newline at end of file +} diff --git a/app/actions/kyc-upload.ts b/app/actions/kyc-upload.ts index 2f90131..0bec1d3 100644 --- a/app/actions/kyc-upload.ts +++ b/app/actions/kyc-upload.ts @@ -2,11 +2,18 @@ /** * Server Action: Handle KYC document upload - * Validates user, uploads to Supabase Storage, stores reference in database + * Validates the caller, stores the file in Vercel Blob (private), and records + * the reference on the caller's profile. */ -import { createClient } from "@supabase/supabase-js"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { put } from "@vercel/blob"; +import { eq } from "drizzle-orm"; +import { getSessionUser } from "@/lib/auth/session"; +import { getDb } from "@/lib/db/client"; +import { profiles } from "@/lib/db/schema"; + +const VALID_TYPES = ["image/jpeg", "image/png", "image/webp", "application/pdf"]; +const MAX_BYTES = 10 * 1024 * 1024; // 10 MB export async function uploadKYCDocument(formData: FormData): Promise<{ success: boolean; @@ -14,122 +21,62 @@ export async function uploadKYCDocument(formData: FormData): Promise<{ error?: string; }> { try { - const supabase = await getServerSupabaseClient(); - if (!supabase) { - return { success: false, error: "Authentication service unavailable" }; - } - - // Get current user - const { - data: { user }, - error: authError, - } = await supabase.auth.getUser(); - if (authError || !user) { + const user = await getSessionUser(); + if (!user) { return { success: false, error: "Not authenticated" }; } + const db = getDb(); + if (!db) { + return { success: false, error: "Database unavailable" }; + } - // Get file from form const file = formData.get("government_id") as File | null; if (!file) { return { success: false, error: "No file provided" }; } - - // Validate file type and size - const validTypes = ["image/jpeg", "image/png", "image/webp", "application/pdf"]; - if (!validTypes.includes(file.type)) { + if (!VALID_TYPES.includes(file.type)) { return { success: false, error: "Invalid file type. Please upload JPG, PNG, WebP, or PDF.", }; } - - if (file.size > 10 * 1024 * 1024) { - // 10MB limit + if (file.size > MAX_BYTES) { return { success: false, error: "File too large. Maximum 10MB allowed." }; } - const url = process.env.NEXT_PUBLIC_SUPABASE_URL; - const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - if (!url || !anonKey) { - return { success: false, error: "Supabase is not configured." }; - } - - // Use the signed-in user's session token for storage upload so no service-role key is needed. - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (!session?.access_token) { + if (!process.env.BLOB_READ_WRITE_TOKEN) { return { success: false, - error: "Your session has expired. Please sign in again before uploading KYC documents.", + error: "Document storage is not configured yet (BLOB_READ_WRITE_TOKEN missing).", }; } - const client = createClient(url, anonKey, { - auth: { persistSession: false, autoRefreshToken: false }, - global: { - headers: { - Authorization: `Bearer ${session.access_token}`, - }, - }, - }); - - // Create unique object key inside bucket: {userId}/government_id_{timestamp} - const filename = `government_id_${Date.now()}_${Math.random().toString(36).substring(7)}`; - const filepath = `${user.id}/${filename}`; + // Object key: kyc/{userId}/government_id_{timestamp}_{random}.{ext} + const ext = file.name.includes(".") ? file.name.split(".").pop() : undefined; + const filename = `government_id_${Date.now()}_${Math.random().toString(36).substring(7)}${ext ? `.${ext}` : ""}`; + const filepath = `kyc/${user.id}/${filename}`; - console.log(`📤 Uploading ${file.name} to Supabase Storage: ${filepath}`); - - const { error: uploadError } = await client.storage - .from("kyc-documents") - .upload(filepath, file, { - cacheControl: "3600", - upsert: false, - }); - - if (uploadError) { - console.error("Upload error:", uploadError); - if (uploadError.message.toLowerCase().includes("row-level security")) { - return { - success: false, - error: - "KYC storage access is not configured yet. Apply the KYC storage RLS migration, then try again.", - }; - } - return { success: false, error: uploadError.message }; - } - - // Get public URL for the uploaded document path - const { - data: { publicUrl }, - } = client.storage.from("kyc-documents").getPublicUrl(filepath); - - // Store reference in profiles table with caller session (RLS-protected) + const blob = await put(filepath, file, { + access: "private", + contentType: file.type, + addRandomSuffix: false, + }); - const { error: updateError } = await supabase - .from("profiles") - .update({ - government_id_ipfs_hash: filepath, - government_id_url: publicUrl, - kyc_status: "submitted", - kyc_submitted_at: new Date().toISOString(), + await db + .update(profiles) + .set({ + governmentIdIpfsHash: blob.pathname, + governmentIdUrl: blob.url, + kycStatus: "submitted", + kycSubmittedAt: new Date(), }) - .eq("id", user.id); - - if (updateError) { - console.error("Database update error:", updateError); - return { success: false, error: "Failed to save document reference" }; - } + .where(eq(profiles.id, user.id)); - console.log(`✅ KYC document uploaded for user ${user.id}: ${filepath}`); + console.log(`[TrustLend] KYC document uploaded for user ${user.id}: ${blob.pathname}`); - return { - success: true, - path: filepath, - }; + return { success: true, path: blob.pathname }; } catch (error) { - console.error("❌ KYC upload failed:", error); + console.error("[TrustLend] KYC upload failed:", error); return { success: false, error: error instanceof Error ? error.message : "Upload failed", diff --git a/app/actions/update-profile.ts b/app/actions/update-profile.ts index 7ec5a0d..e1c6cf7 100644 --- a/app/actions/update-profile.ts +++ b/app/actions/update-profile.ts @@ -1,15 +1,18 @@ "use server"; /** - * Server Action: Update user profile fields - * Uses getServerSupabaseClient() — authenticated via cookie (anon key + user JWT). - * DB writes are done with the caller session and enforced by RLS. + * Server Actions: profile self-service. + * + * Every action resolves the caller from the session cookie and only ever + * writes the caller's own `profiles` row. */ -import { getServerSupabaseClient } from "@/lib/supabase/server"; - -import { z } from "zod"; +import { eq } from "drizzle-orm"; import sanitizeHtml from "sanitize-html"; +import { z } from "zod"; +import { getSessionUser } from "@/lib/auth/session"; +import { getDb } from "@/lib/db/client"; +import { profiles } from "@/lib/db/schema"; const profileSchema = z.object({ full_name: z.string().min(2, "Full legal name must be at least 2 characters."), @@ -46,59 +49,33 @@ export async function updateUserProfile( payload: ProfileUpdatePayload ): Promise { try { - // 1. Identify the caller using the cookie-based client (verifies their JWT) - const supabase = await getServerSupabaseClient(); - if (!supabase) { - return { success: false, error: "Authentication service unavailable." }; - } - - const { - data: { user }, - error: authError, - } = await supabase.auth.getUser(); - - if (authError || !user) { + const user = await getSessionUser(); + if (!user) { return { success: false, error: "You must be logged in to update your profile." }; } + const db = getDb(); + if (!db) { + return { success: false, error: "Database unavailable." }; + } - // 2. Validate fields using Zod const validationResult = profileSchema.safeParse(payload); if (!validationResult.success) { - return { - success: false, - error: validationResult.error.issues[0]?.message || "Invalid input data." + return { + success: false, + error: validationResult.error.issues[0]?.message || "Invalid input data.", }; } - const validatedData = validationResult.data; - // 3. Sanitize inputs to prevent XSS - const name = sanitize(validatedData.full_name.trim()); - const phone = sanitize(validatedData.phone.trim()); - - // 4. Build update object - const updates: Record = { - full_name: name, - phone: phone, + const updates: Partial = { + fullName: sanitize(validatedData.full_name.trim()), + phone: sanitize(validatedData.phone.trim()), }; - if (validatedData.date_of_birth && validatedData.date_of_birth.trim() !== "") { - updates.date_of_birth = sanitize(validatedData.date_of_birth.trim()); + updates.dateOfBirth = sanitize(validatedData.date_of_birth.trim()); } - // 5. Write with the caller session; RLS restricts updates to the caller row - const { error: updateError } = await supabase - .from("profiles") - .update(updates) - .eq("id", user.id); - - if (updateError) { - console.error("[TrustLend] Profile update error:", updateError); - return { - success: false, - error: updateError.message ?? "Failed to update profile.", - }; - } + await db.update(profiles).set(updates).where(eq(profiles.id, user.id)); console.log(`[TrustLend] Profile updated for user ${user.id}`); return { success: true }; @@ -110,3 +87,37 @@ export async function updateUserProfile( }; } } + +/** + * Persist the wallet the user connected in the dashboard. `null` disconnects. + * The sign-in wallet on `users` is never changed here — that is the identity. + */ +export async function updateWalletAddress( + nextAddress: string | null, +): Promise { + try { + const user = await getSessionUser(); + if (!user) { + return { success: false, error: "Not authenticated." }; + } + const db = getDb(); + if (!db) { + return { success: false, error: "Database unavailable." }; + } + if (nextAddress !== null && !/^G[A-Z2-7]{55}$/.test(nextAddress)) { + return { success: false, error: "Invalid Stellar address." }; + } + + await db + .update(profiles) + .set({ walletAddress: nextAddress }) + .where(eq(profiles.id, user.id)); + + return { success: true }; + } catch (err) { + return { + success: false, + error: err instanceof Error ? err.message : "An unexpected error occurred.", + }; + } +} diff --git a/app/api/admin/kyc/document/route.ts b/app/api/admin/kyc/document/route.ts new file mode 100644 index 0000000..8ee78cb --- /dev/null +++ b/app/api/admin/kyc/document/route.ts @@ -0,0 +1,40 @@ +import { get } from "@vercel/blob"; +import { NextRequest, NextResponse } from "next/server"; +import { requireApiAdmin, UnauthorizedError } from "@/lib/auth/session"; + +/** + * GET /api/admin/kyc/document?path=kyc// + * + * Streams a private KYC document from Vercel Blob to an authenticated admin. + * Documents are never public: this route is the only way to view them. + */ +export async function GET(request: NextRequest) { + try { + await requireApiAdmin(); + } catch (err) { + const status = err instanceof UnauthorizedError ? 403 : 500; + return NextResponse.json({ error: "Admin access required" }, { status }); + } + + const path = request.nextUrl.searchParams.get("path") ?? ""; + if (!/^kyc\/[0-9a-f-]{36}\/[A-Za-z0-9_.-]+$/i.test(path)) { + return NextResponse.json({ error: "Invalid document path" }, { status: 400 }); + } + + try { + const result = await get(path, { access: "private", useCache: false }); + if (!result || !result.stream) { + return NextResponse.json({ error: "Document not found" }, { status: 404 }); + } + return new Response(result.stream, { + headers: { + "Content-Type": result.blob.contentType ?? "application/octet-stream", + "Content-Disposition": `inline; filename="${path.split("/").pop()}"`, + "Cache-Control": "private, no-store", + }, + }); + } catch (err) { + console.error("[admin/kyc/document]", err instanceof Error ? err.message : err); + return NextResponse.json({ error: "Could not load document" }, { status: 500 }); + } +} diff --git a/app/api/admin/webhooks/[id]/route.ts b/app/api/admin/webhooks/[id]/route.ts index 624238b..83b7373 100644 --- a/app/api/admin/webhooks/[id]/route.ts +++ b/app/api/admin/webhooks/[id]/route.ts @@ -1,16 +1,34 @@ import { NextRequest, NextResponse } from "next/server"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; -import { enforceRouteRateLimit } from "@/lib/rate-limit"; +import { eq } from "drizzle-orm"; import { z } from "zod"; +import { requireApiAdmin, UnauthorizedError } from "@/lib/auth/session"; +import { getDb } from "@/lib/db/client"; +import { webhookEndpoints } from "@/lib/db/schema"; +import { serializeWebhook } from "@/lib/webhooks/serialize"; +import { enforceRouteRateLimit } from "@/lib/rate-limit"; const patchWebhookSchema = z.object({ name: z.string().min(1, "Name is required").optional(), - url: z.string().url("Must be a valid URL").optional(), + url: z.string().url("Must be a valid URL").startsWith("https://", "Webhook URLs must use HTTPS").optional(), platform: z.enum(["discord", "telegram", "slack", "custom"]).optional(), topic: z.string().min(1, "Topic is required").optional(), is_active: z.boolean().optional(), }); +async function guard() { + try { + await requireApiAdmin(); + const db = getDb(); + if (!db) { + return { error: NextResponse.json({ error: "Database not configured" }, { status: 500 }) }; + } + return { db }; + } catch (err) { + const status = err instanceof UnauthorizedError ? 401 : 500; + return { error: NextResponse.json({ error: "Unauthorized" }, { status }) }; + } +} + export async function PATCH( request: NextRequest, { params }: { params: Promise<{ id: string }> } @@ -19,18 +37,13 @@ export async function PATCH( if (rateLimited) return rateLimited; const { id } = await params; - const supabase = await getServerSupabaseClient(); - if (!supabase) return NextResponse.json({ error: "Supabase not configured" }, { status: 500 }); - - const { data: { user }, error: userError } = await supabase.auth.getUser(); - if (userError || !user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } + const g = await guard(); + if ("error" in g) return g.error; let body; try { body = await request.json(); - } catch (err) { + } catch { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); } @@ -39,25 +52,18 @@ export async function PATCH( return NextResponse.json({ error: parsed.error.issues }, { status: 400 }); } - const payload = parsed.data; - - // The RLS policy will deny the update if they aren't admin anyway. - const { data, error } = await supabase - .from("webhook_endpoints") - .update(payload) - .eq("id", id) - .select("*") - .single(); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } + const { is_active, ...rest } = parsed.data; + const [row] = await g.db + .update(webhookEndpoints) + .set({ ...rest, ...(is_active !== undefined ? { isActive: is_active } : {}) }) + .where(eq(webhookEndpoints.id, id)) + .returning(); - if (!data) { + if (!row) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } - return NextResponse.json({ webhook: data }); + return NextResponse.json({ webhook: serializeWebhook(row) }); } export async function DELETE( @@ -68,22 +74,10 @@ export async function DELETE( if (rateLimited) return rateLimited; const { id } = await params; - const supabase = await getServerSupabaseClient(); - if (!supabase) return NextResponse.json({ error: "Supabase not configured" }, { status: 500 }); + const g = await guard(); + if ("error" in g) return g.error; - const { data: { user }, error: userError } = await supabase.auth.getUser(); - if (userError || !user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { error } = await supabase - .from("webhook_endpoints") - .delete() - .eq("id", id); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } + await g.db.delete(webhookEndpoints).where(eq(webhookEndpoints.id, id)); return NextResponse.json({ success: true }); } diff --git a/app/api/admin/webhooks/route.ts b/app/api/admin/webhooks/route.ts index 44a04de..4425b2c 100644 --- a/app/api/admin/webhooks/route.ts +++ b/app/api/admin/webhooks/route.ts @@ -1,56 +1,55 @@ import { NextRequest, NextResponse } from "next/server"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; -import { enforceRouteRateLimit } from "@/lib/rate-limit"; +import { desc } from "drizzle-orm"; import { z } from "zod"; +import { requireApiAdmin, UnauthorizedError } from "@/lib/auth/session"; +import { getDb } from "@/lib/db/client"; +import { webhookEndpoints } from "@/lib/db/schema"; +import { serializeWebhook } from "@/lib/webhooks/serialize"; +import { enforceRouteRateLimit } from "@/lib/rate-limit"; const webhookSchema = z.object({ name: z.string().min(1, "Name is required"), - url: z.string().url("Must be a valid URL"), + url: z.string().url("Must be a valid URL").startsWith("https://", "Webhook URLs must use HTTPS"), platform: z.enum(["discord", "telegram", "slack", "custom"]), topic: z.string().min(1, "Topic is required"), }); +async function guard() { + try { + const user = await requireApiAdmin(); + const db = getDb(); + if (!db) { + return { error: NextResponse.json({ error: "Database not configured" }, { status: 500 }) }; + } + return { user, db }; + } catch (err) { + const status = err instanceof UnauthorizedError ? 401 : 500; + return { error: NextResponse.json({ error: "Unauthorized" }, { status }) }; + } +} + export async function GET(request: NextRequest) { const rateLimited = await enforceRouteRateLimit(request); if (rateLimited) return rateLimited; - const supabase = await getServerSupabaseClient(); - if (!supabase) return NextResponse.json({ error: "Supabase not configured" }, { status: 500 }); - - const { data: { user }, error: userError } = await supabase.auth.getUser(); - if (userError || !user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } + const g = await guard(); + if ("error" in g) return g.error; - // Fetch webhooks — RLS ensures only admins get rows. - const { data, error } = await supabase - .from("webhook_endpoints") - .select("*") - .order("created_at", { ascending: false }); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } - - return NextResponse.json({ webhooks: data ?? [] }); + const rows = await g.db.select().from(webhookEndpoints).orderBy(desc(webhookEndpoints.createdAt)); + return NextResponse.json({ webhooks: rows.map(serializeWebhook) }); } export async function POST(request: NextRequest) { const rateLimited = await enforceRouteRateLimit(request); if (rateLimited) return rateLimited; - const supabase = await getServerSupabaseClient(); - if (!supabase) return NextResponse.json({ error: "Supabase not configured" }, { status: 500 }); - - const { data: { user }, error: userError } = await supabase.auth.getUser(); - if (userError || !user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } + const g = await guard(); + if ("error" in g) return g.error; let body; try { body = await request.json(); - } catch (err) { + } catch { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); } @@ -59,22 +58,17 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: parsed.error.issues }, { status: 400 }); } - const { data, error } = await supabase - .from("webhook_endpoints") - .insert([{ + const [row] = await g.db + .insert(webhookEndpoints) + .values({ name: parsed.data.name, url: parsed.data.url, platform: parsed.data.platform, topic: parsed.data.topic, - is_active: true, - created_by: user.id - }]) - .select("*") - .single(); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } + isActive: true, + createdBy: g.user.id, + }) + .returning(); - return NextResponse.json({ webhook: data }, { status: 201 }); + return NextResponse.json({ webhook: serializeWebhook(row) }, { status: 201 }); } diff --git a/app/api/analytics/route.ts b/app/api/analytics/route.ts index 7f0a771..9e7d4e5 100644 --- a/app/api/analytics/route.ts +++ b/app/api/analytics/route.ts @@ -9,7 +9,7 @@ import { getCachedPlatformAnalytics, setCachedPlatformAnalytics, } from "@/lib/analytics-cache"; -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { getDb } from "@/lib/db/client"; export const revalidate = 3600; // Match ANALYTICS_CACHE_TTL_SECONDS (1 hour) @@ -33,9 +33,9 @@ export async function GET(request: NextRequest) { }); } - const supabase = getServiceRoleClient(); + const db = getDb(); - if (!supabase) { + if (!db) { return NextResponse.json( { error: "Analytics service unavailable" }, { status: 503 } @@ -43,7 +43,7 @@ export async function GET(request: NextRequest) { } try { - const metrics = await fetchPlatformAnalytics(supabase); + const metrics = await fetchPlatformAnalytics(db); const payload = buildPlatformAnalyticsResponse(metrics); await setCachedPlatformAnalytics(payload, ANALYTICS_CACHE_TTL_SECONDS); diff --git a/app/api/auth/signout/route.ts b/app/api/auth/signout/route.ts new file mode 100644 index 0000000..b3d835d --- /dev/null +++ b/app/api/auth/signout/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; +import { SESSION_COOKIE_NAME, sessionCookieOptions } from "@/lib/auth/session-token"; + +/** + * POST /api/auth/signout + * + * Clears the session cookie. Sessions are stateless JWTs, so there is nothing + * to revoke server-side; the cookie simply stops being sent. + */ +export async function POST() { + const response = NextResponse.json({ ok: true }); + response.cookies.set(SESSION_COOKIE_NAME, "", { ...sessionCookieOptions(), maxAge: 0 }); + return response; +} diff --git a/app/api/auth/siws/verify/route.ts b/app/api/auth/siws/verify/route.ts index 0e81901..464355f 100644 --- a/app/api/auth/siws/verify/route.ts +++ b/app/api/auth/siws/verify/route.ts @@ -4,18 +4,22 @@ import { SiwsError, verifyChallenge, } from "@/lib/auth/siws-server"; +import { + SESSION_COOKIE_NAME, + sessionCookieOptions, + signSessionToken, +} from "@/lib/auth/session-token"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; /** * POST /api/auth/siws/verify * * Steps 4-5 of Sign-In with Stellar (SEP-0010). Validates the wallet-signed - * challenge (structure, expiry, signature) and — on success — provisions / - * signs in the wallet's Supabase user, returning session tokens the client - * adopts via `supabase.auth.setSession(...)`. + * challenge (structure, expiry, signature) and — on success — provisions the + * wallet's account and sets the HttpOnly session cookie. * - * Body: { address: "G...", signedTxXdr: "" } - * 200: { access_token, refresh_token, isNewUser } + * Body: { address: "G...", signedTxXdr: "", role?: "borrower" | "lender" } + * 200: { userId, role, isNewUser } (+ Set-Cookie: tl_session) * 4xx: { error, code } (invalid_address | invalid_challenge | expired_challenge * | invalid_signature | address_mismatch | ...) */ @@ -39,14 +43,21 @@ export async function POST(request: NextRequest) { // Validate the SEP-10 challenge — throws SiwsError with a clear code/status. const wallet = verifyChallenge(signedTxXdr, address); - // Provision / sign in the wallet identity and return the session. - const { session, isNewUser } = await issueSessionForWallet(wallet, role); + // Provision the wallet identity and mint a session cookie. + const identity = await issueSessionForWallet(wallet, role); + const token = await signSessionToken({ + sub: identity.userId, + wallet, + role: identity.role, + }); - return NextResponse.json({ - access_token: session.access_token, - refresh_token: session.refresh_token, - isNewUser, + const response = NextResponse.json({ + userId: identity.userId, + role: identity.role, + isNewUser: identity.isNewUser, }); + response.cookies.set(SESSION_COOKIE_NAME, token, sessionCookieOptions()); + return response; } catch (err) { if (err instanceof SiwsError) { return NextResponse.json({ error: err.message, code: err.code }, { status: err.status }); diff --git a/app/api/borrower/transactions/route.ts b/app/api/borrower/transactions/route.ts index 8fb6947..0897b49 100644 --- a/app/api/borrower/transactions/route.ts +++ b/app/api/borrower/transactions/route.ts @@ -1,10 +1,23 @@ import { NextRequest, NextResponse } from "next/server"; +import { and, desc, eq, gt, inArray, lt } from "drizzle-orm"; import { requireAuthenticatedUser } from "@/lib/auth/session"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { getDb } from "@/lib/db/client"; +import { metaString } from "@/lib/db/metadata"; +import { ledgerTransactions, loanRepayments, loans } from "@/lib/db/schema"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; const PAGE_SIZE = 20; +type BorrowerTransaction = { + id: string; + type: "loan_requested" | "funding_received" | "repayment_made"; + loanId: string; + amount: number; + date: string; + txHash: string; + loanStatus: string; +}; + /** * GET /api/borrower/transactions?cursor=&direction= * @@ -17,187 +30,147 @@ export async function GET(request: NextRequest) { if (rateLimited) return rateLimited; const { user } = await requireAuthenticatedUser("borrower"); - const supabase = await getServerSupabaseClient(); - - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 500 }); } const cursor = request.nextUrl.searchParams.get("cursor") || undefined; const direction = request.nextUrl.searchParams.get("direction") || "next"; - // Fetch loans with cursor-based pagination - let loansQuery = supabase - .from("loans") - .select("id, status, principal_amount, repaid_amount, apr_bps, duration_days, due_at, created_at") - .eq("borrower_id", user.id) - .order("created_at", { ascending: false }); - - if (cursor) { - const cursorDate = new Date(cursor); - if (direction === "next") { - loansQuery = loansQuery.lt("created_at", cursor); - } else { - loansQuery = loansQuery.gt("created_at", cursor); - } + // Loans with cursor-based pagination (one extra row to detect "more"). + const cursorDate = cursor ? new Date(cursor) : null; + const cursorClause = + cursorDate && !Number.isNaN(cursorDate.getTime()) + ? direction === "next" + ? lt(loans.createdAt, cursorDate) + : gt(loans.createdAt, cursorDate) + : undefined; + + const loanRows = await db + .select({ + id: loans.id, + status: loans.status, + principalAmount: loans.principalAmount, + createdAt: loans.createdAt, + }) + .from(loans) + .where(and(eq(loans.borrowerId, user.id), cursorClause)) + .orderBy(desc(loans.createdAt)) + .limit(PAGE_SIZE + 1); + + const hasMore = loanRows.length > PAGE_SIZE; + const items = loanRows.slice(0, PAGE_SIZE); + const loanIds = items.map((l) => l.id); + + if (loanIds.length === 0) { + return NextResponse.json({ transactions: [], hasMore: false, nextCursor: undefined }); } - loansQuery = loansQuery.limit(PAGE_SIZE + 1); // Fetch one extra to check if there are more - - const { data: loans, error: loansError } = await loansQuery; - - if (loansError) { - console.error("Loans fetch error:", loansError); - return NextResponse.json({ error: "Failed to fetch loans" }, { status: 500 }); - } - - const hasMore = (loans?.length ?? 0) > PAGE_SIZE; - const items = loans?.slice(0, PAGE_SIZE) ?? []; - - // Fetch ledger transactions for these loans - const loanIds = items.map((l) => String(l.id)); - - const [ledgerRes, requestLedgerRes] = loanIds.length > 0 - ? await Promise.all([ - supabase - .from("ledger_transactions") - .select("ref_id, metadata, created_at, amount") - .eq("ref_type", "loan_fund") - .in("ref_id", loanIds), - supabase - .from("ledger_transactions") - .select("ref_id, metadata, created_at, amount") - .eq("ref_type", "loan_request") - .in("ref_id", loanIds), - ]) - : [{ data: [] }, { data: [] }]; - - // Fetch repayments - const repaymentsRes = loanIds.length > 0 - ? await supabase - .from("loan_repayments") - .select("id, loan_id, amount, created_at") - .in("loan_id", loanIds) - .order("created_at", { ascending: false }) - .limit(100) - : { data: [] }; - - // Build transaction feed - const transactions: Array<{ - id: string; - type: "loan_requested" | "funding_received" | "repayment_made"; - loanId: string; - amount: number; - date: string; - txHash: string; - loanStatus: string; - }> = []; - - const requestTxMap: Record = {}; - for (const entry of requestLedgerRes.data ?? []) { - if (!entry.ref_id) continue; - requestTxMap[String(entry.ref_id)] = { - date: String(entry.created_at ?? ""), - amount: Number(entry.amount ?? 0), - }; + // Ledger entries (request + funding) and repayments for this page of loans. + const [ledgerRows, repaymentRows] = await Promise.all([ + db + .select({ + refType: ledgerTransactions.refType, + refId: ledgerTransactions.refId, + metadata: ledgerTransactions.metadata, + createdAt: ledgerTransactions.createdAt, + amount: ledgerTransactions.amount, + }) + .from(ledgerTransactions) + .where( + and(inArray(ledgerTransactions.refType, ["loan_fund", "loan_request"]), inArray(ledgerTransactions.refId, loanIds)), + ), + db + .select({ + id: loanRepayments.id, + loanId: loanRepayments.loanId, + amount: loanRepayments.amount, + createdAt: loanRepayments.createdAt, + }) + .from(loanRepayments) + .where(inArray(loanRepayments.loanId, loanIds)) + .orderBy(desc(loanRepayments.createdAt)) + .limit(100), + ]); + + // Repayment ledger rows carry the tx hash; fetch them in one query. + const repaymentIds = repaymentRows.map((r) => r.id); + const repayLedgerRows = repaymentIds.length + ? await db + .select({ refId: ledgerTransactions.refId, metadata: ledgerTransactions.metadata }) + .from(ledgerTransactions) + .where(and(eq(ledgerTransactions.refType, "loan_repay"), inArray(ledgerTransactions.refId, repaymentIds))) + : []; + const repayHashByRepaymentId = new Map( + repayLedgerRows.map((row) => [row.refId ?? "", metaString(row.metadata, "txHash")]), + ); + + const requestTxMap = new Map(); + const fundTxMap = new Map(); + for (const entry of ledgerRows) { + if (!entry.refId) continue; + if (entry.refType === "loan_request") { + requestTxMap.set(entry.refId, { date: entry.createdAt.toISOString(), amount: Number(entry.amount ?? 0) }); + } else { + // A loan may have several fundings (#269); keep the latest for the feed. + fundTxMap.set(entry.refId, { + hash: metaString(entry.metadata, "txHash"), + amount: (fundTxMap.get(entry.refId)?.amount ?? 0) + Number(entry.amount ?? 0), + date: entry.createdAt.toISOString(), + }); + } } - const loanTxMap: Record = {}; - for (const entry of ledgerRes.data ?? []) { - try { - const meta = JSON.parse(String(entry.metadata ?? "{}")); - if (String(entry.ref_id)) { - loanTxMap[String(entry.ref_id)] = { - hash: String(meta.txHash ?? ""), - amount: Number(entry.amount ?? 0), - date: String(entry.created_at ?? ""), - }; - } - } catch { /* ignore */ } - } + const transactions: BorrowerTransaction[] = []; - // Request events for (const loan of items) { - const loanId = String(loan.id); - const requestTx = requestTxMap[loanId]; - const amount = requestTx?.amount ?? Number(loan.principal_amount ?? 0); - const date = requestTx?.date || String(loan.created_at ?? ""); - + const requestTx = requestTxMap.get(loan.id); transactions.push({ - id: `request-${loanId}`, + id: `request-${loan.id}`, type: "loan_requested", - loanId, - amount, - date, + loanId: loan.id, + amount: requestTx?.amount ?? Number(loan.principalAmount ?? 0), + date: requestTx?.date || loan.createdAt.toISOString(), txHash: "", - loanStatus: String(loan.status), + loanStatus: loan.status, }); - } - // Funding events - for (const loan of items) { - const loanId = String(loan.id); - const ledger = loanTxMap[loanId]; - if (ledger && ledger.amount > 0) { + const fund = fundTxMap.get(loan.id); + if (fund && fund.amount > 0) { transactions.push({ - id: `fund-${loanId}`, + id: `fund-${loan.id}`, type: "funding_received", - loanId, - amount: ledger.amount, - date: ledger.date || String(loan.created_at ?? ""), - txHash: ledger.hash, - loanStatus: String(loan.status), + loanId: loan.id, + amount: fund.amount, + date: fund.date || loan.createdAt.toISOString(), + txHash: fund.hash, + loanStatus: loan.status, }); } } - // Repayment events - for (const r of repaymentsRes.data ?? []) { - const loan = items.find((l) => String(l.id) === String(r.loan_id)); + for (const r of repaymentRows) { + const loan = items.find((l) => l.id === r.loanId); if (!loan) continue; - - // Get repayment tx hash - let txHash = ""; - try { - const { data: repayTx } = await supabase - .from("ledger_transactions") - .select("metadata") - .eq("ref_type", "loan_repay") - .eq("ref_id", String(r.id)) - .maybeSingle(); - - if (repayTx) { - const meta = JSON.parse(String(repayTx.metadata ?? "{}")); - txHash = String(meta.txHash ?? ""); - } - } catch { /* ignore */ } - transactions.push({ id: `repay-${r.id}`, type: "repayment_made", - loanId: String(r.loan_id), + loanId: r.loanId, amount: Number(r.amount), - date: String(r.created_at ?? ""), - txHash, - loanStatus: String(loan.status), + date: r.createdAt.toISOString(), + txHash: repayHashByRepaymentId.get(r.id) ?? "", + loanStatus: loan.status, }); } - // Sort by date descending transactions.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); - // Get next cursor - const nextCursor = transactions.length > 0 - ? transactions[transactions.length - 1].date - : undefined; + const nextCursor = transactions.length > 0 ? transactions[transactions.length - 1].date : undefined; - return NextResponse.json({ - transactions, - hasMore, - nextCursor, - }); + return NextResponse.json({ transactions, hasMore, nextCursor }); } catch (err) { console.error("Transactions fetch error:", err); return NextResponse.json({ error: "Internal error" }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/app/api/cron/liquidation/route.ts b/app/api/cron/liquidation/route.ts index 49586df..c3311ee 100644 --- a/app/api/cron/liquidation/route.ts +++ b/app/api/cron/liquidation/route.ts @@ -5,8 +5,9 @@ import { loadConfig, runLiquidationKeeper } from "@/scripts/liquidation-keeper"; /** * POST/GET /api/cron/liquidation * - * Automated Liquidation Bot (issue #259). Triggered by Vercel Cron every minute - * (`vercel.json`) or any external scheduler (GitHub Actions, cURL, systemd). + * Automated Liquidation Bot (issue #259). Triggered every 5 minutes by + * `.github/workflows/keepers.yml`, once a day by Vercel Cron (`vercel.json`, + * the Hobby-plan ceiling) or any external scheduler (cURL, systemd). * Loads the keeper configuration from env, scans open loans for * under-collateralization, and automatically submits `mark_defaulted` for any * loan whose LTV has crossed the contract's dynamic liquidation threshold. diff --git a/app/api/kyc/token/route.ts b/app/api/kyc/token/route.ts index 226d4cb..44dd8f2 100644 --- a/app/api/kyc/token/route.ts +++ b/app/api/kyc/token/route.ts @@ -3,7 +3,9 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { getDashboardPath } from "@/lib/auth/roles"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient, getServiceRoleClient } from "@/lib/supabase/server"; +import { eq } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { profiles } from "@/lib/db/schema"; import { createApplicant, getApplicantId, generateSdkToken } from "@/lib/kyc/provider"; import { isRedirectError } from "next/dist/client/components/redirect-error"; @@ -36,20 +38,20 @@ export async function POST(request: NextRequest) { redirect(getDashboardPath(role)); } - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); } // ── 2. Load profile ────────────────────────────────────────────────────── - const { data: profile } = await supabase - .from("profiles") - .select("full_name, kyc_provider_id, kyc_status") - .eq("id", user.id) - .maybeSingle(); + const [profile] = await db + .select({ fullName: profiles.fullName, kycProviderId: profiles.kycProviderId, kycStatus: profiles.kycStatus }) + .from(profiles) + .where(eq(profiles.id, user.id)) + .limit(1); - const fullName = String(profile?.full_name ?? "").trim() || "Unknown"; - const existingApplicantId = profile?.kyc_provider_id as string | null; + const fullName = String(profile?.fullName ?? "").trim() || "Unknown"; + const existingApplicantId = profile?.kycProviderId ?? null; // Don't re-create for already verified users — just return a refresh token let applicantId = existingApplicantId; @@ -63,18 +65,15 @@ export async function POST(request: NextRequest) { fullName ); - // Persist the applicantId using service role to bypass RLS - const serviceClient = getServiceRoleClient(); - if (serviceClient) { - await serviceClient - .from("profiles") - .update({ - kyc_provider_id: applicantId, - kyc_status: profile?.kyc_status === "pending" ? "submitted" : profile?.kyc_status, - kyc_submitted_at: new Date().toISOString(), - }) - .eq("id", user.id); - } + // Persist the applicantId on the caller's profile + await db + .update(profiles) + .set({ + kycProviderId: applicantId, + kycStatus: profile?.kycStatus === "pending" ? "submitted" : profile?.kycStatus, + kycSubmittedAt: new Date(), + }) + .where(eq(profiles.id, user.id)); } // ── 3. Generate SDK token ──────────────────────────────────────────────── @@ -101,24 +100,31 @@ export async function GET(request: NextRequest) { if (rateLimited) return rateLimited; const { user } = await requireAuthenticatedUser(); - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); } - const { data: profile } = await supabase - .from("profiles") - .select("kyc_status, kyc_provider_id, kyc_submitted_at, kyc_verified_at, kyc_rejection_reason, regulated_pool_access") - .eq("id", user.id) - .maybeSingle(); + const [profile] = await db + .select({ + kycStatus: profiles.kycStatus, + kycProviderId: profiles.kycProviderId, + kycSubmittedAt: profiles.kycSubmittedAt, + kycVerifiedAt: profiles.kycVerifiedAt, + kycRejectionReason: profiles.kycRejectionReason, + regulatedPoolAccess: profiles.regulatedPoolAccess, + }) + .from(profiles) + .where(eq(profiles.id, user.id)) + .limit(1); return NextResponse.json({ - kycStatus: profile?.kyc_status ?? "pending", - applicantId: profile?.kyc_provider_id ?? null, - submittedAt: profile?.kyc_submitted_at ?? null, - verifiedAt: profile?.kyc_verified_at ?? null, - rejectionReason: profile?.kyc_rejection_reason ?? null, - regulatedPoolAccess: profile?.regulated_pool_access ?? false, + kycStatus: profile?.kycStatus ?? "pending", + applicantId: profile?.kycProviderId ?? null, + submittedAt: profile?.kycSubmittedAt ? profile.kycSubmittedAt.toISOString() : null, + verifiedAt: profile?.kycVerifiedAt ? profile.kycVerifiedAt.toISOString() : null, + rejectionReason: profile?.kycRejectionReason ?? null, + regulatedPoolAccess: profile?.regulatedPoolAccess ?? false, }); } catch (error) { if (isRedirectError(error)) throw error; diff --git a/app/api/kyc/webhook/route.ts b/app/api/kyc/webhook/route.ts index bd858e9..d804317 100644 --- a/app/api/kyc/webhook/route.ts +++ b/app/api/kyc/webhook/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { eq, sql } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { profiles, reputationSnapshots } from "@/lib/db/schema"; import { verifyWebhookSignature, mapProviderStatus, @@ -63,60 +65,61 @@ export async function POST(request: NextRequest) { const rejectionReason = extractRejectionReason(payload); const isVerified = newKycStatus === "verified"; - // ── 5. Update profile in database (service role bypasses RLS) ───────────── - const supabase = getServiceRoleClient(); - if (!supabase) { + // ── 5. Update profile in database ──────────────────────────────────────── + const db = getDb(); + if (!db) { // Don't fail the webhook — log and return 200 so SumSub doesn't retry endlessly - console.error("[KYC Webhook] Supabase service client unavailable"); + console.error("[KYC Webhook] Database unavailable"); return NextResponse.json({ received: true }, { status: 200 }); } - const updatePayload: Record = { - kyc_status: newKycStatus, - kyc_provider_id: applicantId, - kyc_provider_status: type, - regulated_pool_access: isVerified, + const updatePayload: Partial = { + kycStatus: newKycStatus, + kycProviderId: applicantId, + kycProviderStatus: type, + regulatedPoolAccess: isVerified, }; if (isVerified) { - updatePayload.kyc_verified_at = new Date().toISOString(); - updatePayload.kyc_rejection_reason = null; + updatePayload.kycVerifiedAt = new Date(); + updatePayload.kycRejectionReason = null; } else if (newKycStatus === "rejected" && rejectionReason) { - updatePayload.kyc_rejection_reason = rejectionReason; + updatePayload.kycRejectionReason = rejectionReason; } else if (newKycStatus === "submitted") { - updatePayload.kyc_submitted_at = new Date().toISOString(); + updatePayload.kycSubmittedAt = new Date(); } // Try lookup by our user UUID first (reliable), fallback to provider ID - const { error: updateError } = await supabase - .from("profiles") - .update(updatePayload) - .eq("id", externalUserId); - - if (updateError) { - // Fallback: look up by kyc_provider_id (handles re-used applicants) - const { error: fallbackError } = await supabase - .from("profiles") - .update(updatePayload) - .eq("kyc_provider_id", applicantId); - - if (fallbackError) { - console.error("[KYC Webhook] Failed to update profile:", fallbackError.message); - // Still return 200 — log error but don't trigger SumSub retries for DB issues + try { + const updated = await db + .update(profiles) + .set(updatePayload) + .where(eq(profiles.id, externalUserId)) + .returning({ id: profiles.id }); + + if (updated.length === 0) { + // Fallback: look up by kyc_provider_id (handles re-used applicants) + await db.update(profiles).set(updatePayload).where(eq(profiles.kycProviderId, applicantId)); } + } catch (err) { + console.error("[KYC Webhook] Failed to update profile:", err instanceof Error ? err.message : err); + // Still return 200 — log error but don't trigger SumSub retries for DB issues } if (isVerified) { // Seed initial reputation snapshot on first verification - await supabase.rpc("seed_reputation_snapshot", { - p_user_id: externalUserId, - p_initial_score: 100, - }).then(({ error }) => { - if (error) { - // Non-fatal — reputation will be seeded on next profile interaction - console.warn("[KYC Webhook] Could not seed reputation:", error.message); - } - }); + try { + await db + .insert(reputationSnapshots) + .values({ userId: externalUserId, scoreTotal: 100 }) + .onConflictDoUpdate({ + target: reputationSnapshots.userId, + set: { scoreTotal: 100, updatedAt: sql`now()` }, + }); + } catch (err) { + // Non-fatal — reputation will be seeded on next profile interaction + console.warn("[KYC Webhook] Could not seed reputation:", err instanceof Error ? err.message : err); + } } console.log( diff --git a/app/api/lender/tax-report/route.ts b/app/api/lender/tax-report/route.ts index 0f08fb1..7c228eb 100644 --- a/app/api/lender/tax-report/route.ts +++ b/app/api/lender/tax-report/route.ts @@ -2,7 +2,10 @@ import fs from "node:fs/promises"; import path from "node:path"; import PDFDocument from "pdfkit"; import { NextRequest, NextResponse } from "next/server"; -import { getServerSupabaseClient, getServiceRoleClient } from "@/lib/supabase/server"; +import { eq } from "drizzle-orm"; +import { getSessionUser } from "@/lib/auth/session"; +import { getDb, type Db } from "@/lib/db/client"; +import { lendingPools, poolPositions, profiles } from "@/lib/db/schema"; import { getLenderTaxReportData } from "@/lib/lender/tax-report-data"; import { buildTaxReportRows, @@ -37,22 +40,19 @@ export async function GET(request: NextRequest) { ? parsedYear : new Date().getFullYear(); - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); } - const { - data: { user }, - } = await supabase.auth.getUser(); - + const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } if (format === "csv") { - return await buildCsvResponse(supabase, user.id, { - walletAddress: String(user.user_metadata?.wallet_address ?? "") || null, + return await buildCsvResponse(db, user.id, { + walletAddress: user.walletAddress || null, year, }); } @@ -60,32 +60,27 @@ export async function GET(request: NextRequest) { // ── PDF summary (original behaviour) ───────────────────────────────────── const pdfYear = year ?? new Date().getFullYear(); - const [profileRes, positionsRes] = await Promise.all([ - supabase - .from("profiles") - .select("full_name") - .eq("id", user.id) - .maybeSingle(), - supabase - .from("pool_positions") - .select(` - id, - principal_amount, - earned_interest, - opened_at, - closed_at, - status, - lending_pools ( name ) - `) - .eq("lender_id", user.id) + const [[profile], positions] = await Promise.all([ + db.select({ fullName: profiles.fullName }).from(profiles).where(eq(profiles.id, user.id)).limit(1), + db + .select({ + id: poolPositions.id, + principal_amount: poolPositions.principalAmount, + earned_interest: poolPositions.earnedInterest, + opened_at: poolPositions.openedAt, + closed_at: poolPositions.closedAt, + status: poolPositions.status, + pool_name: lendingPools.name, + }) + .from(poolPositions) + .leftJoin(lendingPools, eq(lendingPools.id, poolPositions.poolId)) + .where(eq(poolPositions.lenderId, user.id)), ]); - const positions = positionsRes.data ?? []; - // Filter positions active or closed in the given year const yearPositions = positions.filter((pos) => { - const openedAt = new Date(pos.opened_at); - const closedAt = pos.closed_at ? new Date(pos.closed_at) : new Date(); + const openedAt = pos.opened_at; + const closedAt = pos.closed_at ?? new Date(); return openedAt.getFullYear() <= pdfYear && closedAt.getFullYear() >= pdfYear; }); @@ -125,7 +120,7 @@ export async function GET(request: NextRequest) { doc.fillColor("#111827").fontSize(12); - const lenderName = profileRes.data?.full_name ?? user.user_metadata?.full_name ?? "TrustLend Lender"; + const lenderName = profile?.fullName || user.fullName || "TrustLend Lender"; const summaryRows = [ ["Report generated", new Date().toLocaleString("en-US")], @@ -177,8 +172,7 @@ export async function GET(request: NextRequest) { y = 60; } - const poolRaw = Array.isArray(pos.lending_pools) ? pos.lending_pools[0] : pos.lending_pools; - const poolName = (poolRaw as { name?: string })?.name ?? "Unknown Pool"; + const poolName = pos.pool_name ?? "Unknown Pool"; doc .fillColor("#111827") @@ -224,19 +218,11 @@ export async function GET(request: NextRequest) { * loans — where the PDF summary only ever reported pool positions. */ async function buildCsvResponse( - supabase: Awaited>, + db: Db, userId: string, { walletAddress, year }: { walletAddress: string | null; year: number | null } ) { - if (!supabase) { - return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); - } - - // Repayment ledger rows are written by the borrower, so reading them needs - // the service-role client. Without it the report still covers pool interest. - const srClient = getServiceRoleClient(); - - const data = await getLenderTaxReportData(supabase, srClient, userId, walletAddress); + const data = await getLenderTaxReportData(db, userId, walletAddress); const rows = buildTaxReportRows({ ...data, year }); const summary = summarizeTaxReport(rows); const csv = toCsv(rows); @@ -252,9 +238,7 @@ async function buildCsvResponse( // without having to parse the file back. "X-Report-Rows": String(summary.rowCount), "X-Report-Total-Interest": String(summary.totalInterest), - // A lender with no P2P history still gets a valid pool-only report; this - // flags when the P2P half could not be read at all. - "X-Report-P2P-Included": String(Boolean(srClient)), + "X-Report-P2P-Included": "true", }, }); } diff --git a/app/api/lender/transactions/route.ts b/app/api/lender/transactions/route.ts index 9982ef3..46ba6e5 100644 --- a/app/api/lender/transactions/route.ts +++ b/app/api/lender/transactions/route.ts @@ -1,6 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; +import { and, desc, eq, gt, lt } from "drizzle-orm"; import { requireAuthenticatedUser } from "@/lib/auth/session"; -import { getServerSupabaseClient, getServiceRoleClient } from "@/lib/supabase/server"; +import { getDb } from "@/lib/db/client"; +import { metaString, readMetadata } from "@/lib/db/metadata"; +import { ledgerTransactions } from "@/lib/db/schema"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; const PAGE_SIZE = 20; @@ -16,92 +19,90 @@ export async function GET(request: NextRequest) { if (rateLimited) return rateLimited; const { user } = await requireAuthenticatedUser("lender"); - const supabase = await getServerSupabaseClient(); - const srClient = getServiceRoleClient(); - - if (!supabase || !srClient) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 500 }); } const cursor = request.nextUrl.searchParams.get("cursor") || undefined; const direction = request.nextUrl.searchParams.get("direction") || "next"; - // Fetch user-initiated transactions with cursor-based pagination - let userTxsQuery = supabase - .from("ledger_transactions") - .select("id, category, ref_type, ref_id, amount, currency, status, metadata, created_at") - .eq("user_id", user.id) - .order("created_at", { ascending: false }); - - if (cursor) { - const cursorDate = new Date(cursor); - if (direction === "next") { - userTxsQuery = userTxsQuery.lt("created_at", cursor); - } else { - userTxsQuery = userTxsQuery.gt("created_at", cursor); - } - } - - userTxsQuery = userTxsQuery.limit(PAGE_SIZE + 1); - - const { data: userTxs, error: userTxsError } = await userTxsQuery; - - if (userTxsError) { - console.error("User transactions fetch error:", userTxsError); - return NextResponse.json({ error: "Failed to fetch transactions" }, { status: 500 }); - } - - const hasMore = (userTxs?.length ?? 0) > PAGE_SIZE; - const items = userTxs?.slice(0, PAGE_SIZE) ?? []; - - // Fetch incoming repayments (where lender is the recipient) - const { data: allRepays } = await srClient - .from("ledger_transactions") - .select("id, category, ref_type, ref_id, amount, currency, status, metadata, created_at") - .eq("ref_type", "loan_repay") - .order("created_at", { ascending: false }) + const columns = { + id: ledgerTransactions.id, + category: ledgerTransactions.category, + refType: ledgerTransactions.refType, + refId: ledgerTransactions.refId, + amount: ledgerTransactions.amount, + currency: ledgerTransactions.currency, + status: ledgerTransactions.status, + metadata: ledgerTransactions.metadata, + createdAt: ledgerTransactions.createdAt, + }; + + // The lender's own transactions with cursor-based pagination. + const cursorDate = cursor ? new Date(cursor) : null; + const cursorClause = + cursorDate && !Number.isNaN(cursorDate.getTime()) + ? direction === "next" + ? lt(ledgerTransactions.createdAt, cursorDate) + : gt(ledgerTransactions.createdAt, cursorDate) + : undefined; + + const userTxs = await db + .select(columns) + .from(ledgerTransactions) + .where(and(eq(ledgerTransactions.userId, user.id), cursorClause)) + .orderBy(desc(ledgerTransactions.createdAt)) + .limit(PAGE_SIZE + 1); + + const hasMore = userTxs.length > PAGE_SIZE; + const items = userTxs.slice(0, PAGE_SIZE); + + // Incoming repayments are written by the borrower; the lender is + // identified from the metadata the repayment route records. + const allRepays = await db + .select(columns) + .from(ledgerTransactions) + .where(eq(ledgerTransactions.refType, "loan_repay")) + .orderBy(desc(ledgerTransactions.createdAt)) .limit(200); - const incomingRepays = (allRepays ?? []).filter((tx) => { - try { - const meta = JSON.parse(String(tx.metadata || "{}")); - return String(meta.lenderUserId) === String(user.id) || String(meta.lenderAddress) === String(user.id); - } catch { - return false; - } + const incomingRepays = allRepays.filter((tx) => { + const meta = readMetadata(tx.metadata); + return String(meta.lenderUserId) === user.id || String(meta.lenderAddress) === user.walletAddress; }); // Merge and dedup - const txMap = new Map(); + const txMap = new Map(); for (const t of items) txMap.set(t.id, t); for (const t of incomingRepays) txMap.set(t.id, t); const transactions = Array.from(txMap.values()).sort( - (a, b) => new Date(String(b.created_at)).getTime() - new Date(String(a.created_at)).getTime() + (a, b) => b.createdAt.getTime() - a.createdAt.getTime(), ); - // Format transactions const formattedTransactions = transactions.map((tx) => { - let txHash = ""; + const meta = readMetadata(tx.metadata); + const txHash = metaString(tx.metadata, "txHash"); let subLabel = ""; - try { - const meta = JSON.parse(String(tx.metadata ?? "{}")); - txHash = String(meta.txHash ?? ""); - if (meta.loanId) subLabel = `Loan #${String(meta.loanId).slice(0, 8)}`; - else if (tx.ref_id) subLabel = `Ref #${String(tx.ref_id).slice(0, 8)}`; - } catch { /* ok */ } + if (meta.loanId) subLabel = `Loan #${String(meta.loanId).slice(0, 8)}`; + else if (tx.refId) subLabel = `Ref #${tx.refId.slice(0, 8)}`; let label = "Transaction"; - if (tx.ref_type === "loan_fund") label = "P2P Loan Deployed"; - else if (tx.ref_type === "loan_repay") label = "Repayment Received"; - else if (tx.category === "pool_deposit") label = "Pool Deposit"; - else if (tx.category === "pool_withdraw") label = "Pool Withdrawal"; - let type: "funding" | "repayment" | "deposit" | "withdrawal" = "funding"; - if (tx.ref_type === "loan_fund") type = "funding"; - else if (tx.ref_type === "loan_repay") type = "repayment"; - else if (tx.category === "pool_deposit") type = "deposit"; - else if (tx.category === "pool_withdraw") type = "withdrawal"; + if (tx.refType === "loan_fund") { + label = "P2P Loan Deployed"; + type = "funding"; + } else if (tx.refType === "loan_repay") { + label = "Repayment Received"; + type = "repayment"; + } else if (tx.category === "deposit" || tx.category === "pool_deposit") { + label = "Pool Deposit"; + type = "deposit"; + } else if (tx.category === "withdrawal" || tx.category === "pool_withdraw") { + label = "Pool Withdrawal"; + type = "withdrawal"; + } return { id: tx.id, @@ -109,24 +110,19 @@ export async function GET(request: NextRequest) { subLabel, amount: Number(tx.amount), currency: tx.currency || "XLM", - date: String(tx.created_at), + date: tx.createdAt.toISOString(), status: tx.status || "completed", txHash, type, }; }); - const nextCursor = formattedTransactions.length > 0 - ? formattedTransactions[formattedTransactions.length - 1].date - : undefined; + const nextCursor = + formattedTransactions.length > 0 ? formattedTransactions[formattedTransactions.length - 1].date : undefined; - return NextResponse.json({ - transactions: formattedTransactions, - hasMore, - nextCursor, - }); + return NextResponse.json({ transactions: formattedTransactions, hasMore, nextCursor }); } catch (err) { console.error("Lender transactions fetch error:", err); return NextResponse.json({ error: "Internal error" }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/app/api/loans/[id]/receipt/route.ts b/app/api/loans/[id]/receipt/route.ts index eb11944..c6f09a0 100644 --- a/app/api/loans/[id]/receipt/route.ts +++ b/app/api/loans/[id]/receipt/route.ts @@ -2,7 +2,10 @@ import fs from "node:fs/promises"; import path from "node:path"; import PDFDocument from "pdfkit"; import { NextRequest, NextResponse } from "next/server"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { and, asc, eq } from "drizzle-orm"; +import { getSessionUser } from "@/lib/auth/session"; +import { getDb } from "@/lib/db/client"; +import { loanRepayments, loans, profiles } from "@/lib/db/schema"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; export const runtime = "nodejs"; @@ -157,7 +160,7 @@ async function generateReceiptPdf({ .fillColor("#6b7280") .fontSize(9) .text( - "This receipt was generated by TrustLend from repayment records stored in Supabase and verified borrower loan history.", + "This receipt was generated by TrustLend from its repayment records and verified borrower loan history.", 50, 760, { width: 495, align: "center" } @@ -173,60 +176,63 @@ export async function GET(request: NextRequest, context: RouteContext) { if (rateLimited) return rateLimited; const { id } = await context.params; - const supabase = await getServerSupabaseClient(); - - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); } - const { - data: { user }, - } = await supabase.auth.getUser(); - + const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const [loanRes, repaymentsRes, profileRes] = await Promise.all([ - supabase - .from("loans") - .select("id, borrower_id, principal_amount, repaid_amount, apr_bps, duration_days, status, created_at") - .eq("id", id) - .eq("borrower_id", user.id) - .maybeSingle(), - supabase - .from("loan_repayments") - .select("id, amount, created_at") - .eq("loan_id", id) - .order("created_at", { ascending: true }), - supabase - .from("profiles") - .select("full_name") - .eq("id", user.id) - .maybeSingle(), + const [[loan], repaymentRows, [profile]] = await Promise.all([ + db + .select({ + id: loans.id, + borrowerId: loans.borrowerId, + principalAmount: loans.principalAmount, + repaidAmount: loans.repaidAmount, + aprBps: loans.aprBps, + durationDays: loans.durationDays, + status: loans.status, + createdAt: loans.createdAt, + }) + .from(loans) + .where(and(eq(loans.id, id), eq(loans.borrowerId, user.id))) + .limit(1), + db + .select({ id: loanRepayments.id, amount: loanRepayments.amount, createdAt: loanRepayments.createdAt }) + .from(loanRepayments) + .where(eq(loanRepayments.loanId, id)) + .orderBy(asc(loanRepayments.createdAt)), + db.select({ fullName: profiles.fullName }).from(profiles).where(eq(profiles.id, user.id)).limit(1), ]); - const loan = loanRes.data; if (!loan) { return NextResponse.json({ error: "Loan not found" }, { status: 404 }); } - if (String(loan.status) !== "repaid") { + if (loan.status !== "repaid") { return NextResponse.json({ error: "Receipt is only available for repaid loans" }, { status: 400 }); } - const repayments = (repaymentsRes.data ?? []) as LoanRepaymentRow[]; + const repayments: LoanRepaymentRow[] = repaymentRows.map((r) => ({ + id: r.id, + amount: r.amount, + created_at: r.createdAt.toISOString(), + })); const finalRepaymentAt = repayments.at(-1)?.created_at ?? null; const pdf = await generateReceiptPdf({ - loanId: String(loan.id), - borrowerId: String(loan.borrower_id), - borrowerName: String(profileRes.data?.full_name ?? user.user_metadata?.full_name ?? "TrustLend Borrower"), - principalAmount: loan.principal_amount, - aprBps: loan.apr_bps, - durationDays: loan.duration_days, - repaidAmount: loan.repaid_amount, - createdAt: loan.created_at, + loanId: loan.id, + borrowerId: loan.borrowerId, + borrowerName: profile?.fullName || user.fullName || "TrustLend Borrower", + principalAmount: loan.principalAmount, + aprBps: loan.aprBps, + durationDays: loan.durationDays, + repaidAmount: loan.repaidAmount, + createdAt: loan.createdAt.toISOString(), finalRepaymentAt, repayments, }); diff --git a/app/api/loans/apply/route.ts b/app/api/loans/apply/route.ts index 63d7ed9..019c245 100644 --- a/app/api/loans/apply/route.ts +++ b/app/api/loans/apply/route.ts @@ -1,7 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; +import { and, desc, eq, gte, notInArray } from "drizzle-orm"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { getDb } from "@/lib/db/client"; +import { ledgerTransactions, lendingPools, loans, reputationSnapshots } from "@/lib/db/schema"; import { requireKycVerified } from "@/lib/kyc/middleware"; import { isRedirectError } from "next/dist/client/components/redirect-error"; @@ -13,13 +15,13 @@ export async function POST(request: NextRequest) { } const { user } = await requireAuthenticatedUser("borrower"); - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); } // ── KYC guard: regulated pools require verified identity ───────────────── - const kycCheck = await requireKycVerified(user.id, supabase); + const kycCheck = await requireKycVerified(user.id, db); if (!kycCheck.allowed) { return NextResponse.json( { error: kycCheck.reason, kycStatus: kycCheck.kycStatus }, @@ -49,7 +51,7 @@ export async function POST(request: NextRequest) { ); } - if (!['fixed', 'floating'].includes(rateModel)) { + if (!["fixed", "floating"].includes(rateModel)) { return NextResponse.json( { error: `Invalid rate model: must be 'fixed' or 'floating'` }, { status: 400 } @@ -57,14 +59,15 @@ export async function POST(request: NextRequest) { } // ── 1. Anti-scam: only ONE active loan at a time ───────────────────────── - const { data: existingLoans } = await supabase - .from("loans") - .select("id, status") - .eq("borrower_id", user.id) - .not("status", "in", '("repaid","defaulted","cancelled")') + const existingLoans = await db + .select({ id: loans.id }) + .from(loans) + .where( + and(eq(loans.borrowerId, user.id), notInArray(loans.status, ["repaid", "defaulted", "cancelled"])), + ) .limit(1); - if (existingLoans && existingLoans.length > 0) { + if (existingLoans.length > 0) { return NextResponse.json( { error: @@ -75,13 +78,13 @@ export async function POST(request: NextRequest) { } // ── 2. Reputation / credit limit check ─────────────────────────────────── - const { data: reputation } = await supabase - .from("reputation_snapshots") - .select("score_total") - .eq("user_id", user.id) - .maybeSingle(); + const [reputation] = await db + .select({ scoreTotal: reputationSnapshots.scoreTotal }) + .from(reputationSnapshots) + .where(eq(reputationSnapshots.userId, user.id)) + .limit(1); - const reputationScore: number = reputation?.score_total ?? 250; + const reputationScore: number = reputation?.scoreTotal ?? 250; const maxLoan = reputationScore * 10; if (amount > maxLoan) { @@ -93,7 +96,7 @@ export async function POST(request: NextRequest) { // ── 3. Calculate APR ───────────────────────────────────────────────────────── let aprBps: number; - if (rateModel === 'floating') { + if (rateModel === "floating") { // Floating rate: base 5% + utilization slope // Start lower than fixed — the rate will be updated dynamically aprBps = 500; // 5% base floating rate @@ -107,19 +110,22 @@ export async function POST(request: NextRequest) { } // ── 4. Try to auto-assign a pool with enough liquidity and headroom under cap ─ - const { data: availablePools } = await supabase - .from("lending_pools") - .select("id, available_liquidity, total_borrowed, borrow_cap") - .eq("status", "active") - .gte("available_liquidity", amount) - .order("available_liquidity", { ascending: false }) + const availablePools = await db + .select({ + id: lendingPools.id, + availableLiquidity: lendingPools.availableLiquidity, + totalBorrowed: lendingPools.totalBorrowed, + borrowCap: lendingPools.borrowCap, + }) + .from(lendingPools) + .where(and(eq(lendingPools.status, "active"), gte(lendingPools.availableLiquidity, String(amount)))) + .orderBy(desc(lendingPools.availableLiquidity)) .limit(10); // fetch a few so we can apply cap filtering - const eligiblePool = (availablePools ?? []).find((p) => { + const eligiblePool = availablePools.find((p) => { // If a borrow cap is set, ensure there is headroom (#153) - if (p.borrow_cap !== null && p.borrow_cap !== undefined) { - const currentBorrowed = Number(p.total_borrowed ?? 0); - return currentBorrowed + amount <= Number(p.borrow_cap); + if (p.borrowCap !== null) { + return Number(p.totalBorrowed ?? 0) + amount <= Number(p.borrowCap); } return true; // no cap set — pool is eligible }); @@ -127,55 +133,44 @@ export async function POST(request: NextRequest) { const poolId = eligiblePool ? eligiblePool.id : null; // loan will be funded directly by a lender // ── 5. Create the loan ─────────────────────────────────────────────────── - const { data: loan, error: loanError } = await supabase - .from("loans") - .insert({ - borrower_id: user.id, - ...(poolId ? { pool_id: poolId } : {}), - principal_amount: amount, - apr_bps: aprBps, - duration_days: Number(durationDays), + const [loan] = await db + .insert(loans) + .values({ + borrowerId: user.id, + poolId, + principalAmount: String(amount), + aprBps, + durationDays: Number(durationDays), + rateModel, status: "requested", - metadata: { - rate_model: rateModel, - }, + metadata: { rate_model: rateModel }, }) - .select() - .single(); - - if (loanError) { - return NextResponse.json({ error: loanError.message }, { status: 500 }); - } + .returning(); // ── 6. Record request in ledger for traceability ──────────────────────── - const { error: ledgerError } = await supabase - .from("ledger_transactions") - .insert({ - user_id: user.id, + try { + await db.insert(ledgerTransactions).values({ + userId: user.id, category: "loan_request", - amount: Number(amount), + amount: String(amount), currency: "XLM", status: "confirmed", - ref_type: "loan_request", - ref_id: String(loan.id), + refType: "loan_request", + refId: loan.id, metadata: { stage: "requested", - loanId: String(loan.id), + loanId: loan.id, durationDays: Number(durationDays), aprBps, rateModel, fundingPath: poolId ? "pool" : "direct", }, }); - - if (ledgerError) { + } catch (ledgerError) { // Roll back the just-created loan to keep invariants strict: every request must have a ledger entry. - await supabase - .from("loans") - .delete() - .eq("id", String(loan.id)) - .eq("borrower_id", user.id); - return NextResponse.json({ error: `Failed to record transaction trail: ${ledgerError.message}` }, { status: 500 }); + await db.delete(loans).where(and(eq(loans.id, loan.id), eq(loans.borrowerId, user.id))); + const message = ledgerError instanceof Error ? ledgerError.message : String(ledgerError); + return NextResponse.json({ error: `Failed to record transaction trail: ${message}` }, { status: 500 }); } // ── Emit notification ── diff --git a/app/api/loans/fund/route.ts b/app/api/loans/fund/route.ts index 7dc266d..e2748d1 100644 --- a/app/api/loans/fund/route.ts +++ b/app/api/loans/fund/route.ts @@ -1,7 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { eq, sql } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { ledgerTransactions, loanFundings, loans } from "@/lib/db/schema"; import { sendLoanFundedEmail } from "@/lib/email/resend"; import { getFundingProgress, validateFundingAmount } from "@/lib/loans/funding"; import { qualifyReferralForLoan } from "@/lib/referrals/qualify"; @@ -31,8 +33,8 @@ export async function POST(request: NextRequest) { } const { user } = await requireAuthenticatedUser("lender"); - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); } @@ -59,11 +61,11 @@ export async function POST(request: NextRequest) { // ── Replay guard ───────────────────────────────────────────────────────── // Dedupe on the transaction hash, not on the loan: a loan may legitimately // receive many contributions, but each Stellar payment is claimable once. - const { data: existingFunding } = await supabase - .from("loan_fundings") - .select("id") - .eq("tx_hash", normalizedTxHash) - .maybeSingle(); + const [existingFunding] = await db + .select({ id: loanFundings.id }) + .from(loanFundings) + .where(eq(loanFundings.txHash, normalizedTxHash)) + .limit(1); if (existingFunding) { return NextResponse.json( @@ -73,29 +75,30 @@ export async function POST(request: NextRequest) { } // ── Fetch the loan ─────────────────────────────────────────────────────── - const { data: loan, error: fetchErr } = await supabase - .from("loans") - .select( - "id, status, principal_amount, funded_amount, borrower_id, pool_id, apr_bps, duration_days" - ) - .eq("id", loanId) - .maybeSingle(); - - if (fetchErr) { - // A database that has not had sql/08_partial_loan_fills.sql applied has - // no funded_amount column; say so instead of reporting "Loan not found". - if (String(fetchErr.message ?? "").includes("funded_amount")) { - return NextResponse.json( - { - error: - "Partial-fill columns are not installed in this database yet. Apply sql/08_partial_loan_fills.sql in Supabase, then retry funding.", - }, - { status: 500 } - ); - } - - return NextResponse.json({ error: "Loan not found" }, { status: 404 }); - } + const [loanRow] = await db + .select({ + id: loans.id, + status: loans.status, + principalAmount: loans.principalAmount, + fundedAmount: loans.fundedAmount, + borrowerId: loans.borrowerId, + aprBps: loans.aprBps, + durationDays: loans.durationDays, + }) + .from(loans) + .where(eq(loans.id, loanId)) + .limit(1); + + const loan = loanRow + ? { + ...loanRow, + principal_amount: Number(loanRow.principalAmount), + funded_amount: Number(loanRow.fundedAmount), + borrower_id: loanRow.borrowerId, + apr_bps: loanRow.aprBps, + duration_days: loanRow.durationDays, + } + : null; if (!loan) { return NextResponse.json({ error: "Loan not found" }, { status: 404 }); @@ -150,34 +153,27 @@ export async function POST(request: NextRequest) { // ── Record the contribution atomically ─────────────────────────────────── // The RPC locks the loan row, so concurrent lenders cannot both read the // same remaining balance and collectively overfund the loan. - const { data: fundingResult, error: rpcErr } = await supabase.rpc( - "record_loan_funding", - { - p_loan_id: loanId, - p_lender_id: user.id, - p_amount: contribution, - p_tx_hash: normalizedTxHash, - p_lender_address: lenderAddress ?? null, - p_funded_at: now, - } - ); - - if (rpcErr) { - const message = String(rpcErr.message ?? ""); - - if (message.includes("Could not find the function public.record_loan_funding")) { - return NextResponse.json( - { - error: - "Partial-fill funding RPC is not installed in this database yet. Apply sql/08_partial_loan_fills.sql in Supabase, then retry funding.", - }, - { status: 500 } - ); - } + type FundingResultRow = { + loan_id: string; + status: string; + principal_amount: string | number; + funded_amount: string | number; + remaining_amount: string | number; + is_fully_funded: boolean; + funding_id: string; + }; + let fundingRows: FundingResultRow[]; + try { + const executed = await db.execute( + sql`select loan_id, status, principal_amount, funded_amount, remaining_amount, is_fully_funded, funding_id + from public.record_loan_funding(${loanId}::uuid, ${user.id}::uuid, ${contribution}::numeric, ${normalizedTxHash}::text, ${lenderAddress ?? null}::text, ${now}::timestamptz)`, + ); + fundingRows = executed.rows as FundingResultRow[]; + } catch (rpcErr) { + const message = rpcErr instanceof Error ? rpcErr.message : String(rpcErr); - // The tx_hash unique index is the authoritative replay guard. The - // pre-check above can miss a duplicate recorded by a *different* lender, - // whose row RLS hides from this caller. + // The tx_hash unique index is the authoritative replay guard against a + // duplicate that slipped past the pre-check in a concurrent request. if ( message.includes("idx_loan_fundings_tx_hash") || message.includes("duplicate key value") @@ -201,8 +197,8 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: message }, { status: 500 }); } - // The RPC returns a single-row table. - const result = Array.isArray(fundingResult) ? fundingResult[0] : fundingResult; + // The function returns a single-row table. + const result = fundingRows[0]; const progressAfter = getFundingProgress( result?.principal_amount ?? loan.principal_amount, result?.funded_amount ?? progressBefore.funded + contribution @@ -210,15 +206,15 @@ export async function POST(request: NextRequest) { const isFullyFunded = Boolean(result?.is_fully_funded ?? progressAfter.isFullyFunded); // ── Record in ledger with full transparency info ────────────────────────── - await supabase.from("ledger_transactions").insert({ - user_id: user.id, // the lender + await db.insert(ledgerTransactions).values({ + userId: user.id, // the lender category: "loan_fund", - amount: contribution, + amount: String(contribution), currency: "XLM", status: "confirmed", - ref_type: "loan_fund", - ref_id: loanId, - metadata: JSON.stringify({ + refType: "loan_fund", + refId: loanId, + metadata: { txHash: normalizedTxHash, lenderAddress, lenderUserId: user.id, @@ -232,7 +228,7 @@ export async function POST(request: NextRequest) { aprBps: loan.apr_bps, durationDays: loan.duration_days, fundedAt: now, - }), + }, }); // ── Emit notifications ── @@ -258,7 +254,7 @@ export async function POST(request: NextRequest) { // referrer. The XLM payout itself is made on-chain by the lending // contract during activate_loan; this mirrors it for the dashboard. const referral = await qualifyReferralForLoan({ - supabase, + db, refereeId: String(loan.borrower_id), loanId, }); diff --git a/app/api/loans/repay/preflight/route.ts b/app/api/loans/repay/preflight/route.ts index 73d621a..765f065 100644 --- a/app/api/loans/repay/preflight/route.ts +++ b/app/api/loans/repay/preflight/route.ts @@ -1,7 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient, getServiceRoleClient } from "@/lib/supabase/server"; +import { and, eq } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { loans } from "@/lib/db/schema"; import { getLoanLenders } from "@/lib/loans/lenders"; import { MAX_LENDERS_PER_REPAYMENT } from "@/lib/loans/funding"; import { isRedirectError } from "next/dist/client/components/redirect-error"; @@ -24,18 +26,32 @@ export async function GET(request: NextRequest) { const loanId = request.nextUrl.searchParams.get("loanId"); if (!loanId) return NextResponse.json({ error: "loanId required" }, { status: 400 }); - const supabase = await getServerSupabaseClient(); - const srClient = getServiceRoleClient(); - if (!supabase || !srClient) return NextResponse.json({ error: "Database unavailable" }, { status: 500 }); - - const { data: loan } = await supabase - .from("loans") - .select("id, status, principal_amount, repaid_amount, apr_bps, duration_days, borrower_id, created_at, due_at") - .eq("id", loanId) - .eq("borrower_id", user.id) - .maybeSingle(); - - if (!loan) return NextResponse.json({ error: "Loan not found" }, { status: 404 }); + const db = getDb(); + if (!db) return NextResponse.json({ error: "Database unavailable" }, { status: 500 }); + + const [loanRow] = await db + .select({ + id: loans.id, + status: loans.status, + principalAmount: loans.principalAmount, + repaidAmount: loans.repaidAmount, + aprBps: loans.aprBps, + durationDays: loans.durationDays, + createdAt: loans.createdAt, + }) + .from(loans) + .where(and(eq(loans.id, loanId), eq(loans.borrowerId, user.id))) + .limit(1); + + if (!loanRow) return NextResponse.json({ error: "Loan not found" }, { status: 404 }); + const loan = { + status: loanRow.status, + principal_amount: Number(loanRow.principalAmount), + repaid_amount: Number(loanRow.repaidAmount), + apr_bps: loanRow.aprBps, + duration_days: loanRow.durationDays, + created_at: loanRow.createdAt.toISOString(), + }; const repayableStatuses = ["active", "funded", "approved"]; if (!repayableStatuses.includes(String(loan.status))) { @@ -44,9 +60,7 @@ export async function GET(request: NextRequest) { // Find every lender who funded this loan. A loan can be filled by several // lenders (Issue #269), so repayment is split pro-rata across all of them. - // Service role client: contributions belong to lenders and are not readable - // by the borrower under RLS. - const lenders = await getLoanLenders(srClient, loanId); + const lenders = await getLoanLenders(db, loanId); if (lenders.length === 0) { return NextResponse.json({ error: "Lender wallet not found for this loan. Cannot process on-chain repayment." }, { status: 422 }); @@ -110,7 +124,7 @@ export async function GET(request: NextRequest) { contribution: +entry.contribution.toFixed(7), share: totalContributed > 0 ? +(entry.contribution / totalContributed).toFixed(7) : 0, })), - borrowerAddress: user.user_metadata?.wallet_address ?? "", + borrowerAddress: user.walletAddress ?? "", breakdown: { principal: +principal.toFixed(7), interest: +totalInterest.toFixed(7), diff --git a/app/api/loans/repay/route.ts b/app/api/loans/repay/route.ts index d83ed0b..6fee0a2 100644 --- a/app/api/loans/repay/route.ts +++ b/app/api/loans/repay/route.ts @@ -1,7 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient, getServiceRoleClient } from "@/lib/supabase/server"; +import { and, eq, sql } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { ledgerTransactions, loanRepayments, loans, reputationEvents } from "@/lib/db/schema"; import { getLoanLenders } from "@/lib/loans/lenders"; import { splitRepaymentAcrossLenders } from "@/lib/loans/funding"; import { isRedirectError } from "next/dist/client/components/redirect-error"; @@ -30,31 +32,48 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "A confirmed Stellar transaction hash is required for on-chain repayment" }, { status: 400 }); } - const supabase = await getServerSupabaseClient(); - const srClient = getServiceRoleClient(); - if (!supabase || !srClient) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 500 }); } // Double-check borrower & loan - const { data: loan, error: loanError } = await supabase - .from("loans") - .select("id, borrower_id, status, repaid_amount, principal_amount, apr_bps, duration_days") - .eq("id", loanId) - .eq("borrower_id", user.id) - .single(); - - if (loanError || !loan) return NextResponse.json({ error: "Loan not found" }, { status: 404 }); + const [loanRow] = await db + .select({ + id: loans.id, + status: loans.status, + repaidAmount: loans.repaidAmount, + principalAmount: loans.principalAmount, + aprBps: loans.aprBps, + durationDays: loans.durationDays, + }) + .from(loans) + .where(and(eq(loans.id, loanId), eq(loans.borrowerId, user.id))) + .limit(1); + + if (!loanRow) return NextResponse.json({ error: "Loan not found" }, { status: 404 }); + const loan = { + id: loanRow.id, + status: loanRow.status as string, + repaid_amount: Number(loanRow.repaidAmount), + principal_amount: Number(loanRow.principalAmount), + apr_bps: loanRow.aprBps, + duration_days: loanRow.durationDays, + }; if (loan.status === "repaid") return NextResponse.json({ error: "Loan is already fully repaid" }, { status: 400 }); if (loan.status === "defaulted") return NextResponse.json({ error: "Loan is in default" }, { status: 400 }); // Prevent duplicate txHash - const { data: existingTx } = await srClient - .from("ledger_transactions") - .select("id") - .eq("ref_type", "loan_repay") - .ilike("metadata->>txHash", txHash) // check if JSON contains this hash - .maybeSingle(); + const [existingTx] = await db + .select({ id: ledgerTransactions.id }) + .from(ledgerTransactions) + .where( + and( + eq(ledgerTransactions.refType, "loan_repay"), + sql`lower(${ledgerTransactions.metadata}->>'txHash') = lower(${txHash})`, + ), + ) + .limit(1); if (existingTx) { return NextResponse.json({ error: "This transaction hash has already been recorded" }, { status: 409 }); @@ -62,25 +81,17 @@ export async function POST(request: NextRequest) { // Figure out every lender to notify. A loan can be funded by several // lenders (Issue #269), each owed a pro-rata slice of this repayment. - const lenders = await getLoanLenders(srClient, loanId); + const lenders = await getLoanLenders(db, loanId); const primaryLender = lenders[0]; const lenderUserId = primaryLender?.lenderId ?? ""; const lenderAddress = primaryLender?.address ?? ""; const lenderPayouts = splitRepaymentAcrossLenders(amount, lenders); // Create repayment record in DB - const { data: repayment, error: repaymentError } = await srClient - .from("loan_repayments") - .insert({ - loan_id: loanId, - payer_id: user.id, - amount: amount, - tx_ref: txHash, - }) - .select() - .single(); - - if (repaymentError) return NextResponse.json({ error: repaymentError.message }, { status: 500 }); + const [repayment] = await db + .insert(loanRepayments) + .values({ loanId, payerId: user.id, amount: String(amount), txRef: txHash }) + .returning(); // Calculate updated balances const newRepaidAmount = (loan.repaid_amount || 0) + amount; @@ -101,26 +112,24 @@ export async function POST(request: NextRequest) { newStatus = "active"; } - const { error: updateError } = await srClient - .from("loans") - .update({ - repaid_amount: newRepaidAmount, - status: newStatus, + await db + .update(loans) + .set({ + repaidAmount: String(newRepaidAmount), + status: newStatus as typeof loans.$inferInsert.status, }) - .eq("id", loanId); - - if (updateError) return NextResponse.json({ error: updateError.message }, { status: 500 }); + .where(eq(loans.id, loanId)); // Record on Ledger - await srClient.from("ledger_transactions").insert({ - user_id: user.id, // the borrower + await db.insert(ledgerTransactions).values({ + userId: user.id, // the borrower category: "loan_repay", - amount: amount, + amount: String(amount), currency: "XLM", status: "confirmed", - ref_type: "loan_repay", - ref_id: repayment.id, // link to the repayment record - metadata: JSON.stringify({ + refType: "loan_repay", + refId: repayment.id, // link to the repayment record + metadata: { txHash, borrowerAddress, lenderAddress, @@ -138,17 +147,17 @@ export async function POST(request: NextRequest) { principalAmount: loan.principal_amount, repaidSoFar: newRepaidAmount, repaidAt: new Date().toISOString(), - }), + }, }); // Add reputation points const repayPoints = newStatus === "repaid" ? 20 : 5; - await srClient.from("reputation_events").insert({ - user_id: user.id, - source_type: "loan_repayment", - source_id: loanId, - points_delta: repayPoints, - reason: `On-chain repayment of ${amount.toFixed(2)} XLM`, + await db.insert(reputationEvents).values({ + userId: user.id, + sourceType: "loan_repayment", + sourceId: loanId, + pointsDelta: repayPoints, + reason: `On-chain repayment of ${amount.toFixed(2)} XLM`, }); // Notifications diff --git a/app/api/loans/repayments/route.ts b/app/api/loans/repayments/route.ts index 1a9bec6..858ef9f 100644 --- a/app/api/loans/repayments/route.ts +++ b/app/api/loans/repayments/route.ts @@ -1,7 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { and, desc, eq } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { loanRepayments, loans } from "@/lib/db/schema"; import { isRedirectError } from "next/dist/client/components/redirect-error"; export async function GET(request: NextRequest) { @@ -17,55 +19,39 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: "loanId is required" }, { status: 400 }); } - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 500 }); } // Verify loan belongs to this borrower - const { data: loan } = await supabase - .from("loans") - .select("id, principal_amount, repaid_amount, status") - .eq("id", loanId) - .eq("borrower_id", user.id) - .maybeSingle(); + const [loan] = await db + .select({ + id: loans.id, + principal_amount: loans.principalAmount, + repaid_amount: loans.repaidAmount, + status: loans.status, + }) + .from(loans) + .where(and(eq(loans.id, loanId), eq(loans.borrowerId, user.id))) + .limit(1); if (!loan) { return NextResponse.json({ error: "Loan not found" }, { status: 404 }); } - // TODO (SubQuery Indexer Migration): - // Migrate this data fetch to read from the SubQuery Indexer: - // 1. Fetch repayment events by querying the SubQuery GraphQL endpoint: - // query { - // repayments(filter: { loanId: { equalTo: "${loanId}" } }, orderBy: TIMESTAMP_DESC) { - // nodes { - // id - // amount - // timestamp - // } - // } - // } - // 2. Map the results back to the expected output payload: - // repayments: subqueryData.repayments.nodes.map(r => ({ - // id: r.id, - // repayment_id: r.id, - // amount: Number(r.amount), - // created_at: r.timestamp - // })) - // Fetch repayment history - const { data: repayments } = await supabase - .from("loan_repayments") - .select("id, amount, created_at") - .eq("loan_id", loanId) - .order("created_at", { ascending: false }) + const repayments = await db + .select({ id: loanRepayments.id, amount: loanRepayments.amount, created_at: loanRepayments.createdAt }) + .from(loanRepayments) + .where(eq(loanRepayments.loanId, loanId)) + .orderBy(desc(loanRepayments.createdAt)) .limit(50); const dueAmount = Math.max(0, Number(loan.principal_amount) - Number(loan.repaid_amount ?? 0)); return NextResponse.json({ - repayments: (repayments ?? []).map((r) => ({ + repayments: repayments.map((r) => ({ id: r.id, repayment_id: r.id, amount: Number(r.amount), diff --git a/app/api/notifications/clear/route.ts b/app/api/notifications/clear/route.ts index a34cfaa..b9b1afe 100644 --- a/app/api/notifications/clear/route.ts +++ b/app/api/notifications/clear/route.ts @@ -1,36 +1,32 @@ import { NextRequest, NextResponse } from "next/server"; +import { eq } from "drizzle-orm"; +import { getSessionUser } from "@/lib/auth/session"; +import { getDb } from "@/lib/db/client"; +import { notifications } from "@/lib/db/schema"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; -export async function POST(_request: NextRequest) { +export async function POST(request: NextRequest) { try { - const rateLimitResponse = await enforceRouteRateLimit(_request); + const rateLimitResponse = await enforceRouteRateLimit(request); if (rateLimitResponse) { return rateLimitResponse; } - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "DB unavailable" }, { status: 503 }); } - const { data: { user } } = await supabase.auth.getUser(); + const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Delete all notifications for this user - const { error } = await supabase - .from("notifications") - .delete() - .eq("user_id", user.id); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } + await db.delete(notifications).where(eq(notifications.userId, user.id)); return NextResponse.json({ success: true }, { status: 200 }); - } catch (_error) { + } catch { return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } diff --git a/app/api/notifications/route.ts b/app/api/notifications/route.ts index 4fe2c86..e99bb6f 100644 --- a/app/api/notifications/route.ts +++ b/app/api/notifications/route.ts @@ -1,5 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { desc, eq } from "drizzle-orm"; +import { getSessionUser } from "@/lib/auth/session"; +import { getDb } from "@/lib/db/client"; +import { notifications } from "@/lib/db/schema"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; export async function GET(request: NextRequest) { @@ -7,29 +10,36 @@ export async function GET(request: NextRequest) { const rateLimited = await enforceRouteRateLimit(request); if (rateLimited) return rateLimited; - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "DB unavailable" }, { status: 503 }); } - const { data: { user } } = await supabase.auth.getUser(); + const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const { data, error } = await supabase - .from("notifications") - .select("*") - .eq("user_id", user.id) - .order("created_at", { ascending: false }) + const rows = await db + .select() + .from(notifications) + .where(eq(notifications.userId, user.id)) + .orderBy(desc(notifications.createdAt)) .limit(20); - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } + // Keep the snake_case wire format the NotificationWidget expects. + const data = rows.map((n) => ({ + id: n.id, + user_id: n.userId, + title: n.title, + message: n.message, + type: n.type, + read: n.read, + created_at: n.createdAt.toISOString(), + })); return NextResponse.json({ notifications: data }, { status: 200 }); - } catch (_error) { + } catch { return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } diff --git a/app/api/pools/deposit/route.ts b/app/api/pools/deposit/route.ts index 61a0c5c..d034fbf 100644 --- a/app/api/pools/deposit/route.ts +++ b/app/api/pools/deposit/route.ts @@ -1,7 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { and, eq, sql } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { ledgerTransactions, lendingPools, poolPositions } from "@/lib/db/schema"; import { requireKycVerified } from "@/lib/kyc/middleware"; import { isRedirectError } from "next/dist/client/components/redirect-error"; @@ -14,9 +16,6 @@ import { isRedirectError } from "next/dist/client/components/redirect-error"; * 1. Lender signs a real Stellar payment tx in Freighter (client-side) * 2. Client passes the confirmed tx hash here * 3. We verify the tx hash is non-empty, then record the position - * - * Using direct supabase.auth.getUser() to return JSON on auth failure - * instead of calling requireAuthenticatedUser which redirect()s → 307 HTML. */ export async function POST(request: NextRequest) { try { @@ -26,13 +25,13 @@ export async function POST(request: NextRequest) { } const { user } = await requireAuthenticatedUser("lender"); - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); } // ── KYC guard: regulated pool deposits require verified identity ──────── - const kycCheck = await requireKycVerified(user.id, supabase, { regulatedPoolOnly: true }); + const kycCheck = await requireKycVerified(user.id, db, { regulatedPoolOnly: true }); if (!kycCheck.allowed) { return NextResponse.json( { error: kycCheck.reason, kycStatus: kycCheck.kycStatus }, @@ -64,11 +63,11 @@ export async function POST(request: NextRequest) { } // Prevent duplicate recording of the same tx - const { data: existingTx } = await supabase - .from("ledger_transactions") - .select("id") - .eq("metadata->>txHash", txHash) - .maybeSingle(); + const [existingTx] = await db + .select({ id: ledgerTransactions.id }) + .from(ledgerTransactions) + .where(sql`${ledgerTransactions.metadata}->>'txHash' = ${txHash}`) + .limit(1); if (existingTx) { return NextResponse.json( @@ -78,83 +77,58 @@ export async function POST(request: NextRequest) { } // Verify pool exists and is active - const { data: pool, error: poolError } = await supabase - .from("lending_pools") - .select("id, status, total_liquidity, available_liquidity") - .eq("id", poolId) - .eq("status", "active") - .single(); + const [pool] = await db + .select({ id: lendingPools.id }) + .from(lendingPools) + .where(and(eq(lendingPools.id, poolId), eq(lendingPools.status, "active"))) + .limit(1); - if (poolError || !pool) { + if (!pool) { return NextResponse.json({ error: "Pool not found or inactive" }, { status: 404 }); } // Upsert pool position (add to existing or create new) - const { data: existingPosition } = await supabase - .from("pool_positions") - .select("id, principal_amount") - .eq("pool_id", poolId) - .eq("lender_id", user.id) - .eq("status", "active") - .maybeSingle(); + const [existingPosition] = await db + .select({ id: poolPositions.id }) + .from(poolPositions) + .where( + and(eq(poolPositions.poolId, poolId), eq(poolPositions.lenderId, user.id), eq(poolPositions.status, "active")), + ) + .limit(1); let position; if (existingPosition) { - const { data: updated, error: updateError } = await supabase - .from("pool_positions") - .update({ - principal_amount: Number(existingPosition.principal_amount ?? 0) + amount, - }) - .eq("id", existingPosition.id) - .select() - .single(); - - if (updateError) return NextResponse.json({ error: updateError.message }, { status: 500 }); - position = updated; + [position] = await db + .update(poolPositions) + .set({ principalAmount: sql`${poolPositions.principalAmount} + ${amount}` }) + .where(eq(poolPositions.id, existingPosition.id)) + .returning(); } else { - const { data: newPosition, error: insertError } = await supabase - .from("pool_positions") - .insert({ - pool_id: poolId, - lender_id: user.id, - principal_amount: amount, - status: "active", - opened_at: new Date().toISOString(), - }) - .select() - .single(); - - if (insertError) return NextResponse.json({ error: insertError.message }, { status: 500 }); - position = newPosition; + [position] = await db + .insert(poolPositions) + .values({ poolId, lenderId: user.id, principalAmount: String(amount), status: "active" }) + .returning(); } - // Update pool liquidity atomically - const { error: poolUpdateError } = await supabase - .from("lending_pools") - .update({ - total_liquidity: Number(pool.total_liquidity ?? 0) + amount, - available_liquidity: Number(pool.available_liquidity ?? 0) + amount, + // Update pool liquidity atomically (SQL-side increment, no read-modify-write race) + await db + .update(lendingPools) + .set({ + totalLiquidity: sql`${lendingPools.totalLiquidity} + ${amount}`, + availableLiquidity: sql`${lendingPools.availableLiquidity} + ${amount}`, }) - .eq("id", poolId); - - if (poolUpdateError) { - return NextResponse.json({ error: poolUpdateError.message }, { status: 500 }); - } + .where(eq(lendingPools.id, poolId)); // Record ledger entry with tx hash for on-chain verification - await supabase.from("ledger_transactions").insert({ - user_id: user.id, + await db.insert(ledgerTransactions).values({ + userId: user.id, category: "deposit", - amount, + amount: String(amount), currency: "XLM", status: "confirmed", - ref_type: "pool_position", - ref_id: position.id, - metadata: JSON.stringify({ - txHash, - lenderAddress: lenderAddress ?? null, - poolId, - }), + refType: "pool_position", + refId: position.id, + metadata: { txHash, lenderAddress: lenderAddress ?? null, poolId }, }); return NextResponse.json( diff --git a/app/api/pools/route.ts b/app/api/pools/route.ts index ccae105..7d1b30e 100644 --- a/app/api/pools/route.ts +++ b/app/api/pools/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { getDb } from "@/lib/db/client"; import { fetchPools } from "@/lib/db/pools"; /** @@ -40,8 +40,8 @@ export async function GET(request: NextRequest) { const rateLimited = await enforceRouteRateLimit(request); if (rateLimited) return rateLimited; - const supabase = getServiceRoleClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json( { error: "Database service unavailable" }, { status: 500 } @@ -70,7 +70,7 @@ export async function GET(request: NextRequest) { | "desc"; // Fetch pools using optimized function - const result = await fetchPools(supabase, { + const result = await fetchPools(db, { status: status || undefined, limit, offset, diff --git a/app/api/pools/sep31-deposit/route.ts b/app/api/pools/sep31-deposit/route.ts index a4b5649..018d45d 100644 --- a/app/api/pools/sep31-deposit/route.ts +++ b/app/api/pools/sep31-deposit/route.ts @@ -1,7 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { and, eq, sql } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { ledgerTransactions, lendingPools } from "@/lib/db/schema"; import { requireKycVerified } from "@/lib/kyc/middleware"; /** @@ -19,13 +21,13 @@ export async function POST(request: NextRequest) { } const { user } = await requireAuthenticatedUser("lender"); - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); } // Require KYC verification for lenders using fiat rails - const kycCheck = await requireKycVerified(user.id, supabase, { regulatedPoolOnly: true }); + const kycCheck = await requireKycVerified(user.id, db, { regulatedPoolOnly: true }); if (!kycCheck.allowed) { return NextResponse.json( { error: kycCheck.reason, kycStatus: kycCheck.kycStatus }, @@ -48,23 +50,22 @@ export async function POST(request: NextRequest) { } // Verify pool exists and is active - const { data: pool, error: poolError } = await supabase - .from("lending_pools") - .select("id, status") - .eq("id", poolId) - .eq("status", "active") - .single(); + const [pool] = await db + .select({ id: lendingPools.id }) + .from(lendingPools) + .where(and(eq(lendingPools.id, poolId), eq(lendingPools.status, "active"))) + .limit(1); - if (poolError || !pool) { + if (!pool) { return NextResponse.json({ error: "Pool not found or inactive" }, { status: 404 }); } // Prevent duplicate recording of the same anchor transaction - const { data: existingTx } = await supabase - .from("ledger_transactions") - .select("id") - .eq("metadata->>anchorTxId", anchorTxId) - .maybeSingle(); + const [existingTx] = await db + .select({ id: ledgerTransactions.id }) + .from(ledgerTransactions) + .where(sql`${ledgerTransactions.metadata}->>'anchorTxId' = ${anchorTxId}`) + .limit(1); if (existingTx) { return NextResponse.json( @@ -74,30 +75,19 @@ export async function POST(request: NextRequest) { } // Record the pending deposit ledger entry - const { data: transaction, error: txError } = await supabase - .from("ledger_transactions") - .insert({ - user_id: user.id, + const [transaction] = await db + .insert(ledgerTransactions) + .values({ + userId: user.id, category: "deposit", - amount, + amount: String(amount), currency, status: "pending", - ref_type: "pool_position", - ref_id: null, // pool_position is created asynchronously upon webhook confirmation - metadata: JSON.stringify({ - anchorTxId, - instructions, - poolId, - lenderAddress: lenderAddress ?? null, - isSep31: true, - }), + refType: "pool_position", + refId: null, // pool_position is created asynchronously upon webhook confirmation + metadata: { anchorTxId, instructions, poolId, lenderAddress: lenderAddress ?? null, isSep31: true }, }) - .select() - .single(); - - if (txError) { - return NextResponse.json({ error: txError.message }, { status: 500 }); - } + .returning(); return NextResponse.json({ success: true, transaction }); } catch (error) { diff --git a/app/api/pools/withdraw/route.ts b/app/api/pools/withdraw/route.ts index 2c70a6d..89e5e4b 100644 --- a/app/api/pools/withdraw/route.ts +++ b/app/api/pools/withdraw/route.ts @@ -1,7 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { and, eq, sql } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { ledgerTransactions, lendingPools, poolPositions } from "@/lib/db/schema"; import { isRedirectError } from "next/dist/client/components/redirect-error"; export async function POST(request: NextRequest) { @@ -21,23 +23,34 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Amount exceeds maximum allowed" }, { status: 400 }); } - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 500 }); } // Get position and verify ownership - const { data: position, error: positionError } = await supabase - .from("pool_positions") - .select("id, pool_id, principal_amount, withdrawn_amount") - .eq("id", positionId) - .eq("lender_id", user.id) - .eq("status", "active") - .single(); + const [positionRow] = await db + .select({ + id: poolPositions.id, + poolId: poolPositions.poolId, + principalAmount: poolPositions.principalAmount, + withdrawnAmount: poolPositions.withdrawnAmount, + }) + .from(poolPositions) + .where( + and(eq(poolPositions.id, positionId), eq(poolPositions.lenderId, user.id), eq(poolPositions.status, "active")), + ) + .limit(1); - if (positionError || !position) { + if (!positionRow) { return NextResponse.json({ error: "Position not found" }, { status: 404 }); } + const position = { + id: positionRow.id, + pool_id: positionRow.poolId, + principal_amount: Number(positionRow.principalAmount), + withdrawn_amount: Number(positionRow.withdrawnAmount), + }; if (amount > position.principal_amount) { return NextResponse.json( @@ -47,17 +60,17 @@ export async function POST(request: NextRequest) { } // Get pool - const { data: pool, error: poolError } = await supabase - .from("lending_pools") - .select("id, total_liquidity, available_liquidity") - .eq("id", position.pool_id) - .single(); + const [poolRow] = await db + .select({ availableLiquidity: lendingPools.availableLiquidity }) + .from(lendingPools) + .where(eq(lendingPools.id, position.pool_id)) + .limit(1); - if (poolError || !pool) { + if (!poolRow) { return NextResponse.json({ error: "Pool not found" }, { status: 404 }); } - if (amount > pool.available_liquidity) { + if (amount > Number(poolRow.availableLiquidity)) { return NextResponse.json( { error: "Insufficient liquidity in pool for withdrawal" }, { status: 400 } @@ -66,42 +79,34 @@ export async function POST(request: NextRequest) { // Update position const newPrincipal = position.principal_amount - amount; - const { error: updateError } = await supabase - .from("pool_positions") - .update({ - principal_amount: newPrincipal, - withdrawn_amount: (position.withdrawn_amount || 0) + amount, + await db + .update(poolPositions) + .set({ + principalAmount: String(newPrincipal), + withdrawnAmount: String(position.withdrawn_amount + amount), status: newPrincipal === 0 ? "closed" : "active", - closed_at: newPrincipal === 0 ? new Date().toISOString() : null, + closedAt: newPrincipal === 0 ? new Date() : null, }) - .eq("id", positionId); - - if (updateError) { - return NextResponse.json({ error: updateError.message }, { status: 500 }); - } - - // Update pool liquidity - const { error: poolUpdateError } = await supabase - .from("lending_pools") - .update({ - total_liquidity: pool.total_liquidity - amount, - available_liquidity: pool.available_liquidity - amount, + .where(eq(poolPositions.id, positionId)); + + // Update pool liquidity (SQL-side decrement) + await db + .update(lendingPools) + .set({ + totalLiquidity: sql`${lendingPools.totalLiquidity} - ${amount}`, + availableLiquidity: sql`${lendingPools.availableLiquidity} - ${amount}`, }) - .eq("id", position.pool_id); - - if (poolUpdateError) { - return NextResponse.json({ error: poolUpdateError.message }, { status: 500 }); - } + .where(eq(lendingPools.id, position.pool_id)); // Record transaction - await supabase.from("ledger_transactions").insert({ - user_id: user.id, + await db.insert(ledgerTransactions).values({ + userId: user.id, category: "withdrawal", - amount: amount, + amount: String(amount), currency: "XLM", status: "confirmed", - ref_type: "pool_position", - ref_id: positionId, + refType: "pool_position", + refId: positionId, }); return NextResponse.json( diff --git a/app/api/referrals/claim/route.ts b/app/api/referrals/claim/route.ts index c058329..7fb5ab8 100644 --- a/app/api/referrals/claim/route.ts +++ b/app/api/referrals/claim/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { sql } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; import { normalizeReferralCode } from "@/lib/referrals/codes"; import { isRedirectError } from "next/dist/client/components/redirect-error"; @@ -12,7 +13,8 @@ import { isRedirectError } from "next/dist/client/components/redirect-error"; * Called once, right after a user signs up through an invite link. * * Attribution is deliberately server-side and idempotent: - * • record_referral() is security definer, so the referee cannot forge a row + * • record_referral() runs in SQL with the caller id taken from the session, + * so the referee cannot forge a row * • the unique constraint on referee_id makes a double submit a no-op * • self-referral is rejected in SQL as well as here * @@ -26,8 +28,8 @@ export async function POST(request: NextRequest) { } const { user } = await requireAuthenticatedUser(); - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); } @@ -41,13 +43,14 @@ export async function POST(request: NextRequest) { ); } - const { data, error } = await supabase.rpc("record_referral", { - p_referee_id: user.id, - p_referral_code: code, - }); - - if (error) { - const message = error.message ?? ""; + let row: { referral_id?: string; status?: string } | undefined; + try { + const result = await db.execute( + sql`select referral_id, referrer_id, status from public.record_referral(${user.id}::uuid, ${code}::text)`, + ); + row = result.rows[0] as typeof row; + } catch (error) { + const message = error instanceof Error ? error.message : ""; // Map the SQL guards onto meaningful status codes rather than a blanket // 500 — an unknown code is a client mistake, not a server fault. if (message.includes("Unknown referral code")) { @@ -69,8 +72,6 @@ export async function POST(request: NextRequest) { ); } - const row = Array.isArray(data) ? data[0] : data; - return NextResponse.json( { referralId: row?.referral_id ? String(row.referral_id) : null, diff --git a/app/api/referrals/route.ts b/app/api/referrals/route.ts index fbb782c..863bef0 100644 --- a/app/api/referrals/route.ts +++ b/app/api/referrals/route.ts @@ -1,7 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { desc, eq, sql } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { referrals } from "@/lib/db/schema"; import { buildReferralLink } from "@/lib/referrals/codes"; import { resolveSiteUrl } from "@/lib/referrals/site-url"; import { isRedirectError } from "next/dist/client/components/redirect-error"; @@ -22,47 +24,48 @@ export async function GET(request: NextRequest) { } const { user } = await requireAuthenticatedUser(); - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); } // Guarantees a code exists before we try to build a link from it. - const { data: code, error: codeError } = await supabase.rpc( - "ensure_referral_code", - { p_user_id: user.id }, + const codeResult = await db.execute( + sql`select public.ensure_referral_code(${user.id}::uuid) as code`, ); + const code = (codeResult.rows[0] as { code?: string } | undefined)?.code; - if (codeError || !code) { - console.error("Referral code assignment failed:", codeError?.message); + if (!code) { + console.error("Referral code assignment failed for", user.id); return NextResponse.json( { error: "Could not prepare your referral code" }, { status: 500 }, ); } - const { data: statsRows, error: statsError } = await supabase.rpc( - "get_referral_stats", - { p_user_id: user.id }, - ); - - if (statsError) { - console.error("Referral stats lookup failed:", statsError.message); - return NextResponse.json( - { error: "Could not load your referral stats" }, - { status: 500 }, - ); - } - - const stats = Array.isArray(statsRows) ? statsRows[0] : statsRows; + const [stats] = await db + .select({ + total_invited: sql`count(*)::int`, + pending_count: sql`count(*) filter (where ${referrals.status} = 'pending')::int`, + qualified_count: sql`count(*) filter (where ${referrals.status} = 'qualified')::int`, + paid_count: sql`count(*) filter (where ${referrals.status} = 'paid')::int`, + total_earned: sql`coalesce(sum(${referrals.bonusAmount}) filter (where ${referrals.status} = 'paid'), 0)`, + }) + .from(referrals) + .where(eq(referrals.referrerId, user.id)); - // The invited-user list is read directly; RLS restricts it to rows where - // the caller is the referrer. - const { data: referrals } = await supabase - .from("referrals") - .select("id, status, bonus_amount, created_at, qualified_at, paid_at") - .eq("referrer_id", user.id) - .order("created_at", { ascending: false }) + const invited = await db + .select({ + id: referrals.id, + status: referrals.status, + bonus_amount: referrals.bonusAmount, + created_at: referrals.createdAt, + qualified_at: referrals.qualifiedAt, + paid_at: referrals.paidAt, + }) + .from(referrals) + .where(eq(referrals.referrerId, user.id)) + .orderBy(desc(referrals.createdAt)) .limit(50); return NextResponse.json( @@ -76,13 +79,13 @@ export async function GET(request: NextRequest) { paid: Number(stats?.paid_count ?? 0), totalEarned: Number(stats?.total_earned ?? 0), }, - referrals: (referrals ?? []).map((r) => ({ - id: String(r.id), - status: String(r.status), + referrals: invited.map((r) => ({ + id: r.id, + status: r.status, bonusAmount: Number(r.bonus_amount ?? 0), - invitedAt: r.created_at, - qualifiedAt: r.qualified_at, - paidAt: r.paid_at, + invitedAt: r.created_at.toISOString(), + qualifiedAt: r.qualified_at ? r.qualified_at.toISOString() : null, + paidAt: r.paid_at ? r.paid_at.toISOString() : null, })), }, { status: 200 }, diff --git a/app/api/reputation/route.ts b/app/api/reputation/route.ts index 726d6e5..1881673 100644 --- a/app/api/reputation/route.ts +++ b/app/api/reputation/route.ts @@ -1,12 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { desc, eq } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { profiles, reputationEvents, reputationSnapshots } from "@/lib/db/schema"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { - scoreToTier, - TIER_INTEREST_BPS, - TIER_MAX_LOAN, -} from "@/types/contracts"; -import { STANDARD_BASE_APR_BPS } from "@/lib/reputation/scoring"; +import { TIER_INTEREST_BPS, TIER_MAX_LOAN } from "@/types/contracts"; +import { offchainScoreToTier, STANDARD_BASE_APR_BPS } from "@/lib/reputation/scoring"; export async function GET(request: NextRequest) { try { @@ -20,55 +18,54 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: "wallet address is required" }, { status: 400 }); } - const supabase = getServiceRoleClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database service unavailable" }, { status: 500 }); } // 1. Fetch user profile by wallet_address to get user_id - const { data: profile, error: profileError } = await supabase - .from("profiles") - .select("id, full_name, wallet_address, kyc_status") - .eq("wallet_address", address) - .maybeSingle(); - - if (profileError) { - return NextResponse.json({ error: profileError.message }, { status: 500 }); - } + const [profile] = await db + .select({ id: profiles.id, full_name: profiles.fullName, wallet_address: profiles.walletAddress }) + .from(profiles) + .where(eq(profiles.walletAddress, address)) + .limit(1); if (!profile) { return NextResponse.json({ error: "Borrower profile not found for this address" }, { status: 404 }); } // 2. Fetch reputation snapshot - const { data: reputation, error: reputationError } = await supabase - .from("reputation_snapshots") - .select("score_total, tier, score_breakdown, updated_at") - .eq("user_id", profile.id) - .maybeSingle(); - - if (reputationError) { - return NextResponse.json({ error: reputationError.message }, { status: 500 }); - } + const [reputation] = await db + .select({ + score_total: reputationSnapshots.scoreTotal, + score_breakdown: reputationSnapshots.scoreBreakdown, + updated_at: reputationSnapshots.updatedAt, + }) + .from(reputationSnapshots) + .where(eq(reputationSnapshots.userId, profile.id)) + .limit(1); const score = Number(reputation?.score_total ?? 250); - const tier = scoreToTier(BigInt(score)); + const tier = offchainScoreToTier(score); const interestRateBps = TIER_INTEREST_BPS[tier] ?? STANDARD_BASE_APR_BPS; const rateDiscountBps = Math.max(0, STANDARD_BASE_APR_BPS - interestRateBps); const maxLoanStroops = TIER_MAX_LOAN[tier] ?? TIER_MAX_LOAN.None; const maxLoanXlm = Number(maxLoanStroops / 10_000_000n); // 3. Fetch reputation history events - const { data: history, error: historyError } = await supabase - .from("reputation_events") - .select("id, event_type, points, description, created_at") - .eq("user_id", profile.id) - .order("created_at", { ascending: false }) + const historyRows = await db + .select({ + id: reputationEvents.id, + event_type: reputationEvents.sourceType, + points: reputationEvents.pointsDelta, + description: reputationEvents.reason, + created_at: reputationEvents.createdAt, + }) + .from(reputationEvents) + .where(eq(reputationEvents.userId, profile.id)) + .orderBy(desc(reputationEvents.createdAt)) .limit(10); - - if (historyError) { - return NextResponse.json({ error: historyError.message }, { status: 500 }); - } + const history = historyRows.map((h) => ({ ...h, created_at: h.created_at.toISOString() })); return NextResponse.json( { @@ -83,9 +80,9 @@ export async function GET(request: NextRequest) { limit_xlm: maxLoanXlm, breakdown: reputation?.score_breakdown || null, calculated_daily: true, - updated_at: reputation?.updated_at || null, + updated_at: reputation?.updated_at ? reputation.updated_at.toISOString() : null, }, - history: history || [], + history, }, { status: 200 } ); diff --git a/app/api/tasks/complete/route.ts b/app/api/tasks/complete/route.ts index 04fcf33..a94b08c 100644 --- a/app/api/tasks/complete/route.ts +++ b/app/api/tasks/complete/route.ts @@ -1,6 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { enforceRouteRateLimit } from "@/lib/rate-limit"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { and, eq } from "drizzle-orm"; +import { getSessionUser } from "@/lib/auth/session"; +import { getDb } from "@/lib/db/client"; +import { reputationEvents } from "@/lib/db/schema"; /** * POST /api/tasks/complete @@ -15,12 +18,12 @@ export async function POST(request: NextRequest) { return rateLimitResponse; } - const supabase = await getServerSupabaseClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); } - const { data: { user } } = await supabase.auth.getUser(); + const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } @@ -37,17 +40,36 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Task not found" }, { status: 404 }); } - const { data: awardedPoints, error: rpcErr } = await supabase.rpc("complete_platform_task", { - p_task_id: taskId, - }); + // Each task can only be claimed once per user. + const [existing] = await db + .select({ id: reputationEvents.id }) + .from(reputationEvents) + .where( + and( + eq(reputationEvents.userId, user.id), + eq(reputationEvents.sourceType, "task_completion"), + eq(reputationEvents.sourceKey, taskId), + ), + ) + .limit(1); - if (rpcErr) { - const message = rpcErr.message || "Failed to complete task."; - const status = /already completed/i.test(message) ? 409 : 500; - return NextResponse.json({ error: message }, { status }); + if (existing) { + return NextResponse.json( + { error: "Task already completed. Each task can only be claimed once." }, + { status: 409 }, + ); } - const pointsAwarded = Number(awardedPoints ?? task.points); + // The reputation_snapshots trigger folds this into the user's score. + await db.insert(reputationEvents).values({ + userId: user.id, + sourceType: "task_completion", + sourceKey: taskId, + pointsDelta: task.points, + reason: `Completed: ${task.title}`, + }); + + const pointsAwarded = task.points; return NextResponse.json({ taskId, diff --git a/app/api/webhooks/sep31/route.ts b/app/api/webhooks/sep31/route.ts index 2721692..5f80257 100644 --- a/app/api/webhooks/sep31/route.ts +++ b/app/api/webhooks/sep31/route.ts @@ -1,5 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { and, eq, sql } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { ledgerTransactions, lendingPools, poolPositions } from "@/lib/db/schema"; import { discoverSep31Anchor, verifyAnchorSignature } from "@/lib/stellar/sep31"; /** @@ -25,19 +27,25 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Invalid webhook payload" }, { status: 400 }); } - const supabase = getServiceRoleClient(); - if (!supabase) { + const db = getDb(); + if (!db) { return NextResponse.json({ error: "Database unavailable" }, { status: 503 }); } // Find the matching pending transaction in the ledger - const { data: ledgerTx, error: dbError } = await supabase - .from("ledger_transactions") - .select("id, user_id, amount, status, metadata") - .eq("metadata->>anchorTxId", anchorTxId) - .maybeSingle(); - - if (dbError || !ledgerTx) { + const [ledgerTx] = await db + .select({ + id: ledgerTransactions.id, + user_id: ledgerTransactions.userId, + amount: ledgerTransactions.amount, + status: ledgerTransactions.status, + metadata: ledgerTransactions.metadata, + }) + .from(ledgerTransactions) + .where(sql`${ledgerTransactions.metadata}->>'anchorTxId' = ${anchorTxId}`) + .limit(1); + + if (!ledgerTx) { return NextResponse.json({ error: "Transaction not found" }, { status: 404 }); } @@ -85,66 +93,46 @@ export async function POST(request: NextRequest) { // ── Handle Payment Lifecycle ────────────────────────────────────────────────── if (status === "completed") { // 1. Check if lender already has an active position in this pool - const { data: existingPosition } = await supabase - .from("pool_positions") - .select("id, principal_amount") - .eq("pool_id", poolId) - .eq("lender_id", ledgerTx.user_id) - .eq("status", "active") - .maybeSingle(); + const [existingPosition] = await db + .select({ id: poolPositions.id }) + .from(poolPositions) + .where( + and( + eq(poolPositions.poolId, poolId), + eq(poolPositions.lenderId, ledgerTx.user_id), + eq(poolPositions.status, "active"), + ), + ) + .limit(1); let positionId = ""; const depositAmount = Number(ledgerTx.amount); if (existingPosition) { // Update existing position amount - const { data: updatedPosition, error: updateError } = await supabase - .from("pool_positions") - .update({ - principal_amount: Number(existingPosition.principal_amount ?? 0) + depositAmount, - }) - .eq("id", existingPosition.id) - .select("id") - .single(); - - if (updateError) throw updateError; + const [updatedPosition] = await db + .update(poolPositions) + .set({ principalAmount: sql`${poolPositions.principalAmount} + ${depositAmount}` }) + .where(eq(poolPositions.id, existingPosition.id)) + .returning({ id: poolPositions.id }); positionId = updatedPosition.id; } else { // Create new active position - const { data: newPosition, error: insertError } = await supabase - .from("pool_positions") - .insert({ - pool_id: poolId, - lender_id: ledgerTx.user_id, - principal_amount: depositAmount, - status: "active", - opened_at: new Date().toISOString(), - }) - .select("id") - .single(); - - if (insertError) throw insertError; + const [newPosition] = await db + .insert(poolPositions) + .values({ poolId, lenderId: ledgerTx.user_id, principalAmount: String(depositAmount), status: "active" }) + .returning({ id: poolPositions.id }); positionId = newPosition.id; } - // 2. Fetch current pool liquidity and update it - const { data: pool, error: poolError } = await supabase - .from("lending_pools") - .select("total_liquidity, available_liquidity") - .eq("id", poolId) - .single(); - - if (poolError) throw poolError; - - const { error: poolUpdateError } = await supabase - .from("lending_pools") - .update({ - total_liquidity: Number(pool.total_liquidity ?? 0) + depositAmount, - available_liquidity: Number(pool.available_liquidity ?? 0) + depositAmount, + // 2. Update pool liquidity (SQL-side increment) + await db + .update(lendingPools) + .set({ + totalLiquidity: sql`${lendingPools.totalLiquidity} + ${depositAmount}`, + availableLiquidity: sql`${lendingPools.availableLiquidity} + ${depositAmount}`, }) - .eq("id", poolId); - - if (poolUpdateError) throw poolUpdateError; + .where(eq(lendingPools.id, poolId)); // 3. Confirm the ledger transaction and associate with the position const updatedMetadata = { @@ -153,16 +141,10 @@ export async function POST(request: NextRequest) { anchorStatus: status, }; - const { error: txConfirmError } = await supabase - .from("ledger_transactions") - .update({ - status: "confirmed", - ref_id: positionId, - metadata: updatedMetadata, - }) - .eq("id", ledgerTx.id); - - if (txConfirmError) throw txConfirmError; + await db + .update(ledgerTransactions) + .set({ status: "confirmed", refId: positionId, metadata: updatedMetadata }) + .where(eq(ledgerTransactions.id, ledgerTx.id)); } else if (status === "error" || status === "refunded") { // Compliance check failed or transaction was refunded @@ -173,15 +155,10 @@ export async function POST(request: NextRequest) { refunded: status === "refunded", }; - const { error: txFailError } = await supabase - .from("ledger_transactions") - .update({ - status: "failed", - metadata: updatedMetadata, - }) - .eq("id", ledgerTx.id); - - if (txFailError) throw txFailError; + await db + .update(ledgerTransactions) + .set({ status: "failed", metadata: updatedMetadata }) + .where(eq(ledgerTransactions.id, ledgerTx.id)); } else { // For intermediate statuses (like pending_stellar, pending_sender, hold), // we update the anchorStatus in metadata to track live progress. @@ -190,12 +167,10 @@ export async function POST(request: NextRequest) { anchorStatus: status, }; - await supabase - .from("ledger_transactions") - .update({ - metadata: updatedMetadata, - }) - .eq("id", ledgerTx.id); + await db + .update(ledgerTransactions) + .set({ metadata: updatedMetadata }) + .where(eq(ledgerTransactions.id, ledgerTx.id)); } return NextResponse.json({ success: true }); diff --git a/app/dashboard/admin/activity/page.tsx b/app/dashboard/admin/activity/page.tsx index 15ce419..b239b15 100644 --- a/app/dashboard/admin/activity/page.tsx +++ b/app/dashboard/admin/activity/page.tsx @@ -6,7 +6,10 @@ import { getAdminDashboardMetrics, presentAdminMetrics, } from "@/lib/dashboard/metrics"; -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { desc } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { ledgerToRow } from "@/lib/db/rows"; +import { ledgerTransactions } from "@/lib/db/schema"; import { buildStellarTxVerificationUrl, extractPossibleTxHash, @@ -26,19 +29,13 @@ function sumByPeriod( export default async function AdminActivityPage() { const { user } = await requireTradeVaultAdmin(); const metrics = await getAdminDashboardMetrics(); - const walletAddress = String(user.user_metadata?.wallet_address ?? "") || null; + const walletAddress = String(user.walletAddress ?? "") || null; const walletConnected = Boolean(walletAddress); - const srClient = getServiceRoleClient(); - const { data: ledgerRows } = srClient - ? await srClient - .from("ledger_transactions") - .select("id, user_id, amount, category, status, created_at, metadata") - .order("created_at", { ascending: false }) - .limit(500) - : { data: [] as Array> }; - - const rows = ledgerRows ?? []; + const db = getDb(); + const rows = db + ? (await db.select().from(ledgerTransactions).orderBy(desc(ledgerTransactions.createdAt)).limit(500)).map(ledgerToRow) + : []; const anchorTime = new Date().getTime(); const baseTime = Number.isFinite(anchorTime) ? anchorTime : 0; @@ -95,7 +92,7 @@ export default async function AdminActivityPage() { heading="Treasury & Platform Activity" description="Track platform-wide transaction throughput, treasury movement, and full chronological on-chain verification." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? "Admin")} + userName={String(user.fullName ?? "Admin")} metrics={presentAdminMetrics(metrics)} links={[...adminNavLinks]} currentPath="/dashboard/admin/activity" diff --git a/app/dashboard/admin/kyc/page.tsx b/app/dashboard/admin/kyc/page.tsx index 114e462..f00364f 100644 --- a/app/dashboard/admin/kyc/page.tsx +++ b/app/dashboard/admin/kyc/page.tsx @@ -11,7 +11,7 @@ import AdminKYCClient from "./kyc-client"; export default async function AdminKYCPage() { const { user } = await requireTradeVaultAdmin(); - const walletAddress = String(user.user_metadata?.wallet_address ?? "") || null; + const walletAddress = String(user.walletAddress ?? "") || null; const walletConnected = Boolean(walletAddress); const metrics = await getAdminDashboardMetrics(); @@ -23,7 +23,7 @@ export default async function AdminKYCPage() { heading="KYC Verification Center" description="Review and verify identity documents for lender/borrower KYC compliance." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? "Admin")} + userName={String(user.fullName ?? "Admin")} metrics={presentAdminMetrics(metrics)} currentPath="/dashboard/admin/kyc" links={[...adminNavLinks]} diff --git a/app/dashboard/admin/loans/page.tsx b/app/dashboard/admin/loans/page.tsx index 92989f3..7b2d126 100644 --- a/app/dashboard/admin/loans/page.tsx +++ b/app/dashboard/admin/loans/page.tsx @@ -6,46 +6,39 @@ import { getAdminDashboardMetrics, presentAdminMetrics, } from "@/lib/dashboard/metrics"; -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { desc, eq } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { loanToRow, repaymentToRow } from "@/lib/db/rows"; +import { ledgerTransactions, loanRepayments, loans as loansTable } from "@/lib/db/schema"; import { buildStellarTxVerificationUrl, extractPossibleTxHash, isLikelyTxHash } from "@/lib/stellar/explorer"; import { formatCurrency } from "@/lib/utils/formatting"; export default async function AdminLoansPage() { const { user } = await requireTradeVaultAdmin(); const metrics = await getAdminDashboardMetrics(); - const walletAddress = String(user.user_metadata?.wallet_address ?? "") || null; + const walletAddress = String(user.walletAddress ?? "") || null; const walletConnected = Boolean(walletAddress); - const srClient = getServiceRoleClient(); - const [loansRes, repaymentsRes, ledgerRepaysRes] = srClient + const db = getDb(); + const [loanRows, repaymentRows, ledgerRepays] = db ? await Promise.all([ - srClient - .from("loans") - .select("id, borrower_id, status, principal_amount, apr_bps, duration_days, due_at") - .order("requested_at", { ascending: false }) - .limit(40), - srClient - .from("loan_repayments") - .select("id, loan_id, payer_id, amount, paid_at, tx_ref") - .order("paid_at", { ascending: false }) - .limit(40), - srClient - .from("ledger_transactions") - .select("ref_id, metadata") - .eq("ref_type", "loan_repay") + db.select().from(loansTable).orderBy(desc(loansTable.requestedAt)).limit(40), + db.select().from(loanRepayments).orderBy(desc(loanRepayments.paidAt)).limit(40), + db + .select({ ref_id: ledgerTransactions.refId, metadata: ledgerTransactions.metadata }) + .from(ledgerTransactions) + .where(eq(ledgerTransactions.refType, "loan_repay")), ]) - : [{ data: [] }, { data: [] }, { data: [] }]; + : [[], [], []]; - const loans = loansRes.data ?? []; - const repayments = repaymentsRes.data ?? []; + const loans = loanRows.map(loanToRow); + const repayments = repaymentRows.map(repaymentToRow); const oldHashesMap: Record = {}; - - if (ledgerRepaysRes?.data) { - for (const r of ledgerRepaysRes.data) { - const extracted = extractPossibleTxHash(r.metadata); - if (extracted) { - oldHashesMap[String(r.ref_id)] = extracted; - } + + for (const r of ledgerRepays) { + const extracted = extractPossibleTxHash(r.metadata); + if (extracted) { + oldHashesMap[String(r.ref_id)] = extracted; } } const sanctionedAmount = loans @@ -59,7 +52,7 @@ export default async function AdminLoansPage() { heading="Loan Operations" description="Monitor loan lifecycle, exposure, and maturity timelines across the platform." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? "Admin")} + userName={String(user.fullName ?? "Admin")} metrics={presentAdminMetrics(metrics)} links={[...adminNavLinks]} currentPath="/dashboard/admin/loans" diff --git a/app/dashboard/admin/page.tsx b/app/dashboard/admin/page.tsx index 1d1aa49..844deaa 100644 --- a/app/dashboard/admin/page.tsx +++ b/app/dashboard/admin/page.tsx @@ -6,7 +6,17 @@ import { getAdminDashboardMetrics, presentAdminMetrics, } from "@/lib/dashboard/metrics"; -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { desc } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { ledgerToRow, loanToRow, poolToRow, profileToRow, repaymentToRow } from "@/lib/db/rows"; +import { + fraudSignals as fraudSignalsTable, + ledgerTransactions, + lendingPools, + loanRepayments, + loans as loansTable, + profiles as profilesTable, +} from "@/lib/db/schema"; import Link from "next/link"; import { formatCurrency } from "@/lib/utils/formatting"; @@ -22,57 +32,43 @@ function sumByPeriod( export default async function AdminDashboardPage() { const { user } = await requireTradeVaultAdmin(); const metrics = await getAdminDashboardMetrics(); - const walletAddress = String(user.user_metadata?.wallet_address ?? "") || null; + const walletAddress = String(user.walletAddress ?? "") || null; const walletConnected = Boolean(walletAddress); - // Use service role client to bypass RLS and view platform-wide aggregates - const srClient = getServiceRoleClient(); + // Platform-wide aggregates for the admin overview + const db = getDb(); - const [profilesRes, loansRes, repaymentsRes, ledgerRes, fraudRes, poolsRes] = srClient + const [profileRows, loanRows, repaymentRows, ledgerRowsRaw, fraudRows, poolRows] = db ? await Promise.all([ - srClient - .from("profiles") - .select("id, role, kyc_status, risk_status, full_name, phone, country_code, created_at") - .order("created_at", { ascending: false }) - .limit(10), - srClient - .from("loans") - .select("id, borrower_id, status, principal_amount, requested_at") - .order("requested_at", { ascending: false }) - .limit(10), - srClient - .from("loan_repayments") - .select("id, payer_id, amount, paid_at, tx_ref") - .order("paid_at", { ascending: false }) - .limit(120), - srClient - .from("ledger_transactions") - .select("id, user_id, amount, category, status, created_at, metadata") - .order("created_at", { ascending: false }) - .limit(400), - srClient - .from("fraud_signals") - .select("id, user_id, signal_type, severity, resolved, created_at") - .order("created_at", { ascending: false }) + db.select().from(profilesTable).orderBy(desc(profilesTable.createdAt)).limit(10), + db.select().from(loansTable).orderBy(desc(loansTable.requestedAt)).limit(10), + db.select().from(loanRepayments).orderBy(desc(loanRepayments.paidAt)).limit(120), + db.select().from(ledgerTransactions).orderBy(desc(ledgerTransactions.createdAt)).limit(400), + db + .select({ + id: fraudSignalsTable.id, + user_id: fraudSignalsTable.userId, + signal_type: fraudSignalsTable.signalType, + severity: fraudSignalsTable.severity, + resolved: fraudSignalsTable.resolved, + created_at: fraudSignalsTable.createdAt, + }) + .from(fraudSignalsTable) + .orderBy(desc(fraudSignalsTable.createdAt)) .limit(120), - srClient - .from("lending_pools") - .select("id, name, status, total_liquidity, apr_bps, created_at") - .order("created_at", { ascending: false }) - .limit(10) + db.select().from(lendingPools).orderBy(desc(lendingPools.createdAt)).limit(10), ]) - : [{ data: [] }, { data: [] }, { data: [] }, { data: [] }, { data: [] }, { data: [] }]; + : [[], [], [], [], [], []]; - const profiles = profilesRes.data ?? []; - const dbLoans = loansRes.data ?? []; - const loans = dbLoans; - const repayments = repaymentsRes.data ?? []; - const ledgerRows = ledgerRes.data ?? []; - const fraudSignals = fraudRes.data ?? []; - const pools = poolsRes.data ?? []; + const profiles = profileRows.map(profileToRow); + const loans = loanRows.map(loanToRow); + const repayments = repaymentRows.map(repaymentToRow); + const ledgerRows = ledgerRowsRaw.map(ledgerToRow); + const fraudSignals = fraudRows.map((r) => ({ ...r, created_at: r.created_at.toISOString() })); + const pools = poolRows.map(poolToRow); const anchorTime = new Date( - String(ledgerRows[0]?.created_at ?? user.last_sign_in_at ?? user.created_at), + String(ledgerRows[0]?.created_at ?? user.lastSignInAt ?? user.createdAt), ).getTime(); const baseTime = Number.isFinite(anchorTime) ? anchorTime : 0; const baseDate = new Date(baseTime); @@ -136,7 +132,7 @@ export default async function AdminDashboardPage() { heading="Control Panel" description="Monitor platform health, credit activity, and security posture across TrustLend operations." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? "Admin")} + userName={String(user.fullName ?? "Admin")} metrics={presentAdminMetrics(metrics)} links={[...adminNavLinks]} currentPath="/dashboard/admin" diff --git a/app/dashboard/admin/pools/page.tsx b/app/dashboard/admin/pools/page.tsx index ac655ac..49d4964 100644 --- a/app/dashboard/admin/pools/page.tsx +++ b/app/dashboard/admin/pools/page.tsx @@ -2,7 +2,7 @@ import { WorkspaceFrame } from "@/components/dashboard/WorkspaceFrame"; import { adminNavLinks } from "@/lib/dashboard/admin-links"; import { requireTradeVaultAdmin } from "@/lib/auth/session"; import { getAdminDashboardMetrics, presentAdminMetrics } from "@/lib/dashboard/metrics"; -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { getDb } from "@/lib/db/client"; import { fetchAdminDashboardPools } from "@/lib/db/pools"; import AdminPoolsClient from "./pools-client"; @@ -18,16 +18,14 @@ import AdminPoolsClient from "./pools-client"; export default async function AdminPoolsPage() { const { user } = await requireTradeVaultAdmin(); const metrics = await getAdminDashboardMetrics(); - const admin = getServiceRoleClient(); + const db = getDb(); - if (!admin) { + if (!db) { throw new Error("Database service unavailable"); } - // Fetch pools and pending loans using optimized function - // Queries execute in parallel for better performance - const { pools: rawPools, pendingLoans: rawLoans } = - await fetchAdminDashboardPools(admin); + // Pools and pending loans are fetched in parallel. + const { pools: rawPools, pendingLoans: rawLoans } = await fetchAdminDashboardPools(db); // Transform to component-friendly format const pools = rawPools.map((p) => ({ @@ -59,7 +57,7 @@ export default async function AdminPoolsPage() { heading="Pool Management" description="Create lending pools, approve borrower loans, and run auto-matching to deploy capital efficiently." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? "Admin")} + userName={String(user.fullName ?? "Admin")} metrics={presentAdminMetrics(metrics)} links={[ ...adminNavLinks, diff --git a/app/dashboard/admin/risk/page.tsx b/app/dashboard/admin/risk/page.tsx index edca7e3..3e6f5e9 100644 --- a/app/dashboard/admin/risk/page.tsx +++ b/app/dashboard/admin/risk/page.tsx @@ -36,7 +36,7 @@ export default async function AdminRiskParametersPage() { heading="Risk Parameters" description="View and securely adjust platform risk limits, collateral factors, jump-rate interest curves, and protocol fees." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? "Admin")} + userName={String(user.fullName ?? "Admin")} metrics={presentAdminMetrics(metrics)} links={[...adminNavLinks]} currentPath="/dashboard/admin/risk" diff --git a/app/dashboard/admin/security/page.tsx b/app/dashboard/admin/security/page.tsx index a80c33e..d1abb88 100644 --- a/app/dashboard/admin/security/page.tsx +++ b/app/dashboard/admin/security/page.tsx @@ -6,38 +6,58 @@ import { getAdminDashboardMetrics, presentAdminMetrics, } from "@/lib/dashboard/metrics"; -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { desc } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { fraudSignals, profiles as profilesTable, riskAssessments } from "@/lib/db/schema"; export default async function AdminSecurityPage() { const { user } = await requireTradeVaultAdmin(); const metrics = await getAdminDashboardMetrics(); - const walletAddress = String(user.user_metadata?.wallet_address ?? "") || null; + const walletAddress = String(user.walletAddress ?? "") || null; const walletConnected = Boolean(walletAddress); - const supabase = getServiceRoleClient(); - const [signalsRes, riskRes, profilesRes] = supabase + const db = getDb(); + const [signalRows, assessmentRows, profileRows] = db ? await Promise.all([ - supabase - .from("fraud_signals") - .select("id, user_id, signal_type, severity, resolved, created_at") - .order("created_at", { ascending: false }) + db + .select({ + id: fraudSignals.id, + user_id: fraudSignals.userId, + signal_type: fraudSignals.signalType, + severity: fraudSignals.severity, + resolved: fraudSignals.resolved, + created_at: fraudSignals.createdAt, + }) + .from(fraudSignals) + .orderBy(desc(fraudSignals.createdAt)) .limit(40), - supabase - .from("risk_assessments") - .select("id, user_id, score, decision, assessed_at") - .order("assessed_at", { ascending: false }) + db + .select({ + id: riskAssessments.id, + user_id: riskAssessments.userId, + score: riskAssessments.score, + decision: riskAssessments.decision, + assessed_at: riskAssessments.assessedAt, + }) + .from(riskAssessments) + .orderBy(desc(riskAssessments.assessedAt)) .limit(40), - supabase - .from("profiles") - .select("id, full_name, kyc_status, risk_status") - .order("created_at", { ascending: false }) + db + .select({ + id: profilesTable.id, + full_name: profilesTable.fullName, + kyc_status: profilesTable.kycStatus, + risk_status: profilesTable.riskStatus, + }) + .from(profilesTable) + .orderBy(desc(profilesTable.createdAt)) .limit(120), ]) - : [{ data: [] as Array> }, { data: [] as Array> }, { data: [] as Array> }]; + : [[], [], []]; - const signals = signalsRes.data ?? []; - const assessments = riskRes.data ?? []; - const profiles = profilesRes.data ?? []; + const signals = signalRows.map((r) => ({ ...r, created_at: r.created_at.toISOString() })); + const assessments = assessmentRows.map((r) => ({ ...r, assessed_at: r.assessed_at.toISOString() })); + const profiles = profileRows; const maliciousIds = new Set( signals @@ -55,7 +75,7 @@ export default async function AdminSecurityPage() { heading="Security Center" description="Investigate fraud signals, manual-review decisions, and suspicious account behavior." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? "Admin")} + userName={String(user.fullName ?? "Admin")} metrics={presentAdminMetrics(metrics)} links={[...adminNavLinks]} currentPath="/dashboard/admin/security" diff --git a/app/dashboard/admin/users/page.tsx b/app/dashboard/admin/users/page.tsx index df74a2f..307d03b 100644 --- a/app/dashboard/admin/users/page.tsx +++ b/app/dashboard/admin/users/page.tsx @@ -6,24 +6,21 @@ import { getAdminDashboardMetrics, presentAdminMetrics, } from "@/lib/dashboard/metrics"; -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { desc } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { profileToRow } from "@/lib/db/rows"; +import { profiles } from "@/lib/db/schema"; export default async function AdminUsersPage() { const { user } = await requireTradeVaultAdmin(); const metrics = await getAdminDashboardMetrics(); - const walletAddress = String(user.user_metadata?.wallet_address ?? "") || null; + const walletAddress = String(user.walletAddress ?? "") || null; const walletConnected = Boolean(walletAddress); - const srClient = getServiceRoleClient(); - const { data: users } = srClient - ? await srClient - .from("profiles") - .select("id, full_name, role, kyc_status, risk_status, created_at") - .order("created_at", { ascending: false }) - .limit(80) - : { data: [] as Array> }; - - const allUsers = users ?? []; + const db = getDb(); + const allUsers = db + ? (await db.select().from(profiles).orderBy(desc(profiles.createdAt)).limit(80)).map(profileToRow) + : []; const borrowers = allUsers.filter((profile) => String(profile.role) === "borrower").length; const lenders = allUsers.filter((profile) => String(profile.role) === "lender").length; const flagged = allUsers.filter((profile) => ["high", "blocked"].includes(String(profile.risk_status))).length; @@ -35,7 +32,7 @@ export default async function AdminUsersPage() { heading="User Governance" description="Review user role distribution, KYC state, and high-risk identities." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? "Admin")} + userName={String(user.fullName ?? "Admin")} metrics={presentAdminMetrics(metrics)} links={[...adminNavLinks]} currentPath="/dashboard/admin/users" diff --git a/app/dashboard/borrower/history/page.tsx b/app/dashboard/borrower/history/page.tsx index d266d27..37e3762 100644 --- a/app/dashboard/borrower/history/page.tsx +++ b/app/dashboard/borrower/history/page.tsx @@ -1,7 +1,9 @@ import { WorkspaceFrame } from "@/components/dashboard/WorkspaceFrame"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { getBorrowerDashboardMetrics, presentBorrowerMetrics } from "@/lib/dashboard/metrics"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { getDb } from "@/lib/db/client"; +import { metaString } from "@/lib/db/metadata"; +import { getBorrowerLoans, getLedgerByRef, getProfile, getRepaymentsForLoans } from "@/lib/db/queries"; import { borrowerNavLinks } from "@/lib/dashboard/borrower-links"; import { ExportCsvButton } from "@/components/dashboard/ExportCsvButton"; import { formatCurrency } from "@/lib/utils/formatting"; @@ -11,74 +13,41 @@ export default async function BorrowerHistoryPage() { const { user } = await requireAuthenticatedUser("borrower"); const metrics = await getBorrowerDashboardMetrics(user.id); - const supabase = await getServerSupabaseClient(); + const db = getDb(); // Fetch initial data for summary stats and first page - const [profileRes, loansRes] = supabase - ? await Promise.all([ - supabase.from("profiles").select("full_name").eq("id", user.id).maybeSingle(), - supabase - .from("loans") - .select("id, status, principal_amount, repaid_amount, apr_bps, duration_days, due_at, created_at") - .eq("borrower_id", user.id) - .order("created_at", { ascending: false }) - .limit(20), - ]) - : [{ data: null }, { data: [] }]; + const [profile, loans] = await Promise.all([getProfile(db, user.id), getBorrowerLoans(db, user.id, 20)]); + const loanIds = loans.map((l) => l.id); - const loans = loansRes.data ?? []; - const loanIds = loans.map((l) => String(l.id)); - - // Fetch Stellar TX hashes for funded loans - const ledgerRes = supabase && loanIds.length > 0 - ? await supabase - .from("ledger_transactions") - .select("ref_id, metadata, created_at, amount") - .eq("ref_type", "loan_fund") - .in("ref_id", loanIds) - : { data: [] }; - - // Fetch request-stage ledger events - const requestLedgerRes = supabase && loanIds.length > 0 - ? await supabase - .from("ledger_transactions") - .select("ref_id, metadata, created_at, amount") - .eq("ref_type", "loan_request") - .in("ref_id", loanIds) - : { data: [] }; + // Ledger events (funding + request stage) and repayments for these loans + const [fundLedger, requestLedger, repayments] = await Promise.all([ + getLedgerByRef(db, "loan_fund", loanIds), + getLedgerByRef(db, "loan_request", loanIds), + getRepaymentsForLoans(db, loanIds, 100), + ]); const loanTxMap: Record = {}; - for (const entry of ledgerRes.data ?? []) { - try { - const meta = JSON.parse(String(entry.metadata ?? "{}")); - if (String(entry.ref_id)) { - loanTxMap[String(entry.ref_id)] = { - hash: String(meta.txHash ?? ""), - amount: Number(entry.amount ?? 0), - date: String(entry.created_at ?? ""), - }; - } - } catch { /* ignore */ } + for (const entry of fundLedger) { + if (!entry.ref_id) continue; + loanTxMap[entry.ref_id] = { + hash: metaString(entry.metadata, "txHash"), + amount: Number(entry.amount ?? 0), + date: entry.created_at, + }; } const requestTxMap: Record = {}; - for (const entry of requestLedgerRes.data ?? []) { + for (const entry of requestLedger) { if (!entry.ref_id) continue; - requestTxMap[String(entry.ref_id)] = { - date: String(entry.created_at ?? ""), - amount: Number(entry.amount ?? 0), - }; + requestTxMap[entry.ref_id] = { date: entry.created_at, amount: Number(entry.amount ?? 0) }; } - // Fetch repayments - const repaymentsRes = supabase && loanIds.length > 0 - ? await supabase - .from("loan_repayments") - .select("id, loan_id, amount, created_at") - .in("loan_id", loanIds) - .order("created_at", { ascending: false }) - .limit(100) - : { data: [] }; + // Repayment ledger rows carry the tx hash (one query instead of one per repayment). + const repayLedger = await getLedgerByRef(db, "loan_repay", repayments.map((r) => r.id)); + const repayHashById: Record = {}; + for (const entry of repayLedger) { + if (entry.ref_id) repayHashById[entry.ref_id] = metaString(entry.metadata, "txHash"); + } // Build initial transaction feed const initialTransactions: Array<{ @@ -127,25 +96,11 @@ export default async function BorrowerHistoryPage() { } // Repayment events - for (const r of repaymentsRes.data ?? []) { - const loan = loans.find((l) => String(l.id) === String(r.loan_id)); + for (const r of repayments) { + const loan = loans.find((l) => l.id === r.loan_id); if (!loan) continue; - let txHash = ""; - try { - if (!supabase) continue; - const { data: repayTx } = await supabase - .from("ledger_transactions") - .select("metadata") - .eq("ref_type", "loan_repay") - .eq("ref_id", String(r.id)) - .maybeSingle(); - - if (repayTx) { - const meta = JSON.parse(String(repayTx.metadata ?? "{}")); - txHash = String(meta.txHash ?? ""); - } - } catch { /* ignore */ } + const txHash = repayHashById[r.id] ?? ""; initialTransactions.push({ id: `repay-${r.id}`, @@ -200,7 +155,7 @@ export default async function BorrowerHistoryPage() { description="Every funding received and repayment made — with on-chain verification links." email={user.email ?? null} userName={String( - user.user_metadata?.full_name ?? profileRes.data?.full_name ?? "" + user.fullName ?? profile?.full_name ?? "" )} metrics={presentBorrowerMetrics(metrics)} currentPath="/dashboard/borrower/history" diff --git a/app/dashboard/borrower/loans/page.tsx b/app/dashboard/borrower/loans/page.tsx index 49bcb60..b452871 100644 --- a/app/dashboard/borrower/loans/page.tsx +++ b/app/dashboard/borrower/loans/page.tsx @@ -3,32 +3,16 @@ import { BorrowerForms } from "@/components/dashboard/BorrowerForms"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { getBorrowerDashboardMetrics, presentBorrowerMetrics } from "@/lib/dashboard/metrics"; import { borrowerNavLinks } from "@/lib/dashboard/borrower-links"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { getDb } from "@/lib/db/client"; +import { getBorrowerLoans, getProfile } from "@/lib/db/queries"; import { getFundingProgress } from "@/lib/loans/funding"; export default async function BorrowerLoansPage() { const { user } = await requireAuthenticatedUser("borrower"); const metrics = await getBorrowerDashboardMetrics(user.id); - const supabase = await getServerSupabaseClient(); - const [loansRes, profileRes] = supabase - ? await Promise.all([ - supabase - .from("loans") - .select("id, status, principal_amount, funded_amount, apr_bps, duration_days, repaid_amount, due_at, created_at") - .eq("borrower_id", user.id) - .order("created_at", { ascending: false }) - .limit(20), - supabase - .from("profiles") - .select("full_name, kyc_status") - .eq("id", user.id) - .maybeSingle(), - ]) - : [{ data: [] }, { data: null }]; - - const loans = loansRes.data ?? []; - const profile = profileRes.data; + const db = getDb(); + const [loans, profile] = await Promise.all([getBorrowerLoans(db, user.id, 20), getProfile(db, user.id)]); // Funding progress drives the status shown to the borrower (Issue #269). // A request is only "funded" once contributions cover the full principal — @@ -59,7 +43,7 @@ export default async function BorrowerLoansPage() { heading="Apply for a Loan" description="Submit a new loan request or make a repayment on your active loan." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? profile?.full_name ?? "")} + userName={String(user.fullName ?? profile?.full_name ?? "")} metrics={presentBorrowerMetrics(metrics)} currentPath="/dashboard/borrower/loans" links={borrowerNavLinks} @@ -83,8 +67,8 @@ export default async function BorrowerLoansPage() { diff --git a/app/dashboard/borrower/page.tsx b/app/dashboard/borrower/page.tsx index d21bb7c..4d29e14 100644 --- a/app/dashboard/borrower/page.tsx +++ b/app/dashboard/borrower/page.tsx @@ -8,7 +8,8 @@ import { getBorrowerDashboardMetrics, presentBorrowerMetrics, } from "@/lib/dashboard/metrics"; -import { getServerSupabaseClient, getServiceRoleClient } from "@/lib/supabase/server"; +import { getDb } from "@/lib/db/client"; +import { getBorrowerLoans, getLedgerByRef, getProfile } from "@/lib/db/queries"; import { buildStellarTxVerificationUrl, extractPossibleTxHash, isLikelyTxHash } from "@/lib/stellar/explorer"; import { BorrowerRepayWidget } from "@/components/dashboard/BorrowerRepayWidget"; import { WithdrawToFiatButton } from "@/components/dashboard/WithdrawToFiatButton"; @@ -43,48 +44,21 @@ function EmptyLoansIllustration() { export default async function BorrowerDashboardPage() { const { user } = await requireAuthenticatedUser("borrower"); - const walletAddress = String(user.user_metadata?.wallet_address ?? "") || null; + const walletAddress = String(user.walletAddress ?? "") || null; const metrics = await getBorrowerDashboardMetrics(user.id); - const supabase = await getServerSupabaseClient(); - const srClient = getServiceRoleClient(); - - const [profileRes, loansRes] = supabase - ? await Promise.all([ - supabase - .from("profiles") - .select("full_name, phone, date_of_birth, country_code, kyc_status, risk_status, government_id_url, kyc_submitted_at") - .eq("id", user.id) - .maybeSingle(), - supabase - .from("loans") - .select("id, status, principal_amount, funded_amount, repaid_amount, apr_bps, duration_days, due_at, created_at, metadata") - .eq("borrower_id", user.id) - .order("created_at", { ascending: false }) - .limit(20), - ]) - : [{ data: null }, { data: [] }]; - - const profile = profileRes.data; - const dbLoans = loansRes.data ?? []; - const loans = dbLoans; + const db = getDb(); + const [profile, loans] = await Promise.all([getProfile(db, user.id), getBorrowerLoans(db, user.id, 20)]); // Stellar TX lookups - const loanIds = loans.map((l) => String(l.id)); - const ledgerRes = srClient && loanIds.length > 0 - ? await srClient - .from("ledger_transactions") - .select("ref_id, metadata") - .eq("ref_type", "loan_fund") - .in("ref_id", loanIds) - : { data: [] }; + const loanIds = loans.map((l) => l.id); + const fundLedger = await getLedgerByRef(db, "loan_fund", loanIds); const loanTxMap: Record = {}; - for (const entry of ledgerRes.data ?? []) { - if (String(entry.ref_id)) { - const extracted = extractPossibleTxHash(entry.metadata); - if (extracted) { - loanTxMap[String(entry.ref_id)] = extracted; - } + for (const entry of fundLedger) { + if (!entry.ref_id) continue; + const extracted = extractPossibleTxHash(entry.metadata); + if (extracted) { + loanTxMap[entry.ref_id] = extracted; } } @@ -110,7 +84,7 @@ export default async function BorrowerDashboardPage() { const hasGovIdSubmission = Boolean(profile?.government_id_url || profile?.kyc_submitted_at || kycStatus === "submitted" || isKycVerified); const verificationItems = [ - { label: "Email Verified", done: Boolean(user.email_confirmed_at) }, + { label: "Wallet Verified", done: Boolean(user.walletAddress) }, { label: "Legal Name Set", done: Boolean(profile?.full_name) }, { label: "Phone Number", done: Boolean(profile?.phone) }, { label: "Date of Birth", done: Boolean(profile?.date_of_birth) }, @@ -160,7 +134,7 @@ export default async function BorrowerDashboardPage() { heading="My Dashboard" description="Your active loans, verification status, and quick actions — all in one place." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? profile?.full_name ?? "")} + userName={String(user.fullName ?? profile?.full_name ?? "")} metrics={presentBorrowerMetrics(metrics)} headerWidget={ l.status === "repaid").length; @@ -148,7 +135,7 @@ export default async function BorrowerProfilePage() { totalBorrowedXlm: totalBorrowed, totalRepaidXlm: totalRepaid, kycVerified: profile?.kyc_status === "verified", - emailVerified: Boolean(user.email_confirmed_at), + emailVerified: Boolean(user.walletAddress), accountAgeDays, }; @@ -156,7 +143,7 @@ export default async function BorrowerProfilePage() { // Compute real profile completion based on actual data const checks = [ - { label: "Email confirmed", done: Boolean(user.email_confirmed_at) }, + { label: "Wallet verified", done: Boolean(user.walletAddress) }, { label: "Full name", done: Boolean(profile?.full_name && String(profile.full_name).trim().length > 1) }, { label: "Phone number", done: Boolean(profile?.phone && String(profile.phone).trim().length > 4) }, { label: "Date of birth", done: Boolean(profile?.date_of_birth) }, @@ -181,7 +168,7 @@ export default async function BorrowerProfilePage() { heading="Profile Settings & Verification" description="Update your personal details and complete KYC milestones to unlock full platform features." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? profile?.full_name ?? "")} + userName={String(user.fullName ?? profile?.full_name ?? "")} metrics={presentBorrowerMetrics(metrics)} currentPath="/dashboard/borrower/profile" profilePath="/dashboard/borrower/profile" @@ -200,7 +187,7 @@ export default async function BorrowerProfilePage() { {/* ── TOP: Borrower Reputation & Credit Score Card ── */}
@@ -476,14 +463,14 @@ export default async function BorrowerProfilePage() {
  • - Email Verified + Wallet Verified @@ -492,18 +479,18 @@ export default async function BorrowerProfilePage() { width: "7px", height: "7px", borderRadius: "50%", - background: user.email_confirmed_at ? "#22cf9d" : "#f59e0b", + background: user.walletAddress ? "#22cf9d" : "#f59e0b", display: "inline-block", }} /> - {user.email_confirmed_at ? "Verified" : "Not verified"} + {user.walletAddress ? "Verified" : "Not verified"}
  • Member Since - {user.created_at - ? new Date(user.created_at).toLocaleDateString("en-US", { + {user.createdAt + ? new Date(user.createdAt).toLocaleDateString("en-US", { month: "long", year: "numeric", }) diff --git a/app/dashboard/borrower/referrals/page.tsx b/app/dashboard/borrower/referrals/page.tsx index 9261c98..a296b39 100644 --- a/app/dashboard/borrower/referrals/page.tsx +++ b/app/dashboard/borrower/referrals/page.tsx @@ -16,7 +16,7 @@ export default async function BorrowerReferralsPage() { heading="Refer a friend" description="Share your invite link and earn a TLND bonus each time someone you invited takes out their first loan." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? "")} + userName={String(user.fullName ?? "")} metrics={[]} links={borrowerNavLinks} currentPath="/dashboard/borrower/referrals" diff --git a/app/dashboard/borrower/repay/page.tsx b/app/dashboard/borrower/repay/page.tsx index 3a1201f..842ceb0 100644 --- a/app/dashboard/borrower/repay/page.tsx +++ b/app/dashboard/borrower/repay/page.tsx @@ -3,7 +3,8 @@ import { BorrowerRepayWidget } from "@/components/dashboard/BorrowerRepayWidget" import { requireAuthenticatedUser } from "@/lib/auth/session"; import { getBorrowerDashboardMetrics, presentBorrowerMetrics } from "@/lib/dashboard/metrics"; import { borrowerNavLinks } from "@/lib/dashboard/borrower-links"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { getDb } from "@/lib/db/client"; +import { getBorrowerLoans, getProfile } from "@/lib/db/queries"; import { Badge } from "@/components/ui/Badge"; import { formatCurrency } from "@/lib/utils/formatting"; import { getFundingProgress } from "@/lib/loans/funding"; @@ -19,25 +20,8 @@ export default async function BorrowerRepayPage({ const { user } = await requireAuthenticatedUser("borrower"); const metrics = await getBorrowerDashboardMetrics(user.id); - const supabase = await getServerSupabaseClient(); - const [loansRes, profileRes] = supabase - ? await Promise.all([ - supabase - .from("loans") - .select("id, status, principal_amount, funded_amount, repaid_amount, apr_bps, duration_days, due_at, created_at") - .eq("borrower_id", user.id) - .order("created_at", { ascending: false }) - .limit(20), - supabase - .from("profiles") - .select("full_name") - .eq("id", user.id) - .maybeSingle(), - ]) - : [{ data: [] }, { data: null }]; - - const loans = loansRes.data ?? []; - const profile = profileRes.data; + const db = getDb(); + const [loans, profile] = await Promise.all([getBorrowerLoans(db, user.id, 20), getProfile(db, user.id)]); // A loan is only repayable once lenders have covered the full principal — // a partially filled request is not yet active (Issue #269). @@ -68,7 +52,7 @@ export default async function BorrowerRepayPage({ heading="Repay Loan" description="Make an early repayment on your active loan to save on interest and boost your Trust Score." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? profile?.full_name ?? "")} + userName={String(user.fullName ?? profile?.full_name ?? "")} metrics={presentBorrowerMetrics(metrics)} currentPath="/dashboard/borrower/repay" links={borrowerNavLinks} diff --git a/app/dashboard/borrower/tasks/page.tsx b/app/dashboard/borrower/tasks/page.tsx index 1557038..41b535f 100644 --- a/app/dashboard/borrower/tasks/page.tsx +++ b/app/dashboard/borrower/tasks/page.tsx @@ -3,33 +3,30 @@ import { TasksBoard } from "@/components/dashboard/TasksBoard"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { getBorrowerDashboardMetrics, presentBorrowerMetrics } from "@/lib/dashboard/metrics"; import { borrowerNavLinks } from "@/lib/dashboard/borrower-links"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { and, eq } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { getProfile } from "@/lib/db/queries"; +import { reputationEvents } from "@/lib/db/schema"; import { getPlatformTasks } from "@/app/api/tasks/complete/route"; export default async function BorrowerTasksPage() { const { user } = await requireAuthenticatedUser("borrower"); const metrics = await getBorrowerDashboardMetrics(user.id); - const supabase = await getServerSupabaseClient(); + const db = getDb(); - const [profileRes, completedEventsRes] = supabase - ? await Promise.all([ - supabase - .from("profiles") - .select("full_name") - .eq("id", user.id) - .maybeSingle(), - // Which tasks has this user already completed? - supabase - .from("reputation_events") - .select("source_key, source_id") - .eq("user_id", user.id) - .eq("source_type", "task_completion"), - ]) - : [{ data: null }, { data: [] }]; + const [profile, completedEvents] = await Promise.all([ + getProfile(db, user.id), + // Which tasks has this user already completed? + db + ? db + .select({ source_key: reputationEvents.sourceKey, source_id: reputationEvents.sourceId }) + .from(reputationEvents) + .where(and(eq(reputationEvents.userId, user.id), eq(reputationEvents.sourceType, "task_completion"))) + : Promise.resolve([]), + ]); - const profile = profileRes.data; const completedTaskIds = new Set( - (completedEventsRes.data ?? []).map((e) => String(e.source_key ?? e.source_id ?? "")) + completedEvents.map((e) => String(e.source_key ?? e.source_id ?? "")) ); const currentScore = metrics.reputationScore; @@ -46,7 +43,7 @@ export default async function BorrowerTasksPage() { heading="Trust Tasks" description="Complete these tasks to build your trust score. Higher score = better loan terms and higher limits." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? profile?.full_name ?? "")} + userName={String(user.fullName ?? profile?.full_name ?? "")} metrics={presentBorrowerMetrics(metrics)} currentPath="/dashboard/borrower/tasks" links={borrowerNavLinks} diff --git a/app/dashboard/lender/history/page.tsx b/app/dashboard/lender/history/page.tsx index 703ad35..250147e 100644 --- a/app/dashboard/lender/history/page.tsx +++ b/app/dashboard/lender/history/page.tsx @@ -1,7 +1,12 @@ import { WorkspaceFrame } from "@/components/dashboard/WorkspaceFrame"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { getLenderDashboardMetrics, presentLenderMetrics } from "@/lib/dashboard/metrics"; -import { getServerSupabaseClient, getServiceRoleClient } from "@/lib/supabase/server"; +import { desc, eq } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { readMetadata } from "@/lib/db/metadata"; +import { getProfile } from "@/lib/db/queries"; +import { ledgerToRow } from "@/lib/db/rows"; +import { ledgerTransactions } from "@/lib/db/schema"; import { lenderNavLinks } from "@/lib/dashboard/lender-links"; import { ExportCsvButton } from "@/components/dashboard/ExportCsvButton"; import { LenderHistoryClient } from "./client"; @@ -9,67 +14,56 @@ import { LenderHistoryClient } from "./client"; export default async function LenderHistoryPage() { const { user } = await requireAuthenticatedUser("lender"); const metrics = await getLenderDashboardMetrics(user.id); - const supabase = await getServerSupabaseClient(); - const srClient = getServiceRoleClient(); + const db = getDb(); // Profile data - const { data: profile } = supabase - ? await supabase.from("profiles").select("full_name").eq("id", user.id).maybeSingle() - : { data: null }; + const profile = await getProfile(db, user.id); // Fetch initial transactions with limit for server-side rendering const PAGE_SIZE = 20; - const userTxsQuery = supabase - ? await supabase - .from("ledger_transactions") - .select("id, category, ref_type, ref_id, amount, currency, status, metadata, created_at") - .eq("user_id", user.id) - .order("created_at", { ascending: false }) - .limit(PAGE_SIZE + 1) - : { data: [] }; - - const { data: userTxs } = userTxsQuery; - - const hasMore = (userTxs?.length ?? 0) > PAGE_SIZE; - const items = userTxs?.slice(0, PAGE_SIZE) ?? []; - - // Fetch incoming repayments - const { data: allRepays } = srClient - ? await srClient - .from("ledger_transactions") - .select("id, category, ref_type, ref_id, amount, currency, status, metadata, created_at") - .eq("ref_type", "loan_repay") - .order("created_at", { ascending: false }) - .limit(200) - : { data: [] }; - - const incomingRepays = (allRepays ?? []).filter((tx) => { - try { - const meta = JSON.parse(String(tx.metadata || "{}")); - return String(meta.lenderUserId) === String(user.id) || String(meta.lenderAddress) === String(user.id); - } catch { return false; } + const [userTxs, allRepays] = db + ? await Promise.all([ + db + .select() + .from(ledgerTransactions) + .where(eq(ledgerTransactions.userId, user.id)) + .orderBy(desc(ledgerTransactions.createdAt)) + .limit(PAGE_SIZE + 1), + // Incoming repayments are written by the borrower; match on metadata. + db + .select() + .from(ledgerTransactions) + .where(eq(ledgerTransactions.refType, "loan_repay")) + .orderBy(desc(ledgerTransactions.createdAt)) + .limit(200), + ]) + : [[], []]; + + const hasMore = userTxs.length > PAGE_SIZE; + const items = userTxs.slice(0, PAGE_SIZE).map(ledgerToRow); + + const incomingRepays = allRepays.map(ledgerToRow).filter((tx) => { + const meta = readMetadata(tx.metadata); + return String(meta.lenderUserId) === user.id || String(meta.lenderAddress) === user.walletAddress; }); // Merge and dedup - const txMap = new Map(); + const txMap = new Map(); for (const t of items) txMap.set(t.id, t); for (const t of incomingRepays) txMap.set(t.id, t); const transactions = Array.from(txMap.values()).sort( - (a, b) => new Date(String(b.created_at)).getTime() - new Date(String(a.created_at)).getTime() + (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() ); // Format transactions const initialTransactions = transactions.map((tx) => { - let txHash = ""; + const meta = readMetadata(tx.metadata); + const txHash = String(meta.txHash ?? ""); let subLabel = ""; - try { - const meta = JSON.parse(String(tx.metadata ?? "{}")); - txHash = String(meta.txHash ?? ""); - if (meta.loanId) subLabel = `Loan #${String(meta.loanId).slice(0, 8)}`; - else if (tx.ref_id) subLabel = `Ref #${String(tx.ref_id).slice(0, 8)}`; - } catch { /* ok */ } + if (meta.loanId) subLabel = `Loan #${String(meta.loanId).slice(0, 8)}`; + else if (tx.ref_id) subLabel = `Ref #${String(tx.ref_id).slice(0, 8)}`; let label = "Transaction"; if (tx.ref_type === "loan_fund") label = "P2P Loan Deployed"; @@ -114,7 +108,7 @@ export default async function LenderHistoryPage() { description="A full chronological record of every investment, pool deposit, and repayment — fully verifiable on-chain." email={user.email ?? null} userName={String( - user.user_metadata?.full_name ?? profile?.full_name ?? "" + user.fullName ?? profile?.full_name ?? "" )} metrics={presentLenderMetrics(metrics)} currentPath="/dashboard/lender/history" diff --git a/app/dashboard/lender/loading.tsx b/app/dashboard/lender/loading.tsx index e2381f9..71ec014 100644 --- a/app/dashboard/lender/loading.tsx +++ b/app/dashboard/lender/loading.tsx @@ -3,7 +3,7 @@ import { WorkspaceFrame } from "@/components/dashboard/WorkspaceFrame"; /** * Next.js renders this file immediately (via React Suspense) while - * the main `LenderHomePage` resolves its Supabase + Stellar fetches. + * the main `LenderHomePage` resolves its database + Stellar fetches. * Prevents the blank/jump screen on initial lender dashboard load. */ export default function LenderDashboardLoading() { diff --git a/app/dashboard/lender/marketplace/page.tsx b/app/dashboard/lender/marketplace/page.tsx index e44aa44..fa4213a 100644 --- a/app/dashboard/lender/marketplace/page.tsx +++ b/app/dashboard/lender/marketplace/page.tsx @@ -11,10 +11,12 @@ import { buildStellarTxVerificationUrl, isLikelyTxHash, } from "@/lib/stellar/explorer"; -import { - getServerSupabaseClient, - getServiceRoleClient, -} from "@/lib/supabase/server"; +import { and, desc, eq } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { getMarketplaceLoans, getProfile } from "@/lib/db/queries"; +import { metaString } from "@/lib/db/metadata"; +import { ledgerToRow } from "@/lib/db/rows"; +import { ledgerTransactions } from "@/lib/db/schema"; import { DEFAULT_SORT, DURATION_FILTER_OPTIONS, @@ -26,21 +28,6 @@ import { type SearchParams = Promise>; -type MarketplaceLoanRow = { - id: string; - principal_amount: number; - /** Total contributed by all lenders so far (Issue #269). */ - funded_amount?: number; - /** Lenders who already hold a slice of this loan. */ - lender_count?: number; - apr_bps: number; - duration_days: number; - borrower_id: string; - borrower_name: string; - borrower_wallet: string; - trust_score: number; -}; - function readSearchParam( params: Record, key: string, @@ -64,95 +51,24 @@ export default async function LenderMarketplacePage({ const { user } = await requireAuthenticatedUser("lender"); const walletAddress = - String(user.user_metadata?.wallet_address ?? "") || null; + String(user.walletAddress ?? "") || null; const metrics = await getLenderDashboardMetrics(user.id); - const supabase = await getServerSupabaseClient(); - const srClient = getServiceRoleClient(); - - const fundedTxsRes = srClient - ? await srClient - .from("ledger_transactions") - .select("id, ref_id, amount, metadata, created_at") - .eq("user_id", user.id) - .eq("ref_type", "loan_fund") - .order("created_at", { ascending: false }) - .limit(20) - : { data: [] }; - - const openLoansRes = srClient - ? await srClient.rpc("get_marketplace_loans") - : { data: null, error: null }; - - let openLoans: MarketplaceLoanRow[] = []; - - if (!openLoansRes.error) { - openLoans = (openLoansRes.data ?? []) as MarketplaceLoanRow[]; - } else if (srClient) { - const fallbackLoansRes = await srClient - .from("loans") - .select( - "id, principal_amount, funded_amount, apr_bps, duration_days, borrower_id", - ) - .in("status", ["requested", "approved"]) - .order( - sort === "term_asc" || sort === "term_desc" - ? "duration_days" - : "apr_bps", - { ascending: sort === "apr_asc" || sort === "term_asc" }, - ); + const db = getDb(); - const fallbackLoans = fallbackLoansRes.data ?? []; - const borrowerIds = Array.from( - new Set(fallbackLoans.map((loan) => String(loan.borrower_id))), - ); + const [fundedTxRows, openLoans] = await Promise.all([ + db + ? db + .select() + .from(ledgerTransactions) + .where(and(eq(ledgerTransactions.userId, user.id), eq(ledgerTransactions.refType, "loan_fund"))) + .orderBy(desc(ledgerTransactions.createdAt)) + .limit(20) + : Promise.resolve([]), + getMarketplaceLoans(db), + ]); - const [profilesRes, snapshotsRes] = - borrowerIds.length > 0 - ? await Promise.all([ - srClient - .from("profiles") - .select("id, full_name, wallet_address") - .in("id", borrowerIds), - srClient - .from("reputation_snapshots") - .select("user_id, score_total") - .in("user_id", borrowerIds), - ]) - : [{ data: [] }, { data: [] }]; - - const profileMap = new Map( - (profilesRes.data ?? []).map((profile) => [String(profile.id), profile]), - ); - const scoreMap = new Map( - (snapshotsRes.data ?? []).map((snapshot) => [ - String(snapshot.user_id), - Number(snapshot.score_total ?? 250), - ]), - ); - - openLoans = fallbackLoans.map((loan) => { - const borrowerId = String(loan.borrower_id); - const profile = profileMap.get(borrowerId); - - return { - id: String(loan.id), - principal_amount: Number(loan.principal_amount ?? 0), - funded_amount: Number(loan.funded_amount ?? 0), - apr_bps: Number(loan.apr_bps ?? 0), - duration_days: Number(loan.duration_days ?? 30), - borrower_id: borrowerId, - borrower_name: - profile?.full_name && String(profile.full_name).trim() !== "" - ? String(profile.full_name) - : `Borrower ${borrowerId.slice(0, 6)}`, - borrower_wallet: String(profile?.wallet_address ?? ""), - trust_score: Number(scoreMap.get(borrowerId) ?? 250), - }; - }); - } - - const fundedTxs = fundedTxsRes.data ?? []; + const fundedTxs = fundedTxRows.map(ledgerToRow); const marketplaceLoans = openLoans .map((loan) => ({ id: String(loan.id), @@ -167,8 +83,7 @@ export default async function LenderMarketplacePage({ ), borrower_wallet: String(loan.borrower_wallet ?? ""), })) - // Drop anything already at 100%. The RPC filters these out server-side, but - // the fallback query cannot compare two columns, so enforce it here too. + // Drop anything already at 100% (the query filters these too; belt and braces). .filter((loan) => !getFundingProgress(loan.principal_amount, loan.funded_amount).isFullyFunded); const visibleMarketplaceLoans = filterMarketplaceLoans(marketplaceLoans, { @@ -178,14 +93,7 @@ export default async function LenderMarketplacePage({ highReputationThreshold: HIGH_REPUTATION_THRESHOLD, }); - const profileRes = supabase - ? await supabase - .from("profiles") - .select("full_name") - .eq("id", user.id) - .maybeSingle() - : { data: null }; - const profile = profileRes.data; + const profile = await getProfile(db, user.id); return ( {fundedTxs.map((tx) => { - let meta: Record = {}; - - try { - meta = JSON.parse(String(tx.metadata ?? "{}")); - } catch { - meta = {}; - } - - const txHash = meta.txHash ?? ""; + const txHash = metaString(tx.metadata, "txHash"); return ( diff --git a/app/dashboard/lender/page.tsx b/app/dashboard/lender/page.tsx index 322fc16..26e5946 100644 --- a/app/dashboard/lender/page.tsx +++ b/app/dashboard/lender/page.tsx @@ -5,10 +5,12 @@ import { getLenderDashboardMetrics, presentLenderMetrics, } from "@/lib/dashboard/metrics"; -import { - getServerSupabaseClient, - getServiceRoleClient, -} from "@/lib/supabase/server"; +import { and, desc, eq, inArray, sql } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { metaString } from "@/lib/db/metadata"; +import { getProfile } from "@/lib/db/queries"; +import { ledgerToRow, loanToRow, positionToRow } from "@/lib/db/rows"; +import { ledgerTransactions, loans as loansTable, poolPositions } from "@/lib/db/schema"; import { formatTokenBalance } from "@/lib/utils/formatting"; import { lenderNavLinks } from "@/lib/dashboard/lender-links"; import Link from "next/link"; @@ -16,79 +18,48 @@ import Link from "next/link"; export default async function LenderHomePage() { const { user } = await requireAuthenticatedUser("lender"); const walletAddress = - String(user.user_metadata?.wallet_address ?? "") || null; + String(user.walletAddress ?? "") || null; const metrics = await getLenderDashboardMetrics(user.id); - const supabase = await getServerSupabaseClient(); - const srClient = getServiceRoleClient(); + const db = getDb(); - const [ - positionsRes, - profileRes, - p2pRes, - openLoanCountRes, - allLoansRes, - repaysRes, - ] = - supabase && srClient - ? await Promise.all([ - supabase - .from("pool_positions") - .select("id, pool_id, status, principal_amount, earned_interest") - .eq("lender_id", user.id) - .order("created_at", { ascending: false }) - .limit(5), - supabase - .from("profiles") - .select("full_name, kyc_status") - .eq("id", user.id) - .maybeSingle(), - supabase - .from("ledger_transactions") - .select("id, ref_id, amount, status, metadata, created_at") - .eq("user_id", user.id) - .eq("ref_type", "loan_fund") - .order("created_at", { ascending: false }) - .limit(20), - supabase - .from("loans") - .select("id", { count: "exact", head: true }) - .in("status", ["requested", "approved"]), - srClient - .from("loans") - .select("id, status, repaid_amount, principal_amount"), - srClient - .from("ledger_transactions") - .select("ref_id, metadata") - .eq("ref_type", "loan_repay"), - ]) - : [ - { data: [] }, - { data: null }, - { data: [] }, - { count: 0 }, - { data: [] }, - { data: [] }, - ]; + const [positionRows, profile, p2pRows, [openLoanCountRow], allLoanRows, repayRows] = db + ? await Promise.all([ + db + .select() + .from(poolPositions) + .where(eq(poolPositions.lenderId, user.id)) + .orderBy(desc(poolPositions.createdAt)) + .limit(5), + getProfile(db, user.id), + db + .select() + .from(ledgerTransactions) + .where(and(eq(ledgerTransactions.userId, user.id), eq(ledgerTransactions.refType, "loan_fund"))) + .orderBy(desc(ledgerTransactions.createdAt)) + .limit(20), + db + .select({ count: sql`count(*)::int` }) + .from(loansTable) + .where(inArray(loansTable.status, ["requested", "approved"])), + db.select().from(loansTable), + db + .select({ ref_id: ledgerTransactions.refId, metadata: ledgerTransactions.metadata }) + .from(ledgerTransactions) + .where(eq(ledgerTransactions.refType, "loan_repay")), + ]) + : [[], null, [], [{ count: 0 }], [], []]; - const positions = positionsRes.data ?? []; - const dbP2pInvestments = p2pRes.data ?? []; - const profile = profileRes.data; - const openLoanCount = openLoanCountRes.count ?? 0; + const positions = positionRows.map(positionToRow); + const p2pInvestments = p2pRows.map(ledgerToRow); + const openLoanCount = openLoanCountRow?.count ?? 0; const isKycVerified = profile?.kyc_status === "verified"; - - const p2pInvestments = dbP2pInvestments; - const allLoansArray = allLoansRes.data ?? []; - const loanMap = Object.fromEntries( - allLoansArray.map((l) => [String(l.id), l]), - ); + const loanMap = Object.fromEntries(allLoanRows.map(loanToRow).map((l) => [l.id, l])); const repayMap: Record = {}; - for (const r of repaysRes.data ?? []) { - try { - const m = JSON.parse(String(r.metadata || "{}")); - if (m.txHash) repayMap[String(r.ref_id)] = m.txHash; - } catch {} + for (const r of repayRows) { + const hash = metaString(r.metadata, "txHash"); + if (hash && r.ref_id) repayMap[r.ref_id] = hash; } const netEarnings = metrics.totalEarnings; @@ -100,7 +71,7 @@ export default async function LenderHomePage() { description="Your lending overview at a glance. Use the navigation to fund loans or manage your pool investments." email={user.email ?? null} userName={String( - user.user_metadata?.full_name ?? profile?.full_name ?? "", + user.fullName ?? profile?.full_name ?? "", )} metrics={presentLenderMetrics(metrics)} headerWidget={ @@ -428,15 +399,11 @@ export default async function LenderHomePage() { {p2pInvestments.map((tx) => { - let fundTxHash = ""; - try { - const meta = JSON.parse(String(tx.metadata || "{}")); - fundTxHash = meta.txHash ?? ""; - } catch {} + const fundTxHash = metaString(tx.metadata, "txHash"); // Find actual loan data const actualLoan = loanMap[String(tx.ref_id)]; - const rawStatus = actualLoan?.status ?? "processing"; + const rawStatus: string = actualLoan?.status ?? "processing"; const repaid = Number(actualLoan?.repaid_amount ?? 0); const profit = Math.max(0, repaid - Number(tx.amount)); diff --git a/app/dashboard/lender/pools/loading.tsx b/app/dashboard/lender/pools/loading.tsx index a9ecced..e7af471 100644 --- a/app/dashboard/lender/pools/loading.tsx +++ b/app/dashboard/lender/pools/loading.tsx @@ -5,7 +5,7 @@ import { WorkspaceFrame } from "@/components/dashboard/WorkspaceFrame"; /** * Next.js automatically renders this file (via React Suspense) while * `LenderPoolsPage` is waiting for its async server-side data fetches - * (Supabase queries + Stellar Horizon balance). + * (database queries + Stellar Horizon balance). * * The result: users see a polished skeleton layout instantly on navigation * instead of a blank/delayed screen. diff --git a/app/dashboard/lender/pools/page.tsx b/app/dashboard/lender/pools/page.tsx index 9d9ee8c..fd528d9 100644 --- a/app/dashboard/lender/pools/page.tsx +++ b/app/dashboard/lender/pools/page.tsx @@ -6,7 +6,12 @@ import { getLenderDashboardMetrics, presentLenderMetrics, } from "@/lib/dashboard/metrics"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { and, asc, desc, eq } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { metaString } from "@/lib/db/metadata"; +import { getProfile } from "@/lib/db/queries"; +import { ledgerToRow, poolToRow, positionToRow } from "@/lib/db/rows"; +import { ledgerTransactions, lendingPools, poolPositions } from "@/lib/db/schema"; import { lenderNavLinks } from "@/lib/dashboard/lender-links"; import { formatTokenBalance, formatCurrency, formatXlmPrecise } from "@/lib/utils/formatting"; import { @@ -18,46 +23,32 @@ import { STELLAR_TESTNET } from "@/lib/stellar/testnet"; export default async function LenderPoolsPage() { const { user } = await requireAuthenticatedUser("lender"); const metrics = await getLenderDashboardMetrics(user.id); - const supabase = await getServerSupabaseClient(); + const db = getDb(); const walletAddress = - String(user.user_metadata?.wallet_address ?? "") || null; + String(user.walletAddress ?? "") || null; - const [poolsRes, positionsRes, profileRes, txHistoryRes] = supabase + const [poolRows, positionRows, profile, txHistoryRows] = db ? await Promise.all([ - supabase - .from("lending_pools") - .select( - "id, name, status, apr_bps, total_liquidity, available_liquidity", - ) - .order("created_at", { ascending: false }) - .limit(8), - supabase - .from("pool_positions") - .select( - "id, pool_id, status, principal_amount, earned_interest, opened_at", - ) - .eq("lender_id", user.id) - .order("opened_at", { ascending: true }), - supabase - .from("profiles") - .select("full_name, kyc_status") - .eq("id", user.id) - .maybeSingle(), - supabase - .from("ledger_transactions") - .select("id, amount, category, metadata, status, created_at") - .eq("user_id", user.id) - .eq("ref_type", "pool_position") - .order("created_at", { ascending: false }) + db.select().from(lendingPools).orderBy(desc(lendingPools.createdAt)).limit(8), + db + .select() + .from(poolPositions) + .where(eq(poolPositions.lenderId, user.id)) + .orderBy(asc(poolPositions.openedAt)), + getProfile(db, user.id), + db + .select() + .from(ledgerTransactions) + .where(and(eq(ledgerTransactions.userId, user.id), eq(ledgerTransactions.refType, "pool_position"))) + .orderBy(desc(ledgerTransactions.createdAt)) .limit(10), ]) - : [{ data: [] }, { data: [] }, { data: null }, { data: [] }]; + : [[], [], null, []]; - const pools = poolsRes.data ?? []; - const positions = positionsRes.data ?? []; - const profile = profileRes.data; - const txHistory = txHistoryRes.data ?? []; + const pools = poolRows.map(poolToRow); + const positions = positionRows.map(positionToRow); + const txHistory = txHistoryRows.map(ledgerToRow); const isKycVerified = profile?.kyc_status === "verified"; const totalDeployed = positions.reduce( @@ -131,7 +122,7 @@ export default async function LenderPoolsPage() { description="Deposit XLM into a lending pool and earn passive APR. The pool auto-matches your capital to open borrower requests." email={user.email ?? null} userName={String( - user.user_metadata?.full_name ?? profile?.full_name ?? "", + user.fullName ?? profile?.full_name ?? "", )} metrics={presentLenderMetrics(metrics)} currentPath="/dashboard/lender/pools" @@ -369,15 +360,6 @@ export default async function LenderPoolsPage() { - {/* ── Available pools – rendered client-side with skeleton loading ── */} - {/* - AvailablePools is a Client Component that: - 1. Starts with isLoading = true and renders - 2. Fetches lending_pools from Supabase browser client - 3. Sets isLoading = false and renders animated pool cards - This eliminates the blank-screen delay caused by the old - server-side blocking table render. - */}

    Available Lending Pools

    {pools.length === 0 ? ( @@ -456,8 +438,8 @@ export default async function LenderPoolsPage() { {/* ── Deposit / Withdraw forms ──────────────────────────── */}
    ({ ...p, available_liquidity: Number(p.available_liquidity) }))} + positions={positions.map((p) => ({ ...p, principal_amount: Number(p.principal_amount) }))} walletBalance={availableWalletBalance} isKycVerified={isKycVerified} /> @@ -572,11 +554,7 @@ export default async function LenderPoolsPage() { {txHistory.map((tx) => { - let txHash = ""; - try { - const meta = JSON.parse(String(tx.metadata || "{}")); - txHash = meta.txHash ?? ""; - } catch {} + const txHash = metaString(tx.metadata, "txHash"); const isDeposit = tx.category === "deposit"; diff --git a/app/dashboard/lender/portfolio/page.tsx b/app/dashboard/lender/portfolio/page.tsx index e494491..2d55dee 100644 --- a/app/dashboard/lender/portfolio/page.tsx +++ b/app/dashboard/lender/portfolio/page.tsx @@ -1,7 +1,12 @@ import { WorkspaceFrame } from "@/components/dashboard/WorkspaceFrame"; import { requireAuthenticatedUser } from "@/lib/auth/session"; import { getLenderDashboardMetrics, presentLenderMetrics } from "@/lib/dashboard/metrics"; -import { getServerSupabaseClient, getServiceRoleClient } from "@/lib/supabase/server"; +import { and, desc, eq } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { readMetadata } from "@/lib/db/metadata"; +import { getProfile } from "@/lib/db/queries"; +import { ledgerToRow, poolToRow, positionToRow } from "@/lib/db/rows"; +import { ledgerTransactions, lendingPools, poolPositions } from "@/lib/db/schema"; import { lenderNavLinks } from "@/lib/dashboard/lender-links"; import { formatCurrency, formatXlmPrecise } from "@/lib/utils/formatting"; import { TaxReportExportButton } from "@/components/dashboard/TaxReportExportButton"; @@ -13,62 +18,42 @@ export default async function LenderPortfolioPage() { const { user } = await requireAuthenticatedUser("lender"); const metrics = await getLenderDashboardMetrics(user.id); - const supabase = await getServerSupabaseClient(); - const srClient = getServiceRoleClient(); + const db = getDb(); - // 1. Fetch Pool Positions, Profiles, and Lending Pools - const [positionsRes, profileRes, poolsRes] = supabase + // 1. Pool positions, profile, lending pools, and both sides of the P2P ledger + const [positionRows, profile, poolRows, p2pFundRows, p2pRepayRows] = db ? await Promise.all([ - supabase - .from("pool_positions") - .select("id, pool_id, status, principal_amount, earned_interest, opened_at, closed_at") - .eq("lender_id", user.id) - .order("opened_at", { ascending: false }) + db + .select() + .from(poolPositions) + .where(eq(poolPositions.lenderId, user.id)) + .orderBy(desc(poolPositions.openedAt)) .limit(20), - supabase - .from("profiles") - .select("full_name") - .eq("id", user.id) - .maybeSingle(), - supabase - .from("lending_pools") - .select("id, name, status, apr_bps, total_liquidity, available_liquidity"), + getProfile(db, user.id), + db.select().from(lendingPools), + db + .select() + .from(ledgerTransactions) + .where(and(eq(ledgerTransactions.userId, user.id), eq(ledgerTransactions.refType, "loan_fund"))), + db.select().from(ledgerTransactions).where(eq(ledgerTransactions.refType, "loan_repay")), ]) - : [{ data: [] }, { data: null }, { data: [] }]; + : [[], null, [], [], []]; - const positions = positionsRes.data ?? []; - const profile = profileRes.data; - const pools = poolsRes.data ?? []; + const positions = positionRows.map(positionToRow); + const pools = poolRows.map(poolToRow); + const p2pFunds = p2pFundRows.map(ledgerToRow); - // 2. Fetch Direct Marketplace Loans for Profit - // P2P Funds - const { data: p2pFunds } = supabase - ? await supabase - .from("ledger_transactions") - .select("amount, ref_id, created_at") - .eq("user_id", user.id) - .eq("ref_type", "loan_fund") - : { data: [] }; - - const { data: p2pRepays } = srClient - ? await srClient - .from("ledger_transactions") - .select("amount, metadata, ref_id, created_at") - .eq("ref_type", "loan_repay") - : { data: [] }; - - const lenderRepays = (p2pRepays ?? []).filter(tx => { - try { - const meta = JSON.parse(String(tx.metadata || "{}")); - return String(meta.lenderUserId) === String(user.id) || String(meta.lenderAddress) === String(user.id); - } catch { return false; } + // Repayment rows are written by the borrower; the lender is identified from metadata. + const lenderRepays = p2pRepayRows.map(ledgerToRow).filter((tx) => { + const meta = readMetadata(tx.metadata); + return String(meta.lenderUserId) === user.id || String(meta.lenderAddress) === user.walletAddress; }); // Calculate comprehensive yield analytics & pool breakdown (Issue #256) const yieldAnalytics = calculateLenderYieldAnalytics({ positions, pools, - p2pFunds: p2pFunds ?? [], + p2pFunds, p2pRepays: lenderRepays, }); @@ -94,7 +79,7 @@ export default async function LenderPortfolioPage() { heading="Portfolio & Yield Analytics" description="Track historical APY yield, projected future returns, and earnings breakdown across lending pools." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? profile?.full_name ?? "")} + userName={String(user.fullName ?? profile?.full_name ?? "")} metrics={presentLenderMetrics(metrics)} currentPath="/dashboard/lender/portfolio" links={lenderNavLinks} diff --git a/app/dashboard/lender/profile/page.tsx b/app/dashboard/lender/profile/page.tsx index 348649b..8c2bf97 100644 --- a/app/dashboard/lender/profile/page.tsx +++ b/app/dashboard/lender/profile/page.tsx @@ -7,7 +7,8 @@ import { presentLenderMetrics, } from "@/lib/dashboard/metrics"; import { lenderNavLinks } from "@/lib/dashboard/lender-links"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { getDb } from "@/lib/db/client"; +import { getProfile } from "@/lib/db/queries"; const KYC_CONFIG: Record< string, @@ -23,14 +24,7 @@ export default async function LenderProfilePage() { const { user } = await requireAuthenticatedUser("lender"); const metrics = await getLenderDashboardMetrics(user.id); - const supabase = await getServerSupabaseClient(); - const { data: profile } = supabase - ? await supabase - .from("profiles") - .select("full_name, phone, date_of_birth, role, kyc_status, risk_status, government_id_url, kyc_submitted_at, kyc_provider_id") - .eq("id", user.id) - .maybeSingle() - : { data: null as Record | null }; + const profile = await getProfile(getDb(), user.id); const kycStatusKey = String(profile?.kyc_status ?? "pending") as keyof typeof KYC_CONFIG; const kycInfo = KYC_CONFIG[kycStatusKey] ?? KYC_CONFIG.pending; @@ -42,7 +36,7 @@ export default async function LenderProfilePage() { heading="Profile Settings & Security" description="Update your personal details and complete required compliance checks to manage lending pools." email={user.email ?? null} - userName={String(user.user_metadata?.full_name ?? profile?.full_name ?? "")} + userName={String(user.fullName ?? profile?.full_name ?? "")} metrics={presentLenderMetrics(metrics)} currentPath="/dashboard/lender/profile" links={lenderNavLinks} @@ -145,16 +139,16 @@ export default async function LenderProfilePage() {
  • - Email Verified - - - {user.email_confirmed_at ? "Verified" : "Not verified"} + Wallet Verified + + + {user.walletAddress ? "Verified" : "Not verified"}
  • Member Since - {user.created_at ? new Date(user.created_at).toLocaleDateString("en-US", { month: "long", year: "numeric" }) : "—"} + {user.createdAt ? new Date(user.createdAt).toLocaleDateString("en-US", { month: "long", year: "numeric" }) : "—"}
  • diff --git a/app/dashboard/lender/risk/page.tsx b/app/dashboard/lender/risk/page.tsx index c53ad1f..1fc50e6 100644 --- a/app/dashboard/lender/risk/page.tsx +++ b/app/dashboard/lender/risk/page.tsx @@ -5,30 +5,22 @@ import { presentLenderMetrics, } from "@/lib/dashboard/metrics"; import { lenderNavLinks } from "@/lib/dashboard/lender-links"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { asc } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { getProfile } from "@/lib/db/queries"; +import { loanToRow } from "@/lib/db/rows"; +import { loans as loansTable } from "@/lib/db/schema"; export default async function LenderRiskPage() { const { user } = await requireAuthenticatedUser("lender"); const metrics = await getLenderDashboardMetrics(user.id); - const supabase = await getServerSupabaseClient(); - const [loansRes, profileRes] = supabase - ? await Promise.all([ - supabase - .from("loans") - .select("id, status, principal_amount, due_at") - .order("due_at", { ascending: true }) - .limit(12), - supabase - .from("profiles") - .select("full_name") - .eq("id", user.id) - .maybeSingle(), - ]) - : [{ data: [] }, { data: null }]; - - const loans = loansRes.data ?? []; - const profile = profileRes.data; + const db = getDb(); + const [loanRows, profile] = await Promise.all([ + db ? db.select().from(loansTable).orderBy(asc(loansTable.dueAt)).limit(12) : Promise.resolve([]), + getProfile(db, user.id), + ]); + const loans = loanRows.map(loanToRow); return ( diff --git a/commitlint.config.ts b/commitlint.config.ts index 8f04d9b..cf5660d 100644 --- a/commitlint.config.ts +++ b/commitlint.config.ts @@ -4,7 +4,7 @@ import type { UserConfig } from "@commitlint/types"; * commitlint configuration for TrustLend. * * Extends `@commitlint/config-conventional` with additional scopes and types - * relevant to the project's stack (Soroban, Stellar, Next.js, Supabase). + * relevant to the project's stack (Soroban, Stellar, Next.js, Neon). * * Conventional commit format: * (): @@ -64,7 +64,8 @@ const Configuration: UserConfig = { "api", "ci", "db", - "supabase", + "neon", + "drizzle", "stellar", "soroban", "docs", diff --git a/components/dashboard/ProfileSettingsForm.tsx b/components/dashboard/ProfileSettingsForm.tsx index 9bdfdb5..d5639c9 100644 --- a/components/dashboard/ProfileSettingsForm.tsx +++ b/components/dashboard/ProfileSettingsForm.tsx @@ -97,12 +97,10 @@ export function ProfileSettingsForm({ const handleLogout = async () => { setSigningOut(true); try { - const { getBrowserSupabaseClient } = await import("@/lib/supabase/client"); - const supabase = getBrowserSupabaseClient(); - if (supabase) { - await supabase.auth.signOut(); - } + const { signOut } = await import("@/lib/auth/siws-client"); + await signOut(); router.push("/auth"); + router.refresh(); } catch (err) { console.error("Logout failed:", err); setSigningOut(false); diff --git a/components/dashboard/WalletCard.tsx b/components/dashboard/WalletCard.tsx index a7f6626..77cac36 100644 --- a/components/dashboard/WalletCard.tsx +++ b/components/dashboard/WalletCard.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useMemo, useState } from "react"; -import { getBrowserSupabaseClient } from "@/lib/supabase/client"; +import { updateWalletAddress } from "@/app/actions/update-profile"; import { formatCurrency } from "@/lib/utils/formatting"; import { STELLAR_TESTNET } from "@/lib/stellar/testnet"; import { @@ -80,31 +80,9 @@ export function WalletCard({ nextAddress: string | null, provider: StellarWalletProvider | null, ) => { - const supabase = getBrowserSupabaseClient(); - if (!supabase) return; - - const { data } = await supabase.auth.getSession(); - const session = data.session; - if (!session) return; - - const nextMetadata = { - ...session.user.user_metadata, - wallet_address: nextAddress, - wallet_network: nextAddress ? "stellar-testnet" : null, - wallet_provider: provider, - }; - const { error: authErr } = await supabase.auth.updateUser({ - data: nextMetadata, - }); - if (authErr) throw new Error(authErr.message); - - const { error: profileErr } = await supabase - .from("profiles") - .update({ wallet_address: nextAddress }) - .eq("id", session.user.id); - - if (profileErr) { - console.warn("profiles wallet_address sync failed:", profileErr.message); + const result = await updateWalletAddress(nextAddress); + if (!result.success) { + console.warn("profiles wallet_address sync failed:", result.error); } if (nextAddress) { diff --git a/contracts/governance/test_snapshots/test/test_double_vote_panics.1.json b/contracts/governance/test_snapshots/test/test_double_vote_panics.1.json index 1e7181a..17dc4ba 100644 --- a/contracts/governance/test_snapshots/test/test_double_vote_panics.1.json +++ b/contracts/governance/test_snapshots/test/test_double_vote_panics.1.json @@ -881,18 +881,18 @@ }, { "key": { - "symbol": "freeze_reason" + "symbol": "flags" }, "val": { - "string": "" + "u32": 2 } }, { "key": { - "symbol": "is_frozen" + "symbol": "freeze_reason" }, "val": { - "bool": false + "string": "" } }, { diff --git a/contracts/governance/test_snapshots/test/test_execute_requires_passed.1.json b/contracts/governance/test_snapshots/test/test_execute_requires_passed.1.json index 4a9b75d..7af17d1 100644 --- a/contracts/governance/test_snapshots/test/test_execute_requires_passed.1.json +++ b/contracts/governance/test_snapshots/test/test_execute_requires_passed.1.json @@ -856,18 +856,18 @@ }, { "key": { - "symbol": "freeze_reason" + "symbol": "flags" }, "val": { - "string": "" + "u32": 2 } }, { "key": { - "symbol": "is_frozen" + "symbol": "freeze_reason" }, "val": { - "bool": false + "string": "" } }, { diff --git a/contracts/governance/test_snapshots/test/test_full_fee_change_flow.1.json b/contracts/governance/test_snapshots/test/test_full_fee_change_flow.1.json index 42d531b..78db879 100644 --- a/contracts/governance/test_snapshots/test/test_full_fee_change_flow.1.json +++ b/contracts/governance/test_snapshots/test/test_full_fee_change_flow.1.json @@ -1055,18 +1055,18 @@ }, { "key": { - "symbol": "freeze_reason" + "symbol": "flags" }, "val": { - "string": "" + "u32": 2 } }, { "key": { - "symbol": "is_frozen" + "symbol": "freeze_reason" }, "val": { - "bool": false + "string": "" } }, { @@ -1194,18 +1194,18 @@ }, { "key": { - "symbol": "freeze_reason" + "symbol": "flags" }, "val": { - "string": "" + "u32": 2 } }, { "key": { - "symbol": "is_frozen" + "symbol": "freeze_reason" }, "val": { - "bool": false + "string": "" } }, { diff --git a/contracts/governance/test_snapshots/test/test_propose_above_cap_rejected.1.json b/contracts/governance/test_snapshots/test/test_propose_above_cap_rejected.1.json index 6fe328e..ded68f8 100644 --- a/contracts/governance/test_snapshots/test/test_propose_above_cap_rejected.1.json +++ b/contracts/governance/test_snapshots/test/test_propose_above_cap_rejected.1.json @@ -834,18 +834,18 @@ }, { "key": { - "symbol": "freeze_reason" + "symbol": "flags" }, "val": { - "string": "" + "u32": 2 } }, { "key": { - "symbol": "is_frozen" + "symbol": "freeze_reason" }, "val": { - "bool": false + "string": "" } }, { diff --git a/contracts/governance/test_snapshots/test/test_rejected_when_against_wins.1.json b/contracts/governance/test_snapshots/test/test_rejected_when_against_wins.1.json index 6ac2380..7236bf1 100644 --- a/contracts/governance/test_snapshots/test/test_rejected_when_against_wins.1.json +++ b/contracts/governance/test_snapshots/test/test_rejected_when_against_wins.1.json @@ -1050,18 +1050,18 @@ }, { "key": { - "symbol": "freeze_reason" + "symbol": "flags" }, "val": { - "string": "" + "u32": 2 } }, { "key": { - "symbol": "is_frozen" + "symbol": "freeze_reason" }, "val": { - "bool": false + "string": "" } }, { @@ -1189,18 +1189,18 @@ }, { "key": { - "symbol": "freeze_reason" + "symbol": "flags" }, "val": { - "string": "" + "u32": 2 } }, { "key": { - "symbol": "is_frozen" + "symbol": "freeze_reason" }, "val": { - "bool": false + "string": "" } }, { diff --git a/contracts/governance/test_snapshots/test/test_rejected_when_quorum_not_met.1.json b/contracts/governance/test_snapshots/test/test_rejected_when_quorum_not_met.1.json index edaf9bd..52bf5d9 100644 --- a/contracts/governance/test_snapshots/test/test_rejected_when_quorum_not_met.1.json +++ b/contracts/governance/test_snapshots/test/test_rejected_when_quorum_not_met.1.json @@ -323,18 +323,18 @@ }, { "key": { - "symbol": "freeze_reason" + "symbol": "flags" }, "val": { - "string": "" + "u32": 2 } }, { "key": { - "symbol": "is_frozen" + "symbol": "freeze_reason" }, "val": { - "bool": false + "string": "" } }, { diff --git a/contracts/governance/test_snapshots/test/test_vote_after_period_panics.1.json b/contracts/governance/test_snapshots/test/test_vote_after_period_panics.1.json index 0e022df..9f5cca9 100644 --- a/contracts/governance/test_snapshots/test/test_vote_after_period_panics.1.json +++ b/contracts/governance/test_snapshots/test/test_vote_after_period_panics.1.json @@ -856,18 +856,18 @@ }, { "key": { - "symbol": "freeze_reason" + "symbol": "flags" }, "val": { - "string": "" + "u32": 2 } }, { "key": { - "symbol": "is_frozen" + "symbol": "freeze_reason" }, "val": { - "bool": false + "string": "" } }, { diff --git a/contracts/governance/test_snapshots/test/test_vote_without_power_panics.1.json b/contracts/governance/test_snapshots/test/test_vote_without_power_panics.1.json index 37d6f7a..7188a75 100644 --- a/contracts/governance/test_snapshots/test/test_vote_without_power_panics.1.json +++ b/contracts/governance/test_snapshots/test/test_vote_without_power_panics.1.json @@ -856,18 +856,18 @@ }, { "key": { - "symbol": "freeze_reason" + "symbol": "flags" }, "val": { - "string": "" + "u32": 2 } }, { "key": { - "symbol": "is_frozen" + "symbol": "freeze_reason" }, "val": { - "bool": false + "string": "" } }, { diff --git a/contracts/usdc_lending_pool/src/lib.rs b/contracts/usdc_lending_pool/src/lib.rs index 821f919..8b4c7d8 100644 --- a/contracts/usdc_lending_pool/src/lib.rs +++ b/contracts/usdc_lending_pool/src/lib.rs @@ -140,10 +140,11 @@ impl UsdcLendingPool { panic!("amount must be > 0"); } - // Transfer USDC from depositor → pool contract. + // Transfer USDC from depositor → pool contract. `depositor.require_auth()` + // above covers the token transfer, so no prior allowance is needed. let usdc = Self::usdc_client(&env); let contract_addr = env.current_contract_address(); - usdc.transfer_from(&contract_addr, &depositor, &contract_addr, &amount); + usdc.transfer(&depositor, &contract_addr, &amount); let current_ledger = env.ledger().sequence(); @@ -351,7 +352,7 @@ impl UsdcLendingPool { // Use u128 intermediary to avoid i128 overflow on large principals. let numerator = (principal as u128) .saturating_mul(annual_yield_bps as u128) - .saturating_mul(ledgers_elapsed); + .saturating_mul(ledgers_elapsed as u128); let denominator = MAX_BPS.saturating_mul(LEDGERS_PER_YEAR); (numerator / denominator as u128) as i128 } @@ -367,7 +368,7 @@ impl UsdcLendingPool { } /// Build a typed SEP-41 token client for the USDC contract. - fn usdc_client(env: &Env) -> token::TokenClient { + fn usdc_client(env: &Env) -> token::TokenClient<'_> { let addr: Address = env .storage() .instance() diff --git a/contracts/usdc_lending_pool/src/test.rs b/contracts/usdc_lending_pool/src/test.rs index 0902850..c89f169 100644 --- a/contracts/usdc_lending_pool/src/test.rs +++ b/contracts/usdc_lending_pool/src/test.rs @@ -12,7 +12,7 @@ use soroban_sdk::{ // ─── Test helpers ───────────────────────────────────────────────────────────── /// Deploy a native Stellar asset (acts as USDC in tests) and return its address. -fn create_token(env: &Env, admin: &Address) -> (Address, StellarAssetClient, TokenClient) { +fn create_token<'a>(env: &'a Env, admin: &Address) -> (Address, StellarAssetClient<'a>, TokenClient<'a>) { let token_id = env.register_stellar_asset_contract_v2(admin.clone()); let addr = token_id.address(); let asset_client = StellarAssetClient::new(env, &addr); @@ -25,6 +25,13 @@ fn create_token(env: &Env, admin: &Address) -> (Address, StellarAssetClient, Tok fn setup() -> (Env, Address, Address, Address, Address) { let env = Env::default(); env.mock_all_auths(); + // Yield tests fast-forward the ledger by up to a full year (6.3M ledgers). + // Raise the entry TTLs so contract storage is not archived along the way. + env.ledger().with_mut(|li| { + li.min_temp_entry_ttl = 100_000_000; + li.min_persistent_entry_ttl = 100_000_000; + li.max_entry_ttl = 100_000_000; + }); let admin = Address::generate(&env); let user = Address::generate(&env); @@ -131,7 +138,7 @@ fn test_deposit_rejects_negative_amount() { #[test] fn test_deposit_accumulates_on_second_deposit() { - let (env, pool_id, _admin, user, usdc_addr) = setup(); + let (env, pool_id, _admin, user, _usdc_addr) = setup(); let client = UsdcLendingPoolClient::new(&env, &pool_id); // First deposit @@ -319,10 +326,9 @@ fn test_pause_prevents_deposit() { client.pause(&admin); assert!(client.is_paused()); - // deposit should panic - let result = std::panic::catch_unwind(|| { - client.deposit(&user, &100_000_000_i128); - }); + // deposit must fail while paused (try_ variant surfaces the panic as Err + // without needing catch_unwind, which the Env handle does not support) + let result = client.try_deposit(&user, &100_000_000_i128); assert!(result.is_err(), "deposit should be blocked when paused"); } diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_deposit_accumulates_on_second_deposit.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_deposit_accumulates_on_second_deposit.1.json new file mode 100644 index 0000000..82277f6 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_deposit_accumulates_on_second_deposit.1.json @@ -0,0 +1,849 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 50000000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 50000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 6307200, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 106307199 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "deposit_ledger" + }, + "val": { + "u32": 6307200 + } + }, + { + "key": { + "symbol": "principal" + }, + "val": { + "i128": { + "hi": 0, + "lo": 155000000 + } + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 150000000 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 850000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 650000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_deposit_records_principal_and_updates_pool.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_deposit_records_principal_and_updates_pool.1.json new file mode 100644 index 0000000..3250cdf --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_deposit_records_principal_and_updates_pool.1.json @@ -0,0 +1,767 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "deposit_ledger" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "principal" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 900000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 600000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_deposit_rejects_negative_amount.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_deposit_rejects_negative_amount.1.json new file mode 100644 index 0000000..a155904 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_deposit_rejects_negative_amount.1.json @@ -0,0 +1,619 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_deposit_rejects_zero_amount.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_deposit_rejects_zero_amount.1.json new file mode 100644 index 0000000..a155904 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_deposit_rejects_zero_amount.1.json @@ -0,0 +1,619 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_full_year_yield_approximately_correct.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_full_year_yield_approximately_correct.1.json new file mode 100644 index 0000000..11b7179 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_full_year_yield_approximately_correct.1.json @@ -0,0 +1,766 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 6307200, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "deposit_ledger" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "principal" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 900000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 600000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_initialize_rejects_double_init.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_initialize_rejects_double_init.1.json new file mode 100644 index 0000000..a155904 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_initialize_rejects_double_init.1.json @@ -0,0 +1,619 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_initialize_rejects_yield_over_max.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_initialize_rejects_yield_over_max.1.json new file mode 100644 index 0000000..a677e02 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_initialize_rejects_yield_over_max.1.json @@ -0,0 +1,268 @@ +{ + "generators": { + "address": 3, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + { + "function": { + "contract_fn": { + "contract_address": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000002" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 120960 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_initialize_rejects_zero_yield.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_initialize_rejects_zero_yield.1.json new file mode 100644 index 0000000..a677e02 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_initialize_rejects_zero_yield.1.json @@ -0,0 +1,268 @@ +{ + "generators": { + "address": 3, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + { + "function": { + "contract_fn": { + "contract_address": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000002" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 120960 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_initialize_stores_pool_state.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_initialize_stores_pool_state.1.json new file mode 100644 index 0000000..a155904 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_initialize_stores_pool_state.1.json @@ -0,0 +1,619 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_multiple_depositors_tracked_independently.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_multiple_depositors_tracked_independently.1.json new file mode 100644 index 0000000..2cd3633 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_multiple_depositors_tracked_independently.1.json @@ -0,0 +1,1046 @@ +{ + "generators": { + "address": 5, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "i128": { + "hi": 0, + "lo": 200000000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 200000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 100, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "deposit_ledger" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "principal" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "deposit_ledger" + }, + "val": { + "u32": 100 + } + }, + { + "key": { + "symbol": "principal" + }, + "val": { + "i128": { + "hi": 0, + "lo": 200000000 + } + } + } + ] + } + } + }, + "ext": "v0" + }, + 100000099 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 300000000 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": 4270020994084947596 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": 4270020994084947596 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 100000099 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 900000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 800000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 300000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_pause_by_non_admin_panics.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_pause_by_non_admin_panics.1.json new file mode 100644 index 0000000..a155904 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_pause_by_non_admin_panics.1.json @@ -0,0 +1,619 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_pause_prevents_deposit.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_pause_prevents_deposit.1.json new file mode 100644 index 0000000..d6d77ae --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_pause_prevents_deposit.1.json @@ -0,0 +1,672 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "pause", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": true + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_set_yield_rate_by_non_admin_panics.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_set_yield_rate_by_non_admin_panics.1.json new file mode 100644 index 0000000..a155904 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_set_yield_rate_by_non_admin_panics.1.json @@ -0,0 +1,619 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_set_yield_rate_rejects_zero.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_set_yield_rate_rejects_zero.1.json new file mode 100644 index 0000000..a155904 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_set_yield_rate_rejects_zero.1.json @@ -0,0 +1,619 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_set_yield_rate_updates_pool_state.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_set_yield_rate_updates_pool_state.1.json new file mode 100644 index 0000000..305d070 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_set_yield_rate_updates_pool_state.1.json @@ -0,0 +1,674 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "set_yield_rate", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "u32": 1000 + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 1000 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_unpause_restores_deposits.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_unpause_restores_deposits.1.json new file mode 100644 index 0000000..2fde57e --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_unpause_restores_deposits.1.json @@ -0,0 +1,871 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "pause", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "unpause", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4270020994084947596 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4270020994084947596 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "deposit_ledger" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "principal" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 900000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 600000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_withdraw_clears_deposit_record.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_withdraw_clears_deposit_record.1.json new file mode 100644 index 0000000..139fc90 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_withdraw_clears_deposit_record.1.json @@ -0,0 +1,753 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "withdraw", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_withdraw_returns_principal_plus_yield.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_withdraw_returns_principal_plus_yield.1.json new file mode 100644 index 0000000..cd1a5cd --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_withdraw_returns_principal_plus_yield.1.json @@ -0,0 +1,755 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "withdraw", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 3153600, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 103153599 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 102500000 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1002500000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 497500000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_withdraw_updates_pool_total_withdrawn.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_withdraw_updates_pool_total_withdrawn.1.json new file mode 100644 index 0000000..401a56a --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_withdraw_updates_pool_total_withdrawn.1.json @@ -0,0 +1,754 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 50000000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 50000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "withdraw", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 100, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 100000099 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 50000000 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 50000039 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000039 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 499999961 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_withdraw_without_deposit_panics.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_withdraw_without_deposit_panics.1.json new file mode 100644 index 0000000..a155904 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_withdraw_without_deposit_panics.1.json @@ -0,0 +1,619 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_yield_increases_with_ledger_advancement.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_yield_increases_with_ledger_advancement.1.json new file mode 100644 index 0000000..45d524e --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_yield_increases_with_ledger_advancement.1.json @@ -0,0 +1,767 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 2000000, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "deposit_ledger" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "principal" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 900000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 600000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/usdc_lending_pool/test_snapshots/test/test_zero_yield_when_same_ledger.1.json b/contracts/usdc_lending_pool/test_snapshots/test/test_zero_yield_when_same_ledger.1.json new file mode 100644 index 0000000..b4a3304 --- /dev/null +++ b/contracts/usdc_lending_pool/test_snapshots/test/test_zero_yield_when_same_ledger.1.json @@ -0,0 +1,766 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [ + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "transfer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 100000000, + "min_temp_entry_ttl": 100000000, + "max_entry_ttl": 100000000, + "ledger_entries": [ + [ + { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": 0, + "seq_num": 0, + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + null + ] + ], + [ + { + "contract_data": { + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Deposit" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "deposit_ledger" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "principal" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "IsPaused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Pool" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "annual_yield_bps" + }, + "val": { + "u32": 500 + } + }, + { + "key": { + "symbol": "depositor_count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "total_deposited" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100000000 + } + } + }, + { + "key": { + "symbol": "total_withdrawn" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "UsdcToken" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 900000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 600000000 + } + } + }, + { + "key": { + "symbol": "authorized" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "clawback" + }, + "val": { + "bool": false + } + } + ] + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_data": { + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 99999999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 99999999 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/docs/auth-siws.md b/docs/auth-siws.md index 0950564..edd8b47 100644 --- a/docs/auth-siws.md +++ b/docs/auth-siws.md @@ -2,10 +2,10 @@ > Implements issue **#69 — [Backend/Integration] Implement Web3 authentication via SIWS** -Adds "Sign in with Stellar" as a Web3-native login option alongside -password/email and Google — users authenticate by **signing a challenge with -their wallet** (Freighter) instead of typing a password, and the backend turns -that proof into a normal Supabase session. +"Sign in with Stellar" is the only login method: users authenticate by +**signing a challenge with their wallet** (Freighter, xBull, Albedo, or a +WalletConnect mobile wallet) instead of typing a password, and the backend turns +that proof into a signed session cookie. --- @@ -17,31 +17,29 @@ challenge-response transaction — it's the same mechanism the SEP-24 fiat-ramp issuing the challenge and the **relying party** consuming the proof: ``` -Client Backend (/api/auth/siws/*) Supabase - │ connect wallet (Freighter) │ │ +Client Backend (/api/auth/siws/*) Postgres (Neon) + │ connect wallet │ │ │────────────────────────────────────►│ │ │ POST /challenge { address } │ │ │─────────────────────────────────────► buildChallenge(address) │ │ │ WebAuth.buildChallengeTx() │ │ ◄──────────── { transaction, networkPassphrase } │ - │ sign challenge tx (Freighter) │ │ - │ POST /verify { address, signedTxXdr} │ + │ sign challenge tx (wallet) │ │ + │ POST /verify { address, signedTxXdr, role? } │ │─────────────────────────────────────► verifyChallenge() │ │ │ WebAuth.readChallengeTx() │ │ │ WebAuth.verifyChallengeTxSigners() - │ │ issueSessionForWallet() ────► admin.createUser / signInWithPassword - │ ◄──────── { access_token, refresh_token, isNewUser } ◄─────────────│ - │ supabase.auth.setSession(...) │ │ + │ │ issueSessionForWallet() ────► upsert users + profiles + │ ◄──── 200 { userId, role, isNewUser } + Set-Cookie: tl_session ◄──│ ``` -**Why this bridges to Supabase without a custom-JWT signer:** rather than hand- -rolling a Supabase-compatible JWT (which requires the project's JWT signing -secret and is brittle across Supabase versions), the backend uses the -**service-role key** to deterministically provision a Supabase Auth user per -wallet (`
    @siws.trustlend.app`, HMAC-derived password) and mints a real -session via `signInWithPassword`. The client then adopts it with -`supabase.auth.setSession(...)`. This is a standard, supported pattern for -"custom auth providers" on Supabase and requires no extra Supabase config. +**Sessions** are stateless: `/verify` signs an HS256 JWT (`jose`) holding the +user id, wallet and role, and stores it in the HttpOnly `tl_session` cookie +(7 days). The edge proxy verifies the cookie locally for routing; pages and API +routes call `getSessionUser()` / `requireAuthenticatedUser()` in +[lib/auth/session.ts](../lib/auth/session.ts), which re-reads the `users` row so +a deleted or re-roled account is reflected immediately. +`POST /api/auth/signout` clears the cookie. ## 2. Backend: challenge endpoint (Task 2) @@ -108,8 +106,8 @@ details matter for this to work end to end: ```jsonc // request { "address": "GABC...", "signedTxXdr": "" } -// response 200 -{ "access_token": "...", "refresh_token": "...", "isNewUser": true } +// response 200 (+ Set-Cookie: tl_session=; HttpOnly; SameSite=Lax) +{ "userId": "uuid", "role": "borrower", "isNewUser": true } ``` `verifyChallenge()` performs three checks via the Stellar SDK's `WebAuth` module: 1. **Structure & expiry** — `WebAuth.readChallengeTx` (throws on a malformed or @@ -119,10 +117,10 @@ details matter for this to work end to end: 3. **Signature** — `WebAuth.verifyChallengeTxSigners` confirms the wallet actually signed it. -On success, `issueSessionForWallet()` creates (if new) a Supabase user keyed to -the wallet and returns a session; the client adopts it with `setSession`, and -existing role-based routing (`getDashboardPath`) takes over — new SIWS users -default to the `borrower` role like any fresh signup. +On success, `issueSessionForWallet()` upserts the `users` row keyed by wallet +address (and a matching `profiles` row), the route sets the session cookie, and +role-based routing (`getDashboardPath`) takes over. The role chosen on the auth +page only applies to brand-new accounts; an existing account keeps its role. ## 5. Error states (Task 5) @@ -147,10 +145,9 @@ network, user rejected) via the existing `signTransactionWithWallet` error paths ```bash NEXT_PUBLIC_SIWS_DOMAIN=localhost:3000 # SEP-10 home/web-auth domain (both sides must match) SIWS_SERVER_SECRET= # dedicated SEP-10 signing key (S...) -SIWS_PASSWORD_SECRET= # HMAC secret for wallet-user passwords +SESSION_SECRET= # >= 32 chars; signs the session cookie NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= # Reown/WalletConnect Cloud project id (enables mobile wallets) -# reuses: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, -# SUPABASE_SERVICE_ROLE_KEY, NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE +# reuses: DATABASE_URL, NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE ``` ## 7. Tests @@ -158,14 +155,14 @@ NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= # Reown/WalletConnect Cloud project id [__tests__/auth/siws.test.ts](__tests__/auth/siws.test.ts) exercises the real `WebAuth` roundtrip (build → sign → verify) with an in-memory test keypair — happy path, wrong-signer, address mismatch, expired challenge, and malformed XDR — -without hitting the network or Supabase. +without hitting the network or the database. ## 8. Notes - The SEP-10 signing key is intentionally **separate** from the platform admin key used elsewhere (oracle, governance, default-management) — compromising one cannot forge the other. -- Rotating `SIWS_PASSWORD_SECRET` invalidates existing wallet-derived passwords; - treat it like rotating a real secret (plan a re-auth, don't do it casually). -- Future: link a SIWS identity to an *existing* password-based account instead of - always creating a fresh one keyed by address. +- Rotating `SESSION_SECRET` signs every user out (their cookies stop + verifying); the accounts themselves are unaffected. +- Admin access requires both an allowlist entry (`TRADE_VAULT_ADMIN_EMAILS` + accepts wallet addresses or e-mails) and `profiles.role = 'admin'`. diff --git a/docs/default-automation.md b/docs/default-automation.md index 6f3599b..9eb8803 100644 --- a/docs/default-automation.md +++ b/docs/default-automation.md @@ -81,7 +81,7 @@ period; `trigger_insurance_payout` fires once a loan reaches the Reported phase ```bash CRON_SECRET= # required: shared scheduler secret ADMIN_SECRET_KEY= # required for on-chain calls (S...) -SUPABASE_SERVICE_ROLE_KEY= # required: trusted DB access +DATABASE_URL= # required: Neon connection string DEFAULT_GRACE_PERIOD_DAYS=7 # optional (default 7) DEFAULT_INSURANCE_PAYOUT_DAYS=60 # optional (default 60) # reuses: NEXT_PUBLIC_LENDING_CONTRACT_ID, NEXT_PUBLIC_DEFAULT_CONTRACT_ID, diff --git a/docs/disaster-recovery.md b/docs/disaster-recovery.md index fe21e41..5e0fd3f 100644 --- a/docs/disaster-recovery.md +++ b/docs/disaster-recovery.md @@ -156,20 +156,18 @@ Restoring a single table: pg_restore --dbname="$DATABASE_URL" --data-only --table=loans restore.dump ``` -### 3.3 Supabase notes +### 3.3 Neon notes - Dumps are taken with `--no-owner --no-privileges` so they restore into a project whose roles differ from production — which is the normal case when restoring into a fresh project. -- A full dump includes Supabase-managed schemas (`auth`, `storage`, …). - Restoring those into a *new* project can conflict with what the platform - provisions. If you hit that, restore only what you need: - `pg_restore --schema=public …`. -- If `pg_dump` fails on a platform-managed schema (`vault`, `pgsodium`), set the - `BACKUP_EXCLUDE_SCHEMAS` repository variable, e.g. `vault,pgsodium`. -- RLS policies live in [supabase/rls-policies.sql](supabase/rls-policies.sql) and - are captured by the dump, but re-applying that file is a good sanity check - after a restore into a new project. +- Use the **direct** (non-pooler) Neon host for `pg_dump`/`pg_restore`; set it as + `BACKUP_DATABASE_URL` when the app's `DATABASE_URL` is the pooled one. +- Restore only the application schema into a new project + (`pg_restore --schema=public …`); Neon provisions its own system schemas. +- The schema, triggers and SQL functions are also reproducible from source with + `npm run db:migrate` against an empty database — a restore only needs to + bring back the *data* if the migrations have already been applied. --- @@ -212,8 +210,9 @@ hypothesis, not a backup.** in repositories with no activity for 60 days — if the repo goes quiet, confirm the schedule is still enabled. - **Recovery point objective is 24 hours.** Up to a day of writes can be lost. - Supabase's own point-in-time recovery is the tool for a tighter RPO; this job - is the independent, off-site copy that survives losing the Supabase project. + Neon's own point-in-time restore (branch from a timestamp) is the tool for a + tighter RPO; this job is the independent, off-site copy that survives losing + the Neon project. - **Restores are manual.** There is deliberately no automated restore path — an automated process that can overwrite production is a bigger risk than the minutes it saves. diff --git a/docs/getting-started.md b/docs/getting-started.md index db35d2f..a8c8bdd 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -100,7 +100,7 @@ git remote add upstream https://github.com/thisisouvik/trustlend-stellar.git npm install ``` -This installs all frontend dependencies listed in `package.json` (React 19, Next.js 16, Tailwind CSS 4, Supabase SDK, Stellar SDK, etc.). The install also triggers Husky's `prepare` script which activates the commit-msg hook for conventional commit enforcement. +This installs all frontend dependencies listed in `package.json` (React 19, Next.js 16, Tailwind CSS 4, Drizzle ORM, Stellar SDK, etc.). The install also triggers Husky's `prepare` script which activates the commit-msg hook for conventional commit enforcement. ### 3.2 Configure Environment Variables @@ -110,11 +110,12 @@ Copy the example environment file: cp .env.example .env.local ``` -Open `.env.local` and set the required values. For **local development with an offline Supabase project**, at minimum configure: +Open `.env.local` and set the required values. At minimum configure: ```bash -NEXT_PUBLIC_SUPABASE_URL=https://.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY= +DATABASE_URL=postgres://... # Neon pooled connection string +SESSION_SECRET= +SIWS_SERVER_SECRET= NEXT_PUBLIC_STELLAR_NETWORK=testnet NEXT_PUBLIC_STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org @@ -122,21 +123,36 @@ NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org > Refer to `.env.example` for the full list of supported variables and their descriptions. -### 3.3 Set Up Supabase (Database) +### 3.3 Set Up the Database (Neon Postgres) -The project uses Supabase for auth, Postgres, and storage. Run the SQL migration files in your Supabase SQL editor **in order**: +TrustLend stores off-chain state in Postgres on [Neon](https://neon.tech) and +talks to it through [Drizzle ORM](https://orm.drizzle.team). The schema lives in +`lib/db/schema.ts`; SQL migrations are generated from it into `drizzle/`. -| Order | File | Purpose | -|---|---|---| -| 1 | `sql/01_core_schema.sql` | Core tables: users, loans, pools, etc. | -| 2 | `sql/02_security_rls.sql` | Row-level security policies | -| 3 | `sql/03_functions_rpcs.sql` | PostgreSQL functions & RPCs | -| 4 | `sql/04_pool_performance_rpc.sql` | Optimized pool-query RPCs | -| 5 | `sql/05_interest_rate_model.sql` | Interest rate model logic | -| 6 | `sql/05_horizon_sync_schema.sql` | Horizon sync tables | -| 7 | `sql/06_kyc_provider.sql` | KYC provider schema | +1. Create a Neon project and copy the **pooled** connection string into + `DATABASE_URL` in `.env.local`. +2. Apply the migrations: + + ```bash + npm run db:migrate + ``` + + This creates every table, trigger and SQL function the app needs (the + `drizzle/0001_functions_and_triggers.sql` migration holds the row-locked + loan-funding and referral functions). +3. Optional: `npm run db:studio` opens a browser UI over the database. + +When you change `lib/db/schema.ts`, run `npm run db:generate` to produce the +next migration and commit it alongside the schema change. + +**Authentication** is Sign-In with Stellar (SEP-10): there are no passwords and +no third-party auth service. A successful wallet signature creates a row in +`users` + `profiles` and sets a signed HttpOnly session cookie +(`SESSION_SECRET`). See [auth-siws.md](auth-siws.md). -Also create a private storage bucket named **`kyc-documents`** in the Supabase dashboard for user document uploads. +**KYC documents** are stored as private files in +[Vercel Blob](https://vercel.com/docs/storage/vercel-blob); set +`BLOB_READ_WRITE_TOKEN` to enable uploads locally. ### 3.4 Start the Development Server @@ -408,11 +424,14 @@ rustup target add wasm32-unknown-unknown ``` - Ensure your Node.js version is >= 20. -### Supabase queries return empty / auth doesn't work +### Dashboards are empty / sign-in fails -- Verify your `.env.local` has the correct `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY`. -- Ensure all SQL migration files have been run in your Supabase project. -- Check that Row-Level Security (RLS) policies are applied. +- Verify `DATABASE_URL` in `.env.local` points at your Neon project and that + `npm run db:migrate` has been run against it. +- `SESSION_SECRET` must be at least 32 characters; `SIWS_SERVER_SECRET` must + be a valid Stellar secret key. +- The dev server logs `Database is not configured` when `DATABASE_URL` is + missing — pages then render their empty states rather than crashing. ### Tests are slow diff --git a/docs/liquidation-keeper.md b/docs/liquidation-keeper.md index db127a3..038b1ad 100644 --- a/docs/liquidation-keeper.md +++ b/docs/liquidation-keeper.md @@ -10,7 +10,7 @@ loans and liquidates them on-chain before they become bad debt. ## Flow -1. **Fetch open loans** — either `--source=db` (Supabase `loans` table, resolving the +1. **Fetch open loans** — either `--source=db` (the `loans` table, resolving the on-chain loan id from the funding ledger entry) or `--source=chain` (iterates the LendingContract directly via `get_loan_count`/`get_loan`). 2. **Read authoritative on-chain state** — collateral + remaining debt from @@ -39,17 +39,22 @@ individually error-handled — one bad loan never aborts the run — matching th ## Deployment (issue #259) -Two ways to run the keeper as a background worker that monitors every minute: +Three ways to run the keeper as a background worker: -1. **Vercel Cron (default deployment).** [`vercel.json`](vercel.json) schedules - `POST /api/cron/liquidation` every minute (`* * * * *`). The route - ([`app/api/cron/liquidation/route.ts`](app/api/cron/liquidation/route.ts)) +1. **GitHub Actions (default deployment).** + [`.github/workflows/keepers.yml`](../.github/workflows/keepers.yml) calls + `POST /api/cron/liquidation` every 5 minutes. Set the `KEEPER_BASE_URL` and + `CRON_SECRET` repository secrets to enable it; without them the workflow is a + no-op. The route + ([`app/api/cron/liquidation/route.ts`](../app/api/cron/liquidation/route.ts)) authenticates the caller with `Bearer ${CRON_SECRET}` (same scheme as the `payment-due` / `default-management` crons), loads the keeper config from env, - runs a full scan, and returns the per-run summary. Every-minute schedules - require a Vercel Pro plan; on Hobby, run the self-hosted variant below (or an - external cron) instead. -2. **Self-hosted service.** `npm run liquidation:keeper:service` runs + runs a full scan, and returns the per-run summary. +2. **Vercel Cron (safety net).** [`vercel.json`](../vercel.json) also schedules + the route once a day. Vercel Hobby rejects deployments that declare any cron + more frequent than daily, which is why the 5-minute cadence lives in GitHub + Actions rather than here. +3. **Self-hosted service.** `npm run liquidation:keeper:service` runs `scripts/liquidation-keeper.ts --interval=60` — a long-lived loop that rescans prices and liquidates every 60 seconds. Wrap it in Docker/systemd/PM2 on any always-on host. One-shot cron invocations (item 1 of [Usage](#usage)) remain diff --git a/docs/oracle-price-feeds.md b/docs/oracle-price-feeds.md index fec1c08..05e4616 100644 --- a/docs/oracle-price-feeds.md +++ b/docs/oracle-price-feeds.md @@ -51,8 +51,8 @@ supplies the missing off-chain feeder. ## The 5-second requirement -Vercel Cron's finest granularity is **one minute**, so a cron route alone cannot -meet the acceptance criterion. The 5-second cadence comes from a long-lived +A scheduled cron route alone cannot meet the acceptance criterion (Vercel Hobby +allows one run per day; GitHub Actions bottoms out at five minutes). The 5-second cadence comes from a long-lived process, mirroring how `liquidation-keeper` handles its own sub-minute mode: ```bash @@ -64,9 +64,10 @@ npm run price:oracle -- --interval=15 Run it under Docker, systemd or PM2 on any always-on host. -`/api/cron/price-oracle` is also scheduled every minute in `vercel.json` as a -**safety net** — if the long-lived keeper dies, prices still refresh once a -minute rather than going completely stale. It is not a substitute for the +`/api/cron/price-oracle` is also triggered every 5 minutes by +`.github/workflows/keepers.yml` (and once a day by `vercel.json`) as a +**safety net** — if the long-lived keeper dies, prices still refresh every few +minutes rather than going completely stale. It is not a substitute for the keeper. Note that each cron invocation starts with empty state, so its in-memory cache fallback is unavailable and it falls straight through to TWAP. diff --git a/docs/roadmap.md b/docs/roadmap.md index 3dbd8c2..2c1f5c0 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,7 +4,7 @@ Welcome to the TrustLend Roadmap! This document outlines our high-level goals an ## 🚀 Phase 1: Foundation (Current) - [x] Initial smart contract deployment on Stellar/Soroban Testnet -- [x] Supabase integration for auth & user profiles +- [x] Wallet-native auth (SEP-10) with Postgres-backed user profiles - [x] Web frontend MVP with Next.js & React - [x] Open-source repository setup & documentation diff --git a/docs/sep24-fiat-ramp.md b/docs/sep24-fiat-ramp.md index 386f0d2..b3fc73d 100644 --- a/docs/sep24-fiat-ramp.md +++ b/docs/sep24-fiat-ramp.md @@ -98,6 +98,6 @@ serving the borrower's region (bank / mobile-money rails) and set the appropriat - Surface a matching **"Add Funds from Fiat"** (deposit / on-ramp) button using the already-implemented `startInteractiveDeposit`. -- Persist SEP-24 transaction ids to Supabase so withdrawals show in **History**. +- Persist SEP-24 transaction ids to the database so withdrawals show in **History**. - Multi-anchor selection by borrower region/currency. - SEP-6 (programmatic, non-interactive) rails for power users. diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..e44bd3e --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "drizzle-kit"; + +/** + * drizzle-kit configuration. + * + * npm run db:generate # diff lib/db/schema.ts → new SQL migration in drizzle/ + * npm run db:migrate # apply pending migrations to DATABASE_URL + * npm run db:studio # browse the database + * + * DATABASE_URL is only needed for migrate/studio, not for generate. + */ +export default defineConfig({ + dialect: "postgresql", + schema: "./lib/db/schema.ts", + out: "./drizzle", + dbCredentials: { + url: process.env.DATABASE_URL ?? "postgres://localhost:5432/trustlend", + }, + strict: true, + verbose: true, +}); diff --git a/drizzle/0000_init.sql b/drizzle/0000_init.sql new file mode 100644 index 0000000..d230b5a --- /dev/null +++ b/drizzle/0000_init.sql @@ -0,0 +1,334 @@ +CREATE TYPE "public"."app_role" AS ENUM('borrower', 'lender', 'admin');--> statement-breakpoint +CREATE TYPE "public"."kyc_status" AS ENUM('pending', 'submitted', 'verified', 'rejected');--> statement-breakpoint +CREATE TYPE "public"."loan_status" AS ENUM('requested', 'approved', 'funded', 'active', 'repaid', 'defaulted', 'cancelled');--> statement-breakpoint +CREATE TYPE "public"."pool_status" AS ENUM('active', 'paused', 'closed');--> statement-breakpoint +CREATE TYPE "public"."position_status" AS ENUM('active', 'closed');--> statement-breakpoint +CREATE TYPE "public"."referral_status" AS ENUM('pending', 'qualified', 'paid', 'rejected');--> statement-breakpoint +CREATE TYPE "public"."risk_decision" AS ENUM('allow', 'manual_review', 'reject');--> statement-breakpoint +CREATE TYPE "public"."risk_status" AS ENUM('low', 'medium', 'high', 'blocked');--> statement-breakpoint +CREATE TYPE "public"."task_difficulty" AS ENUM('easy', 'medium', 'hard');--> statement-breakpoint +CREATE TYPE "public"."task_status" AS ENUM('open', 'assigned', 'completed', 'verified', 'cancelled');--> statement-breakpoint +CREATE TYPE "public"."tx_status" AS ENUM('pending', 'confirmed', 'failed', 'cancelled');--> statement-breakpoint +CREATE TYPE "public"."verification_status" AS ENUM('pending', 'verified', 'rejected', 'expired');--> statement-breakpoint +CREATE TABLE "chain_events" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tx_hash" text NOT NULL, + "contract_id" text, + "event_type" text NOT NULL, + "payload" jsonb DEFAULT '{}'::jsonb NOT NULL, + "happened_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "external_verifications" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "provider" text NOT NULL, + "verification_type" text NOT NULL, + "status" "verification_status" DEFAULT 'pending' NOT NULL, + "verified_at" timestamp with time zone, + "payload_meta" jsonb DEFAULT '{}'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "fraud_signals" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "signal_type" text NOT NULL, + "severity" smallint NOT NULL, + "payload" jsonb DEFAULT '{}'::jsonb NOT NULL, + "resolved" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "resolved_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "ledger_transactions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "category" text NOT NULL, + "amount" numeric(20, 6) NOT NULL, + "currency" text DEFAULT 'XLM' NOT NULL, + "status" "tx_status" DEFAULT 'pending' NOT NULL, + "ref_type" text, + "ref_id" uuid, + "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "lending_pools" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "description" text, + "status" "pool_status" DEFAULT 'active' NOT NULL, + "currency" text DEFAULT 'XLM' NOT NULL, + "apr_bps" integer NOT NULL, + "total_liquidity" numeric(20, 6) DEFAULT '0' NOT NULL, + "available_liquidity" numeric(20, 6) DEFAULT '0' NOT NULL, + "total_borrowed" numeric(20, 6) DEFAULT '0' NOT NULL, + "borrow_cap" numeric(20, 7), + "created_by" uuid, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "loan_fundings" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "loan_id" uuid NOT NULL, + "lender_id" uuid NOT NULL, + "amount" numeric(20, 6) NOT NULL, + "tx_hash" text NOT NULL, + "lender_address" text, + "funded_at" timestamp with time zone DEFAULT now() NOT NULL, + "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "loan_repayments" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "loan_id" uuid NOT NULL, + "payer_id" uuid NOT NULL, + "amount" numeric(20, 6) NOT NULL, + "paid_at" timestamp with time zone DEFAULT now() NOT NULL, + "tx_ref" text, + "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "loans" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "borrower_id" uuid NOT NULL, + "pool_id" uuid, + "status" "loan_status" DEFAULT 'requested' NOT NULL, + "principal_amount" numeric(20, 6) NOT NULL, + "apr_bps" integer NOT NULL, + "duration_days" integer NOT NULL, + "rate_model" text DEFAULT 'fixed' NOT NULL, + "rate_switch_count" integer DEFAULT 0 NOT NULL, + "last_rate_switch_at" timestamp with time zone, + "funded_amount" numeric(20, 6) DEFAULT '0' NOT NULL, + "repaid_amount" numeric(20, 6) DEFAULT '0' NOT NULL, + "requested_at" timestamp with time zone DEFAULT now() NOT NULL, + "approved_at" timestamp with time zone, + "funded_at" timestamp with time zone, + "due_at" timestamp with time zone, + "closed_at" timestamp with time zone, + "defaulted_at" timestamp with time zone, + "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "notifications" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "type" text NOT NULL, + "title" text NOT NULL, + "message" text NOT NULL, + "read" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "pool_positions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "pool_id" uuid NOT NULL, + "lender_id" uuid NOT NULL, + "status" "position_status" DEFAULT 'active' NOT NULL, + "principal_amount" numeric(20, 6) NOT NULL, + "earned_interest" numeric(20, 6) DEFAULT '0' NOT NULL, + "withdrawn_amount" numeric(20, 6) DEFAULT '0' NOT NULL, + "opened_at" timestamp with time zone DEFAULT now() NOT NULL, + "closed_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "profiles" ( + "id" uuid PRIMARY KEY NOT NULL, + "full_name" text DEFAULT '' NOT NULL, + "role" "app_role" DEFAULT 'borrower' NOT NULL, + "wallet_address" text, + "country_code" text, + "phone" text, + "date_of_birth" date, + "kyc_status" "kyc_status" DEFAULT 'pending' NOT NULL, + "risk_status" "risk_status" DEFAULT 'medium' NOT NULL, + "government_id_ipfs_hash" text, + "government_id_url" text, + "kyc_submitted_at" timestamp with time zone, + "kyc_verified_at" timestamp with time zone, + "kyc_rejection_reason" text, + "kyc_provider_id" text, + "kyc_provider_status" text, + "regulated_pool_access" boolean DEFAULT false NOT NULL, + "referral_code" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "referrals" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "referrer_id" uuid NOT NULL, + "referee_id" uuid NOT NULL, + "referral_code" text NOT NULL, + "status" "referral_status" DEFAULT 'pending' NOT NULL, + "qualifying_loan_id" uuid, + "bonus_amount" numeric(20, 7) DEFAULT '0' NOT NULL, + "payout_tx_hash" text, + "qualified_at" timestamp with time zone, + "paid_at" timestamp with time zone, + "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "reputation_events" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "source_type" text NOT NULL, + "source_id" uuid, + "source_key" text, + "points_delta" integer NOT NULL, + "reason" text NOT NULL, + "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "reputation_snapshots" ( + "user_id" uuid PRIMARY KEY NOT NULL, + "score_total" integer DEFAULT 0 NOT NULL, + "repayment_score" integer DEFAULT 0 NOT NULL, + "lending_score" integer DEFAULT 0 NOT NULL, + "consistency_score" integer DEFAULT 0 NOT NULL, + "external_score" integer DEFAULT 0 NOT NULL, + "reputation_level" text DEFAULT 'bronze' NOT NULL, + "score_breakdown" jsonb DEFAULT '{}'::jsonb NOT NULL, + "calculated_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "risk_assessments" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "score" numeric(5, 2) NOT NULL, + "decision" "risk_decision" NOT NULL, + "reasons" jsonb DEFAULT '[]'::jsonb NOT NULL, + "assessed_at" timestamp with time zone DEFAULT now() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "tasks" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "creator_id" uuid NOT NULL, + "assigned_to" uuid, + "title" text NOT NULL, + "description" text, + "category" text, + "reward_xlm" numeric(20, 6) DEFAULT '0' NOT NULL, + "difficulty" "task_difficulty" DEFAULT 'easy' NOT NULL, + "status" "task_status" DEFAULT 'open' NOT NULL, + "completion_deadline" timestamp with time zone, + "completion_date" timestamp with time zone, + "proof_submission" text, + "creator_rating" smallint, + "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "wallet_address" text NOT NULL, + "role" "app_role" DEFAULT 'borrower' NOT NULL, + "email" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "last_sign_in_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "webhook_endpoints" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "url" text NOT NULL, + "platform" text NOT NULL, + "topic" text NOT NULL, + "is_active" boolean DEFAULT true NOT NULL, + "created_by" uuid, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "external_verifications" ADD CONSTRAINT "external_verifications_user_id_profiles_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "fraud_signals" ADD CONSTRAINT "fraud_signals_user_id_profiles_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ledger_transactions" ADD CONSTRAINT "ledger_transactions_user_id_profiles_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lending_pools" ADD CONSTRAINT "lending_pools_created_by_profiles_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."profiles"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "loan_fundings" ADD CONSTRAINT "loan_fundings_loan_id_loans_id_fk" FOREIGN KEY ("loan_id") REFERENCES "public"."loans"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "loan_fundings" ADD CONSTRAINT "loan_fundings_lender_id_profiles_id_fk" FOREIGN KEY ("lender_id") REFERENCES "public"."profiles"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "loan_repayments" ADD CONSTRAINT "loan_repayments_loan_id_loans_id_fk" FOREIGN KEY ("loan_id") REFERENCES "public"."loans"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "loan_repayments" ADD CONSTRAINT "loan_repayments_payer_id_profiles_id_fk" FOREIGN KEY ("payer_id") REFERENCES "public"."profiles"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "loans" ADD CONSTRAINT "loans_borrower_id_profiles_id_fk" FOREIGN KEY ("borrower_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "loans" ADD CONSTRAINT "loans_pool_id_lending_pools_id_fk" FOREIGN KEY ("pool_id") REFERENCES "public"."lending_pools"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pool_positions" ADD CONSTRAINT "pool_positions_pool_id_lending_pools_id_fk" FOREIGN KEY ("pool_id") REFERENCES "public"."lending_pools"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pool_positions" ADD CONSTRAINT "pool_positions_lender_id_profiles_id_fk" FOREIGN KEY ("lender_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "profiles" ADD CONSTRAINT "profiles_id_users_id_fk" FOREIGN KEY ("id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "referrals" ADD CONSTRAINT "referrals_referrer_id_profiles_id_fk" FOREIGN KEY ("referrer_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "referrals" ADD CONSTRAINT "referrals_referee_id_profiles_id_fk" FOREIGN KEY ("referee_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "referrals" ADD CONSTRAINT "referrals_qualifying_loan_id_loans_id_fk" FOREIGN KEY ("qualifying_loan_id") REFERENCES "public"."loans"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "reputation_events" ADD CONSTRAINT "reputation_events_user_id_profiles_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "reputation_snapshots" ADD CONSTRAINT "reputation_snapshots_user_id_profiles_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "risk_assessments" ADD CONSTRAINT "risk_assessments_user_id_profiles_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tasks" ADD CONSTRAINT "tasks_creator_id_profiles_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tasks" ADD CONSTRAINT "tasks_assigned_to_profiles_id_fk" FOREIGN KEY ("assigned_to") REFERENCES "public"."profiles"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "webhook_endpoints" ADD CONSTRAINT "webhook_endpoints_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "chain_events_tx_hash_event_type_key" ON "chain_events" USING btree ("tx_hash","event_type");--> statement-breakpoint +CREATE INDEX "idx_chain_events_contract_id" ON "chain_events" USING btree ("contract_id");--> statement-breakpoint +CREATE INDEX "idx_chain_events_happened_at" ON "chain_events" USING btree ("happened_at");--> statement-breakpoint +CREATE INDEX "idx_external_verifications_user_id" ON "external_verifications" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_external_verifications_status" ON "external_verifications" USING btree ("status");--> statement-breakpoint +CREATE INDEX "idx_fraud_signals_user_id_created_at" ON "fraud_signals" USING btree ("user_id","created_at");--> statement-breakpoint +CREATE INDEX "idx_fraud_signals_resolved" ON "fraud_signals" USING btree ("resolved");--> statement-breakpoint +CREATE INDEX "idx_ledger_transactions_user_id_created_at" ON "ledger_transactions" USING btree ("user_id","created_at");--> statement-breakpoint +CREATE INDEX "idx_ledger_transactions_status" ON "ledger_transactions" USING btree ("status");--> statement-breakpoint +CREATE INDEX "idx_lending_pools_status" ON "lending_pools" USING btree ("status");--> statement-breakpoint +CREATE INDEX "idx_lending_pools_status_available" ON "lending_pools" USING btree ("status","available_liquidity");--> statement-breakpoint +CREATE INDEX "idx_lending_pools_created_at_desc" ON "lending_pools" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "idx_loan_fundings_loan_id" ON "loan_fundings" USING btree ("loan_id");--> statement-breakpoint +CREATE INDEX "idx_loan_fundings_lender_id" ON "loan_fundings" USING btree ("lender_id");--> statement-breakpoint +CREATE INDEX "idx_loan_fundings_lender_loan" ON "loan_fundings" USING btree ("lender_id","loan_id");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_loan_fundings_tx_hash" ON "loan_fundings" USING btree ("tx_hash");--> statement-breakpoint +CREATE INDEX "idx_loan_repayments_loan_id" ON "loan_repayments" USING btree ("loan_id");--> statement-breakpoint +CREATE INDEX "idx_loan_repayments_payer_id" ON "loan_repayments" USING btree ("payer_id");--> statement-breakpoint +CREATE INDEX "idx_loans_borrower_id" ON "loans" USING btree ("borrower_id");--> statement-breakpoint +CREATE INDEX "idx_loans_pool_id" ON "loans" USING btree ("pool_id");--> statement-breakpoint +CREATE INDEX "idx_loans_status" ON "loans" USING btree ("status");--> statement-breakpoint +CREATE INDEX "idx_loans_due_at" ON "loans" USING btree ("due_at");--> statement-breakpoint +CREATE INDEX "idx_loans_borrower_status" ON "loans" USING btree ("borrower_id","status");--> statement-breakpoint +CREATE INDEX "idx_loans_rate_model" ON "loans" USING btree ("rate_model");--> statement-breakpoint +CREATE INDEX "idx_loans_status_funded" ON "loans" USING btree ("status","funded_amount");--> statement-breakpoint +CREATE INDEX "idx_notifications_user_id_created_at" ON "notifications" USING btree ("user_id","created_at");--> statement-breakpoint +CREATE INDEX "idx_pool_positions_lender_id" ON "pool_positions" USING btree ("lender_id");--> statement-breakpoint +CREATE INDEX "idx_pool_positions_pool_id" ON "pool_positions" USING btree ("pool_id");--> statement-breakpoint +CREATE INDEX "idx_profiles_role" ON "profiles" USING btree ("role");--> statement-breakpoint +CREATE INDEX "idx_profiles_wallet_address" ON "profiles" USING btree ("wallet_address");--> statement-breakpoint +CREATE INDEX "idx_profiles_kyc_status" ON "profiles" USING btree ("kyc_status");--> statement-breakpoint +CREATE INDEX "idx_profiles_risk_status" ON "profiles" USING btree ("risk_status");--> statement-breakpoint +CREATE INDEX "idx_profiles_kyc_submitted_at" ON "profiles" USING btree ("kyc_submitted_at");--> statement-breakpoint +CREATE UNIQUE INDEX "profiles_referral_code_key" ON "profiles" USING btree ("referral_code");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_profiles_kyc_provider_id" ON "profiles" USING btree ("kyc_provider_id") WHERE "profiles"."kyc_provider_id" is not null;--> statement-breakpoint +CREATE INDEX "idx_profiles_regulated_pool_access" ON "profiles" USING btree ("regulated_pool_access");--> statement-breakpoint +CREATE UNIQUE INDEX "referrals_referee_id_key" ON "referrals" USING btree ("referee_id");--> statement-breakpoint +CREATE INDEX "idx_referrals_referrer_id" ON "referrals" USING btree ("referrer_id");--> statement-breakpoint +CREATE INDEX "idx_referrals_status" ON "referrals" USING btree ("status");--> statement-breakpoint +CREATE INDEX "idx_referrals_referrer_status" ON "referrals" USING btree ("referrer_id","status");--> statement-breakpoint +CREATE INDEX "idx_rep_events_user_id_created_at" ON "reputation_events" USING btree ("user_id","created_at");--> statement-breakpoint +CREATE INDEX "idx_rep_events_source" ON "reputation_events" USING btree ("source_type","source_id");--> statement-breakpoint +CREATE INDEX "idx_rep_events_source_key" ON "reputation_events" USING btree ("source_type","source_key");--> statement-breakpoint +CREATE INDEX "idx_risk_assessments_user_id_assessed_at" ON "risk_assessments" USING btree ("user_id","assessed_at");--> statement-breakpoint +CREATE INDEX "idx_tasks_creator_id" ON "tasks" USING btree ("creator_id");--> statement-breakpoint +CREATE INDEX "idx_tasks_assigned_to" ON "tasks" USING btree ("assigned_to");--> statement-breakpoint +CREATE INDEX "idx_tasks_status" ON "tasks" USING btree ("status");--> statement-breakpoint +CREATE INDEX "idx_tasks_created_at" ON "tasks" USING btree ("created_at");--> statement-breakpoint +CREATE UNIQUE INDEX "users_wallet_address_key" ON "users" USING btree ("wallet_address");--> statement-breakpoint +CREATE INDEX "idx_webhook_endpoints_platform" ON "webhook_endpoints" USING btree ("platform");--> statement-breakpoint +CREATE INDEX "idx_webhook_endpoints_topic" ON "webhook_endpoints" USING btree ("topic"); \ No newline at end of file diff --git a/drizzle/0001_functions_and_triggers.sql b/drizzle/0001_functions_and_triggers.sql new file mode 100644 index 0000000..1449e9d --- /dev/null +++ b/drizzle/0001_functions_and_triggers.sql @@ -0,0 +1,439 @@ +-- Functions and triggers that live in the database. +-- +-- Authorization is NOT enforced here (there is no auth.uid() on Neon): every +-- caller is the application server, which verifies the session before it +-- invokes any of these. Keep them limited to logic that genuinely benefits +-- from running inside Postgres — row locks, atomic multi-row transitions and +-- triggers. + +-- ─── updated_at maintenance ─────────────────────────────────────────────────── + +create or replace function public.set_updated_at() +returns trigger +language plpgsql +as $$ +begin + new.updated_at = now(); + return new; +end; +$$; +--> statement-breakpoint + +create trigger trg_profiles_updated_at +before update on public.profiles +for each row execute function public.set_updated_at(); +--> statement-breakpoint +create trigger trg_reputation_snapshots_updated_at +before update on public.reputation_snapshots +for each row execute function public.set_updated_at(); +--> statement-breakpoint +create trigger trg_tasks_updated_at +before update on public.tasks +for each row execute function public.set_updated_at(); +--> statement-breakpoint +create trigger trg_lending_pools_updated_at +before update on public.lending_pools +for each row execute function public.set_updated_at(); +--> statement-breakpoint +create trigger trg_pool_positions_updated_at +before update on public.pool_positions +for each row execute function public.set_updated_at(); +--> statement-breakpoint +create trigger trg_loans_updated_at +before update on public.loans +for each row execute function public.set_updated_at(); +--> statement-breakpoint +create trigger trg_ledger_transactions_updated_at +before update on public.ledger_transactions +for each row execute function public.set_updated_at(); +--> statement-breakpoint +create trigger trg_external_verifications_updated_at +before update on public.external_verifications +for each row execute function public.set_updated_at(); +--> statement-breakpoint +create trigger trg_webhook_endpoints_updated_at +before update on public.webhook_endpoints +for each row execute function public.set_updated_at(); +--> statement-breakpoint +create trigger trg_referrals_updated_at +before update on public.referrals +for each row execute function public.set_updated_at(); +--> statement-breakpoint + +-- ─── Reputation snapshot kept in sync with events ───────────────────────────── + +create or replace function public.sync_reputation_snapshot_from_event() +returns trigger +language plpgsql +set search_path = public +as $$ +declare + existing_repayment_score integer := 0; + existing_lending_score integer := 0; + existing_consistency_score integer := 0; + existing_external_score integer := 0; + existing_level text := 'bronze'; + total_points integer := 0; + computed_total integer := 250; +begin + if new.user_id is null then + return new; + end if; + + select + coalesce(score_total, 250), + coalesce(repayment_score, 0), + coalesce(lending_score, 0), + coalesce(consistency_score, 0), + coalesce(external_score, 0), + coalesce(reputation_level, 'bronze') + into computed_total, existing_repayment_score, existing_lending_score, + existing_consistency_score, existing_external_score, existing_level + from public.reputation_snapshots + where user_id = new.user_id; + + select coalesce(sum(points_delta), 0) + into total_points + from public.reputation_events + where user_id = new.user_id; + + computed_total := greatest(0, least(750, 250 + total_points)); + + insert into public.reputation_snapshots ( + user_id, score_total, repayment_score, lending_score, consistency_score, + external_score, reputation_level, calculated_at, updated_at + ) + values ( + new.user_id, computed_total, existing_repayment_score, existing_lending_score, + existing_consistency_score, existing_external_score, existing_level, now(), now() + ) + on conflict (user_id) do update + set score_total = excluded.score_total, + repayment_score = excluded.repayment_score, + lending_score = excluded.lending_score, + consistency_score = excluded.consistency_score, + external_score = excluded.external_score, + reputation_level = excluded.reputation_level, + calculated_at = excluded.calculated_at, + updated_at = excluded.updated_at; + + return new; +end; +$$; +--> statement-breakpoint + +create trigger trg_reputation_events_snapshot +after insert on public.reputation_events +for each row execute function public.sync_reputation_snapshot_from_event(); +--> statement-breakpoint + +-- ─── Referral codes ─────────────────────────────────────────────────────────── + +create or replace function public.generate_referral_code() +returns text +language plpgsql +volatile +as $$ +declare + v_alphabet constant text := '23456789ABCDEFGHJKMNPQRSTVWXYZ'; + v_code text; + v_attempt int := 0; +begin + loop + v_code := 'TL'; + for _ in 1..6 loop + v_code := v_code || substr(v_alphabet, floor(random() * 30)::int + 1, 1); + end loop; + + exit when not exists ( + select 1 from public.profiles where referral_code = v_code + ); + + v_attempt := v_attempt + 1; + if v_attempt > 20 then + raise exception 'Could not generate a unique referral code after % attempts', v_attempt; + end if; + end loop; + + return v_code; +end; +$$; +--> statement-breakpoint + +create or replace function public.ensure_referral_code(p_user_id uuid) +returns text +language plpgsql +set search_path = public +as $$ +declare + v_code text; +begin + select referral_code into v_code from public.profiles where id = p_user_id; + if v_code is not null then + return v_code; + end if; + + v_code := public.generate_referral_code(); + + update public.profiles + set referral_code = v_code + where id = p_user_id and referral_code is null; + + select referral_code into v_code from public.profiles where id = p_user_id; + return v_code; +end; +$$; +--> statement-breakpoint + +create or replace function public.assign_referral_code_on_profile() +returns trigger +language plpgsql +set search_path = public +as $$ +begin + if new.referral_code is null then + new.referral_code := public.generate_referral_code(); + end if; + return new; +exception + when others then + return new; +end; +$$; +--> statement-breakpoint + +create trigger trg_profiles_referral_code +before insert on public.profiles +for each row execute function public.assign_referral_code_on_profile(); +--> statement-breakpoint + +-- Attribute a new user to a referrer. Idempotent: a referee is attributed once. +create or replace function public.record_referral( + p_referee_id uuid, + p_referral_code text +) +returns table ( + referral_id uuid, + referrer_id uuid, + status public.referral_status +) +language plpgsql +set search_path = public +as $$ +declare + v_referrer_id uuid; + v_code text; + v_referral_id uuid; + v_status public.referral_status; +begin + v_code := upper(trim(p_referral_code)); + + if v_code is null or v_code = '' then + raise exception 'Referral code is required'; + end if; + + select id into v_referrer_id from public.profiles where referral_code = v_code; + + if v_referrer_id is null then + raise exception 'Unknown referral code'; + end if; + + if v_referrer_id = p_referee_id then + raise exception 'Cannot refer yourself'; + end if; + + insert into public.referrals (referrer_id, referee_id, referral_code, status) + values (v_referrer_id, p_referee_id, v_code, 'pending') + on conflict (referee_id) do nothing + returning id into v_referral_id; + + if v_referral_id is null then + select r.id, r.referrer_id, r.status + into v_referral_id, v_referrer_id, v_status + from public.referrals r + where r.referee_id = p_referee_id; + + referral_id := v_referral_id; + referrer_id := v_referrer_id; + status := v_status; + return next; + return; + end if; + + referral_id := v_referral_id; + referrer_id := v_referrer_id; + status := 'pending'::public.referral_status; + return next; +end; +$$; +--> statement-breakpoint + +-- Mark a pending referral qualified when the referee's loan activates. +create or replace function public.qualify_referral( + p_referee_id uuid, + p_loan_id uuid +) +returns table ( + referral_id uuid, + referrer_id uuid, + status public.referral_status +) +language plpgsql +set search_path = public +as $$ +declare + v_row public.referrals; +begin + select * into v_row from public.referrals where referee_id = p_referee_id for update; + + if not found then + return; + end if; + + if v_row.status <> 'pending'::public.referral_status then + referral_id := v_row.id; + referrer_id := v_row.referrer_id; + status := v_row.status; + return next; + return; + end if; + + update public.referrals + set status = 'qualified'::public.referral_status, + qualifying_loan_id = p_loan_id, + qualified_at = now() + where id = v_row.id; + + referral_id := v_row.id; + referrer_id := v_row.referrer_id; + status := 'qualified'::public.referral_status; + return next; +end; +$$; +--> statement-breakpoint + +create or replace function public.settle_referral_payout( + p_referral_id uuid, + p_bonus_amount numeric, + p_tx_hash text +) +returns void +language plpgsql +set search_path = public +as $$ +begin + if p_bonus_amount < 0 then + raise exception 'Bonus amount cannot be negative'; + end if; + + update public.referrals + set status = 'paid'::public.referral_status, + bonus_amount = p_bonus_amount, + payout_tx_hash = p_tx_hash, + paid_at = now() + where id = p_referral_id + and status <> 'paid'::public.referral_status; +end; +$$; +--> statement-breakpoint + +-- ─── Loan funding (partial fills, row-locked) ───────────────────────────────── + +create or replace function public.record_loan_funding( + p_loan_id uuid, + p_lender_id uuid, + p_amount numeric, + p_tx_hash text, + p_lender_address text default null, + p_funded_at timestamptz default now() +) +returns table ( + loan_id uuid, + status public.loan_status, + principal_amount numeric, + funded_amount numeric, + remaining_amount numeric, + is_fully_funded boolean, + funding_id uuid +) +language plpgsql +set search_path = public +as $$ +declare + v_loan public.loans; + v_new_total numeric(20, 6); + v_remaining numeric(20, 6); + v_funding_id uuid; + v_due_at timestamptz; +begin + if p_amount is null or p_amount <= 0 then + raise exception 'funding amount must be greater than zero'; + end if; + + if p_tx_hash is null or length(trim(p_tx_hash)) = 0 then + raise exception 'a stellar transaction hash is required'; + end if; + + -- Lock the loan so two lenders cannot both read the same remaining amount. + select * into v_loan from public.loans where id = p_loan_id for update; + + if not found then + raise exception 'loan not found'; + end if; + + if v_loan.status not in ('requested', 'approved') then + raise exception 'loan is not available for funding (status: %)', v_loan.status; + end if; + + if v_loan.borrower_id = p_lender_id then + raise exception 'you cannot fund your own loan'; + end if; + + v_remaining := v_loan.principal_amount - v_loan.funded_amount; + + if v_remaining <= 0 then + raise exception 'loan is already fully funded'; + end if; + + -- The lender already sent exactly p_amount on-chain; never silently cap it. + if p_amount > v_remaining then + raise exception 'funding amount % exceeds the remaining % on this loan', p_amount, v_remaining; + end if; + + insert into public.loan_fundings (loan_id, lender_id, amount, tx_hash, lender_address, funded_at) + values (p_loan_id, p_lender_id, p_amount, trim(p_tx_hash), p_lender_address, p_funded_at) + returning id into v_funding_id; + + v_new_total := v_loan.funded_amount + p_amount; + + if v_new_total >= v_loan.principal_amount then + v_due_at := p_funded_at + make_interval(days => v_loan.duration_days); + + update public.loans + set funded_amount = v_new_total, + status = 'active', + approved_at = coalesce(approved_at, p_funded_at), + funded_at = coalesce(funded_at, p_funded_at), + due_at = v_due_at, + updated_at = now() + where id = p_loan_id + returning * into v_loan; + else + update public.loans + set funded_amount = v_new_total, + updated_at = now() + where id = p_loan_id + returning * into v_loan; + end if; + + return query + select + v_loan.id, + v_loan.status, + v_loan.principal_amount, + v_loan.funded_amount, + greatest(v_loan.principal_amount - v_loan.funded_amount, 0)::numeric, + (v_loan.funded_amount >= v_loan.principal_amount), + v_funding_id; +end; +$$; diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..f294483 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,2822 @@ +{ + "id": "7651aee8-f11d-4065-a209-a2f45ddb9496", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.chain_events": { + "name": "chain_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contract_id": { + "name": "contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "happened_at": { + "name": "happened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chain_events_tx_hash_event_type_key": { + "name": "chain_events_tx_hash_event_type_key", + "columns": [ + { + "expression": "tx_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chain_events_contract_id": { + "name": "idx_chain_events_contract_id", + "columns": [ + { + "expression": "contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chain_events_happened_at": { + "name": "idx_chain_events_happened_at", + "columns": [ + { + "expression": "happened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_verifications": { + "name": "external_verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_type": { + "name": "verification_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "verification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payload_meta": { + "name": "payload_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_external_verifications_user_id": { + "name": "idx_external_verifications_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_external_verifications_status": { + "name": "idx_external_verifications_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "external_verifications_user_id_profiles_id_fk": { + "name": "external_verifications_user_id_profiles_id_fk", + "tableFrom": "external_verifications", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fraud_signals": { + "name": "fraud_signals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "signal_type": { + "name": "signal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "resolved": { + "name": "resolved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_fraud_signals_user_id_created_at": { + "name": "idx_fraud_signals_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_fraud_signals_resolved": { + "name": "idx_fraud_signals_resolved", + "columns": [ + { + "expression": "resolved", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fraud_signals_user_id_profiles_id_fk": { + "name": "fraud_signals_user_id_profiles_id_fk", + "tableFrom": "fraud_signals", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ledger_transactions": { + "name": "ledger_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'XLM'" + }, + "status": { + "name": "status", + "type": "tx_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "ref_type": { + "name": "ref_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ref_id": { + "name": "ref_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ledger_transactions_user_id_created_at": { + "name": "idx_ledger_transactions_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ledger_transactions_status": { + "name": "idx_ledger_transactions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ledger_transactions_user_id_profiles_id_fk": { + "name": "ledger_transactions_user_id_profiles_id_fk", + "tableFrom": "ledger_transactions", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lending_pools": { + "name": "lending_pools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "pool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'XLM'" + }, + "apr_bps": { + "name": "apr_bps", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_liquidity": { + "name": "total_liquidity", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "available_liquidity": { + "name": "available_liquidity", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_borrowed": { + "name": "total_borrowed", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "borrow_cap": { + "name": "borrow_cap", + "type": "numeric(20, 7)", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_lending_pools_status": { + "name": "idx_lending_pools_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_lending_pools_status_available": { + "name": "idx_lending_pools_status_available", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_liquidity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_lending_pools_created_at_desc": { + "name": "idx_lending_pools_created_at_desc", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "lending_pools_created_by_profiles_id_fk": { + "name": "lending_pools_created_by_profiles_id_fk", + "tableFrom": "lending_pools", + "tableTo": "profiles", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.loan_fundings": { + "name": "loan_fundings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "loan_id": { + "name": "loan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "lender_id": { + "name": "lender_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lender_address": { + "name": "lender_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "funded_at": { + "name": "funded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_loan_fundings_loan_id": { + "name": "idx_loan_fundings_loan_id", + "columns": [ + { + "expression": "loan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_loan_fundings_lender_id": { + "name": "idx_loan_fundings_lender_id", + "columns": [ + { + "expression": "lender_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_loan_fundings_lender_loan": { + "name": "idx_loan_fundings_lender_loan", + "columns": [ + { + "expression": "lender_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "loan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_loan_fundings_tx_hash": { + "name": "idx_loan_fundings_tx_hash", + "columns": [ + { + "expression": "tx_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "loan_fundings_loan_id_loans_id_fk": { + "name": "loan_fundings_loan_id_loans_id_fk", + "tableFrom": "loan_fundings", + "tableTo": "loans", + "columnsFrom": [ + "loan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "loan_fundings_lender_id_profiles_id_fk": { + "name": "loan_fundings_lender_id_profiles_id_fk", + "tableFrom": "loan_fundings", + "tableTo": "profiles", + "columnsFrom": [ + "lender_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.loan_repayments": { + "name": "loan_repayments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "loan_id": { + "name": "loan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "payer_id": { + "name": "payer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tx_ref": { + "name": "tx_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_loan_repayments_loan_id": { + "name": "idx_loan_repayments_loan_id", + "columns": [ + { + "expression": "loan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_loan_repayments_payer_id": { + "name": "idx_loan_repayments_payer_id", + "columns": [ + { + "expression": "payer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "loan_repayments_loan_id_loans_id_fk": { + "name": "loan_repayments_loan_id_loans_id_fk", + "tableFrom": "loan_repayments", + "tableTo": "loans", + "columnsFrom": [ + "loan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "loan_repayments_payer_id_profiles_id_fk": { + "name": "loan_repayments_payer_id_profiles_id_fk", + "tableFrom": "loan_repayments", + "tableTo": "profiles", + "columnsFrom": [ + "payer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.loans": { + "name": "loans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "borrower_id": { + "name": "borrower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pool_id": { + "name": "pool_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "loan_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "principal_amount": { + "name": "principal_amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true + }, + "apr_bps": { + "name": "apr_bps", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_days": { + "name": "duration_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rate_model": { + "name": "rate_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "rate_switch_count": { + "name": "rate_switch_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_rate_switch_at": { + "name": "last_rate_switch_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "funded_amount": { + "name": "funded_amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "repaid_amount": { + "name": "repaid_amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "funded_at": { + "name": "funded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "defaulted_at": { + "name": "defaulted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_loans_borrower_id": { + "name": "idx_loans_borrower_id", + "columns": [ + { + "expression": "borrower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_loans_pool_id": { + "name": "idx_loans_pool_id", + "columns": [ + { + "expression": "pool_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_loans_status": { + "name": "idx_loans_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_loans_due_at": { + "name": "idx_loans_due_at", + "columns": [ + { + "expression": "due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_loans_borrower_status": { + "name": "idx_loans_borrower_status", + "columns": [ + { + "expression": "borrower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_loans_rate_model": { + "name": "idx_loans_rate_model", + "columns": [ + { + "expression": "rate_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_loans_status_funded": { + "name": "idx_loans_status_funded", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "funded_amount", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "loans_borrower_id_profiles_id_fk": { + "name": "loans_borrower_id_profiles_id_fk", + "tableFrom": "loans", + "tableTo": "profiles", + "columnsFrom": [ + "borrower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "loans_pool_id_lending_pools_id_fk": { + "name": "loans_pool_id_lending_pools_id_fk", + "tableFrom": "loans", + "tableTo": "lending_pools", + "columnsFrom": [ + "pool_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_notifications_user_id_created_at": { + "name": "idx_notifications_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pool_positions": { + "name": "pool_positions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "pool_id": { + "name": "pool_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "lender_id": { + "name": "lender_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "position_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "principal_amount": { + "name": "principal_amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true + }, + "earned_interest": { + "name": "earned_interest", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "withdrawn_amount": { + "name": "withdrawn_amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_pool_positions_lender_id": { + "name": "idx_pool_positions_lender_id", + "columns": [ + { + "expression": "lender_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pool_positions_pool_id": { + "name": "idx_pool_positions_pool_id", + "columns": [ + { + "expression": "pool_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pool_positions_pool_id_lending_pools_id_fk": { + "name": "pool_positions_pool_id_lending_pools_id_fk", + "tableFrom": "pool_positions", + "tableTo": "lending_pools", + "columnsFrom": [ + "pool_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pool_positions_lender_id_profiles_id_fk": { + "name": "pool_positions_lender_id_profiles_id_fk", + "tableFrom": "pool_positions", + "tableTo": "profiles", + "columnsFrom": [ + "lender_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "role": { + "name": "role", + "type": "app_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'borrower'" + }, + "wallet_address": { + "name": "wallet_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date_of_birth": { + "name": "date_of_birth", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "kyc_status": { + "name": "kyc_status", + "type": "kyc_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "risk_status": { + "name": "risk_status", + "type": "risk_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "government_id_ipfs_hash": { + "name": "government_id_ipfs_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "government_id_url": { + "name": "government_id_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kyc_submitted_at": { + "name": "kyc_submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "kyc_verified_at": { + "name": "kyc_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "kyc_rejection_reason": { + "name": "kyc_rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kyc_provider_id": { + "name": "kyc_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kyc_provider_status": { + "name": "kyc_provider_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "regulated_pool_access": { + "name": "regulated_pool_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_profiles_role": { + "name": "idx_profiles_role", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_profiles_wallet_address": { + "name": "idx_profiles_wallet_address", + "columns": [ + { + "expression": "wallet_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_profiles_kyc_status": { + "name": "idx_profiles_kyc_status", + "columns": [ + { + "expression": "kyc_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_profiles_risk_status": { + "name": "idx_profiles_risk_status", + "columns": [ + { + "expression": "risk_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_profiles_kyc_submitted_at": { + "name": "idx_profiles_kyc_submitted_at", + "columns": [ + { + "expression": "kyc_submitted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "profiles_referral_code_key": { + "name": "profiles_referral_code_key", + "columns": [ + { + "expression": "referral_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_profiles_kyc_provider_id": { + "name": "idx_profiles_kyc_provider_id", + "columns": [ + { + "expression": "kyc_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"profiles\".\"kyc_provider_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_profiles_regulated_pool_access": { + "name": "idx_profiles_regulated_pool_access", + "columns": [ + { + "expression": "regulated_pool_access", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "profiles_id_users_id_fk": { + "name": "profiles_id_users_id_fk", + "tableFrom": "profiles", + "tableTo": "users", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referrals": { + "name": "referrals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "referrer_id": { + "name": "referrer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "referee_id": { + "name": "referee_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "referral_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "qualifying_loan_id": { + "name": "qualifying_loan_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "bonus_amount": { + "name": "bonus_amount", + "type": "numeric(20, 7)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "payout_tx_hash": { + "name": "payout_tx_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "qualified_at": { + "name": "qualified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "referrals_referee_id_key": { + "name": "referrals_referee_id_key", + "columns": [ + { + "expression": "referee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_referrals_referrer_id": { + "name": "idx_referrals_referrer_id", + "columns": [ + { + "expression": "referrer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_referrals_status": { + "name": "idx_referrals_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_referrals_referrer_status": { + "name": "idx_referrals_referrer_status", + "columns": [ + { + "expression": "referrer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "referrals_referrer_id_profiles_id_fk": { + "name": "referrals_referrer_id_profiles_id_fk", + "tableFrom": "referrals", + "tableTo": "profiles", + "columnsFrom": [ + "referrer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referrals_referee_id_profiles_id_fk": { + "name": "referrals_referee_id_profiles_id_fk", + "tableFrom": "referrals", + "tableTo": "profiles", + "columnsFrom": [ + "referee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referrals_qualifying_loan_id_loans_id_fk": { + "name": "referrals_qualifying_loan_id_loans_id_fk", + "tableFrom": "referrals", + "tableTo": "loans", + "columnsFrom": [ + "qualifying_loan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reputation_events": { + "name": "reputation_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "points_delta": { + "name": "points_delta", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_rep_events_user_id_created_at": { + "name": "idx_rep_events_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_rep_events_source": { + "name": "idx_rep_events_source", + "columns": [ + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_rep_events_source_key": { + "name": "idx_rep_events_source_key", + "columns": [ + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reputation_events_user_id_profiles_id_fk": { + "name": "reputation_events_user_id_profiles_id_fk", + "tableFrom": "reputation_events", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reputation_snapshots": { + "name": "reputation_snapshots", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "score_total": { + "name": "score_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "repayment_score": { + "name": "repayment_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lending_score": { + "name": "lending_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "consistency_score": { + "name": "consistency_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "external_score": { + "name": "external_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reputation_level": { + "name": "reputation_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'bronze'" + }, + "score_breakdown": { + "name": "score_breakdown", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reputation_snapshots_user_id_profiles_id_fk": { + "name": "reputation_snapshots_user_id_profiles_id_fk", + "tableFrom": "reputation_snapshots", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_assessments": { + "name": "risk_assessments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "risk_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "assessed_at": { + "name": "assessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_risk_assessments_user_id_assessed_at": { + "name": "idx_risk_assessments_user_id_assessed_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assessed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_assessments_user_id_profiles_id_fk": { + "name": "risk_assessments_user_id_profiles_id_fk", + "tableFrom": "risk_assessments", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "creator_id": { + "name": "creator_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "assigned_to": { + "name": "assigned_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_xlm": { + "name": "reward_xlm", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "difficulty": { + "name": "difficulty", + "type": "task_difficulty", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'easy'" + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "completion_deadline": { + "name": "completion_deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completion_date": { + "name": "completion_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "proof_submission": { + "name": "proof_submission", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_rating": { + "name": "creator_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_tasks_creator_id": { + "name": "idx_tasks_creator_id", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tasks_assigned_to": { + "name": "idx_tasks_assigned_to", + "columns": [ + { + "expression": "assigned_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tasks_status": { + "name": "idx_tasks_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tasks_created_at": { + "name": "idx_tasks_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_creator_id_profiles_id_fk": { + "name": "tasks_creator_id_profiles_id_fk", + "tableFrom": "tasks", + "tableTo": "profiles", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_assigned_to_profiles_id_fk": { + "name": "tasks_assigned_to_profiles_id_fk", + "tableFrom": "tasks", + "tableTo": "profiles", + "columnsFrom": [ + "assigned_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "wallet_address": { + "name": "wallet_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "app_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'borrower'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_sign_in_at": { + "name": "last_sign_in_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_wallet_address_key": { + "name": "users_wallet_address_key", + "columns": [ + { + "expression": "wallet_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_endpoints": { + "name": "webhook_endpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_webhook_endpoints_platform": { + "name": "idx_webhook_endpoints_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_endpoints_topic": { + "name": "idx_webhook_endpoints_topic", + "columns": [ + { + "expression": "topic", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_endpoints_created_by_users_id_fk": { + "name": "webhook_endpoints_created_by_users_id_fk", + "tableFrom": "webhook_endpoints", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.app_role": { + "name": "app_role", + "schema": "public", + "values": [ + "borrower", + "lender", + "admin" + ] + }, + "public.kyc_status": { + "name": "kyc_status", + "schema": "public", + "values": [ + "pending", + "submitted", + "verified", + "rejected" + ] + }, + "public.loan_status": { + "name": "loan_status", + "schema": "public", + "values": [ + "requested", + "approved", + "funded", + "active", + "repaid", + "defaulted", + "cancelled" + ] + }, + "public.pool_status": { + "name": "pool_status", + "schema": "public", + "values": [ + "active", + "paused", + "closed" + ] + }, + "public.position_status": { + "name": "position_status", + "schema": "public", + "values": [ + "active", + "closed" + ] + }, + "public.referral_status": { + "name": "referral_status", + "schema": "public", + "values": [ + "pending", + "qualified", + "paid", + "rejected" + ] + }, + "public.risk_decision": { + "name": "risk_decision", + "schema": "public", + "values": [ + "allow", + "manual_review", + "reject" + ] + }, + "public.risk_status": { + "name": "risk_status", + "schema": "public", + "values": [ + "low", + "medium", + "high", + "blocked" + ] + }, + "public.task_difficulty": { + "name": "task_difficulty", + "schema": "public", + "values": [ + "easy", + "medium", + "hard" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "open", + "assigned", + "completed", + "verified", + "cancelled" + ] + }, + "public.tx_status": { + "name": "tx_status", + "schema": "public", + "values": [ + "pending", + "confirmed", + "failed", + "cancelled" + ] + }, + "public.verification_status": { + "name": "verification_status", + "schema": "public", + "values": [ + "pending", + "verified", + "rejected", + "expired" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..0a238e4 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,2822 @@ +{ + "id": "0df67a3b-d5f4-42d0-9568-0f32a4f5e11a", + "prevId": "7651aee8-f11d-4065-a209-a2f45ddb9496", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.chain_events": { + "name": "chain_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contract_id": { + "name": "contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "happened_at": { + "name": "happened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chain_events_tx_hash_event_type_key": { + "name": "chain_events_tx_hash_event_type_key", + "columns": [ + { + "expression": "tx_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_chain_events_contract_id": { + "name": "idx_chain_events_contract_id", + "columns": [ + { + "expression": "contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_chain_events_happened_at": { + "name": "idx_chain_events_happened_at", + "columns": [ + { + "expression": "happened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_verifications": { + "name": "external_verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_type": { + "name": "verification_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "verification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payload_meta": { + "name": "payload_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_external_verifications_user_id": { + "name": "idx_external_verifications_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_external_verifications_status": { + "name": "idx_external_verifications_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "external_verifications_user_id_profiles_id_fk": { + "name": "external_verifications_user_id_profiles_id_fk", + "tableFrom": "external_verifications", + "columnsFrom": [ + "user_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fraud_signals": { + "name": "fraud_signals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "signal_type": { + "name": "signal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "resolved": { + "name": "resolved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_fraud_signals_user_id_created_at": { + "name": "idx_fraud_signals_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_fraud_signals_resolved": { + "name": "idx_fraud_signals_resolved", + "columns": [ + { + "expression": "resolved", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "fraud_signals_user_id_profiles_id_fk": { + "name": "fraud_signals_user_id_profiles_id_fk", + "tableFrom": "fraud_signals", + "columnsFrom": [ + "user_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ledger_transactions": { + "name": "ledger_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'XLM'" + }, + "status": { + "name": "status", + "type": "tx_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "ref_type": { + "name": "ref_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ref_id": { + "name": "ref_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ledger_transactions_user_id_created_at": { + "name": "idx_ledger_transactions_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_ledger_transactions_status": { + "name": "idx_ledger_transactions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "ledger_transactions_user_id_profiles_id_fk": { + "name": "ledger_transactions_user_id_profiles_id_fk", + "tableFrom": "ledger_transactions", + "columnsFrom": [ + "user_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lending_pools": { + "name": "lending_pools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "pool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'XLM'" + }, + "apr_bps": { + "name": "apr_bps", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_liquidity": { + "name": "total_liquidity", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "available_liquidity": { + "name": "available_liquidity", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_borrowed": { + "name": "total_borrowed", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "borrow_cap": { + "name": "borrow_cap", + "type": "numeric(20, 7)", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_lending_pools_status": { + "name": "idx_lending_pools_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_lending_pools_status_available": { + "name": "idx_lending_pools_status_available", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_liquidity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_lending_pools_created_at_desc": { + "name": "idx_lending_pools_created_at_desc", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "lending_pools_created_by_profiles_id_fk": { + "name": "lending_pools_created_by_profiles_id_fk", + "tableFrom": "lending_pools", + "columnsFrom": [ + "created_by" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.loan_fundings": { + "name": "loan_fundings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "loan_id": { + "name": "loan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "lender_id": { + "name": "lender_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lender_address": { + "name": "lender_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "funded_at": { + "name": "funded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_loan_fundings_loan_id": { + "name": "idx_loan_fundings_loan_id", + "columns": [ + { + "expression": "loan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_loan_fundings_lender_id": { + "name": "idx_loan_fundings_lender_id", + "columns": [ + { + "expression": "lender_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_loan_fundings_lender_loan": { + "name": "idx_loan_fundings_lender_loan", + "columns": [ + { + "expression": "lender_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "loan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_loan_fundings_tx_hash": { + "name": "idx_loan_fundings_tx_hash", + "columns": [ + { + "expression": "tx_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "loan_fundings_loan_id_loans_id_fk": { + "name": "loan_fundings_loan_id_loans_id_fk", + "tableFrom": "loan_fundings", + "columnsFrom": [ + "loan_id" + ], + "tableTo": "loans", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "loan_fundings_lender_id_profiles_id_fk": { + "name": "loan_fundings_lender_id_profiles_id_fk", + "tableFrom": "loan_fundings", + "columnsFrom": [ + "lender_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.loan_repayments": { + "name": "loan_repayments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "loan_id": { + "name": "loan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "payer_id": { + "name": "payer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tx_ref": { + "name": "tx_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_loan_repayments_loan_id": { + "name": "idx_loan_repayments_loan_id", + "columns": [ + { + "expression": "loan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_loan_repayments_payer_id": { + "name": "idx_loan_repayments_payer_id", + "columns": [ + { + "expression": "payer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "loan_repayments_loan_id_loans_id_fk": { + "name": "loan_repayments_loan_id_loans_id_fk", + "tableFrom": "loan_repayments", + "columnsFrom": [ + "loan_id" + ], + "tableTo": "loans", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "loan_repayments_payer_id_profiles_id_fk": { + "name": "loan_repayments_payer_id_profiles_id_fk", + "tableFrom": "loan_repayments", + "columnsFrom": [ + "payer_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.loans": { + "name": "loans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "borrower_id": { + "name": "borrower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pool_id": { + "name": "pool_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "loan_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "principal_amount": { + "name": "principal_amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true + }, + "apr_bps": { + "name": "apr_bps", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_days": { + "name": "duration_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rate_model": { + "name": "rate_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "rate_switch_count": { + "name": "rate_switch_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_rate_switch_at": { + "name": "last_rate_switch_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "funded_amount": { + "name": "funded_amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "repaid_amount": { + "name": "repaid_amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "funded_at": { + "name": "funded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "defaulted_at": { + "name": "defaulted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_loans_borrower_id": { + "name": "idx_loans_borrower_id", + "columns": [ + { + "expression": "borrower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_loans_pool_id": { + "name": "idx_loans_pool_id", + "columns": [ + { + "expression": "pool_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_loans_status": { + "name": "idx_loans_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_loans_due_at": { + "name": "idx_loans_due_at", + "columns": [ + { + "expression": "due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_loans_borrower_status": { + "name": "idx_loans_borrower_status", + "columns": [ + { + "expression": "borrower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_loans_rate_model": { + "name": "idx_loans_rate_model", + "columns": [ + { + "expression": "rate_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_loans_status_funded": { + "name": "idx_loans_status_funded", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "funded_amount", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "loans_borrower_id_profiles_id_fk": { + "name": "loans_borrower_id_profiles_id_fk", + "tableFrom": "loans", + "columnsFrom": [ + "borrower_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "loans_pool_id_lending_pools_id_fk": { + "name": "loans_pool_id_lending_pools_id_fk", + "tableFrom": "loans", + "columnsFrom": [ + "pool_id" + ], + "tableTo": "lending_pools", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_notifications_user_id_created_at": { + "name": "idx_notifications_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pool_positions": { + "name": "pool_positions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "pool_id": { + "name": "pool_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "lender_id": { + "name": "lender_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "position_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "principal_amount": { + "name": "principal_amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true + }, + "earned_interest": { + "name": "earned_interest", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "withdrawn_amount": { + "name": "withdrawn_amount", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_pool_positions_lender_id": { + "name": "idx_pool_positions_lender_id", + "columns": [ + { + "expression": "lender_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_pool_positions_pool_id": { + "name": "idx_pool_positions_pool_id", + "columns": [ + { + "expression": "pool_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "pool_positions_pool_id_lending_pools_id_fk": { + "name": "pool_positions_pool_id_lending_pools_id_fk", + "tableFrom": "pool_positions", + "columnsFrom": [ + "pool_id" + ], + "tableTo": "lending_pools", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pool_positions_lender_id_profiles_id_fk": { + "name": "pool_positions_lender_id_profiles_id_fk", + "tableFrom": "pool_positions", + "columnsFrom": [ + "lender_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "role": { + "name": "role", + "type": "app_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'borrower'" + }, + "wallet_address": { + "name": "wallet_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date_of_birth": { + "name": "date_of_birth", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "kyc_status": { + "name": "kyc_status", + "type": "kyc_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "risk_status": { + "name": "risk_status", + "type": "risk_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "government_id_ipfs_hash": { + "name": "government_id_ipfs_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "government_id_url": { + "name": "government_id_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kyc_submitted_at": { + "name": "kyc_submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "kyc_verified_at": { + "name": "kyc_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "kyc_rejection_reason": { + "name": "kyc_rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kyc_provider_id": { + "name": "kyc_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kyc_provider_status": { + "name": "kyc_provider_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "regulated_pool_access": { + "name": "regulated_pool_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_profiles_role": { + "name": "idx_profiles_role", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_profiles_wallet_address": { + "name": "idx_profiles_wallet_address", + "columns": [ + { + "expression": "wallet_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_profiles_kyc_status": { + "name": "idx_profiles_kyc_status", + "columns": [ + { + "expression": "kyc_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_profiles_risk_status": { + "name": "idx_profiles_risk_status", + "columns": [ + { + "expression": "risk_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_profiles_kyc_submitted_at": { + "name": "idx_profiles_kyc_submitted_at", + "columns": [ + { + "expression": "kyc_submitted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "profiles_referral_code_key": { + "name": "profiles_referral_code_key", + "columns": [ + { + "expression": "referral_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_profiles_kyc_provider_id": { + "name": "idx_profiles_kyc_provider_id", + "columns": [ + { + "expression": "kyc_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"profiles\".\"kyc_provider_id\" is not null", + "concurrently": false + }, + "idx_profiles_regulated_pool_access": { + "name": "idx_profiles_regulated_pool_access", + "columns": [ + { + "expression": "regulated_pool_access", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "profiles_id_users_id_fk": { + "name": "profiles_id_users_id_fk", + "tableFrom": "profiles", + "columnsFrom": [ + "id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referrals": { + "name": "referrals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "referrer_id": { + "name": "referrer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "referee_id": { + "name": "referee_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "referral_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "qualifying_loan_id": { + "name": "qualifying_loan_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "bonus_amount": { + "name": "bonus_amount", + "type": "numeric(20, 7)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "payout_tx_hash": { + "name": "payout_tx_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "qualified_at": { + "name": "qualified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "referrals_referee_id_key": { + "name": "referrals_referee_id_key", + "columns": [ + { + "expression": "referee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_referrals_referrer_id": { + "name": "idx_referrals_referrer_id", + "columns": [ + { + "expression": "referrer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_referrals_status": { + "name": "idx_referrals_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_referrals_referrer_status": { + "name": "idx_referrals_referrer_status", + "columns": [ + { + "expression": "referrer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "referrals_referrer_id_profiles_id_fk": { + "name": "referrals_referrer_id_profiles_id_fk", + "tableFrom": "referrals", + "columnsFrom": [ + "referrer_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "referrals_referee_id_profiles_id_fk": { + "name": "referrals_referee_id_profiles_id_fk", + "tableFrom": "referrals", + "columnsFrom": [ + "referee_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "referrals_qualifying_loan_id_loans_id_fk": { + "name": "referrals_qualifying_loan_id_loans_id_fk", + "tableFrom": "referrals", + "columnsFrom": [ + "qualifying_loan_id" + ], + "tableTo": "loans", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reputation_events": { + "name": "reputation_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "points_delta": { + "name": "points_delta", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_rep_events_user_id_created_at": { + "name": "idx_rep_events_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_rep_events_source": { + "name": "idx_rep_events_source", + "columns": [ + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_rep_events_source_key": { + "name": "idx_rep_events_source_key", + "columns": [ + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "reputation_events_user_id_profiles_id_fk": { + "name": "reputation_events_user_id_profiles_id_fk", + "tableFrom": "reputation_events", + "columnsFrom": [ + "user_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reputation_snapshots": { + "name": "reputation_snapshots", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "score_total": { + "name": "score_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "repayment_score": { + "name": "repayment_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lending_score": { + "name": "lending_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "consistency_score": { + "name": "consistency_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "external_score": { + "name": "external_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reputation_level": { + "name": "reputation_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'bronze'" + }, + "score_breakdown": { + "name": "score_breakdown", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reputation_snapshots_user_id_profiles_id_fk": { + "name": "reputation_snapshots_user_id_profiles_id_fk", + "tableFrom": "reputation_snapshots", + "columnsFrom": [ + "user_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_assessments": { + "name": "risk_assessments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "risk_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "assessed_at": { + "name": "assessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_risk_assessments_user_id_assessed_at": { + "name": "idx_risk_assessments_user_id_assessed_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assessed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "risk_assessments_user_id_profiles_id_fk": { + "name": "risk_assessments_user_id_profiles_id_fk", + "tableFrom": "risk_assessments", + "columnsFrom": [ + "user_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "creator_id": { + "name": "creator_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "assigned_to": { + "name": "assigned_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_xlm": { + "name": "reward_xlm", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "difficulty": { + "name": "difficulty", + "type": "task_difficulty", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'easy'" + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "completion_deadline": { + "name": "completion_deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completion_date": { + "name": "completion_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "proof_submission": { + "name": "proof_submission", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_rating": { + "name": "creator_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_tasks_creator_id": { + "name": "idx_tasks_creator_id", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_tasks_assigned_to": { + "name": "idx_tasks_assigned_to", + "columns": [ + { + "expression": "assigned_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_tasks_status": { + "name": "idx_tasks_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_tasks_created_at": { + "name": "idx_tasks_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tasks_creator_id_profiles_id_fk": { + "name": "tasks_creator_id_profiles_id_fk", + "tableFrom": "tasks", + "columnsFrom": [ + "creator_id" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tasks_assigned_to_profiles_id_fk": { + "name": "tasks_assigned_to_profiles_id_fk", + "tableFrom": "tasks", + "columnsFrom": [ + "assigned_to" + ], + "tableTo": "profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "wallet_address": { + "name": "wallet_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "app_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'borrower'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_sign_in_at": { + "name": "last_sign_in_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_wallet_address_key": { + "name": "users_wallet_address_key", + "columns": [ + { + "expression": "wallet_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_endpoints": { + "name": "webhook_endpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_webhook_endpoints_platform": { + "name": "idx_webhook_endpoints_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_webhook_endpoints_topic": { + "name": "idx_webhook_endpoints_topic", + "columns": [ + { + "expression": "topic", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "webhook_endpoints_created_by_users_id_fk": { + "name": "webhook_endpoints_created_by_users_id_fk", + "tableFrom": "webhook_endpoints", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.app_role": { + "name": "app_role", + "schema": "public", + "values": [ + "borrower", + "lender", + "admin" + ] + }, + "public.kyc_status": { + "name": "kyc_status", + "schema": "public", + "values": [ + "pending", + "submitted", + "verified", + "rejected" + ] + }, + "public.loan_status": { + "name": "loan_status", + "schema": "public", + "values": [ + "requested", + "approved", + "funded", + "active", + "repaid", + "defaulted", + "cancelled" + ] + }, + "public.pool_status": { + "name": "pool_status", + "schema": "public", + "values": [ + "active", + "paused", + "closed" + ] + }, + "public.position_status": { + "name": "position_status", + "schema": "public", + "values": [ + "active", + "closed" + ] + }, + "public.referral_status": { + "name": "referral_status", + "schema": "public", + "values": [ + "pending", + "qualified", + "paid", + "rejected" + ] + }, + "public.risk_decision": { + "name": "risk_decision", + "schema": "public", + "values": [ + "allow", + "manual_review", + "reject" + ] + }, + "public.risk_status": { + "name": "risk_status", + "schema": "public", + "values": [ + "low", + "medium", + "high", + "blocked" + ] + }, + "public.task_difficulty": { + "name": "task_difficulty", + "schema": "public", + "values": [ + "easy", + "medium", + "hard" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "open", + "assigned", + "completed", + "verified", + "cancelled" + ] + }, + "public.tx_status": { + "name": "tx_status", + "schema": "public", + "values": [ + "pending", + "confirmed", + "failed", + "cancelled" + ] + }, + "public.verification_status": { + "name": "verification_status", + "schema": "public", + "values": [ + "pending", + "verified", + "rejected", + "expired" + ] + } + }, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..799bf34 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1789217087472, + "tag": "0000_init", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1789217089648, + "tag": "0001_functions_and_triggers", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/lib/analytics.ts b/lib/analytics.ts index 099c1c5..30d5761 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -1,4 +1,5 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import type { AnyDb } from "@/lib/db/pools"; +import { ledgerTransactions, loanRepayments, loans, poolPositions } from "@/lib/db/schema"; const DAY_MS = 24 * 60 * 60 * 1_000; export const ANALYTICS_CACHE_TTL_SECONDS = 60 * 60; @@ -70,13 +71,14 @@ export function aggregatePlatformAnalytics( ledgerTransactions?: LedgerTransactionRow[] | null; }, activeWindowMs: number = ANALYTICS_ACTIVE_WINDOW_MS, + now: number = Date.now(), ): PlatformAnalyticsMetrics { const loans = normalizeRows(rows.loans); const poolPositions = normalizeRows(rows.poolPositions); const loanRepayments = normalizeRows(rows.loanRepayments); const ledgerTransactions = normalizeRows(rows.ledgerTransactions); - const activeWindowStart = Date.now() - activeWindowMs; + const activeWindowStart = now - activeWindowMs; const tvlFromLoans = loans .filter((loan) => isActiveLoanStatus(loan.status)) @@ -110,31 +112,32 @@ export function aggregatePlatformAnalytics( }; } -export async function fetchPlatformAnalytics( - supabase: SupabaseClient, -): Promise { - const [loansRes, poolPositionsRes, loanRepaymentsRes, ledgerTransactionsRes] = await Promise.all([ - supabase.from("loans").select("principal_amount,status"), - supabase.from("pool_positions").select("principal_amount,earned_interest,status"), - supabase.from("loan_repayments").select("amount"), - supabase.from("ledger_transactions").select("amount,user_id,status,created_at"), +export async function fetchPlatformAnalytics(db: AnyDb): Promise { + const [loanRows, positionRows, repaymentRows, ledgerRows] = await Promise.all([ + db.select({ principal_amount: loans.principalAmount, status: loans.status }).from(loans), + db + .select({ + principal_amount: poolPositions.principalAmount, + earned_interest: poolPositions.earnedInterest, + status: poolPositions.status, + }) + .from(poolPositions), + db.select({ amount: loanRepayments.amount }).from(loanRepayments), + db + .select({ + amount: ledgerTransactions.amount, + user_id: ledgerTransactions.userId, + status: ledgerTransactions.status, + created_at: ledgerTransactions.createdAt, + }) + .from(ledgerTransactions), ]); - const firstError = - loansRes.error ?? - poolPositionsRes.error ?? - loanRepaymentsRes.error ?? - ledgerTransactionsRes.error; - - if (firstError) { - throw new Error(firstError.message); - } - return aggregatePlatformAnalytics({ - loans: loansRes.data, - poolPositions: poolPositionsRes.data, - loanRepayments: loanRepaymentsRes.data, - ledgerTransactions: ledgerTransactionsRes.data, + loans: loanRows, + poolPositions: positionRows, + loanRepayments: repaymentRows, + ledgerTransactions: ledgerRows, }); } diff --git a/lib/auth/kyc.ts b/lib/auth/kyc.ts deleted file mode 100644 index d28c053..0000000 --- a/lib/auth/kyc.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * KYC (Know Your Customer) verification helpers - * Manages identity verification status and document storage - */ - -import { getServerSupabaseClient } from "@/lib/supabase/server"; - -export type KYCStatus = "pending" | "submitted" | "verified" | "rejected"; - -export interface KYCData { - status: KYCStatus; - government_id_ipfs_hash?: string; - government_id_url?: string; - submitted_at?: string; - verified_at?: string; - rejection_reason?: string; -} - -/** - * Store KYC document hash after IPFS upload - */ -export async function storeKYCDocument( - userId: string, - ipfsHash: string, - ipfsUrl: string -): Promise { - const supabase = await getServerSupabaseClient(); - if (!supabase) throw new Error("Supabase not available"); - - const { error } = await supabase - .from("profiles") - .update({ - government_id_ipfs_hash: ipfsHash, - government_id_url: ipfsUrl, - kyc_status: "submitted", - kyc_submitted_at: new Date().toISOString(), - }) - .eq("id", userId); - - if (error) throw error; -} - -/** - * Get KYC data for a user (admin only) - */ -export async function getKYCData(userId: string): Promise { - const supabase = await getServerSupabaseClient(); - if (!supabase) throw new Error("Supabase not available"); - - const { data, error } = await supabase - .from("profiles") - .select( - "kyc_status, government_id_ipfs_hash, government_id_url, kyc_submitted_at, kyc_verified_at, kyc_rejection_reason" - ) - .eq("id", userId) - .maybeSingle(); - - if (error) throw error; - - return data - ? { - status: data.kyc_status as KYCStatus, - government_id_ipfs_hash: data.government_id_ipfs_hash, - government_id_url: data.government_id_url, - submitted_at: data.kyc_submitted_at, - verified_at: data.kyc_verified_at, - rejection_reason: data.kyc_rejection_reason, - } - : null; -} - -/** - * Check if user is verified (admin checker) - */ -export async function isUserVerified(userId: string): Promise { - const kycData = await getKYCData(userId); - return kycData?.status === "verified"; -} diff --git a/lib/auth/session-token.ts b/lib/auth/session-token.ts new file mode 100644 index 0000000..563d19c --- /dev/null +++ b/lib/auth/session-token.ts @@ -0,0 +1,80 @@ +/** + * lib/auth/session-token.ts + * + * Stateless session tokens. A signed JWT (HS256, `jose`) is stored in an + * HttpOnly cookie after a successful SEP-10 sign-in. It carries only what the + * edge proxy needs to route requests (user id, wallet, role); everything else + * is looked up in the database by `lib/auth/session.ts`. + * + * This module is Edge-safe (no Node-only APIs) so proxy.ts can verify tokens + * without a database round-trip. + */ + +import { jwtVerify, SignJWT } from "jose"; +import type { UserRole } from "@/lib/auth/roles"; + +export const SESSION_COOKIE_NAME = "tl_session"; + +/** Session lifetime in seconds (7 days). */ +export const SESSION_TTL_SECONDS = 7 * 24 * 60 * 60; + +export interface SessionClaims { + /** users.id */ + sub: string; + /** Stellar public key (G...) */ + wallet: string; + role: UserRole; +} + +function secretKey(): Uint8Array { + const secret = process.env.SESSION_SECRET; + if (!secret || secret.length < 32) { + throw new Error( + "SESSION_SECRET is missing or shorter than 32 characters. Generate one with `openssl rand -base64 48`.", + ); + } + return new TextEncoder().encode(secret); +} + +/** True when a session secret is configured. */ +export function isSessionSigningConfigured(): boolean { + const secret = process.env.SESSION_SECRET; + return typeof secret === "string" && secret.length >= 32; +} + +export async function signSessionToken(claims: SessionClaims): Promise { + return new SignJWT({ wallet: claims.wallet, role: claims.role }) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setSubject(claims.sub) + .setIssuedAt() + .setExpirationTime(`${SESSION_TTL_SECONDS}s`) + .sign(secretKey()); +} + +/** Verify a token; returns null for anything invalid or expired. */ +export async function verifySessionToken(token: string | undefined | null): Promise { + if (!token) return null; + if (!isSessionSigningConfigured()) return null; + try { + const { payload } = await jwtVerify(token, secretKey(), { algorithms: ["HS256"] }); + const sub = payload.sub; + const wallet = payload.wallet; + const role = payload.role; + if (typeof sub !== "string" || typeof wallet !== "string") return null; + const safeRole: UserRole = role === "lender" || role === "admin" ? role : "borrower"; + return { sub, wallet, role: safeRole }; + } catch { + return null; + } +} + +/** Cookie attributes shared by set and clear so the browser matches them. */ +export function sessionCookieOptions() { + return { + httpOnly: true, + sameSite: "lax" as const, + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: SESSION_TTL_SECONDS, + }; +} diff --git a/lib/auth/session.ts b/lib/auth/session.ts index b152e7e..4b9216c 100644 --- a/lib/auth/session.ts +++ b/lib/auth/session.ts @@ -1,41 +1,155 @@ +/** + * lib/auth/session.ts + * + * Server-side session helpers for pages, server actions and API routes. + * + * getSessionUser() → SessionUser | null + * requireAuthenticatedUser() → SessionUser (redirects to /auth otherwise) + * requireApiUser() → SessionUser (throws UnauthorizedError — for JSON routes) + * requireTradeVaultAdmin() → SessionUser (allowlisted e-mail/wallet AND profiles.role = admin) + * + * The session cookie holds a signed JWT (see session-token.ts). We still read + * the user row on every call so a deleted or re-roled user is reflected + * immediately rather than at token expiry. + */ + +import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; -import { - getDashboardPath, - normalizeUserRole, - type UserRole, -} from "@/lib/auth/roles"; -import { getServerSupabaseClient } from "@/lib/supabase/server"; - -export async function requireAuthenticatedUser(expectedRole?: UserRole) { - const supabase = await getServerSupabaseClient(); +import { eq } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { profiles, users } from "@/lib/db/schema"; +import { getDashboardPath, normalizeUserRole, type UserRole } from "@/lib/auth/roles"; +import { SESSION_COOKIE_NAME, verifySessionToken } from "@/lib/auth/session-token"; + +/** The authenticated identity as seen by the application layer. */ +export interface SessionUser { + id: string; + walletAddress: string; + role: UserRole; + /** Display name from the profile ("" when the user never set one). */ + fullName: string; + /** Optional contact e-mail (SIWS users have none unless they add one). */ + email: string | null; + createdAt: string; + lastSignInAt: string | null; +} - if (!supabase) { - redirect("/auth"); +export class UnauthorizedError extends Error { + constructor(message = "Not authenticated") { + super(message); + this.name = "UnauthorizedError"; } +} + +const DEV_BYPASS_ENABLED = + process.env.NODE_ENV !== "production" && process.env.ENABLE_DEV_AUTH_BYPASS === "true"; + +function isValidUuid(value: string): boolean { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); +} - const { - data: { user }, - } = await supabase.auth.getUser(); +/** + * Local-dev / e2e escape hatch: `x-dev-user-id` + `x-dev-role` headers act as + * the session. Only honoured outside production with ENABLE_DEV_AUTH_BYPASS. + */ +async function devBypassUser(): Promise { + if (!DEV_BYPASS_ENABLED) return null; + const h = await headers(); + const id = h.get("x-dev-user-id")?.trim() ?? ""; + if (!id || !isValidUuid(id)) return null; + const role = normalizeUserRole(h.get("x-dev-role")?.trim()); + const db = getDb(); + const row = db + ? await db + .select({ wallet: users.walletAddress, fullName: profiles.fullName, email: users.email }) + .from(users) + .leftJoin(profiles, eq(profiles.id, users.id)) + .where(eq(users.id, id)) + .limit(1) + .then((r) => r[0]) + : undefined; + return { + id, + walletAddress: row?.wallet ?? "", + role, + fullName: row?.fullName ?? "Dev User", + email: row?.email ?? null, + createdAt: new Date(0).toISOString(), + lastSignInAt: null, + }; +} + +/** Resolve the current user from the session cookie, or null. */ +export async function getSessionUser(): Promise { + const bypass = await devBypassUser(); + if (bypass) return bypass; + + const cookieStore = await cookies(); + const claims = await verifySessionToken(cookieStore.get(SESSION_COOKIE_NAME)?.value); + if (!claims) return null; + + const db = getDb(); + if (!db) return null; + + const [row] = await db + .select({ + id: users.id, + walletAddress: users.walletAddress, + role: users.role, + email: users.email, + createdAt: users.createdAt, + lastSignInAt: users.lastSignInAt, + fullName: profiles.fullName, + }) + .from(users) + .leftJoin(profiles, eq(profiles.id, users.id)) + .where(eq(users.id, claims.sub)) + .limit(1); + + if (!row) return null; + + return { + id: row.id, + walletAddress: row.walletAddress, + role: normalizeUserRole(row.role), + fullName: row.fullName ?? "", + email: row.email ?? null, + createdAt: row.createdAt.toISOString(), + lastSignInAt: row.lastSignInAt ? row.lastSignInAt.toISOString() : null, + }; +} +/** + * For server components / actions: redirect to /auth when signed out, or to + * the user's own dashboard when they are the wrong role for this page. + */ +export async function requireAuthenticatedUser(expectedRole?: UserRole): Promise<{ + user: SessionUser; + role: UserRole; +}> { + const user = await getSessionUser(); if (!user) { redirect("/auth"); } - - const role = normalizeUserRole(user.user_metadata?.account_type); - - if (expectedRole && role !== expectedRole) { - redirect(getDashboardPath(role)); + if (expectedRole && user.role !== expectedRole) { + redirect(getDashboardPath(user.role)); } - - return { user, role }; + return { user, role: user.role }; } -function parseAllowedAdminEmails(): Set { - const value = process.env.TRADE_VAULT_ADMIN_EMAILS; - if (!value) { - return new Set(); +/** For API routes: throw instead of redirecting so the caller can return JSON. */ +export async function requireApiUser(expectedRole?: UserRole): Promise { + const user = await getSessionUser(); + if (!user) throw new UnauthorizedError(); + if (expectedRole && user.role !== expectedRole) { + throw new UnauthorizedError(`This action requires the ${expectedRole} role.`); } + return user; +} +function parseAllowedAdmins(): Set { + const value = process.env.TRADE_VAULT_ADMIN_EMAILS; + if (!value) return new Set(); return new Set( value .split(",") @@ -44,39 +158,42 @@ function parseAllowedAdminEmails(): Set { ); } -export function isTradeVaultAdminUser(user: { - email?: string; - app_metadata?: Record; - user_metadata?: Record; -}) { - const allowedAdmins = parseAllowedAdminEmails(); +/** + * Admin allowlist check. TRADE_VAULT_ADMIN_EMAILS accepts e-mails and Stellar + * addresses (comma-separated) so wallet-only accounts can be admins too. + */ +export function isTradeVaultAdminUser(user: Pick): boolean { + const allowed = parseAllowedAdmins(); const email = user.email?.toLowerCase() ?? ""; - - // Do not trust user/app metadata claims for admin access. - // Only allowlisted email + DB role check in requireTradeVaultAdmin grants access. - return allowedAdmins.has(email); + const wallet = user.walletAddress?.toLowerCase() ?? ""; + return (email !== "" && allowed.has(email)) || (wallet !== "" && allowed.has(wallet)); } -export async function requireTradeVaultAdmin() { +/** Allowlisted AND profiles.role = 'admin' — both are required. */ +export async function requireTradeVaultAdmin(): Promise<{ user: SessionUser; role: UserRole }> { const { user, role } = await requireAuthenticatedUser(); - const emailAllowlisted = isTradeVaultAdminUser(user); - - const supabase = await getServerSupabaseClient(); - if (!supabase) { - redirect(getDashboardPath(normalizeUserRole(role))); - } - const { data: profile } = await supabase - .from("profiles") - .select("role") - .eq("id", user.id) - .maybeSingle(); + const db = getDb(); + const [profile] = db + ? await db.select({ role: profiles.role }).from(profiles).where(eq(profiles.id, user.id)).limit(1) + : []; const dbAdmin = profile?.role === "admin"; - - if (!emailAllowlisted || !dbAdmin) { + if (!isTradeVaultAdminUser(user) || !dbAdmin) { redirect(getDashboardPath(normalizeUserRole(role))); } - return { user, role }; -} \ No newline at end of file +} + +/** API-route flavour of requireTradeVaultAdmin (throws instead of redirecting). */ +export async function requireApiAdmin(): Promise { + const user = await requireApiUser(); + const db = getDb(); + const [profile] = db + ? await db.select({ role: profiles.role }).from(profiles).where(eq(profiles.id, user.id)).limit(1) + : []; + if (!isTradeVaultAdminUser(user) || profile?.role !== "admin") { + throw new UnauthorizedError("Admin access required."); + } + return user; +} diff --git a/lib/auth/siws-client.ts b/lib/auth/siws-client.ts index 33a507c..03a62e6 100644 --- a/lib/auth/siws-client.ts +++ b/lib/auth/siws-client.ts @@ -4,11 +4,10 @@ * lib/auth/siws-client.ts * * Browser side of Sign-In with Stellar (SEP-0010): - * connect wallet → fetch challenge → sign with Freighter/Albedo → - * verify on the backend → adopt the returned Supabase session. + * connect wallet → fetch challenge → sign with the wallet → + * verify on the backend (which sets the HttpOnly session cookie). */ -import { getBrowserSupabaseClient } from "@/lib/supabase/client"; import { getConnectedWallet, signTransactionWithWallet, @@ -45,11 +44,6 @@ export async function signInWithStellar( preferredProvider?: StellarWalletProvider, role?: UserRole ): Promise { - const supabase = getBrowserSupabaseClient(); - if (!supabase) { - throw new Error("Supabase is not configured in this environment."); - } - // 1. Connect the wallet (Freighter by default). const wallet = await getConnectedWallet(preferredProvider); const address = wallet.address; @@ -73,7 +67,7 @@ export async function signInWithStellar( provider: wallet.provider, }); - // 4. Verify + obtain a Supabase session. + // 4. Verify — on success the server sets the session cookie. const verify = await postJson("/api/auth/siws/verify", { address, signedTxXdr: signed.signedTxXdr, @@ -82,22 +76,17 @@ export async function signInWithStellar( if (!verify.res.ok) { throw new Error(mapVerifyError(verify.payload)); } - const accessToken = verify.payload.access_token as string; - const refreshToken = verify.payload.refresh_token as string; - if (!accessToken || !refreshToken) { - throw new Error("Sign-in succeeded but no session was returned."); - } - // 5. Adopt the session (persists cookies for SSR + client). - const { data, error } = await supabase.auth.setSession({ - access_token: accessToken, - refresh_token: refreshToken, - }); - if (error) { - throw new Error(`Could not establish session: ${error.message}`); - } - const normalizedRole = normalizeUserRole(data.user?.user_metadata?.account_type); - return { address, role: normalizedRole, isNewUser: Boolean(verify.payload.isNewUser) }; + return { + address, + role: normalizeUserRole(verify.payload.role), + isNewUser: Boolean(verify.payload.isNewUser), + }; +} + +/** Clear the session cookie. */ +export async function signOut(): Promise { + await fetch("/api/auth/signout", { method: "POST" }); } /** Map backend SIWS error codes to friendly, actionable messages. */ @@ -117,7 +106,7 @@ function mapVerifyError(payload: Record): string { return "The sign-in challenge was invalid. Please retry."; case "not_configured": case "session_failed": - return "Stellar sign-in is temporarily unavailable. Please try another method."; + return "Stellar sign-in is temporarily unavailable. Please try again later."; default: return fallback; } diff --git a/lib/auth/siws-server.ts b/lib/auth/siws-server.ts index c0e284e..4c57f4e 100644 --- a/lib/auth/siws-server.ts +++ b/lib/auth/siws-server.ts @@ -4,15 +4,17 @@ * Server-side Sign-In with Stellar (SEP-0010) logic: * 1. buildChallenge() — generate a signed SEP-10 challenge transaction * 2. verifyChallenge() — validate structure, expiry and the wallet signature - * 3. issueSessionForWallet() — mint a Supabase session for the wallet identity + * 3. issueSessionForWallet() — upsert the users/profiles rows for the wallet * - * SERVER-ONLY. Reads SIWS_SERVER_SECRET / SIWS_PASSWORD_SECRET / service-role key. + * SERVER-ONLY. Reads SIWS_SERVER_SECRET and the database. */ -import { createHmac } from "node:crypto"; +import { eq } from "drizzle-orm"; import { Keypair, StrKey, Transaction, WebAuth } from "@stellar/stellar-sdk"; -import { createClient, type Session } from "@supabase/supabase-js"; import { SIWS_NETWORK_PASSPHRASE, getSiwsDomain } from "@/lib/auth/siws-config"; +import { normalizeUserRole, type UserRole } from "@/lib/auth/roles"; +import { getDb } from "@/lib/db/client"; +import { profiles, users } from "@/lib/db/schema"; /** SEP-10 challenge validity window (seconds). */ const CHALLENGE_TIMEOUT_SECS = 300; @@ -166,89 +168,74 @@ export function verifyChallenge(signedTxXdr: string, expectedAddress: string): s return expectedAddress; } -// ─── 3. Issue a Supabase session for the wallet identity ────────────────────── +// ─── 3. Provision the wallet identity ───────────────────────────────────────── -/** Deterministic e-mail identity for a wallet (never receives real mail). */ -export function walletEmail(address: string): string { - return `${address.toLowerCase()}@siws.trustlend.app`; +export interface WalletIdentity { + userId: string; + role: UserRole; + isNewUser: boolean; } /** - * Server-derived, deterministic password for the wallet's Supabase user. - * Never leaves the server — used only to mint a session after SEP-10 passes. + * Ensure a `users` + `profiles` row exists for `address` and return the + * identity the API route turns into a session cookie. + * + * Idempotent: signing in again just bumps `last_sign_in_at`. The role chosen on + * the auth page only applies to brand-new accounts — an existing account keeps + * whatever role it already has. */ -function walletPassword(address: string): string { - const secret = process.env.SIWS_PASSWORD_SECRET; - if (!secret) { - throw new SiwsError( - "not_configured", - "SIWS is not configured on the server (SIWS_PASSWORD_SECRET missing).", - 503 - ); +export async function issueSessionForWallet(address: string, role?: string): Promise { + const db = getDb(); + if (!db) { + throw new SiwsError("session_failed", "Database is not configured.", 503); } - return createHmac("sha256", secret).update(address).digest("hex"); -} - -function adminClient() { - const url = process.env.NEXT_PUBLIC_SUPABASE_URL; - const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_KEY; - if (!url || !key) { - throw new SiwsError("session_failed", "Supabase service role is not configured.", 503); + const requestedRole: UserRole = role === "lender" ? "lender" : "borrower"; + const now = new Date(); + + const [existing] = await db + .select({ id: users.id, role: users.role }) + .from(users) + .where(eq(users.walletAddress, address)) + .limit(1); + + if (existing) { + await db.update(users).set({ lastSignInAt: now }).where(eq(users.id, existing.id)); + // Heal a missing profile row (e.g. a partially failed first sign-in). + await db + .insert(profiles) + .values({ id: existing.id, role: existing.role, walletAddress: address, fullName: shortName(address) }) + .onConflictDoNothing(); + return { userId: existing.id, role: normalizeUserRole(existing.role), isNewUser: false }; } - return createClient(url, key, { auth: { autoRefreshToken: false, persistSession: false } }); -} -/** - * Ensure a Supabase user exists for `address` and return a fresh session - * (access + refresh tokens) the client can adopt via `auth.setSession`. - */ -export async function issueSessionForWallet(address: string, role?: string): Promise<{ - session: Session; - isNewUser: boolean; -}> { - const email = walletEmail(address); - const password = walletPassword(address); - const admin = adminClient(); - const accountType = (role === "borrower" || role === "lender") ? role : "borrower"; - - // Create the wallet user if it doesn't exist yet (idempotent). - let isNewUser = false; - const { error: createErr } = await admin.auth.admin.createUser({ - email, - password, - email_confirm: true, - user_metadata: { - wallet_address: address, - account_type: accountType, - auth_method: "siws", - full_name: `Stellar ${address.slice(0, 4)}…${address.slice(-4)}`, - }, - }); - - if (createErr) { - const msg = createErr.message?.toLowerCase() ?? ""; - const alreadyExists = - msg.includes("already been registered") || - msg.includes("already registered") || - msg.includes("email_exists") || - (createErr as { code?: string }).code === "email_exists"; - if (!alreadyExists) { - throw new SiwsError("session_failed", `Could not provision wallet account: ${createErr.message}`, 500); + const [created] = await db + .insert(users) + .values({ walletAddress: address, role: requestedRole, lastSignInAt: now }) + .onConflictDoNothing({ target: users.walletAddress }) + .returning({ id: users.id, role: users.role }); + + if (!created) { + // Lost a race with a concurrent first sign-in for the same wallet. + const [raced] = await db + .select({ id: users.id, role: users.role }) + .from(users) + .where(eq(users.walletAddress, address)) + .limit(1); + if (!raced) { + throw new SiwsError("session_failed", "Could not provision wallet account.", 500); } - } else { - isNewUser = true; + return { userId: raced.id, role: normalizeUserRole(raced.role), isNewUser: false }; } - // Mint a session with the deterministic password (never returned to client). - const url = process.env.NEXT_PUBLIC_SUPABASE_URL!; - const anon = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; - const authClient = createClient(url, anon, { - auth: { autoRefreshToken: false, persistSession: false }, - }); - const { data, error } = await authClient.auth.signInWithPassword({ email, password }); - if (error || !data.session) { - throw new SiwsError("session_failed", `Failed to issue session: ${error?.message ?? "no session"}`, 500); - } + await db + .insert(profiles) + .values({ id: created.id, role: requestedRole, walletAddress: address, fullName: shortName(address) }) + .onConflictDoNothing(); + + return { userId: created.id, role: requestedRole, isNewUser: true }; +} - return { session: data.session, isNewUser }; +/** "Stellar GABC…WXYZ" — the default display name for a wallet-only account. */ +function shortName(address: string): string { + return `Stellar ${address.slice(0, 4)}…${address.slice(-4)}`; } diff --git a/lib/commitlint/validate-commit.test.ts b/lib/commitlint/validate-commit.test.ts index bd57f08..76e78a7 100644 --- a/lib/commitlint/validate-commit.test.ts +++ b/lib/commitlint/validate-commit.test.ts @@ -22,7 +22,7 @@ const ALLOWED_TYPES = [ const ALLOWED_SCOPES = [ "lending", "escrow", "governance", "default-management", "multisig-admin", "borrower-reputation", "auto-compound-vault", "treasury", "contracts", - "frontend", "dashboard", "auth", "kyc", "api", "ci", "db", "supabase", + "frontend", "dashboard", "auth", "kyc", "api", "ci", "db", "neon", "drizzle", "stellar", "soroban", "docs", "deps", "config", "landing", "hooks", ] as const; diff --git a/lib/dashboard/metrics.ts b/lib/dashboard/metrics.ts index b964c82..86fb56e 100644 --- a/lib/dashboard/metrics.ts +++ b/lib/dashboard/metrics.ts @@ -1,4 +1,6 @@ -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { and, eq, inArray, sql } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { ledgerTransactions, loans, poolPositions, profiles, reputationEvents } from "@/lib/db/schema"; export interface BorrowerDashboardMetrics { reputationScore: number; @@ -67,34 +69,24 @@ const ACTIVE_LOAN_STATUSES = ["active", "funded", "approved"]; export async function getBorrowerDashboardMetrics( userId: string, ): Promise { - - const supabase = await getServerSupabaseClient(); - - if (!supabase) { + const db = getDb(); + if (!db) { return { reputationScore: 0, availableCredit: 0, activeLoans: 0, pendingLoans: 0, repaymentRate: 0 }; } try { - const [eventsRes, loansRes] = await Promise.all([ - supabase - .from("reputation_events") - .select("points_delta") - .eq("user_id", userId), - supabase - .from("loans") - .select("status") - .eq("borrower_id", userId), + const [events, loanRows] = await Promise.all([ + db.select({ pointsDelta: reputationEvents.pointsDelta }).from(reputationEvents).where(eq(reputationEvents.userId, userId)), + db.select({ status: loans.status }).from(loans).where(eq(loans.borrowerId, userId)), ]); - const events = eventsRes.data ?? []; - const reputationPoints = events.reduce((sum, row) => sum + Number(row.points_delta ?? 0), 0); + const reputationPoints = events.reduce((sum, row) => sum + Number(row.pointsDelta ?? 0), 0); const reputation = Math.max(0, 250 + reputationPoints); - const loans = loansRes.data ?? []; - const pendingLoans = loans.filter((loan) => loan.status === "requested").length; - const activeLoans = loans.filter((loan) => ACTIVE_LOAN_STATUSES.includes(loan.status)).length; - const repaidLoans = loans.filter((loan) => loan.status === "repaid").length; - const defaultedLoans = loans.filter((loan) => loan.status === "defaulted").length; + const pendingLoans = loanRows.filter((loan) => loan.status === "requested").length; + const activeLoans = loanRows.filter((loan) => ACTIVE_LOAN_STATUSES.includes(loan.status)).length; + const repaidLoans = loanRows.filter((loan) => loan.status === "repaid").length; + const defaultedLoans = loanRows.filter((loan) => loan.status === "defaulted").length; const repaymentBase = repaidLoans + defaultedLoans; const repaymentRate = repaymentBase > 0 ? (repaidLoans / repaymentBase) * 100 : 100; @@ -110,67 +102,71 @@ export async function getBorrowerDashboardMetrics( } } +type LedgerMeta = { lenderUserId?: unknown; lenderAddress?: unknown; loanId?: unknown }; + +function parseMeta(raw: unknown): LedgerMeta { + if (!raw) return {}; + try { + return (typeof raw === "string" ? JSON.parse(raw) : raw) as LedgerMeta; + } catch { + return {}; + } +} + export async function getLenderDashboardMetrics( userId: string, ): Promise { - - const { getServerSupabaseClient, getServiceRoleClient } = await import("@/lib/supabase/server"); - const supabase = await getServerSupabaseClient(); - const srClient = getServiceRoleClient(); - - if (!supabase || !srClient) { + const db = getDb(); + if (!db) { return { deployedCapital: 0, totalEarnings: 0, activePositions: 0, defaultRate: 0 }; } try { // 1. Pool positions - const positionsRes = await supabase - .from("pool_positions") - .select("status, principal_amount, earned_interest") - .eq("lender_id", userId); - - const positions = positionsRes.data ?? []; - const poolDeployed = positions.reduce((s, r) => s + Number(r.principal_amount ?? 0), 0); - const poolEarnings = positions.reduce((s, r) => s + Number(r.earned_interest ?? 0), 0); + const positions = await db + .select({ + status: poolPositions.status, + principalAmount: poolPositions.principalAmount, + earnedInterest: poolPositions.earnedInterest, + }) + .from(poolPositions) + .where(eq(poolPositions.lenderId, userId)); + + const poolDeployed = positions.reduce((s, r) => s + Number(r.principalAmount ?? 0), 0); + const poolEarnings = positions.reduce((s, r) => s + Number(r.earnedInterest ?? 0), 0); const poolActive = positions.filter((r) => r.status === "active").length; - // 2. P2P Metrics - const { data: p2pFunds } = await supabase - .from("ledger_transactions") - .select("amount, ref_id") - .eq("user_id", userId) - .eq("ref_type", "loan_fund"); - - const { data: p2pRepays } = await srClient - .from("ledger_transactions") - .select("amount, metadata") - .eq("ref_type", "loan_repay"); - - const lenderRepays = (p2pRepays ?? []).filter(tx => { - try { - const meta = JSON.parse(String(tx.metadata || "{}")); - return String(meta.lenderUserId) === String(userId) || String(meta.lenderAddress) === String(userId); - } catch { return false; } + // 2. P2P metrics + const [p2pFunds, p2pRepays] = await Promise.all([ + db + .select({ amount: ledgerTransactions.amount, refId: ledgerTransactions.refId }) + .from(ledgerTransactions) + .where(and(eq(ledgerTransactions.userId, userId), eq(ledgerTransactions.refType, "loan_fund"))), + db + .select({ amount: ledgerTransactions.amount, metadata: ledgerTransactions.metadata }) + .from(ledgerTransactions) + .where(eq(ledgerTransactions.refType, "loan_repay")), + ]); + + const lenderRepays = p2pRepays.filter((tx) => { + const meta = parseMeta(tx.metadata); + return String(meta.lenderUserId) === userId || String(meta.lenderAddress) === userId; }); - // We must group by loan (ref_id) so a newly funded loan doesn't wipe out past profits! + // Group by loan so a newly funded loan doesn't wipe out past profits. const loanProfitMap = new Map(); - for (const tx of (p2pFunds ?? [])) { - const id = String(tx.ref_id); + for (const tx of p2pFunds) { + const id = String(tx.refId); const cur = loanProfitMap.get(id) ?? { deployed: 0, received: 0 }; cur.deployed += Number(tx.amount || 0); loanProfitMap.set(id, cur); } for (const tx of lenderRepays) { - let id = ""; - try { - const meta = JSON.parse(String(tx.metadata || "{}")); - id = meta.loanId ? String(meta.loanId) : ""; - } catch {} + const meta = parseMeta(tx.metadata); + const id = meta.loanId ? String(meta.loanId) : ""; if (!id) continue; - const cur = loanProfitMap.get(id) ?? { deployed: 0, received: 0 }; cur.received += Number(tx.amount || 0); loanProfitMap.set(id, cur); @@ -181,49 +177,43 @@ export async function getLenderDashboardMetrics( p2pProfit += Math.max(0, received - deployed); } - const p2pDeployed = (p2pFunds ?? []).reduce((s, t) => s + Number(t.amount || 0), 0); + const p2pDeployed = p2pFunds.reduce((s, t) => s + Number(t.amount || 0), 0); - // Get active loan count from the funded loans - const loanIds = Array.from(loanProfitMap.keys()); + const loanIds = Array.from(loanProfitMap.keys()).filter((id) => id && id !== "null"); let p2pActiveCount = 0; if (loanIds.length > 0) { - const { data: loans } = await srClient - .from("loans") - .select("status") - .in("id", loanIds); - p2pActiveCount = (loans ?? []).filter(l => l.status === "active").length; + const loanRows = await db.select({ status: loans.status }).from(loans).where(inArray(loans.id, loanIds)); + p2pActiveCount = loanRows.filter((l) => l.status === "active").length; } - const deployedCapital = poolDeployed + p2pDeployed; - const totalEarnings = poolEarnings + p2pProfit; - const activePositions = poolActive + p2pActiveCount; - const defaultRate = 0; - - return { deployedCapital, totalEarnings, activePositions, defaultRate }; + return { + deployedCapital: poolDeployed + p2pDeployed, + totalEarnings: poolEarnings + p2pProfit, + activePositions: poolActive + p2pActiveCount, + defaultRate: 0, + }; } catch { return { deployedCapital: 0, totalEarnings: 0, activePositions: 0, defaultRate: 0 }; } } export async function getAdminDashboardMetrics(): Promise { - const supabase = await getServerSupabaseClient(); - if (!supabase) return { totalUsers: 0, totalLoans: 0, activeLoans: 0, highRiskUsers: 0 }; + const db = getDb(); + if (!db) return { totalUsers: 0, totalLoans: 0, activeLoans: 0, highRiskUsers: 0 }; try { - - const [usersRes, totalLoansRes, activeLoansRes, highRiskRes] = await Promise.all([ - supabase.from("profiles").select("id", { count: "exact", head: true }), - supabase.from("loans").select("id", { count: "exact", head: true }), - supabase.from("loans").select("id", { count: "exact", head: true }) - .in("status", ["approved", "funded", "active", "requested"]), - supabase.from("profiles").select("id", { count: "exact", head: true }) - .in("risk_status", ["high", "blocked"]), + const countOf = sql`count(*)::int`; + const [[usersRes], [totalLoansRes], [activeLoansRes], [highRiskRes]] = await Promise.all([ + db.select({ count: countOf }).from(profiles), + db.select({ count: countOf }).from(loans), + db.select({ count: countOf }).from(loans).where(inArray(loans.status, ["approved", "funded", "active", "requested"])), + db.select({ count: countOf }).from(profiles).where(inArray(profiles.riskStatus, ["high", "blocked"])), ]); return { - totalUsers: usersRes.count ?? 0, - totalLoans: totalLoansRes.count ?? 0, - activeLoans: activeLoansRes.count ?? 0, - highRiskUsers: highRiskRes.count ?? 0, + totalUsers: usersRes?.count ?? 0, + totalLoans: totalLoansRes?.count ?? 0, + activeLoans: activeLoansRes?.count ?? 0, + highRiskUsers: highRiskRes?.count ?? 0, }; } catch { return { totalUsers: 0, totalLoans: 0, activeLoans: 0, highRiskUsers: 0 }; diff --git a/lib/db/client.ts b/lib/db/client.ts new file mode 100644 index 0000000..6343898 --- /dev/null +++ b/lib/db/client.ts @@ -0,0 +1,91 @@ +/** + * lib/db/client.ts + * + * Drizzle + Neon database access. + * + * - `getDb()` — HTTP driver. One round-trip per query, no connection to keep + * alive: the right choice for Vercel serverless / edge handlers and for the + * vast majority of reads and single-statement writes. + * - `getPooledDb()` — WebSocket pool driver. Required for interactive + * transactions (`db.transaction(...)`) and for long-running scripts (keepers, + * backfills) that issue many statements. + * + * Both return `null` when DATABASE_URL is not configured so callers can degrade + * gracefully (the app renders an empty dashboard instead of crashing at build + * time, matching the previous behaviour when Supabase env was absent). + * + * SERVER-ONLY. Never import from a client component. (Not guarded with the + * `server-only` package because keeper scripts and vitest import this too.) + */ + +import { neon, neonConfig, Pool } from "@neondatabase/serverless"; +import { drizzle as drizzleHttp, type NeonHttpDatabase } from "drizzle-orm/neon-http"; +import { drizzle as drizzleWs, type NeonDatabase } from "drizzle-orm/neon-serverless"; +import * as schema from "./schema"; + +export type Db = NeonHttpDatabase; +export type PooledDb = NeonDatabase; + +let httpDb: Db | null | undefined; +let pooledDb: PooledDb | null | undefined; + +function databaseUrl(): string | null { + const url = process.env.DATABASE_URL; + return url && url.trim().length > 0 ? url : null; +} + +/** True when the database is configured. */ +export function isDatabaseConfigured(): boolean { + return databaseUrl() !== null; +} + +/** Drizzle over Neon's HTTP driver. Cached per runtime. */ +export function getDb(): Db | null { + if (httpDb !== undefined) return httpDb; + const url = databaseUrl(); + if (!url) { + httpDb = null; + return null; + } + httpDb = drizzleHttp(neon(url), { schema }); + return httpDb; +} + +/** Drizzle over a Neon WebSocket pool — use for transactions and scripts. */ +export function getPooledDb(): PooledDb | null { + if (pooledDb !== undefined) return pooledDb; + const url = databaseUrl(); + if (!url) { + pooledDb = null; + return null; + } + // Node < 22 has no global WebSocket; the `ws` package is pulled in by + // @neondatabase/serverless when needed. Vercel's runtime provides one. + if (typeof WebSocket === "undefined") { + // eslint-disable-next-line @typescript-eslint/no-require-imports + neonConfig.webSocketConstructor = require("ws"); + } + pooledDb = drizzleWs(new Pool({ connectionString: url }), { schema }); + return pooledDb; +} + +/** + * Like `getDb()` but throws a descriptive error instead of returning null. + * Use in API routes where "database not configured" is a hard 503. + */ +export function requireDb(): Db { + const db = getDb(); + if (!db) { + throw new DatabaseNotConfiguredError(); + } + return db; +} + +export class DatabaseNotConfiguredError extends Error { + constructor() { + super("Database is not configured (DATABASE_URL is missing)."); + this.name = "DatabaseNotConfiguredError"; + } +} + +export { schema }; diff --git a/lib/db/metadata.ts b/lib/db/metadata.ts new file mode 100644 index 0000000..6c7431b --- /dev/null +++ b/lib/db/metadata.ts @@ -0,0 +1,28 @@ +/** + * lib/db/metadata.ts + * + * jsonb `metadata` columns come back from Drizzle as parsed objects. Rows + * written by the old Supabase code were sometimes stored as a JSON *string* + * inside the jsonb column, so readers must tolerate both shapes. + */ + +export type Metadata = Record; + +export function readMetadata(raw: unknown): Metadata { + if (raw === null || raw === undefined) return {}; + if (typeof raw === "string") { + try { + const parsed = JSON.parse(raw) as unknown; + return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Metadata) : {}; + } catch { + return {}; + } + } + return typeof raw === "object" && !Array.isArray(raw) ? (raw as Metadata) : {}; +} + +/** Read one string field out of a metadata blob ("" when absent). */ +export function metaString(raw: unknown, key: string): string { + const value = readMetadata(raw)[key]; + return value === null || value === undefined ? "" : String(value); +} diff --git a/lib/db/pools.test.ts b/lib/db/pools.test.ts deleted file mode 100644 index 228c8a7..0000000 --- a/lib/db/pools.test.ts +++ /dev/null @@ -1,617 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/** - * Tests for optimized pool database queries - * - * Tests verify: - * - Only ONE query/RPC call is made instead of multiple - * - Correct data shape and types are returned - * - Pagination works correctly - * - Filtering by status works - * - Error handling - * - * Run with: npm test -- pools.test.ts - */ - -import { describe, it, expect, vi } from "vitest"; -import { SupabaseClient } from "@supabase/supabase-js"; -import { - fetchPools, - fetchPoolById, - fetchActivePoolsWithLiquidity, - fetchAdminDashboardPools, - type Pool, -} from "./pools"; - -// ───────────────────────────────────────────────────────────────────────────── -// MOCK DATA -// ───────────────────────────────────────────────────────────────────────────── - -const MOCK_POOLS: Pool[] = [ - { - id: "pool-1", - name: "Alpha Pool", - description: "Premium lending pool", - status: "active", - apr_bps: 1500, - total_liquidity: 50000, - available_liquidity: 25000, - total_borrowed: 25000, - borrow_cap: null, - created_at: "2024-01-01T00:00:00Z", - updated_at: "2024-01-01T00:00:00Z", - }, - { - id: "pool-2", - name: "Beta Pool", - description: null, - status: "active", - apr_bps: 2000, - total_liquidity: 100000, - available_liquidity: 100000, - total_borrowed: 0, - borrow_cap: null, - created_at: "2024-01-02T00:00:00Z", - updated_at: "2024-01-02T00:00:00Z", - }, - { - id: "pool-3", - name: "Gamma Pool", - description: "Paused pool", - status: "paused", - apr_bps: 1000, - total_liquidity: 30000, - available_liquidity: 30000, - total_borrowed: 0, - borrow_cap: null, - created_at: "2024-01-03T00:00:00Z", - updated_at: "2024-01-03T00:00:00Z", - }, -]; - -// ───────────────────────────────────────────────────────────────────────────── -// HELPER FUNCTIONS -// ───────────────────────────────────────────────────────────────────────────── - -function createMockSupabaseClient( - overrides: Partial = {} -): SupabaseClient { - const mockClient = { - from: vi.fn(), - ...overrides, - } as unknown as SupabaseClient; - - return mockClient; -} - -function createMockQuery(data: any[] = [], count: number | null = null) { - const select = vi.fn(); - const eq = vi.fn(); - const gt = vi.fn(); - const order = vi.fn(); - const limit = vi.fn(); - const range = vi.fn(); - const maybeSingle = vi.fn(); - - const queryChain = { - select, - eq, - gt, - order, - limit, - range, - maybeSingle, - data, - error: null, - count, - }; - - // Setup chainable methods - select.mockReturnValue({ - ...queryChain, - eq: eq.mockReturnValue(queryChain), - gt: gt.mockReturnValue(queryChain), - order: order.mockReturnValue(queryChain), - limit: limit.mockReturnValue(queryChain), - range: range.mockReturnValue(queryChain), - maybeSingle: maybeSingle.mockReturnValue({ data: data[0] || null, error: null }), - }); - - eq.mockReturnValue({ - ...queryChain, - gt: gt.mockReturnValue(queryChain), - order: order.mockReturnValue(queryChain), - limit: limit.mockReturnValue(queryChain), - range: range.mockReturnValue(queryChain), - maybeSingle: maybeSingle.mockReturnValue({ data: data[0] || null, error: null }), - }); - - gt.mockReturnValue({ - ...queryChain, - order: order.mockReturnValue(queryChain), - limit: limit.mockReturnValue(queryChain), - range: range.mockReturnValue(queryChain), - }); - - order.mockReturnValue({ - ...queryChain, - limit: limit.mockReturnValue(queryChain), - range: range.mockReturnValue(queryChain), - maybeSingle: maybeSingle.mockReturnValue({ data: data[0] || null, error: null }), - }); - - limit.mockReturnValue({ - ...queryChain, - order: order.mockReturnValue(queryChain), - }); - - range.mockReturnValue(queryChain); - - return queryChain as any; -} - -// ───────────────────────────────────────────────────────────────────────────── -// TEST SUITES -// ───────────────────────────────────────────────────────────────────────────── - -describe("fetchPools", () => { - it("should make a single query with explicit columns", async () => { - const mockFrom = vi.fn(); - const mockQuery = createMockQuery(MOCK_POOLS.slice(0, 2), 2); - mockFrom.mockReturnValue(mockQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - - const result = await fetchPools(client); - - // Verify only ONE query was made - expect(mockFrom).toHaveBeenCalledTimes(1); - expect(mockFrom).toHaveBeenCalledWith("lending_pools"); - - // Verify explicit column selection (no SELECT *) - expect(mockQuery.select).toHaveBeenCalledWith( - "id, name, description, status, apr_bps, total_liquidity, available_liquidity, total_borrowed, borrow_cap, created_at, updated_at", - expect.any(Object) - ); - - // Verify pagination was applied - expect(mockQuery.range).toHaveBeenCalledWith(0, 9); // default limit 10 - - // Verify result structure - expect(result.pools).toHaveLength(2); - expect(result.pools[0]).toEqual(MOCK_POOLS[0]); - expect(result.hasMore).toBe(false); // got 2, wanted 10 - }); - - it("should apply status filter when provided", async () => { - const mockFrom = vi.fn(); - const activePools = MOCK_POOLS.filter((p) => p.status === "active"); - const mockQuery = createMockQuery(activePools, activePools.length); - mockFrom.mockReturnValue(mockQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - - await fetchPools(client, { status: "active" }); - - // Verify status filter was applied - expect(mockQuery.eq).toHaveBeenCalledWith("status", "active"); - }); - - it("should handle pagination correctly", async () => { - const mockFrom = vi.fn(); - const mockQuery = createMockQuery(MOCK_POOLS.slice(0, 1), 100); - mockFrom.mockReturnValue(mockQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - - const result = await fetchPools(client, { limit: 10, offset: 20 }); - - // Verify correct range was requested - expect(mockQuery.range).toHaveBeenCalledWith(20, 29); - expect(result.hasMore).toBe(false); // Only 1 item returned, so no more on page - }); - - it("should throw error on query failure", async () => { - const mockFrom = vi.fn(); - const errorQuery = { - select: vi.fn().mockReturnThis(), - eq: vi.fn().mockReturnThis(), - order: vi.fn().mockReturnThis(), - range: vi.fn().mockResolvedValue({ - data: null, - error: { message: "Database error" }, - }), - }; - mockFrom.mockReturnValue(errorQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - - await expect(fetchPools(client)).rejects.toThrow("Failed to fetch pools"); - }); - - it("should clamp limit to max 100", async () => { - const mockFrom = vi.fn(); - const mockQuery = createMockQuery([], 0); - mockFrom.mockReturnValue(mockQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - - await fetchPools(client, { limit: 500 }); - - // Should clamp to 100 - expect(mockQuery.range).toHaveBeenCalledWith(0, 99); - }); -}); - -describe("fetchPoolById", () => { - it("should fetch a single pool by ID with one query", async () => { - const mockFrom = vi.fn(); - const mockQuery = createMockQuery([MOCK_POOLS[0]]); - mockFrom.mockReturnValue(mockQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - - const result = await fetchPoolById(client, "pool-1"); - - expect(mockFrom).toHaveBeenCalledTimes(1); - expect(mockQuery.eq).toHaveBeenCalledWith("id", "pool-1"); - expect(result).toEqual(MOCK_POOLS[0]); - }); - - it("should return null if pool not found", async () => { - const mockFrom = vi.fn(); - const mockQuery = createMockQuery([]); - mockFrom.mockReturnValue(mockQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - - const result = await fetchPoolById(client, "nonexistent"); - - expect(result).toBeNull(); - }); -}); - -describe("fetchActivePoolsWithLiquidity", () => { - it("should fetch only active pools with one query", async () => { - const mockFrom = vi.fn(); - const activePools = MOCK_POOLS.filter((p) => p.status === "active"); - const mockQuery = createMockQuery(activePools); - mockFrom.mockReturnValue(mockQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - - const result = await fetchActivePoolsWithLiquidity(client); - - expect(mockFrom).toHaveBeenCalledTimes(1); - expect(mockQuery.eq).toHaveBeenCalledWith("status", "active"); - expect(result).toHaveLength(2); - expect(result.every((p) => p.status === "active")).toBe(true); - }); - - it("should filter by minimum liquidity when provided", async () => { - const mockFrom = vi.fn(); - const highLiquidityPools = MOCK_POOLS.filter( - (p) => p.status === "active" && p.available_liquidity >= 50000 - ); - const mockQuery = createMockQuery(highLiquidityPools); - mockFrom.mockReturnValue(mockQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - - const result = await fetchActivePoolsWithLiquidity(client, 50000); - - expect(mockQuery.gt).toHaveBeenCalledWith("available_liquidity", 50000); - expect(result.every((p) => p.available_liquidity >= 50000)).toBe(true); - }); - - it("should sort by available liquidity descending", async () => { - const mockFrom = vi.fn(); - const mockQuery = createMockQuery(MOCK_POOLS); - mockFrom.mockReturnValue(mockQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - - await fetchActivePoolsWithLiquidity(client); - - expect(mockQuery.order).toHaveBeenCalledWith("available_liquidity", { - ascending: false, - }); - }); -}); - -describe("fetchAdminDashboardPools", () => { - it("should fetch pools and loans in parallel (2 queries, not sequential)", async () => { - const mockLoans = [ - { - id: "loan-1", - status: "requested", - principal_amount: 5000, - apr_bps: 1500, - duration_days: 30, - requested_at: "2024-01-01T00:00:00Z", - borrower_id: "user-1", - profiles: { full_name: "John Doe" }, - }, - ]; - - const mockFromPool = vi.fn(); - const mockPoolQuery = createMockQuery(MOCK_POOLS); - mockFromPool.mockReturnValue(mockPoolQuery); - - const mockFromLoans = vi.fn(); - const mockLoanQuery = createMockQuery(mockLoans); - mockFromLoans.mockReturnValue(mockLoanQuery); - - const client = createMockSupabaseClient({ - from: (table: string) => { - if (table === "lending_pools") return mockFromPool(); - if (table === "loans") return mockFromLoans(); - }, - }); - - const result = await fetchAdminDashboardPools(client); - - expect(result.pools).toHaveLength(3); - expect(result.pendingLoans).toHaveLength(1); - expect(result.pendingLoans[0].borrower_profile?.full_name).toBe("John Doe"); - }); - - it("should handle profile relation cardinality (array format)", async () => { - const mockLoans = [ - { - id: "loan-1", - status: "requested", - principal_amount: 5000, - apr_bps: 1500, - duration_days: 30, - requested_at: "2024-01-01T00:00:00Z", - borrower_id: "user-1", - profiles: [{ full_name: "Jane Doe" }], // Array format - }, - ]; - - const mockFromPool = vi.fn(); - const mockPoolQuery = createMockQuery(MOCK_POOLS); - mockFromPool.mockReturnValue(mockPoolQuery); - - const mockFromLoans = vi.fn(); - const mockLoanQuery = createMockQuery(mockLoans); - mockFromLoans.mockReturnValue(mockLoanQuery); - - const client = createMockSupabaseClient({ - from: (table: string) => { - if (table === "lending_pools") return mockFromPool(); - if (table === "loans") return mockFromLoans(); - }, - }); - - const result = await fetchAdminDashboardPools(client); - - expect(result.pendingLoans[0].borrower_profile?.full_name).toBe("Jane Doe"); - }); - - it("should handle null borrower profile", async () => { - const mockLoans = [ - { - id: "loan-1", - status: "requested", - principal_amount: 5000, - apr_bps: 1500, - duration_days: 30, - requested_at: "2024-01-01T00:00:00Z", - borrower_id: "user-1", - profiles: null, - }, - ]; - - const mockFromPool = vi.fn(); - const mockPoolQuery = createMockQuery(MOCK_POOLS); - mockFromPool.mockReturnValue(mockPoolQuery); - - const mockFromLoans = vi.fn(); - const mockLoanQuery = createMockQuery(mockLoans); - mockFromLoans.mockReturnValue(mockLoanQuery); - - const client = createMockSupabaseClient({ - from: (table: string) => { - if (table === "lending_pools") return mockFromPool(); - if (table === "loans") return mockFromLoans(); - }, - }); - - const result = await fetchAdminDashboardPools(client); - - expect(result.pendingLoans[0].borrower_profile).toBeNull(); - }); -}); - -describe("Performance: Query Count Verification", () => { - it("fetchPools should make exactly 1 query", async () => { - let queryCount = 0; - const mockFrom = vi.fn(() => { - queryCount++; - return createMockQuery(MOCK_POOLS); - }); - - const client = createMockSupabaseClient({ from: mockFrom }); - - await fetchPools(client); - - expect(queryCount).toBe(1); - }); - - it("fetchPoolById should make exactly 1 query", async () => { - let queryCount = 0; - const mockFrom = vi.fn(() => { - queryCount++; - return createMockQuery([MOCK_POOLS[0]]); - }); - - const client = createMockSupabaseClient({ from: mockFrom }); - - await fetchPoolById(client, "pool-1"); - - expect(queryCount).toBe(1); - }); - - it("fetchActivePoolsWithLiquidity should make exactly 1 query", async () => { - let queryCount = 0; - const mockFrom = vi.fn(() => { - queryCount++; - return createMockQuery(MOCK_POOLS.filter((p) => p.status === "active")); - }); - - const client = createMockSupabaseClient({ from: mockFrom }); - - await fetchActivePoolsWithLiquidity(client); - - expect(queryCount).toBe(1); - }); - - it("fetchAdminDashboardPools should make exactly 2 queries (parallel)", async () => { - let queryCount = 0; - const mockFrom = vi.fn(() => { - queryCount++; - return createMockQuery(queryCount === 1 ? MOCK_POOLS : []); - }); - - const client = createMockSupabaseClient({ from: mockFrom }); - - await fetchAdminDashboardPools(client); - - // Should be 2 total: 1 for pools, 1 for loans - expect(queryCount).toBe(2); - }); -}); - -describe("Pool borrow_cap field", () => { - const POOLS_WITH_CAP: Pool[] = [ - { - id: "pool-capped", - name: "Capped Pool", - description: null, - status: "active", - apr_bps: 1500, - total_liquidity: 100000, - available_liquidity: 50000, - total_borrowed: 30000, - borrow_cap: 50000, - created_at: "2024-01-01T00:00:00Z", - updated_at: "2024-01-01T00:00:00Z", - }, - { - id: "pool-uncapped", - name: "Uncapped Pool", - description: null, - status: "active", - apr_bps: 1500, - total_liquidity: 100000, - available_liquidity: 100000, - total_borrowed: 0, - borrow_cap: null, - created_at: "2024-01-01T00:00:00Z", - updated_at: "2024-01-01T00:00:00Z", - }, - ]; - - it("should include borrow_cap in Pool type (null for uncapped)", () => { - const pool = POOLS_WITH_CAP[1]; - expect(pool.borrow_cap).toBeNull(); - }); - - it("should include borrow_cap in Pool type (number for capped)", () => { - const pool = POOLS_WITH_CAP[0]; - expect(pool.borrow_cap).toBe(50000); - }); - - it("should fetch borrow_cap as part of pool data from DB", async () => { - const mockFrom = vi.fn(); - const mockQuery = createMockQuery([ - { ...POOLS_WITH_CAP[0] }, - ]); - mockFrom.mockReturnValue(mockQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - const result = await fetchPoolById(client, "pool-capped"); - - expect(result).not.toBeNull(); - expect(result?.borrow_cap).toBe(50000); - }); - - it("should select borrow_cap column in fetchPools", async () => { - const mockFrom = vi.fn(); - const mockQuery = createMockQuery(POOLS_WITH_CAP, 2); - mockFrom.mockReturnValue(mockQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - await fetchPools(client); - - // Verify borrow_cap is in the column list - expect(mockQuery.select).toHaveBeenCalledWith( - expect.stringContaining("borrow_cap"), - expect.any(Object) - ); - }); - - it("should select borrow_cap column in fetchActivePoolsWithLiquidity", async () => { - const mockFrom = vi.fn(); - const mockQuery = createMockQuery(POOLS_WITH_CAP); - mockFrom.mockReturnValue(mockQuery); - - const client = createMockSupabaseClient({ from: mockFrom }); - await fetchActivePoolsWithLiquidity(client); - - expect(mockQuery.select).toHaveBeenCalledWith( - expect.stringContaining("borrow_cap") - ); - }); -}); - -describe("Borrow Cap Enforcement Logic", () => { - it("should detect when a loan would exceed pool borrow cap", () => { - const poolBorrowCap = 50000; - const totalBorrowed = 45000; - const loanAmount = 6000; - - const wouldExceedCap = totalBorrowed + loanAmount > poolBorrowCap; - expect(wouldExceedCap).toBe(true); - }); - - it("should allow a loan that fits under the borrow cap", () => { - const poolBorrowCap = 50000; - const totalBorrowed = 30000; - const loanAmount = 10000; - - const wouldExceedCap = totalBorrowed + loanAmount > poolBorrowCap; - expect(wouldExceedCap).toBe(false); - }); - - it("should allow a loan when pool has no borrow cap (null)", () => { - const poolBorrowCap: number | null = null; - const totalBorrowed = 999999; - const loanAmount = 999999; - - const wouldExceedCap = - poolBorrowCap !== null && - totalBorrowed + loanAmount > poolBorrowCap; - expect(wouldExceedCap).toBe(false); - }); - - it("should allow a loan exactly at the borrow cap", () => { - const poolBorrowCap = 50000; - const totalBorrowed = 40000; - const loanAmount = 10000; - - // Exactly at cap: 40000 + 10000 = 50000 (not exceeded) - const wouldExceedCap = totalBorrowed + loanAmount > poolBorrowCap; - expect(wouldExceedCap).toBe(false); - }); - - it("should reject a loan 1 unit over the borrow cap", () => { - const poolBorrowCap = 50000; - const totalBorrowed = 40000; - const loanAmount = 10001; - - const wouldExceedCap = totalBorrowed + loanAmount > poolBorrowCap; - expect(wouldExceedCap).toBe(true); - }); -}); diff --git a/lib/db/pools.ts b/lib/db/pools.ts index 4b9c4ff..b807f91 100644 --- a/lib/db/pools.ts +++ b/lib/db/pools.ts @@ -1,21 +1,19 @@ /** - * Optimized Supabase database queries for lending pools. + * lib/db/pools.ts * - * BEFORE OPTIMIZATION (Issue #39): - * - Multiple waterfall queries when fetching pools with related data - * - Each fetch was a separate round-trip to Supabase - * - No pagination or count estimation for large datasets - * - Missing indexes on commonly filtered columns + * Lending-pool queries. Every function takes the Drizzle handle explicitly so + * callers decide whether they are on the HTTP or pooled driver, and so tests + * can inject a fake. * - * OPTIMIZATION APPROACH: - * - Single RPC call for fetching pools with optional filters - * - Explicit column selection (no SELECT *) - * - Pagination support with consistent ordering - * - Estimated row counts for large tables - * - Proper indexes on status, created_at, and other filter columns + * Numeric columns come back from Postgres as strings; they are coerced to + * numbers at this boundary so the rest of the app never has to. */ -import { SupabaseClient } from "@supabase/supabase-js"; +import { asc, desc, eq, gt, sql, type SQL } from "drizzle-orm"; +import type { Db, PooledDb } from "@/lib/db/client"; +import { lendingPools, loans, profiles } from "@/lib/db/schema"; + +export type AnyDb = Db | PooledDb; // ───────────────────────────────────────────────────────────────────────────── // TYPE DEFINITIONS @@ -50,312 +48,157 @@ export interface PoolFetchResult { hasMore: boolean; } +const POOL_COLUMNS = { + id: lendingPools.id, + name: lendingPools.name, + description: lendingPools.description, + status: lendingPools.status, + apr_bps: lendingPools.aprBps, + total_liquidity: lendingPools.totalLiquidity, + available_liquidity: lendingPools.availableLiquidity, + total_borrowed: lendingPools.totalBorrowed, + borrow_cap: lendingPools.borrowCap, + created_at: lendingPools.createdAt, + updated_at: lendingPools.updatedAt, +}; + // ───────────────────────────────────────────────────────────────────────────── -// OPTIMIZED FETCH FUNCTIONS +// FETCH FUNCTIONS // ───────────────────────────────────────────────────────────────────────────── -/** - * Fetch pools with optional filtering and pagination. - * - * OPTIMIZATION: Uses explicit column selection and single query. - * Previously this required multiple queries in waterfall pattern. - * - * Indexes used: - * - idx_lending_pools_status (on status column) - * - Implicit index on created_at for ordering - * - * @param supabase - Supabase client instance - * @param options - Fetch options (status filter, pagination, ordering) - * @returns Pool data with pagination metadata - */ -export async function fetchPools( - supabase: SupabaseClient, - options: PoolFetchOptions = {} -): Promise { - const { - status, - limit = 10, - offset = 0, - orderBy = "created_at", - orderDirection = "desc", - } = options; +/** Fetch pools with optional filtering and pagination. */ +export async function fetchPools(db: AnyDb, options: PoolFetchOptions = {}): Promise { + const { status, limit = 10, offset = 0, orderBy = "created_at", orderDirection = "desc" } = options; - // Validate and clamp pagination parameters const validLimit = Math.min(Math.max(Math.floor(limit) || 10, 1), 100); const validOffset = Math.max(Math.floor(offset) || 0, 0); - // Build the query with explicit column selection (no SELECT *) - let query = supabase - .from("lending_pools") - .select( - "id, name, description, status, apr_bps, total_liquidity, available_liquidity, total_borrowed, borrow_cap, created_at, updated_at", - { count: "estimated" } - ); - - // Add status filter if provided - if (status) { - query = query.eq("status", status); - } - - // Apply ordering (uses index on created_at or available_liquidity) - const ascending = orderDirection === "asc"; - query = query.order(orderBy, { ascending }); + const where: SQL | undefined = status ? eq(lendingPools.status, status) : undefined; + const orderColumn = orderBy === "available_liquidity" ? lendingPools.availableLiquidity : lendingPools.createdAt; + const order = orderDirection === "asc" ? asc(orderColumn) : desc(orderColumn); - // Apply pagination - query = query.range(validOffset, validOffset + validLimit - 1); - - const { data, error, count } = await query; - - if (error) { - throw new Error(`Failed to fetch pools: ${error.message}`); - } - - // Transform raw data to typed Pool objects - const pools = (data ?? []).map(mapRawPoolToPool); + const [rows, [{ count }]] = await Promise.all([ + db.select(POOL_COLUMNS).from(lendingPools).where(where).orderBy(order).limit(validLimit).offset(validOffset), + db.select({ count: sql`count(*)::int` }).from(lendingPools).where(where), + ]); + const pools = rows.map(mapRawPoolToPool); return { pools, - totalCount: count ?? 0, - estimatedTotalCount: count ?? 0, - hasMore: pools.length === validLimit, // Has more if we got a full page + totalCount: count, + estimatedTotalCount: count, + hasMore: validOffset + pools.length < count, }; } -/** - * Fetch a single pool by ID. - * - * OPTIMIZATION: Direct single-row lookup with explicit columns. - * Avoids unnecessary joins or additional queries. - * - * @param supabase - Supabase client instance - * @param poolId - Pool UUID - * @returns Pool data or null if not found - */ -export async function fetchPoolById( - supabase: SupabaseClient, - poolId: string -): Promise { - const { data, error } = await supabase - .from("lending_pools") - .select( - "id, name, description, status, apr_bps, total_liquidity, available_liquidity, total_borrowed, borrow_cap, created_at, updated_at" - ) - .eq("id", poolId) - .maybeSingle(); - - if (error) { - throw new Error(`Failed to fetch pool ${poolId}: ${error.message}`); - } - - return data ? mapRawPoolToPool(data) : null; +/** Fetch a single pool by ID. */ +export async function fetchPoolById(db: AnyDb, poolId: string): Promise { + const [row] = await db.select(POOL_COLUMNS).from(lendingPools).where(eq(lendingPools.id, poolId)).limit(1); + return row ? mapRawPoolToPool(row) : null; } /** - * Fetch pools with active status and available liquidity. - * - * OPTIMIZATION: Common query pattern optimized with index on (status, available_liquidity). - * Used for auto-matching and loan approval. - * - * Previously required: - * 1. Fetch active pools - * 2. Filter in client code based on liquidity - * - * Now: Single query with both filters applied at DB level. - * - * @param supabase - Supabase client instance - * @param minimumLiquidity - Minimum available liquidity required (optional) - * @returns List of active pools with liquidity + * Active pools ordered by available liquidity (desc). Used for auto-matching + * and loan approval. */ -export async function fetchActivePoolsWithLiquidity( - supabase: SupabaseClient, - minimumLiquidity: number = 0 -): Promise { - let query = supabase - .from("lending_pools") - .select( - "id, name, description, status, apr_bps, total_liquidity, available_liquidity, total_borrowed, borrow_cap, created_at, updated_at" - ) - .eq("status", "active"); - - // Only add liquidity filter if minimum is > 0 +export async function fetchActivePoolsWithLiquidity(db: AnyDb, minimumLiquidity: number = 0): Promise { + const conditions = [eq(lendingPools.status, "active")]; if (minimumLiquidity > 0) { - query = query.gt("available_liquidity", minimumLiquidity); - } - - // Order by available liquidity descending for better allocation - query = query.order("available_liquidity", { ascending: false }); - - const { data, error } = await query; - - if (error) { - throw new Error(`Failed to fetch active pools: ${error.message}`); + conditions.push(gt(lendingPools.availableLiquidity, String(minimumLiquidity))); } + const rows = await db + .select(POOL_COLUMNS) + .from(lendingPools) + .where(sql.join(conditions, sql` and `)) + .orderBy(desc(lendingPools.availableLiquidity)); + return rows.map(mapRawPoolToPool); +} - return (data ?? []).map(mapRawPoolToPool); +export interface PendingLoanSummary { + id: string; + status: string; + principal_amount: number; + apr_bps: number; + duration_days: number; + requested_at: string; + borrower_id: string; + borrower_profile: { full_name: string | null } | null; } -/** - * Fetch pools with admin dashboard data (pools + pending loans with borrower info). - * - * OPTIMIZATION: Previously required 2 separate queries: - * 1. SELECT from lending_pools - * 2. SELECT from loans with LEFT JOIN to profiles - * - * Now: Fetch pools and loans separately but with explicit columns, allowing: - * - Better caching at HTTP level - * - Easier to scale with separate RPC calls if needed - * - Clear separation of concerns - * - * @param supabase - Supabase client instance - * @returns Object containing pools and pending loans - */ -export async function fetchAdminDashboardPools( - supabase: SupabaseClient -): Promise<{ +/** Pools + pending loans (with borrower name) for the admin pool dashboard. */ +export async function fetchAdminDashboardPools(db: AnyDb): Promise<{ pools: Pool[]; - pendingLoans: Array<{ - id: string; - status: string; - principal_amount: number; - apr_bps: number; - duration_days: number; - requested_at: string; - borrower_id: string; - borrower_profile: { full_name: string | null } | null; - }>; + pendingLoans: PendingLoanSummary[]; }> { - // Execute both queries in parallel (still 2 queries, but faster than sequential) - const [poolsRes, loansRes] = await Promise.all([ - supabase - .from("lending_pools") - .select( - "id, name, description, status, apr_bps, total_liquidity, available_liquidity, total_borrowed, borrow_cap, created_at, updated_at" - ) - .order("created_at", { ascending: false }), - - supabase - .from("loans") - .select( - "id, status, principal_amount, apr_bps, duration_days, requested_at, borrower_id, profiles:borrower_id(full_name)" - ) - .eq("status", "requested") - .order("requested_at", { ascending: true }), + const [poolRows, loanRows] = await Promise.all([ + db.select(POOL_COLUMNS).from(lendingPools).orderBy(desc(lendingPools.createdAt)), + db + .select({ + id: loans.id, + status: loans.status, + principal_amount: loans.principalAmount, + apr_bps: loans.aprBps, + duration_days: loans.durationDays, + requested_at: loans.requestedAt, + borrower_id: loans.borrowerId, + borrower_name: profiles.fullName, + }) + .from(loans) + .leftJoin(profiles, eq(profiles.id, loans.borrowerId)) + .where(eq(loans.status, "requested")) + .orderBy(asc(loans.requestedAt)), ]); - if (poolsRes.error) { - throw new Error(`Failed to fetch pools: ${poolsRes.error.message}`); - } - - if (loansRes.error) { - throw new Error(`Failed to fetch pending loans: ${loansRes.error.message}`); - } - - const pools = (poolsRes.data ?? []).map(mapRawPoolToPool); - - const pendingLoans = (loansRes.data ?? []).map((loan) => { - // Handle Supabase relation cardinality: profiles can be object or array - const raw = loan.profiles; - const profileData = Array.isArray(raw) - ? (raw[0] as { full_name: string | null } | undefined) ?? null - : (raw as { full_name: string | null } | null); - - return { - id: String(loan.id), - status: String(loan.status ?? "requested"), + return { + pools: poolRows.map(mapRawPoolToPool), + pendingLoans: loanRows.map((loan) => ({ + id: loan.id, + status: loan.status, principal_amount: Number(loan.principal_amount ?? 0), - apr_bps: Number(loan.apr_bps ?? 0), - duration_days: Number(loan.duration_days ?? 30), - requested_at: String(loan.requested_at ?? ""), - borrower_id: String(loan.borrower_id), - borrower_profile: profileData - ? { full_name: profileData.full_name ?? null } - : null, - }; - }); - - return { pools, pendingLoans }; + apr_bps: loan.apr_bps, + duration_days: loan.duration_days, + requested_at: loan.requested_at.toISOString(), + borrower_id: loan.borrower_id, + borrower_profile: loan.borrower_name !== null ? { full_name: loan.borrower_name } : null, + })), + }; } // ───────────────────────────────────────────────────────────────────────────── -// HELPER FUNCTIONS +// HELPERS // ───────────────────────────────────────────────────────────────────────────── interface RawPool { - id: unknown; - name?: unknown; - description?: unknown; - status?: unknown; - apr_bps?: unknown; - total_liquidity?: unknown; - available_liquidity?: unknown; - total_borrowed?: unknown; - borrow_cap?: unknown; - created_at?: unknown; - updated_at?: unknown; + id: string; + name: string; + description: string | null; + status: "active" | "paused" | "closed"; + apr_bps: number; + total_liquidity: string | number; + available_liquidity: string | number; + total_borrowed: string | number; + borrow_cap: string | number | null; + created_at: Date | string; + updated_at: Date | string; } -/** - * Transform raw database row to typed Pool object. - * Ensures consistent type coercion across all fetch functions. - */ -function mapRawPoolToPool(raw: RawPool): Pool { +function toIso(value: Date | string): string { + return value instanceof Date ? value.toISOString() : String(value); +} + +/** Coerce a raw row to a typed Pool (numeric → number, timestamps → ISO). */ +export function mapRawPoolToPool(raw: RawPool): Pool { return { - id: String(raw.id), - name: String(raw.name ?? ""), - description: raw.description ? String(raw.description) : null, - status: String(raw.status ?? "paused") as "active" | "paused" | "closed", - apr_bps: Number(raw.apr_bps ?? 0), + id: raw.id, + name: raw.name, + description: raw.description, + status: raw.status, + apr_bps: Number(raw.apr_bps), total_liquidity: Number(raw.total_liquidity ?? 0), available_liquidity: Number(raw.available_liquidity ?? 0), total_borrowed: Number(raw.total_borrowed ?? 0), borrow_cap: raw.borrow_cap !== null && raw.borrow_cap !== undefined ? Number(raw.borrow_cap) : null, - created_at: String(raw.created_at ?? ""), - updated_at: String(raw.updated_at ?? ""), + created_at: toIso(raw.created_at), + updated_at: toIso(raw.updated_at), }; } - -// ───────────────────────────────────────────────────────────────────────────── -// INDEX RECOMMENDATIONS -// ───────────────────────────────────────────────────────────────────────────── - -/** - * RECOMMENDED INDEXES FOR OPTIMAL PERFORMANCE: - * - * Current indexes (in 01_core_schema.sql): - * - idx_lending_pools_status: Used by status filters ✓ - * - * RECOMMENDED ADDITIONAL INDEXES: - * - * 1. Composite index for active pools with available liquidity: - * CREATE INDEX idx_lending_pools_status_available - * ON public.lending_pools (status, available_liquidity DESC) - * REASON: Speeds up fetchActivePoolsWithLiquidity queries - * Allows index-only scans for admin auto-match operations - * - * 2. Index on created_at for default ordering: - * CREATE INDEX idx_lending_pools_created_at_desc - * ON public.lending_pools (created_at DESC) - * REASON: Default sort order in fetchPools uses created_at - * Improves pagination performance on large tables - * - * 3. Index on available_liquidity for alternative sort: - * CREATE INDEX idx_lending_pools_available_liquidity - * ON public.lending_pools (available_liquidity DESC) - * REASON: When users sort by available liquidity - * Optimizes fetchPools with orderBy: 'available_liquidity' - * - * To apply these indexes, run in Supabase SQL editor: - * - * CREATE INDEX IF NOT EXISTS idx_lending_pools_status_available - * ON public.lending_pools (status, available_liquidity DESC); - * - * CREATE INDEX IF NOT EXISTS idx_lending_pools_created_at_desc - * ON public.lending_pools (created_at DESC); - * - * CREATE INDEX IF NOT EXISTS idx_lending_pools_available_liquidity - * ON public.lending_pools (available_liquidity DESC); - * - * ESTIMATED IMPROVEMENT: - * - Reduces query time from 50-200ms to 5-20ms for tables with 10k+ pools - * - Compound index saves full table scans on status + liquidity queries - */ diff --git a/lib/db/queries.ts b/lib/db/queries.ts new file mode 100644 index 0000000..9c8f9cc --- /dev/null +++ b/lib/db/queries.ts @@ -0,0 +1,171 @@ +/** + * lib/db/queries.ts + * + * Shared read queries for server components. Each returns the snake_case row + * shapes from lib/db/rows.ts. Functions accept a nullable db so pages can + * render an empty state when DATABASE_URL is not configured. + */ + +import { and, asc, desc, eq, inArray, lt, sql } from "drizzle-orm"; +import type { Db } from "@/lib/db/client"; +import { + ledgerTransactions, + loanFundings, + loanRepayments, + loans, + profiles, + reputationEvents, + reputationSnapshots, +} from "@/lib/db/schema"; +import { + ledgerToRow, + loanToRow, + profileToRow, + repaymentToRow, + reputationEventToRow, + snapshotToRow, + type LedgerRow, + type LoanRow, + type ProfileRow, + type RepaymentRow, + type ReputationEventRow, + type SnapshotRow, +} from "@/lib/db/rows"; + +export async function getProfile(db: Db | null, userId: string): Promise { + if (!db) return null; + const [row] = await db.select().from(profiles).where(eq(profiles.id, userId)).limit(1); + return row ? profileToRow(row) : null; +} + +export async function getProfilesByIds(db: Db | null, ids: string[]): Promise> { + if (!db || ids.length === 0) return new Map(); + const rows = await db.select().from(profiles).where(inArray(profiles.id, ids)); + return new Map(rows.map((r) => [r.id, profileToRow(r)])); +} + +export async function getBorrowerLoans(db: Db | null, borrowerId: string, limit = 20): Promise { + if (!db) return []; + const rows = await db + .select() + .from(loans) + .where(eq(loans.borrowerId, borrowerId)) + .orderBy(desc(loans.createdAt)) + .limit(limit); + return rows.map(loanToRow); +} + +export async function getLoansByIds(db: Db | null, ids: string[]): Promise { + if (!db || ids.length === 0) return []; + const rows = await db.select().from(loans).where(inArray(loans.id, ids)); + return rows.map(loanToRow); +} + +export async function getRepaymentsForLoans(db: Db | null, loanIds: string[], limit = 100): Promise { + if (!db || loanIds.length === 0) return []; + const rows = await db + .select() + .from(loanRepayments) + .where(inArray(loanRepayments.loanId, loanIds)) + .orderBy(desc(loanRepayments.createdAt)) + .limit(limit); + return rows.map(repaymentToRow); +} + +export async function getLedgerByRef( + db: Db | null, + refType: string, + refIds: string[], +): Promise { + if (!db || refIds.length === 0) return []; + const rows = await db + .select() + .from(ledgerTransactions) + .where(and(eq(ledgerTransactions.refType, refType), inArray(ledgerTransactions.refId, refIds))) + .orderBy(desc(ledgerTransactions.createdAt)); + return rows.map(ledgerToRow); +} + +export async function getUserLedger(db: Db | null, userId: string, limit = 50): Promise { + if (!db) return []; + const rows = await db + .select() + .from(ledgerTransactions) + .where(eq(ledgerTransactions.userId, userId)) + .orderBy(desc(ledgerTransactions.createdAt)) + .limit(limit); + return rows.map(ledgerToRow); +} + +export async function getReputationSnapshot(db: Db | null, userId: string): Promise { + if (!db) return null; + const [row] = await db.select().from(reputationSnapshots).where(eq(reputationSnapshots.userId, userId)).limit(1); + return row ? snapshotToRow(row) : null; +} + +export async function getReputationEvents(db: Db | null, userId: string, limit = 50): Promise { + if (!db) return []; + const rows = await db + .select() + .from(reputationEvents) + .where(eq(reputationEvents.userId, userId)) + .orderBy(desc(reputationEvents.createdAt)) + .limit(limit); + return rows.map(reputationEventToRow); +} + +export interface MarketplaceLoan { + id: string; + principal_amount: number; + /** Total contributed by all lenders so far (Issue #269). */ + funded_amount: number; + /** Lenders who already hold a slice of this loan. */ + lender_count: number; + apr_bps: number; + duration_days: number; + borrower_id: string; + borrower_name: string; + borrower_wallet: string; + trust_score: number; +} + +/** + * Open loan requests for the lender marketplace: requested/approved loans that + * are not yet fully funded, with the borrower's display name, wallet and trust + * score. Ordered oldest-first so early requests get seen. + */ +export async function getMarketplaceLoans(db: Db | null): Promise { + if (!db) return []; + const rows = await db + .select({ + id: loans.id, + principal_amount: loans.principalAmount, + funded_amount: loans.fundedAmount, + apr_bps: loans.aprBps, + duration_days: loans.durationDays, + borrower_id: loans.borrowerId, + borrower_name: profiles.fullName, + borrower_wallet: profiles.walletAddress, + trust_score: reputationSnapshots.scoreTotal, + lender_count: sql`(select count(*)::int from ${loanFundings} lf where lf.loan_id = ${loans.id})`, + }) + .from(loans) + .leftJoin(profiles, eq(profiles.id, loans.borrowerId)) + .leftJoin(reputationSnapshots, eq(reputationSnapshots.userId, loans.borrowerId)) + .where(and(inArray(loans.status, ["requested", "approved"]), lt(loans.fundedAmount, loans.principalAmount))) + .orderBy(asc(loans.createdAt)); + + return rows.map((r) => ({ + id: r.id, + principal_amount: Number(r.principal_amount), + funded_amount: Number(r.funded_amount), + lender_count: r.lender_count ?? 0, + apr_bps: r.apr_bps, + duration_days: r.duration_days, + borrower_id: r.borrower_id, + borrower_name: + r.borrower_name && r.borrower_name.trim() !== "" ? r.borrower_name : `Borrower ${r.borrower_id.slice(0, 6)}`, + borrower_wallet: r.borrower_wallet ?? "", + trust_score: r.trust_score ?? 250, + })); +} diff --git a/lib/db/rows.ts b/lib/db/rows.ts new file mode 100644 index 0000000..fb877f0 --- /dev/null +++ b/lib/db/rows.ts @@ -0,0 +1,190 @@ +/** + * lib/db/rows.ts + * + * Snake_case row shapes for server components. + * + * The dashboard pages were written against PostgREST rows (snake_case keys, + * ISO-8601 timestamp strings, numeric columns as strings). These mappers turn + * Drizzle rows into that shape so the rendering code is untouched by the + * database migration. New code should prefer the camelCase Drizzle types. + */ + +import type { + LedgerTransaction, + LendingPool, + Loan, + LoanFunding, + LoanRepayment, + PoolPosition, + Profile, + ReputationEvent, + ReputationSnapshot, +} from "@/lib/db/schema"; + +const iso = (d: Date | null | undefined): string | null => (d ? d.toISOString() : null); + +export function profileToRow(p: Profile) { + return { + id: p.id, + full_name: p.fullName, + role: p.role, + wallet_address: p.walletAddress, + country_code: p.countryCode, + phone: p.phone, + date_of_birth: p.dateOfBirth, + kyc_status: p.kycStatus, + risk_status: p.riskStatus, + government_id_ipfs_hash: p.governmentIdIpfsHash, + government_id_url: p.governmentIdUrl, + kyc_submitted_at: iso(p.kycSubmittedAt), + kyc_verified_at: iso(p.kycVerifiedAt), + kyc_rejection_reason: p.kycRejectionReason, + kyc_provider_id: p.kycProviderId, + kyc_provider_status: p.kycProviderStatus, + regulated_pool_access: p.regulatedPoolAccess, + referral_code: p.referralCode, + created_at: p.createdAt.toISOString(), + updated_at: p.updatedAt.toISOString(), + }; +} +export type ProfileRow = ReturnType; + +export function loanToRow(l: Loan) { + return { + id: l.id, + borrower_id: l.borrowerId, + pool_id: l.poolId, + status: l.status, + principal_amount: l.principalAmount, + apr_bps: l.aprBps, + duration_days: l.durationDays, + rate_model: l.rateModel, + rate_switch_count: l.rateSwitchCount, + last_rate_switch_at: iso(l.lastRateSwitchAt), + funded_amount: l.fundedAmount, + repaid_amount: l.repaidAmount, + requested_at: l.requestedAt.toISOString(), + approved_at: iso(l.approvedAt), + funded_at: iso(l.fundedAt), + due_at: iso(l.dueAt), + closed_at: iso(l.closedAt), + defaulted_at: iso(l.defaultedAt), + metadata: (l.metadata ?? {}) as Record, + created_at: l.createdAt.toISOString(), + updated_at: l.updatedAt.toISOString(), + }; +} +export type LoanRow = ReturnType; + +export function repaymentToRow(r: LoanRepayment) { + return { + id: r.id, + loan_id: r.loanId, + payer_id: r.payerId, + amount: r.amount, + paid_at: r.paidAt.toISOString(), + tx_ref: r.txRef, + metadata: (r.metadata ?? {}) as Record, + created_at: r.createdAt.toISOString(), + }; +} +export type RepaymentRow = ReturnType; + +export function fundingToRow(f: LoanFunding) { + return { + id: f.id, + loan_id: f.loanId, + lender_id: f.lenderId, + amount: f.amount, + tx_hash: f.txHash, + lender_address: f.lenderAddress, + funded_at: f.fundedAt.toISOString(), + metadata: (f.metadata ?? {}) as Record, + created_at: f.createdAt.toISOString(), + }; +} +export type FundingRow = ReturnType; + +export function ledgerToRow(t: LedgerTransaction) { + return { + id: t.id, + user_id: t.userId, + category: t.category, + amount: t.amount, + currency: t.currency, + status: t.status, + ref_type: t.refType, + ref_id: t.refId, + metadata: (t.metadata ?? {}) as Record, + created_at: t.createdAt.toISOString(), + updated_at: t.updatedAt.toISOString(), + }; +} +export type LedgerRow = ReturnType; + +export function poolToRow(p: LendingPool) { + return { + id: p.id, + name: p.name, + description: p.description, + status: p.status, + currency: p.currency, + apr_bps: p.aprBps, + total_liquidity: p.totalLiquidity, + available_liquidity: p.availableLiquidity, + total_borrowed: p.totalBorrowed, + borrow_cap: p.borrowCap, + created_by: p.createdBy, + created_at: p.createdAt.toISOString(), + updated_at: p.updatedAt.toISOString(), + }; +} +export type PoolRow = ReturnType; + +export function positionToRow(p: PoolPosition) { + return { + id: p.id, + pool_id: p.poolId, + lender_id: p.lenderId, + status: p.status, + principal_amount: p.principalAmount, + earned_interest: p.earnedInterest, + withdrawn_amount: p.withdrawnAmount, + opened_at: p.openedAt.toISOString(), + closed_at: iso(p.closedAt), + created_at: p.createdAt.toISOString(), + updated_at: p.updatedAt.toISOString(), + }; +} +export type PositionRow = ReturnType; + +export function reputationEventToRow(e: ReputationEvent) { + return { + id: e.id, + user_id: e.userId, + source_type: e.sourceType, + source_id: e.sourceId, + source_key: e.sourceKey, + points_delta: e.pointsDelta, + reason: e.reason, + metadata: (e.metadata ?? {}) as Record, + created_at: e.createdAt.toISOString(), + }; +} +export type ReputationEventRow = ReturnType; + +export function snapshotToRow(s: ReputationSnapshot) { + return { + user_id: s.userId, + score_total: s.scoreTotal, + repayment_score: s.repaymentScore, + lending_score: s.lendingScore, + consistency_score: s.consistencyScore, + external_score: s.externalScore, + reputation_level: s.reputationLevel, + score_breakdown: (s.scoreBreakdown ?? {}) as Record, + calculated_at: s.calculatedAt.toISOString(), + updated_at: s.updatedAt.toISOString(), + }; +} +export type SnapshotRow = ReturnType; diff --git a/lib/db/schema.ts b/lib/db/schema.ts new file mode 100644 index 0000000..c1cfcb5 --- /dev/null +++ b/lib/db/schema.ts @@ -0,0 +1,604 @@ +/** + * lib/db/schema.ts + * + * Drizzle schema for the TrustLend Postgres database (Neon). + * + * This is the single source of truth for the relational model. Migrations are + * generated from it with `npm run db:generate` and applied with + * `npm run db:migrate` (see drizzle.config.ts and drizzle/). + * + * Identity: `users` replaces Supabase's `auth.users`. A user is a Stellar + * wallet (SEP-10 sign-in); `profiles` holds the application-facing profile and + * shares the user's id. + */ + +import { relations, sql } from "drizzle-orm"; +import { + boolean, + date, + index, + integer, + jsonb, + numeric, + pgEnum, + pgTable, + smallint, + text, + timestamp, + uniqueIndex, + uuid, +} from "drizzle-orm/pg-core"; + +// ─── Enums ──────────────────────────────────────────────────────────────────── + +export const appRoleEnum = pgEnum("app_role", ["borrower", "lender", "admin"]); +export const kycStatusEnum = pgEnum("kyc_status", ["pending", "submitted", "verified", "rejected"]); +export const riskStatusEnum = pgEnum("risk_status", ["low", "medium", "high", "blocked"]); +export const loanStatusEnum = pgEnum("loan_status", [ + "requested", + "approved", + "funded", + "active", + "repaid", + "defaulted", + "cancelled", +]); +export const poolStatusEnum = pgEnum("pool_status", ["active", "paused", "closed"]); +export const positionStatusEnum = pgEnum("position_status", ["active", "closed"]); +export const txStatusEnum = pgEnum("tx_status", ["pending", "confirmed", "failed", "cancelled"]); +export const verificationStatusEnum = pgEnum("verification_status", [ + "pending", + "verified", + "rejected", + "expired", +]); +export const taskStatusEnum = pgEnum("task_status", [ + "open", + "assigned", + "completed", + "verified", + "cancelled", +]); +export const taskDifficultyEnum = pgEnum("task_difficulty", ["easy", "medium", "hard"]); +export const riskDecisionEnum = pgEnum("risk_decision", ["allow", "manual_review", "reject"]); +export const referralStatusEnum = pgEnum("referral_status", [ + "pending", + "qualified", + "paid", + "rejected", +]); + +// ─── Shared column helpers ──────────────────────────────────────────────────── + +const createdAt = () => timestamp("created_at", { withTimezone: true }).notNull().defaultNow(); +const updatedAt = () => timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(); +const money = (name: string) => numeric(name, { precision: 20, scale: 6 }); + +// ─── Identity ───────────────────────────────────────────────────────────────── + +/** One row per Stellar wallet that has signed in. Replaces Supabase auth.users. */ +export const users = pgTable( + "users", + { + id: uuid("id").primaryKey().defaultRandom(), + walletAddress: text("wallet_address").notNull(), + role: appRoleEnum("role").notNull().default("borrower"), + email: text("email"), + createdAt: createdAt(), + lastSignInAt: timestamp("last_sign_in_at", { withTimezone: true }), + }, + (t) => [uniqueIndex("users_wallet_address_key").on(t.walletAddress)], +); + +export const profiles = pgTable( + "profiles", + { + id: uuid("id") + .primaryKey() + .references(() => users.id, { onDelete: "cascade" }), + fullName: text("full_name").notNull().default(""), + role: appRoleEnum("role").notNull().default("borrower"), + walletAddress: text("wallet_address"), + countryCode: text("country_code"), + phone: text("phone"), + dateOfBirth: date("date_of_birth"), + kycStatus: kycStatusEnum("kyc_status").notNull().default("pending"), + riskStatus: riskStatusEnum("risk_status").notNull().default("medium"), + // KYC document + provider fields + governmentIdIpfsHash: text("government_id_ipfs_hash"), + governmentIdUrl: text("government_id_url"), + kycSubmittedAt: timestamp("kyc_submitted_at", { withTimezone: true }), + kycVerifiedAt: timestamp("kyc_verified_at", { withTimezone: true }), + kycRejectionReason: text("kyc_rejection_reason"), + kycProviderId: text("kyc_provider_id"), + kycProviderStatus: text("kyc_provider_status"), + regulatedPoolAccess: boolean("regulated_pool_access").notNull().default(false), + referralCode: text("referral_code"), + createdAt: createdAt(), + updatedAt: updatedAt(), + }, + (t) => [ + index("idx_profiles_role").on(t.role), + index("idx_profiles_wallet_address").on(t.walletAddress), + index("idx_profiles_kyc_status").on(t.kycStatus), + index("idx_profiles_risk_status").on(t.riskStatus), + index("idx_profiles_kyc_submitted_at").on(t.kycSubmittedAt), + uniqueIndex("profiles_referral_code_key").on(t.referralCode), + uniqueIndex("idx_profiles_kyc_provider_id") + .on(t.kycProviderId) + .where(sql`${t.kycProviderId} is not null`), + index("idx_profiles_regulated_pool_access").on(t.regulatedPoolAccess), + ], +); + +// ─── Reputation ─────────────────────────────────────────────────────────────── + +export const reputationEvents = pgTable( + "reputation_events", + { + id: uuid("id").primaryKey().defaultRandom(), + userId: uuid("user_id") + .notNull() + .references(() => profiles.id, { onDelete: "cascade" }), + sourceType: text("source_type").notNull(), + sourceId: uuid("source_id"), + sourceKey: text("source_key"), + pointsDelta: integer("points_delta").notNull(), + reason: text("reason").notNull(), + metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`), + createdAt: createdAt(), + }, + (t) => [ + index("idx_rep_events_user_id_created_at").on(t.userId, t.createdAt), + index("idx_rep_events_source").on(t.sourceType, t.sourceId), + index("idx_rep_events_source_key").on(t.sourceType, t.sourceKey), + ], +); + +export const reputationSnapshots = pgTable("reputation_snapshots", { + userId: uuid("user_id") + .primaryKey() + .references(() => profiles.id, { onDelete: "cascade" }), + scoreTotal: integer("score_total").notNull().default(0), + repaymentScore: integer("repayment_score").notNull().default(0), + lendingScore: integer("lending_score").notNull().default(0), + consistencyScore: integer("consistency_score").notNull().default(0), + externalScore: integer("external_score").notNull().default(0), + reputationLevel: text("reputation_level").notNull().default("bronze"), + /** Per-factor breakdown from the last daily recalculation (lib/reputation/scoring). */ + scoreBreakdown: jsonb("score_breakdown").notNull().default(sql`'{}'::jsonb`), + calculatedAt: timestamp("calculated_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: updatedAt(), +}); + +// ─── Tasks ──────────────────────────────────────────────────────────────────── + +export const tasks = pgTable( + "tasks", + { + id: uuid("id").primaryKey().defaultRandom(), + creatorId: uuid("creator_id") + .notNull() + .references(() => profiles.id, { onDelete: "cascade" }), + assignedTo: uuid("assigned_to").references(() => profiles.id, { onDelete: "set null" }), + title: text("title").notNull(), + description: text("description"), + category: text("category"), + rewardXlm: money("reward_xlm").notNull().default("0"), + difficulty: taskDifficultyEnum("difficulty").notNull().default("easy"), + status: taskStatusEnum("status").notNull().default("open"), + completionDeadline: timestamp("completion_deadline", { withTimezone: true }), + completionDate: timestamp("completion_date", { withTimezone: true }), + proofSubmission: text("proof_submission"), + creatorRating: smallint("creator_rating"), + metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`), + createdAt: createdAt(), + updatedAt: updatedAt(), + }, + (t) => [ + index("idx_tasks_creator_id").on(t.creatorId), + index("idx_tasks_assigned_to").on(t.assignedTo), + index("idx_tasks_status").on(t.status), + index("idx_tasks_created_at").on(t.createdAt), + ], +); + +// ─── Lending pools and positions ────────────────────────────────────────────── + +export const lendingPools = pgTable( + "lending_pools", + { + id: uuid("id").primaryKey().defaultRandom(), + name: text("name").notNull(), + description: text("description"), + status: poolStatusEnum("status").notNull().default("active"), + currency: text("currency").notNull().default("XLM"), + aprBps: integer("apr_bps").notNull(), + totalLiquidity: money("total_liquidity").notNull().default("0"), + availableLiquidity: money("available_liquidity").notNull().default("0"), + totalBorrowed: money("total_borrowed").notNull().default("0"), + /** Max total principal this pool may lend out (null = unlimited). */ + borrowCap: numeric("borrow_cap", { precision: 20, scale: 7 }), + createdBy: uuid("created_by").references(() => profiles.id, { onDelete: "set null" }), + createdAt: createdAt(), + updatedAt: updatedAt(), + }, + (t) => [ + index("idx_lending_pools_status").on(t.status), + index("idx_lending_pools_status_available").on(t.status, t.availableLiquidity), + index("idx_lending_pools_created_at_desc").on(t.createdAt), + ], +); + +export const poolPositions = pgTable( + "pool_positions", + { + id: uuid("id").primaryKey().defaultRandom(), + poolId: uuid("pool_id") + .notNull() + .references(() => lendingPools.id, { onDelete: "cascade" }), + lenderId: uuid("lender_id") + .notNull() + .references(() => profiles.id, { onDelete: "cascade" }), + status: positionStatusEnum("status").notNull().default("active"), + principalAmount: money("principal_amount").notNull(), + earnedInterest: money("earned_interest").notNull().default("0"), + withdrawnAmount: money("withdrawn_amount").notNull().default("0"), + openedAt: timestamp("opened_at", { withTimezone: true }).notNull().defaultNow(), + closedAt: timestamp("closed_at", { withTimezone: true }), + createdAt: createdAt(), + updatedAt: updatedAt(), + }, + (t) => [ + index("idx_pool_positions_lender_id").on(t.lenderId), + index("idx_pool_positions_pool_id").on(t.poolId), + ], +); + +// ─── Loans ──────────────────────────────────────────────────────────────────── + +export const loans = pgTable( + "loans", + { + id: uuid("id").primaryKey().defaultRandom(), + borrowerId: uuid("borrower_id") + .notNull() + .references(() => profiles.id, { onDelete: "cascade" }), + /** Null for loans funded directly by lenders on the marketplace. */ + poolId: uuid("pool_id").references(() => lendingPools.id, { onDelete: "restrict" }), + status: loanStatusEnum("status").notNull().default("requested"), + principalAmount: money("principal_amount").notNull(), + aprBps: integer("apr_bps").notNull(), + durationDays: integer("duration_days").notNull(), + /** "fixed" | "floating" — mirrors the on-chain InterestRateModel. */ + rateModel: text("rate_model").notNull().default("fixed"), + rateSwitchCount: integer("rate_switch_count").notNull().default(0), + lastRateSwitchAt: timestamp("last_rate_switch_at", { withTimezone: true }), + /** Running total of partial fills (issue #269). */ + fundedAmount: money("funded_amount").notNull().default("0"), + repaidAmount: money("repaid_amount").notNull().default("0"), + requestedAt: timestamp("requested_at", { withTimezone: true }).notNull().defaultNow(), + approvedAt: timestamp("approved_at", { withTimezone: true }), + fundedAt: timestamp("funded_at", { withTimezone: true }), + dueAt: timestamp("due_at", { withTimezone: true }), + closedAt: timestamp("closed_at", { withTimezone: true }), + defaultedAt: timestamp("defaulted_at", { withTimezone: true }), + metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`), + createdAt: createdAt(), + updatedAt: updatedAt(), + }, + (t) => [ + index("idx_loans_borrower_id").on(t.borrowerId), + index("idx_loans_pool_id").on(t.poolId), + index("idx_loans_status").on(t.status), + index("idx_loans_due_at").on(t.dueAt), + index("idx_loans_borrower_status").on(t.borrowerId, t.status), + index("idx_loans_rate_model").on(t.rateModel), + index("idx_loans_status_funded").on(t.status, t.fundedAmount), + ], +); + +export const loanRepayments = pgTable( + "loan_repayments", + { + id: uuid("id").primaryKey().defaultRandom(), + loanId: uuid("loan_id") + .notNull() + .references(() => loans.id, { onDelete: "cascade" }), + payerId: uuid("payer_id") + .notNull() + .references(() => profiles.id, { onDelete: "restrict" }), + amount: money("amount").notNull(), + paidAt: timestamp("paid_at", { withTimezone: true }).notNull().defaultNow(), + txRef: text("tx_ref"), + metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`), + createdAt: createdAt(), + }, + (t) => [ + index("idx_loan_repayments_loan_id").on(t.loanId), + index("idx_loan_repayments_payer_id").on(t.payerId), + ], +); + +/** Individual lender contributions to a loan (partial fills, issue #269). */ +export const loanFundings = pgTable( + "loan_fundings", + { + id: uuid("id").primaryKey().defaultRandom(), + loanId: uuid("loan_id") + .notNull() + .references(() => loans.id, { onDelete: "cascade" }), + lenderId: uuid("lender_id") + .notNull() + .references(() => profiles.id, { onDelete: "restrict" }), + amount: money("amount").notNull(), + txHash: text("tx_hash").notNull(), + lenderAddress: text("lender_address"), + fundedAt: timestamp("funded_at", { withTimezone: true }).notNull().defaultNow(), + metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`), + createdAt: createdAt(), + }, + (t) => [ + index("idx_loan_fundings_loan_id").on(t.loanId), + index("idx_loan_fundings_lender_id").on(t.lenderId), + index("idx_loan_fundings_lender_loan").on(t.lenderId, t.loanId), + uniqueIndex("idx_loan_fundings_tx_hash").on(t.txHash), + ], +); + +// ─── Risk and fraud ─────────────────────────────────────────────────────────── + +export const riskAssessments = pgTable( + "risk_assessments", + { + id: uuid("id").primaryKey().defaultRandom(), + userId: uuid("user_id") + .notNull() + .references(() => profiles.id, { onDelete: "cascade" }), + score: numeric("score", { precision: 5, scale: 2 }).notNull(), + decision: riskDecisionEnum("decision").notNull(), + reasons: jsonb("reasons").notNull().default(sql`'[]'::jsonb`), + assessedAt: timestamp("assessed_at", { withTimezone: true }).notNull().defaultNow(), + createdAt: createdAt(), + }, + (t) => [index("idx_risk_assessments_user_id_assessed_at").on(t.userId, t.assessedAt)], +); + +export const fraudSignals = pgTable( + "fraud_signals", + { + id: uuid("id").primaryKey().defaultRandom(), + userId: uuid("user_id") + .notNull() + .references(() => profiles.id, { onDelete: "cascade" }), + signalType: text("signal_type").notNull(), + severity: smallint("severity").notNull(), + payload: jsonb("payload").notNull().default(sql`'{}'::jsonb`), + resolved: boolean("resolved").notNull().default(false), + createdAt: createdAt(), + resolvedAt: timestamp("resolved_at", { withTimezone: true }), + }, + (t) => [ + index("idx_fraud_signals_user_id_created_at").on(t.userId, t.createdAt), + index("idx_fraud_signals_resolved").on(t.resolved), + ], +); + +// ─── Ledger and chain mapping ───────────────────────────────────────────────── + +export const ledgerTransactions = pgTable( + "ledger_transactions", + { + id: uuid("id").primaryKey().defaultRandom(), + userId: uuid("user_id") + .notNull() + .references(() => profiles.id, { onDelete: "cascade" }), + category: text("category").notNull(), + amount: money("amount").notNull(), + currency: text("currency").notNull().default("XLM"), + status: txStatusEnum("status").notNull().default("pending"), + refType: text("ref_type"), + refId: uuid("ref_id"), + metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`), + createdAt: createdAt(), + updatedAt: updatedAt(), + }, + (t) => [ + index("idx_ledger_transactions_user_id_created_at").on(t.userId, t.createdAt), + index("idx_ledger_transactions_status").on(t.status), + ], +); + +export const chainEvents = pgTable( + "chain_events", + { + id: uuid("id").primaryKey().defaultRandom(), + txHash: text("tx_hash").notNull(), + contractId: text("contract_id"), + eventType: text("event_type").notNull(), + payload: jsonb("payload").notNull().default(sql`'{}'::jsonb`), + happenedAt: timestamp("happened_at", { withTimezone: true }), + createdAt: createdAt(), + }, + (t) => [ + uniqueIndex("chain_events_tx_hash_event_type_key").on(t.txHash, t.eventType), + index("idx_chain_events_contract_id").on(t.contractId), + index("idx_chain_events_happened_at").on(t.happenedAt), + ], +); + +// ─── External verification ──────────────────────────────────────────────────── + +export const externalVerifications = pgTable( + "external_verifications", + { + id: uuid("id").primaryKey().defaultRandom(), + userId: uuid("user_id") + .notNull() + .references(() => profiles.id, { onDelete: "cascade" }), + provider: text("provider").notNull(), + verificationType: text("verification_type").notNull(), + status: verificationStatusEnum("status").notNull().default("pending"), + verifiedAt: timestamp("verified_at", { withTimezone: true }), + payloadMeta: jsonb("payload_meta").notNull().default(sql`'{}'::jsonb`), + createdAt: createdAt(), + updatedAt: updatedAt(), + }, + (t) => [ + index("idx_external_verifications_user_id").on(t.userId), + index("idx_external_verifications_status").on(t.status), + ], +); + +// ─── Webhooks ───────────────────────────────────────────────────────────────── + +export const webhookEndpoints = pgTable( + "webhook_endpoints", + { + id: uuid("id").primaryKey().defaultRandom(), + name: text("name").notNull(), + url: text("url").notNull(), + /** "discord" | "telegram" | "slack" | "custom" */ + platform: text("platform").notNull(), + topic: text("topic").notNull(), + isActive: boolean("is_active").notNull().default(true), + createdBy: uuid("created_by").references(() => users.id, { onDelete: "set null" }), + createdAt: createdAt(), + updatedAt: updatedAt(), + }, + (t) => [ + index("idx_webhook_endpoints_platform").on(t.platform), + index("idx_webhook_endpoints_topic").on(t.topic), + ], +); + +// ─── Referrals ──────────────────────────────────────────────────────────────── + +export const referrals = pgTable( + "referrals", + { + id: uuid("id").primaryKey().defaultRandom(), + referrerId: uuid("referrer_id") + .notNull() + .references(() => profiles.id, { onDelete: "cascade" }), + refereeId: uuid("referee_id") + .notNull() + .references(() => profiles.id, { onDelete: "cascade" }), + referralCode: text("referral_code").notNull(), + status: referralStatusEnum("status").notNull().default("pending"), + qualifyingLoanId: uuid("qualifying_loan_id").references(() => loans.id, { + onDelete: "set null", + }), + /** Bonus as reported by the contract, in whole reward tokens. */ + bonusAmount: numeric("bonus_amount", { precision: 20, scale: 7 }).notNull().default("0"), + /** Stellar transaction that carried the payout. */ + payoutTxHash: text("payout_tx_hash"), + qualifiedAt: timestamp("qualified_at", { withTimezone: true }), + paidAt: timestamp("paid_at", { withTimezone: true }), + metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`), + createdAt: createdAt(), + updatedAt: updatedAt(), + }, + (t) => [ + uniqueIndex("referrals_referee_id_key").on(t.refereeId), + index("idx_referrals_referrer_id").on(t.referrerId), + index("idx_referrals_status").on(t.status), + index("idx_referrals_referrer_status").on(t.referrerId, t.status), + ], +); + +// ─── Notifications ──────────────────────────────────────────────────────────── + +export const notifications = pgTable( + "notifications", + { + id: uuid("id").primaryKey().defaultRandom(), + userId: uuid("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + type: text("type").notNull(), + title: text("title").notNull(), + message: text("message").notNull(), + read: boolean("read").notNull().default(false), + createdAt: createdAt(), + }, + (t) => [index("idx_notifications_user_id_created_at").on(t.userId, t.createdAt)], +); + +// ─── Relations (for db.query.* relational API) ─────────────────────────────── + +export const usersRelations = relations(users, ({ one }) => ({ + profile: one(profiles, { fields: [users.id], references: [profiles.id] }), +})); + +export const profilesRelations = relations(profiles, ({ one, many }) => ({ + user: one(users, { fields: [profiles.id], references: [users.id] }), + loans: many(loans), + poolPositions: many(poolPositions), + ledgerTransactions: many(ledgerTransactions), + reputationEvents: many(reputationEvents), + reputationSnapshot: one(reputationSnapshots, { + fields: [profiles.id], + references: [reputationSnapshots.userId], + }), +})); + +export const loansRelations = relations(loans, ({ one, many }) => ({ + borrower: one(profiles, { fields: [loans.borrowerId], references: [profiles.id] }), + pool: one(lendingPools, { fields: [loans.poolId], references: [lendingPools.id] }), + repayments: many(loanRepayments), + fundings: many(loanFundings), +})); + +export const loanRepaymentsRelations = relations(loanRepayments, ({ one }) => ({ + loan: one(loans, { fields: [loanRepayments.loanId], references: [loans.id] }), + payer: one(profiles, { fields: [loanRepayments.payerId], references: [profiles.id] }), +})); + +export const loanFundingsRelations = relations(loanFundings, ({ one }) => ({ + loan: one(loans, { fields: [loanFundings.loanId], references: [loans.id] }), + lender: one(profiles, { fields: [loanFundings.lenderId], references: [profiles.id] }), +})); + +export const lendingPoolsRelations = relations(lendingPools, ({ many }) => ({ + positions: many(poolPositions), + loans: many(loans), +})); + +export const poolPositionsRelations = relations(poolPositions, ({ one }) => ({ + pool: one(lendingPools, { fields: [poolPositions.poolId], references: [lendingPools.id] }), + lender: one(profiles, { fields: [poolPositions.lenderId], references: [profiles.id] }), +})); + +export const ledgerTransactionsRelations = relations(ledgerTransactions, ({ one }) => ({ + user: one(profiles, { fields: [ledgerTransactions.userId], references: [profiles.id] }), +})); + +export const reputationEventsRelations = relations(reputationEvents, ({ one }) => ({ + user: one(profiles, { fields: [reputationEvents.userId], references: [profiles.id] }), +})); + +export const referralsRelations = relations(referrals, ({ one }) => ({ + referrer: one(profiles, { fields: [referrals.referrerId], references: [profiles.id] }), + referee: one(profiles, { fields: [referrals.refereeId], references: [profiles.id] }), + qualifyingLoan: one(loans, { fields: [referrals.qualifyingLoanId], references: [loans.id] }), +})); + +// ─── Row types ──────────────────────────────────────────────────────────────── + +export type User = typeof users.$inferSelect; +export type Profile = typeof profiles.$inferSelect; +export type Loan = typeof loans.$inferSelect; +export type LoanRepayment = typeof loanRepayments.$inferSelect; +export type LoanFunding = typeof loanFundings.$inferSelect; +export type LendingPool = typeof lendingPools.$inferSelect; +export type PoolPosition = typeof poolPositions.$inferSelect; +export type LedgerTransaction = typeof ledgerTransactions.$inferSelect; +export type ReputationEvent = typeof reputationEvents.$inferSelect; +export type ReputationSnapshot = typeof reputationSnapshots.$inferSelect; +export type Task = typeof tasks.$inferSelect; +export type WebhookEndpoint = typeof webhookEndpoints.$inferSelect; +export type Referral = typeof referrals.$inferSelect; +export type Notification = typeof notifications.$inferSelect; +export type FraudSignal = typeof fraudSignals.$inferSelect; +export type RiskAssessment = typeof riskAssessments.$inferSelect; diff --git a/lib/deployment/env-file.test.ts b/lib/deployment/env-file.test.ts index 23c2264..2f165b7 100644 --- a/lib/deployment/env-file.test.ts +++ b/lib/deployment/env-file.test.ts @@ -93,8 +93,8 @@ describe("upsertEnvVars", () => { it("preserves unrelated keys and their comments", () => { const original = [ - "# Supabase", - "SUPABASE_SERVICE_ROLE_KEY=super-secret", + "# Database", + "SESSION_SECRET=super-secret", "", "# Contracts", "NEXT_PUBLIC_LENDING_CONTRACT_ID=old", @@ -104,8 +104,8 @@ describe("upsertEnvVars", () => { NEXT_PUBLIC_LENDING_CONTRACT_ID: CONTRACT_ID, }); - expect(result).toContain("# Supabase"); - expect(result).toContain("SUPABASE_SERVICE_ROLE_KEY=super-secret"); + expect(result).toContain("# Database"); + expect(result).toContain("SESSION_SECRET=super-secret"); expect(result).toContain("# Contracts"); expect(result).toContain(`NEXT_PUBLIC_LENDING_CONTRACT_ID=${CONTRACT_ID}`); expect(result).not.toContain("=old"); @@ -194,8 +194,8 @@ describe("upsertEnvVars", () => { it("keeps secrets intact across a realistic merge", () => { const original = [ - "NEXT_PUBLIC_SUPABASE_URL=https://xyz.supabase.co", - "SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi.secret.value", + "DATABASE_URL=postgres://user:pw@ep-xyz.neon.tech/db", + "SESSION_SECRET=eyJhbGciOi.secret.value", "ADMIN_SECRET_KEY=SXXXXXXX", "NEXT_PUBLIC_LENDING_CONTRACT_ID=", ].join("\n"); @@ -207,7 +207,7 @@ describe("upsertEnvVars", () => { }) ); - expect(parsed.SUPABASE_SERVICE_ROLE_KEY).toBe("eyJhbGciOi.secret.value"); + expect(parsed.SESSION_SECRET).toBe("eyJhbGciOi.secret.value"); expect(parsed.ADMIN_SECRET_KEY).toBe("SXXXXXXX"); expect(parsed.NEXT_PUBLIC_LENDING_CONTRACT_ID).toBe(CONTRACT_ID); expect(parsed.NEXT_PUBLIC_ADMIN_ADDRESS).toBe("GADMIN"); diff --git a/lib/deployment/env-file.ts b/lib/deployment/env-file.ts index 5296e66..b0cd5e9 100644 --- a/lib/deployment/env-file.ts +++ b/lib/deployment/env-file.ts @@ -2,7 +2,7 @@ * Reading and rewriting `.env` files in place (Issue #270). * * The testnet deployment CLI writes freshly deployed contract IDs straight into - * the developer's `.env.local`. That file usually already holds Supabase keys, + * the developer's `.env.local`. That file usually already holds the database URL, * API secrets and hand-written comments, so the merge has to be surgical: * update the values we own, leave every other byte alone. */ diff --git a/lib/deployment/parse-deployment-url.ts b/lib/deployment/parse-deployment-url.ts index 92e50e7..514674c 100644 --- a/lib/deployment/parse-deployment-url.ts +++ b/lib/deployment/parse-deployment-url.ts @@ -34,18 +34,19 @@ export function parseDeploymentUrl(rawOutput: string): string { const url = urlCandidates[urlCandidates.length - 1].trim(); // Validate the URL is well-formed + let parsed: URL; try { - const parsed = new URL(url); - if (parsed.protocol !== "https:") { - throw new Error("Deployment URL must use HTTPS"); - } - return url; + parsed = new URL(url); } catch (cause) { throw new Error( `Extracted candidate "${url}" is not a valid URL`, { cause }, ); } + if (parsed.protocol !== "https:") { + throw new Error("Deployment URL must use HTTPS"); + } + return url; } /** diff --git a/lib/email/resend.ts b/lib/email/resend.ts index 1c21859..e253f9c 100644 --- a/lib/email/resend.ts +++ b/lib/email/resend.ts @@ -1,4 +1,6 @@ -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { eq } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { users } from "@/lib/db/schema"; import { loanApprovedTemplate, loanFundedTemplate, @@ -32,17 +34,17 @@ function appUrl(path: string) { } async function getUserEmail(userId: string): Promise { - const supabase = getServiceRoleClient(); - const admin = supabase?.auth?.admin; - if (!admin) return null; + const db = getDb(); + if (!db) return null; - const { data, error } = await admin.getUserById(userId); - if (error) { - console.warn(`[email] Could not resolve email for user ${userId}: ${error.message}`); + try { + const [row] = await db.select({ email: users.email }).from(users).where(eq(users.id, userId)).limit(1); + return row?.email ?? null; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn(`[email] Could not resolve email for user ${userId}: ${message}`); return null; } - - return data.user?.email ?? null; } async function sendEmail(payload: EmailPayload): Promise { diff --git a/lib/kyc/middleware.ts b/lib/kyc/middleware.ts index c6347fb..89b7307 100644 --- a/lib/kyc/middleware.ts +++ b/lib/kyc/middleware.ts @@ -2,13 +2,15 @@ * Reusable KYC guard for API routes. * * Usage: - * const check = await requireKycVerified(user.id, supabase); + * const check = await requireKycVerified(user.id, db); * if (!check.allowed) { * return NextResponse.json({ error: check.reason }, { status: 403 }); * } */ -import type { SupabaseClient } from "@supabase/supabase-js"; +import { eq } from "drizzle-orm"; +import type { AnyDb } from "@/lib/db/pools"; +import { profiles } from "@/lib/db/schema"; export interface KycGuardResult { allowed: boolean; @@ -22,16 +24,23 @@ export interface KycGuardResult { */ export async function requireKycVerified( userId: string, - supabase: SupabaseClient, + db: AnyDb, options: { regulatedPoolOnly?: boolean } = {} ): Promise { - const { data: profile, error } = await supabase - .from("profiles") - .select("kyc_status, regulated_pool_access, risk_status") - .eq("id", userId) - .maybeSingle(); - - if (error) { + let profile: + | { kycStatus: string; regulatedPoolAccess: boolean; riskStatus: string } + | undefined; + try { + [profile] = await db + .select({ + kycStatus: profiles.kycStatus, + regulatedPoolAccess: profiles.regulatedPoolAccess, + riskStatus: profiles.riskStatus, + }) + .from(profiles) + .where(eq(profiles.id, userId)) + .limit(1); + } catch { return { allowed: false, reason: "Unable to verify identity status. Please try again.", @@ -45,8 +54,8 @@ export async function requireKycVerified( }; } - const kycStatus = (profile.kyc_status as string) ?? "pending"; - const riskStatus = (profile.risk_status as string) ?? "medium"; + const kycStatus = profile.kycStatus ?? "pending"; + const riskStatus = profile.riskStatus ?? "medium"; // Blocked accounts can never access regulated pools if (riskStatus === "blocked") { @@ -78,7 +87,7 @@ export async function requireKycVerified( } // For regulated pools specifically, also check the explicit access flag - if (options.regulatedPoolOnly && !profile.regulated_pool_access) { + if (options.regulatedPoolOnly && !profile.regulatedPoolAccess) { return { allowed: false, kycStatus, @@ -91,18 +100,14 @@ export async function requireKycVerified( } /** - * Lightweight check — returns `true` if KYC verified, `false` otherwise. + * Lightweight check: `true` if KYC verified, `false` otherwise. * Use this for UI gates where you don't need the reason string. */ -export async function isKycVerified( - userId: string, - supabase: SupabaseClient -): Promise { - const { data } = await supabase - .from("profiles") - .select("kyc_status") - .eq("id", userId) - .maybeSingle(); - - return data?.kyc_status === "verified"; +export async function isKycVerified(userId: string, db: AnyDb): Promise { + const [row] = await db + .select({ kycStatus: profiles.kycStatus }) + .from(profiles) + .where(eq(profiles.id, userId)) + .limit(1); + return row?.kycStatus === "verified"; } diff --git a/lib/lender/tax-report-data.ts b/lib/lender/tax-report-data.ts index 60ef698..5eb2ce8 100644 --- a/lib/lender/tax-report-data.ts +++ b/lib/lender/tax-report-data.ts @@ -1,4 +1,6 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import { and, asc, eq } from "drizzle-orm"; +import type { AnyDb } from "@/lib/db/pools"; +import { ledgerTransactions, lendingPools, poolPositions } from "@/lib/db/schema"; import type { P2pFundingInput, P2pRepaymentInput, @@ -9,10 +11,10 @@ import type { * Gather everything a lender's tax report is built from (Issue #271). * * Two income sources, and they come from different places: - * * Pool positions live in `pool_positions`, readable by the lender. + * * Pool positions live in `pool_positions`. * * P2P activity lives in `ledger_transactions`. The lender's own fundings - * are theirs to read, but the matching repayments were written by the - * *borrower*, so they need the service-role client and a metadata match. + * are keyed by user_id, but the matching repayments were written by the + * *borrower*, so they are found by a metadata match. */ export type TaxReportData = { @@ -43,59 +45,62 @@ function parseMetadata(raw: unknown): LedgerMetadata { } export async function getLenderTaxReportData( - supabase: SupabaseClient, - srClient: SupabaseClient | null, + db: AnyDb, userId: string, walletAddress?: string | null ): Promise { - const [positionsRes, fundingsRes] = await Promise.all([ - supabase - .from("pool_positions") - .select( - "id, pool_id, principal_amount, earned_interest, opened_at, closed_at, status, lending_pools ( name, currency )" - ) - .eq("lender_id", userId), - supabase - .from("ledger_transactions") - .select("ref_id, amount, currency, created_at, metadata") - .eq("user_id", userId) - .eq("ref_type", "loan_fund"), + const [positionRows, fundingRows] = await Promise.all([ + db + .select({ + id: poolPositions.id, + pool_id: poolPositions.poolId, + principal_amount: poolPositions.principalAmount, + earned_interest: poolPositions.earnedInterest, + opened_at: poolPositions.openedAt, + closed_at: poolPositions.closedAt, + pool_name: lendingPools.name, + pool_currency: lendingPools.currency, + }) + .from(poolPositions) + .leftJoin(lendingPools, eq(lendingPools.id, poolPositions.poolId)) + .where(eq(poolPositions.lenderId, userId)), + db + .select({ + ref_id: ledgerTransactions.refId, + amount: ledgerTransactions.amount, + currency: ledgerTransactions.currency, + created_at: ledgerTransactions.createdAt, + metadata: ledgerTransactions.metadata, + }) + .from(ledgerTransactions) + .where(and(eq(ledgerTransactions.userId, userId), eq(ledgerTransactions.refType, "loan_fund"))), ]); - const poolPositions: PoolPositionInput[] = (positionsRes.data ?? []).map((row) => { - // PostgREST returns an embedded to-one relation as an object, but as an - // array when it cannot prove the relationship is single-valued. - const poolRaw = Array.isArray(row.lending_pools) ? row.lending_pools[0] : row.lending_pools; - const pool = poolRaw as { name?: string; currency?: string } | null; - - return { - id: String(row.id), - poolId: String(row.pool_id ?? ""), - poolName: pool?.name ?? null, - asset: pool?.currency ?? null, - principalAmount: row.principal_amount, - earnedInterest: row.earned_interest, - openedAt: row.opened_at ? String(row.opened_at) : null, - closedAt: row.closed_at ? String(row.closed_at) : null, - }; - }); - - const fundings: P2pFundingInput[] = (fundingsRes.data ?? []).map((row) => { + const positions: PoolPositionInput[] = positionRows.map((row) => ({ + id: row.id, + poolId: row.pool_id, + poolName: row.pool_name ?? null, + asset: row.pool_currency ?? null, + principalAmount: row.principal_amount, + earnedInterest: row.earned_interest, + openedAt: row.opened_at ? row.opened_at.toISOString() : null, + closedAt: row.closed_at ? row.closed_at.toISOString() : null, + })); + + const fundings: P2pFundingInput[] = fundingRows.map((row) => { const meta = parseMetadata(row.metadata); return { loanId: String(meta.loanId ?? row.ref_id ?? ""), amount: row.amount, asset: row.currency ? String(row.currency) : null, - date: row.created_at ? String(row.created_at) : null, + date: row.created_at ? row.created_at.toISOString() : null, }; }); - const repayments = srClient - ? await getLenderRepayments(srClient, userId, walletAddress) - : []; + const repayments = await getLenderRepayments(db, userId, walletAddress); - return { poolPositions, fundings, repayments }; + return { poolPositions: positions, fundings, repayments }; } /** @@ -106,20 +111,26 @@ export async function getLenderTaxReportData( * are checked because older rows recorded only one of them. */ async function getLenderRepayments( - srClient: SupabaseClient, + db: AnyDb, userId: string, walletAddress?: string | null ): Promise { - const { data } = await srClient - .from("ledger_transactions") - .select("ref_id, amount, currency, created_at, metadata") - .eq("ref_type", "loan_repay") - .order("created_at", { ascending: true }) + const data = await db + .select({ + ref_id: ledgerTransactions.refId, + amount: ledgerTransactions.amount, + currency: ledgerTransactions.currency, + created_at: ledgerTransactions.createdAt, + metadata: ledgerTransactions.metadata, + }) + .from(ledgerTransactions) + .where(eq(ledgerTransactions.refType, "loan_repay")) + .orderBy(asc(ledgerTransactions.createdAt)) .limit(REPAYMENT_SCAN_LIMIT); const repayments: P2pRepaymentInput[] = []; - for (const row of data ?? []) { + for (const row of data) { const meta = parseMetadata(row.metadata); const matchesUser = meta.lenderUserId != null && String(meta.lenderUserId) === userId; @@ -144,7 +155,7 @@ async function getLenderRepayments( loanId: String(meta.loanId ?? row.ref_id ?? ""), amount: payout?.payout ?? row.amount, asset: row.currency ? String(row.currency) : null, - date: row.created_at ? String(row.created_at) : null, + date: row.created_at ? row.created_at.toISOString() : null, txHash: meta.txHash ? String(meta.txHash) : null, }); } diff --git a/lib/lender/yield-analytics.ts b/lib/lender/yield-analytics.ts index 46f34cc..a1556dc 100644 --- a/lib/lender/yield-analytics.ts +++ b/lib/lender/yield-analytics.ts @@ -96,7 +96,7 @@ export interface RawP2pTransaction { amount?: number | string | null; ref_id?: string | null; created_at?: string | null; - metadata?: string | null; + metadata?: string | Record | null; } export interface CalculateYieldOptions { diff --git a/lib/loans/lenders.ts b/lib/loans/lenders.ts index 91f15c5..8d7f075 100644 --- a/lib/loans/lenders.ts +++ b/lib/loans/lenders.ts @@ -1,4 +1,6 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import { and, desc, eq } from "drizzle-orm"; +import type { AnyDb } from "@/lib/db/pools"; +import { ledgerTransactions, loanFundings } from "@/lib/db/schema"; import type { LenderContribution } from "./funding"; /** @@ -7,21 +9,19 @@ import type { LenderContribution } from "./funding"; * Reads `loan_fundings`, the per-contribution table. Loans funded before * partial fills existed are recorded only in `ledger_transactions`, so those * fall back to the ledger — the same place the single-lender code used to look. - * - * Requires a service-role client: contributions belong to lenders, and the - * borrower calling repayment cannot read them under RLS. */ -export async function getLoanLenders( - srClient: SupabaseClient, - loanId: string -): Promise { - const { data: fundings, error } = await srClient - .from("loan_fundings") - .select("lender_id, lender_address, amount") - .eq("loan_id", loanId) - .order("amount", { ascending: false }); +export async function getLoanLenders(db: AnyDb, loanId: string): Promise { + const fundings = await db + .select({ + lender_id: loanFundings.lenderId, + lender_address: loanFundings.lenderAddress, + amount: loanFundings.amount, + }) + .from(loanFundings) + .where(eq(loanFundings.loanId, loanId)) + .orderBy(desc(loanFundings.amount)); - if (!error && fundings && fundings.length > 0) { + if (fundings.length > 0) { return mergeByLender( fundings.map((row) => ({ lenderId: String(row.lender_id ?? ""), @@ -32,15 +32,17 @@ export async function getLoanLenders( } // ── Legacy fallback ──────────────────────────────────────────────────────── - // Pre-#269 loans, or a database where sql/08_partial_loan_fills.sql has not - // been applied yet. - const { data: fundTxs } = await srClient - .from("ledger_transactions") - .select("user_id, amount, metadata") - .eq("ref_type", "loan_fund") - .eq("ref_id", loanId); + // Pre-#269 loans are recorded only in the ledger. + const fundTxs = await db + .select({ + user_id: ledgerTransactions.userId, + amount: ledgerTransactions.amount, + metadata: ledgerTransactions.metadata, + }) + .from(ledgerTransactions) + .where(and(eq(ledgerTransactions.refType, "loan_fund"), eq(ledgerTransactions.refId, loanId))); - const legacy = (fundTxs ?? []).map((row) => { + const legacy = fundTxs.map((row) => { let address = ""; try { diff --git a/lib/notifications.ts b/lib/notifications.ts index 9fe9757..b527e9c 100644 --- a/lib/notifications.ts +++ b/lib/notifications.ts @@ -1,4 +1,5 @@ -import { getServerSupabaseClient } from "@/lib/supabase/server"; +import { getDb } from "@/lib/db/client"; +import { notifications } from "@/lib/db/schema"; export async function createNotification({ userId, @@ -11,19 +12,11 @@ export async function createNotification({ message: string; type: string; }) { - const supabase = await getServerSupabaseClient(); - if (!supabase) return null; + const db = getDb(); + if (!db) return null; try { - const { error } = await supabase.from("notifications").insert({ - user_id: userId, - title, - message, - type, - }); - if (error) { - console.error("Failed to create notification:", error); - } + await db.insert(notifications).values({ userId, title, message, type }); } catch (err) { console.error("Error creating notification", err); } diff --git a/lib/oracle/sources.ts b/lib/oracle/sources.ts index 180074c..ce07720 100644 --- a/lib/oracle/sources.ts +++ b/lib/oracle/sources.ts @@ -52,12 +52,20 @@ export async function fetchJsonSafe( if (typeof fetchImpl !== "function") return null; const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), timeoutMs); + let timer: ReturnType | undefined; + // Race the request against the deadline as well as aborting it: a fetch + // implementation that ignores the signal must still not hang the poll loop. + const deadline = new Promise((resolve) => { + timer = setTimeout(() => { + controller.abort(); + resolve(null); + }, timeoutMs); + }); try { - const res = await fetchImpl(url, { signal: controller.signal, headers }); - if (!res.ok) return null; - return await res.json(); + const res = await Promise.race([fetchImpl(url, { signal: controller.signal, headers }), deadline]); + if (!res || !res.ok) return null; + return await Promise.race([res.json(), deadline]); } catch { // Timeout, DNS failure, malformed JSON — all "source down". return null; diff --git a/lib/referrals/qualify.ts b/lib/referrals/qualify.ts index 1768a21..e70e6b3 100644 --- a/lib/referrals/qualify.ts +++ b/lib/referrals/qualify.ts @@ -4,17 +4,18 @@ * When a borrower's loan reaches 100% funded and activates, their referrer's * bonus becomes payable. The authoritative payout happens on-chain — the * lending contract invokes ReferralRewardsContract::claim_referral_bonus during - * activate_loan — so this module's job is only to mirror that into Supabase and + * activate_loan — so this module's job is only to mirror that into the database and * notify the referrer. * * Every failure here is swallowed and logged. A referral is a bonus; it must * never turn a successful loan funding into a failed API request. */ -import type { SupabaseClient } from "@supabase/supabase-js"; +import { sql } from "drizzle-orm"; +import type { AnyDb } from "@/lib/db/pools"; interface QualifyReferralParams { - supabase: SupabaseClient; + db: AnyDb; /** The borrower whose loan just activated — the referred user. */ refereeId: string; /** The loan that triggered qualification. */ @@ -37,7 +38,7 @@ export interface QualifyReferralResult { * and simply carry on otherwise. */ export async function qualifyReferralForLoan({ - supabase, + db, refereeId, loanId, }: QualifyReferralParams): Promise { @@ -50,17 +51,10 @@ export async function qualifyReferralForLoan({ if (!refereeId || !loanId) return none; try { - const { data, error } = await supabase.rpc("qualify_referral", { - p_referee_id: refereeId, - p_loan_id: loanId, - }); - - if (error) { - console.error("[referrals] qualify_referral failed:", error.message); - return none; - } - - const row = Array.isArray(data) ? data[0] : data; + const result = await db.execute( + sql`select referral_id, referrer_id, status from public.qualify_referral(${refereeId}::uuid, ${loanId}::uuid)`, + ); + const row = (result.rows as Array<{ referral_id?: string; referrer_id?: string; status?: string }>)[0]; // No row means the borrower was not referred by anyone. if (!row?.referral_id) return none; diff --git a/lib/reputation/daily-sync.ts b/lib/reputation/daily-sync.ts index 02e41cd..a44f187 100644 --- a/lib/reputation/daily-sync.ts +++ b/lib/reputation/daily-sync.ts @@ -2,11 +2,13 @@ * lib/reputation/daily-sync.ts * * Daily reputation calculation runner. Iterates over borrower accounts, - * aggregates on-chain loan/repayment performance, recalculates scores, - * and updates persistent snapshots and on-chain contract state. + * aggregates loan/repayment performance, recalculates scores, and updates the + * persistent snapshots. */ -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { eq, inArray } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { loanRepayments, loans, profiles, reputationEvents, reputationSnapshots } from "@/lib/db/schema"; import { computeBorrowerReputationScore, BorrowerRepaymentStats, @@ -32,60 +34,81 @@ export interface DailyCalculationSummary { * Runs the daily reputation recalculation for all active borrowers. */ export async function runDailyReputationRecalculation(): Promise { - const supabase = getServiceRoleClient(); - if (!supabase) { - throw new Error("Supabase service client unavailable"); + const db = getDb(); + if (!db) { + throw new Error("Database unavailable"); } // 1. Fetch all borrower profiles - const { data: borrowers, error: borrowersError } = await supabase - .from("profiles") - .select("id, wallet_address, kyc_status, full_name, created_at") - .eq("role", "borrower"); - - if (borrowersError) { - throw new Error(`Failed to fetch borrowers: ${borrowersError.message}`); - } + const borrowers = await db + .select({ + id: profiles.id, + walletAddress: profiles.walletAddress, + kycStatus: profiles.kycStatus, + createdAt: profiles.createdAt, + }) + .from(profiles) + .where(eq(profiles.role, "borrower")); const summary: DailyCalculationSummary = { - scanned: borrowers?.length ?? 0, + scanned: borrowers.length, updated: 0, tierUpgrades: 0, errors: 0, details: [], }; - if (!borrowers || borrowers.length === 0) { + if (borrowers.length === 0) { return summary; } + // 2. Bulk-load loans, repayments and snapshots for every borrower (3 queries + // instead of 3 per borrower). + const borrowerIds = borrowers.map((b) => b.id); + const [allLoans, allRepayments, allSnapshots] = await Promise.all([ + db + .select({ + id: loans.id, + borrowerId: loans.borrowerId, + status: loans.status, + principalAmount: loans.principalAmount, + dueAt: loans.dueAt, + createdAt: loans.createdAt, + metadata: loans.metadata, + }) + .from(loans) + .where(inArray(loans.borrowerId, borrowerIds)), + db + .select({ + payerId: loanRepayments.payerId, + loanId: loanRepayments.loanId, + amount: loanRepayments.amount, + paidAt: loanRepayments.paidAt, + }) + .from(loanRepayments) + .where(inArray(loanRepayments.payerId, borrowerIds)), + db + .select({ + userId: reputationSnapshots.userId, + scoreTotal: reputationSnapshots.scoreTotal, + level: reputationSnapshots.reputationLevel, + }) + .from(reputationSnapshots) + .where(inArray(reputationSnapshots.userId, borrowerIds)), + ]); + + const loansByBorrower = groupBy(allLoans, (l) => l.borrowerId); + const repaymentsByPayer = groupBy(allRepayments, (r) => r.payerId); + const snapshotByUser = new Map(allSnapshots.map((s) => [s.userId, s])); + for (const borrower of borrowers) { try { - // 2. Fetch loan history - const { data: loans } = await supabase - .from("loans") - .select("id, status, principal_amount, repaid_amount, due_at, created_at, metadata") - .eq("borrower_id", borrower.id); - - // 3. Fetch repayments - const { data: repayments } = await supabase - .from("loan_repayments") - .select("id, amount, paid_at, loan_id") - .eq("payer_id", borrower.id); - - // 4. Fetch current reputation snapshot - const { data: snapshot } = await supabase - .from("reputation_snapshots") - .select("score_total, tier") - .eq("user_id", borrower.id) - .maybeSingle(); - - const previousScore = snapshot?.score_total ?? 250; - const previousTier = snapshot?.tier ?? "None"; - - // 5. Aggregate stats - const userLoans = loans ?? []; - const userRepayments = repayments ?? []; + const userLoans = loansByBorrower.get(borrower.id) ?? []; + const userRepayments = repaymentsByPayer.get(borrower.id) ?? []; + const snapshot = snapshotByUser.get(borrower.id); + + const previousScore = snapshot?.scoreTotal ?? 250; + const previousTier = snapshot?.level ?? "None"; const completedLoans = userLoans.filter((l) => l.status === "repaid").length; const defaultedLoans = userLoans.filter((l) => l.status === "defaulted").length; @@ -95,37 +118,29 @@ export async function runDailyReputationRecalculation(): Promise 0) { - // Find latest payment date for this loan - const loanPayments = userRepayments.filter((r) => r.loan_id === loan.id); - const latestPayment = loanPayments.reduce( - (latest, r) => Math.max(latest, new Date(r.paid_at).getTime()), - creationTime - ); - - if (latestPayment <= dueTime) { - onTimeCount++; - } else { - lateCount++; - } - } else { - onTimeCount++; - } + if (loan.status !== "repaid") continue; + const dueTime = loan.dueAt ? loan.dueAt.getTime() : 0; + const creationTime = loan.createdAt ? loan.createdAt.getTime() : 0; + + if (loan.metadata && typeof loan.metadata === "object" && (loan.metadata as { is_early?: boolean }).is_early) { + earlyCount++; + } else if (dueTime > 0) { + const loanPayments = userRepayments.filter((r) => r.loanId === loan.id); + const latestPayment = loanPayments.reduce( + (latest, r) => Math.max(latest, r.paidAt.getTime()), + creationTime, + ); + if (latestPayment <= dueTime) onTimeCount++; + else lateCount++; + } else { + onTimeCount++; } } - const totalBorrowed = userLoans.reduce((sum, l) => sum + Number(l.principal_amount ?? 0), 0); + const totalBorrowed = userLoans.reduce((sum, l) => sum + Number(l.principalAmount ?? 0), 0); const totalRepaid = userRepayments.reduce((sum, r) => sum + Number(r.amount ?? 0), 0); - - const accountAgeDays = borrower.created_at - ? Math.floor((Date.now() - new Date(borrower.created_at).getTime()) / (86400 * 1000)) + const accountAgeDays = borrower.createdAt + ? Math.floor((Date.now() - borrower.createdAt.getTime()) / (86400 * 1000)) : 0; const stats: BorrowerRepaymentStats = { @@ -137,51 +152,51 @@ export async function runDailyReputationRecalculation(): Promise previousScore) { summary.tierUpgrades++; - - // Log celebration event - await supabase.from("reputation_events").insert({ - user_id: borrower.id, - event_type: "tier_upgrade", - points: result.score - previousScore, - description: `Tier upgraded to ${result.tier}! Unlocked rate discount: ${result.rateDiscountPct}% APR.`, - created_at: now, + await db.insert(reputationEvents).values({ + userId: borrower.id, + sourceType: "tier_upgrade", + sourceKey: `${result.tier}:${new Date().toISOString().slice(0, 10)}`, + pointsDelta: 0, + reason: `Tier upgraded to ${result.tier}! Unlocked rate discount: ${result.rateDiscountPct}% APR.`, + metadata: { previousTier, previousScore, newScore: result.score }, }); } summary.updated++; summary.details.push({ userId: borrower.id, - walletAddress: borrower.wallet_address, + walletAddress: borrower.walletAddress ?? undefined, previousScore, newScore: result.score, tier: result.tier, @@ -195,3 +210,14 @@ export async function runDailyReputationRecalculation(): Promise(rows: T[], key: (row: T) => string): Map { + const map = new Map(); + for (const row of rows) { + const k = key(row); + const list = map.get(k); + if (list) list.push(row); + else map.set(k, [row]); + } + return map; +} diff --git a/lib/reputation/scoring.ts b/lib/reputation/scoring.ts index 724c7d2..f82e9df 100644 --- a/lib/reputation/scoring.ts +++ b/lib/reputation/scoring.ts @@ -9,7 +9,6 @@ import { ReputationTier, TIER_MAX_LOAN, TIER_INTEREST_BPS, - scoreToTier, } from "@/types/contracts"; export interface BorrowerRepaymentStats { @@ -63,6 +62,26 @@ export const MAX_REPUTATION_SCORE = 1000; export const MIN_REPUTATION_SCORE = 0; export const STANDARD_BASE_APR_BPS = 1500; // 15.00% APR standard rate +/** + * Off-chain tier thresholds on the 0–1000 platform scale (base score 250). + * This is deliberately NOT the on-chain `scoreToTier` mapping in + * types/contracts.ts, which works on the contract's raw point scale. + */ +export const OFFCHAIN_TIER_MIN_SCORE: Record, number> = { + Beginner: 300, + Silver: 500, + Gold: 700, + Platinum: 850, +}; + +export function offchainScoreToTier(score: number): ReputationTier { + if (score >= OFFCHAIN_TIER_MIN_SCORE.Platinum) return "Platinum"; + if (score >= OFFCHAIN_TIER_MIN_SCORE.Gold) return "Gold"; + if (score >= OFFCHAIN_TIER_MIN_SCORE.Silver) return "Silver"; + if (score >= OFFCHAIN_TIER_MIN_SCORE.Beginner) return "Beginner"; + return "None"; +} + // Score weights export const SCORING_WEIGHTS = { ON_TIME_LOAN_PTS: 35, // +35 pts per on-time loan repayment @@ -129,7 +148,7 @@ export function computeBorrowerReputationScore( const score = Math.max(MIN_REPUTATION_SCORE, Math.min(MAX_REPUTATION_SCORE, Math.round(rawScore))); // 9. Tier and Rate Determination - const tier = scoreToTier(BigInt(score)); + const tier = offchainScoreToTier(score); const interestRateBps = TIER_INTEREST_BPS[tier] ?? STANDARD_BASE_APR_BPS; const interestRatePct = Number((interestRateBps / 100).toFixed(2)); const standardRateBps = STANDARD_BASE_APR_BPS; diff --git a/lib/scheduler/default-management.ts b/lib/scheduler/default-management.ts index a93ebf8..c3ffd85 100644 --- a/lib/scheduler/default-management.ts +++ b/lib/scheduler/default-management.ts @@ -17,11 +17,13 @@ * contract so it can propose; a human still has to approve + execute before * any funds actually move. See `docs/contracts/multisig-admin.md`. * - * Every step is idempotent (guarded by Supabase state) and individually + * Every step is idempotent (guarded by database state) and individually * error-handled so one bad loan never aborts the whole run. */ -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { and, desc, eq, inArray, isNotNull, lt, sql } from "drizzle-orm"; +import { getDb, type Db } from "@/lib/db/client"; +import { ledgerTransactions, loans, profiles } from "@/lib/db/schema"; import { addr, getAdminKeypair, @@ -93,23 +95,19 @@ function outstandingXlm(loan: LoanRow): number { } /** Resolve the borrower's Stellar wallet from the profiles table. */ -async function getWallet( - supabase: ReturnType, - profileId: string -): Promise { - if (!supabase) return null; - const { data } = await supabase - .from("profiles") - .select("wallet_address") - .eq("id", profileId) - .maybeSingle(); - const w = data?.wallet_address; +async function getWallet(db: Db, profileId: string): Promise { + const [row] = await db + .select({ walletAddress: profiles.walletAddress }) + .from(profiles) + .where(eq(profiles.id, profileId)) + .limit(1); + const w = row?.walletAddress; return typeof w === "string" && w.startsWith("G") ? w : null; } /** Lender wallet + on-chain loan id are recorded at funding time in the ledger. */ async function getFundingInfo( - supabase: ReturnType, + db: Db, loanId: string, loanMeta: Record | null ): Promise<{ lenderAddress: string | null; onchainLoanId: number | null }> { @@ -118,20 +116,16 @@ async function getFundingInfo( toOnchainId(loanMeta?.onchain_loan_id) ?? toOnchainId(loanMeta?.onchainLoanId); let lenderAddress: string | null = null; - if (!supabase) return { lenderAddress, onchainLoanId }; - // A loan can be filled by several lenders (Issue #269), so this may match // many rows. Take the largest contributor as the payout designee — the // MultiSigAdmin insurance proposal names a single lender, so splitting an // insurance payout across lenders is a separate piece of work. - const { data } = await supabase - .from("ledger_transactions") - .select("metadata, amount") - .eq("ref_type", "loan_fund") - .eq("ref_id", loanId) - .order("amount", { ascending: false }) - .limit(1) - .maybeSingle(); + const [data] = await db + .select({ metadata: ledgerTransactions.metadata, amount: ledgerTransactions.amount }) + .from(ledgerTransactions) + .where(and(eq(ledgerTransactions.refType, "loan_fund"), eq(ledgerTransactions.refId, loanId))) + .orderBy(desc(ledgerTransactions.amount)) + .limit(1); const raw = data?.metadata; const meta: Record | null = @@ -160,18 +154,20 @@ function safeJson(s: string): Record | null { } async function setLoanMetadataFlag( - supabase: ReturnType, + db: Db, loanId: string, patch: Record, - extraCols: Record = {} + extraCols: { status?: "defaulted"; defaulted_at?: string } = {} ): Promise { - if (!supabase) return; - const { data } = await supabase.from("loans").select("metadata").eq("id", loanId).maybeSingle(); - const current = (data?.metadata as Record) ?? {}; - await supabase - .from("loans") - .update({ metadata: { ...current, ...patch }, ...extraCols }) - .eq("id", loanId); + await db + .update(loans) + .set({ + // Atomic jsonb merge so concurrent runs cannot clobber each other's flags. + metadata: sql`coalesce(${loans.metadata}, '{}'::jsonb) || ${JSON.stringify(patch)}::jsonb`, + ...(extraCols.status ? { status: extraCols.status } : {}), + ...(extraCols.defaulted_at ? { defaultedAt: new Date(extraCols.defaulted_at) } : {}), + }) + .where(eq(loans.id, loanId)); } // ─── Core run ───────────────────────────────────────────────────────────────── @@ -179,26 +175,36 @@ async function setLoanMetadataFlag( /** * Query loans that are live (active/funded) and already past their due date. */ -async function queryOverdueLoans( - supabase: NonNullable>, - nowIso: string -): Promise { - const { data, error } = await supabase - .from("loans") - .select( - "id, borrower_id, status, principal_amount, repaid_amount, due_at, defaulted_at, metadata" - ) - .in("status", ["active", "funded"]) - .not("due_at", "is", null) - .lt("due_at", nowIso); - - if (error) throw new Error(`Failed to query overdue loans: ${error.message}`); - return (data ?? []) as LoanRow[]; +async function queryOverdueLoans(db: Db, nowIso: string): Promise { + const rows = await db + .select({ + id: loans.id, + borrowerId: loans.borrowerId, + status: loans.status, + principalAmount: loans.principalAmount, + repaidAmount: loans.repaidAmount, + dueAt: loans.dueAt, + defaultedAt: loans.defaultedAt, + metadata: loans.metadata, + }) + .from(loans) + .where(and(inArray(loans.status, ["active", "funded"]), isNotNull(loans.dueAt), lt(loans.dueAt, new Date(nowIso)))); + + return rows.map((r) => ({ + id: r.id, + borrower_id: r.borrowerId, + status: r.status, + principal_amount: Number(r.principalAmount), + repaid_amount: Number(r.repaidAmount), + due_at: r.dueAt ? r.dueAt.toISOString() : null, + defaulted_at: r.defaultedAt ? r.defaultedAt.toISOString() : null, + metadata: (r.metadata as Record | null) ?? null, + })); } export async function runDefaultManagement(): Promise { - const supabase = getServiceRoleClient(); - if (!supabase) throw new Error("Service role client unavailable (check SUPABASE_SERVICE_ROLE_KEY)"); + const db = getDb(); + if (!db) throw new Error("Database unavailable (check DATABASE_URL)"); const ledgerTimeSecs = await getLedgerTimeSecs(); const ledgerIso = new Date(ledgerTimeSecs * 1000).toISOString(); @@ -212,7 +218,7 @@ export async function runDefaultManagement(): Promise { ); } - const loans = await queryOverdueLoans(supabase, ledgerIso); + const loans = await queryOverdueLoans(db, ledgerIso); const result: DefaultRunResult = { ledgerTime: ledgerIso, @@ -247,9 +253,9 @@ export async function runDefaultManagement(): Promise { const alreadyDefaulted = Boolean(loan.defaulted_at) || Boolean(meta.defaulted_onchain_at); const alreadyProposedPayout = Boolean(meta.insurance_payout_proposed_at); - const { lenderAddress, onchainLoanId } = await getFundingInfo(supabase, loan.id, meta); + const { lenderAddress, onchainLoanId } = await getFundingInfo(db, loan.id, meta); outcome.onchainLoanId = onchainLoanId; - const borrowerWallet = await getWallet(supabase, loan.borrower_id); + const borrowerWallet = await getWallet(db, loan.borrower_id); const amountStroops = xlmToStroops(outstandingXlm(loan)); // ── 1 + 2: mark defaulted & record the phase ───────────────────────────── @@ -267,7 +273,7 @@ export async function runDefaultManagement(): Promise { outcome.actions.push("skipped on-chain default (missing onchain id / wallet)"); } await setLoanMetadataFlag( - supabase, + db, loan.id, { defaulted_onchain_at: ledgerIso, days_overdue: daysOverdue }, { status: "defaulted", defaulted_at: ledgerIso } @@ -289,7 +295,7 @@ export async function runDefaultManagement(): Promise { amountStroops ); outcome.actions.push("propose:trigger_insurance_payout"); - await setLoanMetadataFlag(supabase, loan.id, { + await setLoanMetadataFlag(db, loan.id, { insurance_payout_proposed_at: ledgerIso, insurance_payout_proposal_id: proposalId, insurance_amount_stroops: amountStroops.toString(), diff --git a/lib/scheduler/payment-due.ts b/lib/scheduler/payment-due.ts index a9135ce..3848681 100644 --- a/lib/scheduler/payment-due.ts +++ b/lib/scheduler/payment-due.ts @@ -1,4 +1,6 @@ -import { getServiceRoleClient } from "@/lib/supabase/server"; +import { and, eq, gt, inArray, isNotNull, lt, lte, sql } from "drizzle-orm"; +import { getDb } from "@/lib/db/client"; +import { loans } from "@/lib/db/schema"; import { isResendConfigured, sendPaymentOverdueEmail, @@ -30,49 +32,80 @@ export interface WebhookPayload { * have not already had a payment-due notification sent. */ export async function queryDueLoans(): Promise { - const supabase = getServiceRoleClient(); - if (!supabase) throw new Error("Service role client unavailable"); + const db = getDb(); + if (!db) throw new Error("Database unavailable"); const now = new Date(); const cutoff = new Date(now.getTime() + LOOKAHEAD_HOURS * 60 * 60 * 1000); - const { data, error } = await supabase - .from("loans") - .select("id, borrower_id, due_at, principal_amount, repaid_amount, metadata") - .in("status", ["active", "funded"]) - .not("due_at", "is", null) - .gt("due_at", now.toISOString()) - .lte("due_at", cutoff.toISOString()); - - if (error) throw new Error(`Failed to query due loans: ${error.message}`); + const rows = await db + .select(DUE_LOAN_COLUMNS) + .from(loans) + .where( + and( + inArray(loans.status, ["active", "funded"]), + isNotNull(loans.dueAt), + gt(loans.dueAt, now), + lte(loans.dueAt, cutoff), + ), + ); // Filter out already-notified loans in JS (avoids complex jsonb query) - return (data ?? []).filter( - (loan) => !(loan.metadata as Record)?.payment_due_notified_at - ); + return rows.map(toDueLoan).filter((loan) => !loan.metadata?.payment_due_notified_at); } /** * Query active loans that are already overdue and have not had an overdue email sent. */ export async function queryOverdueEmailLoans(): Promise { - const supabase = getServiceRoleClient(); - if (!supabase) throw new Error("Service role client unavailable"); + const db = getDb(); + if (!db) throw new Error("Database unavailable"); - const now = new Date(); + const rows = await db + .select(DUE_LOAN_COLUMNS) + .from(loans) + .where( + and(inArray(loans.status, ["active", "funded"]), isNotNull(loans.dueAt), lt(loans.dueAt, new Date())), + ); - const { data, error } = await supabase - .from("loans") - .select("id, borrower_id, due_at, principal_amount, repaid_amount, metadata") - .in("status", ["active", "funded"]) - .not("due_at", "is", null) - .lt("due_at", now.toISOString()); + return rows.map(toDueLoan).filter((loan) => !loan.metadata?.payment_overdue_emailed_at); +} - if (error) throw new Error(`Failed to query overdue loans: ${error.message}`); +const DUE_LOAN_COLUMNS = { + id: loans.id, + borrowerId: loans.borrowerId, + dueAt: loans.dueAt, + principalAmount: loans.principalAmount, + repaidAmount: loans.repaidAmount, + metadata: loans.metadata, +}; + +function toDueLoan(row: { + id: string; + borrowerId: string; + dueAt: Date | null; + principalAmount: string; + repaidAmount: string; + metadata: unknown; +}): DueLoan { + return { + id: row.id, + borrower_id: row.borrowerId, + due_at: row.dueAt ? row.dueAt.toISOString() : "", + principal_amount: Number(row.principalAmount), + repaid_amount: Number(row.repaidAmount), + metadata: (row.metadata ?? {}) as Record, + }; +} - return (data ?? []).filter( - (loan) => !(loan.metadata as Record)?.payment_overdue_emailed_at - ); +/** Atomically merge one key into loans.metadata. */ +async function setLoanMetadataKey(loanId: string, key: string, value: string): Promise { + const db = getDb(); + if (!db) throw new Error("Database unavailable"); + await db + .update(loans) + .set({ metadata: sql`coalesce(${loans.metadata}, '{}'::jsonb) || jsonb_build_object(${key}::text, ${value}::text)` }) + .where(eq(loans.id, loanId)); } /** @@ -113,71 +146,11 @@ export async function sendWebhookNotification( * Mark a loan as notified by writing a timestamp into its metadata. */ export async function markLoanNotified(loanId: string): Promise { - const supabase = getServiceRoleClient(); - if (!supabase) throw new Error("Service role client unavailable"); - - const { error } = await supabase.rpc("jsonb_set_metadata_key", { - p_loan_id: loanId, - p_key: "payment_due_notified_at", - p_value: new Date().toISOString(), - }); - - // Fallback: manual merge if RPC not available - if (error) { - const { data: loan, error: fetchErr } = await supabase - .from("loans") - .select("metadata") - .eq("id", loanId) - .single(); - - if (fetchErr) throw new Error(`Failed to fetch loan for metadata update: ${fetchErr.message}`); - - const { error: updateErr } = await supabase - .from("loans") - .update({ - metadata: { - ...(loan.metadata as Record), - payment_due_notified_at: new Date().toISOString(), - }, - }) - .eq("id", loanId); - - if (updateErr) throw new Error(`Failed to mark loan notified: ${updateErr.message}`); - } + await setLoanMetadataKey(loanId, "payment_due_notified_at", new Date().toISOString()); } export async function markLoanOverdueEmailed(loanId: string): Promise { - const supabase = getServiceRoleClient(); - if (!supabase) throw new Error("Service role client unavailable"); - - const sentAt = new Date().toISOString(); - const { error } = await supabase.rpc("jsonb_set_metadata_key", { - p_loan_id: loanId, - p_key: "payment_overdue_emailed_at", - p_value: sentAt, - }); - - if (error) { - const { data: loan, error: fetchErr } = await supabase - .from("loans") - .select("metadata") - .eq("id", loanId) - .single(); - - if (fetchErr) throw new Error(`Failed to fetch loan for metadata update: ${fetchErr.message}`); - - const { error: updateErr } = await supabase - .from("loans") - .update({ - metadata: { - ...(loan.metadata as Record), - payment_overdue_emailed_at: sentAt, - }, - }) - .eq("id", loanId); - - if (updateErr) throw new Error(`Failed to mark overdue email sent: ${updateErr.message}`); - } + await setLoanMetadataKey(loanId, "payment_overdue_emailed_at", new Date().toISOString()); } export interface RunResult { diff --git a/lib/supabase/client.ts b/lib/supabase/client.ts deleted file mode 100644 index 31e1383..0000000 --- a/lib/supabase/client.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createBrowserClient } from "@supabase/ssr"; -import type { SupabaseClient } from "@supabase/supabase-js"; - -let browserClient: SupabaseClient | null | undefined; - -export function getBrowserSupabaseClient(): SupabaseClient | null { - if (browserClient !== undefined) { - return browserClient; - } - - const url = process.env.NEXT_PUBLIC_SUPABASE_URL; - const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - - if (!url || !anonKey) { - browserClient = null; - return null; - } - - browserClient = createBrowserClient(url, anonKey, { - auth: { - persistSession: true, - autoRefreshToken: true, - detectSessionInUrl: true, - }, - }); - return browserClient; -} diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts deleted file mode 100644 index 2b62ef4..0000000 --- a/lib/supabase/server.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createServerClient } from "@supabase/ssr"; -import { type SupabaseClient } from "@supabase/supabase-js"; -import { cookies } from "next/headers"; - -/** - * Session-bound Supabase client — respects RLS. - * Use for all user-scoped reads (e.g. "my loans", "my profile"). - */ -export async function getServerSupabaseClient(): Promise { - const url = process.env.NEXT_PUBLIC_SUPABASE_URL; - const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - if (!url || !anonKey) { - return null; - } - - const cookieStore = await cookies(); - - return createServerClient(url, anonKey, { - cookies: { - getAll() { - return cookieStore.getAll(); - }, - setAll(cookiesToSet) { - try { - cookiesToSet.forEach(({ name, value, options }) => { - cookieStore.set(name, value, options); - }); - } catch { - // Server Components can run in read-only cookie contexts — safe to ignore. - } - }, - }, - }); -} - -import { createClient } from "@supabase/supabase-js"; - -/** - * Service Role Client — bypasses RLS. - * Use strictly for trusted server-side admin logic where you need elevated privileges. - */ -export function getServiceRoleClient(): SupabaseClient | null { - const url = process.env.NEXT_PUBLIC_SUPABASE_URL; - const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_KEY; - if (!url || !key) { - return null; - } - return createClient(url, key, { - auth: { - autoRefreshToken: false, - persistSession: false, - }, - }); -} \ No newline at end of file diff --git a/lib/utils/formatting.test.ts b/lib/utils/formatting.test.ts index 4bf61bc..039ccbf 100644 --- a/lib/utils/formatting.test.ts +++ b/lib/utils/formatting.test.ts @@ -40,7 +40,7 @@ describe("formatXlm", () => { describe("formatXlmPrecise", () => { it("shows up to 4 decimal places", () => { expect(formatXlmPrecise(12.3456789)).toBe("12.3457 XLM"); - expect(formatXlmPrecise(0.001)).toBe("0.0010 XLM"); + expect(formatXlmPrecise(0.001)).toBe("0.001 XLM"); // trailing zeros beyond 2 decimals are dropped }); it("still shows at least 2 decimal places for whole numbers", () => { diff --git a/lib/utils/formatting.ts b/lib/utils/formatting.ts index b65b3bb..2d14248 100644 --- a/lib/utils/formatting.ts +++ b/lib/utils/formatting.ts @@ -20,10 +20,12 @@ * user's OS locale setting. * - In Node / SSR we fall back to `"en-US"` for deterministic output. */ +/** + * Amounts are always formatted with en-US separators. Reading the browser + * locale here made the server (en-US) and the client (user locale) disagree, + * which produced React hydration mismatches on every amount. + */ function getLocale(): string { - if (typeof navigator !== "undefined" && navigator.language) { - return navigator.language; - } return "en-US"; } diff --git a/lib/webhooks/serialize.ts b/lib/webhooks/serialize.ts new file mode 100644 index 0000000..f2de844 --- /dev/null +++ b/lib/webhooks/serialize.ts @@ -0,0 +1,16 @@ +import type { WebhookEndpoint } from "@/lib/db/schema"; + +/** Wire format (snake_case) shared by the admin API and scripts/webhook-listener.ts. */ +export function serializeWebhook(row: WebhookEndpoint) { + return { + id: row.id, + name: row.name, + url: row.url, + platform: row.platform, + topic: row.topic, + is_active: row.isActive, + created_by: row.createdBy, + created_at: row.createdAt.toISOString(), + updated_at: row.updatedAt.toISOString(), + }; +} diff --git a/next.config.ts b/next.config.ts index da1516b..9174356 100644 --- a/next.config.ts +++ b/next.config.ts @@ -35,17 +35,16 @@ const securityHeaders = [ "script-src 'self' 'unsafe-eval' 'unsafe-inline'", "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", "font-src 'self' https://fonts.gstatic.com", - // Supabase + Stellar APIs + the WalletConnect relay + // Stellar APIs + the WalletConnect relay. The database is only reached + // server-side, so no DB host is needed here. [ "connect-src 'self'", - "https://*.supabase.co", - "wss://*.supabase.co", "https://horizon-testnet.stellar.org", "https://soroban-testnet.stellar.org", "https://friendbot.stellar.org", ...WALLET_CONNECT_CONNECT_SRC, ].join(" "), - ["img-src 'self' data: blob:", "https://*.supabase.co", ...WALLET_IMG_SRC].join(" "), + ["img-src 'self' data: blob:", ...WALLET_IMG_SRC].join(" "), "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", diff --git a/package-lock.json b/package-lock.json index 8a649df..2bf6600 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,17 @@ "version": "0.1.0", "dependencies": { "@creit.tech/stellar-wallets-kit": "^2.5.0", + "@neondatabase/serverless": "^1.1.0", "@radix-ui/react-tooltip": "^1.2.16", "@stellar/freighter-api": "^6.0.1", "@stellar/stellar-sdk": "^16.0.1", - "@supabase/ssr": "^0.10.2", - "@supabase/supabase-js": "^2.103.0", "@upstash/ratelimit": "^2.0.8", "@upstash/redis": "^1.38.0", + "@vercel/blob": "^2.8.0", "clsx": "^2.1.1", + "drizzle-orm": "^0.45.2", "framer-motion": "^12.38.0", + "jose": "^6.2.12", "lucide-react": "^1.8.0", "next": "16.2.6", "next-themes": "^0.4.6", @@ -36,14 +38,17 @@ "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/pdfkit": "^0.17.6", + "@types/pg": "^8.23.1", "@types/react": "^19", "@types/react-dom": "^19", "@types/sanitize-html": "^2.16.1", "@vitest/coverage-v8": "^4.1.9", "cross-env": "^10.1.0", + "drizzle-kit": "^0.31.10", "eslint": "^9", "eslint-config-next": "16.2.3", "husky": "^9.1.7", + "pg": "^8.23.0", "tailwindcss": "^4", "tsx": "^4.19.2", "typescript": "^5", @@ -1038,6 +1043,13 @@ "node": ">=16" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", @@ -1078,27 +1090,22 @@ "dev": true, "license": "MIT" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", "cpu": [ "arm" ], @@ -1109,13 +1116,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", "cpu": [ "arm64" ], @@ -1126,13 +1133,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", "cpu": [ "x64" ], @@ -1143,13 +1150,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "cpu": [ "arm64" ], @@ -1160,13 +1167,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", "cpu": [ "x64" ], @@ -1177,13 +1184,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", "cpu": [ "arm64" ], @@ -1194,13 +1201,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", "cpu": [ "x64" ], @@ -1211,13 +1218,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", "cpu": [ "arm" ], @@ -1228,13 +1235,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", "cpu": [ "arm64" ], @@ -1245,13 +1252,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", "cpu": [ "ia32" ], @@ -1262,13 +1269,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", "cpu": [ "loong64" ], @@ -1279,13 +1286,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", "cpu": [ "mips64el" ], @@ -1296,13 +1303,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", "cpu": [ "ppc64" ], @@ -1313,13 +1320,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", "cpu": [ "riscv64" ], @@ -1330,13 +1337,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", "cpu": [ "s390x" ], @@ -1347,13 +1354,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "cpu": [ "x64" ], @@ -1364,15 +1371,15 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", @@ -1381,13 +1388,13 @@ "netbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", "cpu": [ "x64" ], @@ -1395,16 +1402,33 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "openbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", "cpu": [ "arm64" ], @@ -1412,16 +1436,33 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "cpu": [ "x64" ], @@ -1429,50 +1470,100 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openharmony-arm64": { + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { + "node_modules/@esbuild/android-arm": { "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "sunos" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { + "node_modules/@esbuild/android-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -1480,675 +1571,674 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { + "node_modules/@esbuild/android-x64": { "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "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==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, + "optional": true, + "os": [ + "darwin" + ], "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": ">=18" } }, - "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==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], "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" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@floating-ui/core": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", - "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.12" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@floating-ui/dom": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", - "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.8.0", - "@floating-ui/utils": "^0.2.12" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", - "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.8.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", - "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", - "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==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "optional": true, + "os": [ + "netbsd" + ], "engines": { "node": ">=18" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "openbsd" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "openbsd" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "openharmony" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "sunos" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ - "ppc64" + "ia32" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ - "riscv64" + "x64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "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==", + "dev": true, + "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/libvips" + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], + "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", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], + "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", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@eslint/core": "^0.17.0" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], + "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", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@types/json-schema": "^7.0.15" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "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.17.0 || ^20.3.0 || >=21.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "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.17.0 || ^20.3.0 || >=21.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "url": "https://eslint.org/donate" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], + "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", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "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.17.0 || ^20.3.0 || >=21.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" }, "funding": { - "url": "https://opencollective.com/libvips" + "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" }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@img/sharp-linuxmusl-x64": { + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ - "x64" + "arm64" ], "license": "Apache-2.0", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -2157,108 +2247,466 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, - "node_modules/@img/sharp-wasm32": { + "node_modules/@img/sharp-darwin-x64": { "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ - "wasm32" + "x64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, + "os": [ + "darwin" + ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "win32" + "darwin" ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ - "ia32" + "x64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "win32" + "darwin" ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ - "x64" + "arm" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "win32" + "linux" ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, - "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/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "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/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@jridgewell/resolve-uri": { + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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/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/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==", @@ -2483,6 +2931,15 @@ "tslib": "^2.1.0" } }, + "node_modules/@neondatabase/serverless": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.1.0.tgz", + "integrity": "sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q==", + "license": "MIT", + "engines": { + "node": ">=19.0.0" + } + }, "node_modules/@next/env": { "version": "16.2.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", @@ -4206,21 +4663,280 @@ } } }, - "node_modules/@solana/offchain-messages": { + "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/offchain-messages/-/offchain-messages-5.5.1.tgz", - "integrity": "sha512-g+xHH95prTU+KujtbOzj8wn+C7ZNoiLhf3hj6nYq3MTyxOXtBEysguc97jJveUZG0K97aIKG6xVUlMutg5yxhw==", + "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/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" + "@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" @@ -4234,18 +4950,18 @@ } } }, - "node_modules/@solana/options": { + "node_modules/@solana/rpc-subscriptions-channel-websocket": { "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/options/-/options-5.5.1.tgz", - "integrity": "sha512-eo971c9iLNLmk+yOFyo7yKIJzJ/zou6uKpy6mBuyb/thKtS/haiKIc3VLhyTXty3OH2PW8yOlORJnv4DexJB8A==", + "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/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/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" @@ -4259,12 +4975,18 @@ } } }, - "node_modules/@solana/plugin-core": { + "node_modules/@solana/rpc-subscriptions-spec": { "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/plugin-core/-/plugin-core-5.5.1.tgz", - "integrity": "sha512-VUZl30lDQFJeiSyNfzU1EjYt2QZvoBFKEwjn1lilUJw7KgqD5z7mbV7diJhT+dLFs36i0OsjXvq5kSygn8YJ3A==", + "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" }, @@ -4277,15 +4999,18 @@ } } }, - "node_modules/@solana/programs": { + "node_modules/@solana/rpc-transformers": { "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/programs/-/programs-5.5.1.tgz", - "integrity": "sha512-7U9kn0Jsx1NuBLn5HRTFYh78MV4XN145Yc3WP/q5BlqAVNlMoU9coG5IUTJIG847TUqC1lRto3Dnpwm6T4YRpA==", + "resolved": "https://registry.npmjs.org/@solana/rpc-transformers/-/rpc-transformers-5.5.1.tgz", + "integrity": "sha512-OsWqLCQdcrRJKvHiMmwFhp9noNZ4FARuMkHT5us3ustDLXaxOjF0gfqZLnMkulSLcKt7TGXqMhBV+HCo7z5M8Q==", "license": "MIT", "optional": true, "dependencies": { - "@solana/addresses": "5.5.1", - "@solana/errors": "5.5.1" + "@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" @@ -4299,12 +5024,18 @@ } } }, - "node_modules/@solana/promises": { + "node_modules/@solana/rpc-transport-http": { "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/promises/-/promises-5.5.1.tgz", - "integrity": "sha512-T9lfuUYkGykJmppEcssNiCf6yiYQxJkhiLPP+pyAc2z84/7r3UVIb2tNJk4A9sucS66pzJnVHZKcZVGUUp6wzA==", + "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" }, @@ -4317,22 +5048,26 @@ } } }, - "node_modules/@solana/rpc": { + "node_modules/@solana/rpc-transport-http/node_modules/undici-types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.29.0.tgz", + "integrity": "sha512-vamA8dGlzMwhpyYpQp9d8vka3o4D/yn5I7ez7Or+msDA4bZ8Uh+Zy91WvWf3I73gDAkFha9JcYRqm2li0Npfgg==", + "license": "MIT", + "optional": true + }, + "node_modules/@solana/rpc-types": { "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc/-/rpc-5.5.1.tgz", - "integrity": "sha512-ku8zTUMrkCWci66PRIBC+1mXepEnZH/q1f3ck0kJZ95a06bOTl5KU7HeXWtskkyefzARJ5zvCs54AD5nxjQJ+A==", + "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/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" + "@solana/nominal-types": "5.5.1" }, "engines": { "node": ">=20.18.0" @@ -4346,22 +5081,20 @@ } } }, - "node_modules/@solana/rpc-api": { + "node_modules/@solana/signers": { "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-api/-/rpc-api-5.5.1.tgz", - "integrity": "sha512-XWOQQPhKl06Vj0xi3RYHAc6oEQd8B82okYJ04K7N0Vvy3J4PN2cxeK7klwkjgavdcN9EVkYCChm2ADAtnztKnA==", + "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/codecs-strings": "5.5.1", "@solana/errors": "5.5.1", + "@solana/instructions": "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/nominal-types": "5.5.1", + "@solana/offchain-messages": "5.5.1", "@solana/transaction-messages": "5.5.1", "@solana/transactions": "5.5.1" }, @@ -4377,12 +5110,15 @@ } } }, - "node_modules/@solana/rpc-parsed-types": { + "node_modules/@solana/subscribable": { "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-parsed-types/-/rpc-parsed-types-5.5.1.tgz", - "integrity": "sha512-HEi3G2nZqGEsa3vX6U0FrXLaqnUCg4SKIUrOe8CezD+cSFbRTOn3rCLrUmJrhVyXlHoQVaRO9mmeovk31jWxJg==", + "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" }, @@ -4395,15 +5131,17 @@ } } }, - "node_modules/@solana/rpc-spec": { + "node_modules/@solana/sysvars": { "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-spec/-/rpc-spec-5.5.1.tgz", - "integrity": "sha512-m3LX2bChm3E3by4mQrH4YwCAFY57QBzuUSWqlUw7ChuZ+oLLOq7b2czi4i6L4Vna67j3eCmB3e+4tqy1j5wy7Q==", + "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-spec-types": "5.5.1" + "@solana/rpc-types": "5.5.1" }, "engines": { "node": ">=20.18.0" @@ -4417,12 +5155,24 @@ } } }, - "node_modules/@solana/rpc-spec-types": { + "node_modules/@solana/transaction-confirmation": { "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==", + "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" }, @@ -4435,24 +5185,22 @@ } } }, - "node_modules/@solana/rpc-subscriptions": { + "node_modules/@solana/transaction-messages": { "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions/-/rpc-subscriptions-5.5.1.tgz", - "integrity": "sha512-CTMy5bt/6mDh4tc6vUJms9EcuZj3xvK0/xq8IQ90rhkpYvate91RjBP+egvjgSayUg9yucU9vNuUpEjz4spM7w==", + "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/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" + "@solana/instructions": "5.5.1", + "@solana/nominal-types": "5.5.1", + "@solana/rpc-types": "5.5.1" }, "engines": { "node": ">=20.18.0" @@ -4466,20 +5214,25 @@ } } }, - "node_modules/@solana/rpc-subscriptions-api": { + "node_modules/@solana/transactions": { "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==", + "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/rpc-subscriptions-spec": "5.5.1", - "@solana/rpc-transformers": "5.5.1", + "@solana/nominal-types": "5.5.1", "@solana/rpc-types": "5.5.1", - "@solana/transaction-messages": "5.5.1", - "@solana/transactions": "5.5.1" + "@solana/transaction-messages": "5.5.1" }, "engines": { "node": ">=20.18.0" @@ -4493,2110 +5246,2218 @@ } } }, - "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, + "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/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" + "@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.4.0", + "resolved": "https://registry.npmjs.org/@solana/wallet-standard-features/-/wallet-standard-features-1.4.0.tgz", + "integrity": "sha512-f0tAdqwM2aL6CiFbIgt9h5zKFp+mgY/iNGwoxPMTj9VSTeQj7d1GGSmWhZw0XWoZ4N/1tnKTKmYFq+Dyq08jRw==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.0", + "@wallet-standard/features": "^1.1.0" }, "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" + "node": ">=22" + } + }, + "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/@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" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "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==", + "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", - "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" + "@solana/errors": "2.3.0" }, "engines": { "node": ">=20.18.0" }, "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "typescript": ">=5.3.3" } }, - "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==", + "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", - "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" + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" }, "engines": { "node": ">=20.18.0" }, "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "typescript": ">=5.3.3" } }, - "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==", + "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", - "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" + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" }, "engines": { "node": ">=20.18.0" }, "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "typescript": ">=5.3.3" } }, - "node_modules/@solana/rpc-transport-http/node_modules/undici-types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.29.0.tgz", - "integrity": "sha512-vamA8dGlzMwhpyYpQp9d8vka3o4D/yn5I7ez7Or+msDA4bZ8Uh+Zy91WvWf3I73gDAkFha9JcYRqm2li0Npfgg==", + "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", - "optional": true + "dependencies": { + "safe-buffer": "^5.0.1" + } }, - "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==", + "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", - "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" - }, + "base-x": "^3.0.2" + } + }, + "node_modules/@solana/web3.js/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": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "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, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stellar/freighter-api": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@stellar/freighter-api/-/freighter-api-6.0.1.tgz", + "integrity": "sha512-eqwakEqSg+zoLuPpSbKyrX0pG8DQFzL/J5GtbfuMCmJI+h+oiC9pQ5C6QLc80xopZQKdGt8dUAFCmDMNdAG95w==", + "license": "Apache-2.0", "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" + "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": ">=20.18.0" + "node": ">=10" + } + }, + "node_modules/@stellar/js-xdr": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-4.0.0.tgz", + "integrity": "sha512-+NmNa7Tk5BI5XFdy/6xGTqAN4J9a9KgCrCGhj2uEUTCBhLkch0M+QbKzNH8zEnejWe0p8w+0q5hUVX6L3OzoVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.0.0", + "pnpm": ">=9.0.0" + } + }, + "node_modules/@stellar/stellar-sdk": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-16.0.1.tgz", + "integrity": "sha512-bxKohaiyKVqoudRhbOOHeHhHIaeYV5Zab4rCjxhP4Ty1h1ozTLBOv8lWFnZz9ilBzXG8Bb7usQI3rlEcfvUynA==", + "license": "Apache-2.0", + "dependencies": { + "@noble/ed25519": "^3.1.0", + "@noble/hashes": "^2.2.0", + "@stellar/js-xdr": "4.0.0", + "axios": "1.16.1", + "base32.js": "^0.1.0", + "bignumber.js": "^11.1.1", + "buffer": "^6.0.3", + "commander": "^14.0.3", + "eventsource": "^4.1.0", + "feaxios": "^0.0.23", + "smol-toml": "^1.6.1", + "uint8array-extras": "^1.5.0" }, - "peerDependencies": { - "typescript": "^5.0.0" + "bin": { + "stellar-js": "bin/stellar-js" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">=22.0.0" } }, - "node_modules/@solana/subscribable": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/subscribable/-/subscribable-5.5.1.tgz", - "integrity": "sha512-9K0PsynFq0CsmK1CDi5Y2vUIJpCqkgSS5yfDN0eKPgHqEptLEaia09Kaxc90cSZDZU5mKY/zv1NBmB6Aro9zQQ==", + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@solana/errors": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@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.2" } }, - "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==", + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, "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" + "node": ">= 20" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, - "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==", + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, "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" - }, + "os": [ + "android" + ], "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">= 20" } }, - "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==", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, "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" - }, + "os": [ + "darwin" + ], "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">= 20" } }, - "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==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, "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" - }, + "os": [ + "darwin" + ], "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">= 20" } }, - "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" - }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@solana/web3.js": "^1.98.0" + "node": ">= 20" } }, - "node_modules/@solana/wallet-standard-features": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@solana/wallet-standard-features/-/wallet-standard-features-1.4.0.tgz", - "integrity": "sha512-f0tAdqwM2aL6CiFbIgt9h5zKFp+mgY/iNGwoxPMTj9VSTeQj7d1GGSmWhZw0XWoZ4N/1tnKTKmYFq+Dyq08jRw==", - "license": "Apache-2.0", - "dependencies": { - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/features": "^1.1.0" - }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=22" + "node": ">= 20" } }, - "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==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, "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" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" } }, - "node_modules/@solana/web3.js/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==", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">= 20" } }, - "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==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@solana/errors": "2.3.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" + "node": ">= 20" } }, - "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==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@solana/codecs-core": "2.3.0", - "@solana/errors": "2.3.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" + "node": ">= 20" } }, - "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==", + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "chalk": "^5.4.1", - "commander": "^14.0.0" - }, - "bin": { - "errors": "bin/cli.mjs" + "@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": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.3.3" + "node": ">=14.0.0" } }, - "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==", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.8.1", + "dev": true, + "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { - "safe-buffer": "^5.0.1" + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" } }, - "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", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, "dependencies": { - "bn.js": "^5.2.0", - "bs58": "^4.0.0", - "text-encoding-utf-8": "^1.0.2" + "tslib": "^2.4.0" } }, - "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==", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { - "base-x": "^3.0.2" + "tslib": "^2.4.0" } }, - "node_modules/@solana/web3.js/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==", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "dev": true, + "inBundle": true, "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, - "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==", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", "dev": true, - "license": "MIT" - }, - "node_modules/@stellar/freighter-api": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@stellar/freighter-api/-/freighter-api-6.0.1.tgz", - "integrity": "sha512-eqwakEqSg+zoLuPpSbKyrX0pG8DQFzL/J5GtbfuMCmJI+h+oiC9pQ5C6QLc80xopZQKdGt8dUAFCmDMNdAG95w==", - "license": "Apache-2.0", + "inBundle": true, + "license": "MIT", + "optional": true, "dependencies": { - "buffer": "6.0.3", - "semver": "7.7.1" + "tslib": "^2.4.0" } }, - "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" - }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=10" + "node": ">= 20" } }, - "node_modules/@stellar/js-xdr": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-4.0.0.tgz", - "integrity": "sha512-+NmNa7Tk5BI5XFdy/6xGTqAN4J9a9KgCrCGhj2uEUTCBhLkch0M+QbKzNH8zEnejWe0p8w+0q5hUVX6L3OzoVA==", - "license": "Apache-2.0", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=20.0.0", - "pnpm": ">=9.0.0" + "node": ">= 20" } }, - "node_modules/@stellar/stellar-sdk": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-16.0.1.tgz", - "integrity": "sha512-bxKohaiyKVqoudRhbOOHeHhHIaeYV5Zab4rCjxhP4Ty1h1ozTLBOv8lWFnZz9ilBzXG8Bb7usQI3rlEcfvUynA==", - "license": "Apache-2.0", + "node_modules/@tailwindcss/postcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@noble/ed25519": "^3.1.0", - "@noble/hashes": "^2.2.0", - "@stellar/js-xdr": "4.0.0", - "axios": "1.16.1", - "base32.js": "^0.1.0", - "bignumber.js": "^11.1.1", - "buffer": "^6.0.3", - "commander": "^14.0.3", - "eventsource": "^4.1.0", - "feaxios": "^0.0.23", - "smol-toml": "^1.6.1", - "uint8array-extras": "^1.5.0" - }, - "bin": { - "stellar-js": "bin/stellar-js" + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "postcss": "^8.5.6", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@trezor/connect-common": { + "version": "10.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@trezor/connect-common/-/connect-common-10.0.0-alpha.1.tgz", + "integrity": "sha512-tuvM72XHACogT/fkp9D8wyzTE8h4X9emYUhvFs2pdTyIfTFs5RHDbcHQWFsvJgJoTK0H44XSSJb8pORijRfn9A==", + "license": "MIT", + "dependencies": { + "@trezor/device-utils": "10.0.0-alpha.1", + "@trezor/protobuf": "10.0.0-alpha.1", + "@trezor/protocol": "^10.0.0-alpha.1", + "@trezor/schema-utils": "10.0.0-alpha.1", + "@trezor/type-utils": "10.0.0-alpha.1", + "@trezor/utils": "10.0.0-alpha.1" }, - "engines": { - "node": ">=22.0.0" + "peerDependencies": { + "tslib": "^2.6.2" } }, - "node_modules/@supabase/auth-js": { - "version": "2.103.0", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.103.0.tgz", - "integrity": "sha512-6zAanO6c+6gpHOlt5Lb9TlBBkJdZiUWkWCJKAxzkywBDcwaHlLJKXnjQGX6GyVCyKRR1e7sTq4re/yRTH6U/9A==", - "license": "MIT", + "node_modules/@trezor/connect-plugin-stellar": { + "version": "10.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@trezor/connect-plugin-stellar/-/connect-plugin-stellar-10.0.0-alpha.1.tgz", + "integrity": "sha512-v4lMnnBPxLoV/Yf8F0WmtkZGNsqurpxK62kqvaDeVoAtn5Ud+l6POQkLeQRiGsJYVm0vSDc4LHb5vLMrZTH/KA==", + "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "tslib": "2.8.1" + "@trezor/utils": "10.0.0-alpha.1" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "@stellar/stellar-sdk": "^13.3.0", + "@trezor/connect": "9.x.x", + "tslib": "^2.6.2" } }, - "node_modules/@supabase/functions-js": { - "version": "2.103.0", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.103.0.tgz", - "integrity": "sha512-YrneV2NjskUkkmkZ2Jt2n3elBgbWzV4Y1M9MM370z2Zd5ZPFqFbY8KIoPwuNjtAGE9YrpKBxnbZqeF07BiN9Og==", + "node_modules/@trezor/connect-web": { + "version": "10.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@trezor/connect-web/-/connect-web-10.0.0-alpha.1.tgz", + "integrity": "sha512-CW1ZXzXNVYrJ2WuYneLK2LznkRAfKxRPB3vJyv1U2gia5Q7hsblImITsutLOztO0C2xO1sfETTCyVzfRkRVJEA==", "license": "MIT", "dependencies": { - "tslib": "2.8.1" + "@trezor/connect-common": "10.0.0-alpha.1", + "@trezor/utils": "10.0.0-alpha.1", + "@trezor/websocket-client": "10.0.0-alpha.1" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "tslib": "^2.6.2" } }, - "node_modules/@supabase/phoenix": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz", - "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==", + "node_modules/@trezor/device-utils": { + "version": "10.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@trezor/device-utils/-/device-utils-10.0.0-alpha.1.tgz", + "integrity": "sha512-93LhEqH8uKCZHLtF+62uxd3/PXCfqpK4Z98ztfGu5vrtmoONb9dV8Falha0b3KXPDrQ10TUKasFGKLg570Z/Ew==", "license": "MIT" }, - "node_modules/@supabase/postgrest-js": { - "version": "2.103.0", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.103.0.tgz", - "integrity": "sha512-rC3sRxYdPZymkp2CZR1MiNQgbOleD01bGsW8VxEKRR5nMkLZ1NgAS1QTQf78Wh30czFyk505ZYr9Od8/mWT2TA==", + "node_modules/@trezor/protobuf": { + "version": "10.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-10.0.0-alpha.1.tgz", + "integrity": "sha512-aCUyqAr7Sho9TequaNAjnf27mgae6qzesNrOgB19bUrDrhaDDd/2eyr2SrrY1G4hPYkTyBC0jymvQnozn9f/kA==", "license": "MIT", "dependencies": { - "tslib": "2.8.1" + "@bufbuild/protobuf": "^2.11.0", + "@trezor/schema-utils": "10.0.0-alpha.1" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "tslib": "^2.6.2" } }, - "node_modules/@supabase/realtime-js": { - "version": "2.103.0", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.103.0.tgz", - "integrity": "sha512-gcPtXzZ6izyyBVf2of7K3dEt8CScPJn8VcSlQq6oWL9QoE1kqfQl0oFrOMHd5qrcADewxI7OxxosLB8W4XqtIQ==", + "node_modules/@trezor/protocol": { + "version": "10.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@trezor/protocol/-/protocol-10.0.0-alpha.1.tgz", + "integrity": "sha512-f+rXjtmAdHD93vaLq/QofLJlMP/ZQM4iJqTrcjkUgXKzu8/Jz03tttodBib8A6g9ruGcxLr2smWaRMwwreb8Pg==", "license": "MIT", - "dependencies": { - "@supabase/phoenix": "^0.4.0", - "@types/ws": "^8.18.1", - "tslib": "2.8.1", - "ws": "^8.18.2" - }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "tslib": "^2.6.2" } }, - "node_modules/@supabase/ssr": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.10.2.tgz", - "integrity": "sha512-JFbchN63CXLFHJRNT7udec4/RoD9PmXkSGko3QSO6vUuqGBtSzdmxR7FPfQNr7SuFd65I7Xv46q66ALjEN1cgQ==", + "node_modules/@trezor/schema-utils": { + "version": "10.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@trezor/schema-utils/-/schema-utils-10.0.0-alpha.1.tgz", + "integrity": "sha512-0beYv0b0De3Z60sZzM1T+IrYwXeNL+7RKfLokTvqEFDzsQkwxhYrIX47FsBhGsOmGWx9x4+65dxDbnyd4wZT6g==", "license": "MIT", "dependencies": { - "cookie": "^1.0.2" + "@sinclair/typebox": "^0.34.49", + "@trezor/type-utils": "10.0.0-alpha.1", + "ts-mixer": "^6.0.4" }, "peerDependencies": { - "@supabase/supabase-js": "^2.102.1" + "tslib": "^2.6.2" } }, - "node_modules/@supabase/storage-js": { - "version": "2.103.0", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.103.0.tgz", - "integrity": "sha512-DHmlvdAXwtOmZNbkIZi4lkobPR3XjIzoOgzoz5duMf6G+sDeY015YrzMJCnqdccuYr7X5x4yYuSwF//RoN2dvQ==", + "node_modules/@trezor/type-utils": { + "version": "10.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@trezor/type-utils/-/type-utils-10.0.0-alpha.1.tgz", + "integrity": "sha512-wK75rrwlyXFgPfdHQFRbeKCoj+orQLXKj7rliMK30z+F1y+rEcfv/MW2VP48Q9mL5pea+ZX1nRnb4hYx1v38Ow==", + "license": "MIT" + }, + "node_modules/@trezor/utils": { + "version": "10.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@trezor/utils/-/utils-10.0.0-alpha.1.tgz", + "integrity": "sha512-A+txzREoeX2MO1AqocDBsMNe+R21jTx5kungXGuzeTRRevARLsYh8MPes0170uMjq4qNSgopBnYZK18WyksOmQ==", "license": "MIT", "dependencies": { - "iceberg-js": "^0.8.1", - "tslib": "2.8.1" + "bignumber.js": "^9.3.1" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "tslib": "^2.6.2" } }, - "node_modules/@supabase/supabase-js": { - "version": "2.103.0", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.103.0.tgz", - "integrity": "sha512-j/6q5+LtXbR/YOLSLhy7Na74RD1cV2v+KwIIuuqMEjk1JpLEEyu0ynwDHpGoxMncDQl+R5FogaVqZm+85lZvtw==", + "node_modules/@trezor/utils/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", - "dependencies": { - "@supabase/auth-js": "2.103.0", - "@supabase/functions-js": "2.103.0", - "@supabase/postgrest-js": "2.103.0", - "@supabase/realtime-js": "2.103.0", - "@supabase/storage-js": "2.103.0" - }, "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" + "node": "*" } }, - "node_modules/@tailwindcss/node": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", - "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", - "dev": true, + "node_modules/@trezor/websocket-client": { + "version": "10.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@trezor/websocket-client/-/websocket-client-10.0.0-alpha.1.tgz", + "integrity": "sha512-129Ot1m+fDodRFBvav2d6v8nLxZLAl/udQP5l5W6jxQZcWUuviphDceT/juF8EvNGdw+L6OIu6fH+yCXARpFWQ==", "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.2" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", - "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" + "@trezor/utils": "10.0.0-alpha.1", + "ws": "^8.20.0" }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-x64": "4.2.2", - "@tailwindcss/oxide-freebsd-x64": "4.2.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-x64-musl": "4.2.2", - "@tailwindcss/oxide-wasm32-wasi": "4.2.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + "peerDependencies": { + "tslib": "^2.6.2" } }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", - "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", - "cpu": [ - "arm64" + "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" + } ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "csstype": "^3.1.1" + }, "engines": { - "node": ">= 20" + "node": ">=14.15.0" + }, + "peerDependencies": { + "typescript": "^4.8.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", - "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", - "cpu": [ - "arm64" + "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" + } ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "style-vendorizer": "^2.2.3" + }, "engines": { - "node": ">= 20" + "node": ">=14.15.0" + }, + "peerDependencies": { + "@twind/core": "^1.1.0", + "typescript": "^4.8.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", - "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" + "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": ">= 20" + "node": ">=14.15.0" + }, + "peerDependencies": { + "@twind/core": "^1.1.0", + "typescript": "^4.8.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", - "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", - "cpu": [ - "x64" - ], + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", - "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", - "cpu": [ - "arm" - ], + "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", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", - "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", - "cpu": [ - "arm64" - ], - "dev": true, + "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", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" + "dependencies": { + "@types/node": "*" } }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", - "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", - "cpu": [ - "arm64" - ], + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.2.tgz", + "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" + "dependencies": { + "@types/node": "*" } }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", - "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", - "cpu": [ - "x64" - ], + "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", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } + "license": "MIT" }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", - "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", - "cpu": [ - "x64" - ], + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } + "license": "MIT" }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", - "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], + "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/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", "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" + "undici-types": "~6.21.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.8.1", + "node_modules/@types/pdfkit": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.6.tgz", + "integrity": "sha512-tIwzxk2uWKp0Cq9JIluQXJid77lYhF52EsIOwhsMF4iWLA6YneoBR1xVKYYdAysHuepUB0OX4tdwMiUDdGKmig==", "dev": true, - "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" + "@types/node": "*" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.8.1", + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", "dev": true, - "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", + "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, - "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "csstype": "^3.2.2" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", + "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, - "inBundle": true, "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "peerDependencies": { + "@types/react": "^19.2.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", + "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, - "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "htmlparser2": "^10.1" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true + "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/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", - "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", - "cpu": [ - "arm64" - ], - "dev": true, + "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/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", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" + "dependencies": { + "@types/node": "*" } }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", - "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", + "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/type-utils": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": ">= 20" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@tailwindcss/postcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", - "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "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==", "dev": true, "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.2.2", - "@tailwindcss/oxide": "4.2.2", - "postcss": "^8.5.6", - "tailwindcss": "4.2.2" + "engines": { + "node": ">= 4" } }, - "node_modules/@trezor/connect-common": { - "version": "10.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@trezor/connect-common/-/connect-common-10.0.0-alpha.1.tgz", - "integrity": "sha512-tuvM72XHACogT/fkp9D8wyzTE8h4X9emYUhvFs2pdTyIfTFs5RHDbcHQWFsvJgJoTK0H44XSSJb8pORijRfn9A==", + "node_modules/@typescript-eslint/parser": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", + "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", + "dev": true, "license": "MIT", "dependencies": { - "@trezor/device-utils": "10.0.0-alpha.1", - "@trezor/protobuf": "10.0.0-alpha.1", - "@trezor/protocol": "^10.0.0-alpha.1", - "@trezor/schema-utils": "10.0.0-alpha.1", - "@trezor/type-utils": "10.0.0-alpha.1", - "@trezor/utils": "10.0.0-alpha.1" + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3" }, - "peerDependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@trezor/connect-plugin-stellar": { - "version": "10.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@trezor/connect-plugin-stellar/-/connect-plugin-stellar-10.0.0-alpha.1.tgz", - "integrity": "sha512-v4lMnnBPxLoV/Yf8F0WmtkZGNsqurpxK62kqvaDeVoAtn5Ud+l6POQkLeQRiGsJYVm0vSDc4LHb5vLMrZTH/KA==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@trezor/utils": "10.0.0-alpha.1" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@stellar/stellar-sdk": "^13.3.0", - "@trezor/connect": "9.x.x", - "tslib": "^2.6.2" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@trezor/connect-web": { - "version": "10.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@trezor/connect-web/-/connect-web-10.0.0-alpha.1.tgz", - "integrity": "sha512-CW1ZXzXNVYrJ2WuYneLK2LznkRAfKxRPB3vJyv1U2gia5Q7hsblImITsutLOztO0C2xO1sfETTCyVzfRkRVJEA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", + "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", + "dev": true, "license": "MIT", "dependencies": { - "@trezor/connect-common": "10.0.0-alpha.1", - "@trezor/utils": "10.0.0-alpha.1", - "@trezor/websocket-client": "10.0.0-alpha.1" + "@typescript-eslint/tsconfig-utils": "^8.58.1", + "@typescript-eslint/types": "^8.58.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "tslib": "^2.6.2" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@trezor/device-utils": { - "version": "10.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@trezor/device-utils/-/device-utils-10.0.0-alpha.1.tgz", - "integrity": "sha512-93LhEqH8uKCZHLtF+62uxd3/PXCfqpK4Z98ztfGu5vrtmoONb9dV8Falha0b3KXPDrQ10TUKasFGKLg570Z/Ew==", - "license": "MIT" - }, - "node_modules/@trezor/protobuf": { - "version": "10.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-10.0.0-alpha.1.tgz", - "integrity": "sha512-aCUyqAr7Sho9TequaNAjnf27mgae6qzesNrOgB19bUrDrhaDDd/2eyr2SrrY1G4hPYkTyBC0jymvQnozn9f/kA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", + "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", + "dev": true, "license": "MIT", "dependencies": { - "@bufbuild/protobuf": "^2.11.0", - "@trezor/schema-utils": "10.0.0-alpha.1" + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1" }, - "peerDependencies": { - "tslib": "^2.6.2" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@trezor/protocol": { - "version": "10.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@trezor/protocol/-/protocol-10.0.0-alpha.1.tgz", - "integrity": "sha512-f+rXjtmAdHD93vaLq/QofLJlMP/ZQM4iJqTrcjkUgXKzu8/Jz03tttodBib8A6g9ruGcxLr2smWaRMwwreb8Pg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", + "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", + "dev": true, "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, "peerDependencies": { - "tslib": "^2.6.2" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@trezor/schema-utils": { - "version": "10.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@trezor/schema-utils/-/schema-utils-10.0.0-alpha.1.tgz", - "integrity": "sha512-0beYv0b0De3Z60sZzM1T+IrYwXeNL+7RKfLokTvqEFDzsQkwxhYrIX47FsBhGsOmGWx9x4+65dxDbnyd4wZT6g==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", + "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", + "dev": true, "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.34.49", - "@trezor/type-utils": "10.0.0-alpha.1", - "ts-mixer": "^6.0.4" + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "tslib": "^2.6.2" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@trezor/type-utils": { - "version": "10.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@trezor/type-utils/-/type-utils-10.0.0-alpha.1.tgz", - "integrity": "sha512-wK75rrwlyXFgPfdHQFRbeKCoj+orQLXKj7rliMK30z+F1y+rEcfv/MW2VP48Q9mL5pea+ZX1nRnb4hYx1v38Ow==", - "license": "MIT" + "node_modules/@typescript-eslint/types": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", + "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, - "node_modules/@trezor/utils": { - "version": "10.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@trezor/utils/-/utils-10.0.0-alpha.1.tgz", - "integrity": "sha512-A+txzREoeX2MO1AqocDBsMNe+R21jTx5kungXGuzeTRRevARLsYh8MPes0170uMjq4qNSgopBnYZK18WyksOmQ==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", + "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", + "dev": true, "license": "MIT", "dependencies": { - "bignumber.js": "^9.3.1" + "@typescript-eslint/project-service": "8.58.1", + "@typescript-eslint/tsconfig-utils": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "tslib": "^2.6.2" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@trezor/utils/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==", + "node_modules/@typescript-eslint/typescript-estree/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==", + "dev": true, "license": "MIT", "engines": { - "node": "*" + "node": "18 || 20 || >=22" } }, - "node_modules/@trezor/websocket-client": { - "version": "10.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@trezor/websocket-client/-/websocket-client-10.0.0-alpha.1.tgz", - "integrity": "sha512-129Ot1m+fDodRFBvav2d6v8nLxZLAl/udQP5l5W6jxQZcWUuviphDceT/juF8EvNGdw+L6OIu6fH+yCXARpFWQ==", + "node_modules/@typescript-eslint/typescript-estree/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==", + "dev": true, "license": "MIT", "dependencies": { - "@trezor/utils": "10.0.0-alpha.1", - "ws": "^8.20.0" + "balanced-match": "^4.0.2" }, - "peerDependencies": { - "tslib": "^2.6.2" + "engines": { + "node": "18 || 20 || >=22" } }, - "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", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "csstype": "^3.1.1" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=14.15.0" + "node": "18 || 20 || >=22" }, - "peerDependencies": { - "typescript": "^4.8.4" + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">=10" } }, - "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" - } - ], + "node_modules/@typescript-eslint/utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", + "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", + "dev": true, "license": "MIT", "dependencies": { - "style-vendorizer": "^2.2.3" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1" }, "engines": { - "node": ">=14.15.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "@twind/core": "^1.1.0", - "typescript": "^4.8.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "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" - } - ], + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", + "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "eslint-visitor-keys": "^5.0.0" + }, "engines": { - "node": ">=14.15.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "@twind/core": "^1.1.0", - "typescript": "^4.8.4" + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "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, - "dependencies": { - "tslib": "^2.4.0" - } + "os": [ + "android" + ] }, - "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==", + "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", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "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", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@types/conventional-commits-parser": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.2.tgz", - "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==", + "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, - "dependencies": { - "@types/node": "*" - } + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "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==", + "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" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "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" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "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==", + "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" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "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" - }, - "node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/pdfkit": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.6.tgz", - "integrity": "sha512-tIwzxk2uWKp0Cq9JIluQXJid77lYhF52EsIOwhsMF4iWLA6YneoBR1xVKYYdAysHuepUB0OX4tdwMiUDdGKmig==", + "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", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "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==", + "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", - "dependencies": { - "csstype": "^3.2.2" - } + "optional": true, + "os": [ + "linux" + ] }, - "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==", + "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", - "peerDependencies": { - "@types/react": "^19.2.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "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==", + "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", - "dependencies": { - "htmlparser2": "^10.1" - } - }, - "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" + "optional": true, + "os": [ + "linux" + ] }, - "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/@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/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "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", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", - "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", + "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": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.58.1", - "@typescript-eslint/type-utils": "8.58.1", - "@typescript-eslint/utils": "8.58.1", - "@typescript-eslint/visitor-keys": "8.58.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" + "@napi-rs/wasm-runtime": "^0.2.11" }, "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.58.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=14.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==", + "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", - "engines": { - "node": ">= 4" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@typescript-eslint/parser": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", - "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", + "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/@upstash/core-analytics": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@upstash/core-analytics/-/core-analytics-0.0.10.tgz", + "integrity": "sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ==", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.58.1", - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/typescript-estree": "8.58.1", - "@typescript-eslint/visitor-keys": "8.58.1", - "debug": "^4.4.3" + "@upstash/redis": "^1.28.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": ">=16.0.0" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", - "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", - "dev": true, + "node_modules/@upstash/ratelimit": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@upstash/ratelimit/-/ratelimit-2.0.8.tgz", + "integrity": "sha512-YSTMBJ1YIxsoPkUMX/P4DDks/xV5YYCswWMamU8ZIfK9ly6ppjRnVOyBhMDXBmzjODm4UQKcxsJPvaeFAijp5w==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.58.1", - "@typescript-eslint/types": "^8.58.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@upstash/core-analytics": "^0.0.10" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "@upstash/redis": "^1.34.3" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", - "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", - "dev": true, + "node_modules/@upstash/redis": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.38.0.tgz", + "integrity": "sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/visitor-keys": "8.58.1" + "uncrypto": "^0.1.3" + } + }, + "node_modules/@vercel/blob": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@vercel/blob/-/blob-2.8.0.tgz", + "integrity": "sha512-Nu+HWKpkgovCh/ezlG7wCVwF7RErTzLzZMbGKFBdGBCbTKyK+s5VXPLl+0+TpNEQPH8AVaGzOpIsXUOtkqylCQ==", + "license": "Apache-2.0", + "dependencies": { + "@vercel/oidc": "^3.6.1", + "async-retry": "^1.3.3", + "is-buffer": "^2.0.5", + "is-node-process": "^1.2.0", + "throttleit": "^2.1.0", + "undici": "^6.23.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", - "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", - "dev": true, + "node_modules/@vercel/blob/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "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", "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": ">=4" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", - "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", - "dev": true, + "node_modules/@vercel/cli-config": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@vercel/cli-config/-/cli-config-0.2.6.tgz", + "integrity": "sha512-2AsKCf6gE/Eniq09ARm1RK9rQMV5pcAHU7BFcR9MISO+4/RsjjkaYbQN6G85/xi9pYZ5CJNXvYn4TI6p1jKBcg==", + "license": "Apache-2.0", + "dependencies": { + "xdg-app-paths": "5", + "zod": "4.1.11" + } + }, + "node_modules/@vercel/cli-config/node_modules/zod": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@vercel/cli-exec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vercel/cli-exec/-/cli-exec-1.0.1.tgz", + "integrity": "sha512-g9XerViJ/paZujufXYcu5XYI2vU2rtB4sgdpjUHde5RnOkdmpu0ngH46LCFGHoPXO/C+qDPSczIHIRN+8Q2YKQ==", + "license": "Apache-2.0", "dependencies": { - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/typescript-estree": "8.58.1", - "@typescript-eslint/utils": "8.58.1", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" + "execa": "5.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">= 18" + } + }, + "node_modules/@vercel/oidc": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.8.7.tgz", + "integrity": "sha512-fRu59npOu+1vsV570tiLKskfuRyOJ1MqceAkXbTIfWPnM+c9ve1tSK81oj4umKKirymlGUss3V60Lm5I38rKDw==", + "license": "Apache-2.0", + "dependencies": { + "@vercel/cli-config": "0.2.6", + "@vercel/cli-exec": "1.0.1", + "jose": "^5.9.6" }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "engines": { + "node": ">= 20" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", - "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", - "dev": true, + "node_modules/@vercel/oidc/node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://github.com/sponsors/panva" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", - "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", + "node_modules/@vitest/coverage-v8": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", + "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.58.1", - "@typescript-eslint/tsconfig-utils": "8.58.1", - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/visitor-keys": "8.58.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.9", + "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": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/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==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" + "@vitest/browser": "4.1.9", + "vitest": "4.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, - "node_modules/@typescript-eslint/typescript-estree/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==", + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": "18 || 20 || >=22" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/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" + "url": "https://opencollective.com/vitest" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", - "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.58.1", - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/typescript-estree": "8.58.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "tinyrainbow": "^3.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" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", - "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.1", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" } }, - "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==", + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://opencollective.com/vitest" } }, - "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" - ], + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "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" - ], + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "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/@wallet-standard/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.1.tgz", + "integrity": "sha512-gggIHTtxicF9XFMQ12DkfS6NAG92Ak795JeSA7f2whAQ6Y3AkMWWuCMxSZXG2NIPN42kEaZSNVjqMsJRaJRxMQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + } }, - "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/@wallet-standard/features": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.1.tgz", + "integrity": "sha512-aCWYmVeSCGViyEU5k7GMoW8zxE4Gs+C1s1Pp2XLesvSNlnZ4PMES9HUnTB3hl0b3RVj7C61yze3IWyrncqg4MA==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.1" + }, + "engines": { + "node": ">=22" + } }, - "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/@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/@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/@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/@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, + "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", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" + } }, - "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/@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/@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, + "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", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "tslib": "1.14.1" + } }, - "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/@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/@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, + "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", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "keyvaluestorage-interface": "^1.0.0", + "tslib": "1.14.1" + } }, - "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, + "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", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@walletconnect/events": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "events": "^3.3.0" + } }, - "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, + "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", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.6", + "@walletconnect/safe-json": "^1.0.1", + "cross-fetch": "^3.1.4", + "events": "^3.3.0" + } }, - "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, + "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", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.8", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0" + } }, - "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, + "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", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "events": "^3.3.0", + "keyvaluestorage-interface": "^1.0.0" + } }, - "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, + "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", - "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" + "@walletconnect/environment": "^1.0.1", + "@walletconnect/jsonrpc-types": "^1.0.3", + "tslib": "1.14.1" } }, - "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, + "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", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.6", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0", + "ws": "^7.5.1" + } }, - "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, + "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "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/@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, + "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", - "optional": true, - "os": [ - "win32" - ] + "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/@upstash/core-analytics": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@upstash/core-analytics/-/core-analytics-0.0.10.tgz", - "integrity": "sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ==", + "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": { - "@upstash/redis": "^1.28.3" - }, - "engines": { - "node": ">=16.0.0" + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" } }, - "node_modules/@upstash/ratelimit": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@upstash/ratelimit/-/ratelimit-2.0.8.tgz", - "integrity": "sha512-YSTMBJ1YIxsoPkUMX/P4DDks/xV5YYCswWMamU8ZIfK9ly6ppjRnVOyBhMDXBmzjODm4UQKcxsJPvaeFAijp5w==", + "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": { - "@upstash/core-analytics": "^0.0.10" - }, - "peerDependencies": { - "@upstash/redis": "^1.34.3" + "@walletconnect/jsonrpc-types": "^1.0.2" } }, - "node_modules/@upstash/redis": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.38.0.tgz", - "integrity": "sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg==", + "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": { - "uncrypto": "^0.1.3" + "@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/@vitest/coverage-v8": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", - "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", - "dev": true, + "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": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.9", - "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" + "@noble/hashes": "1.7.0" }, - "peerDependencies": { - "@vitest/browser": "4.1.9", - "vitest": "4.1.9" + "engines": { + "node": "^14.21.3 || >=16" }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", - "dev": true, + "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", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" + "engines": { + "node": "^14.21.3 || >=16" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", - "dev": true, + "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": { - "@vitest/spy": "4.1.9", - "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 - } + "tslib": "1.14.1" } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", - "dev": true, - "license": "MIT", + "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": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "@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/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", - "dev": true, + "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": { - "@vitest/utils": "4.1.9", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" } }, - "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", - "dev": true, - "license": "MIT", + "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": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "@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/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", - "dev": true, + "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", - "funding": { - "url": "https://opencollective.com/vitest" + "dependencies": { + "tslib": "1.14.1" } }, - "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", - "dev": true, - "license": "MIT", + "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": { - "@vitest/pretty-format": "4.1.9", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "@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/@wallet-standard/base": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.1.tgz", - "integrity": "sha512-gggIHTtxicF9XFMQ12DkfS6NAG92Ak795JeSA7f2whAQ6Y3AkMWWuCMxSZXG2NIPN42kEaZSNVjqMsJRaJRxMQ==", - "license": "Apache-2.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": ">=22" + "node": ">= 18" } }, - "node_modules/@wallet-standard/features": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.1.tgz", - "integrity": "sha512-aCWYmVeSCGViyEU5k7GMoW8zxE4Gs+C1s1Pp2XLesvSNlnZ4PMES9HUnTB3hl0b3RVj7C61yze3IWyrncqg4MA==", - "license": "Apache-2.0", + "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": { - "@wallet-standard/base": "^1.1.1" + "@noble/hashes": "1.8.0" }, "engines": { - "node": ">=22" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "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" - }, + "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": ">=16" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "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==", + "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", @@ -6605,15 +7466,15 @@ "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "3.0.0", + "@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.0", - "@walletconnect/utils": "2.23.0", + "@walletconnect/types": "2.23.7", + "@walletconnect/utils": "2.23.7", "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.39.3", + "es-toolkit": "1.44.0", "events": "3.3.0", "uint8arrays": "3.1.1" }, @@ -6621,213 +7482,277 @@ "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", + "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/safe-json": "^1.0.2", - "pino": "10.0.0" + "@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/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==", + "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.0", + "@walletconnect/logger": "3.0.2", "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", + "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": { - "tslib": "1.14.1" + "@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/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==", + "node_modules/@walletconnect/universal-provider/node_modules/abitype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.3.0.tgz", + "integrity": "sha512-fk6Te+bojIFrMvMZrnOO+SxCB+RUksTGOzq/60ZRvs1L+BVzvi2bqt9L3W/17ZLdZsyM1FuYf65P5nlmoiH1Bg==", "license": "MIT", - "dependencies": { - "keyvaluestorage-interface": "^1.0.0", - "tslib": "1.14.1" + "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/@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/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/heartbeat": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", - "integrity": "sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==", + "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": { - "@walletconnect/events": "^1.0.1", - "@walletconnect/time": "^1.0.2", - "events": "^3.3.0" + "@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/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==", + "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": { - "@walletconnect/jsonrpc-utils": "^1.0.6", - "@walletconnect/safe-json": "^1.0.1", - "cross-fetch": "^3.1.4", - "events": "^3.3.0" + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "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==", + "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": { - "@walletconnect/jsonrpc-utils": "^1.0.8", - "@walletconnect/safe-json": "^1.0.2", - "events": "^3.3.0" + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "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==", + "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", - "dependencies": { - "events": "^3.3.0", - "keyvaluestorage-interface": "^1.0.0" + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "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==", + "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/environment": "^1.0.1", - "@walletconnect/jsonrpc-types": "^1.0.3", - "tslib": "1.14.1" + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" } }, - "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", + "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/jsonrpc-utils": "^1.0.6", - "@walletconnect/safe-json": "^1.0.2", - "events": "^3.3.0", - "ws": "^7.5.1" + "@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/jsonrpc-ws-connection/node_modules/ws": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", - "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "node_modules/@walletconnect/utils/node_modules/abitype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.3.0.tgz", + "integrity": "sha512-fk6Te+bojIFrMvMZrnOO+SxCB+RUksTGOzq/60ZRvs1L+BVzvi2bqt9L3W/17ZLdZsyM1FuYf65P5nlmoiH1Bg==", "license": "MIT", - "engines": { - "node": ">=8.3.0" + "funding": { + "url": "https://github.com/sponsors/wevm" }, "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" }, "peerDependenciesMeta": { - "bufferutil": { + "typescript": { "optional": true }, - "utf-8-validate": { + "zod": { "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==", + "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": { - "@walletconnect/safe-json": "^1.0.1", - "idb-keyval": "^6.2.1", - "unstorage": "^1.9.0" + "@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": { - "@react-native-async-storage/async-storage": "1.x" + "typescript": ">=5.4.0" }, "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { + "typescript": { "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==", + "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.7.0" + "@noble/hashes": "1.8.0" }, "engines": { "node": "^14.21.3 || >=16" @@ -6836,756 +7761,737 @@ "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==", + "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", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "dependencies": { + "tslib": "1.14.1" } }, - "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==", + "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/safe-json/node_modules/tslib": { + "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/@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/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/@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==", + "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", - "dependencies": { - "@walletconnect/safe-json": "^1.0.2", - "pino": "10.0.0" + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.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/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/@walletconnect/time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", - "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", "dependencies": { - "tslib": "1.14.1" + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" } }, - "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", + "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": { - "@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" + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.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", + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", "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" + "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/@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", + "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": ">= 18" + "node": ">=8" } }, - "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==", + "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": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" + "color-convert": "^2.0.1" }, - "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" + "node": ">=8" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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", + "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": { - "@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" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": ">=18.20.8" + "node": ">= 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/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.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, - "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", + "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==", + "dev": true, + "license": "MIT", "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" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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", + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true + }, + "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==", + "dev": true, + "license": "MIT", "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" + "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/@walletconnect/universal-provider/node_modules/abitype": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.3.0.tgz", - "integrity": "sha512-fk6Te+bojIFrMvMZrnOO+SxCB+RUksTGOzq/60ZRvs1L+BVzvi2bqt9L3W/17ZLdZsyM1FuYf65P5nlmoiH1Bg==", + "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==", + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" + "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" }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3.22.0 || ^4.0.0" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "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" - } - ], + "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==", + "dev": true, "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" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, - "peerDependencies": { - "typescript": ">=5.4.0" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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==", + "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 0.4" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/ljharb" } }, - "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", + "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==", + "dev": true, + "license": "MIT", "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" + "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/@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==", + "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==", + "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "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": "^14.21.3 || >=16" + "node": ">= 0.4" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=12" } }, - "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==", + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", + "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", + "dev": true, "license": "MIT", "dependencies": { - "@walletconnect/safe-json": "^1.0.2", - "pino": "10.0.0" + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^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/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/@walletconnect/utils/node_modules/abitype": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.3.0.tgz", - "integrity": "sha512-fk6Te+bojIFrMvMZrnOO+SxCB+RUksTGOzq/60ZRvs1L+BVzvi2bqt9L3W/17ZLdZsyM1FuYf65P5nlmoiH1Bg==", + "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==", + "dev": true, "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 - } + "engines": { + "node": ">= 0.4" } }, - "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" - } - ], + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", "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 - } + "retry": "0.13.1" } }, - "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==", + "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==", + "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "possible-typed-array-names": "^1.0.0" }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 0.4" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/ljharb" } }, - "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/axe-core": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.2.tgz", + "integrity": "sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" } }, - "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==", + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "license": "MIT", "dependencies": { - "@walletconnect/window-getters": "^1.0.1", - "tslib": "1.14.1" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, - "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/abitype": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.6.tgz", - "integrity": "sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==", - "license": "MIT", + "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, - "funding": { - "url": "https://github.com/sponsors/wevm" + "dependencies": { + "is-retry-allowed": "^2.2.0" }, "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } + "axios": "0.x || 1.x" } }, - "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, + "node_modules/axios-retry/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", - "bin": { - "acorn": "bin/acorn" + "optional": true, + "engines": { + "node": ">=10" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=0.4.0" + "node": ">= 0.4" } }, - "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==", + "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/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", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "engines": { + "node": ">=0.12.0" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" + "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.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" }, "engines": { - "node": ">= 6.0.0" + "node": ">=6.0.0" } }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "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", - "dependencies": { - "humanize-ms": "^1.2.1" - }, "engines": { - "node": ">= 8.0.0" + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bigjs" } }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "node_modules/bignumber.js": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.4.tgz", + "integrity": "sha512-AJ9dSeaUGj2xu7tEwmdqb51dqdb633xo4njI9K8ZFfcLrNr0XN8/EPkkZUNaF9fkCblGt2zVwZymesUdGynEkQ==", + "license": "MIT" + }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "license": "MIT" + }, + "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/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.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", + "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.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "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-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" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "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==", + "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": { - "color-convert": "^2.0.1" + "fill-range": "^7.1.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==", - "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==", - "dev": true, - "license": "Python-2.0" + "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/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" } }, - "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==", - "dev": true, + "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==", "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" + "pako": "~1.0.5" } }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true - }, - "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==", + "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": { - "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" + "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" }, - "engines": { - "node": ">= 0.4" + "bin": { + "browserslist": "cli.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "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==", - "dev": true, + "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": { - "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" + "base-x": "^5.0.0" } }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, + "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": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "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==", + "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==", "dev": true, + "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": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" + "node-gyp-build": "^4.3.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6.14.2" } }, - "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==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" + "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" @@ -7594,37 +8500,28 @@ "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", + "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", - "es-shim-unscopables": "^1.0.2" + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" } }, - "node_modules/arraybuffer.prototype.slice": { + "node_modules/call-bound": { "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==", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "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" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -7633,384 +8530,379 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "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": ">=12" - } - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/ast-v8-to-istanbul": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", - "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" + "node": ">=6" } }, - "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==", - "dev": true, + "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": ">= 0.4" + "node": ">=6" } }, - "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/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "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/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==", + "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": ">=8.0.0" + "node": ">=18" } }, - "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==", + "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": { - "possible-typed-array-names": "^1.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/axe-core": { - "version": "4.11.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.2.tgz", - "integrity": "sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==", - "dev": true, - "license": "MPL-2.0", + "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": ">=4" + "node": "*" } }, - "node_modules/axios": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", - "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "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": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "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, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "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": { - "is-retry-allowed": "^2.2.0" - }, - "peerDependencies": { - "axios": "0.x || 1.x" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" } }, - "node_modules/axios-retry/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==", + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", "license": "MIT", - "optional": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.8" } }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", + "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": ">= 0.4" + "node": ">=6" } }, - "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/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==", + "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": ">=0.12.0" + "node": ">=7.0.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" - } - ], + "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/baseline-browser-mapping": { - "version": "2.10.16", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", - "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" + "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": ">=6.0.0" + "node": ">= 0.8" } }, - "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==", + "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": "*" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/bigjs" + "node": ">=20" } }, - "node_modules/bignumber.js": { - "version": "11.1.4", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.4.tgz", - "integrity": "sha512-AJ9dSeaUGj2xu7tEwmdqb51dqdb633xo4njI9K8ZFfcLrNr0XN8/EPkkZUNaF9fkCblGt2zVwZymesUdGynEkQ==", - "license": "MIT" - }, - "node_modules/bintrees": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", - "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", - "license": "MIT" - }, - "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/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/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } }, - "node_modules/bn.js": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", - "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "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/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } }, - "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", "dev": true, - "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" } }, - "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==", + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", "dev": true, - "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" }, "engines": { - "node": ">=8" + "node": ">=16" } }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "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/brotli": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", - "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", - "license": "MIT", + "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/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, "dependencies": { - "base64-js": "^1.1.2" + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "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==", - "license": "MIT", + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz", + "integrity": "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==", + "dev": true, "dependencies": { - "pako": "~1.0.5" + "jiti": "2.6.1" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=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==", + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", "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" + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" }, "bin": { - "browserslist": "cli.js" + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=20" } }, - "node_modules/bs58": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", - "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "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": { - "base-x": "^5.0.0" + "node-fetch": "^2.7.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" - } - ], + "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": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "uncrypto": "^0.1.3" } }, - "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" - }, + "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": ">=6.14.2" + "node": "*" } }, - "node_modules/call-bind": { + "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/damerau-levenshtein": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/dargs": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", "dev": true, "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" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8019,28 +8911,34 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-bind-apply-helpers": { + "node_modules/data-view-byte-length": { "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==", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, "license": "MIT", "dependencies": { + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" } }, - "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==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -8049,681 +8947,875 @@ "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, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "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" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "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": ">=6" + "node": ">=0.10.0" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001787", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", - "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", - "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/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "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/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": ">=18" + "node": ">=0.10.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==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "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": "*" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "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==", + "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^5.0.0" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { - "node": ">= 20.19.0" + "node": ">= 0.4" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "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/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/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/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "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.8" + "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/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", + "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-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", "engines": { - "node": ">=6" + "node": ">=8" } }, - "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", + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "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==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "color-name": "~1.1.4" + "esutils": "^2.0.2" }, "engines": { - "node": ">=7.0.0" + "node": ">=0.10.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/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "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": { - "delayed-stream": "~1.0.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, - "engines": { - "node": ">= 0.8" + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "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", + "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": ">=20" + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, + "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": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/conventional-changelog-angular": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", - "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", - "dev": true, + "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": { - "compare-func": "^2.0.0" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, - "engines": { - "node": ">=16" + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/conventional-changelog-conventionalcommits": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", - "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "dev": true, "dependencies": { - "compare-func": "^2.0.0" + "is-obj": "^2.0.0" }, "engines": { - "node": ">=16" + "node": ">=8" } }, - "node_modules/conventional-commits-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", - "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", "dev": true, + "license": "MIT", "dependencies": { - "is-text-path": "^2.0.0", - "JSONStream": "^1.3.5", - "meow": "^12.0.1", - "split2": "^4.0.0" + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" }, "bin": { - "conventional-commits-parser": "cli.mjs" - }, - "engines": { - "node": ">=16" + "drizzle-kit": "bin.cjs" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "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/drizzle-kit/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=18" } }, - "node_modules/cosmiconfig-typescript-loader": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz", - "integrity": "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==", + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "jiti": "2.6.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=v18" - }, - "peerDependencies": { - "@types/node": "*", - "cosmiconfig": ">=9", - "typescript": ">=5" + "node": ">=18" } }, - "node_modules/cross-env": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", - "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@epic-web/invariant": "^1.0.0", - "cross-spawn": "^7.0.6" - }, - "bin": { - "cross-env": "dist/bin/cross-env.js", - "cross-env-shell": "dist/bin/cross-env-shell.js" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=20" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 8" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, "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", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "*" + "node": ">=18" } }, - "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/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-2-Clause" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/dargs": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", - "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "node_modules/dayjs": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", - "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "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", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=0.4.0" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">= 0.8" + "node": ">=18" } }, - "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-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, - "license": "Apache-2.0", + "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/dfa": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", - "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "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==", + "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, "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", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node": ">=18" } }, - "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" - } + "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" ], - "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": ">=18" } }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "node_modules/drizzle-kit/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, - "dependencies": { - "is-obj": "^2.0.0" + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } } }, "node_modules/dunder-proto": { @@ -9547,6 +10639,29 @@ "node": ">=18.0.0" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "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/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -9937,6 +11052,18 @@ "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -10254,6 +11381,15 @@ "node": ">= 6" } }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -10278,15 +11414,6 @@ "url": "https://github.com/sponsors/typicode" } }, - "node_modules/iceberg-js": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", - "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/idb-keyval": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", @@ -10664,6 +11791,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -10769,6 +11902,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -10889,7 +12034,6 @@ "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-ws": { @@ -11052,11 +12196,10 @@ } }, "node_modules/jose": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz", - "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==", + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", "license": "MIT", - "optional": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -11766,6 +12909,12 @@ "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==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -11811,6 +12960,15 @@ "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -12101,6 +13259,18 @@ "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==", + "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", @@ -12258,6 +13428,21 @@ "node": ">=14.0.0" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "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", @@ -12276,6 +13461,15 @@ "node": ">= 0.8.0" } }, + "node_modules/os-paths": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/os-paths/-/os-paths-4.4.0.tgz", + "integrity": "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==", + "license": "MIT", + "engines": { + "node": ">= 6.0" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -12434,7 +13628,6 @@ "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" @@ -12480,6 +13673,103 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "dev": true, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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", @@ -12637,6 +13927,49 @@ "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/preact": { "version": "10.29.7", "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.7.tgz", @@ -12959,6 +14292,15 @@ "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", "license": "MIT" }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -13333,7 +14675,6 @@ "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" @@ -13346,7 +14687,6 @@ "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" @@ -13435,6 +14775,12 @@ "dev": true, "license": "ISC" }, + "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==", + "license": "ISC" + }, "node_modules/slow-redact": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/slow-redact/-/slow-redact-0.3.2.tgz", @@ -13462,6 +14808,16 @@ "atomic-sleep": "^1.0.0" } }, + "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", @@ -13471,6 +14827,17 @@ "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", @@ -13685,6 +15052,15 @@ "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -13818,6 +15194,18 @@ "real-require": "^0.2.0" } }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -14187,6 +15575,15 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, + "node_modules/undici": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -14788,7 +16185,6 @@ "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" @@ -14957,6 +16353,41 @@ } } }, + "node_modules/xdg-app-paths": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/xdg-app-paths/-/xdg-app-paths-5.5.1.tgz", + "integrity": "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ==", + "license": "MIT", + "dependencies": { + "os-paths": "^4.0.1", + "xdg-portable": "^7.2.0" + }, + "engines": { + "node": ">= 6.0" + } + }, + "node_modules/xdg-portable": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/xdg-portable/-/xdg-portable-7.3.0.tgz", + "integrity": "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==", + "license": "MIT", + "dependencies": { + "os-paths": "^4.0.1" + }, + "engines": { + "node": ">= 6.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": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index ab488ca..492a195 100644 --- a/package.json +++ b/package.json @@ -23,19 +23,24 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio" }, "dependencies": { "@creit.tech/stellar-wallets-kit": "^2.5.0", + "@neondatabase/serverless": "^1.1.0", "@radix-ui/react-tooltip": "^1.2.16", "@stellar/freighter-api": "^6.0.1", "@stellar/stellar-sdk": "^16.0.1", - "@supabase/ssr": "^0.10.2", - "@supabase/supabase-js": "^2.103.0", "@upstash/ratelimit": "^2.0.8", "@upstash/redis": "^1.38.0", + "@vercel/blob": "^2.8.0", "clsx": "^2.1.1", + "drizzle-orm": "^0.45.2", "framer-motion": "^12.38.0", + "jose": "^6.2.12", "lucide-react": "^1.8.0", "next": "16.2.6", "next-themes": "^0.4.6", @@ -54,14 +59,17 @@ "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/pdfkit": "^0.17.6", + "@types/pg": "^8.23.1", "@types/react": "^19", "@types/react-dom": "^19", "@types/sanitize-html": "^2.16.1", "@vitest/coverage-v8": "^4.1.9", "cross-env": "^10.1.0", + "drizzle-kit": "^0.31.10", "eslint": "^9", "eslint-config-next": "16.2.3", "husky": "^9.1.7", + "pg": "^8.23.0", "tailwindcss": "^4", "tsx": "^4.19.2", "typescript": "^5", diff --git a/proxy.ts b/proxy.ts index a6e2f43..4cd5760 100644 --- a/proxy.ts +++ b/proxy.ts @@ -1,6 +1,6 @@ -import { createServerClient } from "@supabase/ssr"; import { type NextRequest, NextResponse } from "next/server"; import { getDashboardPath, normalizeUserRole } from "@/lib/auth/roles"; +import { SESSION_COOKIE_NAME, verifySessionToken } from "@/lib/auth/session-token"; import { recordRequestMetrics } from "@/lib/monitoring/metrics"; import { enforceGlobalApiRateLimit } from "@/lib/rate-limit"; @@ -42,51 +42,18 @@ export async function proxy(request: NextRequest) { } } - // ── ③ Supabase cookie-based session check (NO NETWORK CALL) ───────────────── - // We use getSession() here because it reads the JWT from the cookie locally. - // getUser() makes a live Supabase network call on every request and is the - // cause of the 10 s connect-timeout errors. Full JWT verification happens - // inside requireAuthenticatedUser() in each protected page/API route. - const url = process.env.NEXT_PUBLIC_SUPABASE_URL; - const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - - if (!url || !anonKey) { - return NextResponse.next({ request }); - } - - let supabaseResponse = NextResponse.next({ request }); - - const supabase = createServerClient(url, anonKey, { - cookies: { - getAll() { - return request.cookies.getAll(); - }, - setAll(cookiesToSet) { - cookiesToSet.forEach(({ name, value }) => { - request.cookies.set(name, value); - }); - supabaseResponse = NextResponse.next({ request }); - cookiesToSet.forEach(({ name, value, options }) => { - supabaseResponse.cookies.set(name, value, options); - }); - }, - }, - }); - - // Securely get user via Supabase Auth server to prevent session spoofing warnings - const { data: { user } } = await supabase.auth.getUser(); - - const effectiveUser = bypassActive - ? { - id: bypassUserId, - user_metadata: { account_type: normalizeUserRole(bypassRoleRaw) }, - } - : user ?? null; + // ── ③ Session cookie check (NO NETWORK CALL) ──────────────────────────────── + // The cookie is a signed JWT, so the edge can verify it locally. Whether the + // user still exists is checked by requireAuthenticatedUser() in each + // protected page / API route. + const claims = bypassActive + ? { sub: bypassUserId, role: normalizeUserRole(bypassRoleRaw) } + : await verifySessionToken(request.cookies.get(SESSION_COOKIE_NAME)?.value); const isDashboardPath = pathname === "/dashboard" || pathname.startsWith("/dashboard/"); const isAuthEntryPath = pathname === "/auth"; - if (isDashboardPath && !effectiveUser) { + if (isDashboardPath && !claims) { const duration = (performance.now() - start) / 1000; recordRequestMetrics(method, pathname, 302, duration); const redirectUrl = request.nextUrl.clone(); @@ -95,21 +62,20 @@ export async function proxy(request: NextRequest) { return NextResponse.redirect(redirectUrl); } - if (isAuthEntryPath && effectiveUser) { + if (isAuthEntryPath && claims) { const duration = (performance.now() - start) / 1000; recordRequestMetrics(method, pathname, 302, duration); const redirectUrl = request.nextUrl.clone(); - const role = normalizeUserRole(effectiveUser.user_metadata?.account_type); - redirectUrl.pathname = getDashboardPath(role); + redirectUrl.pathname = getDashboardPath(normalizeUserRole(claims.role)); redirectUrl.search = ""; return NextResponse.redirect(redirectUrl); } - return supabaseResponse; + return NextResponse.next({ request }); } export const config = { matcher: [ "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", ], -}; \ No newline at end of file +}; diff --git a/scripts/backup.sh b/scripts/backup.sh index 27ef952..97f04aa 100644 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -26,7 +26,9 @@ if [[ -f .env ]]; then fi # Required. -# DATABASE_URL postgres://... connection string (direct, not pooled) +# DATABASE_URL postgres://... connection string. On Neon use the +# DIRECT (non-pooler) host, or set BACKUP_DATABASE_URL +# to it and keep DATABASE_URL pooled for the app. # BACKUP_ENCRYPTION_KEY passphrase for AES-256; store in a password manager, # NOT only in CI — losing it makes every backup useless # S3_BUCKET destination bucket name (no s3:// prefix) @@ -35,7 +37,7 @@ BACKUP_S3_PREFIX="${BACKUP_S3_PREFIX:-backups}" BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}" BACKUP_DRY_RUN="${BACKUP_DRY_RUN:-0}" # Comma-separated schemas to skip. Empty by default: a backup missing data is -# worse than one carrying extra. See docs/disaster-recovery.md for Supabase notes. +# worse than one carrying extra. See docs/disaster-recovery.md for Neon notes. BACKUP_EXCLUDE_SCHEMAS="${BACKUP_EXCLUDE_SCHEMAS:-}" # Minimum plausible dump size; guards against silently archiving an empty file. BACKUP_MIN_BYTES="${BACKUP_MIN_BYTES:-1024}" @@ -103,7 +105,7 @@ if [[ -n "$BACKUP_EXCLUDE_SCHEMAS" ]]; then [[ -n "$schema" ]] && dump_args+=(--exclude-schema="$schema") done < <(tr ',' '\n' <<<"$BACKUP_EXCLUDE_SCHEMAS" | tr -d ' ') fi -pg_dump "$DATABASE_URL" "${dump_args[@]}" +pg_dump "${BACKUP_DATABASE_URL:-$DATABASE_URL}" "${dump_args[@]}" # ── 2. Verify the dump ──────────────────────────────────────────────────────── # A truncated or empty archive uploads just as happily as a good one, so check diff --git a/scripts/deploy-testnet.ts b/scripts/deploy-testnet.ts index 640c48c..0c4dc8c 100644 --- a/scripts/deploy-testnet.ts +++ b/scripts/deploy-testnet.ts @@ -776,7 +776,7 @@ function writeEnvFiles( } // Back up before rewriting a file that already had content, so a bad merge - // can never cost someone their Supabase keys. + // can never cost someone their database credentials. if (existed && current.trim() !== "") { fs.writeFileSync(`${envFilePath}.bak`, current); } diff --git a/scripts/e2e-seed-and-run.mjs b/scripts/e2e-seed-and-run.mjs index eed376d..7c45adf 100644 --- a/scripts/e2e-seed-and-run.mjs +++ b/scripts/e2e-seed-and-run.mjs @@ -1,6 +1,20 @@ +#!/usr/bin/env node +/** + * scripts/e2e-seed-and-run.mjs + * + * Seeds three accounts (borrower, lender, admin) and a liquidity pool directly + * in Postgres, then exercises the running app's dashboards and API routes with + * the dev auth-bypass headers (ENABLE_DEV_AUTH_BYPASS=true on the server). + * + * npm run e2e:seed # against http://localhost:3000 + * E2E_BASE_URL=... npm run e2e:seed + * + * Requires DATABASE_URL in .env.local (or the environment). + */ + import fs from "node:fs"; import path from "node:path"; -import { createClient } from "@supabase/supabase-js"; +import pg from "pg"; function loadEnv(filePath) { const env = {}; @@ -45,240 +59,193 @@ async function http(method, url, headers = {}, body) { return { status: res.status, payload }; } -async function listAllUsers(supabase) { - const users = []; - let page = 1; - const perPage = 200; - - while (true) { - const { data, error } = await supabase.auth.admin.listUsers({ page, perPage }); - if (error) throw error; - const batch = data?.users ?? []; - users.push(...batch); - if (batch.length < perPage) break; - page += 1; - } - - return users; -} - -async function ensureUser(supabase, email, role, fullName) { - const allUsers = await listAllUsers(supabase); - let user = allUsers.find((u) => (u.email ?? "").toLowerCase() === email.toLowerCase()); - - if (!user) { - const { data, error } = await supabase.auth.admin.createUser({ - email, - password: "TempPass123!", - email_confirm: true, - user_metadata: { account_type: role, full_name: fullName }, - }); - if (error) throw error; - user = data.user; - } - - const { error: profileError } = await supabase.from("profiles").upsert({ - id: user.id, - full_name: fullName, - role, - kyc_status: "verified", - risk_status: "low", - }); - if (profileError) throw profileError; +/** Deterministic, valid-looking Stellar public keys for the seeded accounts. */ +const E2E_WALLETS = { + borrower: "GE2EBORROWERADDRESS0000000000000000000000000000000000000", + lender: "GE2ELENDERADDRESS00000000000000000000000000000000000000", + admin: "GE2EADMINADDRESS000000000000000000000000000000000000000", +}; + +/** Upsert a users + profiles pair keyed by wallet address; returns the user. */ +async function ensureUser(client, walletAddress, email, role, fullName) { + const { rows } = await client.query( + `insert into users (wallet_address, role, email, last_sign_in_at) + values ($1, $2, $3, now()) + on conflict (wallet_address) do update set role = excluded.role, email = excluded.email + returning id, email`, + [walletAddress, role, email], + ); + const user = rows[0]; + + await client.query( + `insert into profiles (id, full_name, role, wallet_address, kyc_status, risk_status) + values ($1, $2, $3, $4, 'verified', 'low') + on conflict (id) do update + set full_name = excluded.full_name, role = excluded.role, + wallet_address = excluded.wallet_address, + kyc_status = 'verified', risk_status = 'low'`, + [user.id, fullName, role, walletAddress], + ); - return user; + return { id: user.id, email: user.email, walletAddress }; } async function main() { const cwd = process.cwd(); - const envPath = path.join(cwd, ".env.local"); - const env = loadEnv(envPath); - - const url = env.NEXT_PUBLIC_SUPABASE_URL; - const serviceKey = env.SUPABASE_SERVICE_ROLE_KEY; + const env = { ...loadEnv(path.join(cwd, ".env.local")), ...process.env }; - if (!url || !serviceKey) { - throw new Error("Missing NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY in .env.local"); + const databaseUrl = env.DATABASE_URL; + if (!databaseUrl) { + throw new Error("Missing DATABASE_URL in .env.local"); } - const supabase = createClient(url, serviceKey, { - auth: { persistSession: false, autoRefreshToken: false }, - }); + const client = new pg.Client({ connectionString: databaseUrl }); + await client.connect(); console.log("Starting seeded E2E run...\n"); - const borrower = await ensureUser(supabase, "e2e.borrower@trustlend.local", "borrower", "E2E Borrower"); - const lender = await ensureUser(supabase, "e2e.lender@trustlend.local", "lender", "E2E Lender"); - const admin = await ensureUser(supabase, "souvikmandal2406@gmail.com", "admin", "E2E Admin"); - - // Ensure borrower can request loans. - const { error: repErr } = await supabase.from("reputation_snapshots").upsert({ - user_id: borrower.id, - score_total: 300, - repayment_score: 80, - lending_score: 20, - consistency_score: 90, - external_score: 40, - reputation_level: "silver", - }); - if (repErr) throw repErr; - - let poolId = null; - const { data: existingPool } = await supabase - .from("lending_pools") - .select("id") - .eq("status", "active") - .limit(1) - .maybeSingle(); - - if (existingPool?.id) { - poolId = existingPool.id; - } else { - const { data: createdPool, error: poolErr } = await supabase - .from("lending_pools") - .insert({ - name: "E2E Liquidity Pool", - description: "Seeded pool for automated E2E", - status: "active", - currency: "XLM", - apr_bps: 1200, - total_liquidity: 10000, - available_liquidity: 10000, - total_borrowed: 0, - created_by: lender.id, - }) - .select("id") - .single(); - if (poolErr) throw poolErr; - poolId = createdPool.id; - } - - const base = process.env.E2E_BASE_URL ?? "http://localhost:3000"; - const borrowerHeaders = { - "x-dev-user-id": borrower.id, - "x-dev-role": "borrower", - "x-dev-email": borrower.email, - }; - const lenderHeaders = { - "x-dev-user-id": lender.id, - "x-dev-role": "lender", - "x-dev-email": lender.email, - }; - const adminHeaders = { - "x-dev-user-id": admin.id, - "x-dev-role": "admin", - "x-dev-email": admin.email, - }; - - let pass = 0; - let total = 0; - const check = (name, ok, detail) => { - total += 1; - if (ok) pass += 1; - printResult(name, ok, detail); - }; - - const bDash = await http("GET", `${base}/dashboard/borrower`, borrowerHeaders); - check("Borrower dashboard", bDash.status === 200, `status=${bDash.status}`); - - const lDash = await http("GET", `${base}/dashboard/lender`, lenderHeaders); - check("Lender dashboard", lDash.status === 200, `status=${lDash.status}`); - - const aDash = await http("GET", `${base}/dashboard/admin`, adminHeaders); - check("Admin dashboard", aDash.status === 200, `status=${aDash.status}`); - - const apply = await http("POST", `${base}/api/loans/apply`, borrowerHeaders, { - amount: 120, - durationDays: 30, - }); - const loanId = apply?.payload?.loan?.id; - check("Borrower apply loan", apply.status === 201 && !!loanId, `status=${apply.status}`); - - const deposit = await http("POST", `${base}/api/pools/deposit`, lenderHeaders, { - poolId, - amount: 250, - txHash: "e2e-deposit-tx-001", - lenderAddress: "GE2ELENDERADDRESS0000000000000000000000000000000000000000000", - }); - const positionId = deposit?.payload?.position?.id; - check("Lender deposit", deposit.status === 201 && !!positionId, `status=${deposit.status}`); - - const withdraw = await http("POST", `${base}/api/pools/withdraw`, lenderHeaders, { - positionId, - amount: 50, - }); - check("Lender withdraw", withdraw.status === 200, `status=${withdraw.status}`); - - const repayPartial = await http("POST", `${base}/api/loans/repay`, borrowerHeaders, { - loanId, - amount: 30, - txHash: "e2e-repay-partial-001", - borrowerAddress: "GE2EBORROWERADDRESS0000000000000000000000000000000000000000", - }); - check("Borrower partial repayment", repayPartial.status === 201, `status=${repayPartial.status}`); - - const repayFull = await http("POST", `${base}/api/loans/repay`, borrowerHeaders, { - loanId, - amount: 500, - txHash: "e2e-repay-full-001", - borrowerAddress: "GE2EBORROWERADDRESS0000000000000000000000000000000000000000", - }); - check("Borrower full repayment", repayFull.status === 201, `status=${repayFull.status}`); - - // Verify DB updates are real and persisted. - const { data: loanRow } = await supabase - .from("loans") - .select("id, status, repaid_amount") - .eq("id", loanId) - .maybeSingle(); - check("Loan status persisted", !!loanRow && ["active", "repaid"].includes(loanRow.status), `status=${loanRow?.status ?? "none"}`); - - const { data: posRow } = await supabase - .from("pool_positions") - .select("id, principal_amount, withdrawn_amount") - .eq("id", positionId) - .maybeSingle(); - check( - "Position update persisted", - !!posRow && Number(posRow.withdrawn_amount ?? 0) >= 50, - `withdrawn=${posRow?.withdrawn_amount ?? "none"}`, - ); - - const { data: ledgerRows } = await supabase - .from("ledger_transactions") - .select("id, category") - .eq("user_id", lender.id) - .in("category", ["deposit", "withdrawal"]) - .limit(20); - check( - "Ledger tx recorded", - (ledgerRows ?? []).some((r) => r.category === "deposit") && (ledgerRows ?? []).some((r) => r.category === "withdrawal"), - `rows=${(ledgerRows ?? []).length}`, - ); + try { + const borrower = await ensureUser(client, E2E_WALLETS.borrower, "e2e.borrower@trustlend.local", "borrower", "E2E Borrower"); + const lender = await ensureUser(client, E2E_WALLETS.lender, "e2e.lender@trustlend.local", "lender", "E2E Lender"); + const admin = await ensureUser(client, E2E_WALLETS.admin, "e2e.admin@trustlend.local", "admin", "E2E Admin"); + + // Ensure borrower can request loans. + await client.query( + `insert into reputation_snapshots (user_id, score_total, repayment_score, lending_score, consistency_score, external_score, reputation_level) + values ($1, 300, 80, 20, 90, 40, 'silver') + on conflict (user_id) do update set score_total = 300`, + [borrower.id], + ); + + let poolId = null; + const existingPool = await client.query(`select id from lending_pools where status = 'active' limit 1`); + if (existingPool.rows[0]?.id) { + poolId = existingPool.rows[0].id; + } else { + const created = await client.query( + `insert into lending_pools (name, description, status, currency, apr_bps, total_liquidity, available_liquidity, total_borrowed, created_by) + values ('E2E Liquidity Pool', 'Seeded pool for automated E2E', 'active', 'XLM', 1200, 10000, 10000, 0, $1) + returning id`, + [lender.id], + ); + poolId = created.rows[0].id; + } + + const base = env.E2E_BASE_URL ?? "http://localhost:3000"; + const borrowerHeaders = { "x-dev-user-id": borrower.id, "x-dev-role": "borrower" }; + const lenderHeaders = { "x-dev-user-id": lender.id, "x-dev-role": "lender" }; + const adminHeaders = { "x-dev-user-id": admin.id, "x-dev-role": "admin" }; + + let pass = 0; + let total = 0; + const check = (name, ok, detail) => { + total += 1; + if (ok) pass += 1; + printResult(name, ok, detail); + }; + + const bDash = await http("GET", `${base}/dashboard/borrower`, borrowerHeaders); + check("Borrower dashboard", bDash.status === 200, `status=${bDash.status}`); + + const lDash = await http("GET", `${base}/dashboard/lender`, lenderHeaders); + check("Lender dashboard", lDash.status === 200, `status=${lDash.status}`); + + const aDash = await http("GET", `${base}/dashboard/admin`, adminHeaders); + check("Admin dashboard", aDash.status === 200, `status=${aDash.status}`); + + const apply = await http("POST", `${base}/api/loans/apply`, borrowerHeaders, { + amount: 120, + durationDays: 30, + }); + const loanId = apply?.payload?.loan?.id; + check("Borrower apply loan", apply.status === 201 && !!loanId, `status=${apply.status}`); + + const deposit = await http("POST", `${base}/api/pools/deposit`, lenderHeaders, { + poolId, + amount: 250, + txHash: `e2e-deposit-tx-${Date.now()}`, + lenderAddress: lender.walletAddress, + }); + const positionId = deposit?.payload?.position?.id; + check("Lender deposit", deposit.status === 201 && !!positionId, `status=${deposit.status}`); - const adminKyc = await http("GET", `${base}/dashboard/admin/kyc`, adminHeaders); - check("Admin KYC page", adminKyc.status === 200, `status=${adminKyc.status}`); + const withdraw = await http("POST", `${base}/api/pools/withdraw`, lenderHeaders, { + positionId, + amount: 50, + }); + check("Lender withdraw", withdraw.status === 200, `status=${withdraw.status}`); - const adminUsers = await http("GET", `${base}/dashboard/admin/users`, adminHeaders); - check("Admin users page", adminUsers.status === 200, `status=${adminUsers.status}`); + const repayPartial = await http("POST", `${base}/api/loans/repay`, borrowerHeaders, { + loanId, + amount: 30, + txHash: `e2e-repay-partial-${Date.now()}`, + borrowerAddress: borrower.walletAddress, + }); + check("Borrower partial repayment", repayPartial.status === 201, `status=${repayPartial.status}`); - const roleMismatch = await http("POST", `${base}/api/pools/deposit`, borrowerHeaders, { - poolId, - amount: 10, - }); - check("Role mismatch guard", roleMismatch.status === 307, `status=${roleMismatch.status}`); + const repayFull = await http("POST", `${base}/api/loans/repay`, borrowerHeaders, { + loanId, + amount: 500, + txHash: `e2e-repay-full-${Date.now()}`, + borrowerAddress: borrower.walletAddress, + }); + check("Borrower full repayment", repayFull.status === 201, `status=${repayFull.status}`); + + // Verify DB updates are real and persisted. + const loanRow = (await client.query(`select id, status, repaid_amount from loans where id = $1`, [loanId])).rows[0]; + check( + "Loan status persisted", + !!loanRow && ["active", "repaid"].includes(loanRow.status), + `status=${loanRow?.status ?? "none"}`, + ); + + const posRow = ( + await client.query(`select id, principal_amount, withdrawn_amount from pool_positions where id = $1`, [positionId]) + ).rows[0]; + check( + "Position update persisted", + !!posRow && Number(posRow.withdrawn_amount ?? 0) >= 50, + `withdrawn=${posRow?.withdrawn_amount ?? "none"}`, + ); + + const ledgerRows = ( + await client.query( + `select id, category from ledger_transactions where user_id = $1 and category in ('deposit', 'withdrawal') limit 20`, + [lender.id], + ) + ).rows; + check( + "Ledger tx recorded", + ledgerRows.some((r) => r.category === "deposit") && ledgerRows.some((r) => r.category === "withdrawal"), + `rows=${ledgerRows.length}`, + ); + + const adminKyc = await http("GET", `${base}/dashboard/admin/kyc`, adminHeaders); + check("Admin KYC page", adminKyc.status === 200, `status=${adminKyc.status}`); + + const adminUsers = await http("GET", `${base}/dashboard/admin/users`, adminHeaders); + check("Admin users page", adminUsers.status === 200, `status=${adminUsers.status}`); + + const roleMismatch = await http("POST", `${base}/api/pools/deposit`, borrowerHeaders, { + poolId, + amount: 10, + }); + check("Role mismatch guard", roleMismatch.status === 307, `status=${roleMismatch.status}`); - const invalidApply = await http("POST", `${base}/api/loans/apply`, borrowerHeaders, { - amount: 0, - durationDays: 30, - }); - check("Input validation guard", invalidApply.status === 400, `status=${invalidApply.status}`); + const invalidApply = await http("POST", `${base}/api/loans/apply`, borrowerHeaders, { + amount: 0, + durationDays: 30, + }); + check("Input validation guard", invalidApply.status === 400, `status=${invalidApply.status}`); - console.log("\nSummary"); - console.log(`Passed: ${pass}/${total}`); + console.log("\nSummary"); + console.log(`Passed: ${pass}/${total}`); - if (pass !== total) { - process.exitCode = 1; + if (pass !== total) { + process.exitCode = 1; + } + } finally { + await client.end(); } } diff --git a/scripts/liquidation-keeper.ts b/scripts/liquidation-keeper.ts index 04a853f..af24bb0 100644 --- a/scripts/liquidation-keeper.ts +++ b/scripts/liquidation-keeper.ts @@ -9,7 +9,7 @@ // cron invocation. // // Flow: -// 1. Fetch open (Active) loans — from Supabase (`--source=db`, default) or +// 1. Fetch open (Active) loans — from the database (`--source=db`, default) or // directly from the LendingContract (`--source=chain`). // 2. For each loan, read its authoritative on-chain record (collateral + // remaining debt), the borrower's reputation score, and the dynamic @@ -23,23 +23,25 @@ // ── Usage ──────────────────────────────────────────────────────────────────── // npm run liquidation:keeper # one-shot run (cron-friendly) // npm run liquidation:keeper -- --dry-run # evaluate only, never submit -// npm run liquidation:keeper -- --source=chain # bypass Supabase entirely +// npm run liquidation:keeper -- --source=chain # bypass the database entirely // npm run liquidation:keeper -- --interval=60 # background service, poll every 60s // npm run liquidation:keeper:service # shorthand: poll every minute -// POST /api/cron/liquidation (Vercel Cron, * * * * *) — deployed worker, see -// vercel.json + docs/liquidation-keeper.md +// POST /api/cron/liquidation — deployed worker (GitHub Actions every 5 min + +// a daily Vercel Cron safety net), see docs/liquidation-keeper.md // // ── Required env ───────────────────────────────────────────────────────────── // ADMIN_SECRET_KEY, NEXT_PUBLIC_LENDING_CONTRACT_ID, // NEXT_PUBLIC_REPUTATION_CONTRACT_ID, NEXT_PUBLIC_ADMIN_ADDRESS -// (+ NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY for --source=db) +// (+ DATABASE_URL for --source=db) // See `.env.example` for the full LIQUIDATION_* configuration surface. // ============================================================================= import fs from "node:fs"; import path from "node:path"; import process from "node:process"; -import { createClient, type SupabaseClient } from "@supabase/supabase-js"; +import { and, eq, inArray } from "drizzle-orm"; +import { getDb, type Db } from "@/lib/db/client"; +import { ledgerTransactions, loans } from "@/lib/db/schema"; import type { Keypair } from "@stellar/stellar-sdk"; import { addr, @@ -104,8 +106,7 @@ export interface KeeperConfig { defaultAssetVolatilityBps: number; slackWebhookUrl?: string; discordWebhookUrl?: string; - supabaseUrl?: string; - supabaseServiceKey?: string; + databaseUrl?: string; } function loadPriceTable(): Record { @@ -153,9 +154,7 @@ export function loadConfig(argv: string[] = process.argv.slice(2)): KeeperConfig ), slackWebhookUrl: process.env.LIQUIDATION_SLACK_WEBHOOK_URL || undefined, discordWebhookUrl: process.env.LIQUIDATION_DISCORD_WEBHOOK_URL || undefined, - supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL, - supabaseServiceKey: - process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_KEY, + databaseUrl: process.env.DATABASE_URL, }; } @@ -335,17 +334,13 @@ async function checkLiquidationEligibility( // ─── Candidate discovery ────────────────────────────────────────────────────── -/** Resolve an on-chain loan id for a Supabase loan row from its funding ledger entry. */ -async function resolveOnchainLoanId( - supabase: SupabaseClient, - dbLoanId: string -): Promise { - const { data } = await supabase - .from("ledger_transactions") - .select("metadata") - .eq("ref_type", "loan_fund") - .eq("ref_id", dbLoanId) - .maybeSingle(); +/** Resolve an on-chain loan id for a database loan row from its funding ledger entry. */ +async function resolveOnchainLoanId(db: Db, dbLoanId: string): Promise { + const [data] = await db + .select({ metadata: ledgerTransactions.metadata }) + .from(ledgerTransactions) + .where(and(eq(ledgerTransactions.refType, "loan_fund"), eq(ledgerTransactions.refId, dbLoanId))) + .limit(1); const raw = data?.metadata; const meta: Record | null = @@ -365,25 +360,21 @@ function safeJsonParse(s: string): Record | null { } async function fetchCandidatesFromDb(cfg: KeeperConfig): Promise { - if (!cfg.supabaseUrl || !cfg.supabaseServiceKey) { + const db = cfg.databaseUrl ? getDb() : null; + if (!db) { throw new Error( - "Supabase is not configured (NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY) " + - '— use --source=chain to bypass the database.' + "The database is not configured (DATABASE_URL) — use --source=chain to bypass it." ); } - const supabase = createClient(cfg.supabaseUrl, cfg.supabaseServiceKey, { - auth: { autoRefreshToken: false, persistSession: false }, - }); - const { data, error } = await supabase - .from("loans") - .select("id") - .in("status", ["active", "funded"]); - if (error) throw new Error(`Failed to query open loans: ${error.message}`); + const rows = await db + .select({ id: loans.id }) + .from(loans) + .where(inArray(loans.status, ["active", "funded"])); const onchainIds: number[] = []; - for (const row of data ?? []) { - const onchainId = await resolveOnchainLoanId(supabase, String(row.id)); + for (const row of rows) { + const onchainId = await resolveOnchainLoanId(db, row.id); if (onchainId) onchainIds.push(onchainId); } return onchainIds; diff --git a/scripts/webhook-listener.ts b/scripts/webhook-listener.ts index 0dc022d..f81cce9 100644 --- a/scripts/webhook-listener.ts +++ b/scripts/webhook-listener.ts @@ -3,8 +3,10 @@ import fs from "node:fs"; import path from "node:path"; import process from "node:process"; -import { createClient } from "@supabase/supabase-js"; +import { eq } from "drizzle-orm"; import { rpc, xdr, scValToNative } from "@stellar/stellar-sdk"; +import { getDb } from "@/lib/db/client"; +import { webhookEndpoints } from "@/lib/db/schema"; // ─── .env loader ───────────────────────────────────────────────────────────── function loadEnv(filePath: string): void { @@ -25,21 +27,16 @@ loadEnv(path.resolve(process.cwd(), ".env.local")); loadEnv(path.resolve(process.cwd(), ".env.contracts")); const RPC_URL = process.env.NEXT_PUBLIC_SOROBAN_RPC_URL || "https://soroban-testnet.stellar.org"; -const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL; -const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_KEY; const LENDING_CONTRACT_ID = process.env.NEXT_PUBLIC_LENDING_CONTRACT_ID; const LARGE_LOAN_THRESHOLD_XLM = parseInt(process.env.LARGE_LOAN_THRESHOLD_XLM || "10000", 10); const POLL_INTERVAL_MS = 5000; -if (!SUPABASE_URL || !SUPABASE_SERVICE_KEY) { - throw new Error("Supabase credentials not configured."); +const db = getDb(); +if (!db) { + throw new Error("DATABASE_URL is not configured."); } -const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY, { - auth: { autoRefreshToken: false, persistSession: false }, -}); - const server = new rpc.Server(RPC_URL, { allowHttp: RPC_URL.startsWith("http://") }); /** Formats a Discord embed message */ @@ -100,16 +97,14 @@ function formatTelegramPayload(topic: string, eventData: Record /** Broadcasts message to all active webhooks subscribed to the topic */ async function dispatchWebhooks(topic: string, eventData: Record) { - const { data: webhooks, error } = await supabase - .from("webhook_endpoints") - .select("*") - .eq("is_active", true); - - if (error || !webhooks) { - console.error("[webhooks-listener] Failed to fetch webhooks", error?.message); + let webhooks: Array<{ name: string; topic: string; platform: string; url: string }>; + try { + webhooks = await db!.select().from(webhookEndpoints).where(eq(webhookEndpoints.isActive, true)); + } catch (err) { + console.error("[webhooks-listener] Failed to fetch webhooks", err instanceof Error ? err.message : err); return; } - + if (webhooks.length === 0) return; const discordPayload = formatDiscordPayload(topic, eventData); diff --git a/sql/01_core_schema.sql b/sql/01_core_schema.sql deleted file mode 100644 index 390b7a8..0000000 --- a/sql/01_core_schema.sql +++ /dev/null @@ -1,545 +0,0 @@ --- TrustLend core schema for Supabase --- Apply first - -create extension if not exists pgcrypto; - --- ========================= --- Enums --- ========================= - -do $$ begin - create type public.app_role as enum ('borrower', 'lender', 'admin'); -exception - when duplicate_object then null; -end $$; - -do $$ begin - create type public.kyc_status as enum ('pending', 'submitted', 'verified', 'rejected'); -exception - when duplicate_object then null; -end $$; - -alter type public.kyc_status add value if not exists 'submitted'; - -do $$ begin - create type public.risk_status as enum ('low', 'medium', 'high', 'blocked'); -exception - when duplicate_object then null; -end $$; - -do $$ begin - create type public.loan_status as enum ('requested', 'approved', 'funded', 'active', 'repaid', 'defaulted', 'cancelled'); -exception - when duplicate_object then null; -end $$; - -do $$ begin - create type public.pool_status as enum ('active', 'paused', 'closed'); -exception - when duplicate_object then null; -end $$; - -do $$ begin - create type public.position_status as enum ('active', 'closed'); -exception - when duplicate_object then null; -end $$; - -do $$ begin - create type public.tx_status as enum ('pending', 'confirmed', 'failed', 'cancelled'); -exception - when duplicate_object then null; -end $$; - -do $$ begin - create type public.verification_status as enum ('pending', 'verified', 'rejected', 'expired'); -exception - when duplicate_object then null; -end $$; - -do $$ begin - create type public.task_status as enum ('open', 'assigned', 'completed', 'verified', 'cancelled'); -exception - when duplicate_object then null; -end $$; - -do $$ begin - create type public.task_difficulty as enum ('easy', 'medium', 'hard'); -exception - when duplicate_object then null; -end $$; - -do $$ begin - create type public.risk_decision as enum ('allow', 'manual_review', 'reject'); -exception - when duplicate_object then null; -end $$; - --- ========================= --- Utility functions --- ========================= - -create or replace function public.set_updated_at() -returns trigger -language plpgsql -as $$ -begin - new.updated_at = now(); - return new; -end; -$$; - -create or replace function public.handle_new_user_profile() -returns trigger -language plpgsql -security definer -set search_path = public -as $$ -declare - derived_role public.app_role; -begin - derived_role := case - when new.raw_user_meta_data ->> 'account_type' in ('borrower', 'lender', 'admin') - then (new.raw_user_meta_data ->> 'account_type')::public.app_role - else 'borrower'::public.app_role - end; - - insert into public.profiles ( - id, - full_name, - role, - kyc_status, - risk_status - ) - values ( - new.id, - coalesce(new.raw_user_meta_data ->> 'full_name', ''), - derived_role, - 'pending'::public.kyc_status, - 'medium'::public.risk_status - ) - on conflict (id) do nothing; - - return new; -exception - when others then - return new; -end; -$$; - --- ========================= --- Core identity/profile --- ========================= - -create table if not exists public.profiles ( - id uuid primary key references auth.users(id) on delete cascade, - full_name text not null default '', - role public.app_role not null default 'borrower', - wallet_address text, - country_code text, - phone text, - kyc_status public.kyc_status not null default 'pending', - risk_status public.risk_status not null default 'medium', - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_profiles_role on public.profiles(role); -create index if not exists idx_profiles_wallet_address on public.profiles(wallet_address); -create index if not exists idx_profiles_kyc_status on public.profiles(kyc_status); -create index if not exists idx_profiles_risk_status on public.profiles(risk_status); - --- ========================= --- Reputation --- ========================= - -create table if not exists public.reputation_events ( - id uuid primary key default gen_random_uuid(), - user_id uuid not null references public.profiles(id) on delete cascade, - source_type text not null, - source_id uuid, - source_key text, - points_delta integer not null, - reason text not null, - metadata jsonb not null default '{}'::jsonb, - created_at timestamptz not null default now() -); - -create index if not exists idx_rep_events_user_id_created_at on public.reputation_events(user_id, created_at desc); -create index if not exists idx_rep_events_source on public.reputation_events(source_type, source_id); -create index if not exists idx_rep_events_source_key on public.reputation_events(source_type, source_key); - -create table if not exists public.reputation_snapshots ( - user_id uuid primary key references public.profiles(id) on delete cascade, - score_total integer not null default 0, - repayment_score integer not null default 0, - lending_score integer not null default 0, - consistency_score integer not null default 0, - external_score integer not null default 0, - reputation_level text not null default 'bronze', - calculated_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - --- ========================= --- Tasks --- ========================= - -create table if not exists public.tasks ( - id uuid primary key default gen_random_uuid(), - creator_id uuid not null references public.profiles(id) on delete cascade, - assigned_to uuid references public.profiles(id) on delete set null, - title text not null, - description text, - category text, - reward_xlm numeric(20, 6) not null default 0 check (reward_xlm >= 0), - difficulty public.task_difficulty not null default 'easy', - status public.task_status not null default 'open', - completion_deadline timestamptz, - completion_date timestamptz, - proof_submission text, - creator_rating smallint check (creator_rating between 1 and 5), - metadata jsonb not null default '{}'::jsonb, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_tasks_creator_id on public.tasks(creator_id); -create index if not exists idx_tasks_assigned_to on public.tasks(assigned_to); -create index if not exists idx_tasks_status on public.tasks(status); -create index if not exists idx_tasks_created_at on public.tasks(created_at desc); - --- ========================= --- Lending pools and positions --- ========================= - -create table if not exists public.lending_pools ( - id uuid primary key default gen_random_uuid(), - name text not null, - description text, - status public.pool_status not null default 'active', - currency text not null default 'XLM', - apr_bps integer not null check (apr_bps >= 0 and apr_bps <= 100000), - total_liquidity numeric(20, 6) not null default 0, - available_liquidity numeric(20, 6) not null default 0, - total_borrowed numeric(20, 6) not null default 0, - created_by uuid references public.profiles(id) on delete set null, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_lending_pools_status on public.lending_pools(status); - -create table if not exists public.pool_positions ( - id uuid primary key default gen_random_uuid(), - pool_id uuid not null references public.lending_pools(id) on delete cascade, - lender_id uuid not null references public.profiles(id) on delete cascade, - status public.position_status not null default 'active', - principal_amount numeric(20, 6) not null check (principal_amount >= 0), - earned_interest numeric(20, 6) not null default 0 check (earned_interest >= 0), - withdrawn_amount numeric(20, 6) not null default 0 check (withdrawn_amount >= 0), - opened_at timestamptz not null default now(), - closed_at timestamptz, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_pool_positions_lender_id on public.pool_positions(lender_id); -create index if not exists idx_pool_positions_pool_id on public.pool_positions(pool_id); - --- ========================= --- Loans and repayments --- ========================= - -create table if not exists public.loans ( - id uuid primary key default gen_random_uuid(), - borrower_id uuid not null references public.profiles(id) on delete cascade, - pool_id uuid not null references public.lending_pools(id) on delete restrict, - status public.loan_status not null default 'requested', - principal_amount numeric(20, 6) not null check (principal_amount > 0), - apr_bps integer not null check (apr_bps >= 0 and apr_bps <= 100000), - duration_days integer not null check (duration_days > 0), - requested_at timestamptz not null default now(), - approved_at timestamptz, - funded_at timestamptz, - due_at timestamptz, - closed_at timestamptz, - repaid_amount numeric(20, 6) not null default 0 check (repaid_amount >= 0), - defaulted_at timestamptz, - metadata jsonb not null default '{}'::jsonb, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_loans_borrower_id on public.loans(borrower_id); -create index if not exists idx_loans_pool_id on public.loans(pool_id); -create index if not exists idx_loans_status on public.loans(status); -create index if not exists idx_loans_due_at on public.loans(due_at); -create index if not exists idx_loans_borrower_status on public.loans(borrower_id, status); - -create table if not exists public.loan_repayments ( - id uuid primary key default gen_random_uuid(), - loan_id uuid not null references public.loans(id) on delete cascade, - payer_id uuid not null references public.profiles(id) on delete restrict, - amount numeric(20, 6) not null check (amount > 0), - paid_at timestamptz not null default now(), - tx_ref text, - metadata jsonb not null default '{}'::jsonb, - created_at timestamptz not null default now() -); - -create index if not exists idx_loan_repayments_loan_id on public.loan_repayments(loan_id); -create index if not exists idx_loan_repayments_payer_id on public.loan_repayments(payer_id); - --- ========================= --- Risk and fraud --- ========================= - -create table if not exists public.risk_assessments ( - id uuid primary key default gen_random_uuid(), - user_id uuid not null references public.profiles(id) on delete cascade, - score numeric(5, 2) not null check (score >= 0 and score <= 100), - decision public.risk_decision not null, - reasons jsonb not null default '[]'::jsonb, - assessed_at timestamptz not null default now(), - created_at timestamptz not null default now() -); - -create index if not exists idx_risk_assessments_user_id_assessed_at on public.risk_assessments(user_id, assessed_at desc); - -create table if not exists public.fraud_signals ( - id uuid primary key default gen_random_uuid(), - user_id uuid not null references public.profiles(id) on delete cascade, - signal_type text not null, - severity smallint not null check (severity between 1 and 5), - payload jsonb not null default '{}'::jsonb, - resolved boolean not null default false, - created_at timestamptz not null default now(), - resolved_at timestamptz -); - -create index if not exists idx_fraud_signals_user_id_created_at on public.fraud_signals(user_id, created_at desc); -create index if not exists idx_fraud_signals_resolved on public.fraud_signals(resolved); - --- ========================= --- Ledger and chain mapping --- ========================= - -create table if not exists public.ledger_transactions ( - id uuid primary key default gen_random_uuid(), - user_id uuid not null references public.profiles(id) on delete cascade, - category text not null, - amount numeric(20, 6) not null, - currency text not null default 'XLM', - status public.tx_status not null default 'pending', - ref_type text, - ref_id uuid, - metadata jsonb not null default '{}'::jsonb, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_ledger_transactions_user_id_created_at on public.ledger_transactions(user_id, created_at desc); -create index if not exists idx_ledger_transactions_status on public.ledger_transactions(status); - -create table if not exists public.chain_events ( - id uuid primary key default gen_random_uuid(), - tx_hash text not null, - contract_id text, - event_type text not null, - payload jsonb not null default '{}'::jsonb, - happened_at timestamptz, - created_at timestamptz not null default now(), - unique (tx_hash, event_type) -); - -create index if not exists idx_chain_events_contract_id on public.chain_events(contract_id); -create index if not exists idx_chain_events_happened_at on public.chain_events(happened_at desc); - --- ========================= --- External verification --- ========================= - -create table if not exists public.external_verifications ( - id uuid primary key default gen_random_uuid(), - user_id uuid not null references public.profiles(id) on delete cascade, - provider text not null, - verification_type text not null, - status public.verification_status not null default 'pending', - verified_at timestamptz, - payload_meta jsonb not null default '{}'::jsonb, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_external_verifications_user_id on public.external_verifications(user_id); -create index if not exists idx_external_verifications_status on public.external_verifications(status); - --- ========================= --- Triggers --- ========================= - -drop trigger if exists trg_profiles_updated_at on public.profiles; -create trigger trg_profiles_updated_at -before update on public.profiles -for each row execute function public.set_updated_at(); - -drop trigger if exists trg_reputation_snapshots_updated_at on public.reputation_snapshots; -create trigger trg_reputation_snapshots_updated_at -before update on public.reputation_snapshots -for each row execute function public.set_updated_at(); - -drop trigger if exists trg_tasks_updated_at on public.tasks; -create trigger trg_tasks_updated_at -before update on public.tasks -for each row execute function public.set_updated_at(); - -drop trigger if exists trg_lending_pools_updated_at on public.lending_pools; -create trigger trg_lending_pools_updated_at -before update on public.lending_pools -for each row execute function public.set_updated_at(); - -drop trigger if exists trg_pool_positions_updated_at on public.pool_positions; -create trigger trg_pool_positions_updated_at -before update on public.pool_positions -for each row execute function public.set_updated_at(); - -drop trigger if exists trg_loans_updated_at on public.loans; -create trigger trg_loans_updated_at -before update on public.loans -for each row execute function public.set_updated_at(); - -drop trigger if exists trg_ledger_transactions_updated_at on public.ledger_transactions; -create trigger trg_ledger_transactions_updated_at -before update on public.ledger_transactions -for each row execute function public.set_updated_at(); - -drop trigger if exists trg_external_verifications_updated_at on public.external_verifications; -create trigger trg_external_verifications_updated_at -before update on public.external_verifications -for each row execute function public.set_updated_at(); - -drop trigger if exists on_auth_user_created on auth.users; -create trigger on_auth_user_created -after insert on auth.users -for each row execute function public.handle_new_user_profile(); --- Add wallet_address support to profiles table --- Apply for existing databases created before wallet_address was introduced. - -ALTER TABLE public.profiles -ADD COLUMN IF NOT EXISTS wallet_address TEXT; - -CREATE INDEX IF NOT EXISTS idx_profiles_wallet_address -ON public.profiles(wallet_address); --- TrustLend task-completion reputation key migration --- Stores task slugs in source_key so task completion events do not depend on UUID casting. - -alter table public.reputation_events - add column if not exists source_key text; - -create index if not exists idx_rep_events_source_key - on public.reputation_events(source_type, source_key); --- TRUSTLEND: KYC (Know Your Customer) Verification Schema --- Add these columns to the existing 'profiles' table in Supabase - --- 1. Add KYC verification columns to profiles table -ALTER TABLE profiles ADD COLUMN IF NOT EXISTS government_id_ipfs_hash VARCHAR(255); -ALTER TABLE profiles ADD COLUMN IF NOT EXISTS government_id_url TEXT; -ALTER TABLE profiles ADD COLUMN IF NOT EXISTS kyc_submitted_at TIMESTAMP WITH TIME ZONE; -ALTER TABLE profiles ADD COLUMN IF NOT EXISTS kyc_verified_at TIMESTAMP WITH TIME ZONE; -ALTER TABLE profiles ADD COLUMN IF NOT EXISTS kyc_rejection_reason TEXT; - --- 2. Add indexes for efficient queries -CREATE INDEX IF NOT EXISTS idx_profiles_kyc_status ON profiles(kyc_status); -CREATE INDEX IF NOT EXISTS idx_profiles_kyc_submitted_at ON profiles(kyc_submitted_at); - --- 2.1 Ensure enum supports new workflow status used by upload/review flow -DO $$ -BEGIN - IF EXISTS ( - SELECT 1 - FROM pg_type t - JOIN pg_namespace n ON n.oid = t.typnamespace - WHERE t.typname = 'kyc_status' AND n.nspname = 'public' - ) THEN - ALTER TYPE public.kyc_status ADD VALUE IF NOT EXISTS 'submitted'; - END IF; -END $$; - --- 3. Row Level Security (RLS) Policy: Only admins can view unverified KYC documents -ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; - --- Drop existing policies if any -DROP POLICY IF EXISTS "Users can view own KYC status" ON profiles; -DROP POLICY IF EXISTS "Admins can view all KYC documents" ON profiles; -DROP POLICY IF EXISTS "Users can only update own profile" ON profiles; -DROP POLICY IF EXISTS "Users can view own profile" ON profiles; -DROP POLICY IF EXISTS "Admins can view all profiles" ON profiles; -DROP POLICY IF EXISTS "Users can update own profile" ON profiles; -DROP POLICY IF EXISTS "Admins can update KYC status" ON profiles; - --- New RLS Policies: - --- Policy 1: Users can view only their own basic profile -CREATE POLICY "Users can view own profile" - ON profiles - FOR SELECT - USING (auth.uid() = id); - --- Policy 2: Admins can view all profiles including sensitive KYC data -CREATE POLICY "Admins can view all profiles" - ON profiles - FOR SELECT - USING ( - EXISTS ( - SELECT 1 FROM profiles - WHERE profiles.id = auth.uid() - AND profiles.role = 'admin' - ) - ); - --- Policy 3: Users can update only their own profile -CREATE POLICY "Users can update own profile" - ON profiles - FOR UPDATE - USING (auth.uid() = id) - WITH CHECK (auth.uid() = id); - --- Policy 4: Admins can update KYC status -CREATE POLICY "Admins can update KYC status" - ON profiles - FOR UPDATE - USING ( - EXISTS ( - SELECT 1 FROM profiles - WHERE profiles.id = auth.uid() - AND profiles.role = 'admin' - ) - ) - WITH CHECK ( - EXISTS ( - SELECT 1 FROM profiles - WHERE profiles.id = auth.uid() - AND profiles.role = 'admin' - ) - ); - --- 4. Create view for admin KYC dashboard (optional but useful) -DROP VIEW IF EXISTS admin_kyc_queue; - -CREATE OR REPLACE VIEW admin_kyc_queue AS -SELECT - id, - full_name, - kyc_status, - government_id_ipfs_hash, - government_id_url, - kyc_submitted_at, - kyc_verified_at, - kyc_rejection_reason -FROM profiles -WHERE kyc_status IN ('submitted', 'rejected', 'verified') -ORDER BY kyc_submitted_at DESC; - --- GRANT admin_kyc_queue view access in Supabase dashboard --- (This is automatic for authenticated users, but restrict to admins via application logic) diff --git a/sql/02_security_rls.sql b/sql/02_security_rls.sql deleted file mode 100644 index 8dcb3e0..0000000 --- a/sql/02_security_rls.sql +++ /dev/null @@ -1,894 +0,0 @@ --- TrustLend RLS policies for Supabase --- Apply after 001_schema.sql - --- ========================= --- Enable RLS --- ========================= - -alter table public.profiles enable row level security; -alter table public.reputation_events enable row level security; -alter table public.reputation_snapshots enable row level security; -alter table public.tasks enable row level security; -alter table public.lending_pools enable row level security; -alter table public.pool_positions enable row level security; -alter table public.loans enable row level security; -alter table public.loan_repayments enable row level security; -alter table public.risk_assessments enable row level security; -alter table public.fraud_signals enable row level security; -alter table public.ledger_transactions enable row level security; -alter table public.chain_events enable row level security; -alter table public.external_verifications enable row level security; - --- ========================= --- Profiles --- ========================= - -create or replace function public.is_admin() -returns boolean -language sql -stable -security definer -set search_path = public -as $$ - select exists ( - select 1 - from public.profiles - where id = auth.uid() and role = 'admin' - ); -$$; - -grant execute on function public.is_admin() to authenticated; - -drop policy if exists profiles_select_own on public.profiles; -create policy profiles_select_own -on public.profiles -for select -using (auth.uid() = id); - -drop policy if exists profiles_update_own on public.profiles; -create policy profiles_update_own -on public.profiles -for update -using (auth.uid() = id) -with check (auth.uid() = id); - --- ========================= --- Reputation data --- ========================= - -drop policy if exists rep_events_select_own on public.reputation_events; -create policy rep_events_select_own -on public.reputation_events -for select -using (auth.uid() = user_id); - -drop policy if exists rep_events_select_admin_all on public.reputation_events; -create policy rep_events_select_admin_all -on public.reputation_events -for select -using (public.is_admin()); - -drop policy if exists rep_events_write_admin on public.reputation_events; -create policy rep_events_write_admin -on public.reputation_events -for all -using (public.is_admin()) -with check (public.is_admin()); - -drop policy if exists rep_snapshots_select_own on public.reputation_snapshots; -create policy rep_snapshots_select_own -on public.reputation_snapshots -for select -using (auth.uid() = user_id); - -drop policy if exists rep_snapshots_select_admin_all on public.reputation_snapshots; -create policy rep_snapshots_select_admin_all -on public.reputation_snapshots -for select -using (public.is_admin()); - -drop policy if exists rep_snapshots_write_admin on public.reputation_snapshots; -create policy rep_snapshots_write_admin -on public.reputation_snapshots -for all -using (public.is_admin()) -with check (public.is_admin()); - --- ========================= --- Tasks --- ========================= - -drop policy if exists tasks_select_related on public.tasks; -create policy tasks_select_related -on public.tasks -for select -using (auth.uid() = creator_id or auth.uid() = assigned_to); - -drop policy if exists tasks_insert_creator on public.tasks; -create policy tasks_insert_creator -on public.tasks -for insert -with check (auth.uid() = creator_id); - -drop policy if exists tasks_update_creator_or_assignee on public.tasks; -create policy tasks_update_creator_or_assignee -on public.tasks -for update -using (auth.uid() = creator_id or auth.uid() = assigned_to) -with check (auth.uid() = creator_id or auth.uid() = assigned_to); - -drop policy if exists tasks_write_admin on public.tasks; -create policy tasks_write_admin -on public.tasks -for all -using (public.is_admin()) -with check (public.is_admin()); - --- ========================= --- Lending pools and positions --- ========================= - -drop policy if exists pools_select_authenticated on public.lending_pools; -create policy pools_select_authenticated -on public.lending_pools -for select -using (auth.role() = 'authenticated'); - -drop policy if exists pools_select_admin_all on public.lending_pools; -create policy pools_select_admin_all -on public.lending_pools -for select -using (public.is_admin()); - -drop policy if exists pools_write_admin on public.lending_pools; -create policy pools_write_admin -on public.lending_pools -for all -using (public.is_admin()) -with check (public.is_admin()); - -drop policy if exists pool_positions_select_own on public.pool_positions; -create policy pool_positions_select_own -on public.pool_positions -for select -using (auth.uid() = lender_id); - -drop policy if exists pool_positions_insert_own on public.pool_positions; -create policy pool_positions_insert_own -on public.pool_positions -for insert -with check (auth.uid() = lender_id); - -drop policy if exists pool_positions_update_own on public.pool_positions; -create policy pool_positions_update_own -on public.pool_positions -for update -using (auth.uid() = lender_id) -with check (auth.uid() = lender_id); - --- ========================= --- Loans and repayments --- ========================= - -drop policy if exists loans_select_own on public.loans; -create policy loans_select_own -on public.loans -for select -using ( - auth.uid() = borrower_id - or public.is_admin() - or exists ( - select 1 - from public.ledger_transactions lt - where lt.ref_type = 'loan_fund' - and lt.ref_id = loans.id - and ( - lt.user_id = auth.uid() - or coalesce(lt.metadata->>'lenderUserId', '') = auth.uid()::text - ) - ) -); - -drop policy if exists loans_select_admin_all on public.loans; -create policy loans_select_admin_all -on public.loans -for select -using (public.is_admin()); - -drop policy if exists loans_insert_own on public.loans; -create policy loans_insert_own -on public.loans -for insert -with check (auth.uid() = borrower_id); - -drop policy if exists loans_update_admin on public.loans; -create policy loans_update_admin -on public.loans -for update -using (public.is_admin()) -with check (public.is_admin()); - -drop policy if exists repayments_select_related_loan on public.loan_repayments; -create policy repayments_select_related_loan -on public.loan_repayments -for select -using ( - exists ( - select 1 from public.loans l - where l.id = loan_id and l.borrower_id = auth.uid() - ) - or auth.uid() = payer_id -); - -drop policy if exists repayments_select_admin_all on public.loan_repayments; -create policy repayments_select_admin_all -on public.loan_repayments -for select -using (public.is_admin()); - -drop policy if exists repayments_insert_own on public.loan_repayments; -create policy repayments_insert_own -on public.loan_repayments -for insert -with check ( - auth.uid() = payer_id - and exists ( - select 1 from public.loans l - where l.id = loan_id and l.borrower_id = auth.uid() - ) -); - -drop policy if exists repayments_write_admin on public.loan_repayments; -create policy repayments_write_admin -on public.loan_repayments -for all -using (public.is_admin()) -with check (public.is_admin()); - --- ========================= --- Risk and fraud --- ========================= - -drop policy if exists risk_assessments_select_own on public.risk_assessments; -create policy risk_assessments_select_own -on public.risk_assessments -for select -using (auth.uid() = user_id); - -drop policy if exists risk_assessments_write_admin on public.risk_assessments; -create policy risk_assessments_write_admin -on public.risk_assessments -for all -using (public.is_admin()) -with check (public.is_admin()); - -drop policy if exists fraud_signals_select_own on public.fraud_signals; -create policy fraud_signals_select_own -on public.fraud_signals -for select -using (auth.uid() = user_id); - -drop policy if exists fraud_signals_write_admin on public.fraud_signals; -create policy fraud_signals_write_admin -on public.fraud_signals -for all -using (public.is_admin()) -with check (public.is_admin()); - --- ========================= --- Ledger and chain mapping --- ========================= - -drop policy if exists ledger_select_own on public.ledger_transactions; -create policy ledger_select_own -on public.ledger_transactions -for select -using ( - auth.uid() = user_id - or coalesce(metadata->>'lenderUserId', '') = auth.uid()::text -); - -drop policy if exists ledger_select_admin_all on public.ledger_transactions; -create policy ledger_select_admin_all -on public.ledger_transactions -for select -using (public.is_admin()); - -drop policy if exists ledger_insert_own on public.ledger_transactions; -create policy ledger_insert_own -on public.ledger_transactions -for insert -with check (auth.uid() = user_id); - -drop policy if exists ledger_write_admin on public.ledger_transactions; -create policy ledger_write_admin -on public.ledger_transactions -for all -using (public.is_admin()) -with check (public.is_admin()); - -drop policy if exists chain_events_select_authenticated on public.chain_events; -create policy chain_events_select_authenticated -on public.chain_events -for select -using (auth.role() = 'authenticated'); - -drop policy if exists chain_events_write_admin on public.chain_events; -create policy chain_events_write_admin -on public.chain_events -for all -using (public.is_admin()) -with check (public.is_admin()); - --- ========================= --- External verification --- ========================= - -drop policy if exists external_verifications_select_own on public.external_verifications; -create policy external_verifications_select_own -on public.external_verifications -for select -using (auth.uid() = user_id); - -drop policy if exists external_verifications_write_admin on public.external_verifications; -create policy external_verifications_write_admin -on public.external_verifications -for all -using (public.is_admin()) -with check (public.is_admin()); --- TrustLend RLS Fix v2: Eliminate infinite recursion + fix "new row violates RLS" on UPDATE --- - --- ===================================================================== --- Step 1: Drop ALL conflicting policies on profiles --- ===================================================================== - -DROP POLICY IF EXISTS profiles_select_own ON public.profiles; -DROP POLICY IF EXISTS profiles_update_own ON public.profiles; -DROP POLICY IF EXISTS profiles_admin_select_all ON public.profiles; -DROP POLICY IF EXISTS profiles_admin_update_all ON public.profiles; -DROP POLICY IF EXISTS profiles_service_all ON public.profiles; -DROP POLICY IF EXISTS "Users can view own profile" ON public.profiles; -DROP POLICY IF EXISTS "Users can view own KYC status" ON public.profiles; -DROP POLICY IF EXISTS "Admins can view all profiles" ON public.profiles; -DROP POLICY IF EXISTS "Admins can view all KYC documents" ON public.profiles; -DROP POLICY IF EXISTS "Users can only update own profile" ON public.profiles; -DROP POLICY IF EXISTS "Users can update own profile" ON public.profiles; -DROP POLICY IF EXISTS "Admins can update KYC status" ON public.profiles; -DROP POLICY IF EXISTS "Admins can update all profiles" ON public.profiles; - --- ===================================================================== --- Step 2: Create a SECURITY DEFINER function to safely check admin role --- This bypasses RLS when checking the caller's own role → no recursion. --- ===================================================================== - -CREATE OR REPLACE FUNCTION public.is_admin() -RETURNS boolean -LANGUAGE sql -STABLE -SECURITY DEFINER -SET search_path = public -AS $$ - SELECT EXISTS ( - SELECT 1 FROM public.profiles - WHERE id = auth.uid() AND role = 'admin' - ); -$$; - --- Grant execute to authenticated users -GRANT EXECUTE ON FUNCTION public.is_admin() TO authenticated; - --- ===================================================================== --- Step 3: Recreate clean, non-recursive policies --- ===================================================================== - --- SELECT: users see only their own row -CREATE POLICY profiles_select_own -ON public.profiles -FOR SELECT -USING (auth.uid() = id); - --- SELECT: admins see all rows (via security-definer fn — no recursion) -CREATE POLICY profiles_admin_select_all -ON public.profiles -FOR SELECT -USING (public.is_admin()); - --- The app layer uses session-bound writes, so this is defense-in-depth. -CREATE POLICY profiles_update_own -ON public.profiles -FOR UPDATE -USING (auth.uid() = id); - --- UPDATE: admins can update any row -CREATE POLICY profiles_admin_update_all -ON public.profiles -FOR UPDATE -USING (public.is_admin()); - --- ALL: admin bypasses RLS for backend jobs / server actions -CREATE POLICY profiles_admin_all -ON public.profiles -FOR ALL -USING (public.is_admin()) -WITH CHECK (public.is_admin()); - --- ===================================================================== --- Step 4: Add date_of_birth column if it doesn't exist (for legal KYC) --- ===================================================================== - -ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS date_of_birth date; - --- ===================================================================== --- Step 5: Ensure kyc_submitted_at column exists (used by kyc-upload.ts) --- ===================================================================== - -ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS kyc_submitted_at timestamptz; -ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS government_id_ipfs_hash text; -ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS government_id_url text; --- TrustLend MVP Security Hardening --- Purpose: --- 1) Prevent users from self-escalating role/kyc/risk via profile updates. --- 2) Keep normal profile field updates working for authenticated users. - -create or replace function public.is_admin() -returns boolean -language sql -stable -security definer -set search_path = public -as $$ - select exists ( - select 1 from public.profiles - where id = auth.uid() and role = 'admin' - ); -$$; - -grant execute on function public.is_admin() to authenticated; - --- Security-definer function to compare immutable/sensitive profile fields --- against the existing stored row without hitting RLS recursion. -create or replace function public.profile_sensitive_fields_unchanged( - _id uuid, - _role public.app_role, - _kyc public.kyc_status, - _risk public.risk_status -) -returns boolean -language sql -stable -security definer -set search_path = public -as $$ - select exists ( - select 1 - from public.profiles p - where p.id = _id - and p.role = _role - and ( - p.kyc_status = _kyc - or ( - p.kyc_status in ('pending', 'rejected') - and _kyc = 'submitted' - ) - ) - and p.risk_status = _risk - ); -$$; - -grant execute on function public.profile_sensitive_fields_unchanged(uuid, public.app_role, public.kyc_status, public.risk_status) to authenticated; - --- Replace update-own policy with a version that prevents sensitive field tampering. -drop policy if exists profiles_update_own on public.profiles; -create policy profiles_update_own -on public.profiles -for update -using (auth.uid() = id) -with check ( - auth.uid() = id - and public.profile_sensitive_fields_unchanged(id, role, kyc_status, risk_status) -); - -drop policy if exists profiles_admin_select_all on public.profiles; -create policy profiles_admin_select_all -on public.profiles -for select -using (public.is_admin()); - -drop policy if exists profiles_admin_update_all on public.profiles; -create policy profiles_admin_update_all -on public.profiles -for update -using (public.is_admin()) -with check (public.is_admin()); - -drop policy if exists loans_admin_select_all on public.loans; -create policy loans_admin_select_all -on public.loans -for select -using (public.is_admin()); - -drop policy if exists loans_admin_update_all on public.loans; -create policy loans_admin_update_all -on public.loans -for update -using (public.is_admin()) -with check (public.is_admin()); - -drop policy if exists pools_admin_read_all on public.lending_pools; -create policy pools_admin_read_all -on public.lending_pools -for select -using (public.is_admin()); - -drop policy if exists pools_admin_write_all on public.lending_pools; -create policy pools_admin_write_all -on public.lending_pools -for all -using (public.is_admin()) -with check (public.is_admin()); --- TrustLend KYC Storage RLS --- Grants authenticated users access to their own KYC uploads without using a service-role key. - --- Ensure the KYC bucket exists and remains private. -insert into storage.buckets (id, name, public) -values ('kyc-documents', 'kyc-documents', false) -on conflict (id) do update -set public = excluded.public; - --- storage.objects already has RLS managed by Supabase Storage internals. - -drop policy if exists "Users can upload their own KYC" on storage.objects; -create policy "Users can upload their own KYC" -on storage.objects -for insert -to authenticated -with check ( - bucket_id = 'kyc-documents' - and split_part(name, '/', 1) = auth.uid()::text -); - -drop policy if exists "Users can view own KYC documents" on storage.objects; -create policy "Users can view own KYC documents" -on storage.objects -for select -to authenticated -using ( - bucket_id = 'kyc-documents' - and split_part(name, '/', 1) = auth.uid()::text -); - -drop policy if exists "Admins can view all KYC documents" on storage.objects; -create policy "Admins can view all KYC documents" -on storage.objects -for select -to authenticated -using ( - bucket_id = 'kyc-documents' - and exists ( - select 1 - from public.profiles - where profiles.id = auth.uid() - and profiles.role = 'admin' - ) -); - -drop policy if exists "Documents cannot be deleted" on storage.objects; -create policy "Documents cannot be deleted" -on storage.objects -for delete -to authenticated -using (false); --- TrustLend migration: remove service-role dependence from app flows. --- Replaces privileged writes with guarded RPCs and a reputation snapshot trigger. - --- ----------------------------------------------------------------------------- --- Marketplace reads --- ----------------------------------------------------------------------------- - -drop policy if exists loans_select_open_authenticated on public.loans; -create policy loans_select_open_authenticated -on public.loans -for select -using ( - auth.role() = 'authenticated' - and status in ('requested', 'approved') -); - --- ----------------------------------------------------------------------------- --- Reputation events and snapshots --- ----------------------------------------------------------------------------- - -drop policy if exists rep_events_insert_own on public.reputation_events; -create policy rep_events_insert_own -on public.reputation_events -for insert -with check (auth.uid() = user_id); - -create or replace function public.sync_reputation_snapshot_from_event() -returns trigger -language plpgsql -security definer -set search_path = public -as $$ -declare - existing_repayment_score integer := 0; - existing_lending_score integer := 0; - existing_consistency_score integer := 0; - existing_external_score integer := 0; - existing_level text := 'bronze'; - total_points integer := 0; - computed_total integer := 250; -begin - if new.user_id is null then - return new; - end if; - - select - coalesce(score_total, 250), - coalesce(repayment_score, 0), - coalesce(lending_score, 0), - coalesce(consistency_score, 0), - coalesce(external_score, 0), - coalesce(reputation_level, 'bronze') - into computed_total, existing_repayment_score, existing_lending_score, existing_consistency_score, existing_external_score, existing_level - from public.reputation_snapshots - where user_id = new.user_id; - - select coalesce(sum(points_delta), 0) - into total_points - from public.reputation_events - where user_id = new.user_id; - - computed_total := greatest(0, least(750, 250 + total_points)); - - insert into public.reputation_snapshots ( - user_id, - score_total, - repayment_score, - lending_score, - consistency_score, - external_score, - reputation_level, - calculated_at, - updated_at - ) - values ( - new.user_id, - computed_total, - existing_repayment_score, - existing_lending_score, - existing_consistency_score, - existing_external_score, - existing_level, - now(), - now() - ) - on conflict (user_id) do update - set score_total = excluded.score_total, - repayment_score = excluded.repayment_score, - lending_score = excluded.lending_score, - consistency_score = excluded.consistency_score, - external_score = excluded.external_score, - reputation_level = excluded.reputation_level, - calculated_at = excluded.calculated_at, - updated_at = excluded.updated_at; - - return new; -end; -$$; - -drop trigger if exists trg_reputation_events_snapshot on public.reputation_events; -create trigger trg_reputation_events_snapshot -after insert on public.reputation_events -for each row execute function public.sync_reputation_snapshot_from_event(); - -grant execute on function public.sync_reputation_snapshot_from_event() to authenticated; - -create or replace function public.seed_reputation_snapshot( - p_user_id uuid, - p_initial_score integer -) -returns void -language plpgsql -security definer -set search_path = public -as $$ -begin - if not public.is_admin() then - raise exception 'not authorized'; - end if; - - insert into public.reputation_snapshots ( - user_id, - score_total, - updated_at - ) - values ( - p_user_id, - greatest(0, least(750, p_initial_score)), - now() - ) - on conflict (user_id) do update - set score_total = excluded.score_total, - updated_at = excluded.updated_at; -end; -$$; - -grant execute on function public.seed_reputation_snapshot(uuid, integer) to authenticated; - --- ----------------------------------------------------------------------------- --- Loan funding and repayment transitions --- ----------------------------------------------------------------------------- - -create or replace function public.activate_loan_funding( - p_loan_id uuid, - p_lender_id uuid, - p_approved_at timestamptz, - p_due_at timestamptz -) -returns public.loans -language plpgsql -security definer -set search_path = public -as $$ -declare - updated_loan public.loans; -begin - if auth.uid() is distinct from p_lender_id then - raise exception 'not authorized'; - end if; - - update public.loans - set status = 'active', - approved_at = p_approved_at, - due_at = p_due_at - where id = p_loan_id - and status in ('requested', 'approved') - and borrower_id <> p_lender_id - returning * into updated_loan; - - if not found then - raise exception 'loan not available for funding'; - end if; - - return updated_loan; -end; -$$; - -grant execute on function public.activate_loan_funding(uuid, uuid, timestamptz, timestamptz) to authenticated; - -create or replace function public.record_loan_repayment( - p_loan_id uuid, - p_payer_id uuid, - p_repaid_amount numeric, - p_new_status public.loan_status -) -returns public.loans -language plpgsql -security definer -set search_path = public -as $$ -declare - updated_loan public.loans; -begin - if auth.uid() is distinct from p_payer_id then - raise exception 'not authorized'; - end if; - - update public.loans - set repaid_amount = p_repaid_amount, - status = p_new_status - where id = p_loan_id - and borrower_id = p_payer_id - and status <> 'defaulted' - returning * into updated_loan; - - if not found then - raise exception 'loan not available for repayment'; - end if; - - return updated_loan; -end; -$$; - -grant execute on function public.record_loan_repayment(uuid, uuid, numeric, public.loan_status) to authenticated; - --- ----------------------------------------------------------------------------- --- Lender dashboard metrics without service-role reads --- ----------------------------------------------------------------------------- - -create or replace function public.get_lender_dashboard_metrics(p_user_id uuid) -returns table ( - deployed_capital numeric, - total_earnings numeric, - active_positions integer, - default_rate numeric -) -language sql -stable -security definer -set search_path = public -as $$ - with pool_stats as ( - select - coalesce(sum(principal_amount), 0) as deployed_capital, - coalesce(sum(earned_interest), 0) as total_earnings, - count(*) filter (where status = 'active')::integer as active_positions - from public.pool_positions - where lender_id = p_user_id - ), - p2p_funds as ( - select - coalesce(sum(amount), 0) as deployed_capital, - count(*) filter (where l.status in ('requested', 'approved', 'funded', 'active'))::integer as active_positions - from public.ledger_transactions lt - left join public.loans l on l.id = lt.ref_id - where lt.user_id = p_user_id - and lt.ref_type = 'loan_fund' - ), - p2p_repayments as ( - select coalesce(sum(amount), 0) as total_repaid - from public.ledger_transactions - where ref_type = 'loan_repay' - and coalesce(metadata->>'lenderUserId', '') = p_user_id::text - ), - loan_stats as ( - select - count(*) filter (where status = 'defaulted')::numeric as bad, - count(*) filter (where status in ('repaid', 'defaulted'))::numeric as closed - from public.loans - ) - select - coalesce((select deployed_capital from pool_stats), 0) + coalesce((select deployed_capital from p2p_funds), 0) as deployed_capital, - coalesce((select total_earnings from pool_stats), 0) + greatest(0, coalesce((select total_repaid from p2p_repayments), 0) - coalesce((select deployed_capital from p2p_funds), 0)) as total_earnings, - coalesce((select active_positions from pool_stats), 0) + coalesce((select active_positions from p2p_funds), 0) as active_positions, - case - when coalesce((select closed from loan_stats), 0) > 0 then - (coalesce((select bad from loan_stats), 0) / coalesce((select closed from loan_stats), 1)) * 100 - else 0 - end as default_rate; -$$; - -grant execute on function public.get_lender_dashboard_metrics(uuid) to authenticated; - --- ----------------------------------------------------------------------------- --- Marketplace summary data without broad profile access --- ----------------------------------------------------------------------------- - -create or replace function public.get_marketplace_loans() -returns table ( - id uuid, - principal_amount numeric, - apr_bps integer, - duration_days integer, - borrower_id uuid, - borrower_name text, - borrower_wallet text, - trust_score integer -) -language sql -stable -security definer -set search_path = public -as $$ - select - l.id, - l.principal_amount, - l.apr_bps, - l.duration_days, - l.borrower_id, - case - when coalesce(nullif(p.full_name, ''), '') <> '' then p.full_name - else 'Borrower ' || left(l.borrower_id::text, 6) - end as borrower_name, - coalesce(p.wallet_address, '') as borrower_wallet, - coalesce(rs.score_total, 250) as trust_score - from public.loans l - left join public.profiles p on p.id = l.borrower_id - left join public.reputation_snapshots rs on rs.user_id = l.borrower_id - where l.status in ('requested', 'approved') - order by l.created_at asc; -$$; - -grant execute on function public.get_marketplace_loans() to authenticated; diff --git a/sql/03_functions_rpcs.sql b/sql/03_functions_rpcs.sql deleted file mode 100644 index 871cb57..0000000 --- a/sql/03_functions_rpcs.sql +++ /dev/null @@ -1,106 +0,0 @@ --- TrustLend task completion RPC --- Awards trust points through a security-definer function so task claims do not depend on reputation_events insert RLS. - -create or replace function public.complete_platform_task(p_task_id text) -returns integer -language plpgsql -security definer -set search_path = public -as $$ -declare - current_user_id uuid := auth.uid(); - task_points integer; - task_title text; -begin - if current_user_id is null then - raise exception 'not authenticated'; - end if; - - select t.points, t.title - into task_points, task_title - from ( - values - ('task_stellar_basics', 30, 'Learn: How Stellar Payments Work'), - ('task_credit_score', 25, 'Learn: How Your Trust Score Is Calculated'), - ('task_defi_lending', 35, 'Learn: DeFi Lending vs Traditional Banking') - ) as t(task_id, points, title) - where t.task_id = p_task_id; - - if task_points is null then - raise exception 'Task not found'; - end if; - - if exists ( - select 1 - from public.reputation_events - where user_id = current_user_id - and source_type = 'task_completion' - and source_key = p_task_id - ) then - raise exception 'Task already completed. Each task can only be claimed once.'; - end if; - - insert into public.reputation_events ( - user_id, - source_type, - source_key, - points_delta, - reason - ) - values ( - current_user_id, - 'task_completion', - p_task_id, - task_points, - 'Completed: ' || task_title - ); - - return task_points; -end; -$$; - -grant execute on function public.complete_platform_task(text) to authenticated; --- TrustLend hotfix: ensure activate_loan_funding RPC exists and is visible to PostgREST --- Apply this in Supabase SQL editor if lenders see: --- "Could not find the function public.activate_loan_funding(...) in the schema cache" - -create or replace function public.activate_loan_funding( - p_loan_id uuid, - p_lender_id uuid, - p_approved_at timestamptz, - p_due_at timestamptz -) -returns public.loans -language plpgsql -security definer -set search_path = public -as $$ -declare - updated_loan public.loans; -begin - if auth.uid() is distinct from p_lender_id then - raise exception 'not authorized'; - end if; - - update public.loans - set status = 'active', - approved_at = p_approved_at, - funded_at = coalesce(funded_at, p_approved_at), - due_at = p_due_at - where id = p_loan_id - and status in ('requested', 'approved') - and borrower_id <> p_lender_id - returning * into updated_loan; - - if not found then - raise exception 'loan not available for funding'; - end if; - - return updated_loan; -end; -$$; - -grant execute on function public.activate_loan_funding(uuid, uuid, timestamptz, timestamptz) to authenticated; - --- Force PostgREST schema cache reload so RPC is discoverable immediately. -notify pgrst, 'reload schema'; diff --git a/sql/04_pool_performance_rpc.sql b/sql/04_pool_performance_rpc.sql deleted file mode 100644 index 969f716..0000000 --- a/sql/04_pool_performance_rpc.sql +++ /dev/null @@ -1,188 +0,0 @@ -/** - * MIGRATION: 04_pool_performance_rpc.sql - * - * PURPOSE: Add optimized RPC function for fetching lending pools with filters - * to replace waterfall queries in pool admin operations. - * - * ISSUE: #39 - Optimize Supabase database query performance for large pool lists - * - * PERFORMANCE IMPROVEMENTS: - * - Consolidates multiple sequential queries into ONE RPC call - * - Enables atomic operations for pool status changes - * - Reduces network round-trips from N to 1 - * - Returns pre-filtered, pre-sorted data from database - */ - --- ───────────────────────────────────────────────────────────────────────────── --- RPC: get_lending_pools_paginated --- ───────────────────────────────────────────────────────────────────────────── --- --- Fetches paginated lending pools with optional status filter. --- Returns only explicit columns (no SELECT *) for better performance. --- --- USAGE: --- SELECT * FROM public.get_lending_pools_paginated( --- status_filter := 'active', --- page_limit := 10, --- page_offset := 0, --- order_by_col := 'created_at', --- order_asc := false --- ); - -drop function if exists public.get_lending_pools_paginated( - public.pool_status, - integer, - integer, - text, - boolean -) cascade; - -create or replace function public.get_lending_pools_paginated( - status_filter public.pool_status default null, - page_limit integer default 10, - page_offset integer default 0, - order_by_col text default 'created_at', - order_asc boolean default false -) -returns table ( - id uuid, - name text, - description text, - status public.pool_status, - apr_bps integer, - total_liquidity numeric, - available_liquidity numeric, - total_borrowed numeric, - created_at timestamptz, - updated_at timestamptz, - total_count integer -) -language sql -stable -security definer -set search_path = public -as $$ - with filtered_pools as ( - select - lending_pools.id, - lending_pools.name, - lending_pools.description, - lending_pools.status, - lending_pools.apr_bps, - lending_pools.total_liquidity, - lending_pools.available_liquidity, - lending_pools.total_borrowed, - lending_pools.created_at, - lending_pools.updated_at, - count(*) over () as total_count - from public.lending_pools - where (status_filter is null or status = status_filter) - ), - ordered_pools as ( - select * from filtered_pools - order by - case - when order_by_col = 'created_at' and order_asc then created_at asc - when order_by_col = 'created_at' and not order_asc then created_at desc - when order_by_col = 'available_liquidity' and order_asc then available_liquidity asc - when order_by_col = 'available_liquidity' and not order_asc then available_liquidity desc - when order_by_col = 'total_liquidity' and order_asc then total_liquidity asc - when order_by_col = 'total_liquidity' and not order_asc then total_liquidity desc - else created_at desc - end - limit page_limit - offset page_offset - ) - select * from ordered_pools; -$$; - --- Grant execute to service role and authenticated users (admin check is in application) -grant execute on function public.get_lending_pools_paginated( - public.pool_status, - integer, - integer, - text, - boolean -) to service_role, authenticated; - --- ───────────────────────────────────────────────────────────────────────────── --- RPC: get_active_pools_with_liquidity --- ───────────────────────────────────────────────────────────────────────────── --- --- Optimized for auto-matching: fetch active pools with sufficient liquidity --- in a single query, pre-sorted by available liquidity (descending). --- --- USAGE: --- SELECT * FROM public.get_active_pools_with_liquidity( --- min_liquidity := 1000 --- ); --- --- PERFORMANCE NOTE: --- - Uses index: idx_lending_pools_status (or composite status + available_liquidity) --- - Returns ONLY active pools sorted by available liquidity --- - Replaces client-side filtering in runAutoMatch action - -drop function if exists public.get_active_pools_with_liquidity(numeric) cascade; - -create or replace function public.get_active_pools_with_liquidity( - min_liquidity numeric default 0 -) -returns table ( - id uuid, - name text, - description text, - status public.pool_status, - apr_bps integer, - total_liquidity numeric, - available_liquidity numeric, - total_borrowed numeric, - created_at timestamptz, - updated_at timestamptz -) -language sql -stable -set search_path = public -as $$ - select - lending_pools.id, - lending_pools.name, - lending_pools.description, - lending_pools.status, - lending_pools.apr_bps, - lending_pools.total_liquidity, - lending_pools.available_liquidity, - lending_pools.total_borrowed, - lending_pools.created_at, - lending_pools.updated_at - from public.lending_pools - where status = 'active' - and (min_liquidity = 0 or available_liquidity > min_liquidity) - order by available_liquidity desc; -$$; - -grant execute on function public.get_active_pools_with_liquidity(numeric) - to service_role, authenticated; - --- ───────────────────────────────────────────────────────────────────────────── --- RECOMMENDED INDEX ADDITIONS FOR OPTIMAL RPC PERFORMANCE --- ───────────────────────────────────────────────────────────────────────────── --- --- Run these in Supabase SQL editor to add recommended indexes: - --- 1. Composite index for status + available_liquidity (used by get_active_pools_with_liquidity) --- Speeds up auto-matching and pool selection queries -create index if not exists idx_lending_pools_status_available -on public.lending_pools (status, available_liquidity desc); - --- 2. Index on created_at for default sort order --- Optimizes pagination when sorting by created_at -create index if not exists idx_lending_pools_created_at_desc -on public.lending_pools (created_at desc); - --- 3. Index on available_liquidity for alternative sort --- Allows users to sort pools by available liquidity -create index if not exists idx_lending_pools_available_liquidity_desc -on public.lending_pools (available_liquidity desc); - --- Notify PostgREST to reload schema so new functions are discoverable -notify pgrst, 'reload schema'; diff --git a/sql/05_interest_rate_model.sql b/sql/05_interest_rate_model.sql deleted file mode 100644 index ac4db12..0000000 --- a/sql/05_interest_rate_model.sql +++ /dev/null @@ -1,28 +0,0 @@ --- ========================= --- Interest Rate Model Support (Issue #114) --- ========================= --- Adds support for fixed vs floating interest rate models per loan. --- Backward-compatible: all existing loans default to 'fixed'. - --- 1. Add rate_model column (fixed or floating) -alter table public.loans - add column if not exists rate_model text not null default 'fixed' - check (rate_model in ('fixed', 'floating')); - --- 2. Track rate model switch history -alter table public.loans - add column if not exists rate_switch_count integer not null default 0; - -alter table public.loans - add column if not exists last_rate_switch_at timestamptz; - --- 3. Index for querying loans by rate model -create index if not exists idx_loans_rate_model on public.loans(rate_model); - --- 4. Comment for documentation -comment on column public.loans.rate_model is - 'Interest rate model: fixed (locked at creation) or floating (dynamic based on pool utilization)'; -comment on column public.loans.rate_switch_count is - 'Number of times the borrower has switched between fixed and floating rates'; -comment on column public.loans.last_rate_switch_at is - 'Timestamp of the most recent rate model switch (enforces 24h cooldown)'; diff --git a/sql/06_kyc_provider.sql b/sql/06_kyc_provider.sql deleted file mode 100644 index 8f94cfe..0000000 --- a/sql/06_kyc_provider.sql +++ /dev/null @@ -1,74 +0,0 @@ --- ===================================================================== --- TrustLend: Issue #118 — KYC Provider Integration --- Adds columns for 3rd-party KYC provider (SumSub-compatible) linkage --- and regulated pool access flag. --- Apply after 01_core_schema.sql --- ===================================================================== - --- 1. New columns on profiles for provider linkage -ALTER TABLE public.profiles - ADD COLUMN IF NOT EXISTS kyc_provider_id TEXT, -- SumSub applicantId - ADD COLUMN IF NOT EXISTS kyc_provider_status TEXT, -- raw provider status string - ADD COLUMN IF NOT EXISTS regulated_pool_access BOOLEAN NOT NULL DEFAULT FALSE, - ADD COLUMN IF NOT EXISTS date_of_birth DATE; - --- 2. Index for fast provider-ID lookup (webhook resolution) -CREATE UNIQUE INDEX IF NOT EXISTS idx_profiles_kyc_provider_id - ON public.profiles (kyc_provider_id) - WHERE kyc_provider_id IS NOT NULL; - --- 3. Index for regulated pool access queries -CREATE INDEX IF NOT EXISTS idx_profiles_regulated_pool_access - ON public.profiles (regulated_pool_access); - --- 4. Ensure 'submitted' value exists in kyc_status enum (idempotent) -DO $$ -BEGIN - ALTER TYPE public.kyc_status ADD VALUE IF NOT EXISTS 'submitted'; -EXCEPTION WHEN duplicate_object THEN NULL; -END $$; - --- 5. RLS: only service-role / admin can write kyc_provider_id and regulated_pool_access --- (users can write their own profile fields but NOT these sensitive ones) - -DROP POLICY IF EXISTS "Service role can write kyc provider fields" ON public.profiles; - --- Supabase service role bypasses RLS automatically; this policy is for --- completeness and to document intent. Regular users cannot set these. -CREATE POLICY "Service role can write kyc provider fields" - ON public.profiles - FOR UPDATE - USING ( - -- Only service role (no JWT) or admin role can update provider fields - (auth.uid() IS NULL) -- service role has no JWT - OR EXISTS ( - SELECT 1 FROM public.profiles AS p - WHERE p.id = auth.uid() AND p.role = 'admin' - ) - ) - WITH CHECK ( - (auth.uid() IS NULL) - OR EXISTS ( - SELECT 1 FROM public.profiles AS p - WHERE p.id = auth.uid() AND p.role = 'admin' - ) - ); - --- 6. View: regulated_kyc_queue for admin monitoring -CREATE OR REPLACE VIEW public.regulated_kyc_queue AS -SELECT - id, - full_name, - kyc_status, - kyc_provider_id, - kyc_provider_status, - regulated_pool_access, - kyc_submitted_at, - kyc_verified_at, - kyc_rejection_reason -FROM public.profiles -WHERE kyc_status IN ('submitted', 'verified', 'rejected') -ORDER BY kyc_submitted_at DESC NULLS LAST; - -COMMENT ON VIEW public.regulated_kyc_queue IS - 'Admin-only view of KYC applicants linked to the 3rd-party provider.'; diff --git a/sql/07_webhook_endpoints.sql b/sql/07_webhook_endpoints.sql deleted file mode 100644 index 9f37db5..0000000 --- a/sql/07_webhook_endpoints.sql +++ /dev/null @@ -1,83 +0,0 @@ --- TrustLend schema for Webhook Endpoints --- Add these columns to store Discord/Telegram webhook integrations - -CREATE TABLE IF NOT EXISTS public.webhook_endpoints ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - url TEXT NOT NULL, - platform TEXT NOT NULL CHECK (platform IN ('discord', 'telegram', 'slack', 'custom')), - topic TEXT NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT true, - created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - --- Ensure URL is valid (basic validation) -ALTER TABLE public.webhook_endpoints - ADD CONSTRAINT valid_webhook_url CHECK (url LIKE 'https://%'); - --- Indexes -CREATE INDEX IF NOT EXISTS idx_webhook_endpoints_platform ON public.webhook_endpoints(platform); -CREATE INDEX IF NOT EXISTS idx_webhook_endpoints_topic ON public.webhook_endpoints(topic); - --- Trigger for updated_at -DROP TRIGGER IF EXISTS trg_webhook_endpoints_updated_at ON public.webhook_endpoints; -CREATE TRIGGER trg_webhook_endpoints_updated_at -BEFORE UPDATE ON public.webhook_endpoints -FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); - --- RLS Policies -ALTER TABLE public.webhook_endpoints ENABLE ROW LEVEL SECURITY; - --- Only Admins can manage webhooks -CREATE POLICY "Admins can view webhooks" - ON public.webhook_endpoints - FOR SELECT - USING ( - EXISTS ( - SELECT 1 FROM public.profiles - WHERE profiles.id = auth.uid() - AND profiles.role = 'admin' - ) - ); - -CREATE POLICY "Admins can insert webhooks" - ON public.webhook_endpoints - FOR INSERT - WITH CHECK ( - EXISTS ( - SELECT 1 FROM public.profiles - WHERE profiles.id = auth.uid() - AND profiles.role = 'admin' - ) - ); - -CREATE POLICY "Admins can update webhooks" - ON public.webhook_endpoints - FOR UPDATE - USING ( - EXISTS ( - SELECT 1 FROM public.profiles - WHERE profiles.id = auth.uid() - AND profiles.role = 'admin' - ) - ) - WITH CHECK ( - EXISTS ( - SELECT 1 FROM public.profiles - WHERE profiles.id = auth.uid() - AND profiles.role = 'admin' - ) - ); - -CREATE POLICY "Admins can delete webhooks" - ON public.webhook_endpoints - FOR DELETE - USING ( - EXISTS ( - SELECT 1 FROM public.profiles - WHERE profiles.id = auth.uid() - AND profiles.role = 'admin' - ) - ); diff --git a/sql/08_partial_loan_fills.sql b/sql/08_partial_loan_fills.sql deleted file mode 100644 index f61ddfb..0000000 --- a/sql/08_partial_loan_fills.sql +++ /dev/null @@ -1,316 +0,0 @@ --- ========================= --- Partial Loan Fills Support (Issue #269) --- ========================= --- Allows multiple lenders to each fund a slice of one large loan request. --- --- Before this migration a loan had exactly one lender: /api/loans/fund flipped --- the loan straight to 'active' and recorded the lender in ledger_transactions. --- That model cannot express "50% funded", and it cannot hold two lenders. --- --- After this migration: --- * public.loan_fundings -- one row per lender contribution --- * loans.funded_amount -- running total, the source of truth for progress --- * a loan activates only when funded_amount >= principal_amount --- --- Backward-compatible: existing single-lender loans are backfilled from --- ledger_transactions in step 6, so their progress renders as 100%. - --- ========================= --- 1. Running funded total on the loan --- ========================= - -alter table public.loans - add column if not exists funded_amount numeric(20, 6) not null default 0 - check (funded_amount >= 0); - -comment on column public.loans.funded_amount is - 'Total XLM committed by all lenders so far. The loan activates when this reaches principal_amount (Issue #269).'; - --- Partially funded loans are the hot query on the marketplace. -create index if not exists idx_loans_status_funded on public.loans(status, funded_amount); - --- ========================= --- 2. Per-lender contributions --- ========================= - -create table if not exists public.loan_fundings ( - id uuid primary key default gen_random_uuid(), - loan_id uuid not null references public.loans(id) on delete cascade, - lender_id uuid not null references public.profiles(id) on delete restrict, - amount numeric(20, 6) not null check (amount > 0), - tx_hash text not null, - lender_address text, - funded_at timestamptz not null default now(), - metadata jsonb not null default '{}'::jsonb, - created_at timestamptz not null default now() -); - --- One Stellar payment can only ever be claimed once. This is the replay guard: --- the old code deduped on (ref_type, ref_id) in ledger_transactions, which --- rejected the *second lender* rather than a resubmitted transaction. -create unique index if not exists idx_loan_fundings_tx_hash on public.loan_fundings(tx_hash); - -create index if not exists idx_loan_fundings_loan_id on public.loan_fundings(loan_id); -create index if not exists idx_loan_fundings_lender_id on public.loan_fundings(lender_id); -create index if not exists idx_loan_fundings_lender_loan on public.loan_fundings(lender_id, loan_id); - -comment on table public.loan_fundings is - 'One row per lender contribution to a loan. Multiple rows per loan = a partially filled loan (Issue #269).'; - --- ========================= --- 3. Row level security --- ========================= - -alter table public.loan_fundings enable row level security; - --- A lender sees their own contributions; a borrower sees who funded their loan. -drop policy if exists loan_fundings_select_own on public.loan_fundings; -create policy loan_fundings_select_own -on public.loan_fundings -for select -using ( - auth.uid() = lender_id - or exists ( - select 1 - from public.loans l - where l.id = loan_fundings.loan_id - and l.borrower_id = auth.uid() - ) -); - --- Writes go exclusively through record_loan_funding() below, which is --- security definer. No direct insert/update/delete policy is granted. - --- ========================= --- 4. Atomic funding RPC --- ========================= --- Replaces activate_loan_funding() for partial fills. The loan row is locked --- FOR UPDATE so two lenders funding the same loan concurrently cannot both --- read the same remaining amount and overfund it. - --- Dropped first so re-running this migration after a signature change works. -drop function if exists public.record_loan_funding(uuid, uuid, numeric, text, text, timestamptz); - -create function public.record_loan_funding( - p_loan_id uuid, - p_lender_id uuid, - p_amount numeric, - p_tx_hash text, - p_lender_address text default null, - p_funded_at timestamptz default now() -) -returns table ( - loan_id uuid, - status public.loan_status, - principal_amount numeric, - funded_amount numeric, - remaining_amount numeric, - is_fully_funded boolean, - funding_id uuid -) -language plpgsql -security definer -set search_path = public -as $$ -declare - v_loan public.loans; - v_new_total numeric(20, 6); - v_remaining numeric(20, 6); - v_funding_id uuid; - v_due_at timestamptz; -begin - if auth.uid() is distinct from p_lender_id then - raise exception 'not authorized'; - end if; - - if p_amount is null or p_amount <= 0 then - raise exception 'funding amount must be greater than zero'; - end if; - - if p_tx_hash is null or length(trim(p_tx_hash)) = 0 then - raise exception 'a stellar transaction hash is required'; - end if; - - -- Lock the loan for the duration of the transaction. - select * into v_loan - from public.loans - where id = p_loan_id - for update; - - if not found then - raise exception 'loan not found'; - end if; - - if v_loan.status not in ('requested', 'approved') then - raise exception 'loan is not available for funding (status: %)', v_loan.status; - end if; - - if v_loan.borrower_id = p_lender_id then - raise exception 'you cannot fund your own loan'; - end if; - - v_remaining := v_loan.principal_amount - v_loan.funded_amount; - - if v_remaining <= 0 then - raise exception 'loan is already fully funded'; - end if; - - -- Reject overfunding outright rather than silently capping: the lender has - -- already sent this exact amount on-chain, so accepting a smaller figure - -- would under-credit them. - if p_amount > v_remaining then - raise exception 'funding amount % exceeds the remaining % on this loan', p_amount, v_remaining; - end if; - - insert into public.loan_fundings (loan_id, lender_id, amount, tx_hash, lender_address, funded_at) - values (p_loan_id, p_lender_id, p_amount, trim(p_tx_hash), p_lender_address, p_funded_at) - returning id into v_funding_id; - - v_new_total := v_loan.funded_amount + p_amount; - - if v_new_total >= v_loan.principal_amount then - -- 100% funded: the loan goes live and the repayment clock starts now. - v_due_at := p_funded_at + make_interval(days => v_loan.duration_days); - - update public.loans - set funded_amount = v_new_total, - status = 'active', - approved_at = coalesce(approved_at, p_funded_at), - funded_at = coalesce(funded_at, p_funded_at), - due_at = v_due_at, - updated_at = now() - where id = p_loan_id - returning * into v_loan; - else - -- Still short: stay open on the marketplace for the next lender. - update public.loans - set funded_amount = v_new_total, - updated_at = now() - where id = p_loan_id - returning * into v_loan; - end if; - - return query - select - v_loan.id, - v_loan.status, - v_loan.principal_amount, - v_loan.funded_amount, - greatest(v_loan.principal_amount - v_loan.funded_amount, 0)::numeric, - (v_loan.funded_amount >= v_loan.principal_amount), - v_funding_id; -end; -$$; - -grant execute on function public.record_loan_funding(uuid, uuid, numeric, text, text, timestamptz) to authenticated; - --- ========================= --- 5. Marketplace listing with funding progress --- ========================= --- Adds principal/funded/remaining so the UI can draw the progress bar, and --- drops loans that have already reached 100% but are mid-activation. - --- CREATE OR REPLACE cannot change a function's return type, and this adds --- columns to the existing signature — so drop it first. -drop function if exists public.get_marketplace_loans(); - -create function public.get_marketplace_loans() -returns table ( - id uuid, - principal_amount numeric, - funded_amount numeric, - remaining_amount numeric, - apr_bps integer, - duration_days integer, - borrower_id uuid, - borrower_name text, - borrower_wallet text, - trust_score integer, - lender_count integer -) -language sql -stable -security definer -set search_path = public -as $$ - select - l.id, - l.principal_amount, - l.funded_amount, - greatest(l.principal_amount - l.funded_amount, 0)::numeric as remaining_amount, - l.apr_bps, - l.duration_days, - l.borrower_id, - case - when coalesce(nullif(p.full_name, ''), '') <> '' then p.full_name - else 'Borrower ' || left(l.borrower_id::text, 6) - end as borrower_name, - coalesce(p.wallet_address, '') as borrower_wallet, - coalesce(rs.score_total, 250) as trust_score, - (select count(*) from public.loan_fundings lf where lf.loan_id = l.id)::integer as lender_count - from public.loans l - left join public.profiles p on p.id = l.borrower_id - left join public.reputation_snapshots rs on rs.user_id = l.borrower_id - where l.status in ('requested', 'approved') - and l.funded_amount < l.principal_amount - order by l.created_at asc; -$$; - -grant execute on function public.get_marketplace_loans() to authenticated; - --- ========================= --- 6. Backfill existing single-lender loans --- ========================= --- Loans funded before this migration recorded the lender only in --- ledger_transactions. Replay those into loan_fundings so historical loans --- report 100% progress and lenders keep their claim to repayment. - --- The funding route writes metadata with JSON.stringify(), which lands in the --- jsonb column as a *string scalar* rather than an object. Normalize both --- shapes before reading txHash out of it. -with legacy_funding as ( - select - l.id as loan_id, - lt.id as ledger_tx_id, - lt.user_id as lender_id, - l.principal_amount, - coalesce(l.funded_at, lt.created_at) as funded_at, - case jsonb_typeof(lt.metadata) - when 'object' then lt.metadata - when 'string' then (lt.metadata #>> '{}')::jsonb - else '{}'::jsonb - end as meta - from public.loans l - join public.ledger_transactions lt - on lt.ref_type = 'loan_fund' - and lt.ref_id = l.id - where not exists ( - select 1 from public.loan_fundings lf where lf.loan_id = l.id - ) -) -insert into public.loan_fundings (loan_id, lender_id, amount, tx_hash, lender_address, funded_at, metadata) -select - loan_id, - lender_id, - principal_amount, - coalesce(nullif(meta ->> 'txHash', ''), 'legacy:' || ledger_tx_id::text), - meta ->> 'lenderAddress', - funded_at, - jsonb_build_object('backfilled', true, 'source_ledger_tx', ledger_tx_id) -from legacy_funding -on conflict (tx_hash) do nothing; - --- Mark those loans fully funded. -update public.loans l -set funded_amount = l.principal_amount -where l.funded_amount = 0 - and exists (select 1 from public.loan_fundings lf where lf.loan_id = l.id); - --- Any loan already past the funding stage is by definition fully funded. -update public.loans -set funded_amount = principal_amount -where funded_amount = 0 - and status in ('active', 'funded', 'repaid', 'defaulted'); - --- Force PostgREST schema cache reload so the new RPCs are discoverable. -notify pgrst, 'reload schema'; diff --git a/sql/09_referral_program.sql b/sql/09_referral_program.sql deleted file mode 100644 index b12a8f1..0000000 --- a/sql/09_referral_program.sql +++ /dev/null @@ -1,410 +0,0 @@ --- ========================= --- Referral Program (Issue #266) --- ========================= --- Users get a unique referral link; when an invited friend takes out a loan, --- the referrer earns a bonus paid on-chain by the ReferralRewardsContract. --- --- This migration owns the *off-chain* half: --- * profiles.referral_code -- the unique code behind each user's link --- * public.referrals -- one row per invited user, with payout state --- * claim_referral_bonus() -- marks a referral payable when a loan activates --- --- The on-chain contract remains the source of truth for whether a bonus was --- actually transferred. Rows here track intent and mirror the result, so the --- dashboard can render progress without an RPC round-trip per referral. - --- ========================= --- 1. Referral code on the profile --- ========================= - -alter table public.profiles - add column if not exists referral_code text; - -comment on column public.profiles.referral_code is - 'Unique invite code behind this user''s referral link, e.g. TL7F3KQ2 (Issue #266).'; - --- Codes are handed out in links, so they must be unique. Partial index keeps --- pre-existing rows with a NULL code valid until they are backfilled. -create unique index if not exists idx_profiles_referral_code - on public.profiles(referral_code) - where referral_code is not null; - --- ========================= --- 2. Referral edges --- ========================= - --- Wrapped so re-running this migration is a no-op, matching 01_core_schema. -do $$ begin - create type public.referral_status as enum ( - 'pending', -- friend signed up, has not borrowed yet - 'qualified', -- friend's loan activated; bonus is owed - 'paid', -- bonus confirmed transferred on-chain - 'rejected' -- disqualified (self-referral, fraud, cap reached) - ); -exception - when duplicate_object then null; -end $$; - -create table if not exists public.referrals ( - id uuid primary key default gen_random_uuid(), - referrer_id uuid not null references public.profiles(id) on delete cascade, - referee_id uuid not null references public.profiles(id) on delete cascade, - -- The code as it was used, retained even if the referrer later rotates it. - referral_code text not null, - status public.referral_status not null default 'pending', - -- The loan whose activation qualified this referral. - qualifying_loan_id uuid references public.loans(id) on delete set null, - qualified_at timestamptz, - paid_at timestamptz, - -- Bonus as reported by the contract, in whole reward tokens. - bonus_amount numeric(20, 7) not null default 0 check (bonus_amount >= 0), - -- Stellar transaction that carried the payout. - payout_tx_hash text, - metadata jsonb not null default '{}'::jsonb, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now(), - - -- A user is attributed to exactly one referrer, forever. This is the - -- database-side mirror of the contract's immutable ReferrerOf mapping. - constraint referrals_referee_unique unique (referee_id), - -- Self-referral is meaningless and is the cheapest possible abuse. - constraint referrals_no_self check (referrer_id <> referee_id) -); - -create index if not exists idx_referrals_referrer_id on public.referrals(referrer_id); -create index if not exists idx_referrals_status on public.referrals(status); -create index if not exists idx_referrals_referrer_status - on public.referrals(referrer_id, status); - -comment on table public.referrals is - 'One row per invited user. Mirrors the on-chain ReferralRewardsContract state (Issue #266).'; - -drop trigger if exists trg_referrals_updated_at on public.referrals; -create trigger trg_referrals_updated_at -before update on public.referrals -for each row execute function public.set_updated_at(); - --- ========================= --- 3. Row level security --- ========================= - -alter table public.referrals enable row level security; - --- A referrer sees who they invited; an invited user sees their own edge. -drop policy if exists referrals_select_own on public.referrals; -create policy referrals_select_own -on public.referrals -for select -using (auth.uid() = referrer_id or auth.uid() = referee_id); - -drop policy if exists referrals_admin_all on public.referrals; -create policy referrals_admin_all -on public.referrals -for all -using (public.is_admin()) -with check (public.is_admin()); - --- Writes go through the security-definer functions below. No direct --- insert/update policy is granted to authenticated users, so nobody can --- attribute themselves to a referrer or mark their own bonus paid. - --- ========================= --- 4. Code assignment --- ========================= --- Generating the code in SQL keeps every account guaranteed to have one, even --- for users created before this migration or through a path that bypasses the --- app. The alphabet mirrors lib/referrals/codes.ts (no 0/O/1/I/L/U). - -create or replace function public.generate_referral_code() -returns text -language plpgsql -volatile -as $$ -declare - v_alphabet constant text := '23456789ABCDEFGHJKMNPQRSTVWXYZ'; - v_code text; - v_attempt int := 0; -begin - loop - v_code := 'TL'; - for _ in 1..6 loop - -- floor(random() * 30) + 1 -> 1..30, substr is 1-indexed. - v_code := v_code || substr(v_alphabet, floor(random() * 30)::int + 1, 1); - end loop; - - exit when not exists ( - select 1 from public.profiles where referral_code = v_code - ); - - v_attempt := v_attempt + 1; - if v_attempt > 20 then - -- 30^6 is ~729M; 20 collisions in a row means something is very wrong. - raise exception 'Could not generate a unique referral code after % attempts', v_attempt; - end if; - end loop; - - return v_code; -end; -$$; - --- Assign a code to any profile that lacks one. -create or replace function public.ensure_referral_code(p_user_id uuid) -returns text -language plpgsql -security definer -set search_path = public -as $$ -declare - v_code text; -begin - select referral_code into v_code - from public.profiles - where id = p_user_id; - - if v_code is not null then - return v_code; - end if; - - v_code := public.generate_referral_code(); - - update public.profiles - set referral_code = v_code - where id = p_user_id and referral_code is null; - - -- Another session may have won the race; return whatever actually stuck. - select referral_code into v_code - from public.profiles - where id = p_user_id; - - return v_code; -end; -$$; - -grant execute on function public.ensure_referral_code(uuid) to authenticated; - --- New users get a code at signup. -create or replace function public.assign_referral_code_on_profile() -returns trigger -language plpgsql -security definer -set search_path = public -as $$ -begin - if new.referral_code is null then - new.referral_code := public.generate_referral_code(); - end if; - return new; -exception - when others then - -- Never block profile creation over a referral code; ensure_referral_code() - -- backfills it on first visit to the referrals page. - return new; -end; -$$; - -drop trigger if exists trg_profiles_referral_code on public.profiles; -create trigger trg_profiles_referral_code -before insert on public.profiles -for each row execute function public.assign_referral_code_on_profile(); - --- Backfill everyone who predates this migration. -update public.profiles -set referral_code = public.generate_referral_code() -where referral_code is null; - --- ========================= --- 5. Attribution --- ========================= --- Called when a new user signs up with ?ref=CODE. Security definer because the --- referee must not be able to write arbitrary rows into public.referrals. - -create or replace function public.record_referral( - p_referee_id uuid, - p_referral_code text -) -returns table ( - referral_id uuid, - referrer_id uuid, - status public.referral_status -) -language plpgsql -security definer -set search_path = public -as $$ -declare - v_referrer_id uuid; - v_code text; - v_referral_id uuid; - v_status public.referral_status; -begin - v_code := upper(trim(p_referral_code)); - - if v_code is null or v_code = '' then - raise exception 'Referral code is required'; - end if; - - select id into v_referrer_id - from public.profiles - where referral_code = v_code; - - if v_referrer_id is null then - raise exception 'Unknown referral code'; - end if; - - if v_referrer_id = p_referee_id then - raise exception 'Cannot refer yourself'; - end if; - - -- One attribution per referee, first one wins. ON CONFLICT keeps a double - -- submit idempotent instead of raising. - insert into public.referrals (referrer_id, referee_id, referral_code, status) - values (v_referrer_id, p_referee_id, v_code, 'pending') - on conflict (referee_id) do nothing - returning id into v_referral_id; - - if v_referral_id is null then - -- Already attributed; return the existing edge unchanged. - select r.id, r.referrer_id, r.status - into v_referral_id, v_referrer_id, v_status - from public.referrals r - where r.referee_id = p_referee_id; - - referral_id := v_referral_id; - referrer_id := v_referrer_id; - status := v_status; - return next; - return; - end if; - - referral_id := v_referral_id; - referrer_id := v_referrer_id; - status := 'pending'::public.referral_status; - return next; -end; -$$; - -grant execute on function public.record_referral(uuid, text) to authenticated; - --- ========================= --- 6. Qualification --- ========================= --- Called by the loan-funding route when a loan reaches 100% and activates. --- Flips the referee's pending edge to 'qualified' so the dashboard can show --- the bonus as owed while the chain settles. - -create or replace function public.qualify_referral( - p_referee_id uuid, - p_loan_id uuid -) -returns table ( - referral_id uuid, - referrer_id uuid, - status public.referral_status -) -language plpgsql -security definer -set search_path = public -as $$ -declare - v_row public.referrals; -begin - -- Lock the edge so two concurrent activations cannot both qualify it. - select * into v_row - from public.referrals - where referee_id = p_referee_id - for update; - - if not found then - return; -- borrower was not referred; nothing to do - end if; - - -- Only a pending referral can qualify. Anything already qualified, paid or - -- rejected stays as it is — this is what stops a second loan from earning a - -- second bonus, mirroring the contract's BonusPaid flag. - if v_row.status <> 'pending'::public.referral_status then - referral_id := v_row.id; - referrer_id := v_row.referrer_id; - status := v_row.status; - return next; - return; - end if; - - update public.referrals - set status = 'qualified'::public.referral_status, - qualifying_loan_id = p_loan_id, - qualified_at = now() - where id = v_row.id; - - referral_id := v_row.id; - referrer_id := v_row.referrer_id; - status := 'qualified'::public.referral_status; - return next; -end; -$$; - -grant execute on function public.qualify_referral(uuid, uuid) to service_role; - --- ========================= --- 7. Settlement --- ========================= --- Records the on-chain result once the payout transaction is observed. - -create or replace function public.settle_referral_payout( - p_referral_id uuid, - p_bonus_amount numeric, - p_tx_hash text -) -returns void -language plpgsql -security definer -set search_path = public -as $$ -begin - if p_bonus_amount < 0 then - raise exception 'Bonus amount cannot be negative'; - end if; - - update public.referrals - set status = 'paid'::public.referral_status, - bonus_amount = p_bonus_amount, - payout_tx_hash = p_tx_hash, - paid_at = now() - where id = p_referral_id - and status <> 'paid'::public.referral_status; -end; -$$; - -grant execute on function public.settle_referral_payout(uuid, numeric, text) to service_role; - --- ========================= --- 8. Dashboard read model --- ========================= - -create or replace function public.get_referral_stats(p_user_id uuid) -returns table ( - referral_code text, - total_invited bigint, - pending_count bigint, - qualified_count bigint, - paid_count bigint, - total_earned numeric -) -language sql -stable -security definer -set search_path = public -as $$ - select - p.referral_code, - count(r.id) as total_invited, - count(r.id) filter (where r.status = 'pending') as pending_count, - count(r.id) filter (where r.status = 'qualified') as qualified_count, - count(r.id) filter (where r.status = 'paid') as paid_count, - coalesce(sum(r.bonus_amount) filter (where r.status = 'paid'), 0) as total_earned - from public.profiles p - left join public.referrals r on r.referrer_id = p.id - where p.id = p_user_id - group by p.referral_code; -$$; - -grant execute on function public.get_referral_stats(uuid) to authenticated; diff --git a/sql/10_borrow_cap.sql b/sql/10_borrow_cap.sql deleted file mode 100644 index 8043e2e..0000000 --- a/sql/10_borrow_cap.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Migration: Add borrow_cap column to lending_pools table --- Issue #153: Implement maximum cap on total borrowable amount per pool --- Run this migration in your Supabase SQL editor - --- Add borrow_cap column (nullable - null means no cap) -alter table public.lending_pools - add column if not exists borrow_cap numeric(20, 7) null - constraint lending_pools_borrow_cap_check check (borrow_cap is null or borrow_cap > 0); - -comment on column public.lending_pools.borrow_cap is - 'Maximum total XLM/USDC that can be borrowed from this pool at any time. NULL means no cap enforced.'; - --- Index to quickly find pools that have a cap set -create index if not exists idx_lending_pools_borrow_cap - on public.lending_pools (borrow_cap) - where borrow_cap is not null; diff --git a/supabase/notifications.sql b/supabase/notifications.sql deleted file mode 100644 index 0dd1608..0000000 --- a/supabase/notifications.sql +++ /dev/null @@ -1,34 +0,0 @@ --- Create Notifications Table -CREATE TABLE IF NOT EXISTS public.notifications ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - title TEXT NOT NULL, - message TEXT NOT NULL, - type TEXT NOT NULL, -- e.g., 'loan_requested', 'loan_approved', 'loan_funded', 'loan_repaid', 'system' - read BOOLEAN NOT NULL DEFAULT false, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - --- Turn on RLS -ALTER TABLE public.notifications ENABLE ROW LEVEL SECURITY; - --- Policy: Users can select their own notifications -CREATE POLICY "Users can view their own notifications" - ON public.notifications - FOR SELECT - USING (auth.uid() = user_id); - --- Policy: Users can update their own notifications (e.g., mark as read/clear) -CREATE POLICY "Users can update their own notifications" - ON public.notifications - FOR UPDATE - USING (auth.uid() = user_id); - --- Policy: Users can delete their own notifications (e.g. clear all) -CREATE POLICY "Users can delete their own notifications" - ON public.notifications - FOR DELETE - USING (auth.uid() = user_id); - --- Note: Inserts will be handled by the service role within API routes, --- so we don't need a public INSERT policy. diff --git a/supabase/rls-policies.sql b/supabase/rls-policies.sql deleted file mode 100644 index bbaf053..0000000 --- a/supabase/rls-policies.sql +++ /dev/null @@ -1,66 +0,0 @@ --- ============================================================================= --- TrustLend — Supabase RLS Policies --- Run these in your Supabase SQL Editor (Dashboard → SQL Editor → New query) --- These replace the need for the service role key in most read queries. --- ============================================================================= - --- ── loans ───────────────────────────────────────────────────────────────────── - --- Borrowers see their own loans -CREATE POLICY "borrower_own_loans" - ON loans FOR SELECT - USING (borrower_id = auth.uid()); - --- Lenders (and anyone authenticated) see open/approved loans in the marketplace -CREATE POLICY "lender_sees_open_loans" - ON loans FOR SELECT - USING (status IN ('requested', 'approved')); - --- Lenders see loans they directly funded (via ledger lookup is handled separately) --- (The above two policies cover all cases for MVP) - --- ── profiles ────────────────────────────────────────────────────────────────── - --- Users see their own profile -CREATE POLICY "own_profile" - ON profiles FOR SELECT - USING (id = auth.uid()); - --- Authenticated users can see the public fields of other profiles --- (needed for marketplace: borrower name + wallet address) -CREATE POLICY "public_profile_read" - ON profiles FOR SELECT - USING (auth.role() = 'authenticated'); - --- ── reputation_snapshots ────────────────────────────────────────────────────── - --- Users see their own reputation snapshot -CREATE POLICY "own_reputation" - ON reputation_snapshots FOR SELECT - USING (user_id = auth.uid()); - --- Authenticated users can see others' trust scores (needed for marketplace) -CREATE POLICY "public_reputation_read" - ON reputation_snapshots FOR SELECT - USING (auth.role() = 'authenticated'); - --- ── pool_positions ──────────────────────────────────────────────────────────── - --- Lenders see only their own positions -CREATE POLICY "own_pool_positions" - ON pool_positions FOR SELECT - USING (lender_id = auth.uid()); - --- ── lending_pools ───────────────────────────────────────────────────────────── - --- Anyone authenticated can see pools (needed for lender deposit form) -CREATE POLICY "authenticated_see_pools" - ON lending_pools FOR SELECT - USING (auth.role() = 'authenticated'); - --- ============================================================================= --- IMPORTANT: After applying these policies, the service role key is only --- needed for WRITE operations by admins (approving KYC, etc.) and for --- counting total loans in admin metrics. All read queries on the lender --- marketplace can switch back to the session-bound client. --- ============================================================================= diff --git a/vercel.json b/vercel.json index 5144124..c9c3e2c 100644 --- a/vercel.json +++ b/vercel.json @@ -8,13 +8,17 @@ "path": "/api/cron/default-management", "schedule": "0 2 * * *" }, + { + "path": "/api/cron/reputation-scoring", + "schedule": "0 3 * * *" + }, { "path": "/api/cron/liquidation", - "schedule": "* * * * *" + "schedule": "0 4 * * *" }, { "path": "/api/cron/price-oracle", - "schedule": "* * * * *" + "schedule": "0 5 * * *" } ] }