diff --git a/.env.example b/.env.example index a7260d9..762f23e 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,4 @@ -# --- Hedera network ------------------------------------------------------- -HEDERA_NETWORK=testnet +# --- Hedera operator ------------------------------------------------------ # Operator = deployer + token treasury/keys for the MVP. Funded testnet account on chain 296. # Raw-hex ECDSA key (EVM-style); hardhat.config signs the testnet network with it. OPERATOR_ID=0.0.xxxxxx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e6f6af5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + pull_request: + +jobs: + contracts: + name: contracts (compile · typecheck · test) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm run compile + - run: pnpm run typecheck + - run: pnpm test + + web: + name: web (build) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: web/pnpm-lock.yaml + - run: pnpm install --frozen-lockfile + working-directory: web + - run: pnpm build + working-directory: web diff --git a/.gitignore b/.gitignore index 9b887da..dc62afe 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ artifacts/ cache/ typechain-types/ *.mov + +# Local prep & internal notes (not part of the public repo) +/notes/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b506dc6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Wafer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4129d1b..90c08ae 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,256 @@ -# Wafer +
-> InfraFi liquidity for DePIN. Operators sell their future on-chain rewards for upfront HBAR; -> investors hold a NAV-appreciating, KYC-gated pool share. A **Solidity vault on Hedera (HSCS)** -> that creates and manages **HTS tokens**, settled in **native HBAR**, with a **SaucerSwap** -> secondary market. +# 🗽 Wafer -Wafer is a permanent, NAV-appreciating tokenized fund on Hedera that buys DePIN operators' -future on-chain rewards for upfront HBAR. The name: a *wafer* is the thin slice of silicon every -GPU and chip is cut from — the substrate of the compute economy that Wafer turns into liquid, -on-chain yield. "InfraFi" is the category; Wafer is the product. +**InfraFi liquidity for DePIN — a KYC-gated, NAV-appreciating tokenized credit fund on Hedera.** -- **One-pager (source of truth, judge-facing):** [`docs/ONE-PAGER.md`](docs/ONE-PAGER.md) -- **Full technical spec:** [`SPEC.md`](SPEC.md) -- **Sponsor / track strategy:** [`docs/TRACKS.md`](docs/TRACKS.md) +[![Live Demo](https://img.shields.io/badge/demo-wafer--steel.vercel.app-000?style=flat-square)](https://wafer-steel.vercel.app/) +[![Hedera Testnet](https://img.shields.io/badge/Hedera-Testnet%20(296)-7c3aed?style=flat-square)](https://hashscan.io/testnet/contract/0x8Fb4439f76ea7eAa6DcE88751A20981a796fb311) +[![Sourcify](https://img.shields.io/badge/Sourcify-verified-2ecc71?style=flat-square)](https://repo.sourcify.dev/contracts/full_match/296/0x8Fb4439f76ea7eAa6DcE88751A20981a796fb311/) +[![Solidity](https://img.shields.io/badge/Solidity-0.8.24-363636?style=flat-square)](https://soliditylang.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE) -ETHGlobal New York 2026 · Hedera Testnet (chain 296) · target track: **Hedera Tokenization**. +🏆 **Winner — Tokenization on Hedera ($1,500) · ETHGlobal New York 2026** -## How it works (60 seconds) +
-1. A DePIN operator (GPU/compute, wireless, mapping, energy) needs capital today; rewards arrive - on-chain over time. They sell a slice of those future rewards. -2. The `WaferVault` contract records the financed claim as an **HTS NFT** it holds, and advances - HBAR to the operator. -3. Investors deposit HBAR and receive a fungible **HTS pool-share** token — a share of a - permanent vault, standardized by network + risk (e.g. `GPU-A`). -4. The operator routes its rewards (HBAR) into the vault. **NAV per share rises** continuously. -5. Investors exit any time: **redeem at NAV** (burn shares → HBAR), or **swap on SaucerSwap**. +Wafer turns the future on-chain rewards of physical infrastructure (DePIN) into liquid, on-chain +yield. Operators get HBAR today against the rewards their hardware will earn; investors hold a +fungible, KYC-gated, NAV-appreciating fund share whose value rises live as those rewards stream back +in. It's Centrifuge / Maple, specialized for DePIN — built end-to-end on the **Hedera Token Service**. -The vault is a smart contract → all logic is on-chain and verifiable. The frontend talks to the -contract **directly** via a wallet — no backend, no HCS. +> 🏆 **Winner — "Tokenization on Hedera" at [ETHGlobal New York 2026](https://ethglobal.com/events/newyork2026).** +> One of the year's flagship Ethereum hackathons — **486 hackers** and **682 attendees** from **152 +> cities, 44 countries, and 6 continents**. Out of that global field, the **Hedera** team awarded +> Wafer the Hedera Token Service tokenization bounty. + +> [!NOTE] +> The exact repository submitted to ETHGlobal New York 2026 is +> [`aiden-fianso/Wafer`](https://github.com/aiden-fianso/Wafer) (frozen at submission). This +> repository is the continued, post-hackathon development of the project. + +## Table of contents + +- [Why DePIN](#why-depin) +- [How it works](#how-it-works) +- [Architecture](#architecture) +- [Hedera Token Service](#hedera-token-service) +- [What's live vs. roadmap](#whats-live-vs-roadmap) +- [Tech stack](#tech-stack) +- [Repository structure](#repository-structure) +- [Getting started](#getting-started) +- [Deployed addresses](#deployed-addresses) +- [Security & testing](#security--testing) +- [License](#license) + +## Why DePIN + +DePIN (Decentralized Physical Infrastructure) operators — GPU/compute, wireless, mapping, energy, +storage — buy hardware **today** but earn protocol rewards **over months**. That timing gap is +capital they don't have, and legacy credit can't underwrite a stream of on-chain rewards. + +What makes DePIN the ideal real-world asset for on-chain credit: + +> DePIN cashflow is **natively on-chain** — no invoice, no bank, no fiat bridge. Repayment needs no +> trust in a human paying back: the operator escrows its **device-NFT** (the on-chain object that +> controls where rewards are deposited, e.g. Helium's `recipient/destination` model), so the hardware +> routes its rewards straight to the vault. The fund's NAV ticks up live as they land. + +## How it works + +``` +Operator proposes a deal + └─▶ 1. Admin assigns a risk class (A/B/C) + routes it to a pool + └─▶ 2. Pool finances: advances HBAR + escrows the device-NFT + mints a claim NFT (the receipt) + └─▶ 3. Rewards stream in → NAV per share rises (amortized-cost accrual) + └─▶ 4a. Repaid in full → claim NFT burns, device-NFT returned + 4b. Default → NAV writes down, loss shared pro-rata, collateral retained +``` + +- **Pools** are standardized by **category × risk class** (e.g. `GPU-A`). The pool share is a + **NAV-appreciating** unit (ERC-4626-like): NAV rises only as *realized* reward spread is accreted + over each deal's term. `totalAssets` is derived (`idle cash + receivable`), which structurally kills + the classic double-count bug (deposit 100, advance 90, repay 100 must never read NAV 2.0). +- **Investors** exit any time: redeem at NAV (instant up to the liquidity buffer, remainder + FIFO-queued) or sell on a live **SaucerSwap** share/WHBAR market. +- The advance itself is a **locked transfer** released by the Hedera network on a schedule (HIP-1215) + — no off-chain keeper. ## Architecture ``` - operator ──finance/settle──▶ WaferVault.sol (Hedera EVM) ◀──deposit/redeem── investor - via @hiero-ledger/hiero-contracts: - front (Next.js + viem) ────▶ creates/holds HTS pool-share + reward-claim NFTs, - reads contract views native HBAR settlement, NAV, deposit/redeem/settle - + Mirror Node │ │ - ┌─────────▼──────┐ ┌─────────▼──────┐ - │ Hedera HTS │ │ SaucerSwap V1 │ share/WHBAR pool - └─────────┬──────┘ └────────────────┘ - │ reads - ┌─────────▼──────┐ - │ Mirror Node │──▶ frontend - └────────────────┘ + operator ──propose / escrow / route──▶┌──────────────────────────────────────┐◀──deposit / redeem── investor + │ WaferVault.sol (Hedera EVM) │ + admin ──approve / assign-class / finance──▶│ via @hiero-ledger/hiero-contracts: │ + │ • HTS pool-share (KYC+freeze+pause) │ + settler ──settleRewards (reward HBAR)─────▶│ • HTS claim-NFT (deal receipt) │ + │ • device-NFT escrow (collateral) │ + MockRewardSource (sim. cashflow) ─────────▶│ • amortized-cost NAV, deposit/redeem │ + │ • finance / settle / default / queue │ + React + Vite + viem ──reads/writes───────▶└──────┬───────────────────┬─────────────┘ + (deployed on Vercel) │ HTS @ 0x167 │ share / WHBAR + ┌──────────▼───────┐ ┌────────▼─────────┐ + │ Hedera Token Svc │ │ SaucerSwap V1 │ secondary market + │ Schedule Svc 0x16b│ └──────────────────┘ + └──────────┬───────┘ + │ logs / balances + ┌──────────▼───────┐ + │ Mirror Node │──▶ frontend feed + on-chain audit + └──────────────────┘ ``` -## Repo layout +The vault **is** the backend: all money logic lives on-chain and is verifiable. No HTTP API, no +database — contract events and the Hedera Mirror Node are the read/audit layer. + +## Hedera Token Service + +Wafer was built for the **🪙 Tokenization on Hedera** track: a tokenized fund share as a real-world +asset representation, with compliance and lifecycle management at the protocol level. + +| Qualification requirement | Status | +|---|---| +| Create / manage tokens via the **Hedera Token Service** (SDK or system contracts) | ✅ HTS system contracts (`0x167`) | +| Deployed & demonstrated on **Hedera Testnet** | ✅ `0x8Fb4439f…fb311` | +| Public GitHub repository | ✅ this repo | +| Contracts **verified** (HashScan / Sourcify) | ✅ Sourcify full match | +| ≤ 5-min demo: creation, configuration, a lifecycle operation | ✅ full lifecycle (finance → reward → NAV↑ → repaid/burn → default) | + +| Optional enhancement | Status | +|---|---| +| Use **`@hiero-ledger/hiero-contracts`** for HTS system-contract imports | ✅ | +| Compliance controls: KYC grants, account freeze, token pause | ✅ all three, enforced at the token level (5-key share: supply/kyc/freeze/wipe/pause) | +| Scheduled token operations (vesting/distributions) via Hedera Scheduled Transactions | ✅ HIP-1215: locked advance payout + maturity settlement, keeper-free | +| Custom fee schedules (fixed / fractional / royalty) | 🛣️ roadmap — *see note* | +| Cross-chain (LayerZero / CCIP / HashPort) | 🛣️ roadmap | +| Oracle integration (Chainlink / Pyth / Supra) | 🛣️ roadmap | + +> **On custom fees (a deliberate omission, not a miss):** on Hedera a fractional custom fee is +> assessed on every non-collector transfer and reverts `INVALID_ACCOUNT_ID` on a KYC-gated token, +> which breaks both `redeem` (operator→vault) and the SaucerSwap AMM. A compliant protocol take-rate +> therefore needs a permissioned-transfer design — on the roadmap rather than shipped broken. + +## What's live vs. roadmap + +Wafer is a **hackathon-stage product with production foundations in place**. The architecture, roles, +state machines, and on-chain primitives for the full system already exist; what remains for production +is wiring real-world data sources and decentralizing the trusted control surface. Nothing below is +faked — the hooks are in the contract and surfaced in the UI. + +### Live on Hedera Testnet today + +- **Tokenized fund share** — a real HTS fungible token, 8-decimal, treasury and keys held by the vault + contract (no off-chain signer), redeemable at NAV. +- **Compliance, enforced on-chain** — KYC grant/revoke, per-account freeze, and **token-level pause** + (`pauseToken`), all wired to the HTS token keys. +- **Full credit lifecycle** — proposal → approval → finance (HBAR advance + device-NFT escrow + + claim-NFT mint) → amortized-cost settlement → repaid (NFT burn) / default (NAV write-down). +- **Scheduled transactions (HIP-1215)** — the advance is locked and released by the network on a + schedule; reward settlement is network-scheduled. No keeper, no cron. +- **Liquidity** — deposit/redeem at NAV with an instant + FIFO-queue model, plus a live **SaucerSwap + V1** share/WHBAR secondary market enabled per pool in a single contract call. +- **Verifiable** — deployed and **Sourcify-verified**; the frontend reads NAV/pools/deals/activity + from the contract and the Mirror Node, and is deployed on Vercel. + +### Roadmap — designed, foundations in place, not yet productionized + +| Area | Foundation today | Production direction | +|---|---|---| +| **Deal underwriting & listing** | An **admin** reviews, risk-classes, and approves each deal before a pool finances it. This is a deliberate v1 control surface to demonstrate the lifecycle end-to-end — *intentionally* centralized for the demo, not an oversight. The on-chain role gating, risk-class enum, and approval state machine are already implemented. | Decentralize underwriting: delegated/permissionless underwriters, on-chain credit scoring from Mirror Node reward history, DAO-governed risk parameters — swapping the human admin for a trust-minimized process, **not** adding new primitives. | +| **KYC / identity** | KYC grant/freeze are real and enforced at the token level; the allowlist is currently admin-driven (`adminGrantKyc`). | Wire an identity/KYC provider (on-chain attestations) to drive the allowlist automatically. | +| **DePIN reward cashflow** | The reward stream is simulated on-chain by `MockRewardSource`; the **routing mechanism** (device-NFT escrow, Helium `recipient/destination` model) and the **settlement** (HIP-1215) are real. | Live per-network reward integrations (Helium / Render / io.net) + an HNT→HBAR bridge relayer (the one residual off-chain trust). | +| **Protocol fees** | None (deliberate — see HTS note above). | Compliant fractional/royalty fee via a permissioned-transfer token design. | +| **Pricing / cross-chain** | — | Oracle-priced NAV (Pyth/Supra) and cross-chain deposits (LayerZero/CCIP/HashPort). | + +The mental model: **the primitives are real and on-chain; productionization means replacing trusted +inputs (the admin, the simulated cashflow) with trust-minimized ones — not rebuilding the core.** + +## Tech stack + +- **Smart contract** — Solidity `0.8.24` on the Hedera Smart Contract Service (EVM, optimizer + `viaIR`). +- **Hedera services** — Hedera Token Service (`0x167`) and Schedule Service / HIP-1215 (`0x16b`) via + the [`@hiero-ledger/hiero-contracts`](https://www.npmjs.com/package/@hiero-ledger/hiero-contracts) + system-contract bindings. Settlement in **native HBAR** (8-dp tinybar). +- **Tooling** — Hardhat, `@openzeppelin/contracts` (Ownable2Step, ReentrancyGuard), Sourcify. +- **Frontend** — React 19 + Vite 6 + [viem](https://viem.sh/), MetaMask / EIP-6963, reading the + **Hedera Mirror Node**. No backend. Deployed on **Vercel**. +- **Secondary market** — SaucerSwap V1 (share/WHBAR). + +## Repository structure ``` -contracts/WaferVault.sol the vault — HTS tokens via @hiero-ledger/hiero-contracts, HBAR-settled -hardhat.config.cts Solidity 0.8.24 (optimizer + viaIR), network testnet (chain 296, Hashio) -tsconfig.hardhat.json CommonJS tsconfig for Hardhat (the repo is ESM) +contracts/ + WaferVault.sol the vault — HTS tokens + HIP-1215 scheduling, amortized-cost NAV, HBAR-settled + MockRewardSource.sol the only simulated piece — the DePIN reward stream (HIP-1215 self-settle) + MockDeviceNFT.sol demo device-NFT collection (operator collateral) scripts/ - deploy.ts deploy vault, create GPU-A pool (~100 HBAR), persist ids + verify hint - smoke.ts full lifecycle LIVE: finance → deposit → settle (NAV↑) → redeem, with links - resolve-operator.ts derive OPERATOR_ID from the key (Mirror Node) -test/vault-accounting.test.ts pure-logic NAV/amortized-cost mirror (incl. queue-NAV netting), `pnpm test` -test/vault-statemachine.test.ts pure-logic access/timelock/KYC/pause/secondary-ordering mirror -web/ Vite + React + viem — HBAR-wired; mock mode until VITE_VAULT_ADDRESS is set -deployments/testnet.json committed: vault + token + pool ids + HashScan/Sourcify links -docs/ONE-PAGER.md · docs/TRACKS.md · SPEC.md · CONTRIBUTING.md + deploy.ts deploy the stack + create the GPU-A pool, persist deployments/testnet.json + smoke.ts full lifecycle LIVE on testnet (finance → drip → NAV↑ → repaid/default) + smoke-hss.ts HIP-1215 LIVE: locked advance auto-release + scheduled settle + enable-secondary.ts fallback SaucerSwap enable flow + redeploy-mock.ts redeploy only MockRewardSource against the live vault + resolve-operator.ts derive the operator Hedera id from the key (Mirror Node) +test/ 78 pure-logic tests mirroring the contract's exact integer math +web/ React + Vite + viem frontend (deployed on Vercel) +deployments/testnet.json canonical on-chain addresses (the frontend auto-syncs from this) +SPEC.md · docs/ONE-PAGER.md · CONTRIBUTING.md technical spec + one-pager + contributor guide ``` -## Quick start +## Getting started + +Prerequisites: Node ≥ 22, `pnpm`, and a funded Hedera Testnet ECDSA account. ```bash pnpm install -cp .env.example .env # OPERATOR_ID/KEY already set — testnet HBAR funds everything +cp .env.example .env # set OPERATOR_ID / OPERATOR_KEY (testnet) -pnpm test # pure-logic NAV/units tests (no network) -pnpm run compile # hardhat compile (clean) -pnpm run deploy # deploy vault + GPU-A pool → deployments/testnet.json + VAULT_ADDRESS in .env -pnpm run verify # Sourcify (HashScan reads the verified contract from there) +pnpm test # 78 pure-logic tests (no network) +pnpm run compile # hardhat compile -pnpm run smoke # full lifecycle on testnet — watch NAV per share rise, then redeem +pnpm run deploy # deploy vault + GPU-A pool + mocks → deployments/testnet.json +pnpm run verify # Sourcify (chain 296) -cd web && pnpm install && pnpm dev # frontend (mock mode this increment) +pnpm run smoke # full lifecycle live: NAV 1.0 → 1.1, repaid/burn, then a default run +pnpm run smoke:hss # HIP-1215 live: locked advance + scheduled settle (keeper-free) ``` -Note: use `pnpm run deploy`, not `pnpm deploy` — the latter is shadowed by pnpm's built-in command. - -**HBAR.** Settlement is **native HBAR** — no USDC, no token association/allowance for settlement, -no faucet bridge. `createPool` does two HTS creates and forwards the full balance to each (excess -refunded), so attach ~100 HBAR; SaucerSwap pool ~$50 in testnet HBAR. The operator `0.0.9185964` -holds **~1000 testnet HBAR — sufficient** for the MVP. - -## Test the flow - -`pnpm run smoke` runs the full lifecycle live and prints HashScan links for every tx: it finances a -claim (advances HBAR), associates the share token, deposits HBAR, settles rewards (NAV rises), then -redeems at NAV. The current `deployments/testnet.json` records the live vault, tokens, and pool. -`web/` is now HBAR-wired (deposit is native-HBAR `payable`, 8-dp tinybar accounting, ABI matched to -the deployed contract). Set `VITE_VAULT_ADDRESS` to the deployed vault and the app reads NAV/pools/ -balances from the contract and the activity feed from the Mirror Node. - -### Test coverage — what `pnpm test` does and does NOT prove - -`pnpm test` is a **pure-logic mirror**, not on-chain bytecode coverage. Every money path in -`WaferVault.sol` calls the Hedera HTS system contract at `0x167` (mint/burn/transfer/grant-KYC/ -freeze/wipe), which has no local Hardhat EVM implementation, and this install has no -`ethers`/`hardhat-ethers` reachable from the test process — so the contract cannot be deployed and -called locally. The tests therefore re-implement the contract's exact integer arithmetic and -permissioning in BigInt (annotated `// CONTRACT:` per source line) and assert it against every SPEC -§5.3 worked example and §5.2 invariant — including the queue-NAV netting fix. **If the Solidity math -drifts from the mirror, the mirror cannot catch it**; the deployed-bytecode paths (HTS round-trips, -fee-exemption, KYC gating, device escrow/return, claim-NFT burn, secondary-market create) are proven -**LIVE on testnet by `pnpm run smoke`** (RUN A repaid + RUN B default, reading on-chain `navPerShare` -with HashScan links). Treat a green `pnpm test` as "the arithmetic is correct" and a green -`pnpm run smoke` as "the deployed contract executes it correctly" — you need both. +Run the frontend: + +```bash +cd web && pnpm install && pnpm dev # reads addresses from deployments/testnet.json +``` + +> Settlement is **native HBAR** — no USDC, no token association for settlement, no faucet bridge. +> `createPool` performs two HTS creates; attach ~100 HBAR (excess is refunded to the contract). + +## Deployed addresses + +Hedera Testnet (chain 296): + +| Contract / token | EVM address | Hedera ID | +|---|---|---| +| **WaferVault** | [`0x8Fb4439f…fb311`](https://hashscan.io/testnet/contract/0x8Fb4439f76ea7eAa6DcE88751A20981a796fb311) | `0.0.9250244` | +| Pool share (HTS, GPU-A) | `0x…008D25c5` | [`0.0.9250245`](https://hashscan.io/testnet/token/0.0.9250245) | +| Claim NFT (HTS) | `0x…008D25c6` | [`0.0.9250246`](https://hashscan.io/testnet/token/0.0.9250246) | +| SaucerSwap pair (share/WHBAR) | [`0x7E1aa858…B7Fd`](https://hashscan.io/testnet/contract/0x7E1aa858ff27549A77Fa7D9E1C1299c02672B7Fd) | — | + +The canonical, always-current set lives in [`deployments/testnet.json`](deployments/testnet.json). + +## Security & testing + +- **78 pure-logic tests** mirror the contract's exact integer math (NAV, queue netting, overflow + guards); the deployed-bytecode HTS/HSS round-trips are proven live via `pnpm run smoke` and + `pnpm run smoke:hss`. +- `ReentrancyGuard` + checks-effects-interactions on every value path; `settleRewards` gated by the + claim's settler set and capped at the expected repayment; `Ownable2Step` + a timelock on + finance/default; an operator allowlist; a dead-shares seed against first-depositor inflation; and + `int64` overflow guards at the HTS boundary. +- The codebase went through a two-phase adversarial review (multi-agent audit + independent + verification), with all findings fixed and re-deployed. ## License -MIT. +[MIT](LICENSE) © 2026 Wafer + +
+Built for ETHGlobal New York 2026 · Hedera — Tokenization track +
diff --git a/SPEC.md b/SPEC.md index fde59ed..d46ca0d 100644 --- a/SPEC.md +++ b/SPEC.md @@ -5,10 +5,9 @@ contract** on Hedera EVM (HSCS) creating/holding **HTS tokens** via `@hiero-ledg Scripts + frontend: TypeScript (viem + React). Settlement: **native HBAR**. Target track: **Hedera — Tokenization**. -> This spec is the build blueprint. **If it is implemented as written, the app ships.** The current -> deployed contract (`0xc452D2…cd0A`), `scripts/`, `test/`, and `web/` reference the OLD shape and -> must be re-generated per §12 (Migration). Nothing is mocked except the **DePIN reward cashflow** -> (§9), modeled on-chain by `MockRewardSource`. +> This spec is the build blueprint, implemented as written and **live on Hedera Testnet** +> (Sourcify-verified; canonical addresses in [`deployments/testnet.json`](deployments/testnet.json)). +> Nothing is mocked except the **DePIN reward cashflow** (§9), modeled on-chain by `MockRewardSource`. --- @@ -432,21 +431,15 @@ Gas override on HTS-touching calls (`gas ~1M`, `maxFeePerGas = liveBaseFee×5 + IHRC719 + SaucerSwap router), `format.js` (8dp/tinybar + weibar boundary), `mirror.js`, `errors.js`. The app reads live from the deployed `VITE_VAULT_ADDRESS` (no mock mode at ship). -## 12. Migration from the current contract - -The redesign **renames/splits** storage and changes signatures — enumerate so nothing breaks: -- `Pool.totalAssets` → **`idleTinybar` + `receivableTinybar`** (totalAssets becomes a view). -- `Claim.principalTinybar` → **`advance/expected/carry/settled/startTime/termSeconds`** (+ device - fields). New `Deal` struct + proposal flow. New `RedemptionRequest` queue. -- `financeClaim(poolId, operator, principal, meta)` → **`proposeDeal` + `approveDeal` + - `financeClaim(dealId)`** (term/expected now come from the Deal). -- `settleRewards` → amortized + gated + capped; `markDefault` writes down **carry, not principal**. -- New events (D12). New keys-exempt fee config. Ownable2Step + timelock + operator whitelist. -- **Re-generate:** `contracts/WaferVault.sol`, `contracts/MockRewardSource.sol`, - `contracts/MockDeviceNFT.sol`, `scripts/deploy.ts` (seed dead shares, register operator), - `scripts/smoke.ts` (propose→approve→finance→drip→repaid/burn; default run), `test/*` (see §13), - `web/lib/abi.js` + `config.js`, `deployments/testnet.json`. **Redeploy** — the old vault - `0xc452D2…cd0A` and its hbar-units tests (which certify the bug) are deprecated. +## 12. Design history (shipped) + +The shipped contract is the amortized-cost redesign described above. For the record, the key changes +from the earliest prototype were: `Pool.totalAssets` split into derived **`idleTinybar` + +`receivableTinybar`** (killing the double-count bug); a single `financeClaim(poolId, operator, +principal)` replaced by the **`proposeDeal` → `approveDeal` → `financeClaim(dealId)`** workflow; +`settleRewards` made amortized + gated + capped and `markDefault` writing down **carry, not +principal**; plus Ownable2Step, a timelock, the operator allowlist, and the redemption queue. All of +this is live and verified — see [`deployments/testnet.json`](deployments/testnet.json). ## 13. Testing (required before "ship") diff --git a/contracts/MockRewardSource.sol b/contracts/MockRewardSource.sol index f1a0aa8..c8e4ce5 100644 --- a/contracts/MockRewardSource.sol +++ b/contracts/MockRewardSource.sol @@ -23,9 +23,9 @@ interface IWaferVaultSettle { */ contract MockRewardSource is Ownable, ReentrancyGuard, HederaScheduleService { uint256 internal constant SUCCESS = uint256(int256(HederaResponseCodes.SUCCESS)); // 22 - // Gas budget for an HSS-scheduled scheduledDrip: it must cover the settle (~125k, plus the HTS - // burn/return on the repay interval) AND the reschedule (one scheduleCall ≈ 1.3M gas), so this is - // generously sized — a too-small limit reverts the whole scheduled tx (no settle, chain stops). + // Gas budget for the HSS-scheduled scheduledDrip (a single maturity settle): it must cover the + // settle plus the HTS claim-NFT burn + device-NFT return on the repay interval, so it is + // generously sized — a too-small limit would revert the whole scheduled tx and skip the settle. uint256 internal constant DRIP_GAS = 6_000_000; IWaferVaultSettle public immutable vault; @@ -43,7 +43,6 @@ contract MockRewardSource is Ownable, ReentrancyGuard, HederaScheduleService { } Schedule[] public schedules; - mapping(uint256 => bool) public selfScheduling; // scheduleId => on-chain HIP-1215 self-drip armed event Funded(uint256 indexed scheduleId, uint32 poolId, uint256 claimId, uint64 totalReward, uint64 startTime, uint64 termSeconds, uint32 dripCount); event Dripped(uint256 indexed scheduleId, uint32 intervalsReleased, uint64 amount); @@ -128,10 +127,6 @@ contract MockRewardSource is Ownable, ReentrancyGuard, HederaScheduleService { // HIP-1215 self-scheduling drip (no off-chain keeper / JS loop, SPEC §9) // ------------------------------------------------------------------------- - /// @notice Arm on-chain self-scheduling for a funded schedule: each interval's drip is scheduled - /// on-chain via the Hedera Schedule Service (HSS, 0x16b) and reschedules the next, until - /// the term completes — replacing the off-chain keeper / JS poll loop. The contract is the - /// schedule payer, so it must hold the prefunded reward HBAR (it does, from fund()). /// @notice Arm a keeper-free reward settlement via HSS: schedule ONE scheduledDrip at maturity /// (startTime + termSeconds) that releases the full reward in a single Hedera-scheduled /// transaction — no off-chain keeper, no JS loop. @@ -145,7 +140,6 @@ contract MockRewardSource is Ownable, ReentrancyGuard, HederaScheduleService { require(scheduleId < schedules.length, "NO_SCHEDULE"); Schedule storage s = schedules[scheduleId]; require(!s.defaulted, "DEFAULTED"); - selfScheduling[scheduleId] = true; uint64 at = s.startTime + s.termSeconds; // maturity: by now every interval is due if (at <= uint64(block.timestamp)) at = uint64(block.timestamp) + 3; diff --git a/contracts/WaferVault.sol b/contracts/WaferVault.sol index 8decfb2..cc69f92 100644 --- a/contracts/WaferVault.sol +++ b/contracts/WaferVault.sol @@ -5,7 +5,6 @@ import "@hiero-ledger/hiero-contracts/token-service/HederaTokenService.sol"; import "@hiero-ledger/hiero-contracts/token-service/IHederaTokenService.sol"; import "@hiero-ledger/hiero-contracts/token-service/KeyHelper.sol"; import "@hiero-ledger/hiero-contracts/token-service/ExpiryHelper.sol"; -import "@hiero-ledger/hiero-contracts/token-service/FeeHelper.sol"; import "@hiero-ledger/hiero-contracts/common/HederaResponseCodes.sol"; import "@hiero-ledger/hiero-contracts/schedule-service/HederaScheduleService.sol"; import "@openzeppelin/contracts/access/Ownable2Step.sol"; @@ -57,7 +56,7 @@ interface IShareApprove { * on HTS business errors (KYC not granted, frozen, ...). We therefore check responseCode == 22 * (SUCCESS) on EVERY HTS call and revert otherwise. */ -contract WaferVault is HederaTokenService, HederaScheduleService, KeyHelper, ExpiryHelper, FeeHelper, Ownable2Step, ReentrancyGuard { +contract WaferVault is HederaTokenService, HederaScheduleService, KeyHelper, ExpiryHelper, Ownable2Step, ReentrancyGuard { // --- constants ----------------------------------------------------------- uint256 internal constant ONE = 1e8; // 1.0 in tinybar / share micro-units (8 dp) int32 internal constant SHARE_DECIMALS = 8; @@ -86,7 +85,7 @@ contract WaferVault is HederaTokenService, HederaScheduleService, KeyHelper, Exp // Gas budget for the HIP-1215 scheduled `releaseAdvance` execution (one state write + one payout). uint256 internal constant RELEASE_ADVANCE_GAS = 400_000; - // HIP-1215 "locked virement": if advanceLockSeconds > 0, financeClaim LOCKS the advance in the + // HIP-1215 "locked transfer": if advanceLockSeconds > 0, financeClaim LOCKS the advance in the // vault and schedules a Hedera Schedule Service (HSS, 0x16b) call that auto-releases it to the // operator after the window — no keeper. 0 = pay the advance immediately at finance (default). uint64 public advanceLockSeconds = 0; @@ -647,7 +646,7 @@ contract WaferVault is HederaTokenService, HederaScheduleService, KeyHelper, Exp (bool ok, ) = payable(d.operator).call{value: d.advanceTinybar}(""); require(ok, "HBAR_ADVANCE_FAIL"); } else { - // HIP-1215 "locked virement": the advance stays in the vault (earmarked in + // HIP-1215 "locked transfer": the advance stays in the vault (earmarked in // pendingAdvanceTinybar) and an HSS-scheduled call auto-releases it at unlock time. uint64 unlockAt = uint64(block.timestamp) + advanceLockSeconds; advanceUnlockTime[claimId] = unlockAt; diff --git a/deployments/testnet.json b/deployments/testnet.json index 81ba3fb..0d34e6f 100644 --- a/deployments/testnet.json +++ b/deployments/testnet.json @@ -1,33 +1,33 @@ { "network": "testnet", "chainId": 296, - "createdAt": "2026-06-14T11:00:00.923Z", - "updatedAt": "2026-06-14T13:11:06.540Z", + "createdAt": "2026-06-16T07:07:39.493Z", + "updatedAt": "2026-06-16T07:07:39.493Z", "settlementAsset": "HBAR", "operator": "0xf6fAc89C3a2bAA468c78d3A638cA2F44F5fdBDbF", - "vaultAddress": "0x4B821d6bC76203C3C21131849C40d04C84bb75d5", - "vaultId": "0.0.9231166", + "vaultAddress": "0x8Fb4439f76ea7eAa6DcE88751A20981a796fb311", + "vaultId": "0.0.9250244", "pool": { "id": 0, "name": "Wafer GPU-A", "symbol": "wGPUA", "category": "GPU", "class": "A", - "shareTokenEvm": "0x00000000000000000000000000000000008cDB41", - "shareTokenId": "0.0.9231169", - "claimNftEvm": "0x00000000000000000000000000000000008CDB42", - "claimNftId": "0.0.9231170" + "shareTokenEvm": "0x00000000000000000000000000000000008D25c5", + "shareTokenId": "0.0.9250245", + "claimNftEvm": "0x00000000000000000000000000000000008D25c6", + "claimNftId": "0.0.9250246" }, "mocks": { "rewardSource": { - "evm": "0xe621ea918299E1ED5f69bB8c85D879F4E88aAa1f", - "id": "0.0.9232002" + "evm": "0x70059b045740600D735Ec69Fa37489d990209007", + "id": "0.0.9250248" }, "deviceNft": { - "evm": "0x2618837d4087726cdF67C1B6ee26f39df75Ef6B3", - "id": "0.0.9231175", - "collectionEvm": "0x00000000000000000000000000000000008cDB48", - "collectionId": "0.0.9231176" + "evm": "0xd40e1b1A7119d40426494262F391C239adC0C149", + "id": "0.0.9250249", + "collectionEvm": "0x00000000000000000000000000000000008D25CA", + "collectionId": "0.0.9250250" } }, "secondary": { @@ -36,14 +36,14 @@ "factory": "0x00000000000000000000000000000000000026e7" }, "hashscan": { - "vault": "https://hashscan.io/testnet/contract/0x4B821d6bC76203C3C21131849C40d04C84bb75d5", - "shareToken": "https://hashscan.io/testnet/token/0.0.9231169", - "claimNft": "https://hashscan.io/testnet/token/0.0.9231170", - "rewardSource": "https://hashscan.io/testnet/contract/0xe621ea918299E1ED5f69bB8c85D879F4E88aAa1f", - "deviceNft": "https://hashscan.io/testnet/contract/0x2618837d4087726cdF67C1B6ee26f39df75Ef6B3", - "deviceCollection": "https://hashscan.io/testnet/token/0.0.9231176", - "deployTx": "https://hashscan.io/testnet/transaction/0xf1026dc97e200697e6c991463c79e2a2487a5b45fd8a212a19d94c6484aa7074", - "createPoolTx": "https://hashscan.io/testnet/transaction/0xf0c31f601c5d739d846e96aa2bb32876a200b00afe1f8ac6e86d9c7b768456cd" + "vault": "https://hashscan.io/testnet/contract/0x8Fb4439f76ea7eAa6DcE88751A20981a796fb311", + "shareToken": "https://hashscan.io/testnet/token/0.0.9250245", + "claimNft": "https://hashscan.io/testnet/token/0.0.9250246", + "rewardSource": "https://hashscan.io/testnet/contract/0x70059b045740600D735Ec69Fa37489d990209007", + "deviceNft": "https://hashscan.io/testnet/contract/0xd40e1b1A7119d40426494262F391C239adC0C149", + "deviceCollection": "https://hashscan.io/testnet/token/0.0.9250250", + "deployTx": "https://hashscan.io/testnet/transaction/0xceabb69cf6256101ab0e78f58feeb2bdb0e6a45a9e2fb4713c7b122343c3a8b1", + "createPoolTx": "https://hashscan.io/testnet/transaction/0xcf2173af23e2e83eb3bd49c86fdd1dee0f521241720ef29fe0d2378c9cda5e19" }, - "sourcify": "https://repo.sourcify.dev/contracts/full_match/296/0x4B821d6bC76203C3C21131849C40d04C84bb75d5/" + "sourcify": "https://repo.sourcify.dev/contracts/full_match/296/0x8Fb4439f76ea7eAa6DcE88751A20981a796fb311/" } diff --git a/docs/AUDIT.md b/docs/AUDIT.md deleted file mode 100644 index b606c57..0000000 --- a/docs/AUDIT.md +++ /dev/null @@ -1,235 +0,0 @@ -# Wafer — Code Audit (pre-presentation) - -Date: 2026-06-14 · Network: Hedera Testnet (chain 296) · Commit baseline: `7ffa715` -Method: 8-dimension multi-agent audit (accounting · security · spec-matrix · mocks/scripts · -frontend-wiring · UX-flow · live-on-chain · track-fit), each finding **adversarially -re-verified** against the source, plus a **live Mirror Node** check of the deployed state, plus a -first-hand read of `WaferVault.sol`, both mocks, and `smoke.ts`. - ---- - -## 0. Headline verdict - -**The front and the back are fundamentally sound and the demo flow is real.** Specifics: - -- **Every live transaction passes.** The Mirror Node confirms all **21** of the vault's recent - contract calls returned `SUCCESS` (0 failures), and the three headline txs (deploy, `createPool`, - `enableSecondaryMarket`) all landed `status 0x1`. The share token, claim NFT, and the SaucerSwap - pair exist and the pair genuinely **holds reserves** (2.0 wGPUA + WHBAR + LP). The deployed token - keys all resolve to the vault. -- **The core accounting is provably correct.** All three SPEC §5.3 worked examples (single deal, - 2-deal blend, default) re-derive to the digit from the actual integer Solidity; invariants - I2/I3/I4/I6/I7/I10 hold; the BigInt mirror in `vault-accounting.test.ts` matches the Solidity - line-for-line (no arithmetic drift). `pnpm test`: **70 passing**. Compile: clean. -- **No blocker.** Nothing in the demo path bricks or loses funds. The one **High** is an edge-case - overflow guard reachable only in an *already-impaired* pool with a *near-uint64-max* deposit — not - the demo. Everything else is **Medium/Low polish, doc drift, or track-fit**. - -So: this is a strong base to present. The work below is hardening + credibility + the Hedera -"scheduled transfers" upgrade — not firefighting. - ---- - -## 1. Live on-chain verification (proof the tx pass) - -| Artifact | Hedera id | Status | -|---|---|---| -| Vault contract | `0.0.9228634` | live, not deleted | -| Share token (wGPUA) | `0.0.9228636` | FUNGIBLE_COMMON, 8dp, treasury=vault, keys=vault | -| Claim NFT | `0.0.9228637` | NON_FUNGIBLE_UNIQUE, supply+wipe=vault | -| SaucerSwap pair | `0.0.9228672` (`0x22CD…426B1`) | live, holds 2.0 wGPUA + WHBAR + LP `0.0.9228673` | -| deploy / createPool / enableSecondary txs | — | all `SUCCESS` (0x1) | -| Vault recent calls | — | **21/21 SUCCESS, 0 failed** | -| Operator `0.0.9185964` | `0xf6fAc…BDbF` | live, ~464 HBAR | - -One reality check it surfaced: the **share token has no `wipe` and no `pause` key on-chain** -(`wipe_key: null`, `pause_key: null`) — code and chain agree (only SUPPLY/KYC/FREEZE are set); it's -the **spec that overstates** the key set. See M1. - ---- - -## 2. Findings (consolidated, deduped, post-verification severity) - -Severities are the **adversarially-corrected** ones. Where N agents found the same thing, it's one -row. - -### HIGH - -**H1 — `deposit` mints a uint256-derived share amount but truncates to `int64` with no guard -(overflow/accounting divergence in an impaired pool).** -`WaferVault.sol:705-726`. The only guard is `require(msg.value <= type(uint64).max)`. But -`sharesMinted = assets * totalShares / netAssets` is unbounded: after a `markDefault` writes the -receivable down (NAV < 1, `netAssets << totalShares`), a single legal deposit (≤ uint64.max tinybar) -can compute `sharesMinted > type(int64).max`. `p.totalShares += sharesMinted` then credits the full -uint256 while `mintToken(int64(uint64(sharesMinted)))` / `transferToken(...)` mint the truncated (or -sign-wrapped **negative**) amount → internal accounting permanently diverges from HTS supply, -corrupting `navPerShare` for everyone. `redeem` *does* guard (`shares <= uint64.max`) — `deposit` is -asymmetric. Found independently by the accounting **and** security agents. -**Fix (1 line):** after computing `sharesMinted`, add -`require(sharesMinted <= uint256(uint64(type(int64).max)), "SHARES_OVERFLOW");` -and switch the universal HTS-amount ceiling from `uint64.max` to `int64.max` at `redeem:745`, -`enableSecondaryMarket:847`, and the `msg.value` guards (HTS amounts are `int64`). - -### MEDIUM - -**M1 — `pausePool` is a storage flag, not a real HTS pause; share token also lacks a `wipe` key.** -(SPEC-1 / SEC-3 / LIVE-1 / LIVE-2 / HED-2 — 5 agents + live-confirmed.) `_createShareToken:395-398` -sets only SUPPLY/KYC/FREEZE. `pausePool/unpausePool` flip `Pool.status` (gating -deposit/redeem/claimRedemption) but never call HTS `pauseToken`, so **shares stay tradeable on -SaucerSwap and peer-to-peer while a pool is "paused."** D10/§8/ONE-PAGER advertise pause as a "real -compliance lever" and list `wipe`/`fee_schedule`/`pause` keys that don't exist. KYC + per-account -`freeze` *are* real on-chain. **Fix:** either add the PAUSE (and WIPE) key and call -`pauseToken/unpauseToken`, **or** downgrade the SPEC/ONE-PAGER claims to "pool-status gate only." Pick -one; make code and docs agree. - -**M2 — The "0.10% fee" ghost: dead constants, stale comments, and tests that assert a fee the token -doesn't have.** (SEC-2 / FE-4 / HED-3 / ACC-5.) Ground truth: `_createShareToken` calls -`createFungibleTokenWithCustomFees` with **empty** fee arrays (`:416-417`) → no fee, per D11. But -`FEE_NUMERATOR/DENOMINATOR` (`:74-75`) are dead, comments at `:121/:344/:735` describe a 0.10% fee, -`ONE-PAGER.md:59` advertises one, and **`vault-statemachine.test.ts:565` + `vault-accounting.test.ts:724-748` -assert live-fee behavior + a secondary-market step sequence (`grantKyc:router` → -`addLiquidityETHNewPool`) the contract does NOT implement.** The tests certify fiction. **Fix:** delete -the dead constants + fee comments; rewrite/delete the fee tests; fix the secondary-step assertion to -the real `createPair → grantKyc(pair) → addLiquidityETH` sequence. (Credibility: a judge who reads -the code will catch the mismatch.) - -**M3 — `financeClaim` ignores the senior `queuedShares` earmark and can strand queued redeemers.** -`WaferVault.sol:543`. It checks raw `idle >= advance`, but `idle` may already owe HBAR to the senior -redemption queue (everywhere else, `_liquidAssets` treats only `idle - queuedShares` as free). Admin -can finance a deal that drains the queue's backing into an illiquid receivable; `claimRedemption` -(needs `idle >= owed`) then can't pay the senior redeemers until rewards settle. **Fix:** -`require((p.idleTinybar - p.queuedShares) >= d.advanceTinybar)`. - -**M4 — A queued redemption can become structurally unpayable after a default; `netAssets` silently -clamps to 0.** `WaferVault.sol:953-956, 792-812`. If the receivable backing a queued claim is wiped -by `markDefault`, `_netAssets` clamps to 0 (no event) and the senior queued HBAR can never be paid -while juniors also go to 0. **Fix:** surface the condition (event/view) or pro-rata haircut the queue -on default. - -**M5 — The Secondary (SaucerSwap exit) screen is fully built but never mounted — and `DEMO.md` -promises it.** (FE-1 / UX-1.) `Secondary.jsx` (live pair, reserves, in-app buy, deep link) is never -imported; `App.jsx` has no route, `Explore`'s sub-tabs are only pools/deals/activity, and `TopNav` -even carries a stale "Explore absorbs … secondary" comment for a tab that doesn't exist. DEMO.md -Option B **step 6** tells you to show it. **Fix (one import + render):** add a `secondary` sub-tab in -`Explore.jsx` rendering `` (props -already available). - -**M6 — The `HowItWorks` explainer is built but never rendered; the landing has no priming.** (UX-2 — -this is your "flow not intuitive" pain point.) The disconnected landing renders only `Hero` (dense -copy); the 3-step `HowItWorks.jsx` component is orphaned. **Fix:** render `` -under `Hero` in `App.jsx`. - -**M7 — `smoke.ts` swallows the default-run and secondary-market failures and still exits 0.** -(MSR-1.) Both are SPEC §15 ship requirements; if either reverts live, the run prints a warning but -reports green. **Fix:** `process.exitCode = 1` (or a loud banner) when `defaultRan`/`secondaryOk` are -false. - -**M8 — Timelock is inert by default (`timelockDelay = 0`).** (SPEC-2 / SEC-5.) D9 sells -`financeClaim`/`markDefault` as timelocked, but they execute immediately until `setTimelockDelay(>0)` -is called. **Fix:** set a non-zero default in the constructor or in `deploy.ts`, or document that 0 = -no timelock (fine for the demo, but say so). - -### LOW / polish - -- **L1 (ACC-6)** Tiny holders can't `redeem` when NAV<1 (`ZERO_ASSETS` floor) — secondary exit - exists; document it. -- **L2 (UX-3)** KYC dead-ends with no guided path; the single-key demoer must switch to **Admin** and - self-allowlist. Add an in-context "get allowlisted" hint / persona cue. -- **L3 (MSR-2)** Repeated `smoke` runs can drain `idle` (deposit skipped, fresh deal each run) → - `INSUFFICIENT_IDLE`. Clean deploy+smoke is fine; top-up or skip-if-active-claim for reruns. -- **L4 (MSR-3)** Over-funding a schedule (`reward > expected`) would brick its drips - (`CLAIM_NOT_ACTIVE`). Not triggered (smoke sizes `reward == expected`). Cap drip at remaining. -- **L5 (MSR-6)** Drip deadline is tight and uses `Date.now()` vs on-chain `block.timestamp`; the - Repaid/burn step may not complete (NAV proof still holds). Read `claim.startTime`; widen deadline. -- **L6 (SEC-6)** `enableSecondaryMarket` ignores `addLiquidityETH` returns, leaves a router allowance - + untracked treasury shares. Capture returns; reset allowance. -- **L7 (FE-2)** Deposit **MAX** sets the full HBAR balance → guaranteed gas-shortfall revert. Reserve - ~1–2 HBAR headroom on the deposit tab. -- **L8 (`createPool` over-funding — own finding)** `createPool` forwards the full attached balance to - both HTS creates; only `DEAD_SEED_TINYBAR` (0.00001 HBAR) is recorded as `idle`. The create-refund - surplus (~most of the ~100 HBAR you attach) accrues to `address(this).balance` **untracked, with no - owner `sweep`/`withdraw` function** — solvency-safe (extra cushion) but unrecoverable admin HBAR. - Add an `ownerWithdrawSurplus()` (balance − Σ pool idle) for prod, and attach less in the demo. -- **L9 (SPEC-6)** Post-default recoveries can't route via `settleRewards` (it's `Active`-only), - contradicting §9's "keeper routes proceeds back via settleRewards." Doc it or add a recovery entry. -- **L10 (UX-5)** NAV shown bare (`1.0000`) with no ">1.0 = profit" anchor; orphan - components (`Sidebar`/`Dashboard`/`CardNav`); unsignposted operator/admin steps. - -### INFO / doc-only - -- **§5.1 vs §5.2:** §5.1's formulas use GROSS `idle+receivable`; §5.2 I10 (and the code) use NET - `idle+receivable-queuedShares`. Code is right; §5.1 is stale. -- **§4.1 drift:** `queueHead` is listed but doesn't exist; `RedemptionRequest.shares` is actually - `assetsTinybar`; `Pool.queuedShares` stores **tinybar**, not shares (misleading name). Rename → - `queuedAssetsTinybar` to prevent a future edit mixing it with `totalShares`. -- **`docs/TRACKS.md`** describes a "pure-HTS / no-Solidity architecture" that contradicts the actual - 50 KB Solidity vault — scrub before a judge reads it. -- **D6** says class lives "on Pool + Claim"; the Claim carries neither (derivable via pool). Amend to - "Pool + Deal." - ---- - -## 3. Spec compliance (SPEC.md) - -Near-complete. **Every §7 surface function and event is present with the correct access control and -signature; all §4.1 structs/enums/mappings exist; D1–D13 are reflected in code.** Genuine deviations, -all captured above: - -- **§8 share-token keys:** 3 of 6 set (SUPPLY/KYC/FREEZE; no WIPE/FEE_SCHEDULE/PAUSE) → M1. -- **D10 pause / D9 timelock:** weaker than advertised → M1, M8. -- **§4.1 storage names / §5.1 formulas / D6:** doc drift (INFO). -- **§11 Secondary screen:** built, unmounted → M5. - -`§15` IN-scope items all have a working code surface (vault, mocks, secondary market, freeze, operator -whitelist, timelock, native-HBAR settlement, live + verified). Pause and the Secondary screen are the -two "partial" items. - ---- - -## 4. Hedera track fit + the "lock des virements" opportunity - -Target: **Tokenization on Hedera** ($3k) + main Hedera ($15k). - -**Real, code-backed:** vault-keyed HTS fungible pool-share, claim-NFT receipt, native-HBAR -settlement, amortized-cost NAV, SaucerSwap secondary — and **two genuine compliance levers**: -KYC-gated transfers and per-account freeze (both HTS-enforced, keyed to the vault). That's a solid -Tokenization story. - -**Not met / overclaimed:** custom fee schedule (removed by design, D11 — but docs still imply one → -M2); pause as a *token-level* lever (M1); oracles + cross-chain (roadmap, honestly scoped OUT). - -**The "lock des virements" = real Hedera Scheduled Transactions (HSS / HIP-1215).** Today **none** is -used: the finance/default timelock is a pure EVM `block.timestamp` gate, and the reward "drip" is a -Solidity array advanced by an **off-chain JS poll loop** (`smoke.ts:239`) — which is exactly the -keeper anti-pattern the Automation framing penalizes. The dependency **already vendors** -`HederaScheduleService.sol` / `IHRC1215.sol` (HSS `0x16b`), import-ready and unreferenced. Highest-EV -additions, in order: - -1. **Schedule the advance payout** at `financeClaim` as a programmed/locked native transfer → a - visceral "locked virement" you can show executing on HashScan. -2. **Self-scheduling `settleRewards`** via HIP-1215 (no off-chain keeper) → removes the JS poll loop - and strengthens the "no keepers" automation narrative. -3. **Schedule `markDefault`** at timelock expiry. - -(Track-attribution note: the standalone "Autonomous On-Chain Automation" track is Continuity-only per -`TRACKS.md`; the scheduled-transactions value lands on the **Tokenization / No-Solidity bonus** lists, -not a fresh track.) - ---- - -## 5. Prioritized action list - -**Credibility (do before judges read the code — ~1h, zero risk):** M2 (scrub the fee ghost + -stale tests), the §4.1/§5.1/TRACKS doc drift, and decide M1's framing (fix the key or fix the claim). - -**Demo completeness (make "retrace from start to finish" airtight — ~half day):** M5 (mount -Secondary), M6 (render HowItWorks), M7 (smoke fails loudly), L2 (KYC guidance), L7 (deposit MAX -headroom), L10 (NAV anchor). - -**Correctness hardening (~1–2h):** H1 (deposit overflow guard — trivial, do it), M3 (finance respects -the queue earmark), M8 (timelock default). M4/L-series as time allows. - -**Track upside (the "lock des virements" feature — ~0.5–1.5 days):** HSS scheduled advance payout -(#1), then self-scheduling settle (#2). Highest judge-impact item on the board. - - diff --git a/docs/DEMO.md b/docs/DEMO.md deleted file mode 100644 index 3ffc231..0000000 --- a/docs/DEMO.md +++ /dev/null @@ -1,130 +0,0 @@ -# Wafer — Runbook de présentation (ETHGlobal NY 2026) - -Guide pour pitcher Wafer aux juges Hedera. Track visée : Hedera — Tokenization. -Tout ce qui est ci-dessous est live sur Hedera Testnet (chain 296) et prouvé on-chain. - ---- - -## 0. À faire 5 min avant de passer - -- `cd web && pnpm dev` → l'app lit l'état live du vault déployé. -- MetaMask sur Hedera Testnet (chain 296), compte = l'operator `0xf6fAc89C…` (admin + investisseur dans la démo). -- Onglets ouverts : HashScan du vault, Sourcify (vérifié), l'app. -- Optionnel (preuve live béton) : un terminal prêt à lancer `pnpm run smoke` (≈ 4 min, ~20 HBAR). -- Balance testnet : garder > 100 HBAR (le faucet recharge à 1000/jour ; un smoke complet coûte ~20 HBAR, activer un nouveau marché secondaire ~30 HBAR). - ---- - -## 1. Le pitch (ce que tu dis) - -30 secondes : -> "DePIN operators buy hardware today and earn protocol rewards on-chain over months. Wafer is the liquidity layer for that gap: investors fund a pool, the pool advances HBAR to a DePIN operator against its future on-chain rewards, and those rewards stream back into the pool — NAV rises live. It's Centrifuge/Maple, specialized for DePIN, fully on Hedera with HTS + a Solidity vault." - -Le « wow », à dire clairement : -> "DePIN is the one RWA category whose cashflow is natively on-chain — no invoice, no bank, no fiat bridge. Repayment needs no trust in a human paying: the operator routes its on-chain reward stream to the vault, and you watch NAV tick up live." - ---- - -## 2. Le déroulé de démo (2 options — fais les deux si tu as le temps) - -### Option A — preuve live au terminal (`pnpm run smoke`) -C'est le plus fort : chaque étape émet une vraie tx avec un lien HashScan. Commente en direct : - -1. deposit 10 HBAR → parts au NAV 1.0000. -2. proposeDeal → approveDeal (classe A, pool GPU-A) → financeClaim : avance 9 HBAR + escrow du device-NFT + mint du claim-NFT. Pointe : "NAV stays FLAT at finance — drift 0 tinybar. No double-counting." -3. drip loop (MockRewardSource) : NAV monte **1.0000 → 1.0600 → 1.0933 → 1.1000**, monotone, jamais 2.0. À repaid : le claim-NFT **burn**. -4. RUN défaut : un 2e deal, drip partiel, puis markDefault → **NAV écrit en baisse 1.16 → 0.80** (perte partagée pro-rata), collatéral wipe. - -Phrase clé : *"Every number you see, I read back from the contract on-chain. The old double-count bug — deposit 100, advance 90, repay 100 showing NAV 2.0 — is dead: totalAssets is derived (idle + receivable), finance just moves cash to a receivable, and only the realized spread accretes into NAV."* - -### Option B — l'app (visuel) -1. Pools / Fund a category : montre GPU-A, son NAV, sa TVL, et les deals listés dessous (lus via Mirror Node). -2. Deposit : associate (IHRC719) → l'admin t'a allowlisté (KYC) → deposit en HBAR natif → tes parts apparaissent. -3. Pool detail : NAV, deals (repaid / defaulted), liquidité idle vs déployée, file de redemption. -4. Redeem : approve + redeem au NAV (sortie garantie). Montre `maxRedeem` et la file si le cash idle ne suffit pas. -5. Operator portal : proposeDeal + escrow device-NFT. Admin : review + assign class, finance, markDefault (timelock), allowlist KYC, pause/freeze. -6. Secondary : la **pair SaucerSwap share/WHBAR live** (avec liquidité) — sortie au prix marché en plus du redeem. - -### Option C — le "lock des virements" HIP-1215 (`pnpm run smoke:hss`) — le différenciateur -Le plus fort techniquement : des **transactions Hedera programmées (HIP-1215, 0x16b)**, exécutées par -le réseau **sans keeper**. Commente en direct : - -1. **Avance verrouillée** : `setAdvanceLock(10s)` puis `financeClaim` → l'avance N'est PAS versée tout - de suite, elle est **lockée dans le vault** et une *scheduled transaction* est créée - (`AdvanceScheduled`, visible sur HashScan). 10s plus tard, le **réseau** exécute `releaseAdvance` - et l'opérateur est payé — *« no keeper, no cron : c'est le ledger qui débloque le virement »*. -2. **Settle auto-schedulé** : `armSelfDrip` programme le règlement des rewards à maturité ; tu - **attends**, et le NAV monte tout seul quand la scheduled tx s'exécute — aucune boucle off-chain. - -Phrase clé : *« The advance is a locked transfer the network itself releases on a schedule, and the -reward settlement is a scheduled transaction too — there is no bot, no keeper, no cron anywhere. »* -Honnêteté à dire : HIP-1215 interdit la récursion de scheduling, donc le settle est **un** virement -programmé à maturité (le drip par intervalle reste dispo en manuel) — assume-le, c'est une vraie -contrainte réseau bien gérée. - ---- - -## 3. Ce qui est réel vs simulé (à dire, ça inspire confiance) - -Réel et on-chain (testnet) : -- Le vault `WaferVault` (Solidity/HSCS), Sourcify "perfect" verified. -- Les tokens HTS : part de pool fongible (KYC-gated) + claim-NFT (reçu, burn au repaid). -- Toute la compta amortized-cost, deposit/redeem, file de redemption, finance/escrow, défaut. -- Le marché secondaire : vraie pair SaucerSwap V1 share/WHBAR, KYC-enabled, avec liquidité seedée. - -Simulé (assumé, et c'est le SEUL mock) : -- Le **cashflow DePIN** : `MockRewardSource` drip des HBAR dans le vault via `settleRewards`, à la place du flux de rewards qu'un opérateur Helium router­ait après bridge. En prod : escrow du device-NFT (Helium recipient/destination) + keeper HIP-1215 + relayer de bridge HNT→HBAR. - -Dis-le franchement : *"The only thing we mock is the DePIN reward cashflow itself — we can't wire a live Helium reward stream in a hackathon. The routing mechanism (device-NFT escrow) and everything else is real on-chain logic."* - ---- - -## 4. Pourquoi Hedera (track Tokenization) - -- HTS natif : part de pool fongible **KYC-gated** (compliance), claim-NFT, le tout piloté par le contrat (clés CONTRACT_ID, aucun signataire off-chain). -- Settlement en **HBAR natif** (tinybar 8dp), pas de pont fiat. -- Frais bas + finalité ~3s → on peut driper les rewards et voir le NAV bouger en live. -- Bonus track cochés : **compliance réelle** (KYC + freeze + **pause au niveau token HTS** — la part a 5 clés supply/kyc/freeze/wipe/pause), Mirror Node pour le read/audit, secondaire SaucerSwap, et — nouveau — **Scheduled Transactions HIP-1215 (0x16b) LIVE** : l'avance est un *virement verrouillé* auto-libéré par le réseau, et le settle des rewards est auto-schedulé — **sans keeper**. - ---- - -## 5. Liens live (à montrer) — déploiement 2026-06-14 - -- Vault : https://hashscan.io/testnet/contract/0x4B821d6bC76203C3C21131849C40d04C84bb75d5 -- Sourcify (verified) : https://repo.sourcify.dev/contracts/full_match/296/0x4B821d6bC76203C3C21131849C40d04C84bb75d5/ -- Part de pool (HTS, 5 clés : supply/kyc/freeze/wipe/pause) : https://hashscan.io/testnet/token/0.0.9231169 · Claim-NFT : https://hashscan.io/testnet/token/0.0.9231170 -- Pair SaucerSwap share/WHBAR : https://hashscan.io/testnet/contract/0x4B6dEAcA611177F74433A57A3bF6f9b1b95BC182 -- Vault id `0.0.9231166` · operator `0.0.9185964` (`0xf6fAc89C…`) -- **Adresses canoniques : `deployments/testnet.json`** (le front se synchronise automatiquement dessus). -- One-pager : `docs/ONE-PAGER.md` (EN) / `docs/ONE-PAGER.fr.md` (FR) · Spec : `SPEC.md` - ---- - -## 6. Q&A juges — réponses prêtes - -- "Comment l'opérateur rembourse vraiment ?" → Escrow du device-NFT : qui contrôle le NFT contrôle le flux de rewards (modèle Helium recipient/destination). En démo, `MockRewardSource` le simule ; en prod, un relayer bridge HNT→HBAR et appelle `settleRewards` (cadence via HIP-1215). La seule confiance résiduelle = la custody du HBAR bridgé. -- "APR par deal vs par pool ?" → L'APR est par deal (avance/attendu/terme). Le NAV de la pool est le **blend réalisé** de tous ses deals, accru en amortized-cost ; la classe de risque est la curation risque+rendement de l'admin. -- "Et si un opérateur fait défaut ?" → markDefault écrit le carry restant en baisse → le NAV baisse, perte partagée pro-rata ; le device-NFT en escrow est retenu/liquidé. Démo : NAV 1.16 → 0.80. -- "C'est un fonds — la part est-elle tokenisée et transférable ?" → Oui, token HTS fongible KYC-gated, redeem au NAV à tout moment + marché secondaire SaucerSwap. -- "Sécurité ?" → ReentrancyGuard + CEI partout, settleRewards gated par claim + plafonné à `expected`, Ownable2Step + timelock sur finance/default, whitelist opérateurs, seed de dead-shares anti-inflation, accounting uint256. Une revue adverse a été passée et corrigée. - ---- - -## 7. Limites connues (assume-les, ne les cache pas) - -- La part de pool est un **token HTS sans custom fee** : sur Hedera une fee fractionnaire est assessée à chaque transfert non-collector et casse redeem + l'AMM (`INVALID_ACCOUNT_ID`). Une fee compliant demanderait un design de transfert permissionné → roadmap. -- Le marché secondaire s'active en **un seul appel contrat** `enableSecondaryMarket` (createPair → grantKyc(pair) → mint+approve → addLiquidityETH, tient dans le cap 15M gas de Hedera). `scripts/enable-secondary.ts` reste dispo comme fallback équivalent. Le KYC du token rend l'ajout de liquidité non-trivial (il faut KYC la pair avant le seed) — c'est géré, mais c'est une vraie particularité RWA à expliquer. -- Démo single-key : l'operator joue investisseur + opérateur + admin. En prod : owner = multisig (Ofelia/Safe) + relayer dédié + opérateurs distincts. -- Budget testnet : 1000 HBAR/jour (faucet), donc montants de démo volontairement bas (deposit 10, avance 9, etc.). - ---- - -## 8. Relancer une démo propre (si besoin) - -```bash -pnpm run deploy # vault + pool GPU-A + mocks, écrit deployments/testnet.json + .env (~150 HBAR) -pnpm run verify # Sourcify -pnpm run smoke # lifecycle complet live + secondaire one-call (deposit→finance→drip→repaid/burn→default→SaucerSwap) -# le smoke active déjà le secondaire ; pnpm run enable-secondary existe en fallback (~30 HBAR) -``` -Le front lit `deployments/testnet.json` → les nouvelles adresses se propagent automatiquement. diff --git a/docs/ONE-PAGER.fr.md b/docs/ONE-PAGER.fr.md deleted file mode 100644 index 71e6896..0000000 --- a/docs/ONE-PAGER.fr.md +++ /dev/null @@ -1,82 +0,0 @@ -# Wafer - -> Liquidité InfraFi pour le DePIN, sur Hedera. Du capital investisseur poolé finance les futurs -> rewards on-chain des opérateurs DePIN contre des HBAR immédiats ; les investisseurs détiennent une -> part de pool KYC-gated qui s'apprécie au NAV — un fonds de crédit court terme tokenisé. -> Hedera Testnet (chain 296) · track visée : **Hedera Tokenization** · EN : [`ONE-PAGER.md`](ONE-PAGER.md). - -## Problème - -Les opérateurs DePIN (GPU/compute, wireless, mapping, énergie, stockage) doivent acheter du hardware -**aujourd'hui** pour gagner des rewards protocolaires sur **plusieurs mois**. Ce décalage de -trésorerie, c'est du capital qu'ils n'ont pas — et le crédit classique ne sait pas adosser un flux -de rewards on-chain. - -## Ce qu'est Wafer - -Une **couche de financement, pas un opérateur.** Wafer ne fait jamais tourner de nodes et ne prend -aucune position dans les réseaux DePIN. Les opérateurs qui gagnent **déjà** des rewards on-chain -viennent chez Wafer pour du cash immédiat contre ces rewards futurs ; les investisseurs fournissent -ce cash via des pools et en touchent le rendement. Pensez Centrifuge / Maple, spécialisé pour les -flux de rewards DePIN. - -## Deux faces - -- **Investisseurs** : financent une **pool** (catégorie × classe de risque, ex. `GPU-A`) et - reçoivent une **part de pool** fongible, **KYC-gated**, qui s'apprécie au NAV. Exposition - **diversifiée sur tous les deals de la pool** — on achète la pool, pas un deal. Redeem au NAV. -- **Opérateurs** : proposent un **deal** (entreprise, description, avance, remboursement, maturité, - catégorie). Un **admin** review et attribue une **classe de risque** (en pesant risque *et* APR - proposé) → la pool correspondante **finance** : avance des HBAR + mint un **NFT de créance** - détenu par le vault. - -## Pourquoi le wow, c'est DePIN - -DePIN est la seule catégorie RWA dont le cashflow est **nativement on-chain** : le hardware gagne -des rewards automatiquement sur une adresse on-chain — pas de facture, pas de banque, pas de pont -fiat. Le remboursement **n'exige aucune confiance dans un humain qui paie** : l'opérateur **route -son flux de rewards vers le vault** pour la durée (redirection d'adresse de payout, escrow du -device-NFT, ou keeper autorisé), et **le NAV monte en live** à mesure que les rewards tombent. Tout -dans Wafer est de la vraie logique on-chain ; **seul le cashflow de rewards de l'opérateur est -simulé** en démo (une source de rewards de substitution) — le mécanisme de routage est réel, on ne -peut juste pas brancher un réseau DePIN live pendant un hackathon. - -## Économie - -Chaque deal porte sa propre avance / remboursement attendu / maturité → son **propre APR**. Le NAV -de la pool est le **rendement réalisé et blended** de tous ses deals (moins les défauts), accru en -**amortized-cost** — les écarts d'APR par deal deviennent un seul rendement de pool diversifié. La -classe de risque est la **curation risque-et-rendement** de l'admin, pour que chaque pool ait un -profil cohérent. - -## Cycle de vie - -1. L'opérateur propose un deal. -2. L'admin review + attribue une classe → route vers la pool correspondante. -3. La pool **finance** : avance des HBAR, mint le NFT de créance — le **reçu on-chain** (l'état - économique vit dans le contrat ; l'affichage off-chain via Mirror Node). -4. Les rewards **arrivent** → le NAV de la pool monte vers le remboursement attendu. -5. Remboursement complet → le **NFT burn**. Défaut → **write-down** (le NAV baisse), perte partagée - sur la pool. -6. Les investisseurs détiennent / **redeem au NAV** en continu. - -## Stack Hedera - -- **Token de parts HTS fongible** (clés KYC + freeze détenues par le vault, petite fee fractionnaire) — l'unité de fonds tokenisée. -- **NFT de créance HTS** — le reçu on-chain de chaque deal financé, détenu par le vault, burn à maturité. -- **Contrat `WaferVault`** (HSCS, via `@hiero-ledger/hiero-contracts`) — pools, financement, NAV - amortized-cost, deposit/redeem, settlement des rewards, défaut. **HBAR natif**. -- **Mirror Node** — le front lit NAV/pools/deals/activité (termes des deals émis en events). Vérifié Sourcify. - -## Roadmap - -Intégrations réelles de routage de rewards par réseau (Helium, Render, io.net…) · marché secondaire -sur **SaucerSwap** (part / WHBAR) pour une sortie instantanée · **file / epoch** de redemption quand -une pool est entièrement déployée · option de dénomination en stablecoin · plus de catégories et de -classes plus fines. - -## Démo - -Finance `GPU-A` en HBAR → parts au NAV. L'admin finance un deal DePIN (avance + NFT). Des rewards -HBAR arrivent dans le vault → **le NAV monte en live**. Le NFT burn au settlement. L'investisseur -redeem au NAV pour le gain. diff --git a/docs/PITCH.md b/docs/PITCH.md deleted file mode 100644 index 381d160..0000000 --- a/docs/PITCH.md +++ /dev/null @@ -1,236 +0,0 @@ -# Wafer — Runbook de présentation live (ETHGlobal NY 2026) - -> **À quoi sert ce fichier.** Le script de ta **présentation en direct**, en 2 temps : -> **(1) le deck** (`WaferPres.pdf`, 7 slides) — où tu prends le temps d'**expliquer simplement** — -> puis **(2) le front** (l'app live déployée). Les *consignes* sont en français ; ce que tu *dis aux -> juges* est en **anglais**, prêt à lire. -> -> Track : **Hedera (main, $15 000)** + **Tokenization on Hedera ($3 000)**. -> Objectif n°1 : **qu'un juge qui ne connaît pas le DePIN comprenne en 2 minutes.** - ---- - -## 0. Le pitch en une phrase (à mémoriser) - -> **Wafer is InfraFi: a KYC-gated, NAV-appreciating tokenized credit fund on Hedera that advances -> HBAR to DePIN operators against their future on-chain rewards — and you watch NAV tick up live as -> those rewards stream back in.** - -Le **wow** (à dire lentement, c'est l'idée qui gagne la track) : -> **DePIN is the one real-world asset whose cashflow is *natively* on-chain. So repayment needs zero -> trust in a human paying back — the hardware itself routes its rewards to the vault, and NAV rises -> on its own.** - -Positionnement : *« Centrifuge / Maple, but specialized for DePIN reward streams, fully on Hedera. »* - ---- - -## 1. Plan (2 temps, ~5 min) + règles de clarté - -| Temps | Support | Ce que ça prouve | Durée | -|---|---|---|---| -| **1. Le DECK** | `WaferPres.pdf` (7 slides) | Le **pourquoi** + le **comment**, expliqués simplement | ~2:30 | -| **2. Le FRONT** | L'app déployée (Vercel) | C'est **réel, fini, utilisable** | ~2:00 | - -**3 règles pour qu'ils comprennent (tiens-les toute la prés) :** -1. **Une idée par slide.** Ne lis pas la slide — dis l'idée avec **tes mots**, puis avance. -2. **Une analogie par concept abstrait.** À chaque mot crypto (NAV, escrow, HTS…), ajoute tout de - suite l'image du quotidien. *Le juge retient l'image, pas le jargon.* -3. **Reviens toujours au fil rouge :** l'opérateur a besoin de cash maintenant → le pool l'avance → - **le matériel rembourse tout seul** → le NAV (la valeur de la part) monte. Tout ramène à ça. - ---- - -## 2. TEMPS 1 — Le DECK (expliqué à fond, slide par slide) - -> **Comment présenter le deck :** lentement, une idée à la fois. Le deck doit poser **l'histoire** ; -> la démo prouvera que c'est réel. Pour chaque slide ci-dessous : ce qui est **à l'écran**, ce que tu -> **dis** (anglais), et l'**image simple** à donner si tu vois un juge décrocher. - -### Slide 1 — Title · *Wafer — InfraFi liquidity for DePIN — on Hedera.* -**Dis :** -> « Hi, we're Wafer. Quick context first: **DePIN** means decentralized physical infrastructure — -> real hardware, like GPUs for AI, wireless hotspots, or mapping cars, that earns crypto rewards for -> doing useful physical work. We built a tokenized credit fund on Hedera that **finances that -> hardware**. Let me show you the problem we solve. » - -🟢 *Image simple :* « DePIN = real machines earning crypto. We fund the machines. » - -### Slide 2 — Problem · *DePIN operators buy hardware today, earn rewards over months.* -**Dis :** -> « Here's the pain. An operator buys, say, five thousand dollars of Helium hotspots **today** — but -> the rewards only trickle in **over a year**. That's a timing gap: they need capital now, and they -> don't have it. And no bank will lend to them — a bank has no idea how to underwrite 'a stream of -> crypto rewards from a wireless hotspot.' So the capital that would let DePIN scale just isn't there. » - -🟢 *Image simple :* « You pay for the machine now, it pays you back slowly. Banks can't help. » - -### Slide 3 — Solution · *Financing layer, not an operator.* -**Dis :** -> « Wafer sits in the middle of a two-sided market. On one side, **investors** who want yield: they -> put HBAR into a pool and get a tokenized fund share back. On the other side, **operators** who want -> cash now: they sell a slice of their future rewards and get HBAR today. We never run any hardware -> ourselves — we're purely the financing layer. Think of us as a **credit fund** — Centrifuge or -> Maple — but specialized for DePIN. » - -🟢 *Image simple :* « Investors lend, operators borrow, Wafer is the fund in the middle. » - -### Slide 4 — WoW Effect · *(LE slide. Ralentis. C'est ici que tu gagnes la track.)* -**Dis (prends 30–40 s, c'est le cœur) :** -> « Now, *why is DePIN special* for a credit fund? Every other real-world asset — an invoice, a -> mortgage — needs a **human or a bank to actually pay you back**. That's the risk: someone might not -> pay. DePIN is different: **its cashflow is born on-chain**. The hardware earns rewards -> automatically, on-chain, forever — no invoice, no bank, no fiat. -> -> And here's the mechanism that makes it bulletproof. On a network like Helium, **every device is an -> NFT, and that NFT decides where its rewards get deposited.** So the operator hands that device-NFT -> to our vault as collateral — and now **the vault controls where the rewards go**. It's exactly like -> signing over the direct-deposit of your paycheck to your lender: the operator literally *can't* -> redirect the money, because the code holds the key. **Repayment needs no trust in a human paying** — -> the machine pays the vault, and the fund's value ticks up live. » -> -> *(Honnêteté à dire — ça inspire confiance) :* « The one piece we still trust off-chain is a relayer -> that swaps the reward token into HBAR at the moment of payout. That's the only residual trust: -> custody of the bridged HBAR for that instant — and we're upfront about it. » - -🟢 *Image simple :* « The device-NFT = the direct-deposit slip for the rewards. We hold it, so the -machine pays us automatically. » - -### Slide 5 — How it works (lifecycle) · *propose → class → finance → rewards → repaid/default* -**Dis :** -> « The full lifecycle, five steps. **One** — an operator proposes a deal: 'advance me 9 HBAR against -> 10 of future rewards.' **Two** — an admin assigns a risk class, like an underwriter rating it A, B, -> or C. **Three** — the pool finances it: it advances the HBAR and mints a **claim NFT**, an on-chain -> receipt of the loan held by the vault. **Four** — the rewards stream back in, and the pool's **NAV** -> — the value of one fund share — ticks up. **Five** — full repayment **burns** the receipt NFT; a -> **default** writes the NAV **down**, and the loss is shared fairly across everyone in the pool, like -> a fund taking a loss. Investors can redeem at NAV anytime, or sell on SaucerSwap. » -> -> *(Si on te demande « what's NAV? ») :* « NAV is net asset value — the value of one share, like an -> ETF's price. It starts at 1.0; as rewards come in, it goes to 1.1, and that's your yield, live. » - -🟢 *Image simple :* « It's a fund. The share value goes up as the loans get repaid, down if one defaults. » - -### Slide 6 — Why Hedera / HTS · *compliant fund unit, native services* -**Dis :** -> « Why Hedera specifically. The fund share isn't a hand-rolled token — it's a native **Hedera Token -> Service** token, and its **KYC and freeze controls are held by the vault contract itself**, with no -> off-chain signer. That means it's a **compliant fund unit at the protocol level** — exactly what a -> tokenized security needs. The loan receipt is a native NFT. Settlement is in **native HBAR** — -> three-second finality, fees in cents. We read all the live data from the **Mirror Node**, the -> secondary market is **SaucerSwap**, and the contract is **Sourcify-verified**. And we use a second -> native service — **Scheduled Transactions, HIP-1215**: the advance is a *locked transfer the Hedera -> network itself releases on a schedule*, with no bot, no keeper. » - -🟢 *Image simple :* « The fund share IS a Hedera token, with compliance built into the chain — not bolted on. » - -### Slide 7 — What's live · *(transition vers l'app)* -**Dis :** -> « And none of this is a mockup. The vault is **live on Hedera Testnet and Sourcify-verified**. The -> entire lifecycle is proven on-chain — finance, reward stream, NAV up, repaid-and-burn, and a default -> write-down. The SaucerSwap pair is live. So let me stop talking and **show you the real app.** » - -🟢 *Image simple :* « Everything I just described is deployed and working. Here it is. » - -> *(Slide roadmap, seulement si on te le demande en Q&A) :* real per-network reward routing -> (Helium/Render/io.net) replacing the simulated cashflow · redemption epochs · a compliant custom -> fee (permissioned-transfer design) · stablecoin denomination · more categories & risk classes. - ---- - -## 3. TEMPS 2 — Le FRONT (l'app live, déployée) - -> **But :** montrer un **produit fini**. Idéalement l'URL **Vercel** publique (sinon `cd web && pnpm -> dev` — même rendu). Préparé : un pool GPU-A à NAV > 1.00 avec un deal en cours (lance un finance + -> drip avant, ou `pnpm run smoke`, pour que le NAV bouge en live). - -**Ouverture :** -> « Same contract, now the product — live and **deployed on Vercel**, anyone can open this URL. It's a -> React app talking to the contract directly through your wallet, reading state from the Hedera Mirror -> Node. No backend server — the smart contract *is* the backend. » - -**Parcours (clic par clic, dis une phrase par écran) :** -1. **Landing → How it works** — le hero + la bande 3 étapes. *« The pitch in three steps. »* -2. **Explore / Pools** — KPI row (TVL · Deployed · Idle · Pools · Blended APR) + pool **GPU-A** (NAV, - TVL, Trailing APR, % deployed). *« The protocol's live state, read straight from the chain. »* -3. **Deposit** — chips **Associated / KYC granted** → montant → **Deposit HBAR** → parts au NAV. - *« The share is a KYC-gated Hedera token: associate, get allowlisted, deposit, receive fund shares. »* -4. **Operator** — *Propose a deal* + **mint & escrow device-NFT**. *« The operator posts the device-NFT - as collateral — that's the direct-deposit slip for the rewards I mentioned. »* -5. **Admin** — assign **Class A** + pool → **Approve** → **Finance**. *« Risk class, route to the pool, - finance — HBAR advanced, claim NFT minted. »* -6. **Explore → Activity** — pointe l'event **"Advance locked (HIP-1215)"**. *« And there's the locked, - scheduled transfer — the network will release it on its own. »* -7. **NAV monte** (Pool detail / Operator claims) — un drip tombe, auto-refresh. *« Watch the NAV tick - up — that's the yield, read live from the contract. »* -8. **Redeem** — `Max instant redeem` + notice **instant + FIFO queue**. *« Exit at NAV, instant up to - the liquidity buffer, the rest fairly queued. »* -9. **Secondary** — **GPU-A / WHBAR — Live pair**, réserves + price + lien HashScan. *« Or sell on a - live SaucerSwap market — our KYC-gated share against wrapped HBAR. »* - -**Clôture :** -> « So: a compliant tokenized fund, financing real-world infrastructure, where the repayment is -> enforced by the chain itself — live on Hedera, deployed, verifiable end to end. Wafer — InfraFi -> for DePIN. Thank you. » - ---- - -## 4. Ce qu'on a construit (à glisser en slide 6/7 et en Q&A) - -- **Contrat `WaferVault` (Solidity, HSCS)** — compta **amortized-cost** (NAV dérivé `idle + - receivable`, tue le double-comptage), workflow propose→approve→finance, file de redemption, défaut. -- **2 tokens HTS** pilotés par le contrat : part de fonds **5 clés** (supply/kyc/freeze/wipe/**pause - token-level réelle**) + claim-NFT reçu (burné à maturité), + **escrow device-NFT** (collatéral). -- **HIP-1215 / Hedera Schedule Service** (2ᵉ service natif) — **avance verrouillée** auto-libérée par - le réseau + **settle auto-schedulé**, **sans keeper**. -- **Marché secondaire SaucerSwap** en **un seul appel** contrat. -- **Sécurité + audit adverse multi-agents** corrigé (overflow guard, earmark file senior, fee fantôme, - surplus sweep, ReentrancyGuard+CEI, Ownable2Step+timelock, dead-shares anti-inflation) · **78 tests**. -- **Livré** : déployé + **Sourcify-verified** + **frontend déployé sur Vercel**. - ---- - -## 5. Q&A juges — réponses prêtes - -- **« How does the operator actually repay? »** → Device-NFT escrow: whoever controls the NFT controls - the reward stream (Helium recipient/destination model). The maturity settle is already scheduled - on-chain via HIP-1215; in prod a relayer bridges the real reward token to HBAR. Only residual trust - = custody of the bridged HBAR. *(C'est aussi le cœur de la slide 4 — tu l'as déjà raconté.)* -- **« Per-deal vs per-pool APR? »** → APR is per deal; the pool NAV is the *blended realized* return, - accrued amortized-cost; the risk class is the admin's risk/return curation. -- **« What on default? »** → `markDefault` writes the remaining carry down → NAV falls, loss shared - pro-rata; escrowed device-NFT retained/liquidated. Demo: NAV 1.16 → 0.80. -- **« Is the share really tokenized & transferable? »** → Yes — a fungible HTS token, KYC-gated, - redeemable at NAV anytime, plus a SaucerSwap secondary enabled per pool in one contract call. -- **« Where are the custom fees the bounty mentions? »** → Wafer is an *accumulating* fund: yield - compounds into NAV (like an accumulating ETF). A plain HTS fractional fee on a KYC-gated token - breaks redeem and the AMM, so a compliant take-rate needs a permissioned-transfer design — roadmap. -- **« Security? »** → ReentrancyGuard + CEI everywhere, `settleRewards` gated by claim & capped at - `expected`, Ownable2Step + timelock, operator allowlist, dead-shares seed, int64 overflow guards, - real token-level pause. A two-phase adversarial review was run and fixed. - ---- - -## 6. Liens live (à montrer / coller) — déploiement 2026-06-14 - -> ⚠️ Adresses à jour (HEAD `a4ef16e`). L'ancien vault `0x9b8752…` est mort. - -- **App déployée (Vercel) :** `[colle l'URL Vercel ici]` *(sinon `cd web && pnpm dev`).* -- **Vault (HashScan) :** https://hashscan.io/testnet/contract/0x4B821d6bC76203C3C21131849C40d04C84bb75d5 -- **Sourcify (verified) :** https://repo.sourcify.dev/contracts/full_match/296/0x4B821d6bC76203C3C21131849C40d04C84bb75d5/ -- **Pool-share (HTS) :** https://hashscan.io/testnet/token/0.0.9231169 · **Claim-NFT :** https://hashscan.io/testnet/token/0.0.9231170 -- **SaucerSwap pair :** https://hashscan.io/testnet/contract/0x4B6dEAcA611177F74433A57A3bF6f9b1b95BC182 -- Vault id `0.0.9231166` · operator `0.0.9185964` (`0xf6fAc89C…`) · GitHub `github.com/aiden-fianso/Wafer` - ---- - -## 7. Checklist avant de présenter - -- [ ] **App ouverte** : URL Vercel (ou `cd web && pnpm dev`) + MetaMask sur Hedera Testnet (296), - compte operator `0xf6fAc89C…`. -- [ ] **Pool « vivant » pré-chargé** : NAV > 1.00 + un deal en cours + l'event **"Advance locked - (HIP-1215)"** dans Explore → Activity. *(Si le pool est « usé » — NAV < 1 — redéploie frais : - `pnpm run deploy` puis `pnpm run smoke` pour le bel arc 1.0 → 1.1.)* -- [ ] **Onglets prêts** : deck `WaferPres.pdf` · l'app · **HashScan du vault** · **Sourcify verified**. -- [ ] **Répété le deck à voix haute** : une idée/slide, une analogie/concept, et la slide 4 (le wow) - racontée *lentement*. 2 temps : deck (~2:30) → front (~2:00). diff --git a/docs/TRACKS.md b/docs/TRACKS.md deleted file mode 100644 index e5514dd..0000000 --- a/docs/TRACKS.md +++ /dev/null @@ -1,81 +0,0 @@ -# Sponsor & track strategy - -> ⚠️ **Superseded (2026-06-14):** the architecture below ("pure-HTS / no-Solidity") was an early -> plan. Wafer shipped as a **Solidity HSCS vault** (`WaferVault.sol`) that creates/manages HTS tokens -> and now uses **HIP-1215 scheduled transactions** (locked advance + self-scheduling settle). The -> target is **Hedera — Tokenization** ($3k) + main Hedera ($15k); see `SPEC.md` / `docs/DEMO.md` for -> the real, current design. The strategy notes below are kept for historical context only. - -Brand-new project (no Continuity tracks). Max 3 sponsors. World banned. Chainlink (CRE / -Confidential AI) and Unlink ("Add Privacy") avoided — taken by a strong contact. - -**Apply to 2 sponsors and go deep, with ENS as an optional stretch third.** - -## Primary: Hedera (sponsor #1) — $12,000 addressable - -The single highest-leverage decision is the **pure-HTS / no-Solidity architecture**: one -codebase competes for all three reachable Hedera tracks. A Solidity vault would forfeit two of -them. - -| Track | $ | Qualifies | How Wafer hits it | -|---|---|---|---| -| **Tokenization on Hedera** | $3,000 (2×$1,500) | ✅ lead with this | HTS fungible pool-share (tokenized RWA fund share) + claim NFTs, managed via SDK. Hits the bonus list directly: **KYC-gated fund shares** (their literal example), freeze/pause keys, **custom fractional fee** (protocol take-rate). | -| **No Solidity Allowed** | $3,000 (3×$1,000) | ✅ near-free | SDK-only by construction. Combines **3** native services: HTS + HCS (NAV/audit topic) + Scheduled Transactions (reward sweep); Mirror Node is the read layer. | -| **AI & Agentic Payments** | $6,000 (2×$3,000) | ⚠️ stretch | Add an autonomous **settlement agent** (Hedera Agent Kit, LangChain/TS) that monitors the HCS topic + incoming USDC, routes operator rewards into the vault, recomputes/publishes NAV, mints/burns on deposit/redeem. Qualifies if it demonstrably executes a transfer on testnet. Scope as a thin autonomous loop over SDK calls you already have (`src/agent/`). | -| ~~Autonomous On-Chain Automation~~ | $3,000 | ❌ | Continuity-only. Skip. | - -Lead the whole submission with Tokenization; the page does not explicitly confirm one project -can win multiple Hedera bounties, so submit to all three but make Tokenization the primary -narrative. - -## Secondary: Privy (sponsor #2) — $3,750 addressable - -Chain-agnostic; doesn't need to run on Hedera natively. Use it purely for **auth + embedded -wallet + funding UX**, keep settlement on Hedera HTS. - -| Track | $ | How | -|---|---|---| -| Best onchain financial product | $1,250 | Embedded-wallet login + the NAV-vault UI (deposit USDC → hold appreciating share → redeem at NAV). Mirrors Privy's Earn UX with **our own** vault calls. | -| Best cross-chain funding experience | $1,250 | Privy **universal deposit addresses** as the "fund your pool position in one tap" funnel (bridges inbound funds via Relay). | -| Best AI agent built with Privy | $1,250 | Front the settlement agent with a Privy Agent Wallet — reuses the exact agent built for Hedera's agentic track. | - -Integration plan (in `web/`): -- Privy embedded wallet on **Hedera EVM** via `defineChain({ id: 296, rpcUrls: - ['https://testnet.hashio.io/api'], nativeCurrency: HBAR, blockExplorer: hashscan testnet })`, - set as `defaultChain` + in `supportedChains`. -- **Sharp edge — HTS association**: EVM wallets don't understand HTS token association. On - first login, the backend sends a dust HBAR transfer to auto-create the account and enables - auto-association (`maxAutomaticTokenAssociations`). Embedded wallet signs redeem/transfer - intents; the backend does the HTS heavy lifting. **Test this first thing** — it's the most - likely thing to silently break the investor demo. -- **Do NOT** chase Privy "Earn" (managed ERC-4626, Base-only, sales-gated) — a pure-HTS vault - has no on-chain `deposit()/redeem()` to bind to. Hand-roll the UX. - -## Optional stretch: ENS (sponsor #3, only with spare capacity) - -ENS is not deployed on Hedera, so it reads as a bolt-on unless built into onboarding. Plausible -angle: each operator/fleet gets an ENS subname with text records (network, risk class, -expected rewards, claim-NFT pointer) = portable operator identity; optionally name the -settlement agent (ENS suggests naming AI agents). Targets **Most Creative Use of ENS** -($5,000) + the ENS×AI-agent prize. Only if it doesn't cannibalize the Hedera agentic build. - -## Dropped (and why) - -| Sponsor | Reason | -|---|---| -| **LI.FI** | No Hedera support (no chain entry, only Hashport which LI.FI doesn't aggregate). A Composer flow can't reach an HTS vault. | -| **Blink** | No Hedera support (Base/Arbitrum/Eth/Polygon/BNB/Solana only). Deposit can't land on Hedera. | -| **Dynamic** | Redundant with Privy (both wallet/auth), worse Hedera fit (EVM-RPC only), and its headline private-nanopayments track is joint with Unlink (avoided). | - -## Prize math - -- **Two-sponsor addressable** (Continuity excluded): Hedera $12,000 + Privy $3,750 = **$15,750**. -- With ENS stretch: + ~$2.5k–$4k realistic from Most Creative Use / ENS×AI. -- **Realistic expectation** (these are "up to N teams" splits): a focused submission most - plausibly lands Tokenization ($1,500) + No Solidity ($1,000) + one Privy track ($1,250) ≈ - **$2,500–$3,750**, with genuine upside to ~$6k–$9k if the agentic build lands a $3k Hedera - agent slot. The pure-HTS architecture choice is what unlocks 3 Hedera tracks from one build — - worth more expected value than adding any third wallet/bridge sponsor. - -Sources: ETHGlobal NY 2026 prize pages (Hedera, Privy, ENS), Hedera/Circle/Privy/SaucerSwap -docs — see research notes in the project thread. diff --git a/docs/art-refs/Energy.png b/docs/art-refs/Energy.png deleted file mode 100644 index 0aa8e6d..0000000 Binary files a/docs/art-refs/Energy.png and /dev/null differ diff --git a/docs/art-refs/GPU.png b/docs/art-refs/GPU.png deleted file mode 100644 index defe2c9..0000000 Binary files a/docs/art-refs/GPU.png and /dev/null differ diff --git a/docs/art-refs/Mapping.png b/docs/art-refs/Mapping.png deleted file mode 100644 index e5947e7..0000000 Binary files a/docs/art-refs/Mapping.png and /dev/null differ diff --git a/docs/art-refs/Wireless.png b/docs/art-refs/Wireless.png deleted file mode 100644 index 89f06b1..0000000 Binary files a/docs/art-refs/Wireless.png and /dev/null differ diff --git a/docs/art-refs/hero-landing-mockup.png b/docs/art-refs/hero-landing-mockup.png deleted file mode 100644 index c8e8468..0000000 Binary files a/docs/art-refs/hero-landing-mockup.png and /dev/null differ diff --git a/docs/art-refs/storage.png b/docs/art-refs/storage.png deleted file mode 100644 index 0a6db56..0000000 Binary files a/docs/art-refs/storage.png and /dev/null differ diff --git a/docs/art-refs/wafer-wordmark-nyc.png b/docs/art-refs/wafer-wordmark-nyc.png deleted file mode 100644 index 2b43cc0..0000000 Binary files a/docs/art-refs/wafer-wordmark-nyc.png and /dev/null differ diff --git a/docs/superpowers/specs/2026-06-14-app-navbar-resizable-design.md b/docs/superpowers/specs/2026-06-14-app-navbar-resizable-design.md deleted file mode 100644 index 13ddae3..0000000 --- a/docs/superpowers/specs/2026-06-14-app-navbar-resizable-design.md +++ /dev/null @@ -1,73 +0,0 @@ -# Spec — Navbar resizable (Aceternity) sur la page app, fond noir - -Date : 2026-06-14 -Périmètre : **page app uniquement** (shell connecté de `web/src/App.jsx`, lignes ~200-244). -La landing (Hero / HowItWorks) n'est **pas** touchée. - -## Objectif - -Remplacer la navbar actuelle de l'app (`CardNav`) par le composant Aceternity -**`resizable-navbar`** (collé par l'utilisateur), sur un **fond noir uni**, en -gardant les écrans existants accessibles dessous. - -## Décisions (validées avec l'utilisateur) - -1. **Intégration** : ajouter Tailwind v4 + lib `motion` (composant Aceternity fidèle), - plutôt que réécrire en CSS pur. -2. **Portée** : navbar + fond noir seulement. Les écrans (Pools, Dashboard, …) restent - en place. Boutons conservés mais ré-stylés à la DA. -3. **Liens nav** : sections réelles de l'app. -4. **Operate / Admin** : items conditionnels en plus (comme dans `CardNav`). -5. **Process** : spec court + commit, puis implémentation directe. - -## Stack & infra (risque faible pour le CSS existant) - -- Dépendances : `tailwindcss@4`, `@tailwindcss/vite`, `motion`, `clsx`, `tailwind-merge`. -- **Préflight Tailwind désactivé** : on importe seulement les layers `theme` + `utilities` - (pas le reset `base`). Comme `App.css` n'est pas « layered », il gagne la cascade → - aucune régression sur les écrans existants. Les utilitaires Tailwind ne servent que - dans la navbar. -- Alias `@/` → `/src` dans `vite.config.js` (+ `jsconfig.json` pour l'éditeur). -- Variante `dark` Tailwind v4 via `@custom-variant dark (&:where(.dark, .dark *))` ; - classe `dark` posée sur le shell. - -## Fichiers - -- **Nouveau** `web/src/index.css` — imports Tailwind (layers theme+utilities, pas de - preflight) + `@custom-variant dark`. Importé dans `main.jsx` **avant** `App.css`. -- **Nouveau** `web/src/lib/cn.js` — utilitaire `cn(...)` (clsx + tailwind-merge). -- **Nouveau** `web/src/components/ui/resizable-navbar.jsx` — portage JSX du composant - Aceternity. Adaptations : - - `motion/react` (au lieu de framer-motion), `cn` depuis `@/lib/cn` ; - - icônes menu/X en **SVG inline** (pas de `@tabler/icons-react`) ; - - `NavItems` : items `{ name, onClick }`, navigation SPA (appelle `item.onClick` - puis `onItemClick` pour fermer le menu mobile) ; - - `NavbarLogo` : marque Wafer (glyph + wordmark), `onClick` → home ; - - `NavbarButton` : variantes conservées, primary = amber `--amber`. -- **Modifié** `web/vite.config.js` — plugin `@tailwindcss/vite` + alias `@`. -- **Nouveau** `web/jsconfig.json` — `paths` `@/*` → `src/*`. -- **Modifié** `web/src/App.jsx` — shell connecté : remplace `` et la - `.shell-topbar` flottante par une seule ``. Items : - - Pools → `pools`, Portfolio → `dashboard`, Redemption queue → `queue`, - Secondary → `secondary`, Activity → `activity` ; - - + `Operate` si `isOperator || isOwner` → `operator` ; - - + `Admin` si vue admin → `admin`. - - Côté droit : pill **« Hedera Testnet »** (secondary) + `AccountMenu` existant. - - Classe `dark` + fond noir sur le shell. -- **Modifié** `web/src/App.css` — `.shell` en `#000` uni (suppression du dégradé - radial bleu), `padding-top` du contenu ajusté pour la navbar sticky. Grain mis de - côté sur le shell pour l'instant. - -## Hors périmètre - -- Landing page (Hero, HowItWorks) inchangée. -- Écrans (Pools, Dashboard, RedemptionQueue, …) inchangés. -- `CardNav.jsx` / `CardNav.css` restent dans le repo (plus utilisés sur le shell) ; - suppression éventuelle plus tard. - -## Vérification - -- `pnpm build` sans erreur. -- Dev : navbar visible, fond noir, items câblés sur les onglets, menu compte - fonctionnel, rétrécissement au scroll, menu mobile. -- Écrans existants toujours rendus correctement (pas de régression de style). diff --git a/docs/superpowers/specs/2026-06-14-hss-scheduled-transactions-design.md b/docs/superpowers/specs/2026-06-14-hss-scheduled-transactions-design.md deleted file mode 100644 index c5d27de..0000000 --- a/docs/superpowers/specs/2026-06-14-hss-scheduled-transactions-design.md +++ /dev/null @@ -1,81 +0,0 @@ -# Design — HIP-1215 "locked virements" (Hedera Schedule Service) in Wafer - -Date: 2026-06-14 · Status: approved (inline) · Target: Hedera Tokenization bonus (scheduled transactions) - -## Goal - -Make money movements in Wafer **native scheduled/locked transfers** via Hedera Schedule Service -(HIP-1215, system contract `0x16b`, live on testnet) — no off-chain keeper. Two flows: - -1. **Locked advance payout** — the operator's advance is locked in the vault at finance and - auto-released after a window. -2. **Self-scheduling reward settle** — the reward drip schedules its own next interval on-chain, - replacing the off-chain JS poll loop. - -Both are **opt-in toggles**; the default path is exactly today's verified behavior, so the demo can -fall back if HIP-1215 misbehaves live. - -## API (verified) - -`HederaScheduleService.scheduleCall(address to, uint256 expirySecond, uint256 gasLimit, uint64 value, -bytes callData) returns (int64 rc, address scheduleAddress)`. The scheduling **contract is the payer** -and must hold HBAR; the network auto-executes the call at `expirySecond` with no keeper. Self-calls -(`to == address(this)`) are supported. - -## Feature 1 — Locked advance (`WaferVault`) - -State: `uint64 advanceLockSeconds` (owner-set; 0 = instant, today's default), `uint256 -pendingAdvanceTinybar` (Σ scheduled-unreleased advances), `mapping(uint256=>uint64) advanceUnlockTime`, -`mapping(uint256=>bool) advanceReleased`. - -`financeClaim` effects unchanged (`idle -= advance`, `receivable += advance` → NAV flat). The advance -HBAR stays in the vault. The CEI-last step branches: -- `advanceLockSeconds == 0` → pay the operator immediately (today). -- else → set `advanceUnlockTime[claimId]`, `pendingAdvanceTinybar += advance`, and - `scheduleCall(this, now+lock, RELEASE_ADVANCE_GAS, 0, releaseAdvance.selector(claimId))`; emit - `AdvanceScheduled`. - -`releaseAdvance(claimId)` — permissionless, `nonReentrant`, gated by `now >= unlock && !released` -(so it can neither pay early nor twice, even if called manually); HSS auto-fires it at expiry. Pays -the operator from the vault balance, decrements `pendingAdvanceTinybar`, emits `AdvanceReleased`. - -`ownerWithdrawSurplus` excludes `pendingAdvanceTinybar` from sweepable surplus. - -Invariant note: NAV-flat at finance (I3) is unaffected — only the *physical* timing of the operator -payout changes; the accounting (`idle→receivable`) is identical. - -## Feature 2 — Self-scheduling drip (`MockRewardSource`) - -Inherit `HederaScheduleService`. Refactor the drip body into internal `_release(scheduleId)`. Add: -- `armSelfDrip(scheduleId)` (owner) — turns on self-scheduling and schedules the first `scheduledDrip`. -- `scheduledDrip(scheduleId)` — releases the due interval (if any) then `_scheduleNext`; tolerates - not-yet-due (just reschedules); stops when `dripsDone == dripCount` or `defaulted`. -- `_scheduleNext(scheduleId)` — `scheduleCall(this, nextIntervalBoundary, DRIP_GAS, 0, - scheduledDrip.selector(scheduleId))`. - -The manual `drip()` stays as the fallback. Demo: `fund(...)` → `armSelfDrip(id)` → **wait** → NAV -ticks up with no keeper. - -## Frontend - -Add the new functions/events to `web/lib/abi.js`. `AdvanceScheduled` / `AdvanceReleased` / -`Scheduled` then surface automatically in the Mirror-Node Activity feed. (Optional: a "scheduled, -unlocks in Ns" badge — stretch.) - -## Tests - -Pure-logic mirror for `releaseAdvance` gating (unlock-time + once-only) and `_scheduleNext` interval -math. The live HSS round-trips are proven by an extended `pnpm smoke` (a scheduled-advance run and a -self-drip run), like the other 0x167/0x16b paths. - -## Risk - -HIP-1215 is new. Both paths are opt-in; default = proven behavior. The single end-of-work redeploy + -extended smoke validates the scheduled paths live; toggles off = demo the fallback. New surface: -~3 fns + state in the vault, 3 fns + inheritance in the mock, smoke additions, abi entries. - -## Demo - -`advanceLockSeconds = 10s`. financeClaim → a scheduled tx appears on HashScan → 10s later it -self-executes paying the operator. fund + armSelfDrip → NAV rises with no script loop. Pitch: a -literal locked virement + genuine no-keeper automation, on a capability most teams won't touch. diff --git a/scripts/finish-deploy.ts b/scripts/finish-deploy.ts deleted file mode 100644 index ef01776..0000000 --- a/scripts/finish-deploy.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * One-off recovery: finish a half-completed `pnpm run deploy` against an ALREADY-deployed vault. - * - * VAULT=0x... POOL_ID=0 SHARE=0x... CLAIM=0x... \ - * TS_NODE_PROJECT=tsconfig.hardhat.json hardhat run scripts/finish-deploy.ts --network testnet - * - * Deploys MockRewardSource + MockDeviceNFT (+ createCollection) against the existing vault, then - * writes deployments/testnet.json. Used when the main deploy ran out of testnet HBAR mid-way after - * the vault/pool/secondary-config were already created (no point burning another 100 HBAR on a new - * pool). The vault, pool, operator whitelist and SaucerSwap config are assumed already on-chain. - */ -import hre from "hardhat"; -import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs"; -import { dirname, resolve } from "node:path"; - -const { ethers } = hre as any; - -const REPO_ROOT = process.cwd(); -const DEPLOYMENTS_PATH = resolve(REPO_ROOT, "deployments", "testnet.json"); -const ENV_PATH = resolve(REPO_ROOT, ".env"); - -const HBAR = 10n ** 18n; -// HTS NFT-collection create is ~$1; the precompile refunds excess to the contract. Trimmed to fit -// the remaining operator budget (override with DEVICE_FUNDING_HBAR). -const DEVICE_CREATE_FUNDING = BigInt(process.env.DEVICE_FUNDING_HBAR ?? "22") * HBAR; -const CREATE_GAS = 10_000_000n; - -const HASHSCAN = "https://hashscan.io/testnet"; -const MIRROR = "https://testnet.mirrornode.hedera.com/api/v1"; - -const SAUCER_ROUTER = "0x0000000000000000000000000000000000004b40"; -const SAUCER_WHBAR = "0x0000000000000000000000000000000000003ad2"; -const SAUCER_FACTORY = "0x00000000000000000000000000000000000026e7"; - -const POOL_NAME = "Wafer GPU-A"; -const POOL_SYMBOL = "wGPUA"; - -function evmToHederaId(evm: string): string { - return `0.0.${BigInt(evm).toString()}`; -} - -async function resolveContractId(evm: string): Promise { - for (let i = 0; i < 10; i++) { - try { - const res = await fetch(`${MIRROR}/contracts/${evm}`, { headers: { "User-Agent": "curl/8" } }); - if (res.ok) { - const data: any = await res.json(); - if (data.contract_id) return data.contract_id; - } - } catch { /* retry */ } - await new Promise((r) => setTimeout(r, 2500)); - } - return ""; -} - -function updateEnv(updates: Record): void { - if (!existsSync(ENV_PATH)) return; - const lines = readFileSync(ENV_PATH, "utf8").split("\n"); - const seen = new Set(); - const out = lines.map((line) => { - const m = line.match(/^(\s*)([A-Z0-9_]+)=/); - if (m && updates[m[2]] !== undefined) { - seen.add(m[2]); - const comment = line.includes("#") ? " " + line.slice(line.indexOf("#")) : ""; - return `${m[1]}${m[2]}=${updates[m[2]]}${comment}`; - } - return line; - }); - for (const [k, v] of Object.entries(updates)) if (!seen.has(k)) out.push(`${k}=${v}`); - writeFileSync(ENV_PATH, out.join("\n")); -} - -async function main() { - const vaultAddr = process.env.VAULT; - if (!vaultAddr) throw new Error("set VAULT=0x... (the already-deployed vault address)"); - const poolId = Number(process.env.POOL_ID ?? "0"); - - const [deployer] = await ethers.getSigners(); - const net = await ethers.provider.getNetwork(); - const bal = await ethers.provider.getBalance(deployer.address); - console.log(`\n=== Finish Wafer deploy · chain ${net.chainId} ===`); - console.log(`deployer : ${deployer.address}`); - console.log(`balance : ${ethers.formatEther(bal)} HBAR`); - console.log(`vault : ${vaultAddr}\n`); - - const vault = await ethers.getContractAt("WaferVault", vaultAddr, deployer); - const pool = await vault.pools(poolId); - const shareTokenEvm: string = pool.shareToken; - const claimNftEvm: string = pool.claimNft; - if (!shareTokenEvm || /^0x0+$/.test(shareTokenEvm)) throw new Error("pool not created on this vault"); - const shareTokenId = evmToHederaId(shareTokenEvm); - const claimNftId = evmToHederaId(claimNftEvm); - console.log(` share token : ${shareTokenEvm} (${shareTokenId})`); - console.log(` claim NFT : ${claimNftEvm} (${claimNftId})`); - - const nav = await vault.navPerShare(poolId); - console.log(` navPerShare : ${nav.toString()} (expect 100000000)`); - - let rewardSrcAddr: string = process.env.REWARD_SRC ?? ""; - if (rewardSrcAddr) { - console.log(`\nreusing MockRewardSource: ${rewardSrcAddr}`); - } else { - console.log(`\ndeploying MockRewardSource(vault)...`); - const RewardSrc = await ethers.getContractFactory("MockRewardSource"); - const rewardSrc = await RewardSrc.deploy(vaultAddr, { gasLimit: 2_000_000n }); - await rewardSrc.waitForDeployment(); - rewardSrcAddr = await rewardSrc.getAddress(); - console.log(` MockRewardSource: ${rewardSrcAddr}`); - } - - let deviceNftAddr: string = process.env.DEVICE_NFT ?? ""; - let deviceNft: any; - if (deviceNftAddr) { - console.log(`\nreusing MockDeviceNFT: ${deviceNftAddr}`); - deviceNft = await ethers.getContractAt("MockDeviceNFT", deviceNftAddr, deployer); - } else { - console.log(`\ndeploying MockDeviceNFT...`); - const DeviceNFT = await ethers.getContractFactory("MockDeviceNFT"); - deviceNft = await DeviceNFT.deploy({ gasLimit: 3_000_000n }); - await deviceNft.waitForDeployment(); - deviceNftAddr = await deviceNft.getAddress(); - console.log(` MockDeviceNFT: ${deviceNftAddr}`); - } - - let deviceTokenEvm: string = await deviceNft.token(); - if (!deviceTokenEvm || /^0x0+$/.test(deviceTokenEvm)) { - console.log(` createCollection (funding ${DEVICE_CREATE_FUNDING / HBAR} HBAR)...`); - const ccTx = await deviceNft.createCollection("Wafer Device", "wDEV", { - value: DEVICE_CREATE_FUNDING, - gasLimit: CREATE_GAS, - }); - await ccTx.wait(); - deviceTokenEvm = await deviceNft.token(); - } else { - console.log(` collection already created: ${deviceTokenEvm}`); - } - const deviceTokenId = evmToHederaId(deviceTokenEvm); - console.log(` device collection: ${deviceTokenEvm} (${deviceTokenId})`); - - const vaultHederaId = await resolveContractId(vaultAddr); - const rewardSrcHederaId = await resolveContractId(rewardSrcAddr); - const deviceNftHederaId = await resolveContractId(deviceNftAddr); - - const now = new Date().toISOString(); - const deployment = { - network: "testnet", - chainId: Number(net.chainId), - createdAt: now, - updatedAt: now, - settlementAsset: "HBAR", - operator: deployer.address, - vaultAddress: vaultAddr, - vaultId: vaultHederaId, - pool: { - id: poolId, name: POOL_NAME, symbol: POOL_SYMBOL, category: "GPU", class: "A", - shareTokenEvm, shareTokenId, claimNftEvm, claimNftId, - }, - mocks: { - rewardSource: { evm: rewardSrcAddr, id: rewardSrcHederaId }, - deviceNft: { evm: deviceNftAddr, id: deviceNftHederaId, collectionEvm: deviceTokenEvm, collectionId: deviceTokenId }, - }, - secondary: { router: SAUCER_ROUTER, whbar: SAUCER_WHBAR, factory: SAUCER_FACTORY }, - hashscan: { - vault: `${HASHSCAN}/contract/${vaultAddr}`, - shareToken: `${HASHSCAN}/token/${shareTokenId}`, - claimNft: `${HASHSCAN}/token/${claimNftId}`, - rewardSource: `${HASHSCAN}/contract/${rewardSrcAddr}`, - deviceNft: `${HASHSCAN}/contract/${deviceNftAddr}`, - deviceCollection: `${HASHSCAN}/token/${deviceTokenId}`, - }, - sourcify: `https://repo.sourcify.dev/contracts/full_match/296/${vaultAddr}/`, - }; - mkdirSync(dirname(DEPLOYMENTS_PATH), { recursive: true }); - writeFileSync(DEPLOYMENTS_PATH, JSON.stringify(deployment, null, 2) + "\n"); - updateEnv({ VAULT_ADDRESS: vaultAddr, SHARE_TOKEN_ID: shareTokenId, CLAIM_NFT_TOKEN_ID: claimNftId }); - console.log(`\n✓ wrote ${DEPLOYMENTS_PATH}`); - console.log(`✓ wrote VAULT_ADDRESS to .env`); - console.log(`\nNext: pnpm run smoke\n`); -} - -main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/smoke-hss.ts b/scripts/smoke-hss.ts index 288bf94..ede69ec 100644 --- a/scripts/smoke-hss.ts +++ b/scripts/smoke-hss.ts @@ -10,8 +10,8 @@ * confirm the advance was released to the operator — by the network, no keeper (or, if the * schedule has not fired yet, we call releaseAdvance manually to prove the gating + fallback). * - * 2. SELF-DRIP — fund a reward schedule and armSelfDrip: the MockRewardSource schedules its own - * scheduledDrip via HSS and each drip reschedules the next, so NAV rises with NO JS poll loop. + * 2. SELF-DRIP — fund a reward schedule and armSelfDrip: the MockRewardSource schedules ONE + * scheduledDrip at maturity via HSS, so NAV rises with NO off-chain keeper / JS poll loop. * * Run AFTER `pnpm run deploy`. Prints HashScan links. Designed to be re-runnable. Units: HBAR crosses * the RPC boundary as weibar (N*1e18); the contract sees tinybar. diff --git a/scripts/test-redeem.ts b/scripts/test-redeem.ts deleted file mode 100644 index 1c3b88a..0000000 --- a/scripts/test-redeem.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** Prove redeem works now that the share token has no fee (operator->vault pull + full-share burn). */ -import hre from "hardhat"; -import { readFileSync } from "node:fs"; -import { resolve } from "node:path"; -const { ethers } = hre as any; -const HBAR = 10n ** 18n, TINYBAR = 10n ** 8n, HASHSCAN = "https://hashscan.io/testnet"; - -async function main() { - const d = JSON.parse(readFileSync(resolve(process.cwd(), "deployments", "testnet.json"), "utf8")); - const [signer] = await ethers.getSigners(); - const vault = await ethers.getContractAt("WaferVault", d.vaultAddress, signer); - const share = new ethers.Contract(d.pool.shareTokenEvm, [ - "function approve(address,uint256) returns (bool)", - "function balanceOf(address) view returns (uint256)", - ], signer); - const poolId = d.pool.id ?? 0; - const shares = 1n * TINYBAR; // redeem 1.0 share - - const nav: bigint = await vault.navPerShare(poolId); - const preShares: bigint = await share.balanceOf(signer.address); - const preHbar: bigint = await ethers.provider.getBalance(signer.address); - console.log(`NAV ${Number(nav) / 1e8} preShares ${Number(preShares) / 1e8} redeeming 1.0 share...`); - - await (await share.approve(d.vaultAddress, shares, { gasLimit: 1_200_000n })).wait(); - // staticCall first (free) to surface any revert - const [filled, queued] = await vault.redeem.staticCall(poolId, shares, { gasLimit: 4_000_000n }); - console.log(` staticCall OK -> filled ${Number(filled) / 1e8} HBAR, queued ${Number(queued) / 1e8}`); - - const tx = await vault.redeem(poolId, shares, { gasLimit: 4_000_000n }); - await tx.wait(); - console.log(` redeem tx: ${HASHSCAN}/transaction/${tx.hash}`); - - const postShares: bigint = await share.balanceOf(signer.address); - const postHbar: bigint = await ethers.provider.getBalance(signer.address); - console.log(` shares ${Number(preShares) / 1e8} -> ${Number(postShares) / 1e8} (burned ${Number(preShares - postShares) / 1e8})`); - console.log(` HBAR delta (net of gas): ${Number(postHbar - preHbar) / 1e18}`); - console.log(preShares - postShares === shares ? " ✓ exactly 1.0 share burned (no fee) — redeem works" : " WARN: share delta != 1.0"); -} -main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/test/vault-statemachine.test.ts b/test/vault-statemachine.test.ts index a4e00e5..d170ff5 100644 --- a/test/vault-statemachine.test.ts +++ b/test/vault-statemachine.test.ts @@ -624,5 +624,5 @@ describe.skip("WaferVault — live HTS round-trips (run via `pnpm run smoke` on it("adminGrantKyc / freeze: grant/revoke + freeze/unfreeze gate HTS transfers (rc==22 checks)"); it("enableSecondaryMarket: createPair -> grantKyc(pair) -> mint+approve -> addLiquidityETH (router NOT KYC'd)"); it("HIP-1215 locked advance: financeClaim schedules releaseAdvance via HSS (0x16b); auto-fires at unlock, no keeper"); - it("HIP-1215 self-drip: MockRewardSource.armSelfDrip schedules scheduledDrip which reschedules each interval, no keeper"); + it("HIP-1215 self-drip: MockRewardSource.armSelfDrip schedules one scheduledDrip at maturity that settles the reward, no keeper"); }); diff --git a/tsconfig.hardhat.json b/tsconfig.hardhat.json index cea7fd0..d4aae6e 100644 --- a/tsconfig.hardhat.json +++ b/tsconfig.hardhat.json @@ -10,6 +10,6 @@ "resolveJsonModule": true, "forceConsistentCasingInFileNames": true }, - "include": ["hardhat.config.cts", "scripts/deploy.ts", "scripts/smoke.ts", "scripts/enable-secondary.ts", "test/**/*.ts"], + "include": ["hardhat.config.cts", "scripts/**/*.ts", "test/**/*.ts"], "exclude": ["node_modules", "dist", "web", "src", "scripts/resolve-operator.ts"] } diff --git a/web/package.json b/web/package.json index 28b8f50..62c2a56 100644 --- a/web/package.json +++ b/web/package.json @@ -10,12 +10,8 @@ "preview": "vite preview" }, "dependencies": { - "@react-three/drei": "^10.7.7", - "@react-three/fiber": "^9.6.1", - "gsap": "^3.15.0", "react": "^19.1.0", "react-dom": "^19.1.0", - "three": "^0.184.0", "viem": "^2.47.0" }, "devDependencies": { diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index cfb59f2..aa2aae1 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -8,24 +8,12 @@ importers: .: dependencies: - '@react-three/drei': - specifier: ^10.7.7 - version: 10.7.7(@react-three/fiber@9.6.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.184.0))(@types/react@19.2.17)(@types/three@0.184.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.184.0) - '@react-three/fiber': - specifier: ^9.6.1 - version: 9.6.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.184.0) - gsap: - specifier: ^3.15.0 - version: 3.15.0 react: specifier: ^19.1.0 version: 19.2.7 react-dom: specifier: ^19.1.0 version: 19.2.7(react@19.2.7) - three: - specifier: ^0.184.0 - version: 0.184.0 viem: specifier: ^2.47.0 version: 2.52.2 @@ -113,10 +101,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.29.7': - resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} - engines: {node: '>=6.9.0'} - '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} @@ -129,9 +113,6 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@dimforge/rapier3d-compat@0.12.0': - resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} - '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -304,14 +285,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@mediapipe/tasks-vision@0.10.17': - resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} - - '@monogrid/gainmap-js@3.4.0': - resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==} - peerDependencies: - three: '>= 0.159.0' - '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} @@ -324,42 +297,6 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} - '@react-three/drei@10.7.7': - resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==} - peerDependencies: - '@react-three/fiber': ^9.0.0 - react: ^19 - react-dom: ^19 - three: '>=0.159' - peerDependenciesMeta: - react-dom: - optional: true - - '@react-three/fiber@9.6.1': - resolution: {integrity: sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==} - peerDependencies: - expo: '>=43.0' - expo-asset: '>=8.4' - expo-file-system: '>=11.0' - expo-gl: '>=11.0' - react: '>=19 <19.3' - react-dom: '>=19 <19.3' - react-native: '>=0.78' - three: '>=0.156' - peerDependenciesMeta: - expo: - optional: true - expo-asset: - optional: true - expo-file-system: - optional: true - expo-gl: - optional: true - react-dom: - optional: true - react-native: - optional: true - '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -510,9 +447,6 @@ packages: '@scure/bip39@1.6.0': resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} - '@tweenjs/tween.js@23.1.3': - resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -525,40 +459,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/draco3d@1.4.10': - resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} - '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/offscreencanvas@2019.7.3': - resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} - - '@types/react-reconciler@0.28.9': - resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} - peerDependencies: - '@types/react': '*' - - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - - '@types/stats.js@0.17.4': - resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} - - '@types/three@0.184.1': - resolution: {integrity: sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==} - - '@types/webxr@0.5.24': - resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} - - '@use-gesture/core@10.3.1': - resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} - - '@use-gesture/react@10.3.1': - resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} - peerDependencies: - react: '>= 16.8.0' - '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -576,49 +479,22 @@ packages: zod: optional: true - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.37: resolution: {integrity: sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==} engines: {node: '>=6.0.0'} hasBin: true - bidi-js@1.0.3: - resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} - browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - - camera-controls@3.1.2: - resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==} - engines: {node: '>=22.0.0', npm: '>=10.5.1'} - peerDependencies: - three: '>=0.126.1' - caniuse-lite@1.0.30001799: resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cross-env@7.0.3: - resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} - engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} - hasBin: true - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -628,16 +504,10 @@ packages: supports-color: optional: true - detect-gpu@5.0.70: - resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - draco3d@1.5.7: - resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} - electron-to-chromium@1.5.372: resolution: {integrity: sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==} @@ -662,12 +532,6 @@ packages: picomatch: optional: true - fflate@0.6.10: - resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} - - fflate@0.8.3: - resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -677,37 +541,11 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} - glsl-noise@0.0.0: - resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} - - gsap@3.15.0: - resolution: {integrity: sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==} - - hls.js@1.6.16: - resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} - - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - - immediate@3.0.6: - resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - - is-promise@2.2.2: - resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isows@1.0.7: resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} peerDependencies: ws: '*' - its-fine@2.0.0: - resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} - peerDependencies: - react: ^19.0.0 - jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -725,9 +563,6 @@ packages: engines: {node: '>=6'} hasBin: true - lie@3.3.0: - resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} - lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -805,20 +640,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - maath@0.10.8: - resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} - peerDependencies: - '@types/three': '>=0.134.0' - three: '>=0.134.0' - - meshline@3.3.1: - resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} - peerDependencies: - three: '>=0.137' - - meshoptimizer@1.1.1: - resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -839,10 +660,6 @@ packages: typescript: optional: true - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -854,12 +671,6 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - potpack@1.0.2: - resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} - - promise-worker-transferable@1.0.4: - resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} - react-dom@19.2.7: resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: @@ -869,23 +680,10 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} - react-use-measure@2.1.7: - resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} - peerDependencies: - react: '>=16.13' - react-dom: '>=16.13' - peerDependenciesMeta: - react-dom: - optional: true - react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - rollup@4.62.0: resolution: {integrity: sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -898,80 +696,20 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - stats-gl@2.4.2: - resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} - peerDependencies: - '@types/three': '*' - three: '*' - - stats.js@0.17.0: - resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} - - suspend-react@0.1.3: - resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} - peerDependencies: - react: '>=17.0' - - three-mesh-bvh@0.8.3: - resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} - peerDependencies: - three: '>= 0.159.0' - - three-stdlib@2.36.1: - resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==} - peerDependencies: - three: '>=0.128.0' - - three@0.184.0: - resolution: {integrity: sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==} - tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - troika-three-text@0.52.4: - resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} - peerDependencies: - three: '>=0.125.0' - - troika-three-utils@0.52.4: - resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==} - peerDependencies: - three: '>=0.125.0' - - troika-worker-utils@0.52.0: - resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} - - tunnel-rat@0.1.2: - resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - utility-types@3.11.0: - resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} - engines: {node: '>= 4'} - viem@2.52.2: resolution: {integrity: sha512-HSU12p5aD/kAPZfrlbCUqdiP4P/c6hQ9AhfTS51VbLUQIjkWd1d5EjrCx/SCxZ0zhZVRn4Iv5X5WDqXPG8Ubew==} peerDependencies: @@ -1020,17 +758,6 @@ packages: yaml: optional: true - webgl-constants@1.1.1: - resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} - - webgl-sdf-generator@1.1.1: - resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - ws@8.20.1: resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} @@ -1046,39 +773,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - zustand@4.5.7: - resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} - engines: {node: '>=12.7.0'} - peerDependencies: - '@types/react': '>=16.8' - immer: '>=9.0.6' - react: '>=16.8' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - - zustand@5.0.14: - resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} - engines: {node: '>=12.20.0'} - peerDependencies: - '@types/react': '>=18.0.0' - immer: '>=9.0.6' - react: '>=18.0.0' - use-sync-external-store: '>=1.2.0' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - use-sync-external-store: - optional: true - snapshots: '@adraffy/ens-normalize@1.11.1': {} @@ -1172,8 +866,6 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/runtime@7.29.7': {} - '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -1197,8 +889,6 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@dimforge/rapier3d-compat@0.12.0': {} - '@esbuild/aix-ppc64@0.25.12': optional: true @@ -1296,13 +986,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@mediapipe/tasks-vision@0.10.17': {} - - '@monogrid/gainmap-js@3.4.0(three@0.184.0)': - dependencies: - promise-worker-transferable: 1.0.4 - three: 0.184.0 - '@noble/ciphers@1.3.0': {} '@noble/curves@1.9.1': @@ -1311,59 +994,6 @@ snapshots: '@noble/hashes@1.8.0': {} - '@react-three/drei@10.7.7(@react-three/fiber@9.6.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.184.0))(@types/react@19.2.17)(@types/three@0.184.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.184.0)': - dependencies: - '@babel/runtime': 7.29.7 - '@mediapipe/tasks-vision': 0.10.17 - '@monogrid/gainmap-js': 3.4.0(three@0.184.0) - '@react-three/fiber': 9.6.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.184.0) - '@use-gesture/react': 10.3.1(react@19.2.7) - camera-controls: 3.1.2(three@0.184.0) - cross-env: 7.0.3 - detect-gpu: 5.0.70 - glsl-noise: 0.0.0 - hls.js: 1.6.16 - maath: 0.10.8(@types/three@0.184.1)(three@0.184.0) - meshline: 3.3.1(three@0.184.0) - react: 19.2.7 - stats-gl: 2.4.2(@types/three@0.184.1)(three@0.184.0) - stats.js: 0.17.0 - suspend-react: 0.1.3(react@19.2.7) - three: 0.184.0 - three-mesh-bvh: 0.8.3(three@0.184.0) - three-stdlib: 2.36.1(three@0.184.0) - troika-three-text: 0.52.4(three@0.184.0) - tunnel-rat: 0.1.2(@types/react@19.2.17)(react@19.2.7) - use-sync-external-store: 1.6.0(react@19.2.7) - utility-types: 3.11.0 - zustand: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) - optionalDependencies: - react-dom: 19.2.7(react@19.2.7) - transitivePeerDependencies: - - '@types/react' - - '@types/three' - - immer - - '@react-three/fiber@9.6.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.184.0)': - dependencies: - '@babel/runtime': 7.29.7 - '@types/webxr': 0.5.24 - base64-js: 1.5.1 - buffer: 6.0.3 - its-fine: 2.0.0(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-use-measure: 2.1.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - scheduler: 0.27.0 - suspend-react: 0.1.3(react@19.2.7) - three: 0.184.0 - use-sync-external-store: 1.6.0(react@19.2.7) - zustand: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) - optionalDependencies: - react-dom: 19.2.7(react@19.2.7) - transitivePeerDependencies: - - '@types/react' - - immer - '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.62.0': @@ -1454,8 +1084,6 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 - '@tweenjs/tween.js@23.1.3': {} - '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 @@ -1477,40 +1105,8 @@ snapshots: dependencies: '@babel/types': 7.29.7 - '@types/draco3d@1.4.10': {} - '@types/estree@1.0.9': {} - '@types/offscreencanvas@2019.7.3': {} - - '@types/react-reconciler@0.28.9(@types/react@19.2.17)': - dependencies: - '@types/react': 19.2.17 - - '@types/react@19.2.17': - dependencies: - csstype: 3.2.3 - - '@types/stats.js@0.17.4': {} - - '@types/three@0.184.1': - dependencies: - '@dimforge/rapier3d-compat': 0.12.0 - '@tweenjs/tween.js': 23.1.3 - '@types/stats.js': 0.17.4 - '@types/webxr': 0.5.24 - fflate: 0.8.3 - meshoptimizer: 1.1.1 - - '@types/webxr@0.5.24': {} - - '@use-gesture/core@10.3.1': {} - - '@use-gesture/react@10.3.1(react@19.2.7)': - dependencies: - '@use-gesture/core': 10.3.1 - react: 19.2.7 - '@vitejs/plugin-react@4.7.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: '@babel/core': 7.29.7 @@ -1525,14 +1121,8 @@ snapshots: abitype@1.2.3: {} - base64-js@1.5.1: {} - baseline-browser-mapping@2.10.37: {} - bidi-js@1.0.3: - dependencies: - require-from-string: 2.0.2 - browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.37 @@ -1541,44 +1131,17 @@ snapshots: node-releases: 2.0.47 update-browserslist-db: 1.2.3(browserslist@4.28.2) - buffer@6.0.3: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - camera-controls@3.1.2(three@0.184.0): - dependencies: - three: 0.184.0 - caniuse-lite@1.0.30001799: {} convert-source-map@2.0.0: {} - cross-env@7.0.3: - dependencies: - cross-spawn: 7.0.6 - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - csstype@3.2.3: {} - debug@4.4.3: dependencies: ms: 2.1.3 - detect-gpu@5.0.70: - dependencies: - webgl-constants: 1.1.1 - detect-libc@2.1.2: optional: true - draco3d@1.5.7: {} - electron-to-chromium@1.5.372: {} esbuild@0.25.12: @@ -1618,40 +1181,15 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - fflate@0.6.10: {} - - fflate@0.8.3: {} - fsevents@2.3.3: optional: true gensync@1.0.0-beta.2: {} - glsl-noise@0.0.0: {} - - gsap@3.15.0: {} - - hls.js@1.6.16: {} - - ieee754@1.2.1: {} - - immediate@3.0.6: {} - - is-promise@2.2.2: {} - - isexe@2.0.0: {} - isows@1.0.7(ws@8.20.1): dependencies: ws: 8.20.1 - its-fine@2.0.0(@types/react@19.2.17)(react@19.2.7): - dependencies: - '@types/react-reconciler': 0.28.9(@types/react@19.2.17) - react: 19.2.7 - transitivePeerDependencies: - - '@types/react' - jiti@2.7.0: optional: true @@ -1661,10 +1199,6 @@ snapshots: json5@2.2.3: {} - lie@3.3.0: - dependencies: - immediate: 3.0.6 - lightningcss-android-arm64@1.32.0: optional: true @@ -1719,17 +1253,6 @@ snapshots: dependencies: yallist: 3.1.1 - maath@0.10.8(@types/three@0.184.1)(three@0.184.0): - dependencies: - '@types/three': 0.184.1 - three: 0.184.0 - - meshline@3.3.1(three@0.184.0): - dependencies: - three: 0.184.0 - - meshoptimizer@1.1.1: {} - ms@2.1.3: {} nanoid@3.3.12: {} @@ -1749,8 +1272,6 @@ snapshots: transitivePeerDependencies: - zod - path-key@3.1.1: {} - picocolors@1.1.1: {} picomatch@4.0.4: {} @@ -1761,13 +1282,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - potpack@1.0.2: {} - - promise-worker-transferable@1.0.4: - dependencies: - is-promise: 2.2.2 - lie: 3.3.0 - react-dom@19.2.7(react@19.2.7): dependencies: react: 19.2.7 @@ -1775,16 +1289,8 @@ snapshots: react-refresh@0.17.0: {} - react-use-measure@2.1.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - react: 19.2.7 - optionalDependencies: - react-dom: 19.2.7(react@19.2.7) - react@19.2.7: {} - require-from-string@2.0.2: {} - rollup@4.62.0: dependencies: '@types/estree': 1.0.9 @@ -1820,80 +1326,19 @@ snapshots: semver@6.3.1: {} - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - source-map-js@1.2.1: {} - stats-gl@2.4.2(@types/three@0.184.1)(three@0.184.0): - dependencies: - '@types/three': 0.184.1 - three: 0.184.0 - - stats.js@0.17.0: {} - - suspend-react@0.1.3(react@19.2.7): - dependencies: - react: 19.2.7 - - three-mesh-bvh@0.8.3(three@0.184.0): - dependencies: - three: 0.184.0 - - three-stdlib@2.36.1(three@0.184.0): - dependencies: - '@types/draco3d': 1.4.10 - '@types/offscreencanvas': 2019.7.3 - '@types/webxr': 0.5.24 - draco3d: 1.5.7 - fflate: 0.6.10 - potpack: 1.0.2 - three: 0.184.0 - - three@0.184.0: {} - tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - troika-three-text@0.52.4(three@0.184.0): - dependencies: - bidi-js: 1.0.3 - three: 0.184.0 - troika-three-utils: 0.52.4(three@0.184.0) - troika-worker-utils: 0.52.0 - webgl-sdf-generator: 1.1.1 - - troika-three-utils@0.52.4(three@0.184.0): - dependencies: - three: 0.184.0 - - troika-worker-utils@0.52.0: {} - - tunnel-rat@0.1.2(@types/react@19.2.17)(react@19.2.7): - dependencies: - zustand: 4.5.7(@types/react@19.2.17)(react@19.2.7) - transitivePeerDependencies: - - '@types/react' - - immer - - react - update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 - use-sync-external-store@1.6.0(react@19.2.7): - dependencies: - react: 19.2.7 - - utility-types@3.11.0: {} - viem@2.52.2: dependencies: '@noble/curves': 1.9.1 @@ -1922,27 +1367,6 @@ snapshots: jiti: 2.7.0 lightningcss: 1.32.0 - webgl-constants@1.1.1: {} - - webgl-sdf-generator@1.1.1: {} - - which@2.0.2: - dependencies: - isexe: 2.0.0 - ws@8.20.1: {} yallist@3.1.1: {} - - zustand@4.5.7(@types/react@19.2.17)(react@19.2.7): - dependencies: - use-sync-external-store: 1.6.0(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - react: 19.2.7 - - zustand@5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)): - optionalDependencies: - '@types/react': 19.2.17 - react: 19.2.7 - use-sync-external-store: 1.6.0(react@19.2.7) diff --git a/web/src/App.css b/web/src/App.css index df80359..4227da9 100644 --- a/web/src/App.css +++ b/web/src/App.css @@ -180,122 +180,6 @@ a { color: inherit; } margin: 0 auto; } -/* ---- Sidebar ---- */ -.sb { - width: var(--sidebar-w); - flex-shrink: 0; - background: var(--bg-2); - border-right: 1px solid var(--line); - display: flex; - flex-direction: column; - padding: 1.5rem 0.75rem 1.25rem; - position: sticky; - top: 0; - height: 100vh; -} - -.sb-brand { - display: flex; - align-items: center; - gap: 0.625rem; - background: none; - border: none; - cursor: pointer; - padding: 0.5rem 0.6rem; - margin-bottom: 2rem; -} -/* Logo mark sits on a small warm chip so the navy runner stays legible on the - dark-blue sidebar (it would otherwise sink into the background). */ -.sb-logo { - flex-shrink: 0; - display: grid; - place-items: center; - width: 34px; - height: 34px; - border-radius: 9px; - background: linear-gradient(135deg, #f4f4f4, #d6d6d6); - box-shadow: 0 0 0 1px rgba(0,0,0,0.25), 0 4px 14px rgba(0,0,0,0.35), 0 0 16px var(--amber-glow); -} -.sb-logo-svg { display: block; } -.sb-wordmark { - font-family: var(--font-display); - font-weight: 700; - font-size: 1.35rem; - letter-spacing: -0.02em; - color: var(--text); -} - -.sb-nav { display: flex; flex-direction: column; gap: 0.125rem; } - -.sb-link { - position: relative; - display: flex; - align-items: center; - gap: 0.75rem; - background: none; - border: none; - cursor: pointer; - width: 100%; - text-align: left; - padding: 0.6rem 0.75rem; - border-radius: var(--radius-sm); - color: var(--text-2); - font-family: var(--font-body); - font-size: 0.94rem; - font-weight: 500; - transition: color 0.18s ease, background 0.18s ease; -} -.sb-link-bar { - position: absolute; - left: -0.75rem; - top: 50%; - transform: translateY(-50%) scaleY(0); - width: 3px; - height: 1.25rem; - border-radius: 0 3px 3px 0; - background: var(--amber); - box-shadow: 0 0 10px var(--amber-glow); - transition: transform 0.18s ease; -} -.sb-link:hover { color: var(--text); background: rgba(255,255,255,0.02); } -.sb-link-active { color: var(--text); } -.sb-link-active .sb-link-bar { transform: translateY(-50%) scaleY(1); } -.sb-link-admin .sb-link-label::after { - content: "admin"; - margin-left: 0.5rem; - font-family: var(--font-mono); - font-size: 0.6rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--amber); - opacity: 0.7; -} - -.sb-foot { margin-top: auto; padding: 0.5rem 0.5rem 0; } -.sb-foot-label { display: block; margin-bottom: 0.5rem; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-3); } -.sb-roles { - display: flex; - background: var(--bg); - border: 1px solid var(--line); - border-radius: var(--radius-sm); - padding: 3px; - gap: 3px; -} -.sb-role { - flex: 1; - border: none; - background: none; - cursor: pointer; - padding: 0.4rem 0.5rem; - border-radius: 7px; - color: var(--text-2); - font-family: var(--font-body); - font-size: 0.82rem; - font-weight: 500; - transition: all 0.18s ease; -} -.sb-role-active { background: var(--amber-soft); color: var(--amber-hi); } - /* ============================================================================= ACCOUNT MENU (top-right) ============================================================================= */ @@ -426,15 +310,6 @@ a { color: inherit; } } .how-modal-close:hover { color: var(--text); } -/* Animated backdrop behind the whole landing (fixed) + legibility scrim. */ -.landing-bg { position: fixed; inset: 0; z-index: 0; } -.landing-bg canvas { display: block; width: 100% !important; height: 100% !important; } -.landing-scrim { - position: fixed; inset: 0; z-index: 1; pointer-events: none; - background: - linear-gradient(180deg, rgba(14,30,46,0.16) 0%, rgba(14,30,46,0.40) 52%, rgba(14,30,46,0.86) 100%), - radial-gradient(ellipse 95% 85% at 50% 42%, transparent 52%, rgba(0,0,0,0.55) 100%); -} .landing-shell > .hero, .landing-shell > .hiw { position: relative; z-index: 2; } @@ -897,7 +772,6 @@ select option { background: var(--bg-2); color: var(--text); } } @media (max-width: 860px) { - .sb { position: fixed; left: 0; top: 0; z-index: 100; transform: translateX(-100%); transition: transform 0.2s ease; } .shell { padding-left: 0; } .hero-content, .hiw { padding-left: 1.5rem; padding-right: 1.5rem; } .container, .shell-topbar { padding-left: 1rem; padding-right: 1rem; } diff --git a/web/src/components/Beams.css b/web/src/components/Beams.css deleted file mode 100644 index 6c5dac2..0000000 --- a/web/src/components/Beams.css +++ /dev/null @@ -1,5 +0,0 @@ -.beams-container { - position: relative; - width: 100%; - height: 100%; -} diff --git a/web/src/components/Beams.jsx b/web/src/components/Beams.jsx deleted file mode 100644 index 9afae98..0000000 --- a/web/src/components/Beams.jsx +++ /dev/null @@ -1,309 +0,0 @@ -/* eslint-disable react/no-unknown-property */ -import { forwardRef, useImperativeHandle, useEffect, useRef, useMemo } from 'react'; - -import * as THREE from 'three'; - -import { Canvas, useFrame } from '@react-three/fiber'; -import { PerspectiveCamera } from '@react-three/drei'; -import { degToRad } from 'three/src/math/MathUtils.js'; - -import './Beams.css'; - -function extendMaterial(BaseMaterial, cfg) { - const physical = THREE.ShaderLib.physical; - const { vertexShader: baseVert, fragmentShader: baseFrag, uniforms: baseUniforms } = physical; - const baseDefines = physical.defines ?? {}; - - const uniforms = THREE.UniformsUtils.clone(baseUniforms); - - const defaults = new BaseMaterial(cfg.material || {}); - - if (defaults.color) uniforms.diffuse.value = defaults.color; - if ('roughness' in defaults) uniforms.roughness.value = defaults.roughness; - if ('metalness' in defaults) uniforms.metalness.value = defaults.metalness; - if ('envMap' in defaults) uniforms.envMap.value = defaults.envMap; - if ('envMapIntensity' in defaults) uniforms.envMapIntensity.value = defaults.envMapIntensity; - - Object.entries(cfg.uniforms ?? {}).forEach(([key, u]) => { - uniforms[key] = u !== null && typeof u === 'object' && 'value' in u ? u : { value: u }; - }); - - let vert = `${cfg.header}\n${cfg.vertexHeader ?? ''}\n${baseVert}`; - let frag = `${cfg.header}\n${cfg.fragmentHeader ?? ''}\n${baseFrag}`; - - for (const [inc, code] of Object.entries(cfg.vertex ?? {})) { - vert = vert.replace(inc, `${inc}\n${code}`); - } - for (const [inc, code] of Object.entries(cfg.fragment ?? {})) { - frag = frag.replace(inc, `${inc}\n${code}`); - } - - const mat = new THREE.ShaderMaterial({ - defines: { ...baseDefines }, - uniforms, - vertexShader: vert, - fragmentShader: frag, - lights: true, - fog: !!cfg.material?.fog - }); - - return mat; -} - -const CanvasWrapper = ({ children }) => ( - - {children} - -); - -const hexToNormalizedRGB = hex => { - const clean = hex.replace('#', ''); - const r = parseInt(clean.substring(0, 2), 16); - const g = parseInt(clean.substring(2, 4), 16); - const b = parseInt(clean.substring(4, 6), 16); - return [r / 255, g / 255, b / 255]; -}; - -const noise = ` -float random (in vec2 st) { - return fract(sin(dot(st.xy, - vec2(12.9898,78.233)))* - 43758.5453123); -} -float noise (in vec2 st) { - vec2 i = floor(st); - vec2 f = fract(st); - float a = random(i); - float b = random(i + vec2(1.0, 0.0)); - float c = random(i + vec2(0.0, 1.0)); - float d = random(i + vec2(1.0, 1.0)); - vec2 u = f * f * (3.0 - 2.0 * f); - return mix(a, b, u.x) + - (c - a)* u.y * (1.0 - u.x) + - (d - b) * u.x * u.y; -} -vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);} -vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;} -vec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);} -float cnoise(vec3 P){ - vec3 Pi0 = floor(P); - vec3 Pi1 = Pi0 + vec3(1.0); - Pi0 = mod(Pi0, 289.0); - Pi1 = mod(Pi1, 289.0); - vec3 Pf0 = fract(P); - vec3 Pf1 = Pf0 - vec3(1.0); - vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); - vec4 iy = vec4(Pi0.yy, Pi1.yy); - vec4 iz0 = Pi0.zzzz; - vec4 iz1 = Pi1.zzzz; - vec4 ixy = permute(permute(ix) + iy); - vec4 ixy0 = permute(ixy + iz0); - vec4 ixy1 = permute(ixy + iz1); - vec4 gx0 = ixy0 / 7.0; - vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5; - gx0 = fract(gx0); - vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); - vec4 sz0 = step(gz0, vec4(0.0)); - gx0 -= sz0 * (step(0.0, gx0) - 0.5); - gy0 -= sz0 * (step(0.0, gy0) - 0.5); - vec4 gx1 = ixy1 / 7.0; - vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5; - gx1 = fract(gx1); - vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); - vec4 sz1 = step(gz1, vec4(0.0)); - gx1 -= sz1 * (step(0.0, gx1) - 0.5); - gy1 -= sz1 * (step(0.0, gy1) - 0.5); - vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); - vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); - vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); - vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); - vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); - vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); - vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); - vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); - vec4 norm0 = taylorInvSqrt(vec4(dot(g000,g000),dot(g010,g010),dot(g100,g100),dot(g110,g110))); - g000 *= norm0.x; g010 *= norm0.y; g100 *= norm0.z; g110 *= norm0.w; - vec4 norm1 = taylorInvSqrt(vec4(dot(g001,g001),dot(g011,g011),dot(g101,g101),dot(g111,g111))); - g001 *= norm1.x; g011 *= norm1.y; g101 *= norm1.z; g111 *= norm1.w; - float n000 = dot(g000, Pf0); - float n100 = dot(g100, vec3(Pf1.x,Pf0.yz)); - float n010 = dot(g010, vec3(Pf0.x,Pf1.y,Pf0.z)); - float n110 = dot(g110, vec3(Pf1.xy,Pf0.z)); - float n001 = dot(g001, vec3(Pf0.xy,Pf1.z)); - float n101 = dot(g101, vec3(Pf1.x,Pf0.y,Pf1.z)); - float n011 = dot(g011, vec3(Pf0.x,Pf1.yz)); - float n111 = dot(g111, Pf1); - vec3 fade_xyz = fade(Pf0); - vec4 n_z = mix(vec4(n000,n100,n010,n110),vec4(n001,n101,n011,n111),fade_xyz.z); - vec2 n_yz = mix(n_z.xy,n_z.zw,fade_xyz.y); - float n_xyz = mix(n_yz.x,n_yz.y,fade_xyz.x); - return 2.2 * n_xyz; -} -`; - -const Beams = ({ - beamWidth = 2, - beamHeight = 15, - beamNumber = 12, - lightColor = '#ffffff', - speed = 2, - noiseIntensity = 1.75, - scale = 0.2, - rotation = 0 -}) => { - const meshRef = useRef(null); - const beamMaterial = useMemo( - () => - extendMaterial(THREE.MeshStandardMaterial, { - header: ` - varying vec3 vEye; - varying float vNoise; - varying vec2 vUv; - varying vec3 vPosition; - uniform float time; - uniform float uSpeed; - uniform float uNoiseIntensity; - uniform float uScale; - ${noise}`, - vertexHeader: ` - float getPos(vec3 pos) { - vec3 noisePos = - vec3(pos.x * 0., pos.y - uv.y, pos.z + time * uSpeed * 3.) * uScale; - return cnoise(noisePos); - } - vec3 getCurrentPos(vec3 pos) { - vec3 newpos = pos; - newpos.z += getPos(pos); - return newpos; - } - vec3 getNormal(vec3 pos) { - vec3 curpos = getCurrentPos(pos); - vec3 nextposX = getCurrentPos(pos + vec3(0.01, 0.0, 0.0)); - vec3 nextposZ = getCurrentPos(pos + vec3(0.0, -0.01, 0.0)); - vec3 tangentX = normalize(nextposX - curpos); - vec3 tangentZ = normalize(nextposZ - curpos); - return normalize(cross(tangentZ, tangentX)); - }`, - fragmentHeader: '', - vertex: { - '#include ': `transformed.z += getPos(transformed.xyz);`, - '#include ': `objectNormal = getNormal(position.xyz);` - }, - fragment: { - '#include ': ` - float randomNoise = noise(gl_FragCoord.xy); - gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;` - }, - material: { fog: true }, - uniforms: { - diffuse: new THREE.Color(...hexToNormalizedRGB('#000000')), - time: { shared: true, mixed: true, linked: true, value: 0 }, - roughness: 0.3, - metalness: 0.3, - uSpeed: { shared: true, mixed: true, linked: true, value: speed }, - envMapIntensity: 10, - uNoiseIntensity: noiseIntensity, - uScale: scale - } - }), - [speed, noiseIntensity, scale] - ); - - return ( - - - - - - - {/* Wafer DA: blend the canvas into the deep night-blue base instead of pure black. */} - - - - ); -}; - -function createStackedPlanesBufferGeometry(n, width, height, spacing, heightSegments) { - const geometry = new THREE.BufferGeometry(); - const numVertices = n * (heightSegments + 1) * 2; - const numFaces = n * heightSegments * 2; - const positions = new Float32Array(numVertices * 3); - const indices = new Uint32Array(numFaces * 3); - const uvs = new Float32Array(numVertices * 2); - - let vertexOffset = 0; - let indexOffset = 0; - let uvOffset = 0; - const totalWidth = n * width + (n - 1) * spacing; - const xOffsetBase = -totalWidth / 2; - - for (let i = 0; i < n; i++) { - const xOffset = xOffsetBase + i * (width + spacing); - const uvXOffset = Math.random() * 300; - const uvYOffset = Math.random() * 300; - - for (let j = 0; j <= heightSegments; j++) { - const y = height * (j / heightSegments - 0.5); - const v0 = [xOffset, y, 0]; - const v1 = [xOffset + width, y, 0]; - positions.set([...v0, ...v1], vertexOffset * 3); - - const uvY = j / heightSegments; - uvs.set([uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset], uvOffset); - - if (j < heightSegments) { - const a = vertexOffset, - b = vertexOffset + 1, - c = vertexOffset + 2, - d = vertexOffset + 3; - indices.set([a, b, c, c, b, d], indexOffset); - indexOffset += 6; - } - vertexOffset += 2; - uvOffset += 4; - } - } - - geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); - geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2)); - geometry.setIndex(new THREE.BufferAttribute(indices, 1)); - geometry.computeVertexNormals(); - return geometry; -} - -const MergedPlanes = forwardRef(({ material, width, count, height }, ref) => { - const mesh = useRef(null); - useImperativeHandle(ref, () => mesh.current); - const geometry = useMemo( - () => createStackedPlanesBufferGeometry(count, width, height, 0, 100), - [count, width, height] - ); - useFrame((_, delta) => { - mesh.current.material.uniforms.time.value += 0.1 * delta; - }); - return ; -}); -MergedPlanes.displayName = 'MergedPlanes'; - -const PlaneNoise = forwardRef((props, ref) => ( - -)); -PlaneNoise.displayName = 'PlaneNoise'; - -const DirLight = ({ position, color }) => { - const dir = useRef(null); - useEffect(() => { - if (!dir.current) return; - const cam = dir.current.shadow.camera; - if (!cam) return; - cam.top = 24; - cam.bottom = -24; - cam.left = -24; - cam.right = 24; - cam.far = 64; - dir.current.shadow.bias = -0.004; - }, []); - return ; -}; - -export default Beams; diff --git a/web/src/components/CardNav.css b/web/src/components/CardNav.css deleted file mode 100644 index 7732bb3..0000000 --- a/web/src/components/CardNav.css +++ /dev/null @@ -1,169 +0,0 @@ -/* Wafer-themed CardNav (React Bits base, adapted to the night-blue × orange DA). */ -.card-nav-container { - position: fixed; - top: 1.25rem; - left: 50%; - transform: translateX(-50%); - width: 92%; - max-width: 720px; - z-index: 90; - box-sizing: border-box; -} - -.card-nav { - display: block; - height: 60px; - padding: 0; - border: 1px solid var(--line); - border-radius: 0.9rem; - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(0, 0, 0, 0.2), 0 0 24px rgba(240, 162, 60, 0.06); - position: relative; - overflow: hidden; - will-change: height; - backdrop-filter: blur(8px); -} - -.card-nav-top { - position: absolute; - top: 0; - left: 0; - right: 0; - height: 60px; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.5rem 0.6rem 0.55rem 1.1rem; - z-index: 2; -} - -.hamburger-menu { - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - cursor: pointer; - gap: 6px; - padding-right: 0.4rem; -} -.hamburger-menu:hover .hamburger-line { opacity: 0.7; } -.hamburger-line { - width: 26px; - height: 2px; - background-color: currentColor; - border-radius: 2px; - transition: transform 0.25s ease, opacity 0.2s ease, margin 0.3s ease; - transform-origin: 50% 50%; -} -.hamburger-menu.open .hamburger-line:first-child { transform: translateY(4px) rotate(45deg); } -.hamburger-menu.open .hamburger-line:last-child { transform: translateY(-4px) rotate(-45deg); } - -/* Logo (center) — button reset + Wafer glyph + wordmark. */ -.logo-container { - display: flex; - align-items: center; - gap: 0.55rem; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - background: none; - border: none; - cursor: pointer; - padding: 0.25rem 0.5rem; -} -.cn-logo { - display: grid; - place-items: center; - width: 30px; - height: 30px; - border-radius: 8px; - background: linear-gradient(135deg, #F6F0E6, #E7DAC6); - color: #0E1E2E; - box-shadow: 0 0 16px rgba(240, 162, 60, 0.3); -} -.cn-logo-svg { display: block; } -.cn-wordmark { - font-family: var(--font-display); - font-weight: 700; - font-size: 1.2rem; - letter-spacing: -0.02em; - color: var(--text); -} - -/* Right slot (network chip). */ -.card-nav-cta { display: flex; align-items: center; height: 100%; } -.cn-net { - display: inline-flex; - align-items: center; - gap: 0.45rem; - font-size: 0.72rem; - letter-spacing: 0.06em; - text-transform: uppercase; - color: var(--text-2); - border: 1px solid var(--line); - border-radius: 999px; - padding: 0.35rem 0.7rem; - font-family: var(--font-mono); -} -.cn-net-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--up); box-shadow: 0 0 8px var(--up); } - -/* Expanding content (cards). */ -.card-nav-content { - position: absolute; - left: 0; - right: 0; - top: 60px; - bottom: 0; - padding: 0.6rem; - display: flex; - align-items: stretch; - gap: 0.6rem; - visibility: hidden; - pointer-events: none; - z-index: 1; -} -.card-nav.open .card-nav-content { visibility: visible; pointer-events: auto; } - -.nav-card { - flex: 1 1 0; - min-width: 0; - border-radius: 0.7rem; - border: 1px solid rgba(255, 255, 255, 0.05); - display: flex; - flex-direction: column; - padding: 0.9rem 1rem; - gap: 0.5rem; - user-select: none; -} -.nav-card-label { - font-family: var(--font-display); - font-weight: 600; - font-size: 1.15rem; - letter-spacing: -0.01em; -} -.nav-card-links { margin-top: auto; display: flex; flex-direction: column; gap: 0.15rem; align-items: flex-start; } -.nav-card-link { - font-size: 0.92rem; - cursor: pointer; - background: none; - border: none; - color: inherit; - font-family: inherit; - padding: 0.15rem 0; - text-align: left; - transition: opacity 0.2s ease; - display: inline-flex; - align-items: center; - gap: 0.4rem; - opacity: 0.92; -} -.nav-card-link:hover { opacity: 0.6; } -.nav-card-link-icon { flex-shrink: 0; } - -@media (max-width: 768px) { - .card-nav-container { width: 94%; top: 0.9rem; } - .card-nav-cta { display: none; } - .card-nav-content { flex-direction: column; align-items: stretch; gap: 0.5rem; } - .nav-card { min-height: 56px; } -} diff --git a/web/src/components/CardNav.jsx b/web/src/components/CardNav.jsx deleted file mode 100644 index 9fcdac0..0000000 --- a/web/src/components/CardNav.jsx +++ /dev/null @@ -1,221 +0,0 @@ -import { useLayoutEffect, useRef, useState } from 'react'; -import { gsap } from 'gsap'; -import './CardNav.css'; - -// Wafer-adapted CardNav (React Bits). Differences from the upstream component: -// - links route via onClick (SPA tab switch) instead of , and close the menu; -// - the logo is the Wafer glyph + wordmark (onLogoClick -> home); -// - the right slot renders a passed `cta` node (a static network chip here — the -// account menu lives top-right, outside this overflow:hidden nav, so its dropdown -// isn't clipped); -// - the arrow icon is inline SVG (no react-icons dependency). - -function ArrowIcon() { - return ( - - ); -} - -function WaferGlyph() { - return ( - - ); -} - -const CardNav = ({ - items, - cta, - onLogoClick, - className = '', - ease = 'power3.out', - baseColor = '#12283A', - menuColor = '#F4EEE3' -}) => { - const [isHamburgerOpen, setIsHamburgerOpen] = useState(false); - const [isExpanded, setIsExpanded] = useState(false); - const navRef = useRef(null); - const cardsRef = useRef([]); - const tlRef = useRef(null); - - const calculateHeight = () => { - const navEl = navRef.current; - if (!navEl) return 260; - - const isMobile = window.matchMedia('(max-width: 768px)').matches; - if (isMobile) { - const contentEl = navEl.querySelector('.card-nav-content'); - if (contentEl) { - const wasVisible = contentEl.style.visibility; - const wasPointerEvents = contentEl.style.pointerEvents; - const wasPosition = contentEl.style.position; - const wasHeight = contentEl.style.height; - - contentEl.style.visibility = 'visible'; - contentEl.style.pointerEvents = 'auto'; - contentEl.style.position = 'static'; - contentEl.style.height = 'auto'; - - contentEl.offsetHeight; - - const topBar = 60; - const padding = 16; - const contentHeight = contentEl.scrollHeight; - - contentEl.style.visibility = wasVisible; - contentEl.style.pointerEvents = wasPointerEvents; - contentEl.style.position = wasPosition; - contentEl.style.height = wasHeight; - - return topBar + contentHeight + padding; - } - } - return 260; - }; - - const createTimeline = () => { - const navEl = navRef.current; - if (!navEl) return null; - - gsap.set(navEl, { height: 60, overflow: 'hidden' }); - gsap.set(cardsRef.current, { y: 50, opacity: 0 }); - - const tl = gsap.timeline({ paused: true }); - - tl.to(navEl, { height: calculateHeight, duration: 0.4, ease }); - tl.to(cardsRef.current, { y: 0, opacity: 1, duration: 0.4, ease, stagger: 0.08 }, '-=0.1'); - - return tl; - }; - - useLayoutEffect(() => { - const tl = createTimeline(); - tlRef.current = tl; - return () => { - tl?.kill(); - tlRef.current = null; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ease, items]); - - useLayoutEffect(() => { - const handleResize = () => { - if (!tlRef.current) return; - if (isExpanded) { - const newHeight = calculateHeight(); - gsap.set(navRef.current, { height: newHeight }); - tlRef.current.kill(); - const newTl = createTimeline(); - if (newTl) { - newTl.progress(1); - tlRef.current = newTl; - } - } else { - tlRef.current.kill(); - const newTl = createTimeline(); - if (newTl) tlRef.current = newTl; - } - }; - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isExpanded]); - - const closeMenu = () => { - const tl = tlRef.current; - setIsHamburgerOpen(false); - if (tl && isExpanded) { - tl.eventCallback('onReverseComplete', () => setIsExpanded(false)); - tl.reverse(); - } else { - setIsExpanded(false); - } - }; - - const toggleMenu = () => { - const tl = tlRef.current; - if (!tl) return; - if (!isExpanded) { - setIsHamburgerOpen(true); - setIsExpanded(true); - tl.play(0); - } else { - setIsHamburgerOpen(false); - tl.eventCallback('onReverseComplete', () => setIsExpanded(false)); - tl.reverse(); - } - }; - - const setCardRef = i => el => { - if (el) cardsRef.current[i] = el; - }; - - const handleLink = lnk => { - lnk.onClick?.(); - closeMenu(); - }; - - return ( -
- -
- ); -}; - -export default CardNav; diff --git a/web/src/components/Dashboard.jsx b/web/src/components/Dashboard.jsx deleted file mode 100644 index 62f7e3e..0000000 --- a/web/src/components/Dashboard.jsx +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { VAULT_CONFIGURED, poolDisplayName, CATEGORY_LABEL } from "../lib/config.js"; -import { formatHbar, formatNav, assetsForShares } from "../lib/format.js"; - -// Dashboard: the connected wallet's pool-share positions + HBAR value at current -// NAV, plus a count of pending redemption requests. Reads share balances from -// each pool's share-token ERC-20 facade (no vault aggregate view). -export default function Dashboard({ contracts, account, refreshKey }) { - const [rows, setRows] = useState([]); - const [hbarBalance, setHbarBalance] = useState(null); - const [pendingRedemptions, setPendingRedemptions] = useState(0); - - useEffect(() => { - if (!contracts) return; - let cancelled = false; - (async () => { - try { - const pools = await contracts.getPools(); - const [balances, hbar, queue] = await Promise.all([ - Promise.all(pools.map((p) => contracts.getShareBalance(p.shareToken))), - contracts.getHbarBalance(), - contracts.getRedemptionQueue(), - ]); - if (cancelled) return; - const next = pools.map((p, i) => { - const shares = balances[i] ?? 0n; - const value = assetsForShares(shares, p.navPerShare); - return { - poolId: p.poolId, - name: poolDisplayName(p.category, p.class), - network: CATEGORY_LABEL[p.category] ?? "—", - navPerShare: p.navPerShare, - shares, - value, - }; - }); - setRows(next.filter((r) => r.shares > 0n).length ? next : next); - setHbarBalance(hbar); - if (account) { - setPendingRedemptions(queue.filter((r) => !r.filled && r.investor?.toLowerCase?.() === account.toLowerCase()).length); - } - } catch { - // leave previous - } - })(); - return () => { cancelled = true; }; - }, [contracts, account, refreshKey]); - - const totalValue = rows.reduce((acc, r) => acc + (r.value ?? 0n), 0n); - - return ( -
- {!VAULT_CONFIGURED && ( -
- No vault configured — set VITE_VAULT_ADDRESS to a deployed WaferVault to see live positions. -
- )} - -
-

Your Portfolio

-
-
-
Total share value
-
{formatHbar(totalValue)} HBAR
-
-
-
HBAR HBAR balance
-
{hbarBalance == null ? "—" : formatHbar(hbarBalance)}
-
-
-
Pending redemptions
-
{pendingRedemptions}
-
-
-
- -
-

Pool positions

-
- - - - - - - - - - - {rows.filter((r) => r.shares > 0n).map((r) => ( - - - - - - - ))} - {rows.filter((r) => r.shares > 0n).length === 0 && ( - - )} - -
PoolYour sharesNAV / shareValue
-
- {r.name} - {r.network} -
-
{formatHbar(r.shares)}
{formatNav(r.navPerShare)}
{formatHbar(r.value)} HBAR
No positions yet — deposit into a pool to mint shares.
-
-
-
- ); -} diff --git a/web/src/components/Pools.jsx b/web/src/components/Pools.jsx deleted file mode 100644 index f7b3c7b..0000000 --- a/web/src/components/Pools.jsx +++ /dev/null @@ -1,175 +0,0 @@ -import React, { useEffect, useState, useMemo } from "react"; -import DepositWidget from "./DepositWidget.jsx"; -import PoolDetail from "./PoolDetail.jsx"; -import { VAULT_CONFIGURED, CATEGORY_LABEL, CATEGORY_LOGO, RISK_CLASSES, poolDisplayName } from "../lib/config.js"; -import { formatHbar, formatNav, dealApr, formatPercent } from "../lib/format.js"; -import { readDealMeta } from "../lib/mirror.js"; - -// Pools / Fund-a-category. Lists pools by category × risk class with NAV, TVL, -// and trailing (blended) APR; under each, the deals it finances (from the Mirror -// Node DealProposed feed). Clicking a pool opens its detail + deposit/redeem. -// -// Trailing APR per pool = the volume-weighted blend of its financed deals' APRs -// (advance/expected/term), the realized return curve the pool NAV tracks. - -function poolTrailingApr(pool, deals) { - const financed = deals.filter((d) => d.poolId === pool.poolId && (d.status === 3 || d.status === 4)); // Financed | Repaid - let wSum = 0, num = 0; - for (const d of financed) { - const apr = dealApr(d.advance, d.expected, d.term); - if (apr == null) continue; - const w = Number(d.advance); - wSum += w; num += apr * w; - } - return wSum > 0 ? num / wSum : null; -} - -export default function Pools({ contracts, onStatus, refreshKey }) { - const [pools, setPools] = useState([]); - const [deals, setDeals] = useState([]); - const [selected, setSelected] = useState(null); - const [search, setSearch] = useState(""); - const [loading, setLoading] = useState(true); - - useEffect(() => { - if (!contracts) return; - let cancelled = false; - (async () => { - try { - const [list, dealList] = await Promise.all([ - contracts.getPools(), - contracts.getDeals(), - ]); - if (cancelled) return; - setPools(list); - setDeals(dealList); - } catch { - // leave previous - } finally { - if (!cancelled) setLoading(false); - } - })(); - return () => { cancelled = true; }; - }, [contracts, refreshKey]); - - const decorated = useMemo(() => pools.map((p) => ({ - ...p, - name: poolDisplayName(p.category, p.class), - network: CATEGORY_LABEL[p.category] ?? "—", - logo: CATEGORY_LOGO[p.category] ?? "/logos/hedera.svg", - risk: RISK_CLASSES[p.class] ?? "—", - apr: poolTrailingApr(p, deals), - })), [pools, deals]); - - const filtered = decorated.filter((p) => - !search || - p.name.toLowerCase().includes(search.toLowerCase()) || - p.network.toLowerCase().includes(search.toLowerCase()) - ); - - if (selected != null) { - const pool = decorated.find((p) => p.poolId === selected); - if (!pool) { setSelected(null); return null; } - const poolDeals = deals.filter((d) => d.poolId === pool.poolId); - return ( -
-
- -
-
-
{pool.network}
- {pool.name} - Risk {pool.risk} -
-
-
-
- - -
-
- ); - } - - return ( -
- {!VAULT_CONFIGURED && ( -
- No vault configured — set VITE_VAULT_ADDRESS to a deployed WaferVault. -
- )} -
-
-
- Pools - {decorated.length} -
-
-
- - - - - setSearch(e.target.value)} /> -
-
-
- -
- - - - - - - - - - - - - - - {filtered.map((p) => { - const dealCount = deals.filter((d) => d.poolId === p.poolId).length; - return ( - setSelected(p.poolId)}> - - - - - - - - - - ); - })} - {!loading && filtered.length === 0 && ( - - )} - -
CategoryPoolClassNAV / shareTVLTrailing APRDealsStatus
-
-
- {p.network} -
-
-
-
-
- {p.name} - {p.network} -
-
-
{p.risk}
{formatNav(p.navPerShare)}
{formatHbar(p.totalAssets)} HBAR
{p.apr == null ? "—" : formatPercent(p.apr)}
{dealCount}
{p.status === 1 ? "Paused" : "Active"}
No pools yet. An admin creates pools by category × class.
-
-
-
- ); -} diff --git a/web/src/components/Sidebar.jsx b/web/src/components/Sidebar.jsx deleted file mode 100644 index d6ba49c..0000000 --- a/web/src/components/Sidebar.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from "react"; - -// Left sidebar shell (ART-DIRECTION §3). Wafer wordmark + slice glyph at top, -// vertical nav (Pools / Portfolio / Activity, + role-gated extras), and the -// role switch (Investor / Admin) pinned to the bottom. The role switch is a -// VIEW toggle in app state — not a wallet change. Admin nav + screens are -// hidden in Investor mode and only appear in Admin mode. - -export default function Sidebar({ activeTab, onTabChange, role, onRoleChange, roles }) { - const isAdminView = role === "admin"; - - // Base investor nav. Operator is shown when the connected wallet is a - // whitelisted operator (or owner, so they can demo it). Admin tab only in - // Admin view (and only meaningful for the owner, but the toggle is freely - // available per spec — actions revert with a clear notice otherwise). - const nav = [ - { id: "pools", label: "Pools" }, - { id: "dashboard", label: "Portfolio" }, - { id: "queue", label: "Queue" }, - { id: "secondary", label: "Market" }, - { id: "activity", label: "Activity" }, - ]; - if (roles?.isOperator || roles?.isOwner) nav.splice(3, 0, { id: "operator", label: "Operator" }); - if (isAdminView) nav.push({ id: "admin", label: "Admin" }); - - return ( - - ); -}