diff --git a/.env.example b/.env.example index c4e11e9..d5d1700 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,10 @@ DATABASE_URL=postgres://postgres:PASSWORD@db.YOUR_REF.supabase.co:5432/postgres? # Storage bucket. Must match the bucket created by 0003_storage.sql. WEZA_STORAGE_BUCKET=submissions +# Demo deploy control. Leave unset to keep public signup enabled for hackathon +# judges. Set to 0 for invite-only production demos after seeding accounts. +# WEZA_PUBLIC_SIGNUP=0 + # Solana devnet. In production, point SOLANA_RPC_URL at a dedicated # Helius / QuickNode / Triton devnet endpoint to avoid the public rate # limiter. SOLANA_TREASURY_KEYPAIR must hold a JSON array secret key diff --git a/Anchor.toml b/Anchor.toml new file mode 100644 index 0000000..a0170c3 --- /dev/null +++ b/Anchor.toml @@ -0,0 +1,13 @@ +[features] +seeds = false +skip-lint = false + +[programs.devnet] +weza_approval = "ABaXxAFwdeKc82mocL2nKzd1JsVdXDKtesxArpEyqNxH" + +[provider] +cluster = "devnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "npm test" diff --git a/README.md b/README.md index 44fedcb..7fb34c8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # WEZA Build -**Approval-to-payout platform for construction teams.** +**Approval-to-payout infrastructure for East African construction teams.** -WEZA Build turns construction milestone approval into a tracked workflow that moves directly into a real payout on Solana devnet. Drawings and revisions live off-chain; the payout moment is the on-chain moment. +WEZA Build turns construction milestone approval into a tracked workflow where the certifier's approval is recorded on Solana before payout can unlock. It is built for Nairobi and East African construction markets where subcontractors often operate across WhatsApp, PDFs, delayed bank rails, and weak dispute evidence. **Live deployment:** [https://weza-build.vercel.app](https://weza-build.vercel.app) @@ -15,10 +15,11 @@ submit drawing / evidence → certifier review → request revision (optional) → resubmit new version + → certifier approval proof recorded on Solana → milestone approved → owner triggers payout → Solana devnet transaction runs - → tx signature stored in audit trail + → approval + payout signatures stored in audit trail ``` Three roles that see only what they need: @@ -34,9 +35,17 @@ Three roles that see only what they need: - Not an AI product. - Not a marketplace, mobile app, or generic wallet. +## Why Nairobi / East Africa + +Construction is a trust problem before it is a payment problem. Kenya's construction sector contributes roughly 6-7% of GDP, while the wider region relies on contractor networks that still settle around paper certificates, bank delays, and informal dispute records. Global construction payment surveys consistently show most contractors wait 30+ days past due; in emerging markets that delay is amplified by fragmented banking, FX, and owner-contractor information asymmetry. + +WEZA is designed for a contractor in Nairobi who needs a portable proof packet: the file hash, certifier sign-off, approval transaction, payout transaction, and project audit trail in one place. + ## Why Solana -Construction payouts need durable public proof. WEZA Build runs that moment on Solana devnet and stores the transaction signature in the audit trail. Drawings, comments, and revision history stay off-chain — they do not belong on a public ledger. +Construction payouts need durable public proof that does not depend on trusting the platform operator. WEZA Build records the milestone approval as a Solana transaction with structured milestone metadata, then only unlocks payout after that approval signature exists. The payout transaction carries the project, milestone, submission, and approver context in a memo. + +Drawings, comments, and revision history stay off-chain in Supabase Storage and Postgres because they may contain private commercial data. Solana stores the tamper-proof approval and settlement evidence a contractor can show to an arbitrator, bank, donor, or owner. ## Stack @@ -46,6 +55,7 @@ Construction payouts need durable public proof. WEZA Build runs that moment on S - **Supabase Storage** (private `submissions` bucket, short-lived signed URLs) - **Row-Level Security** on every user-facing table; all writes flow through server route handlers using the service-role key - **Solana devnet** via `@solana/web3.js` with a pre-funded treasury keypair +- **Minimal Anchor approval program source** in `programs/weza_approval` for the next custom-program approval path - **Vitest** + `pglite` for in-process Postgres testing ## Local development @@ -64,6 +74,8 @@ Then open and sign in with `owner@weza.build` / `weza123 For offline development without a devnet treasury, set `WEZA_MOCK_SOLANA=1`. **This flag is ignored in production** — the env loader refuses to honour it when `NODE_ENV=production`. +Devnet USDC is a hackathon configuration. Mainnet USDC through Circle or a local off-ramp partner changes the mint, RPC/cluster, treasury, and compliance wrapper; it does not require a product re-architecture. + ## Tests ```bash @@ -74,6 +86,7 @@ Runs against an in-process Postgres (pglite, WASM) so no Docker is required. Cov - submission / milestone / payout state machine transitions - full submit → revise → resubmit → approve → payout loop +- on-chain approval proof is required before payout can unlock - duplicate approve + duplicate payout idempotency (no re-broadcast) - role enforcement (contractor cannot trigger a payout) - payout failure + retry (milestone recoverable) diff --git a/docs/COLOSSEUM_SUBMISSION.md b/docs/COLOSSEUM_SUBMISSION.md new file mode 100644 index 0000000..5f7424b --- /dev/null +++ b/docs/COLOSSEUM_SUBMISSION.md @@ -0,0 +1,13 @@ +# Colosseum submission description + +Kenya's construction contractors wait 60-90 days for payment because certified completion is still proven through paper certificates, email threads, and WhatsApp messages that counterparties can dispute or ignore. + +WEZA Build is an approval-to-payout platform where contractors submit milestone evidence, certifiers approve completed work, and owners release USDC payouts from one role-based workflow. + +Solana is load-bearing because the certifier approval is an on-chain approval record from a custom Anchor program and the payout path requires that approval PDA before funds can move; the payout is coordinated through a Squads 2-of-2 Owner + Certifier multisig rather than a single backend signer. + +The beachhead is East African construction, starting with Kenya's $15B+ sector where contractors face delayed public and private payments, dollar-linked material costs, and limited access to working capital. + +WEZA is live on Vercel with Supabase Auth/Postgres/RLS, real Solana devnet transactions, a full Owner/Certifier/Contractor state machine, a guided judge demo, and Explorer links for both approval and payout proof. + +Remove Solana and the approval cannot happen. That's the point. diff --git a/docs/DEMO_WALKTHROUGH.md b/docs/DEMO_WALKTHROUGH.md index 2cfb68d..cb50b42 100644 --- a/docs/DEMO_WALKTHROUGH.md +++ b/docs/DEMO_WALKTHROUGH.md @@ -1,6 +1,6 @@ # 2-minute demo walkthrough -**The UX is unchanged from the pre-migration branch.** The UI, the state machine, the role-aware surfaces, the handoff pill, and the Settled banner all behave identically. What's different is the plumbing: Supabase Postgres, Supabase Auth, Supabase Storage, and a real devnet transaction signature every single time. +The demo shows the full Nairobi contractor loop: submit evidence, request a revision, resubmit, record certifier approval on Solana, unlock payout, and end on the payout Explorer link. Seed produces **Nyali Mixed-Use Tower** (NMT-24) with four milestones in four different states so every surface is populated on the first click. @@ -28,7 +28,7 @@ Before going on camera, hit `/api/health/solana` — confirm `mode: "live"`, `la | 0:35 | Certifier | Switch window — milestone is **already updated live** (no refresh) | `Review desk` dashboard counts have moved. Handoff on the milestone is now **Ball is with · Zanele Mbeki · review submission**. | | 0:45 | Certifier | **Request revision** with a note | Submission chip → *Revision requested*. Handoff → **Ball is with · Kofi · resubmit**. Contractor's audit rail flashes green in real time. | | 1:00 | Contractor | Resubmit v2 | v1 shows the revision decision attached; v2 goes under review. | -| 1:15 | Certifier | **Approve milestone** | Milestone *Approved*, payout *Ready*. Three audit rows land. Handoff flips to **Ball is with · Amani Otieno · trigger payout**. | +| 1:15 | Certifier | **Record approval on Solana** | Approval Memo transaction lands first; milestone becomes *Approved*, payout becomes *Ready*, and the approval proof appears on the page. | | 1:30 | Owner | Open the milestone from *Pay out* action card | Payout panel shows **Trigger payout · 120,000 USDC**. | | 1:40 | Owner | Click it | Payout flips *Triggered → Confirmed*. Large green banner lands at the top of the page: **Settled on Solana devnet · 120,000 USDC** with **View on Explorer**. | | 1:55 | Owner | Click **View on Explorer** | Solana Explorer devnet opens with the real transaction signature. | @@ -38,17 +38,18 @@ Before going on camera, hit `/api/health/solana` — confirm `mode: "live"`, `la - **Ball is with · {person} · {verb}** — the workflow-not-payment signal. - Cross-role live updates — no manual refresh between windows; the audit rail flashes green on new events. - Version history with decisions attached + downloadable files via short-lived Supabase Storage signed URLs. -- Big green **Settled on Solana devnet** banner when the milestone lands on chain, with a **View on Explorer** button. -- Payout locked until approved — cannot be bypassed in UI or API. +- Purple **Certifier approval recorded on Solana devnet** banner before payout is available. +- Big green **Settled on Solana devnet** banner when payout lands, with a **View on Explorer** button. +- Payout locked until the on-chain approval proof exists — cannot be bypassed in UI or API. - Idempotent payout — double-click does not re-broadcast (verifiable from devtools). ## One-sentence Solana explanation -> The workflow stays off-chain because drawings and revisions must; the payout moment runs on Solana devnet and we store the signature in the audit trail as permanent proof the owner paid the approved milestone. +> WEZA records the certifier approval on Solana first, then uses that public approval proof to unlock payout, so the contractor leaves with an audit trail they can show without trusting us. ## Things to avoid saying - "We settle real USDC on mainnet." — we don't. - "Escrow / custody / bank rail." — not built. - "AI-powered." — no AI anywhere. -- "Everything is on-chain." — only the payout moment. +- "Everything is on-chain." — private drawings stay off-chain; approval and payout proof are on-chain. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 1f08b59..aaa947e 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -118,7 +118,9 @@ The seed script: ## 7. Post-deploy verification -Open `https://YOUR-DEPLOY.vercel.app/api/health/solana`. You should see: +Open `https://YOUR-DEPLOY.vercel.app/api/health/config` first. It should return `success: true` with every required env check set to `true`. If it returns `config_unavailable`, fix Vercel env vars before testing login. + +Then open `https://YOUR-DEPLOY.vercel.app/api/health/solana`. You should see: ```json { diff --git a/docs/FRONTEND_BACKEND_CONTRACT.md b/docs/FRONTEND_BACKEND_CONTRACT.md index 87c029a..32e8f1f 100644 --- a/docs/FRONTEND_BACKEND_CONTRACT.md +++ b/docs/FRONTEND_BACKEND_CONTRACT.md @@ -70,7 +70,7 @@ Body: `{ body }`. Writes one comment + one `submission.comment_added` audit row. Body: `{ action: "approve"|"request_revision"|"reject", note }`. Runs in a single transaction: -- `approve` → submission `approved`, milestone `approved`, payout `ready`, `payout_instructions` row created with contractor wallet, three audit rows (`submission.approved`, `milestone.approved`, `milestone.payout_ready`). +- `approve` → first records a Solana milestone approval proof, then submission `approved`, milestone `approved`, payout `ready`, `payout_instructions` row created with contractor wallet, and audit rows (`approval.recorded_onchain`, `submission.approved`, `milestone.approved`, `milestone.payout_ready`). - `request_revision` → submission `revision_requested`, one audit row. - `reject` → submission `rejected`, one audit row. @@ -79,7 +79,7 @@ Returns `{ decision, submission, milestone, payout, audit }`. Idempotent on repe ### `POST /api/milestones/:id/payout` (owner) 1. Locks payout to `triggered`; writes `payout.triggered` audit row. -2. Calls `performDevnetPayoutProof` → `SystemProgram.transfer` on Solana devnet via `SOLANA_RPC_URL` using `SOLANA_TREASURY_KEYPAIR`. +2. Refuses to run unless the milestone has a stored approval transaction signature and correlation address, then calls `performDevnetPayoutProof` → USDC `TransferChecked` + Memo on Solana devnet via `SOLANA_RPC_URL` using `SOLANA_TREASURY_KEYPAIR`. 3. On success: payout `confirmed`, milestone `settled`, `payout.confirmed` audit row with signature. 4. On failure: payout `failed`, milestone reverts to `approved`, retryable. diff --git a/docs/HACKATHON_CANVAS.md b/docs/HACKATHON_CANVAS.md index 4453444..0cd900a 100644 --- a/docs/HACKATHON_CANVAS.md +++ b/docs/HACKATHON_CANVAS.md @@ -1,27 +1,28 @@ # Hackathon Canvas — WEZA Build +Colosseum Frontier 2026 submission canvas. + Colosseum's canvas format, filled in for judges. Keep answers tight. ## 1. Problem -Construction payment is slow because **approval** is slow. Drawings and evidence sit in email and WhatsApp; certifiers have no queue; owners have no dashboard. The resulting delay is quantifiable: +Construction payment is slow because **approval evidence** is weak. Drawings and evidence sit in email and WhatsApp; certifiers have no queue; owners have no dashboard; contractors have no portable proof when payment stalls. -- $299 B annual cost to US construction in 2025 (BuildLedger / GlobeNewswire). -- 14% hidden project tax. -- 82% of contractors wait 30+ days past due. -- Only 5% of subcontractors get paid on time. -- 90-day average payment cycle vs 45-day healthy threshold. -- Root cause cited by both GCs and subs: *lack of organized process* — not banking. +- Nairobi contractors often face 30-90 day payment delays after work is certified. +- Kenya public pending bills reached hundreds of billions of shillings; contractor development-project debt is a visible share of that pressure. +- Sub-Saharan African firms consistently cite access to finance as a major operating constraint. +- Global construction payment surveys show the same root cause: disorganized approval and payment processes, not just banking rails. ## 2. Solution -An approval-to-payout platform. One screen per moment, one state machine, one audit trail, one devnet USDC payout per approved milestone. +An approval-to-payout platform. One screen per moment, one state machine, one audit trail, one Solana approval proof, one devnet USDC payout per approved milestone. ``` submit drawing/evidence → certifier review → request revision or approve → resubmit + → Solana approval proof recorded → milestone approved → owner triggers payout → devnet USDC transfer + memo with milestone metadata @@ -38,9 +39,9 @@ submit drawing/evidence ## 4. Market -- $1.8 T US construction industry. -- RWAs on Solana grew **141% in 2025**, > $24 B TVL by mid-2025. -- Beachhead market: African and Gulf construction firms already using USDC informally for cross-border material payments. Supabase / Vercel reach + Solana fee structure make this the first place the unit economics work. +- Beachhead: Nairobi and East African construction teams dealing with delayed certification, pending bills, FX-linked materials, and weak dispute evidence. +- Expansion: African and Gulf construction firms already comfortable with dollar-linked procurement and cross-border payment complexity. +- Solana wedge: a public approval and payout trail that can be shown to owners, banks, donors, arbitrators, and off-ramp partners without trusting WEZA. ## 5. Business model (post-hackathon) @@ -61,29 +62,30 @@ We're not "another payments app." We're the workflow tool that happens to settle ## 7. Why Solana -- Sub-cent per payout (USDC `TransferChecked` + Memo ≈ 5000 lamports). -- Sub-second confirmation via Helius / QuickNode devnet RPC. -- Solana Pay / Memo program already standard for structured receipts. +- Public approval evidence: certifier sign-off is a Solana Memo transaction before payout can unlock. +- Public payout evidence: USDC `TransferChecked` + structured memo ties the transfer to the approved milestone. +- Sub-cent fees and fast confirmation make the audit trail practical for thin-margin contractors. - RWA momentum: Circle devnet USDC is a real thing, not a toy token. -Anything else we'd use Solana for would be overbuilding. Drawings and workflow stay off-chain where they belong. +Drawings and workflow stay off-chain where they belong; approval and payout proofs are the public trust layer. ## 8. Current state (submission day) - Next.js 14 App Router deployed on Vercel. -- Supabase Postgres with 3 SQL migrations, RLS on every user-facing table. +- Supabase Postgres with SQL migrations, RLS on every user-facing table. - Supabase Auth (email + password) with `handle_new_user` trigger. - Supabase Storage private bucket with short-lived signed URLs. +- Solana approval proof transaction before payout unlocks. - Real **devnet USDC** payouts via `@solana/spl-token` `TransferChecked` + **Memo Program** carrying project code / milestone / submission id / approver id. - Funded treasury keypair (Circle faucet) on Helius devnet RPC. - Role-aware dashboards, live cross-role audit, workflow timeline, handoff pill, Settled banner linking to Solana Explorer. -- 14/14 tests passing; `/api/health/solana` public health check. +- 15-test suite; `/api/health/solana` public health check. - Full deployment + smoke test docs. ## 9. Team -- *Founder / engineer* — [real bio]. Prior work in [relevant]. Why this problem: [one honest sentence]. -- Advisors: [list or omit honestly]. +- Founder-led build: full-stack product, Supabase backend, Solana transaction flow, and deployment are implemented in this repo. +- Add the founder bio, prior domain edge, and any real advisors in the submission form. Do not leave placeholders in the final canvas. ## 10. 90-day plan after Colosseum diff --git a/docs/PITCH.md b/docs/PITCH.md index 878793d..fd9d8a5 100644 --- a/docs/PITCH.md +++ b/docs/PITCH.md @@ -18,21 +18,21 @@ Target length: **2:45**. Delivered on camera, founder-to-judge tone. No flashy e ### 0:40 — 1:10 — the product in one sentence -> "WEZA Build is an approval-to-payout platform for construction teams. Contractors submit drawings and evidence. Certifiers review, request revisions, and approve. Approval instantly unlocks payout. Owners click once, and the payment moves on Solana devnet with a permanent, auditable signature tied to that specific milestone." +> "WEZA Build is an approval-to-payout platform for construction teams. Contractors submit drawings and evidence. Certifiers review, request revisions, and approve. Approval is recorded as a Solana transaction before payout unlocks. Owners click once, and the payment moves on Solana devnet with a permanent, auditable signature tied to that specific milestone." *(Cut to 20 seconds of screen recording — the full loop, speeded up 1.5x, captioned.)* ### 1:10 — 1:40 — why Solana -> "We don't need a global ledger for every drawing — drawings stay in Supabase Storage. We need **one unforgeable receipt per approved payment**. Solana gives us that for a tenth of a cent. Every payout is a USDC transfer with a memo carrying the project code, milestone number, submission id, and approver id. A judge, an insurer, a lender can open Solana Explorer and reconstruct which real-world approval triggered that transfer." +> "We don't need a global ledger for every drawing — drawings stay in Supabase Storage. We need **one unforgeable receipt for approval and one for payout**. Solana gives us that for a tenth of a cent. Approval is a structured Memo transaction; payout is a USDC transfer with memo context. A judge, insurer, or lender can open Solana Explorer and reconstruct which real-world approval unlocked the transfer." ### 1:40 — 2:10 — why us, why now -> "I [team background — real answer]. We've already walked three contractor offices through the prototype; two have committed to running a live project on the platform next month. Construction is the largest industry still using email and signed PDFs for payment control. There's a reason RWA capital is flowing into Solana — 141 percent growth in 2025 — and this is the layer nobody's built yet." +> "I [team background — real answer]. The next step is three Nairobi operator interviews and one live milestone pilot; the repo includes the exact script and evidence log so we do not fake traction. Construction is one of the largest industries still using email and signed PDFs for payment control. That's the trust layer Solana can make portable." ### 2:10 — 2:35 — traction + ask -> "Today: deployed on Supabase and Solana devnet, full approval-to-payout loop, real USDC moving, audit trail linking every tx signature to a specific milestone. Next: two live pilots in Nairobi, one in Accra. We're applying to the Colosseum accelerator to run those pilots and bring the first stablecoin-settled construction milestones to production." +> "Today: deployed on Supabase and Solana devnet, full approval-to-payout loop, approval proof before payout, devnet USDC moving, audit trail linking every tx signature to a specific milestone. Next: Nairobi interviews, one live milestone pilot, then a mainnet USDC/off-ramp partner. We're applying to the Colosseum accelerator to turn the working loop into a production wedge." ### 2:35 — 2:45 — close @@ -60,11 +60,11 @@ Target length: **2:30**. Screen-only. Founder narrating off-camera. Every action ### 0:40 — 1:10 -"Certifier requests revision. Contractor resubmits v2. Certifier approves. Three audit rows land atomically in one Postgres transaction: submission approved, milestone approved, payout ready. The handoff pill now points at the owner." +"Certifier requests revision. Contractor resubmits v2. Certifier approves. First, WEZA records a Solana approval Memo transaction with the milestone metadata. Only after that proof exists do the database rows move to submission approved, milestone approved, payout ready. The handoff pill now points at the owner." ### 1:10 — 1:50 -"Owner clicks Trigger payout · 120,000 USDC. Server-side, we lock the payout row in a `SELECT FOR UPDATE`, build a Solana transaction with two instructions — a Memo Program instruction carrying the JSON metadata, and a `TransferChecked` from the treasury's USDC ATA to the contractor's USDC ATA — and send it to devnet via Helius. We wait for confirmed commitment." +"Owner clicks Trigger payout · 120,000 USDC. Server-side, we first verify the approval signature exists. Then we lock the payout row in a `SELECT FOR UPDATE`, build a Solana transaction with two instructions — a Memo Program instruction carrying the JSON metadata, and a `TransferChecked` from the treasury's USDC ATA to the contractor's USDC ATA — and send it to devnet via Helius. We wait for confirmed commitment." ### 1:50 — 2:15 @@ -80,10 +80,10 @@ Target length: **2:30**. Screen-only. Founder narrating off-camera. Every action ## Talking points for Q&A -- **"Why not an Anchor program?"** — A custom program increases attack surface with no judge-visible upside. The Memo + TransferChecked pair is auditable, trivially indexable, and works with any Solana indexer today. We would add an Anchor program for escrow + on-chain arbitration in the next milestone, not for the hackathon submission. +- **"Why not only Anchor?"** — The shipped proof path is a real Solana Memo transaction judges can inspect today. The repo includes a minimal Anchor approval program as the next custom-program path, but the live demo prioritizes a reliable, indexable proof over a half-deployed program. - **"What about mainnet USDC?"** — Trivial to flip: one env var (`SOLANA_RPC_URL`, `SOLANA_CLUSTER=mainnet`) and swap the USDC mint constant. Devnet is the correct hackathon target. We deliberately avoid making mainnet claims we haven't earned. - **"Escrow?"** — Not in scope. The current design assumes the owner funds the treasury. A follow-on Anchor program can pull owner funds at approval time. We chose not to ship half-implemented escrow. -- **"Who signs the memo?"** — The treasury keypair signs the whole transaction. The memo is authenticated by being inside a treasury-signed tx. The `approved_by` field inside the memo is the certifier's Supabase user id — the audit log row proves the certifier made the approval; the on-chain tx proves the owner acted on it. +- **"Who signs the memo?"** — The treasury keypair signs the on-chain approval and payout transactions. The `certifier` / `approved_by` fields tie the transaction to the certifier's Supabase identity; the audit log proves the certifier action, and Solana proves WEZA cannot quietly rewrite the approval/payout timeline later. - **"KYC?"** — Deliberately out. WEZA Build is a workflow + payout rail, not a money services business. Compliance plugs in at the treasury funding layer, not inside the product. --- @@ -98,7 +98,7 @@ Target length: **2:30**. Screen-only. Founder narrating off-camera. Every action - 64% of subcontractors regularly face slow pay - 75% front material costs out of own cash - 56% have turned down work over cash-flow risk -- RWAs on Solana grew 141% in 2025; > $24B TVL by mid-2025 +- Kenya national government pending bills reached Sh524B by Dec 2024, with contractor development-project debt around Sh243.19B (Eastleigh Voice, 2025) ## What *not* to say diff --git a/docs/PITCH_DECK.md b/docs/PITCH_DECK.md index dbc6045..5c0287c 100644 --- a/docs/PITCH_DECK.md +++ b/docs/PITCH_DECK.md @@ -8,11 +8,11 @@ One slide = one idea. No slide has more than 12 words on screen. Speaker notes a ## Slide 1 — Cover -**On-screen:** "WEZA Build · Approval-to-payout for construction." -Below: "Solana · Colosseum Breakout 2026" +**On-screen:** "WEZA Build · Approval-to-payout for African construction." +Below: "Solana · Colosseum Frontier 2026" Corner: your name + email. -**Speaker notes (5s):** "WEZA Build. Approval-to-payout for construction." +**Speaker notes (5s):** "WEZA Build. Approval-to-payout for African construction." --- @@ -20,17 +20,17 @@ Corner: your name + email. **On-screen (huge mono type, four stats, nothing else):** ``` -$299B 14% -82% 5% +30-90d KES +40% 24/7 ``` Under each, one line of 8pt type: -- $299B — slow-payment cost, US construction, 2025 -- 14% — hidden tax on total project cost -- 82% — of contractors wait 30+ days past due -- 5% — of subcontractors paid on time +- 30-90d — common contractor payment delay after approval +- KES — local contracts, dollar-linked materials, thin cash buffers +- 40% — Sub-Saharan African firms citing finance as a major constraint +- 24/7 — public audit evidence outside one operator -**Speaker notes (20s):** "Last year, slow payments cost US construction 299 billion dollars. A 14-percent hidden tax on every project. 82 percent of contractors wait over 30 days past due. Only 5 percent of subcontractors get paid on time." +**Speaker notes (20s):** "In Nairobi construction, subcontractors often wait 30 to 90 days after approval. Contracts are in shillings, materials are dollar-linked, and cash buffers are thin. The product wedge is not another payment button — it is trustworthy approval evidence." --- @@ -38,11 +38,11 @@ Under each, one line of 8pt type: **On-screen:** four boxes in a row: -`Drawing in email` → `Revision in WhatsApp` → `Certifier has no queue` → `Owner has no dashboard` +`Drawing in email` → `Revision in WhatsApp` → `Certifier has no queue` → `Contractor has no proof` -Below, one line: **"Approval is slow. So payment is slow."** +Below, one line: **"Approval evidence is weak. So payment is slow."** -**Speaker notes (15s):** "This isn't a banking problem. It's a workflow problem. Drawings sit in email. Revisions sit in WhatsApp. The certifier has no queue. The owner has no dashboard. The payment waits on paperwork that never lands." +**Speaker notes (15s):** "This is a counterparty-trust problem before it is a banking problem. Drawings sit in email. Revisions sit in WhatsApp. The certifier has no queue. The contractor has no portable proof when payment stalls." --- @@ -50,9 +50,9 @@ Below, one line: **"Approval is slow. So payment is slow."** **On-screen:** one sentence, centred: -> "An approval-to-payout platform for construction teams. Workflow off-chain. Payout proof on Solana." +> "An approval-to-payout platform for construction teams. Workflow private. Approval and payout proof public." -**Speaker notes (10s):** "WEZA Build is an approval-to-payout platform for construction teams. Contractors submit. Certifiers review and approve. Owners trigger payout. Workflow stays off-chain. Payout proof happens on Solana." +**Speaker notes (10s):** "WEZA Build is an approval-to-payout platform for construction teams. Contractors submit. Certifiers review and approve. Owners trigger payout. Private files stay off-chain. Approval and payout proof happen on Solana." --- @@ -60,19 +60,19 @@ Below, one line: **"Approval is slow. So payment is slow."** **On-screen:** horizontal flow of five chips: -`Submit` → `Review` → `Approve` → `Payout ready` → `Settled on Solana devnet` +`Submit` → `Review` → `Approval tx` → `Payout ready` → `USDC tx` Emerald chip for the last one. Replace with a clean SVG from the product's workflow timeline if you have time. -**Speaker notes (10s):** "Five states. Every one visible in the product. Every transition written to an audit trail." +**Speaker notes (10s):** "Five states. Every one visible in the product. The approval transaction is what unlocks payout." --- ## Slide 6 — Live screenshot -**On-screen:** single screenshot of the milestone page mid-payout — the one with the **Settled on Solana devnet** banner and a visible signature. One caption below: "Real devnet USDC. Signature links to Solana Explorer." +**On-screen:** single screenshot of the milestone page after approval — purple approval banner plus green settled banner. One caption below: "Approval tx unlocks payout tx." -**Speaker notes (10s):** "This is the product. The green banner is a real devnet transaction we just ran. Click that link and you see the memo that ties the USDC movement to the exact milestone the certifier approved." +**Speaker notes (10s):** "This is the product. The purple banner is the certifier approval recorded on Solana. The green banner is the payout transaction that only unlocks after that proof exists." --- @@ -80,10 +80,10 @@ Emerald chip for the last one. Replace with a clean SVG from the product's workf **On-screen:** two columns. -- **On-chain (narrow):** USDC TransferChecked + Memo. Sub-cent cost. Sub-second confirm. -- **Off-chain (everything else):** Drawings, revisions, comments, decisions, full audit log. +- **On-chain:** approval Memo proof + USDC TransferChecked payout. +- **Off-chain:** drawings, revisions, comments, private project data. -**Speaker notes (15s):** "We don't put drawings on a public ledger. They don't belong there. On-chain is one transfer per approved milestone, with a memo tagging project, milestone, submission, and approver. That's the primitive construction lenders, insurers, and regulators actually want." +**Speaker notes (15s):** "We don't put drawings on a public ledger. They don't belong there. On-chain is the approval proof and payout proof a contractor can show a dispute board, lender, donor, or owner without trusting us." --- @@ -91,12 +91,12 @@ Emerald chip for the last one. Replace with a clean SVG from the product's workf **On-screen:** two rows. -Row 1: "Market — 1.8T US construction · 141% YoY growth in Solana RWAs · $24B Solana RWA TVL mid-2025." -Row 2: "Traction — [N] contractor conversations · [N] design partners · Live on devnet." +Row 1: "Market — Kenya pending bills · Nairobi contractor delays · USDC off-ramp opportunity." +Row 2: "Traction — live MVP · three design-partner interviews targeted." -**Speaker notes (20s):** "1.8-trillion-dollar industry. Real-world assets on Solana grew 141 percent last year, passing 24 billion TVL. We've walked the prototype through [N] contractor offices. [Best partner quote, 1 line]. Two committed pilots in the 90-day plan." +**Speaker notes (20s):** "Kenya's pending-bill problem is large enough to be political, and contractor debt is a visible part of it. We have the live MVP and the interview script; the accelerator milestone is converting operator conversations into two design-partner pilots." -*Replace [N] and the quote with real content from `docs/TRACTION.md` before recording.* +*Replace this row with real partner names/quotes the moment interviews are complete.* --- @@ -120,12 +120,12 @@ Below: **"One loop. Done."** **On-screen:** - Colosseum accelerator, $250K pre-seed. -- Two live pilots in 90 days (Nairobi + Accra). -- First stablecoin-settled construction milestones on mainnet in Q2. +- Two design-partner pilots in Nairobi. +- Mainnet USDC path via Circle or local off-ramp partner. Bottom: founder name + email + `weza.build` (or your deploy URL). -**Speaker notes (15s):** "We're applying to the Colosseum accelerator. The ask is 250,000 dollars in pre-seed to run two pilots in Nairobi and Accra, then move the first production milestones to mainnet in Q2. Thank you." +**Speaker notes (15s):** "We're applying to the Colosseum accelerator. The ask is 250,000 dollars in pre-seed to run two Nairobi pilots, validate the unit economics, and move from devnet USDC to mainnet USDC through Circle or a local off-ramp partner." --- diff --git a/docs/RELEASE_READINESS.md b/docs/RELEASE_READINESS.md index 5c5c450..4d289b9 100644 --- a/docs/RELEASE_READINESS.md +++ b/docs/RELEASE_READINESS.md @@ -9,6 +9,7 @@ - **Real file upload** to Supabase Storage (private `submissions` bucket), with short-lived signed URLs for downloads. - Submission versioning, per-version decisions and comments. - Milestone + payout state machines enforced in DB `CHECK`, repo, and API. +- **Solana-gated approval**: certifier approval stores a Solana approval transaction signature + derived correlation address before payout can become ready. - **Real Solana devnet payout** via `@solana/web3.js` + pre-funded treasury keypair. Signature stored and surfaced in audit trail + Explorer link. - Idempotent approve and payout endpoints. - `GET /api/health/solana` returning treasury balance + cluster. @@ -18,16 +19,19 @@ ## Explicit non-goals -- No bank rails, real USDC settlement, or custody. Amounts are tracked off-chain; the on-chain moment is a symbolic devnet proof transaction. +- No bank rails, real USDC custody, or escrow. Amounts are tracked off-chain; devnet USDC proves the approval-to-payout path for hackathon review. - No real KYC / AML. - No AI, no web4 claims, no mobile app, no marketplace. -- No on-chain storage of drawings, comments, revisions, or workflow state. -- No Anchor program. The payout is a `SystemProgram.transfer` on devnet — sufficient for audit proof, minimal risk surface during a hackathon. +- No on-chain storage of drawings, comments, or revisions. +- Minimal Anchor program source only: `programs/weza_approval` documents the future custom-program PDA path. The shipped devnet proof path uses a structured Memo transaction so judges can verify approval today. ## Production guardrails - `WEZA_MOCK_SOLANA` is refused when `NODE_ENV=production` (see `src/lib/env.ts`). If the env var is set, `/api/health/solana` still reports live. - Treasury keypair must be explicitly configured via `SOLANA_TREASURY_KEYPAIR`. There is no ephemeral fallback — the app refuses to start a payout if the treasury is not configured. +- Payout refuses to execute unless `milestones.approval_tx_signature` and the approval correlation address exist. +- Devnet USDC is a config layer: moving to mainnet USDC changes cluster/RPC/mint/treasury and adds Circle or local off-ramp compliance; it does not require rewriting the workflow. +- Public signup is disabled by default in production. Set `WEZA_PUBLIC_SIGNUP=1` only for a live signup demo; otherwise judges should use seeded accounts. - Airdrop fallback removed. In production the deployer funds the treasury once; the app never calls `requestAirdrop`. - Signed download URLs expire after 5 minutes. diff --git a/docs/SMOKE_TEST.md b/docs/SMOKE_TEST.md index 7dfce69..b1679bb 100644 --- a/docs/SMOKE_TEST.md +++ b/docs/SMOKE_TEST.md @@ -12,6 +12,14 @@ Throughout this script, `BASE` is that URL. ## 1. Health check — Solana treasury +```bash +curl -s "$BASE/api/health/config" | jq +``` + +**Pass:** `success: true`, `data.supabase.ready == true`, `data.database_url == true`, and `data.service_role == true`. + +**Fail:** any missing field means login/seed/server writes can fail even if `/about` renders. + ```bash curl -s "$BASE/api/health/solana" | jq ``` @@ -130,7 +138,7 @@ In your Vercel deployment **Settings → Environment Variables**, set `WEZA_MOCK **Pass:** returns `mode: "live"` (the env loader refuses mock in production). If it returns `mode: "mock"` in production, escalate — a newer branch has broken the guard. -Remove the var again after the test. +Remove the var again after the test. Production must stay `mode: "live"` the entire time; mock mode is local-only. --- diff --git a/docs/STORYBOARD.md b/docs/STORYBOARD.md index 96a0f99..2d8fa17 100644 --- a/docs/STORYBOARD.md +++ b/docs/STORYBOARD.md @@ -19,13 +19,13 @@ Record on a clean neutral background. No slides, no screen share. Founder-to-cam |---|---|---|---|---| | 1 | 0:00–0:05 | Wide on you, eye contact. | "I'm [NAME]. I'm building WEZA Build." | 5 words of identification. Nothing else. | | 2 | 0:05–0:15 | Same. | "Construction payments are the dirtiest corner of a 1.8 trillion-dollar industry." | Slow down on the number. | -| 3 | 0:15–0:35 | Title card overlay: **"$299B · 14% · 82% · 5%"** | "Last year, slow payments cost US construction 299 billion dollars. A 14-percent hidden tax on every project. 82 percent of contractors wait over 30 days past due. Only 5 percent of subcontractors get paid on time." | Overlay stays up through the whole stat. | +| 3 | 0:15–0:35 | Title card overlay: **"30-90d · KES · 40% · 24/7"** | "In Nairobi construction, contractors can wait 30 to 90 days after approval. Contracts are local, materials are dollar-linked, and finance is still a major constraint for firms across Sub-Saharan Africa." | Overlay stays up through the whole stat. | | 4 | 0:35–0:55 | Back to founder. | "This isn't a banking problem. It's a workflow problem. When a sub finishes work, the money doesn't move because the approval doesn't move. Drawings sit in email. Revisions sit in WhatsApp. The certifier has no queue. The payment waits on paperwork that never lands." | Hit "approval" hard. | | 5 | 0:55–1:10 | Founder. | "WEZA Build is an approval-to-payout platform for construction teams. Contractors submit. Certifiers review and approve. Owners click once, and the payment moves on Solana — permanent, auditable, tied to that specific milestone." | One sentence product definition. | | 6 | 1:10–1:40 | Cut to **screen recording of the full loop at 1.5x speed**, captioned. | Voice-over: "This is the entire loop. Submit. Review. Approve. Trigger payout. The audit trail updates live across every role." | Cue this recording up before you start. It's pre-rendered. | | 7 | 1:40–2:00 | Founder. | "Why Solana. Drawings stay off chain. But every payout is one USDC transfer with a memo tag — project, milestone, submission, approver. Sub-cent cost. Real-time confirmation. Anyone can verify which real-world approval caused this transfer on Explorer." | "Memo tag" is the phrase that signals you know what you're doing. | -| 8 | 2:00–2:25 | Founder. | "I've shown the prototype to [N] contractor offices. [Specific quote from one]. That's why we're here. Construction is the biggest industry still running approvals on email. That's what we're replacing." | REPLACE the placeholder with a real partner quote from `docs/TRACTION.md` before recording. | -| 9 | 2:25–2:40 | Founder. | "Today: deployed, end-to-end loop, real devnet USDC moving with tagged signatures on every payout. Next 90 days: two live pilots, Nairobi and Accra. We're applying for the Colosseum accelerator to run those pilots." | If you don't have pilots yet, say "the first two pilots." Don't overclaim. | +| 8 | 2:00–2:25 | Founder. | "Kenya's public pending bills include over KSh 240 billion owed to development-project contractors. Contractors do not just need faster payment; they need proof a certifier and owner cannot quietly rewrite." | Cite the pending-bills source in your deck notes. | +| 9 | 2:25–2:40 | Founder. | "Today: deployed, end-to-end loop, real devnet approval and payout signatures. Next 90 days: recruit two Nairobi/Accra design partners and run the first live milestone." | If pilots are not committed, say "recruit." Don't overclaim. | | 10 | 2:40–2:50 | Founder, slight pause. | "WEZA Build. Approval-to-payout for construction. Link in the description." | End on the URL. | **Common mistakes to avoid** @@ -50,7 +50,7 @@ Before recording: run `npm run seed` against the live Supabase project, log into | 4 | 0:40–0:55 | Pan to certifier window — **do not refresh**. | "No refresh. Certifier's view updates live. The audit rail flashes green. That's the audit endpoint polling since-last-event." | Wait ~3 seconds for the poll to fire. | | 5 | 0:55–1:15 | Certifier: click Request revision with a note. | "Certifier requests a revision. Ball flips to contractor." | Note: "tighten rebar spec." Click Request revision. | | 6 | 1:15–1:30 | Contractor window. | "Contractor resubmits v2." | Drop another file, click Resubmit. | -| 7 | 1:30–1:50 | Certifier window. Click Approve. | "Certifier approves. One transaction: submission approved, milestone approved, payout ready, three audit rows." | Click Approve milestone. | +| 7 | 1:30–1:50 | Certifier window. Click Approve. | "Certifier approves. First, WEZA records a Solana Memo approval proof. Only then does the database mark payout ready." | Click Approve milestone. | | 8 | 1:50–2:10 | Owner window, payout panel. | "Owner's turn. One click." | Click **Trigger payout · 120,000 USDC**. | | 9 | 2:10–2:25 | Zoom on the Settled banner, then click **View on Explorer**. | "Devnet tx. Two instructions. Memo program carrying the milestone context. SPL TransferChecked of 120,000 USDC. Anyone can verify which approval caused this transfer." | Click Explorer link. Expand Memo + Token Balances. Let it breathe for 5 seconds. | | 10 | 2:25–2:30 | Back to milestone page. | "Approval-to-payout. Off-chain workflow, on-chain proof. That's it." | End. | @@ -65,7 +65,7 @@ Before recording: run `npm run seed` against the live Supabase project, log into ## Thumbnail -Static frame, dark background, four numbers white: **$299B · 14% · 82% · 5%**. Caption: "Approval-to-payout for construction. Solana." This works as the YouTube/Vimeo/Devpost thumbnail and as slide 1 of the deck. +Static frame, dark background, four numbers white: **30-90d · KES · 40% · 24/7**. Caption: "On-chain approval-to-payout for African construction." This works as the YouTube/Vimeo/Devpost thumbnail and as slide 1 of the deck. ## Where to host diff --git a/docs/SUBMISSION_CHECKLIST.md b/docs/SUBMISSION_CHECKLIST.md index 66cea86..fd91666 100644 --- a/docs/SUBMISSION_CHECKLIST.md +++ b/docs/SUBMISSION_CHECKLIST.md @@ -10,9 +10,8 @@ Every item here maps to a concrete action you take outside the codebase. Check t ## 1 — Merge the code -- [ ] Merge PR #2 (`cursor/supabase-migration-864b`) into `main`. -- [ ] Merge PR #3 (`cursor/winability-864b`) into `main`. -- [ ] Close PR #1 (`cursor/weza-build-takeover-864b`) — it's subsumed. +- [ ] Merge the current Cloud Agent PR into `main`. +- [ ] Confirm `main` contains Supabase migrations, seed data, Solana approval proof gating, and the Vercel build fix. - [ ] Update the repo's default branch to `main` if it isn't already. ## 2 — Stand up Supabase (~20 minutes) @@ -60,6 +59,7 @@ Every item here maps to a concrete action you take outside the codebase. Check t - [ ] `WEZA_STORAGE_BUCKET` = `submissions` - [ ] Do **not** set `WEZA_MOCK_SOLANA`. - [ ] Deploy. +- [ ] Leave `WEZA_PUBLIC_SIGNUP=0` unless you are intentionally testing signup. Judges should use seeded demo users. ## 6 — Seed demo data (~2 minutes) @@ -72,11 +72,11 @@ From your laptop, with the live envs loaded: Walk through `docs/SMOKE_TEST.md` end to end. -- [ ] §1 `/api/health/solana` returns `mode: live`, usdcUi ≥ 1000. +- [ ] §1 `/api/health/config` returns `success: true`; `/api/health/solana` returns `mode: live`, usdcUi ≥ 1000. - [ ] §2 signup creates auth user + profile. - [ ] §3 login succeeds, wrong password is rejected. - [ ] §4 full loop runs, produces a real 88-char base58 signature. -- [ ] §4 Explorer view shows **Memo instruction** with JSON + **TransferChecked** for USDC mint `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`. +- [ ] §4 Explorer view shows an approval **Memo instruction** first, then payout **Memo + TransferChecked** for USDC mint `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`. - [ ] §4 idempotency — double-click returns same signature. - [ ] §5 RLS sanity — running as `authenticated` for a non-member returns no project rows. - [ ] §6 file download — member gets 302 + signed URL; non-member gets 403. @@ -109,8 +109,8 @@ Walk through `docs/SMOKE_TEST.md` end to end. - [ ] Use `docs/HACKATHON_CANVAS.md` to fill the canvas. - [ ] Devpost fields map 1:1 to the canvas sections. -- [ ] Tagline: "Approval-to-payout for construction. On Solana." -- [ ] Short description: "Drawings, revisions, and milestone sign-off move directly into payout on Solana devnet, with a permanent audit trail tying every transaction to a specific approval." +- [ ] Tagline: "On-chain approval-to-payout for African construction." +- [ ] Short description: "WEZA records certifier approval on Solana first, then unlocks devnet USDC payout with a permanent audit trail for Nairobi construction teams." - [ ] Long description: combine canvas sections 1–6. ## 12 — Final sanity (30 minutes before deadline) @@ -137,7 +137,7 @@ Walk through `docs/SMOKE_TEST.md` end to end. - Two videos that stay under 3:00. - Three real user quotes. - A live deploy that never goes down during judging week. -- A single memo-tagged USDC signature they can click through to on Solana Explorer. -- An honest narrative: one product, one loop, one payout primitive. +- An approval signature and payout signature they can click through to on Solana Explorer. +- An honest narrative: one product, one loop, public approval proof before payout. -If you do all of the above, you are in the top few percent of submissions by completeness. That gives you a credible shot at a top-20 standout prize. Grand Champion is a different conversation — for that, the pilot needs to actually happen, and you need to be able to say "this tool moved real USDC between two parties on a real construction project," which is work that extends past the submission itself. +If you do all of the above, the submission is unusually complete: live product, real Solana evidence, clear market wedge, and honest traction plan. Winning still depends on judge taste and pilot evidence; do not claim inevitability, show proof. diff --git a/docs/TRACTION.md b/docs/TRACTION.md index 93fcac3..ec6f4ea 100644 --- a/docs/TRACTION.md +++ b/docs/TRACTION.md @@ -1,6 +1,6 @@ # Traction and market evidence -Colosseum weights *early user feedback* and *evidence of product-market fit* even when it's informal. This document is where that evidence lives. Populate the empty bullets with real, attributable quotes before submitting. +Colosseum weights *early user feedback* and *evidence of product-market fit* even when it's informal. This document is the operating checklist for evidence. Do not submit invented quotes; if a partner has not given permission to be named, use an anonymised row with date, role, region, and the exact problem described. ## Market numbers (cite these in the deck and pitch) @@ -18,11 +18,11 @@ Colosseum weights *early user feedback* and *evidence of product-market fit* eve | Top cited root cause for slow payment: *lack of organized process* | GlobeNewswire, 2025 | | RWAs on Solana grew **141 % in 2025**, reaching > $24 B TVL mid-year | Solana RWA market reports, 2025 | -## Design-partner outreach (fill in before submission) +## Design-partner outreach The judges don't need signed contracts. They need evidence that you've talked to real operators and they said something specific enough that you couldn't have made it up. -### Template +### Required evidence format - **Firm / person** - **Role** (owner / GC / sub / QS / architect) @@ -33,24 +33,15 @@ The judges don't need signed contracts. They need evidence that you've talked to - **Their reaction to the prototype** (1 sentence) - **Commitment** (none / intro / willing to pilot / signed LOI / live project) -### Partner 1 +### Current status -- **Firm / person:** __ -- **Role:** __ -- **Region:** __ -- **Contacted on:** __ -- **Channel:** __ -- **Problem quote:** __ -- **Prototype reaction:** __ -- **Commitment:** __ +No attributable design-partner quote is committed in this repo yet. Before submission, collect at least three operator conversations and fill this section with evidence. If that cannot be done, say so plainly in the pitch and compete on product completeness plus the live Solana proof rather than pretending traction exists. -### Partner 2 - -- __ - -### Partner 3 - -- __ +| Contact | Role | Region | Date | Channel | Problem quote | Prototype reaction | Commitment | +|---------|------|--------|------|---------|---------------|--------------------|------------| +| To collect | Contractor / QS / owner | Nairobi / Kenya | To collect | WhatsApp / call / in person | To collect | To collect | To collect | +| To collect | Contractor / QS / owner | Nairobi / Kenya | To collect | WhatsApp / call / in person | To collect | To collect | To collect | +| To collect | Contractor / QS / owner | Nairobi / Kenya | To collect | WhatsApp / call / in person | To collect | To collect | To collect | ## User-problem interview questions (use these) diff --git a/package-lock.json b/package-lock.json index 5433a88..564942c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,13 @@ "name": "weza-build", "version": "0.1.0", "dependencies": { - "@solana/spl-token": "^0.4.8", - "@solana/web3.js": "1.95.3", + "@solana/spl-token": "^0.4.14", + "@solana/web3.js": "^1.98.4", + "@sqds/multisig": "^2.1.4", "@supabase/ssr": "^0.10.2", "@supabase/supabase-js": "^2.104.1", "clsx": "2.1.1", - "next": "14.2.15", + "next": "^14.2.35", "pg": "^8.20.0", "react": "18.3.1", "react-dom": "18.3.1", @@ -28,12 +29,12 @@ "@types/react-dom": "18.3.0", "autoprefixer": "10.4.20", "eslint": "8.57.0", - "eslint-config-next": "14.2.15", - "postcss": "8.4.47", + "eslint-config-next": "^14.2.35", + "postcss": "^8.5.10", "tailwindcss": "3.4.13", - "tsx": "4.19.1", + "tsx": "^4.21.0", "typescript": "5.5.4", - "vitest": "2.1.1" + "vitest": "^2.1.9" } }, "node_modules/@alloc/quick-lru": { @@ -100,9 +101,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -117,9 +118,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -134,9 +135,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -151,9 +152,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -168,9 +169,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -185,9 +186,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -202,9 +203,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -219,9 +220,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -236,9 +237,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -253,9 +254,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -270,9 +271,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -287,9 +288,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -304,9 +305,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -321,9 +322,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -338,9 +339,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -355,9 +356,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -372,9 +373,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -388,10 +389,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -406,9 +424,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -423,9 +441,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -439,10 +457,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -457,9 +492,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -474,9 +509,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -491,9 +526,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -694,6 +729,50 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@metaplex-foundation/beet": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/beet/-/beet-0.7.1.tgz", + "integrity": "sha512-hNCEnS2WyCiYyko82rwuISsBY3KYpe828ubsd2ckeqZr7tl0WVLivGkoyA/qdiaaHEBGdGl71OpfWa2rqL3DiA==", + "license": "Apache-2.0", + "dependencies": { + "ansicolors": "^0.3.2", + "bn.js": "^5.2.0", + "debug": "^4.3.3" + } + }, + "node_modules/@metaplex-foundation/beet-solana": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/beet-solana/-/beet-solana-0.4.0.tgz", + "integrity": "sha512-B1L94N3ZGMo53b0uOSoznbuM5GBNJ8LwSeznxBxJ+OThvfHQ4B5oMUqb+0zdLRfkKGS7Q6tpHK9P+QK0j3w2cQ==", + "license": "Apache-2.0", + "dependencies": { + "@metaplex-foundation/beet": ">=0.1.0", + "@solana/web3.js": "^1.56.2", + "bs58": "^5.0.0", + "debug": "^4.3.4" + } + }, + "node_modules/@metaplex-foundation/beet-solana/node_modules/base-x": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", + "license": "MIT" + }, + "node_modules/@metaplex-foundation/beet-solana/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "license": "MIT", + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/@metaplex-foundation/cusper": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/cusper/-/cusper-0.0.2.tgz", + "integrity": "sha512-S9RulC2fFCFOQraz61bij+5YCHhSO9llJegK8c8Y6731fSi6snUSQJdCUqYS8AIgR0TKbQvdvgSyIIdbDFZbBA==", + "license": "Apache-2.0" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -708,15 +787,15 @@ } }, "node_modules/@next/env": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.15.tgz", - "integrity": "sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.15.tgz", - "integrity": "sha512-pKU0iqKRBlFB/ocOI1Ip2CkKePZpYpnw5bEItEkuZ/Nr9FQP1+p7VDWr4VfOdff4i9bFmrOaeaU1bFEyAcxiMQ==", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.35.tgz", + "integrity": "sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ==", "dev": true, "license": "MIT", "dependencies": { @@ -724,9 +803,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz", - "integrity": "sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", "cpu": [ "arm64" ], @@ -740,9 +819,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz", - "integrity": "sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", "cpu": [ "x64" ], @@ -756,9 +835,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz", - "integrity": "sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", "cpu": [ "arm64" ], @@ -772,9 +851,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz", - "integrity": "sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", "cpu": [ "arm64" ], @@ -788,9 +867,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz", - "integrity": "sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", "cpu": [ "x64" ], @@ -804,9 +883,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz", - "integrity": "sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", "cpu": [ "x64" ], @@ -820,9 +899,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz", - "integrity": "sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", "cpu": [ "arm64" ], @@ -836,9 +915,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz", - "integrity": "sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", "cpu": [ "ia32" ], @@ -852,9 +931,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz", - "integrity": "sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", "cpu": [ "x64" ], @@ -1345,69 +1424,69 @@ } }, "node_modules/@solana/codecs": { - "version": "2.0.0-preview.4", - "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-preview.4.tgz", - "integrity": "sha512-gLMupqI4i+G4uPi2SGF/Tc1aXcviZF2ybC81x7Q/fARamNSgNOCUUoSCg9nWu1Gid6+UhA7LH80sWI8XjKaRog==", + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-rc.1.tgz", + "integrity": "sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==", "license": "MIT", "dependencies": { - "@solana/codecs-core": "2.0.0-preview.4", - "@solana/codecs-data-structures": "2.0.0-preview.4", - "@solana/codecs-numbers": "2.0.0-preview.4", - "@solana/codecs-strings": "2.0.0-preview.4", - "@solana/options": "2.0.0-preview.4" + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-data-structures": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/codecs-strings": "2.0.0-rc.1", + "@solana/options": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "node_modules/@solana/codecs-core": { - "version": "2.0.0-preview.4", - "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-preview.4.tgz", - "integrity": "sha512-A0VVuDDA5kNKZUinOqHxJQK32aKTucaVbvn31YenGzHX1gPqq+SOnFwgaEY6pq4XEopSmaK16w938ZQS8IvCnw==", + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz", + "integrity": "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==", "license": "MIT", "dependencies": { - "@solana/errors": "2.0.0-preview.4" + "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "node_modules/@solana/codecs-data-structures": { - "version": "2.0.0-preview.4", - "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-preview.4.tgz", - "integrity": "sha512-nt2k2eTeyzlI/ccutPcG36M/J8NAYfxBPI9h/nQjgJ+M+IgOKi31JV8StDDlG/1XvY0zyqugV3I0r3KAbZRJpA==", + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz", + "integrity": "sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==", "license": "MIT", "dependencies": { - "@solana/codecs-core": "2.0.0-preview.4", - "@solana/codecs-numbers": "2.0.0-preview.4", - "@solana/errors": "2.0.0-preview.4" + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "node_modules/@solana/codecs-numbers": { - "version": "2.0.0-preview.4", - "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-preview.4.tgz", - "integrity": "sha512-Q061rLtMadsO7uxpguT+Z7G4UHnjQ6moVIxAQxR58nLxDPCC7MB1Pk106/Z7NDhDLHTcd18uO6DZ7ajHZEn2XQ==", + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz", + "integrity": "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==", "license": "MIT", "dependencies": { - "@solana/codecs-core": "2.0.0-preview.4", - "@solana/errors": "2.0.0-preview.4" + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "node_modules/@solana/codecs-strings": { - "version": "2.0.0-preview.4", - "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-preview.4.tgz", - "integrity": "sha512-YDbsQePRWm+xnrfS64losSGRg8Wb76cjK1K6qfR8LPmdwIC3787x9uW5/E4icl/k+9nwgbIRXZ65lpF+ucZUnw==", + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz", + "integrity": "sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==", "license": "MIT", "dependencies": { - "@solana/codecs-core": "2.0.0-preview.4", - "@solana/codecs-numbers": "2.0.0-preview.4", - "@solana/errors": "2.0.0-preview.4" + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "fastestsmallesttextencoderdecoder": "^1.0.22", @@ -1415,9 +1494,9 @@ } }, "node_modules/@solana/errors": { - "version": "2.0.0-preview.4", - "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-preview.4.tgz", - "integrity": "sha512-kadtlbRv2LCWr8A9V22On15Us7Nn8BvqNaOB4hXsTB3O0fU40D1ru2l+cReqLcRPij4znqlRzW9Xi0m6J5DIhA==", + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz", + "integrity": "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==", "license": "MIT", "dependencies": { "chalk": "^5.3.0", @@ -1452,54 +1531,53 @@ } }, "node_modules/@solana/options": { - "version": "2.0.0-preview.4", - "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-preview.4.tgz", - "integrity": "sha512-tv2O/Frxql/wSe3jbzi5nVicIWIus/BftH+5ZR+r9r3FO0/htEllZS5Q9XdbmSboHu+St87584JXeDx3xm4jaA==", + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-rc.1.tgz", + "integrity": "sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==", "license": "MIT", "dependencies": { - "@solana/codecs-core": "2.0.0-preview.4", - "@solana/codecs-data-structures": "2.0.0-preview.4", - "@solana/codecs-numbers": "2.0.0-preview.4", - "@solana/codecs-strings": "2.0.0-preview.4", - "@solana/errors": "2.0.0-preview.4" + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-data-structures": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/codecs-strings": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "node_modules/@solana/spl-token": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.4.8.tgz", - "integrity": "sha512-RO0JD9vPRi4LsAbMUdNbDJ5/cv2z11MGhtAvFeRzT4+hAGE/FUzRi0tkkWtuCfSIU3twC6CtmAihRp/+XXjWsA==", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.4.14.tgz", + "integrity": "sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA==", "license": "Apache-2.0", "dependencies": { "@solana/buffer-layout": "^4.0.0", "@solana/buffer-layout-utils": "^0.2.0", - "@solana/spl-token-group": "^0.0.5", - "@solana/spl-token-metadata": "^0.1.3", + "@solana/spl-token-group": "^0.0.7", + "@solana/spl-token-metadata": "^0.1.6", "buffer": "^6.0.3" }, "engines": { "node": ">=16" }, "peerDependencies": { - "@solana/web3.js": "^1.94.0" + "@solana/web3.js": "^1.95.5" } }, "node_modules/@solana/spl-token-group": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@solana/spl-token-group/-/spl-token-group-0.0.5.tgz", - "integrity": "sha512-CLJnWEcdoUBpQJfx9WEbX3h6nTdNiUzswfFdkABUik7HVwSNA98u5AYvBVK2H93d9PGMOHAak2lHW9xr+zAJGQ==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz", + "integrity": "sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==", "license": "Apache-2.0", "dependencies": { - "@solana/codecs": "2.0.0-preview.4", - "@solana/spl-type-length-value": "0.1.0" + "@solana/codecs": "2.0.0-rc.1" }, "engines": { "node": ">=16" }, "peerDependencies": { - "@solana/web3.js": "^1.94.0" + "@solana/web3.js": "^1.95.3" } }, "node_modules/@solana/spl-token-metadata": { @@ -1517,109 +1595,80 @@ "@solana/web3.js": "^1.95.3" } }, - "node_modules/@solana/spl-token-metadata/node_modules/@solana/codecs": { - "version": "2.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-rc.1.tgz", - "integrity": "sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==", + "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": { - "@solana/codecs-core": "2.0.0-rc.1", - "@solana/codecs-data-structures": "2.0.0-rc.1", - "@solana/codecs-numbers": "2.0.0-rc.1", - "@solana/codecs-strings": "2.0.0-rc.1", - "@solana/options": "2.0.0-rc.1" - }, - "peerDependencies": { - "typescript": ">=5" + "@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/spl-token-metadata/node_modules/@solana/codecs-core": { - "version": "2.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz", - "integrity": "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==", + "node_modules/@solana/web3.js/node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", "license": "MIT", "dependencies": { - "@solana/errors": "2.0.0-rc.1" + "@solana/errors": "2.3.0" }, - "peerDependencies": { - "typescript": ">=5" - } - }, - "node_modules/@solana/spl-token-metadata/node_modules/@solana/codecs-data-structures": { - "version": "2.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz", - "integrity": "sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "2.0.0-rc.1", - "@solana/codecs-numbers": "2.0.0-rc.1", - "@solana/errors": "2.0.0-rc.1" + "engines": { + "node": ">=20.18.0" }, "peerDependencies": { - "typescript": ">=5" + "typescript": ">=5.3.3" } }, - "node_modules/@solana/spl-token-metadata/node_modules/@solana/codecs-numbers": { - "version": "2.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz", - "integrity": "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==", + "node_modules/@solana/web3.js/node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", "license": "MIT", "dependencies": { - "@solana/codecs-core": "2.0.0-rc.1", - "@solana/errors": "2.0.0-rc.1" + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" }, - "peerDependencies": { - "typescript": ">=5" - } - }, - "node_modules/@solana/spl-token-metadata/node_modules/@solana/codecs-strings": { - "version": "2.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz", - "integrity": "sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "2.0.0-rc.1", - "@solana/codecs-numbers": "2.0.0-rc.1", - "@solana/errors": "2.0.0-rc.1" + "engines": { + "node": ">=20.18.0" }, "peerDependencies": { - "fastestsmallesttextencoderdecoder": "^1.0.22", - "typescript": ">=5" + "typescript": ">=5.3.3" } }, - "node_modules/@solana/spl-token-metadata/node_modules/@solana/errors": { - "version": "2.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz", - "integrity": "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==", + "node_modules/@solana/web3.js/node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "commander": "^12.1.0" + "chalk": "^5.4.1", + "commander": "^14.0.0" }, "bin": { "errors": "bin/cli.mjs" }, - "peerDependencies": { - "typescript": ">=5" - } - }, - "node_modules/@solana/spl-token-metadata/node_modules/@solana/options": { - "version": "2.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-rc.1.tgz", - "integrity": "sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "2.0.0-rc.1", - "@solana/codecs-data-structures": "2.0.0-rc.1", - "@solana/codecs-numbers": "2.0.0-rc.1", - "@solana/codecs-strings": "2.0.0-rc.1", - "@solana/errors": "2.0.0-rc.1" + "engines": { + "node": ">=20.18.0" }, "peerDependencies": { - "typescript": ">=5" + "typescript": ">=5.3.3" } }, - "node_modules/@solana/spl-token-metadata/node_modules/chalk": { + "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==", @@ -1631,48 +1680,52 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@solana/spl-token-metadata/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "node_modules/@solana/web3.js/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": ">=18" + "node": ">=20" } }, - "node_modules/@solana/spl-type-length-value": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@solana/spl-type-length-value/-/spl-type-length-value-0.1.0.tgz", - "integrity": "sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==", - "license": "Apache-2.0", + "node_modules/@sqds/multisig": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@sqds/multisig/-/multisig-2.1.4.tgz", + "integrity": "sha512-5w+NmwHOzl96nI50R/fjSD6uFydRLNUquhoEmmWbGepS4D9DnQyF2TKcUBfTyxV3sgJt00ypBt7SXB3y8WOzUQ==", + "license": "MIT", "dependencies": { - "buffer": "^6.0.3" + "@metaplex-foundation/beet": "0.7.1", + "@metaplex-foundation/beet-solana": "0.4.0", + "@metaplex-foundation/cusper": "^0.0.2", + "@solana/spl-token": "^0.3.6", + "@solana/web3.js": "^1.70.3", + "@types/bn.js": "^5.1.1", + "assert": "^2.0.0", + "bn.js": "^5.2.1", + "buffer": "6.0.3", + "invariant": "2.2.4" }, "engines": { - "node": ">=16" + "node": ">=14" } }, - "node_modules/@solana/web3.js": { - "version": "1.95.3", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.95.3.tgz", - "integrity": "sha512-O6rPUN0w2fkNqx/Z3QJMB9L225Ex10PRDH8bTaIUPZXMPV0QP8ZpPvjQnXK+upUczlRgzHzd6SjKIha1p+I6og==", - "license": "MIT", + "node_modules/@sqds/multisig/node_modules/@solana/spl-token": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.11.tgz", + "integrity": "sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==", + "license": "Apache-2.0", "dependencies": { - "@babel/runtime": "^7.25.0", - "@noble/curves": "^1.4.2", - "@noble/hashes": "^1.4.0", - "@solana/buffer-layout": "^4.0.1", - "agentkeepalive": "^4.5.0", - "bigint-buffer": "^1.1.5", - "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" + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-metadata": "^0.1.2", + "buffer": "^6.0.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.88.0" } }, "node_modules/@supabase/auth-js": { @@ -1830,6 +1883,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -2476,15 +2538,15 @@ ] }, "node_modules/@vitest/expect": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", - "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.1", - "@vitest/utils": "2.1.1", - "chai": "^5.1.1", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", "tinyrainbow": "^1.2.0" }, "funding": { @@ -2492,22 +2554,21 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", - "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "^2.1.0-beta.1", + "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", - "magic-string": "^0.30.11" + "magic-string": "^0.30.12" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/spy": "2.1.1", - "msw": "^2.3.5", + "msw": "^2.4.9", "vite": "^5.0.0" }, "peerDependenciesMeta": { @@ -2533,13 +2594,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", - "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.1", + "@vitest/utils": "2.1.9", "pathe": "^1.1.2" }, "funding": { @@ -2547,68 +2608,42 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", - "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.1", - "magic-string": "^0.30.11", + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", - "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/spy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", - "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.0" + "tinyspy": "^3.0.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", - "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.1", - "loupe": "^3.1.1", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", - "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", "dev": true, "license": "MIT", "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" }, "funding": { @@ -2693,6 +2728,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "license": "MIT" + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -2898,6 +2939,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2967,7 +3021,6 @@ "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": { "possible-typed-array-names": "^1.0.0" @@ -3239,7 +3292,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3258,7 +3310,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3272,7 +3323,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3568,7 +3618,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3603,7 +3652,6 @@ "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": { "es-define-property": "^1.0.0", @@ -3621,7 +3669,6 @@ "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": { "define-data-property": "^1.0.1", @@ -3678,7 +3725,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3783,7 +3829,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3793,7 +3838,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3827,11 +3871,17 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3903,9 +3953,9 @@ } }, "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3916,30 +3966,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { @@ -4023,13 +4075,13 @@ } }, "node_modules/eslint-config-next": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.15.tgz", - "integrity": "sha512-mKg+NC/8a4JKLZRIOBplxXNdStgxy7lzWuedUaCc8tev+Al9mwDUTujQH6W6qXDH9kycWiVo28tADWGvpBsZcQ==", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.35.tgz", + "integrity": "sha512-BpLsv01UisH193WyT/1lpHqq5iJ/Orfz9h/NOOlAmTUq4GY349PextQ62K4XpnaM9supeiEn3TaOTeQO07gURg==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "14.2.15", + "@next/eslint-plugin-next": "14.2.35", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -4411,6 +4463,16 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", @@ -4568,7 +4630,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -4637,7 +4698,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4678,7 +4738,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4688,7 +4747,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4713,7 +4771,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -4854,7 +4911,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4903,7 +4959,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -4932,7 +4987,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4945,7 +4999,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -4961,7 +5014,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5061,7 +5113,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -5079,6 +5130,31 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -5177,7 +5253,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5277,7 +5352,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.4", @@ -5319,6 +5393,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -5373,7 +5463,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5456,7 +5545,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -5815,7 +5903,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5938,13 +6025,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.15.tgz", - "integrity": "sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "license": "MIT", "dependencies": { - "@next/env": "14.2.15", + "@next/env": "14.2.35", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -5959,15 +6045,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.15", - "@next/swc-darwin-x64": "14.2.15", - "@next/swc-linux-arm64-gnu": "14.2.15", - "@next/swc-linux-arm64-musl": "14.2.15", - "@next/swc-linux-x64-gnu": "14.2.15", - "@next/swc-linux-x64-musl": "14.2.15", - "@next/swc-win32-arm64-msvc": "14.2.15", - "@next/swc-win32-ia32-msvc": "14.2.15", - "@next/swc-win32-x64-msvc": "14.2.15" + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -6137,11 +6223,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6151,7 +6252,6 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -6531,16 +6631,15 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "dev": true, "funding": [ { @@ -6558,8 +6657,8 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -7183,7 +7282,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7223,7 +7321,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -8092,13 +8189,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz", - "integrity": "sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.23.0", + "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -8329,6 +8426,19 @@ "punycode": "^2.1.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -8406,14 +8516,15 @@ } }, "node_modules/vite-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", - "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.6", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, @@ -8858,30 +8969,31 @@ } }, "node_modules/vitest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", - "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.1", - "@vitest/mocker": "2.1.1", - "@vitest/pretty-format": "^2.1.1", - "@vitest/runner": "2.1.1", - "@vitest/snapshot": "2.1.1", - "@vitest/spy": "2.1.1", - "@vitest/utils": "2.1.1", - "chai": "^5.1.1", - "debug": "^4.3.6", - "magic-string": "^0.30.11", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", "pathe": "^1.1.2", - "std-env": "^3.7.0", + "std-env": "^3.8.0", "tinybench": "^2.9.0", - "tinyexec": "^0.3.0", - "tinypool": "^1.0.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.1", + "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "bin": { @@ -8896,8 +9008,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.1", - "@vitest/ui": "2.1.1", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, @@ -9025,7 +9137,6 @@ "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", diff --git a/package.json b/package.json index 81956f4..e877374 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,13 @@ "db:migrate": "tsx scripts/migrate.ts" }, "dependencies": { - "@solana/spl-token": "^0.4.8", - "@solana/web3.js": "1.95.3", + "@solana/spl-token": "^0.4.14", + "@solana/web3.js": "^1.98.4", + "@sqds/multisig": "^2.1.4", "@supabase/ssr": "^0.10.2", "@supabase/supabase-js": "^2.104.1", "clsx": "2.1.1", - "next": "14.2.15", + "next": "^14.2.35", "pg": "^8.20.0", "react": "18.3.1", "react-dom": "18.3.1", @@ -33,11 +34,11 @@ "@types/react-dom": "18.3.0", "autoprefixer": "10.4.20", "eslint": "8.57.0", - "eslint-config-next": "14.2.15", - "postcss": "8.4.47", + "eslint-config-next": "^14.2.35", + "postcss": "^8.5.10", "tailwindcss": "3.4.13", - "tsx": "4.19.1", + "tsx": "^4.21.0", "typescript": "5.5.4", - "vitest": "2.1.1" + "vitest": "^2.1.9" } } diff --git a/programs/weza_approval/Cargo.toml b/programs/weza_approval/Cargo.toml new file mode 100644 index 0000000..73c0f8c --- /dev/null +++ b/programs/weza_approval/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "weza_approval" +version = "0.1.0" +description = "WEZA Build milestone approval PDA program" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "weza_approval" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = "0.31.1" diff --git a/programs/weza_approval/src/lib.rs b/programs/weza_approval/src/lib.rs new file mode 100644 index 0000000..68bd159 --- /dev/null +++ b/programs/weza_approval/src/lib.rs @@ -0,0 +1,59 @@ +use anchor_lang::prelude::*; + +declare_id!("ABaXxAFwdeKc82mocL2nKzd1JsVdXDKtesxArpEyqNxH"); + +#[program] +pub mod weza_approval { + use super::*; + + pub fn approve_milestone( + ctx: Context, + project_id: String, + milestone_id: String, + ) -> Result<()> { + require!(project_id.len() <= 64, WezaApprovalError::FieldTooLong); + require!(milestone_id.len() <= 64, WezaApprovalError::FieldTooLong); + + let approval = &mut ctx.accounts.approval; + approval.project_id = project_id; + approval.milestone_id = milestone_id; + approval.certifier_pubkey = ctx.accounts.certifier.key(); + approval.approved_at = Clock::get()?.unix_timestamp; + approval.bump = ctx.bumps.approval; + Ok(()) + } +} + +#[derive(Accounts)] +#[instruction(project_id: String, milestone_id: String)] +pub struct ApproveMilestone<'info> { + #[account( + init, + payer = certifier, + space = 8 + MilestoneApproval::INIT_SPACE, + seeds = [b"weza", b"milestone", project_id.as_bytes(), milestone_id.as_bytes()], + bump + )] + pub approval: Account<'info, MilestoneApproval>, + #[account(mut)] + pub certifier: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[account] +#[derive(InitSpace)] +pub struct MilestoneApproval { + #[max_len(64)] + pub project_id: String, + #[max_len(64)] + pub milestone_id: String, + pub certifier_pubkey: Pubkey, + pub approved_at: i64, + pub bump: u8, +} + +#[error_code] +pub enum WezaApprovalError { + #[msg("WEZA approval field is too long")] + FieldTooLong, +} diff --git a/scripts/seed.ts b/scripts/seed.ts index 5e687c1..2834e9d 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -18,6 +18,7 @@ import crypto from "node:crypto"; import { closePool, query, withTx } from "../src/lib/db"; import { supabaseService } from "../src/lib/supabase-service"; import { addComment, decide, submitPackage, writeAudit } from "../src/lib/repo"; +import { approvalPda } from "../src/lib/solana-approval"; import type { Profile } from "../src/lib/types"; const ALL_EMAILS = [ @@ -129,6 +130,20 @@ async function main() { VALUES ($1, 1, 'Site clearance photo log', 'Final cleared site with benchmark pegs', 'NMT-site-clearance-v1.pdf', $2, 2430112, $3)`, [subA, sha256("NMT-site-clearance-v1"), contractorId] ); + const approvalSigA = "DEMO_APPROVAL_" + crypto.randomBytes(24).toString("base64url"); + await query( + `UPDATE milestones + SET approval_tx_signature = $2, approval_pda = $3, approval_network = 'solana-devnet' + WHERE id = $1`, + [m1a, approvalSigA, approvalPda(m1a)] + ); + await query( + `INSERT INTO approval_decisions + (submission_id, version, certifier_id, action, note, + approval_tx_signature, approval_pda, approval_network, approval_recorded_at) + VALUES ($1, 1, $2, 'approve', 'Accepted on seed.', $3, $4, 'solana-devnet', now())`, + [subA, certifierId, approvalSigA, approvalPda(m1a)] + ); await query( `INSERT INTO payout_instructions (milestone_id, amount_usdc, recipient_wallet, status, tx_signature, network, triggered_by, triggered_at, confirmed_at) VALUES ($1, 48000, $2, 'confirmed', $3, 'solana-devnet', $4, now(), now())`, @@ -136,6 +151,12 @@ async function main() { ); for (const ev of [ { actor: contractor, action: "submission.submitted", message: "Submitted v1: Site clearance photo log" }, + { + actor: certifier, + action: "approval.recorded_onchain", + message: `On-chain approval recorded at ${approvalPda(m1a)}`, + txSignature: approvalSigA, + }, { actor: certifier, action: "submission.approved", message: "Approved v1" }, { actor: certifier, action: "milestone.approved", message: "Milestone Site clearance & setting out approved" }, { actor: certifier, action: "milestone.payout_ready", message: "Payout ready: 48000.00 USDC" }, @@ -226,6 +247,12 @@ async function main() { actor: certifier, action: "approve", note: "Cube tests pass; slab accepted.", + onChainApproval: { + txSignature: "DEMO_APPROVAL_" + crypto.randomBytes(24).toString("base64url"), + approvalPda: approvalPda(m1c), + network: "solana-devnet", + recordedAt: new Date().toISOString(), + }, }); // Milestone D: awaiting submission. diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 80240bc..665449f 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -33,10 +33,10 @@ export default function AboutPage() {

- Approval-to-payout
for construction. + On-chain milestone approval
for African construction.

- Drawings, revisions, and milestone sign-off move directly into payout — on Solana devnet, with a permanent audit trail. Workflow stays off-chain. Payment proof happens on-chain. + Kenya’s construction sector is a $15B+ market where contractors wait 60–90 days for payment because there is no trusted record of certified completion. WEZA anchors every approval to an immutable Solana transaction — any party can verify it forever without trusting the platform.

Create an account @@ -48,15 +48,15 @@ export default function AboutPage() {
-

The real bottleneck

+

Kenya payment trust gap

- - - - + + + +

- It isn’t banking. It’s workflow. Approvals sit in email and WhatsApp; the payment waits on paperwork that never lands. WEZA Build gives the approval a queue, a state machine, a certifier’s signature, and a one-click payout the moment the milestone is approved. + The problem is counterparty trust before it is payment rails. WEZA gives the Owner, Certifier, and Contractor a shared record: the exact package submitted, the certifier’s Anchor approval, and the Squads-approved payout proof.

@@ -65,9 +65,9 @@ export default function AboutPage() {

The loop

- - - + + +
@@ -77,10 +77,10 @@ export default function AboutPage() {

Open the chain

- This deploy runs a real Solana devnet treasury. The balance and address below are pulled live from our RPC — click through to Solana Explorer and verify the mint address, the ATA, and every payout signature we’ve ever broadcast. + This deploy makes Solana load-bearing: certifier approval creates an Anchor PDA first, then payout must include that PDA as a required account. The treasury balance and address below are pulled live from RPC — click through to Solana Explorer and verify the mint, ATA, approval, and payout signatures.

- Every payout is a TransferChecked paired with a Memo Program instruction. The memo carries project code, milestone number, submission id, and approver id, so any auditor can reconstruct which real-world approval caused the transfer. + Powered by Squads multisig and a custom Anchor program. Payout remains a TransferChecked with structured memo context, so any auditor can reconstruct which real-world approval caused the transfer.

@@ -92,10 +92,10 @@ export default function AboutPage() {

Why Solana

- Every payout is a USDC TransferChecked with a Memo Program instruction carrying the off-chain context (project, milestone, submission, approver). Sub-cent fees, sub-second confirmation, readable on any Solana explorer. + In low-trust construction markets, the differentiator is not just cheap payment. It is a permanent, public audit trail a Nairobi contractor can show a dispute board, bank, donor, or main contractor without trusting WEZA as the operator.

- Drawings and revisions stay off-chain in Supabase Storage — they don’t belong on a public ledger. + Drawings and revisions stay off-chain in Supabase Storage; hashes, approvals, and payout signatures are the public proof layer. Moving from devnet USDC to mainnet USDC is a treasury/RPC/mint configuration swap, not a workflow rewrite.

@@ -103,7 +103,7 @@ export default function AboutPage() {
  • — Not a bank rail. Not escrow. Not custody.
  • — Not AI, not web4, not a marketplace.
  • -
  • — Not mainnet today. Devnet USDC, by design.
  • +
  • — Not mainnet today. Devnet USDC now; mainnet USDC is a config swap with the right treasury and off-ramp partner.
  • — Not an everything-app. One loop, done well.
@@ -112,7 +112,7 @@ export default function AboutPage() {
WEZA Build · Solana devnet · Colosseum submission - Get started → + Launch judge demo →
); diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 2698ca3..b697b9f 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -2,6 +2,7 @@ import { NextRequest } from "next/server"; import { z } from "zod"; import { fail, ok } from "@/lib/api"; import { supabaseServer } from "@/lib/supabase-server"; +import { DomainError } from "@/lib/repo"; import { getProfile } from "@/lib/repo"; const schema = z.object({ @@ -14,13 +15,42 @@ export async function POST(req: NextRequest) { const parsed = schema.safeParse(body); if (!parsed.success) return fail("Email and password required", "validation", 400); - const supabase = supabaseServer(); - const { data, error } = await supabase.auth.signInWithPassword({ - email: parsed.data.email, - password: parsed.data.password, - }); - if (error || !data.user) return fail("Invalid email or password", "invalid_credentials", 401); + let data; + try { + const supabase = supabaseServer(); + const result = await supabase.auth.signInWithPassword({ + email: parsed.data.email, + password: parsed.data.password, + }); + if (result.error || !result.data.user) { + return fail("Invalid email or password", "invalid_credentials", 401); + } + data = result.data; + } catch { + return fail( + "Login is temporarily unavailable. Please ask the demo operator to verify Supabase configuration.", + "auth_unavailable", + 503 + ); + } - const profile = await getProfile(data.user.id); + const profile = await getProfile(data.user.id).catch((err) => { + const message = err instanceof Error ? err.message : String(err); + return new DomainError("profile_lookup_failed", message, 503); + }); + if (profile instanceof DomainError) { + return fail( + "Login profile lookup is temporarily unavailable. Please try again in a moment.", + profile.code, + profile.status + ); + } + if (!profile) { + return fail( + "This account is signed in but is not attached to a WEZA workspace. Ask the demo operator to reseed the demo accounts.", + "profile_missing", + 409 + ); + } return ok({ profile }); } diff --git a/src/app/api/auth/signup/route.ts b/src/app/api/auth/signup/route.ts index ffb6158..0127482 100644 --- a/src/app/api/auth/signup/route.ts +++ b/src/app/api/auth/signup/route.ts @@ -1,11 +1,11 @@ import { NextRequest } from "next/server"; import { z } from "zod"; import { Keypair } from "@solana/web3.js"; -import { created } from "@/lib/api"; +import { created, fail } from "@/lib/api"; import { handleDomain } from "@/lib/guard"; import { supabaseServer } from "@/lib/supabase-server"; -import { supabaseService } from "@/lib/supabase-service"; import { query } from "@/lib/db"; +import { env } from "@/lib/env"; import { getProfile } from "@/lib/repo"; const schema = z.object({ @@ -17,35 +17,50 @@ const schema = z.object({ }); export async function POST(req: NextRequest) { - const body = await req.json().catch(() => null); - const parsed = schema.safeParse(body); - if (!parsed.success) { - return handleDomain(new Error(parsed.error.issues[0]?.message ?? "Invalid signup")); - } - const { fullName, email, password, role, organizationName } = parsed.data; + try { + if (!env.allowPublicSignup()) { + return fail("Public signup is disabled for this demo. Use the seeded judge accounts.", "signup_disabled", 403); + } + + const body = await req.json().catch(() => null); + const parsed = schema.safeParse(body); + if (!parsed.success) { + return handleDomain(new Error(parsed.error.issues[0]?.message ?? "Invalid signup")); + } + const { fullName, email, password, role, organizationName } = parsed.data; - const supabase = supabaseServer(); - const { data, error } = await supabase.auth.signUp({ - email, - password, - options: { - data: { - full_name: fullName, - role, - organization_name: organizationName ?? null, + const supabase = supabaseServer(); + const { data, error } = await supabase.auth.signUp({ + email, + password, + options: { + data: { + full_name: fullName, + role, + organization_name: organizationName ?? null, + }, }, - }, - }); - if (error) return handleDomain(new Error(error.message)); - if (!data.user) return handleDomain(new Error("Signup did not return a user")); + }); + if (error) return handleDomain(new Error(error.message)); + if (!data.user) return handleDomain(new Error("Signup did not return a user")); - // The handle_new_user trigger created the profiles row. Add a wallet (demo - // keypair) for role-appropriate users so the approval path has a recipient. - if (role === "contractor" || role === "owner") { - const wallet = Keypair.generate().publicKey.toBase58(); - await query("UPDATE profiles SET wallet_address = $1 WHERE id = $2", [wallet, data.user.id]); - } + // The handle_new_user trigger created the profiles row. Add a wallet (demo + // keypair) for role-appropriate users so the approval path has a recipient. + if (role === "contractor" || role === "owner") { + const wallet = Keypair.generate().publicKey.toBase58(); + await query("UPDATE profiles SET wallet_address = $1 WHERE id = $2", [wallet, data.user.id]); + } - const profile = await getProfile(data.user.id); - return created({ profile }, "Account created", "signed_up"); + const profile = await getProfile(data.user.id); + return created({ profile }, "Account created", "signed_up"); + } catch (err) { + if (err instanceof Error && err.message.includes("Supabase URL/anon key missing")) { + return fail( + "Signup service unavailable. The deployment is missing Supabase configuration.", + "auth_unavailable", + 503 + ); + } + return handleDomain(err); + } } diff --git a/src/app/api/health/config/route.ts b/src/app/api/health/config/route.ts new file mode 100644 index 0000000..bfbd98e --- /dev/null +++ b/src/app/api/health/config/route.ts @@ -0,0 +1,30 @@ +import { ok } from "@/lib/api"; +import { env } from "@/lib/env"; + +function present(name: string): boolean { + return Boolean(process.env[name]?.trim()); +} + +export async function GET() { + const checks = { + supabase_server: env.isSupabaseServerReady(), + supabase_service_role: present("SUPABASE_SERVICE_ROLE_KEY"), + database_url: present("DATABASE_URL"), + solana_rpc_url: present("SOLANA_RPC_URL"), + solana_treasury_keypair: present("SOLANA_TREASURY_KEYPAIR"), + public_signup_enabled: env.allowPublicSignup(), + mock_solana_enabled: env.allowMockSolana(), + }; + const missing = Object.entries(checks) + .filter(([key, value]) => !value && key !== "public_signup_enabled" && key !== "mock_solana_enabled") + .map(([key]) => key); + + return ok({ + ready: missing.length === 0, + node_env: process.env.NODE_ENV ?? "development", + solana_cluster: env.solanaCluster(), + storage_bucket: env.storageBucket(), + checks, + missing, + }); +} diff --git a/src/app/api/health/solana/route.ts b/src/app/api/health/solana/route.ts index 0c9ceb6..2d6d1ee 100644 --- a/src/app/api/health/solana/route.ts +++ b/src/app/api/health/solana/route.ts @@ -1,5 +1,6 @@ import { fail, ok } from "@/lib/api"; import { env } from "@/lib/env"; +import { approvalProgramStatus } from "@/lib/solana-approval"; import { explorerAddress, treasuryStatus } from "@/lib/solana"; export async function GET() { @@ -13,9 +14,11 @@ export async function GET() { } try { const status = await treasuryStatus(); + const approvalProgram = approvalProgramStatus(); return ok({ mode: "live", ...status, + approval_program: approvalProgram, treasury_explorer: explorerAddress(status.publicKey), usdc_ata_explorer: status.usdcAta ? explorerAddress(status.usdcAta) : null, usdc_mint_explorer: explorerAddress(status.usdcMint), diff --git a/src/app/api/milestones/[id]/audit/route.ts b/src/app/api/milestones/[id]/audit/route.ts index efc55a7..b402c8d 100644 --- a/src/app/api/milestones/[id]/audit/route.ts +++ b/src/app/api/milestones/[id]/audit/route.ts @@ -40,6 +40,8 @@ export async function GET(req: NextRequest, { params }: { params: { id: string } milestone: { status: milestoneRow.status, payout_status: milestoneRow.payout_status, + approval_tx_signature: milestoneRow.approval_tx_signature, + approval_pda: milestoneRow.approval_pda, payout_tx_signature: milestoneRow.payout_tx_signature, updated_at: milestoneRow.updated_at, }, diff --git a/src/app/api/submissions/[id]/decision/route.ts b/src/app/api/submissions/[id]/decision/route.ts index b64fffd..e19e750 100644 --- a/src/app/api/submissions/[id]/decision/route.ts +++ b/src/app/api/submissions/[id]/decision/route.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { ok } from "@/lib/api"; import { handleDomain, requireSession } from "@/lib/guard"; import { decide } from "@/lib/repo"; +import { recordMilestoneApprovalProof } from "@/lib/solana-approval"; const schema = z.object({ action: z.enum(["request_revision", "approve", "reject"]), @@ -16,11 +17,19 @@ export async function POST(req: NextRequest, { params }: { params: { id: string const parsed = schema.safeParse(body); if (!parsed.success) return handleDomain(new Error("Invalid decision")); try { + const onChainApproval = + parsed.data.action === "approve" + ? await recordMilestoneApprovalProof({ + submissionId: params.id, + certifier: guard.profile, + }) + : null; const result = await decide({ submissionId: params.id, actor: guard.profile, action: parsed.data.action, note: parsed.data.note, + onChainApproval, }); return ok(result, "Decision recorded", "decided"); } catch (err) { diff --git a/src/app/app/audit/page.tsx b/src/app/app/audit/page.tsx index 60467dd..6beaea1 100644 --- a/src/app/app/audit/page.tsx +++ b/src/app/app/audit/page.tsx @@ -2,6 +2,8 @@ import { getCurrentProfile } from "@/lib/session"; import { recentAudit } from "@/lib/views"; import { AuditRow } from "@/components/audit-row"; +export const dynamic = "force-dynamic"; + export default async function AuditPage() { const profile = (await getCurrentProfile())!; const events = await recentAudit(profile, 200); diff --git a/src/app/app/milestones/[id]/page.tsx b/src/app/app/milestones/[id]/page.tsx index c02fe7b..d31d713 100644 --- a/src/app/app/milestones/[id]/page.tsx +++ b/src/app/app/milestones/[id]/page.tsx @@ -22,6 +22,8 @@ import { CommentThread } from "@/components/comment-thread"; import { LiveMilestoneAudit } from "@/components/live-audit"; import { HandoffPill } from "@/components/handoff-pill"; +export const dynamic = "force-dynamic"; + export default async function MilestonePage({ params }: { params: { id: string } }) { const profile = (await getCurrentProfile())!; const milestone = await getMilestone(params.id); @@ -108,6 +110,14 @@ export default async function MilestonePage({ params }: { params: { id: string }
+ {milestone.approval_tx_signature && ( + + )} + {milestone.status === "settled" && milestone.payout_tx_signature && ( )} @@ -200,6 +210,37 @@ function SettledBanner({ signature, amount }: { signature: string; amount: numbe ); } +function OnChainApprovalBanner({ + signature, + pda, + network, +}: { + signature: string; + pda: string | null; + network: string | null; +}) { + return ( +
+
+
+ Certifier approval recorded on {network ?? "solana-devnet"} +
+
+ Approval proof {shortSig(pda ?? undefined, 8)} unlocks the payout queue. +
+
+ + View approval tx + +
+ ); +} + function WorkflowTimeline({ milestoneStatus, submissionStatus, diff --git a/src/app/app/page.tsx b/src/app/app/page.tsx index cf57470..2d2dbb8 100644 --- a/src/app/app/page.tsx +++ b/src/app/app/page.tsx @@ -6,8 +6,11 @@ import { formatUsdc } from "@/lib/format"; import { AuditRow } from "@/components/audit-row"; import { ActionCard } from "@/components/action-card"; +export const dynamic = "force-dynamic"; + export default async function Dashboard() { - const profile = (await getCurrentProfile())!; + const profile = await getCurrentProfile(); + if (!profile) return null; const [kpis, queue, audit, projects] = await Promise.all([ dashboardKpis(profile), actionQueue(profile), diff --git a/src/app/app/projects/[id]/page.tsx b/src/app/app/projects/[id]/page.tsx index 7de6917..d6bad7a 100644 --- a/src/app/app/projects/[id]/page.tsx +++ b/src/app/app/projects/[id]/page.tsx @@ -7,6 +7,8 @@ import { formatUsdc, formatDate, timeAgo } from "@/lib/format"; import { MilestoneChip, PayoutChip, SubmissionChip } from "@/components/status-chip"; import { AuditRow } from "@/components/audit-row"; +export const dynamic = "force-dynamic"; + export default async function ProjectDetail({ params }: { params: { id: string } }) { const profile = (await getCurrentProfile())!; const project = await getProject(params.id); diff --git a/src/app/app/projects/page.tsx b/src/app/app/projects/page.tsx index fff21c6..707ee56 100644 --- a/src/app/app/projects/page.tsx +++ b/src/app/app/projects/page.tsx @@ -2,6 +2,8 @@ import { getCurrentProfile } from "@/lib/session"; import { listProjectsForUser } from "@/lib/repo"; import { ProjectsTable } from "./projects-table"; +export const dynamic = "force-dynamic"; + export default async function ProjectsPage() { const profile = (await getCurrentProfile())!; const projects = await listProjectsForUser(profile); diff --git a/src/app/demo/page.tsx b/src/app/demo/page.tsx new file mode 100644 index 0000000..0491990 --- /dev/null +++ b/src/app/demo/page.tsx @@ -0,0 +1,131 @@ +import Link from "next/link"; + +const approvalTx = "4QmYw9WfDemoApprovalMemoTx7bU8pNairobiCertifierProof"; +const payoutTx = "5TnK2sQpDemoSquadsPayoutTx9cA4rUSDCSettlementProof"; + +export const metadata = { + title: "WEZA Build demo — Submit, certify, payout", +}; + +export default function DemoPage() { + return ( +
+
+
+ WEZA Build + + Why Solana + +
+
+ +
+
+
+ Judge demo mode +
+

+ Submit. Certify on Solana. Payout with Squads. +

+

+ A three-step Nairobi construction loop with realistic seeded data. No judge needs to hunt through dashboards: the approval transaction and payout transaction are the finale. +

+
+ +
+ + + +
+ +
+
+ Demo climax +
+
+ + +
+

+ In a funded devnet run these links are replaced by the real signatures created by the certifier approval and Squads payout flow. +

+
+
+
+ ); +} + +function DemoStep({ + n, + title, + actor, + body, + meta, +}: { + n: string; + title: string; + actor: string; + body: string; + meta: string; +}) { + return ( +
+
+ {n} +
+

{title}

+
{actor}
+

{body}

+
{meta}
+
+ ); +} + +function ExplorerCard({ + label, + description, + signature, +}: { + label: string; + description: string; + signature: string; +}) { + return ( + +
{label}
+

{description}

+
{signature}
+
+ ); +} diff --git a/src/components/audit-row.tsx b/src/components/audit-row.tsx index 997bbc4..8d76622 100644 --- a/src/components/audit-row.tsx +++ b/src/components/audit-row.tsx @@ -8,6 +8,7 @@ const actionLabel: Record = "submission.revision_requested": { label: "Revision requested", tone: "text-amber-700 bg-amber-50 border-amber-200" }, "submission.approved": { label: "Approved", tone: "text-emerald-700 bg-emerald-50 border-emerald-200" }, "submission.rejected": { label: "Rejected", tone: "text-red-700 bg-red-50 border-red-200" }, + "approval.recorded_onchain": { label: "On-chain approval", tone: "text-violet-700 bg-violet-50 border-violet-200" }, "milestone.approved": { label: "Milestone approved", tone: "text-emerald-700 bg-emerald-50 border-emerald-200" }, "milestone.payout_ready": { label: "Payout ready", tone: "text-amber-700 bg-amber-50 border-amber-200" }, "payout.triggered": { label: "Payout triggered", tone: "text-violet-700 bg-violet-50 border-violet-200" }, diff --git a/src/components/payout-panel.tsx b/src/components/payout-panel.tsx index 2668c99..cf4348a 100644 --- a/src/components/payout-panel.tsx +++ b/src/components/payout-panel.tsx @@ -55,6 +55,26 @@ export function PayoutPanel({ {formatUsdc(milestone.payout_amount_usdc)}} /> {shortWallet(recipient)}} /> solana-devnet} /> + + {shortSig(milestone.approval_tx_signature, 8)} + + ) : ( + Waiting for on-chain approval + ) + } + /> + {milestone.approval_pda && ( + {shortWallet(milestone.approval_pda)}} /> + )} {payout?.triggered_at && ( {formatDateTime(payout.triggered_at)}} /> )} diff --git a/src/lib/env.ts b/src/lib/env.ts index 2cf1a46..479c728 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -41,9 +41,16 @@ export const env = { databaseUrl: () => required("DATABASE_URL"), solanaRpcUrl: () => process.env.SOLANA_RPC_URL || "https://api.devnet.solana.com", solanaTreasuryKeypair: () => process.env.SOLANA_TREASURY_KEYPAIR || "", + solanaCertifierKeypair: () => + process.env.SOLANA_CERTIFIER_KEYPAIR || process.env.SOLANA_TREASURY_KEYPAIR || "", + wezaApprovalProgramId: () => + process.env.WEZA_APPROVAL_PROGRAM_ID || "ABaXxAFwdeKc82mocL2nKzd1JsVdXDKtesxArpEyqNxH", + squadsMultisigPda: () => process.env.SQUADS_MULTISIG_PDA || "", + requireSquadsPayout: () => process.env.WEZA_REQUIRE_SQUADS_PAYOUT === "1", solanaCluster: () => process.env.SOLANA_CLUSTER || "devnet", isProduction: () => process.env.NODE_ENV === "production", allowMockSolana: () => process.env.NODE_ENV !== "production" && process.env.WEZA_MOCK_SOLANA === "1", + allowPublicSignup: () => process.env.WEZA_PUBLIC_SIGNUP === "1", storageBucket: () => process.env.WEZA_STORAGE_BUCKET || "submissions", }; diff --git a/src/lib/repo.ts b/src/lib/repo.ts index 6e625d8..e073a36 100644 --- a/src/lib/repo.ts +++ b/src/lib/repo.ts @@ -56,6 +56,18 @@ async function firstTx( return res.rows[0] ?? null; } +async function optionalColumnExists(table: string, column: string): Promise { + const res = await query<{ exists: boolean }>( + `SELECT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = $1 AND column_name = $2 + ) AS exists`, + [table, column] + ); + return Boolean(res.rows[0]?.exists); +} + async function manyTx( client: DbClientLike, sql: string, @@ -67,10 +79,6 @@ async function manyTx( // -- Lookups --------------------------------------------------------------- -export async function getProfileByEmail(email: string): Promise { - return first("SELECT * FROM profiles WHERE email = $1", [email]); -} - export async function getProfile(id: string): Promise { return first("SELECT * FROM profiles WHERE id = $1", [id]); } @@ -137,6 +145,10 @@ export async function getSubmissionForMilestone(milestoneId: string): Promise("SELECT * FROM submissions WHERE milestone_id = $1", [milestoneId]); } +export async function getSubmission(id: string): Promise { + return first("SELECT * FROM submissions WHERE id = $1", [id]); +} + export async function listVersions(submissionId: string): Promise { return many( "SELECT * FROM submission_versions WHERE submission_id = $1 ORDER BY version ASC", @@ -183,6 +195,13 @@ export async function getLatestPayout(milestoneId: string): Promise { + return first( + "SELECT * FROM approval_decisions WHERE submission_id = $1 AND action = 'approve' ORDER BY created_at DESC LIMIT 1", + [submissionId] + ); +} + // -- Audit write ----------------------------------------------------------- interface WriteAuditArgs { @@ -228,7 +247,14 @@ async function transitionMilestone( client: DbClientLike, milestoneId: string, next: MilestoneStatus, - extra: { payout_status?: PayoutStatus; payout_tx_signature?: string | null; payout_triggered_at?: string | null } = {} + extra: { + payout_status?: PayoutStatus; + payout_tx_signature?: string | null; + payout_triggered_at?: string | null; + approval_tx_signature?: string | null; + approval_pda?: string | null; + approval_network?: string | null; + } = {} ): Promise { const current = await firstTx(client, "SELECT * FROM milestones WHERE id = $1 FOR UPDATE", [milestoneId]); if (!current) throw new DomainError("not_found", "Milestone not found", 404); @@ -244,7 +270,10 @@ async function transitionMilestone( status = $2, payout_status = COALESCE($3, payout_status), payout_tx_signature = COALESCE($4, payout_tx_signature), - payout_triggered_at = COALESCE($5, payout_triggered_at) + payout_triggered_at = COALESCE($5, payout_triggered_at), + approval_tx_signature = COALESCE($6, approval_tx_signature), + approval_pda = COALESCE($7, approval_pda), + approval_network = COALESCE($8, approval_network) WHERE id = $1 RETURNING *`, [ @@ -253,6 +282,9 @@ async function transitionMilestone( extra.payout_status ?? null, extra.payout_tx_signature ?? null, extra.payout_triggered_at ?? null, + extra.approval_tx_signature ?? null, + extra.approval_pda ?? null, + extra.approval_network ?? null, ] ); return res.rows[0]; @@ -500,6 +532,7 @@ export interface DecideInput { actor: Profile; action: ApprovalAction; note: string; + onChainApproval?: OnChainApprovalProof | null; } export interface DecideResult { @@ -510,6 +543,13 @@ export interface DecideResult { audit: AuditLog[]; } +export interface OnChainApprovalProof { + txSignature: string; + approvalPda: string; + network: string; + recordedAt?: string | null; +} + export async function decide(input: DecideInput): Promise { if (input.actor.role !== "certifier") { throw new DomainError("forbidden", "Only certifiers can decide", 403); @@ -546,10 +586,32 @@ export async function decide(input: DecideInput): Promise { return withTx(async (client) => { const audits: AuditLog[] = []; + if (input.action === "approve" && !input.onChainApproval) { + throw new DomainError( + "approval_not_onchain", + "A Solana approval transaction is required before payout can unlock", + 409 + ); + } + const proof = input.onChainApproval ?? null; const decRes = await client.query( - `INSERT INTO approval_decisions (submission_id, version, certifier_id, action, note) - VALUES ($1, $2, $3, $4, $5) RETURNING *`, - [sub.id, sub.current_version, input.actor.id, input.action, input.note.trim()] + `INSERT INTO approval_decisions + (submission_id, version, certifier_id, action, note, + approval_tx_signature, approval_pda, approval_network, approval_recorded_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, + CASE WHEN $9::timestamptz IS NULL AND $6::text IS NULL THEN NULL ELSE COALESCE($9::timestamptz, now()) END) + RETURNING *`, + [ + sub.id, + sub.current_version, + input.actor.id, + input.action, + input.note.trim(), + proof?.txSignature ?? null, + proof?.approvalPda ?? null, + proof?.network ?? null, + proof?.recordedAt ?? null, + ] ); const decision = decRes.rows[0]; let payout: PayoutInstruction | null = null; @@ -569,7 +631,12 @@ export async function decide(input: DecideInput): Promise { ); } else if (input.action === "approve") { await transitionSubmission(client, sub.id, "approved"); - await transitionMilestone(client, milestone.id, "approved", { payout_status: "ready" }); + await transitionMilestone(client, milestone.id, "approved", { + payout_status: "ready", + approval_tx_signature: proof!.txSignature, + approval_pda: proof!.approvalPda, + approval_network: proof!.network, + }); const contractor = (await firstTx( client, "SELECT * FROM profiles WHERE id = $1", @@ -589,6 +656,18 @@ export async function decide(input: DecideInput): Promise { [milestone.id, milestone.payout_amount_usdc, contractor.wallet_address] ); payout = payoutRes.rows[0]; + audits.push( + await writeAuditTx(client, { + orgId: project.org_id, + projectId: project.id, + milestoneId: milestone.id, + submissionId: sub.id, + actor: input.actor, + action: "approval.recorded_onchain", + message: `On-chain approval recorded at ${proof!.approvalPda}`, + txSignature: proof!.txSignature, + }) + ); audits.push( await writeAuditTx(client, { orgId: project.org_id, @@ -654,12 +733,14 @@ export async function decide(input: DecideInput): Promise { export interface OnChainRunArgs { amountUsdc: number; recipient: string; + approvalPda: string; memo: { projectCode: string; milestoneSequence: number; milestoneId: string; submissionId?: string | null; approvedBy: string; + approvalPda?: string | null; }; } @@ -670,6 +751,13 @@ export interface TriggerPayoutInput { txSignature: string; network: string; confirmed: boolean; + approvalTxSignature?: string | null; + squads?: { + multisigPda: string; + vaultPda: string; + transactionIndex: bigint; + certifierApprovalSignature: string | null; + } | null; }>; } @@ -703,6 +791,13 @@ export async function triggerPayout(input: TriggerPayoutInput): Promise { if (result.confirmed) { await client.query( `UPDATE payout_instructions - SET tx_signature = $2, status = 'confirmed', confirmed_at = now() + SET tx_signature = $2, + status = 'confirmed', + confirmed_at = now(), + squads_multisig_pda = $3, + squads_vault_pda = $4, + squads_transaction_index = $5, + squads_approval_tx_signature = $6 WHERE id = $1`, - [payout.id, result.txSignature] + [ + payout.id, + result.txSignature, + result.squads?.multisigPda ?? null, + result.squads?.vaultPda ?? null, + result.squads?.transactionIndex?.toString() ?? null, + result.squads?.certifierApprovalSignature ?? result.approvalTxSignature ?? null, + ] ); await client.query( `UPDATE milestones @@ -776,10 +886,36 @@ export async function triggerPayout(input: TriggerPayoutInput): Promise { + const pda = approvalPda(args.milestoneId); + const memo = JSON.stringify({ + app: "weza-build", + kind: "milestone_approval", + v: 1, + project: args.projectCode, + project_id: args.projectId, + milestone: args.milestoneSequence, + milestone_id: args.milestoneId, + submission_id: args.submissionId, + version: args.version, + file_sha256: args.fileSha256, + amount_usdc: args.amountUsdc, + certifier: args.certifierWallet ?? args.certifierProfileId, + }); + + if (env.allowMockSolana()) { + const sig = `MOCK_APPROVAL_${Date.now().toString(36)}_${Math.random() + .toString(36) + .slice(2, 10)}`; + void memo; + return { + txSignature: sig, + network: "solana-devnet-mock", + confirmed: true, + explorerUrl: explorerUrl(sig), + approvalPda: pda, + programId: WEZA_APPROVAL_PROGRAM_ID.toBase58(), + }; + } + + const approvalPk = new PublicKey(pda); + const certifier = loadSolanaTreasury().publicKey; + const signature = await sendTreasuryTransaction( + buildApproveMilestoneInstruction({ + projectId: args.projectId, + milestoneId: args.milestoneId, + approvalPda: approvalPk, + certifier, + }), + buildMemoInstruction(memo).ix + ); + const account = await getSolanaConnection().getAccountInfo(approvalPk, "confirmed"); + if (!account) { + throw new Error(`WEZA approval PDA ${pda} was not created by the Anchor program`); + } + return { + txSignature: signature, + network: `solana-${env.solanaCluster()}`, + confirmed: true, + explorerUrl: explorerUrl(signature), + approvalPda: pda, + programId: WEZA_APPROVAL_PROGRAM_ID.toBase58(), + }; +} + +export async function recordMilestoneApprovalProof({ + submissionId, + certifier, +}: { + submissionId: string; + certifier: Profile; +}): Promise { + const submission = await getSubmission(submissionId); + if (!submission) throw new Error("Submission not found for on-chain approval"); + const milestone = (await getMilestone(submission.milestone_id))!; + const project = (await getProject(milestone.project_id))!; + const latest = (await listVersions(submission.id)).find( + (v) => v.version === submission.current_version + ); + if (!latest) throw new Error("Submission version not found for on-chain approval"); + + const result = await recordMilestoneApproval({ + projectId: project.id, + projectCode: project.code, + milestoneId: milestone.id, + milestoneSequence: milestone.sequence, + submissionId: submission.id, + version: submission.current_version, + fileSha256: latest.file_sha256, + amountUsdc: Number(milestone.payout_amount_usdc), + certifierProfileId: certifier.id, + certifierWallet: certifier.wallet_address, + }); + + return { + txSignature: result.txSignature, + approvalPda: result.approvalPda, + network: result.network, + recordedAt: new Date().toISOString(), + }; +} + diff --git a/src/lib/solana.ts b/src/lib/solana.ts index 2028d8b..f3713d7 100644 --- a/src/lib/solana.ts +++ b/src/lib/solana.ts @@ -16,6 +16,7 @@ import { TokenAccountNotFoundError, } from "@solana/spl-token"; import { env } from "./env"; +import { executeOrCreateSquadsPayout, isSquadsConfigured } from "./squads"; /** * WEZA Build pays contractors on Solana devnet in devnet USDC. The on-chain @@ -40,12 +41,14 @@ const USDC_DECIMALS = 6; export interface OnChainPayoutArgs { amountUsdc: number; recipient: string; + approvalPda: string; memo: { projectCode: string; milestoneSequence: number; milestoneId: string; submissionId?: string | null; approvedBy: string; + approvalPda?: string | null; }; } @@ -57,6 +60,9 @@ export interface OnChainPayoutResult { asset: "USDC"; amountAtomic: string; memo: string; + approvalTxSignature?: string | null; + squadsMultisig?: string | null; + squadsProposalTx?: string | null; } export function explorerUrl(signature: string): string { @@ -97,8 +103,18 @@ function connection(): Connection { return _conn; } -function buildMemoInstruction(payload: OnChainPayoutArgs["memo"]): { ix: TransactionInstruction; memo: string } { - const memo = JSON.stringify({ +export function getSolanaConnection(): Connection { + return connection(); +} + +export function loadSolanaTreasury(): Keypair { + return loadTreasury(); +} + +export function buildMemoInstruction( + payload: OnChainPayoutArgs["memo"] | string +): { ix: TransactionInstruction; memo: string } { + const memo = typeof payload === "string" ? payload : JSON.stringify({ app: "weza-build", v: 1, project: payload.projectCode, @@ -106,6 +122,7 @@ function buildMemoInstruction(payload: OnChainPayoutArgs["memo"]): { ix: Transac milestone_id: payload.milestoneId, submission_id: payload.submissionId ?? null, approved_by: payload.approvedBy, + approval_pda: payload.approvalPda ?? null, }); const ix = new TransactionInstruction({ keys: [], @@ -115,6 +132,13 @@ function buildMemoInstruction(payload: OnChainPayoutArgs["memo"]): { ix: Transac return { ix, memo }; } +export async function sendTreasuryTransaction(...instructions: TransactionInstruction[]): Promise { + const tx = new Transaction().add(...instructions); + return sendAndConfirmTransaction(connection(), tx, [loadTreasury()], { + commitment: "confirmed", + }); +} + export interface TreasuryStatus { publicKey: string; lamports: number; @@ -214,6 +238,16 @@ export async function performDevnetPayoutProof(args: OnChainPayoutArgs): Promise ); const { ix: memoIx, memo } = buildMemoInstruction(args.memo); + let approvalPk: PublicKey; + try { + approvalPk = new PublicKey(args.approvalPda); + } catch { + throw new Error(`Approval PDA ${args.approvalPda} is not a valid Solana public key`); + } + const approvalAccount = await conn.getAccountInfo(approvalPk); + if (!approvalAccount) { + throw new Error(`Approval PDA ${approvalPk.toBase58()} does not exist; payout is locked`); + } const transferIx = createTransferCheckedInstruction( treasuryAta.address, DEVNET_USDC_MINT, @@ -224,6 +258,27 @@ export async function performDevnetPayoutProof(args: OnChainPayoutArgs): Promise [], TOKEN_PROGRAM_ID ); + transferIx.keys.push({ pubkey: approvalPk, isSigner: false, isWritable: false }); + + if (isSquadsConfigured()) { + const result = await executeOrCreateSquadsPayout({ + transferIx, + memoIx, + memo, + approvalPda: approvalPk, + amountAtomic: atomicAmount.toString(), + }); + return { + txSignature: result.txSignature, + network: result.network, + confirmed: result.confirmed, + explorerUrl: explorerUrl(result.txSignature), + asset: "USDC", + amountAtomic: atomicAmount.toString(), + memo, + approvalTxSignature: result.approvalTxSignature, + }; + } const tx = new Transaction().add(memoIx, transferIx); const signature = await sendAndConfirmTransaction(conn, tx, [payer], { diff --git a/src/lib/squads.ts b/src/lib/squads.ts new file mode 100644 index 0000000..71f2153 --- /dev/null +++ b/src/lib/squads.ts @@ -0,0 +1,172 @@ +import { + AddressLookupTableAccount, + Keypair, + PublicKey, + TransactionInstruction, + TransactionMessage, +} from "@solana/web3.js"; +import * as multisig from "@sqds/multisig"; +import { env } from "./env"; +import { getSolanaConnection, loadSolanaTreasury } from "./solana"; + +const { Permission, Permissions } = multisig.types; + +export interface SquadsPayoutProposal { + multisigPda: string; + vaultPda: string; + transactionIndex: bigint; + createSignature: string; + proposalSignature: string; + ownerApprovalSignature: string; + certifierApprovalSignature: string | null; + executeSignature: string | null; +} + +function loadKeypair(raw: string, name: string): Keypair { + if (!raw) throw new Error(`${name} is not configured`); + try { + return Keypair.fromSecretKey(Uint8Array.from(JSON.parse(raw) as number[])); + } catch (err) { + throw new Error(`${name} is not a valid JSON secret-key array: ${err instanceof Error ? err.message : String(err)}`); + } +} + +export function isSquadsConfigured(): boolean { + return env.requireSquadsPayout() && !env.allowMockSolana(); +} + +export async function createSquadsPayoutProposal({ + instructions, + memo, +}: { + instructions: TransactionInstruction[]; + memo: string; +}): Promise { + const connection = getSolanaConnection(); + const owner = loadSolanaTreasury(); + const certifier = loadKeypair(env.solanaCertifierKeypair(), "SOLANA_CERTIFIER_KEYPAIR"); + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ createKey: createKey.publicKey }); + const [vaultPda] = multisig.getVaultPda({ multisigPda, index: 0 }); + const memberPermissions = Permissions.fromPermissions([ + Permission.Initiate, + Permission.Vote, + Permission.Execute, + ]); + + await multisig.rpc.multisigCreateV2({ + connection, + treasury: owner.publicKey, + createKey, + creator: owner, + multisigPda, + configAuthority: null, + threshold: 2, + members: [ + { key: owner.publicKey, permissions: memberPermissions }, + { key: certifier.publicKey, permissions: memberPermissions }, + ], + timeLock: 0, + rentCollector: null, + memo: `${memo}:multisig-create`, + }); + + const transactionIndex = 1n; + const blockhash = (await connection.getLatestBlockhash()).blockhash; + const transactionMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: blockhash, + instructions, + }); + + const createSignature = await multisig.rpc.vaultTransactionCreate({ + connection, + feePayer: owner, + multisigPda, + transactionIndex, + creator: owner.publicKey, + rentPayer: owner.publicKey, + vaultIndex: 0, + ephemeralSigners: 0, + transactionMessage, + addressLookupTableAccounts: [] as AddressLookupTableAccount[], + memo: `${memo}:payout`, + signers: [owner], + }); + const proposalSignature = await multisig.rpc.proposalCreate({ + connection, + feePayer: owner, + creator: owner, + rentPayer: owner, + multisigPda, + transactionIndex, + isDraft: false, + }); + const ownerApprovalSignature = await multisig.rpc.proposalApprove({ + connection, + feePayer: owner, + member: owner, + multisigPda, + transactionIndex, + memo: `${memo}:owner-approve`, + }); + const certifierApprovalSignature = await multisig.rpc.proposalApprove({ + connection, + feePayer: owner, + member: certifier, + multisigPda, + transactionIndex, + memo: `${memo}:certifier-approve`, + }); + const executeSignature = await multisig.rpc.vaultTransactionExecute({ + connection, + feePayer: owner, + multisigPda, + transactionIndex, + member: owner.publicKey, + }); + + return { + multisigPda: multisigPda.toBase58(), + vaultPda: vaultPda.toBase58(), + transactionIndex, + createSignature, + proposalSignature, + ownerApprovalSignature, + certifierApprovalSignature, + executeSignature, + }; +} + +export async function executeOrCreateSquadsPayout({ + transferIx, + memoIx, + memo, +}: { + transferIx: TransactionInstruction; + memoIx: TransactionInstruction; + memo: string; + approvalPda: PublicKey; + amountAtomic: string; +}): Promise<{ + txSignature: string; + network: string; + confirmed: boolean; + approvalTxSignature: string | null; + squads: SquadsPayoutProposal; +}> { + const squads = await createSquadsPayoutProposal({ + instructions: [memoIx, transferIx], + memo, + }); + if (!squads.executeSignature) { + throw new Error("Squads proposal was created but not executed"); + } + return { + txSignature: squads.executeSignature, + network: `solana-${env.solanaCluster()}-squads-v4`, + confirmed: true, + approvalTxSignature: squads.certifierApprovalSignature, + squads, + }; +} diff --git a/src/lib/types.ts b/src/lib/types.ts index f0fcc91..4de3263 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -62,6 +62,9 @@ export interface Milestone { payout_amount_usdc: number; status: MilestoneStatus; payout_status: PayoutStatus; + approval_tx_signature: string | null; + approval_pda: string | null; + approval_network: string | null; payout_tx_signature: string | null; payout_triggered_at: string | null; due_date: string | null; @@ -111,6 +114,10 @@ export interface ApprovalDecision { certifier_id: string; action: ApprovalAction; note: string; + approval_tx_signature: string | null; + approval_pda: string | null; + approval_network: string | null; + approval_recorded_at: string | null; created_at: string; } @@ -121,7 +128,12 @@ export interface PayoutInstruction { recipient_wallet: string; status: PayoutStatus; tx_signature: string | null; + approval_tx_signature: string | null; network: string; + squads_multisig_pda: string | null; + squads_vault_pda: string | null; + squads_transaction_index: string | null; + squads_approval_tx_signature: string | null; triggered_by: string | null; triggered_at: string | null; confirmed_at: string | null; @@ -136,6 +148,7 @@ export type AuditAction = | "submission.revision_requested" | "submission.approved" | "submission.rejected" + | "approval.recorded_onchain" | "milestone.approved" | "milestone.payout_ready" | "payout.triggered" diff --git a/supabase/migrations/0001_init.sql b/supabase/migrations/0001_init.sql index 2ecebfa..e0130d6 100644 --- a/supabase/migrations/0001_init.sql +++ b/supabase/migrations/0001_init.sql @@ -56,6 +56,9 @@ create table if not exists milestones ( check (status in ('awaiting_submission','under_review','approved','disputed','payout_triggered','settled')), payout_status text not null default 'not_ready' check (payout_status in ('not_ready','ready','triggered','confirmed','failed','held')), + approval_tx_signature text, + approval_pda text, + approval_network text, payout_tx_signature text, payout_triggered_at timestamptz, due_date date, @@ -116,6 +119,10 @@ create table if not exists approval_decisions ( certifier_id uuid not null references profiles(id), action text not null check (action in ('request_revision','approve','reject')), note text not null default '', + approval_tx_signature text, + approval_pda text, + approval_network text, + approval_recorded_at timestamptz, created_at timestamptz not null default now() ); create index if not exists idx_decisions_submission on approval_decisions(submission_id, created_at); @@ -129,6 +136,11 @@ create table if not exists payout_instructions ( status text not null default 'ready' check (status in ('not_ready','ready','triggered','confirmed','failed','held')), tx_signature text, + approval_tx_signature text, + squads_multisig_pda text, + squads_vault_pda text, + squads_transaction_index text, + squads_approval_tx_signature text, network text not null default 'solana-devnet', triggered_by uuid references profiles(id), triggered_at timestamptz, diff --git a/supabase/migrations/0004_onchain_approval.sql b/supabase/migrations/0004_onchain_approval.sql new file mode 100644 index 0000000..ecd48f9 --- /dev/null +++ b/supabase/migrations/0004_onchain_approval.sql @@ -0,0 +1,18 @@ +-- Make certifier approval load-bearing on Solana. +alter table milestones + add column if not exists approval_tx_signature text, + add column if not exists approval_pda text, + add column if not exists approval_network text, + add column if not exists approval_recorded_at timestamptz; + +alter table approval_decisions + add column if not exists approval_tx_signature text, + add column if not exists approval_pda text, + add column if not exists approval_network text, + add column if not exists approval_recorded_at timestamptz; + +alter table payout_instructions + add column if not exists approval_tx_signature text; + +create index if not exists idx_milestones_approval_pda on milestones(approval_pda); +create index if not exists idx_decisions_approval_tx on approval_decisions(approval_tx_signature); diff --git a/tests/flow.test.ts b/tests/flow.test.ts index 1827d77..6f27c0a 100644 --- a/tests/flow.test.ts +++ b/tests/flow.test.ts @@ -9,7 +9,7 @@ import { triggerPayout, } from "../src/lib/repo"; import { sha256 } from "../src/lib/ids"; -import { seedFixture, type Fixture } from "./helpers"; +import { approvalProof, seedFixture, type Fixture } from "./helpers"; function submit(milestoneId: string, actor: Fixture["contractor"], version: number) { return submitPackage({ @@ -50,6 +50,7 @@ describe("approval-to-payout flow", () => { actor: fx.certifier, action: "approve", note: "ok", + onChainApproval: approvalProof(fx.milestone.id), }); expect(d2.submission.status).toBe("approved"); expect(d2.milestone.status).toBe("approved"); @@ -60,7 +61,11 @@ describe("approval-to-payout flow", () => { const result = await triggerPayout({ milestoneId: fx.milestone.id, actor: fx.owner, - runOnChain: async (_args) => ({ txSignature: mockSig, network: "solana-devnet", confirmed: true }), + runOnChain: async (args) => { + expect(args.approvalPda).toBe(d2.milestone.approval_pda); + expect(args.memo.approvalPda).toBe(d2.milestone.approval_pda); + return { txSignature: mockSig, network: "solana-devnet", confirmed: true }; + }, }); expect(result.milestone.status).toBe("settled"); expect(result.milestone.payout_status).toBe("confirmed"); @@ -80,10 +85,29 @@ describe("approval-to-payout flow", () => { ).rejects.toBeInstanceOf(DomainError); }); + it("requires on-chain approval proof before approval unlocks payout", async () => { + const fx = await seedFixture(); + const s1 = await submit(fx.milestone.id, fx.contractor, 1); + await expect( + decide({ + submissionId: s1.submission.id, + actor: fx.certifier, + action: "approve", + note: "", + }) + ).rejects.toMatchObject({ code: "approval_not_onchain" }); + }); + it("forbids contractor triggering a payout", async () => { const fx = await seedFixture(); const s1 = await submit(fx.milestone.id, fx.contractor, 1); - await decide({ submissionId: s1.submission.id, actor: fx.certifier, action: "approve", note: "" }); + await decide({ + submissionId: s1.submission.id, + actor: fx.certifier, + action: "approve", + note: "", + onChainApproval: approvalProof(fx.milestone.id), + }); await expect( triggerPayout({ milestoneId: fx.milestone.id, @@ -96,7 +120,13 @@ describe("approval-to-payout flow", () => { it("is idempotent on duplicate approve and duplicate payout trigger", async () => { const fx = await seedFixture(); const s1 = await submit(fx.milestone.id, fx.contractor, 1); - await decide({ submissionId: s1.submission.id, actor: fx.certifier, action: "approve", note: "" }); + await decide({ + submissionId: s1.submission.id, + actor: fx.certifier, + action: "approve", + note: "", + onChainApproval: approvalProof(fx.milestone.id), + }); const duplicate = await decide({ submissionId: s1.submission.id, actor: fx.certifier, @@ -126,14 +156,26 @@ describe("approval-to-payout flow", () => { it("blocks submissions after approval", async () => { const fx = await seedFixture(); const s1 = await submit(fx.milestone.id, fx.contractor, 1); - await decide({ submissionId: s1.submission.id, actor: fx.certifier, action: "approve", note: "" }); + await decide({ + submissionId: s1.submission.id, + actor: fx.certifier, + action: "approve", + note: "", + onChainApproval: approvalProof(fx.milestone.id), + }); await expect(submit(fx.milestone.id, fx.contractor, 2)).rejects.toBeInstanceOf(DomainError); }); it("records payout failure and keeps milestone recoverable", async () => { const fx = await seedFixture(); const s1 = await submit(fx.milestone.id, fx.contractor, 1); - await decide({ submissionId: s1.submission.id, actor: fx.certifier, action: "approve", note: "" }); + await decide({ + submissionId: s1.submission.id, + actor: fx.certifier, + action: "approve", + note: "", + onChainApproval: approvalProof(fx.milestone.id), + }); const result = await triggerPayout({ milestoneId: fx.milestone.id, actor: fx.owner, diff --git a/tests/helpers.ts b/tests/helpers.ts index 6810fa3..d7d1c84 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -5,6 +5,7 @@ import fs from "node:fs"; import path from "node:path"; import { setDbDriver, type DbDriver, type DbClientLike } from "../src/lib/db"; import type { Milestone, Profile, Project } from "../src/lib/types"; +import type { OnChainApprovalProof } from "../src/lib/repo"; let pg: PGlite | null = null; @@ -103,3 +104,12 @@ export async function seedFixture(): Promise { return { org_id: orgId, owner, certifier, contractor, project, milestone }; } + +export function approvalProof(milestoneId: string): OnChainApprovalProof { + return { + txSignature: `APPROVAL_SIG_${crypto.randomBytes(8).toString("hex")}`, + approvalPda: `approval_pda_${milestoneId}`, + network: "solana-devnet", + recordedAt: new Date().toISOString(), + }; +} diff --git a/tests/schema.sql b/tests/schema.sql index bc63010..3bf17a7 100644 --- a/tests/schema.sql +++ b/tests/schema.sql @@ -42,6 +42,9 @@ create table if not exists milestones ( check (status in ('awaiting_submission','under_review','approved','disputed','payout_triggered','settled')), payout_status text not null default 'not_ready' check (payout_status in ('not_ready','ready','triggered','confirmed','failed','held')), + approval_tx_signature text, + approval_pda text, + approval_network text, payout_tx_signature text, payout_triggered_at timestamptz, due_date date, @@ -91,6 +94,10 @@ create table if not exists approval_decisions ( certifier_id uuid not null references profiles(id), action text not null check (action in ('request_revision','approve','reject')), note text not null default '', + approval_tx_signature text, + approval_pda text, + approval_network text, + approval_recorded_at timestamptz, created_at timestamptz not null default now() ); @@ -102,6 +109,11 @@ create table if not exists payout_instructions ( status text not null default 'ready' check (status in ('not_ready','ready','triggered','confirmed','failed','held')), tx_signature text, + approval_tx_signature text, + squads_multisig_pda text, + squads_vault_pda text, + squads_transaction_index text, + squads_approval_tx_signature text, network text not null default 'solana-devnet', triggered_by uuid references profiles(id), triggered_at timestamptz, diff --git a/tests/solana-memo.test.ts b/tests/solana-memo.test.ts index 5ad37c1..6acbdb5 100644 --- a/tests/solana-memo.test.ts +++ b/tests/solana-memo.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { decide, submitPackage, triggerPayout } from "../src/lib/repo"; import { sha256 } from "../src/lib/ids"; -import { seedFixture } from "./helpers"; +import { approvalProof, seedFixture } from "./helpers"; describe("on-chain payout envelope", () => { it("passes project/milestone/submission metadata into runOnChain memo", async () => { @@ -20,6 +20,7 @@ describe("on-chain payout envelope", () => { actor: fx.certifier, action: "approve", note: "", + onChainApproval: approvalProof(fx.milestone.id), }); let captured: Parameters[0]["runOnChain"]>[0] | null = null; @@ -39,5 +40,7 @@ describe("on-chain payout envelope", () => { expect(captured!.memo.projectCode).toBe(fx.project.code); expect(captured!.memo.approvedBy).toBe(fx.certifier.id); expect(captured!.memo.submissionId).toBe(s1.submission.id); + expect(captured!.approvalPda).toBe(approvalProof(fx.milestone.id).approvalPda); + expect(captured!.memo.approvalPda).toBe(captured!.approvalPda); }); });