diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md new file mode 100644 index 0000000..9525226 --- /dev/null +++ b/.agents/AGENTS.md @@ -0,0 +1,21 @@ +# Universal Anti-Hallucination & Verification Protocol (MANDATORY FOR ALL AGENTS) + +## 1. Zero-Hallucination Policy & Strict Verification (Universal Scope) +- **Universal Applicability:** This protocol applies universally to ALL technologies, domains, and tools — including **MCP (Model Context Protocol) servers/tools**, smart contracts, blockchain SDKs (e.g., Zama, Circle, Viem), UI frameworks (e.g., shadcn, Next.js, Tailwind), cloud APIs, databases, and system architectures. +- **Never Assume or Guess:** Do NOT invent, guess, or hallucinate function signatures, MCP tool parameters, contract ABIs, addresses, API endpoints, CSS classes, or configuration headers from training data memory. +- **Read Authoritative Sources FIRST:** Before calling any tool, writing code, or generating documentation, you MUST explicitly read and verify ground-truth sources: + 1. **MCP Servers & Tools:** Before calling lazy-loaded MCP tools or using MCP resources, always read their schema definitions (`.json`), check available tools/resources (`list_resources`, `read_resource`), and review server instructions (`instructions.md`). + 2. **Local Codebase & ABIs:** Inspect existing files (e.g., ABI files, types, interfaces, utility wrappers) to verify exact names, types, and signatures in use. + 3. **Installed Dependencies & Skills:** When integrating third-party SDKs or frameworks, inspect `node_modules` or read local skill instructions (`SKILL.md`) and reference docs (`references/`) before implementing domain logic. + +## 2. Mandatory "Think -> Read -> Plan -> Execute" Workflow +Before executing any coding, configuration, or integration task across any technology stack: +1. **Think & Analyze:** Identify exact information needed (e.g., MCP tool schemas, API parameters, contract addresses, decimal scaling rules). +2. **Read & Verify:** Use read tools (`view_file`, `grep_search`, `read_resource`, `call_mcp_tool`) to verify 100% accurate data from ground-truth sources. +3. **Plan:** Outline exact changes or tool invocations, confirming that every name, parameter, and address matches verified sources without guessing. +4. **Execute & Audit:** Apply changes and double-check against ground truth to ensure zero discrepancies, syntax errors, or placeholder values remain. + +## 3. Strict Prohibitions +- **No Hallucinated MCP / Tool Calls:** Never call MCP tools or agent skills with guessed arguments; always verify parameter schemas first. +- **No Unauthorized Architectural / Security Changes:** Never add custom headers (like COOP/COEP/CORP), security policies, or infrastructure overrides without explicit user authorization or documented vendor requirements. +- **No Placeholder Leakage:** Never put fake placeholder addresses (e.g., `0x1000...`), mock ABIs, or fake API keys into production code, SDKs, or LLM reference manifests (`llms.txt`, `llms-full.txt`, `agent-tools.ts`). Always use verified ground-truth values. diff --git a/.claude/launch.json b/.claude/launch.json deleted file mode 100644 index 9f93da6..0000000 --- a/.claude/launch.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "version": "0.0.1", - "configurations": [ - { - "name": "ZamaVault Dev", - "runtimeExecutable": "npm", - "runtimeArgs": ["run", "dev"], - "port": 3000, - "autoPort": true - }, - { - "name": "ZamaVault Production Preview", - "runtimeExecutable": "npx", - "runtimeArgs": ["next", "start", "-p", "3030"], - "port": 3030 - } - ] -} diff --git a/.env.example b/.env.example index 2012454..fb2b602 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# ZamaVault — Environment Variables +# ShadowLine — Environment Variables # Copy this file to .env.local and fill in your values. # None of these are required — the app falls back to public RPC nodes. @@ -11,6 +11,6 @@ NEXT_PUBLIC_MAINNET_RPC= NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= # Public deployment URL — used in docs and API examples. -# Set this to your Vercel deployment URL, e.g. https://zamavault.vercel.app +# Set this to your Vercel deployment URL, e.g. https://shadowline.vercel.app # Falls back to relative paths when not set. NEXT_PUBLIC_APP_URL= diff --git a/.gitignore b/.gitignore index 5ef6a52..e100fea 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# internal working notes — not part of the published app +AGENT_GUIDE.md +AUDIT_REPORT.md diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md deleted file mode 100644 index 76abe01..0000000 --- a/AGENT_GUIDE.md +++ /dev/null @@ -1,468 +0,0 @@ -# ZamaVault — Master Reference Document for AI Agents - -> این فایل یک راهنمای کامل برای هر agent هوش مصنوعی است که روی پروژه ZamaVault کار می‌کند. -> شامل همه جزئیات معماری، قوانین حیاتی، اشتباهات رایج، و تجربیات کسب‌شده است. - ---- - -## ۱. بستر پروژه - -**ZamaVault** یک رابط کاربری برای اکوسیستم توکن محرمانه Zama است: -ثبت‌نام پویای Registry، wrap/unwrap ERC-20، رمزگشایی پورتفولیوی رمزنگاری‌شده، و فاست Sepolia. - -- **مخزن:** https://github.com/hosein-ul/zamavault -- **شاخه:** `feat/dynamic-registry-finding-1` (PR #1 → main) -- **Stack:** Next.js 16 (App Router, Turbopack), React 19, Wagmi 3, Viem 2, @zama-fhe/react-sdk 3.0.1, TypeScript 5 -- **هدف:** Zama Developer Program Mainnet Season 3 Bounty Track (deadline: 2026-07-07 AOE) -- **دایرکتوری:** `C:\NEW work\` - ---- - -## ۲. قوانین حیاتی — هرگز نقض نشوند - -### 🚨 قانون ۱: هیچ اشاره‌ای به Claude/Anthropic -هرگز نام Claude، Claude Code، Anthropic، یا هر چیز مشابه را در کد، commit‌ها، PR، README، یا هر فایل قابل مشاهده توسط داوران قرار ندهید. هیچ `Co-Authored-By` header نزنید. این یک ارسال مسابقه است. - -### 🚨 قانون ۲: هرگز permit EIP-712 را auto-fire نکنید -هر decrypt/permit باید پشت یک کلیک صریح کاربر باشد. الگوی `decryptRequested` در `wrap/page.tsx` دقیقاً برای این است: -- `decryptRequested` به طور پیش‌فرض `false` است -- فقط وقتی کاربر روی "Decrypt" کلیک می‌کند `true` می‌شود -- **سورس باگ رایج:** صدا زدن `refetch()` مستقیم روی `useConfidentialBalance` — TanStack Query متد `refetch()` را از `enabled: false` bypass می‌کند! -- **راه‌حل:** بعد از هر shield/unshield موفق، `setDecryptRequested(false)` صدا بزنید -- Reset را **به صورت synchronous** در `onChange` کنید، نه در `useEffect` (race condition یک فریمی) - -### 🚨 قانون ۳: Decimal Scaling بار حیاتی دارد -``` -wrapper decimals: همیشه 6 (محدودیت euint64 FHE) -underlying decimals: 6 یا 18 - -Shield (wrap): parseAmount(input, underlyingDecimals) ← دسیمال underlying -Unshield: parseAmount(input, 6) ← همیشه 6 -Display: formatAmount(balance, 6) ← همیشه 6 - -⚠️ اشتباه رایج: formatAmount(balance, 18) → نمایش صفر برای توکن‌های 18 دسیمال -``` - -### 🚨 قانون ۴: هرگز secret commit نکنید -فایل‌های `.env` در `.gitignore` هستند. از `.env.example` برای مستندسازی استفاده کنید. - -### 🚨 قانون ۵: Blocklist را بدون تأیید تیم حذف نکنید -`BLOCKLISTED_WRAPPERS` در `registry.ts` دارای مستندات است. `cbbqTGBP` (Mainnet) آدرس vanity مشکوک دارد. - ---- - -## ۳. معماری و ساختار فایل - -``` -C:\NEW work\ -├── src/ -│ ├── app/ -│ │ ├── page.tsx ← جدول Registry (صفحه اصلی) -│ │ ├── wrap/page.tsx ← Shield/Unshield (مهم‌ترین صفحه) -│ │ ├── portfolio/page.tsx ← پورتفولیو محرمانه با batch decrypt -│ │ ├── faucet/page.tsx ← فاست توکن mock سپولیا -│ │ ├── analytics/page.tsx ← داشبورد TVL و activity -│ │ ├── learn/page.tsx ← آموزش تعاملی ۵ مرحله‌ای FHE -│ │ ├── developers/page.tsx ← مولد کد snippet -│ │ ├── docs/page.tsx ← مستندات developer -│ │ ├── error.tsx ← مرز خطای Next.js -│ │ ├── api/registry/route.ts ← REST API عمومی -│ │ ├── ClientLayout.tsx ← provider‌های context (theme/network) -│ │ ├── layout.tsx ← layout ریشه با تزریق theme SSR -│ │ └── globals.css ← سیستم طراحی (4 تم Nordic) -│ ├── components/ -│ │ ├── ui/ ← 13 کامپوننت UI قابل استفاده مجدد -│ │ │ ├── Badge.tsx ← ⚠️ text-transform:uppercase حذف شد (باگ fix) -│ │ │ ├── Button.tsx ← forwardRef, variants: primary/secondary/ghost/danger -│ │ │ ├── Card.tsx ← variants: default/glass/outlined/accent -│ │ │ ├── CopyButton.tsx ← کپی clipboard با بازخورد چک‌مارک -│ │ │ ├── Skeleton.tsx ← props: width, height, variant (نه style!) -│ │ │ ├── Toast.tsx ← ToastProvider + useToast() hook -│ │ │ ├── Modal.tsx -│ │ │ ├── Tooltip.tsx ← Portal-based, HelpCircle پیش‌فرض trigger -│ │ │ ├── TokenIcon.tsx ← نقشه symbol→لوگو، حذف prefix "c" و suffix "Mock" -│ │ │ ├── BlurIn.tsx ← انیمیشن blur-in -│ │ │ ├── TypingAnimation.tsx -│ │ │ ├── Spinner.tsx -│ │ │ └── TransactionSuccessModal.tsx -│ │ ├── layout/ -│ │ │ ├── Header.tsx ← ۸ مسیر nav، wallet connect، network switcher -│ │ │ └── Footer.tsx -│ │ └── PendingUnshieldBanner.tsx ← recovery برای unshield قطع‌شده -│ ├── config/ -│ │ ├── contracts.ts ← WrapperPair، KNOWN_WRAPPERS (fallback)، REGISTRY_ADDRESSES -│ │ ├── chains.ts ← SupportedChainId، explorer Blockscout -│ │ └── tokens.ts ← متادیتای نمایش توکن (لوگو، رنگ) -│ ├── lib/ -│ │ ├── registry.ts ← useRegistryPairs، isMintablePair، blocklist -│ │ ├── errors.ts ← classifyError با 16 کد خطا -│ │ ├── utils.ts ← formatAmount، parseAmount، formatAddress، cn -│ │ ├── wrapper-abi.ts ← WRAPPER_ABI، ERC20_ABI -│ │ └── __tests__/utils.test.ts ← 30 تست Vitest -│ └── providers/ -│ └── Providers.tsx ← Wagmi + Zama + TanStack Query (⚠️ بسیار حساس) -├── .github/workflows/ci.yml ← TypeScript → Vitest → Build -├── .env.example ← همه متغیرهای محیطی اختیاری هستند -├── vitest.config.ts ← alias @/ → src/ -├── CLAUDE.md ← مستندات پروژه -└── memory.md ← درس‌های آموخته‌شده -``` - ---- - -## ۴. چرخه SDK Zama (از دیدگاه flow) - -### Shield (Wrap) -``` -useShield({ tokenAddress: erc7984Address }) -→ mutateAsync({ amount, onApprovalSubmitted, onShieldSubmitted }) -→ اگر allowance < amount: approve ERC-20 → onApprovalSubmitted(txHash) → setTxStep(2) -→ shield tx → onShieldSubmitted(txHash) → setTxStep(4) -→ res.txHash → setTxStep(5), setDecryptRequested(false) -``` - -### Unshield (Unwrap) — دو مرحله‌ای -``` -useUnshield(erc7984Address) ← positional در v3.0.1 -→ mutateAsync({ amount, onUnwrapSubmitted, onFinalizing, onFinalizeSubmitted }) -→ unwrap tx on-chain → onUnwrapSubmitted(txHash) → setTxStep(4) -→ Gateway اثبات رمزگشایی تولید می‌کند → onFinalizing() → toast "15-40s" -→ finalize tx → onFinalizeSubmitted(txHash) → setTxStep(5) -``` - -### Decrypt Balance — PERMIT GATING -``` -const [decryptRequested, setDecryptRequested] = useState(false) - -useConfidentialBalance( - { tokenAddress }, - { enabled: decryptRequested && !!address } -) - -// ✅ درست: فقط روی کلیک صریح - - -// ❌ غلط: refetch() مستقیم enabled را bypass می‌کند! -refetchWrapperBalance() ← هرگز در success handler نزنید - -// ✅ بعد از هر tx موفق: -setDecryptRequested(false) ← جلوگیری از refetchOnWindowFocus -``` - -### Batch Decrypt (Portfolio) -``` -useConfidentialBalances( - { tokenAddresses: requestedAddresses }, - { enabled: isConnected && requestedAddresses.length > 0 } -) -// یک EIP-712 permit، همه توکن‌ها را decrypt می‌کند -``` - ---- - -## ۵. الگوهای کلیدی React - -### الگوی تشخیص Registry -```typescript -// useRegistryPairs محاسبه می‌کند -const isChainAligned = isConnected && chain?.id === chainId - -if (isChainAligned && sdkResult.data?.items?.length > 0) → live data -else if (isChainAligned && sdkResult.isLoading) → loading + fallback -else → KNOWN_WRAPPERS fallback (isFromCache: true) -``` - -### الگوی جایگزینی Tooltip -```tsx -// ✅ درست: ? icon جداگانه -Confidential - {/* HelpCircle پیش‌فرض نشان می‌دهد */} - -// ❌ غلط: badge به‌عنوان trigger -Confidential -``` - -### اشتباه Skeleton Props -```tsx -// ✅ درست - - -// ❌ غلط — Skeleton prop style ندارد! - -``` - -### Badge — text-transform حذف شد -```css -/* ❌ قبلاً وجود داشت — باگ cZAMA → CZAMA */ -.badge { text-transform: uppercase; } - -/* ✅ الان — متن دقیقاً همان‌طور که هست نمایش داده می‌شود */ -/* text-transform حذف شد — cZAMA همیشه cZAMA می‌ماند */ -``` - ---- - -## ۶. درس‌های آموخته‌شده (از اشتباهات گذشته) - -### ۶.۱ باگ Auto-Permit (بارها رخ داد) -**علت اصلی:** TanStack Query's `refetch()` از `enabled: false` bypass می‌کند. -بعد از shield موفق، `refetchWrapperBalance()` صدا می‌زدیم → permit بدون کلیک کاربر fire می‌شد → کیف پول ۳ بار prompt می‌داد. -**راه‌حل:** هرگز `refetchWrapperBalance()` در handler‌های موفقیت صدا نزنید. فقط `refetchPublicBalance()` و `refetchAllowance()` مجاز است. - -### ۶.۲ باگ Display صفر برای توکن‌های ۱۸ دسیمال -**علت:** `formatAmount(balance, 18)` روی موجودی FHE با ۶ دسیمال → مقدار نزدیک به صفر. -**راه‌حل:** همیشه `formatAmount(balance, 6)` برای موجودی‌های FHE confidential. - -### ۶.۳ باگ Race Condition برای Token Select -**علت:** `setDecryptRequested(false)` فقط در `useEffect` → یک فریم delay → درخواست قدیمی `true` با آدرس توکن جدید trigger می‌شد. -**راه‌حل:** reset را **synchronously** در `onChange` handler انجام دهید. - -### ۶.۴ Tooltip چه‌طور باید باشد -**علت:** Tooltip کل button را wrap می‌کرد → کلیک روی Tooltip کار نمی‌کرد. -**راه‌حل:** همیشه `` بدون children (→ ? icon) را **بعد از** button/badge قرار دهید. - -### ۶.۵ باگ TVL Ranking -**علت:** Sort روی raw bigint — ZAMA با ۱۸ دسیمال raw value بزرگتری از USDC با ۶ دسیمال داشت. -**راه‌حل:** `tvlHuman` (float نرمال‌شده) را محاسبه کنید و برای sort و عرض bar استفاده کنید. - -### ۶.۶ CZAMA بجای cZAMA -**علت:** `.badge { text-transform: uppercase }` در CSS. -**راه‌حل:** `text-transform` از `.badge` حذف شد. نام‌گذاری: همیشه lowercase `c` — `cZAMA`، `cUSDC`، `cWETH`. - -### ۶.۷ API Domain هاردکد -**علت:** `zamavault.xyz` مستقیم در کد نوشته شد. -**راه‌حل:** از `process.env.NEXT_PUBLIC_APP_URL` استفاده کنید. در Vercel: Settings → Environment Variables. - -### ۶.۸ useCallback در PendingUnshieldBanner -**علت:** `useCallback` با `sdk?.storage` dependency مشکل react-hooks lint داشت. -**راه‌حل:** از plain async functions به جای `useCallback` استفاده کنید. - -### ۶.۹ Unshield step indicator -**علت:** UI همیشه Approve step نشان می‌داد حتی وقتی allowance کافی بود. -**راه‌حل:** `setTxStep(needsApproval ? 1 : 3)` — Approve step فقط وقتی `needsApproval === true` نشان داده می‌شود. - -### ۶.۱۰ اسکرول CSS بدون `margin: auto` -**علت:** `maxWidth: 640` بدون `margin: '... auto 0'` → متن به چپ می‌رفت. -**راه‌حل:** همیشه `margin: 'var(--sp-3) auto 0'` برای centered containers با max-width. - ---- - -## ۷. قراردادها و استانداردها - -### نام‌گذاری توکن‌ها -``` -ERC-20 عمومی: ZAMA، USDC، USDT، WETH، BRON، tGBP، XAUt -ERC-7984 محرمانه: cZAMA، cUSDC، cUSDT، cWETH، cBRON، ctGBP، cXAUt - ↑ همیشه lowercase c -توکن‌های mock: نمایش: "ZAMA" + badge "Mock" (نه "ZAMAMock") -``` - -### Tooltip -- متن کوتاه: ۱-۲ خط حداکثر -- هیچ‌وقت badge/button را به‌عنوان trigger tooltip wrap نکنید -- همیشه `` standalone (→ ? icon) - -### Explorer -- **Blockscout** (نه Etherscan) — چون FHE/Zama protocol decode را پشتیبانی می‌کند -- Sepolia: `https://eth-sepolia.blockscout.com` -- Mainnet: `https://eth.blockscout.com` - -### Commit Messages -- هیچ اشاره‌ای به AI یا Claude -- format: `fix(scope): description`، `feat: description` - -### CSS Variables -```css ---text-xs، --text-sm، --text-base، --text-lg، --text-xl، --text-2xl، --text-3xl ---sp-1 (4px)، --sp-2 (8px)، --sp-3 (12px)، --sp-4 (16px)، --sp-6 (24px)، --sp-8 (32px) ---accent، --success، --warning، --error، --info ---bg-base، --bg-surface، --bg-elevated، --bg-card ---border، --border-hover، --border-accent ---radius-sm، --radius-md، --radius-lg، --radius-xl -``` - ---- - -## ۸. SDK Zama — مرجع سریع - -### نسخه و API -- **نسخه:** `@zama-fhe/react-sdk@3.0.1` — هنوز v2 TypeScript API -- Migration guide برای ۳.۱.x است — تا وقتی از `@^3.0` استفاده می‌کنیم، نیازی به migration نیست -- `WagmiSigner`، `RelayerWeb`، `indexedDBStorage` هنوز در v3.0.1 export می‌شوند - -### Hook‌های مهم -| Hook | Package | نحوه فراخوانی | -|------|---------|-------------| -| `useListPairs({ page, pageSize, metadata })` | react-sdk | config object | -| `useShield({ tokenAddress })` | react-sdk | config object | -| `useUnshield(tokenAddress)` | react-sdk | positional در v3! | -| `useConfidentialBalance({ tokenAddress }, { enabled })` | react-sdk | 2 arg | -| `useConfidentialBalances({ tokenAddresses }, { enabled })` | react-sdk | 2 arg | -| `useResumeUnshield({ tokenAddress })` | react-sdk | config object | -| `useRevokeSession()` | react-sdk | no args | -| `useZamaSDK()` | react-sdk | SDK instance | -| `loadPendingUnshield(storage, addr)` | react-sdk | function | -| `clearPendingUnshield(storage, addr)` | react-sdk | function | -| `matchZamaError(err, handlers)` | @zama-fhe/sdk | function | - -### کدهای خطا -`SIGNING_REJECTED`، `SIGNING_FAILED`، `ENCRYPTION_FAILED`، `DECRYPTION_FAILED`، `TRANSACTION_REVERTED`، `INVALID_KEYPAIR`، `KEYPAIR_EXPIRED`، `NO_CIPHERTEXT`، `RELAYER_REQUEST_FAILED`، `CONFIGURATION`، `INSUFFICIENT_CONFIDENTIAL_BALANCE`، `INSUFFICIENT_ERC20_BALANCE`، `BALANCE_CHECK_UNAVAILABLE`، `ERC20_READ_FAILED`، `ACL_PAUSED`، `APPROVAL_FAILED` - ---- - -## ۹. آدرس‌های قرارداد - -### WrappersRegistry -- Sepolia: `0x2f0750Bbb0A246059d80e94c454586a7F27a128e` -- Mainnet: `0xeb5015fF021DB115aCe010f23F55C2591059bBA0` - -### Sepolia — ۸ pair (۷ mock + ۱ restricted) -| Symbol | ERC-20 | ERC-7984 | Decimals | -|--------|--------|----------|---------| -| USDC | 0x9b5C...FfFF | 0x7c5B...3639 | 6/6 | -| USDT | 0xa7dA...b0 | 0x4E7B...491 | 6/6 | -| WETH | 0xff54...F3F | 0x4620...158 | 18/6 | -| ZAMA | 0x7535...F57 | 0xf2D6...bFB | 18/6 | -| BRON | 0xFf02...E | 0xaa56...891 | 18/6 | -| tGBP | 0x93c9...442 | 0xfCE5...7CC | 18/6 | -| XAUt | 0x2437...940 | 0xe4Fc...0C7 | 6/6 | -| ctGBP (restricted) | 0x167D...A208 | — | 18/6 | - -### Mainnet — ۷ pair + ۱ blocklisted -| Symbol | ERC-20 | ERC-7984 | Decimals | -|--------|--------|----------|---------| -| USDC | 0xa0b8...48 | 0xe978...2B2 | 6/6 | -| USDT | 0xdAC1...c7 | 0xAe02...c50 | 6/6 | -| WETH | 0xc02a...c2 | 0xda93...893 | 18/6 | -| ZAMA | 0xA12C...A3 | 0x80CB...071 | 18/6 | -| BRON | 0xBA2C...83 | 0x85dE...bc | 18/6 | -| tGBP | 0x27f6...87 | 0xa873...DD9 | 18/6 | -| XAUt | 0x6874...38 | 0x73cc...Ef1 | 6/6 | -| cbbqTGBP | **BLOCKLISTED** | آدرس vanity مشکوک | — | - ---- - -## ۱۰. صفحات و routing - -| مسیر | فایل | توضیح | -|------|------|-------| -| `/` | `app/page.tsx` | جدول Registry با موجودی‌های live | -| `/wrap` | `app/wrap/page.tsx` | Shield/Unshield با query `?token=SYMBOL&action=wrap` | -| `/portfolio` | `app/portfolio/page.tsx` | batch decrypt، activity feed | -| `/faucet` | `app/faucet/page.tsx` | فقط Sepolia، فقط mock token‌ها | -| `/analytics` | `app/analytics/page.tsx` | TVL، ratio، activity (24h) | -| `/learn` | `app/learn/page.tsx` | ۵ مرحله تعاملی | -| `/developers` | `app/developers/page.tsx` | snippet generator | -| `/docs` | `app/docs/page.tsx` | مستندات کامل | -| `/api/registry` | `app/api/registry/route.ts` | `?chain=sepolia\|mainnet` | - -**Nav items در Header (به ترتیب):** -Registry → Wrap → Portfolio → Faucet (TESTNET) → Learn → Dev Tools → Analytics → Docs - ---- - -## ۱۱. متغیرهای محیطی - -همه اختیاری هستند — app fallback عمومی دارد: - -```env -NEXT_PUBLIC_SEPOLIA_RPC= # Alchemy یا RPC دیگر (پیش‌فرض: publicnode) -NEXT_PUBLIC_MAINNET_RPC= # Alchemy یا RPC دیگر (پیش‌فرض: publicnode) -NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= # اگر نبود، فقط injected wallets -NEXT_PUBLIC_APP_URL= # URL deployment برای docs/API (مثال: https://zamavault.vercel.app) -``` - -**تنظیم در Vercel:** Settings → Environment Variables → Add → Deploy دوباره - ---- - -## ۱۲. CI/CD و دستورات - -```bash -# توسعه -npm run dev # سرور dev روی localhost:3000 - -# بررسی کیفیت -npm test # Vitest — 30 تست -npx tsc --noEmit # TypeScript type-check (باید clean باشد) -npm run lint # ESLint (advisory — برخی خطاهای pre-existing وجود دارند) - -# تولید -npx next build # باید موفق باشد -``` - -**GitHub Actions:** Push به `main` یا `feat/**` → TypeScript → Vitest → Next build -ESLint advisory است (نه blocking) — برخی خطاهای pre-existing در AUDIT_REPORT.md ردیابی شده‌اند. - ---- - -## ۱۳. الگوهای Error Handling - -### در هر catch block -```typescript -} catch (err: unknown) { // ← نه err: any - const classified = classifyError(err); - addToast({ - variant: classified.retryable ? 'warning' : 'error', - title: classified.title, - message: classified.message, - }); -} -``` - -### در REST API -```typescript -} catch (err) { - // fallback به KNOWN_WRAPPERS - return NextResponse.json({ pairs: fallback, source: 'cached-snapshot', warning: '...' }) -} -``` - ---- - -## ۱۴. تم‌ها و طراحی - -**۴ تم dark Nordic:** -- Charcoal (پیش‌فرض): accent `#38bdf8` (sky blue) -- Midnight: accent `#f4f4f5` (white) -- Frost: accent `#60a5fa` (ice blue) -- Aurora: accent `#2dd4bf` (teal) - -**Light mode:** accent `#09090b` (black/inverse) - -**فونت‌ها:** -- Sans: Plus Jakarta Sans -- Mono: JetBrains Mono - ---- - -## ۱۵. کارهای باقی‌مانده - -### کاربر باید انجام دهد -- D1: Deploy Vercel + تنظیم `NEXT_PUBLIC_APP_URL` -- D2: Mainnet Relayer API key (اختیاری، برای بهتر شدن UX) - -### کارهای آینده -- Phase 2.2: npm package `@zamavault/sdk` -- Phase 5.1: README rewrite کامل -- Phase 4.4: تست mobile responsive در 360px - ---- - -## ۱۶. URL های مهم مستندات Zama - -- SDK overview: https://docs.zama.org/protocol/sdk/overview -- useShield: https://docs.zama.org/protocol/sdk/api-references/react/useshield -- useUnshield: https://docs.zama.org/protocol/sdk/api-references/react/useunshield -- useConfidentialBalance: https://docs.zama.org/protocol/sdk/api-references/react/useconfidentialbalance -- useResumeUnshield: https://docs.zama.org/protocol/sdk/api-references/react/useresumeunshield -- matchZamaError/errors: https://docs.zama.org/protocol/sdk/api-references/sdk/errors -- WrappersRegistry: https://docs.zama.org/protocol/sdk/api-references/sdk/wrappersregistry -- Sepolia addresses: https://docs.zama.org/protocol/protocol-apps/addresses/testnet/sepolia -- Mainnet addresses: https://docs.zama.org/protocol/protocol-apps/addresses/mainnet/ethereum -- Migration v2→v3: https://docs.zama.org/protocol/sdk/migration/migrate-v2-to-v3 -- Relayer API keys: https://docs.zama.org/protocol/sdk/guides/authentication - ---- - -*آخرین بروزرسانی: 2026-06-24 | branch: feat/dynamic-registry-finding-1* diff --git a/AUDIT_REPORT.md b/AUDIT_REPORT.md deleted file mode 100644 index 6b20040..0000000 --- a/AUDIT_REPORT.md +++ /dev/null @@ -1,229 +0,0 @@ -# ZamaVault — Bounty Submission Audit Report - -**Audit date:** 2026-06-21 -**Submission deadline:** 2026-07-07 AOE -**Target:** Zama Developer Program Mainnet Season 3 — Bounty Track -**Repository:** https://github.com/hosein-ul/zamavault -**Audited commit:** local `main` HEAD as of 2026-06-21 - ---- - -## 1. Executive summary - -ZamaVault is a visually polished Next.js front-end for the Zama confidential wrapper flows (shield/unshield/decrypt) plus a Sepolia mint faucet. However, **the application does not actually read the on-chain Wrappers Registry**: every page renders pairs from a hardcoded list in [contracts.ts](src/config/contracts.ts), and the registry ABI in [registry-abi.ts](src/lib/registry-abi.ts) is dead code. This is the single biggest gap versus the bounty brief, which explicitly asks the app to "surface every registered ERC-20 ↔ ERC-7984 wrapper pair from Zama's on-chain Wrappers Registry." It also directly undermines two of the six judging criteria — *coverage* and *extensibility* — because any new wrapper added to the registry tomorrow will never appear without a redeploy. - -Other meaningful gaps: no `useResumeUnshield` (a user who closes their tab between `unwrap` and `finalizeUnwrap` has no recovery path), no `matchZamaError` classification (every failure surfaces as a raw `err.message`), no relayer-API-key plumbing for Mainnet (Mainnet decrypt/shield/unshield will silently fail — *deferred, user-handled*), no live deployed URL (*deferred, user-handled*), no tests, no CI, no `.env.example`, no error boundaries, no pagination, no detection of revoked (`isValid == false`) registry entries, and no separation between reusable SDK-layer logic and the Next.js app (the bounty's stated category goal is "templates and resources for the developer ecosystem" — a flat single-app structure does not deliver that). - -What is solid: the decimals scaling fix described in [memory.md](memory.md) is correctly applied across the wrap, portfolio, and faucet flows; batched `useConfidentialBalances` on the Portfolio page is the right pattern; the wrap page is correctly gated behind an explicit "Decrypt to view" click rather than auto-prompting a permit on token select; RPC fallback transports are configured; and the visual design is genuinely strong. - -The path to a competitive submission is clear: replace the hardcoded list with a registry-backed hook (top priority); add `useResumeUnshield`, `matchZamaError`, an error boundary, and CI; and (time permitting) extract the registry/wrap/unwrap logic into a reusable `packages/` module. Mainnet relayer key and Vercel deploy are out of scope for this implementation pass — the user will handle them separately before submission. - ---- - -## 2. Findings table - -| # | Criterion | Finding | Severity | File(s) | Recommended fix | -|---|-----------|---------|----------|---------|-----------------| -| 1 | Coverage / Extensibility | Registry is never read on-chain; all pairs come from hardcoded `KNOWN_WRAPPERS`. `REGISTRY_ABI` and `REGISTRY_ADDRESSES` are dead code. | **Critical** | [src/config/contracts.ts](src/config/contracts.ts), [src/lib/registry-abi.ts](src/lib/registry-abi.ts), [src/app/page.tsx:26](src/app/page.tsx:26), [src/app/wrap/page.tsx:58](src/app/wrap/page.tsx:58), [src/app/portfolio/page.tsx:155](src/app/portfolio/page.tsx:155), [src/app/faucet/page.tsx:59](src/app/faucet/page.tsx:59) | Replace `KNOWN_WRAPPERS[chainId]` lookups with the SDK's `useListPairs` / `useTokenPairsRegistry` hook (or a `useReadContract` against `listPairs` + `getTokenConfidentialTokenPairsLength` + slice). Keep hardcoded metadata only as a *display-only* enrichment layer (logos, friendly names) keyed by address. | -| 2 | Correctness / UX | No `useResumeUnshield` implementation. A user closing their tab between the on-chain `unwrap` request and the `finalizeUnwrap` step has no recovery path; their wrapped balance is stuck pending. | **High** | [src/app/wrap/page.tsx:178-203](src/app/wrap/page.tsx:178) (only) | Add a "Pending unshield" banner on Portfolio and Wrap pages that calls `useResumeUnshield` to detect outstanding requests on mount, with a "Resume" action that completes finalization. | -| 3 | Correctness | All SDK errors are reduced to `err.message` strings; no `matchZamaError` classification. Signature-rejected, tx-reverted, allowance-too-low, relayer-down, ratelimit, and bad-chain all surface identically to the user. | **High** | [src/app/wrap/page.tsx:204-213](src/app/wrap/page.tsx:204), [src/app/faucet/page.tsx:129-137](src/app/faucet/page.tsx:129), [src/app/portfolio/page.tsx:219-232](src/app/portfolio/page.tsx:219) | Wrap every SDK call site in `matchZamaError(err, { signatureRejected: ..., relayerUnavailable: ..., decryptionFailed: ..., ... })` and produce a distinct toast title + recovery hint per case. | -| 4 | Production-readiness | No Relayer API key is plumbed into `RelayerWeb` for Mainnet. Mainnet shield / unshield / decrypt will fail with an unhelpful relayer-auth error and no fallback. ***Deferred — user-handled (key obtained later).*** | **High** | [src/providers/Providers.tsx:43-55](src/providers/Providers.tsx:43) | (a) Add `NEXT_PUBLIC_RELAYER_API_KEY` support **only** for build-time configuration and document the backend-proxy pattern per [Zama auth guide](https://docs.zama.org/protocol/sdk/guides/authentication.md); (b) when no key is configured, detect Mainnet selection and degrade to read-only mode with an explanatory banner ("Mainnet write operations require a Zama Relayer API key — browsing pairs only"). Do **not** ship a real key in `NEXT_PUBLIC_*`. | -| 5 | Production-readiness | No live deployed URL in [README.md](README.md), [package.json](package.json), or anywhere else. No `vercel.json`. Judges must `git clone && npm install` to evaluate. ***Deferred — user will deploy separately.*** | **High** | [README.md](README.md), [package.json](package.json) | Deploy to Vercel before submission; add the URL to the README headline and to a `homepage` field in `package.json`. | -| 6 | Coverage | Revoked registry entries (`isValid == false` but non-zero wrapper) are not detected anywhere. They would render as normal usable pairs and a user clicking "Shield" would hit a revert. | **Medium** | [src/config/contracts.ts](src/config/contracts.ts), [src/app/page.tsx](src/app/page.tsx) | When migrating to dynamic registry reads, expose `isValid` from the pair tuple, hide invalid pairs by default with a "Show revoked" toggle, and visually mark them with a "Revoked" badge. | -| 7 | Extensibility | Mainnet wrapper addresses are hardcoded with a stale comment ("Update as needed or read dynamically"). No mechanism to refresh. They may already drift from the [official mainnet addresses page](https://docs.zama.org/protocol/protocol-apps/addresses/mainnet.md). | **Medium** | [src/config/contracts.ts:86-143](src/config/contracts.ts:86) | Same fix as #1 — dynamic registry reads make this self-healing. As an interim mitigation, add a CI check (or a one-off script) that diffs the hardcoded list against the on-chain registry. | -| 8 | Coverage / UX | No pagination. Renders all pairs in one unpaginated table. A registry of 50+ pairs will produce a wall of rows; mobile becomes unusable. | **Medium** | [src/app/page.tsx:119-233](src/app/page.tsx:119) | Add page-size + `useMemo`-based slice, or virtualize with `@tanstack/react-virtual`. | -| 9 | Correctness / Extensibility | `REGISTRY_ABI` in [src/lib/registry-abi.ts](src/lib/registry-abi.ts) declares `getAllWrappers` / `getWrapperCount` / `getWrapper` / `getUnderlying` / `isRegistered` — these names **do not match** Zama's documented registry surface (`listPairs`, `getTokenConfidentialTokenPairsLength`, `isValid`, etc.). The ABI is wrong *and* unused. | **Medium** | [src/lib/registry-abi.ts](src/lib/registry-abi.ts) | Delete this file and use the SDK hooks (`useListPairs` / `useTokenPairsRegistry`) instead. If raw ABI is still needed (e.g. CLI/indexer), regenerate it from the official `WrappersRegistry` ABI. | -| 10 | UX | Mainnet vs Sepolia selector is duplicated in the header and on the home page (two independent `network-switcher` widgets). Visual inconsistency, and the in-page one bypasses wallet chain switching. | **Medium** | [src/components/layout/Header.tsx:173-186](src/components/layout/Header.tsx:173), [src/app/page.tsx:102-116](src/app/page.tsx:102) | Keep one network switcher (the header's, which correctly calls `switchChain` when connected). Remove or pure-display the in-page one. | -| 11 | UX | Portfolio uses batched `useConfidentialBalances` (good), but Wrap page uses single `useConfidentialBalance` per token selection. Selecting four tokens in succession on Wrap can prompt four separate permits. | **Medium** | [src/app/wrap/page.tsx:80-88](src/app/wrap/page.tsx:80) | Hoist permit issuance to a shared cache (already via `indexedDBStorage`); ensure the SDK reuses the session permit across token selections without re-prompting. Verify against `useRevokeSession` reset flow. | -| 12 | UX | "Awaiting Permit..." string is shown for both *signing in progress* and *fetch in progress*. A user who never clicked "Decrypt to view" sees nothing — but a user who clicked and then rejected sees the same "Awaiting Permit..." stuck indefinitely with no recovery button. | **Medium** | [src/app/wrap/page.tsx:261-263](src/app/wrap/page.tsx:261) | Surface the `decryptWrapperError` (already destructured at line 84 but never rendered) with a retry button. | -| 13 | UX / Accessibility | Only one `aria-*` attribute in the entire codebase ([TokenIcon.tsx](src/components/ui/TokenIcon.tsx)). Icon-only buttons (close, copy, theme toggle, swap-arrow) lack labels. Modal lacks `role="dialog"` + `aria-modal`. Color contrast in some themes (Frost) likely fails WCAG AA on `text-muted` over glassmorphism. | **Medium** | [src/components/ui/Modal.tsx](src/components/ui/Modal.tsx), [src/components/ui/CopyButton.tsx](src/components/ui/CopyButton.tsx), [src/components/layout/Header.tsx](src/components/layout/Header.tsx) | Add `aria-label` to all icon-only buttons; add `role="dialog" aria-modal="true" aria-labelledby` to Modal; run an axe-core or Lighthouse pass and fix critical issues. | -| 14 | UX | No stale-while-revalidate / persisted cache for registry & balances. A slow RPC produces a blank screen. The QueryClient's `staleTime: 30_000` does not survive a refresh because there is no persistence layer. | **Medium** | [src/providers/Providers.tsx:58-65](src/providers/Providers.tsx:58) | Add `@tanstack/react-query-persist-client` with `localStorage` persistence for the registry-listing query. | -| 15 | Correctness | The faucet's `COOLDOWN_SECONDS = 5` is a UI-only timer that resets on refresh. The comment in [README.md:15](README.md) calls it a feature but it is not enforced on-chain. | **Low** | [src/app/faucet/page.tsx:42](src/app/faucet/page.tsx:42), [README.md:15](README.md) | Either remove the cooldown (and the README claim) or persist `nextEligibleAt` in `localStorage` so the timer survives a refresh. Be explicit in copy that it is a client-side limiter. | -| 16 | Code quality | TS strict mode is **enabled** ([tsconfig.json:7](tsconfig.json:7)) but three critical paths still use `err: any`. | **Low** | [src/app/faucet/page.tsx:129](src/app/faucet/page.tsx:129), [src/app/wrap/page.tsx:204](src/app/wrap/page.tsx:204), [src/app/portfolio/page.tsx:269](src/app/portfolio/page.tsx:269) | Type as `unknown` and narrow with `matchZamaError` (fix #3). | -| 17 | Code quality | No test suite, no `.github/workflows`, no CI. | **Medium** | (none) | Add a GitHub Actions workflow that runs `eslint`, `tsc --noEmit`, and `next build` on PR. Add at minimum a Vitest test for [utils.ts](src/lib/utils.ts) `formatAmount` / `parseAmount` (the decimals math is load-bearing per [memory.md](memory.md) and is exactly the kind of regression a test catches). | -| 18 | Code quality / Extensibility | Single flat Next.js app. No separation between reusable logic and the UI. The bounty track's category goal is "templates and resources for the developer ecosystem"; a flat repo signals the opposite. | **High** *(differentiation)* | (whole repo) | Convert to a thin monorepo: `packages/registry-sdk` (pure-TS module exporting `listPairs(client, chainId)`, `shield`, `unshield`, `decryptBalance` — viem-based, no React) and `apps/web` (Next.js app, depends on the package). The package then doubles as a publishable artifact and a CLI substrate. | -| 19 | Production-readiness | No `.env.example`. The README documents `NEXT_PUBLIC_SEPOLIA_RPC` / `NEXT_PUBLIC_MAINNET_RPC` / `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` but a new developer must read source to discover the third one. | **Low** | [README.md:74-79](README.md:74), [src/providers/Providers.tsx:12](src/providers/Providers.tsx:12) | Add `.env.example` at repo root with all three keys (empty values) and document each. | -| 20 | Production-readiness | No error boundary. An unexpected error in any page component will white-screen the whole app. | **Medium** | [src/app/layout.tsx](src/app/layout.tsx), [src/app/ClientLayout.tsx](src/app/ClientLayout.tsx) | Add a top-level `error.tsx` (Next.js App Router error boundary) with a "Reload" action and the underlying error message in a `
`. | -| 21 | Code quality | `import { sepolia } from 'wagmi/chains'` in [wrap/page.tsx:21](src/app/wrap/page.tsx:21) is unused. Several `style={{...}}` blocks duplicate values that already exist in `globals.css`. | **Low** | [src/app/wrap/page.tsx:21](src/app/wrap/page.tsx:21) | Remove unused imports; run `eslint --fix`. | -| 22 | Coverage | The README claims "discover all verified ERC-20 ↔ ERC-7984 wrapper pairs on Ethereum and Sepolia" — currently false (no discovery, no verification). | **High** | [README.md:14](README.md:14), [src/app/page.tsx:54-56](src/app/page.tsx:54) | Either fix the registry coverage (preferred) or correct the copy until it is fixed. Misrepresenting capability to judges is worse than under-promising. | -| 23 | Differentiation | No activity / history view, no operator approvals UI, no extracted package, no indexer, no Hoodi support, no CLI. Competing teams are almost certain to ship at least one of these. | **Medium** *(differentiation)* | (none) | See section 4 (Opportunities) for ranking. | - ---- - -## 3. Detailed findings by criterion - -### 3.1 Coverage - -**3.1.1 Registry is not read on-chain (Finding #1, Critical).** -`KNOWN_WRAPPERS` is the *sole* source of pair data for every page: -- Registry table at [src/app/page.tsx:26](src/app/page.tsx:26): `const wrappers = KNOWN_WRAPPERS[activeChainId] ?? [];` -- Wrap page at [src/app/wrap/page.tsx:58](src/app/wrap/page.tsx:58): same. -- Portfolio at [src/app/portfolio/page.tsx:155](src/app/portfolio/page.tsx:155): same. -- Faucet at [src/app/faucet/page.tsx:59](src/app/faucet/page.tsx:59): same (hardcoded to Sepolia only). - -A grep across `src/` for `REGISTRY_ABI`, `REGISTRY_ADDRESSES`, `listPairs`, `useListPairs`, `useTokenPairsRegistry`, `useWrapperDiscovery`, `getAllWrappers`, `getWrapperCount` returns **zero call sites**. The ABI file and the registry address constant exist purely as decoration. This is the single most impactful change the project needs: the bounty's coverage criterion is defined by registry-completeness, and the registry is not being consulted. - -**Recommended fix.** Replace the hardcoded source with one of: -- `useListPairs({ chainId })` from `@zama-fhe/react-sdk` (preferred — handles pagination and caching), -- or `useReadContract` against the real registry ABI (`getTokenConfidentialTokenPairsLength` + a sliced `listPairs(start, count)` call). - -Keep `TOKEN_INFO` in [src/config/tokens.ts](src/config/tokens.ts) as a display-only enrichment layer keyed by symbol or underlying address. - -**3.1.2 No revoked-pair detection (Finding #6, Medium).** When a pair has `isValid == false` but the wrapper address is still non-zero, the UI will treat it as a healthy pair and Shield will revert. Surface `isValid` and visually mark revoked entries. - -**3.1.3 No pagination (Finding #8, Medium).** The table renders all rows. Acceptable today (7 pairs), structurally broken at 50+. - -**3.1.4 Wrong ABI surface (Finding #9, Medium).** [registry-abi.ts](src/lib/registry-abi.ts) declares `getAllWrappers`, `getWrapperCount`, `getWrapper`, `getUnderlying`, `isRegistered` — none of which match the documented `WrappersRegistry` surface (`listPairs`, `getTokenConfidentialTokenPairsLength`, `isValid`, `getPairFromConfidentialToken`, etc.). Either the file is from a prototype or hallucinated. Delete it. - -### 3.2 Correctness - -**3.2.1 No `useResumeUnshield` (Finding #2, High).** The unshield flow involves two on-chain interactions plus an off-chain decryption hop. A user who closes the tab after `unwrap` but before `finalizeUnwrap` has no UI affordance to recover. Implementing `useResumeUnshield` on Portfolio (and showing a "1 pending unshield" banner) is a small change with disproportionate UX impact and a direct match to the docs' "Activity Feeds" and "useResumeUnshield" references. - -**3.2.2 No `matchZamaError` (Finding #3, High).** All three SDK call sites collapse every failure into a single toast: -- [wrap/page.tsx:204-213](src/app/wrap/page.tsx:204): `err.message || 'The transaction was rejected or failed.'` -- [faucet/page.tsx:129-137](src/app/faucet/page.tsx:129): `err.message || 'The faucet mint transaction was rejected.'` -- [portfolio/page.tsx:222-228](src/app/portfolio/page.tsx:222): `err.message || 'The permit signature request was rejected or failed.'` - -Note: with the dynamic registry migration (Finding #1) eliminating one of the three faucet call sites is not in scope — the faucet keeps its own write path against the underlying mock ERC-20, which can still revert (insufficient ETH for gas, paused contract, etc.); `matchZamaError` is still the right wrapper there even though it is not a relayer call. - -Concrete scenarios that produce *no distinct* feedback today: -- User rejects MetaMask signature → identical to "tx reverted". -- Relayer rate-limited → identical to "network error". -- Wrapper not registered for the connected chain → identical to "tx reverted". -- Encrypted balance is zero / address never held the token → no error, just a `0n` value indistinguishable from "decrypt succeeded with zero". - -Wrap each call site in `matchZamaError(err, { signatureRejected, relayerUnavailable, decryptionFailed, allowanceTooLow, ... })`. - -**3.2.4 Decimals scaling.** The fix described in [memory.md](memory.md) is applied correctly in the code I read: -- Wrap input parses with `underlyingDecimals` ([wrap/page.tsx:121](src/app/wrap/page.tsx:121)). -- Unwrap input parses with `wrapperDecimals` (same line). -- Portfolio formats with `wrapper.wrapperDecimals` ([portfolio/page.tsx:85](src/app/portfolio/page.tsx:85)). -- Public-vs-confidential balances in the wrap panel use the right decimals on each side ([wrap/page.tsx:253-260](src/app/wrap/page.tsx:253)). - -Faucet parses with `selectedWrapper.decimals` (underlying) at [faucet/page.tsx:112-113](src/app/faucet/page.tsx:112) — also correct. - -**3.2.5 Permit auto-fire.** The Wrap page passes `enabled: !!address && !!selectedWrapper?.erc7984Address` to `useConfidentialBalance` ([wrap/page.tsx:87](src/app/wrap/page.tsx:87)), which would normally fire as soon as a token is selected. In practice the UI gates display behind a "Decrypt to view" button ([wrap/page.tsx:264-281](src/app/wrap/page.tsx:264)) that calls `refetch()`. This works *only because* the SDK does not pre-issue a permit on the initial enabled fetch — it returns `undefined` until refetch. This is a fragile contract; if the SDK behavior changes, every token-select will trigger a wallet prompt. Consider switching to `enabled: hasUserClickedDecrypt` to make the gating explicit. - -### 3.3 Extensibility - -**3.3.1 Hardcoded wrapper list (Finding #1 again).** Already covered above — this is *the* extensibility failure. - -**3.3.2 Hardcoded Mainnet addresses (Finding #7, Medium).** The Mainnet block in [contracts.ts:86-143](src/config/contracts.ts:86) has the same problem with extra blast radius: Mainnet pair additions cannot reach the app without a redeploy, and a wrong address there silently routes user funds to the wrong contract. I did not full-text-cross-check every Mainnet address against the official page in this session — the well-known underlyings (USDC, USDT, WETH) are correct, but the seven hardcoded ERC-7984 wrapper addresses **must** be verified against [docs.zama.org/protocol/protocol-apps/addresses/mainnet.md](https://docs.zama.org/protocol/protocol-apps/addresses/mainnet.md) before submission. Better still, eliminate them via dynamic reads (#1). - -**3.3.3 No logic / UI separation (Finding #18, High for differentiation).** Every wrapper-related operation is implemented inside a Next.js page component. There is no `lib/registry.ts` exporting `listPairs(client, chainId)`, no `lib/shield.ts` exporting a viem-based `shield(client, account, pair, amount)`, no CLI, no published package. The bounty's category goal is "templates and resources for the developer ecosystem" — a competing team that ships even a thin `@zamavault/sdk` npm package will out-position this submission on the extensibility axis. - -**3.3.4 Chain configuration.** This is one of the better parts: [src/config/chains.ts](src/config/chains.ts) centralizes Sepolia + Mainnet config, and `SupportedChainId` is reused across files. Adding Hoodi is a 10-line change here — see Opportunities. - -### 3.4 UX - -**3.4.1 Permit signing surprise.** The wrap page is correctly gated behind "Decrypt to view" (good). The portfolio page is correctly gated behind "Decrypt Balance" / "Decrypt All" (good). No silent permit prompts on page load — confirmed by inspection. - -**3.4.2 Failure states.** As noted in #12 and #3, the wrap page destructures `decryptWrapperError` ([wrap/page.tsx:84](src/app/wrap/page.tsx:84)) but never renders it. A user who rejects a permit gets stuck on "Awaiting Permit..." with no retry path. - -**3.4.3 "Balance 0" vs "Balance not decrypted."** Handled adequately: portfolio shows `••••••` + "Encrypted" badge when not decrypted, and `0 cXXX` when decrypted-and-zero. Wrap page shows the literal value (`0`) once decrypted, which is correct. - -**3.4.4 Revoked / invalid pairs.** No handling — see #6. - -**3.4.5 Duplicate network switchers.** See #10 — the home page's switcher does not call `switchChain` and so silently desyncs from the wallet's actual chain. - -**3.4.6 Accessibility.** Only one `aria-*` attribute in the entire `src/` tree. Modal lacks `role="dialog" aria-modal aria-labelledby`. Theme variants (Frost especially) need a contrast pass. The custom ``'s `onChange` handler (not just in a `useEffect`). - 3. Removed `refetchWrapperBalance()` from the automatic balance-sync `useEffect` — only public balance and allowance auto-refresh. - 4. Created `ConfidentialBalanceInline` component with proper states: Decrypt button → Awaiting signature → Value → Retry on error. - -### Problem 8: Generic Error Messages for All Failures -* **Symptom:** Every SDK error (signature rejected, relayer down, tx reverted, insufficient balance) showed the same "Transaction Failed" toast with raw `err.message`. -* **Solution:** - 1. Created `src/lib/errors.ts` with `classifyError(err)` using `matchZamaError` from `@zama-fhe/sdk`. - 2. Maps 15+ Zama SDK error codes to distinct user-friendly `{ title, message, retryable }` objects. - 3. Fallback patterns catch common wallet rejections (MetaMask "user rejected", WalletConnect "ACTION_REJECTED", etc.). - 4. All three `catch (err: any)` sites replaced with `catch (err: unknown)` + `classifyError`. - -### Problem 9: No Recovery for Interrupted Unshield -* **Symptom:** A user closing their browser tab between the `unwrap` transaction and the `finalizeUnwrap` step had no way to resume — their tokens were stuck pending. -* **Solution:** - 1. Created `PendingUnshieldBanner` component using `loadPendingUnshield` (checks SDK storage on mount) and `useResumeUnshield` (completes finalization). - 2. Shows a yellow warning banner with "Resume" button when a pending unshield is detected. - 3. Integrated on both Portfolio page (per-wrapper) and Wrap page (for selected token). - ---- - -## 💡 Best Practices for Zama FHE Frontend Projects - -1. **Always Verify Decimals On-Chain:** Never assume a wrapper matches the underlying token's decimals. Write a quick read script (like `scratch_check.js`) to verify the wrapper's `decimals()` output. -2. **Batch Permits Where Possible:** Use batch hooks for decryption (`useConfidentialBalances`) to minimize wallet interaction prompts. -3. **Handle Case Insensitivity:** FHE Relayer/Gateway responses might return token address keys in lowercase or mixed case. Implement `.toLowerCase()` keys when lookup results are cached. -4. **Use Fallback Transports:** In Wagmi configs, always provide fallback providers to guarantee dapp stability. -5. **Never Auto-Fire Permits:** Gate every `useConfidentialBalance` behind an explicit `decryptRequested` state. Reset it synchronously on token change — not in `useEffect` (one-frame race condition). -6. **Use matchZamaError for Error Handling:** The Zama SDK exports typed error codes. Use `matchZamaError(err, { SIGNING_REJECTED: ..., ... })` instead of `err.message` for user-facing errors. -7. **Normalize Mock Symbols:** Sepolia mock tokens have on-chain symbols like `USDCMock`. Strip the `Mock` suffix at the registry-read boundary and show a separate "Mock" badge in the UI. -8. **Blocklist Suspicious Registry Entries:** The on-chain registry may contain test/placeholder entries (e.g., `cbbqTGBP` on Mainnet with vanity address `0xbeeff...`). Use a documented blocklist rather than hiding the issue. -9. **Read the Registry Dynamically:** Never hardcode wrapper pairs as the sole data source. Use `useListPairs` from the SDK or raw `listPairs` contract calls. Keep hardcoded pairs only as a disconnected-wallet fallback. - ---- - -## 📋 Remaining Plan (as of 2026-06-23) - -See `CLAUDE.md` for full project context. See the plan file for the master checklist. - -### Completed -- Phase 1: Bug fixes (tooltip, matchZamaError, useResumeUnshield, error boundary, .env.example) -- Phase 2.1: REST API `/api/registry` -- Phase 2.3: Interactive tutorial `/learn` page (5-step FHE walkthrough with progress tracking) -- Phase 2.4: Code snippet generator `/developers` page (4 operations × 3 frameworks + REST API) -- Phase 4.1: GitHub Actions CI (now with Vitest step) -- Phase 4.2: Vitest unit tests (30 tests for formatAmount, parseAmount, formatAddress, cn, etc.) -- Phase 4.3: Dead code cleanup (error.tsx Link fix, verified no stale KNOWN_WRAPPERS refs) - -### Next Up -- Phase 2.2: npm package `@zamavault/sdk` -- Phase 3: Analytics dashboard + activity feed -- Phase 4.4: Mobile responsive test (360px) -- Phase 5: README rewrite, final polish - -### Deferred (User Handles) -- D1: Vercel deploy + live URL -- D2: Mainnet Relayer API key + read-only fallback diff --git a/next.config.ts b/next.config.ts index e9ffa30..b22af96 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,5 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; -const nextConfig: NextConfig = { - /* config options here */ -}; +const nextConfig: NextConfig = {}; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index b44ca42..fae0483 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "zamavault", + "name": "shadowline", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "zamavault", + "name": "shadowline", "version": "0.1.0", "dependencies": { "@radix-ui/react-icons": "^1.3.2", @@ -13,6 +13,7 @@ "@react-three/fiber": "^9.6.1", "@tanstack/react-query": "^5.101.0", "@zama-fhe/react-sdk": "^3.0.1", + "@zama-fhe/sdk": "^3.0.1", "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -42,6 +43,7 @@ "@types/three": "^0.184.1", "eslint": "^9", "eslint-config-next": "16.2.9", + "jimp": "^1.6.1", "tailwindcss": "^4.3.1", "typescript": "^5", "vitest": "^4.1.9" @@ -315,6 +317,17 @@ "node": ">=6.9.0" } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@dimforge/rapier3d-compat": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", @@ -1075,45 +1088,645 @@ "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jimp/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.1.tgz", + "integrity": "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/file-ops": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "await-to-js": "^3.0.0", + "exif-parser": "^0.1.12", + "file-type": "^21.3.3", + "mime": "3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/diff": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.1.tgz", + "integrity": "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "pixelmatch": "^5.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/file-ops": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.1.tgz", + "integrity": "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-bmp": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.1.tgz", + "integrity": "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "bmp-ts": "^1.0.9" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-gif": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.1.tgz", + "integrity": "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "gifwrap": "^0.10.1", + "omggif": "^1.0.10" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-jpeg": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.1.tgz", + "integrity": "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "jpeg-js": "^0.4.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-png": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.1.tgz", + "integrity": "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "pngjs": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-tiff": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.1.tgz", + "integrity": "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "utif2": "^4.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-blit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.1.tgz", + "integrity": "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-blit/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-blur": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.1.tgz", + "integrity": "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/utils": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-circle": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.1.tgz", + "integrity": "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-circle/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-color": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.1.tgz", + "integrity": "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "tinycolor2": "^1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-color/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.1.tgz", + "integrity": "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-contain/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.1.tgz", + "integrity": "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-cover/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.1.tgz", + "integrity": "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-crop/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.1.tgz", + "integrity": "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-displace/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.1.tgz", + "integrity": "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.1.tgz", + "integrity": "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-fisheye/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.1.tgz", + "integrity": "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-flip/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-hash": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.1.tgz", + "integrity": "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "any-base": "^1.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.1.tgz", + "integrity": "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-mask/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.1.tgz", + "integrity": "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/types": "1.6.1", + "parse-bmfont-ascii": "^1.0.6", + "parse-bmfont-binary": "^1.0.6", + "parse-bmfont-xml": "^1.1.6", + "simple-xml-to-json": "^1.2.2", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-print/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-quantize": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.1.tgz", + "integrity": "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-quantize/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.1.tgz", + "integrity": "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-resize/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.1.tgz", + "integrity": "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-rotate/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.1.tgz", + "integrity": "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-threshold/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node_modules/@jimp/types": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.1.tgz", + "integrity": "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "zod": "^3.23.8" }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/types/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node_modules/@jimp/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "tinycolor2": "^1.6.0" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, "node_modules/@jridgewell/gen-mapping": { @@ -3736,6 +4349,31 @@ "react": "^18 || ^19" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true, + "license": "MIT" + }, "node_modules/@tweenjs/tween.js": { "version": "23.1.3", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", @@ -4684,11 +5322,10 @@ } }, "node_modules/@zama-fhe/relayer-sdk": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@zama-fhe/relayer-sdk/-/relayer-sdk-0.4.3.tgz", - "integrity": "sha512-/Lz+yBda4vppMx3FiCnqjRmBWxxEzGrcyOLeFQg1fqadnCWPU5GCmx7pSBQXesJHQz7MJ5hD07ERv2gdNjxv3w==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@zama-fhe/relayer-sdk/-/relayer-sdk-0.4.4.tgz", + "integrity": "sha512-N+ateFbi7Fu9JsxExapfn/SdU3Bye3tvCfIaZQsRNXsZ7ep39S0BUnmljh+JhalUKT11xlMoA4+1TKLx72amcw==", "license": "BSD-3-Clause-Clear", - "peer": true, "dependencies": { "commander": "^14.0.0", "ethers": "^6.15.0", @@ -4712,7 +5349,6 @@ "resolved": "https://registry.npmjs.org/@zama-fhe/sdk/-/sdk-3.0.1.tgz", "integrity": "sha512-wca16KgwBcZU0dyzTVhmvumLc7D30SO59H7zvaDqcJY3wySdIA2xPzvko/KiwF4/zx7lXq9EQCoYSQNpxwMS3A==", "license": "BSD-3-Clause-Clear", - "peer": true, "dependencies": { "@zama-fhe/relayer-sdk": "~0.4.2", "viem": "^2.47.12" @@ -4785,8 +5421,7 @@ "version": "4.0.0-beta.5", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ajv": { "version": "6.15.0", @@ -4821,6 +5456,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5053,6 +5695,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/axe-core": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", @@ -5121,6 +5773,13 @@ "require-from-string": "^2.0.2" } }, + "node_modules/bmp-ts": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz", + "integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", @@ -5385,7 +6044,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "license": "MIT", - "peer": true, "engines": { "node": ">=20" } @@ -6296,9 +6954,9 @@ } }, "node_modules/ethers": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", - "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz", + "integrity": "sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==", "funding": [ { "type": "individual", @@ -6310,33 +6968,24 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "@adraffy/ens-normalize": "1.10.1", + "@adraffy/ens-normalize": "1.11.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", "@types/node": "22.7.5", "aes-js": "4.0.0-beta.5", "tslib": "2.7.0", - "ws": "8.17.1" + "ws": "8.21.0" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/ethers/node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", - "license": "MIT", - "peer": true - }, "node_modules/ethers/node_modules/@noble/curves": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "license": "MIT", - "peer": true, "dependencies": { "@noble/hashes": "1.3.2" }, @@ -6349,7 +6998,6 @@ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 16" }, @@ -6362,7 +7010,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -6371,22 +7018,19 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/ethers/node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ethers/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -6409,6 +7053,12 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==", + "dev": true + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -6484,8 +7134,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz", "integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fflate": { "version": "0.8.3", @@ -6506,6 +7155,25 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -6574,12 +7242,12 @@ } }, "node_modules/framer-motion": { - "version": "12.41.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.41.0.tgz", - "integrity": "sha512-OHAMNiCEON1RDBlRGuulsN5AD8ptMjvk5QWfFmYmBLPZ3zFGIJe60kQucQQf4cez1OzQmjYBWDY+dYfISkUdqg==", + "version": "12.42.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.0.tgz", + "integrity": "sha512-wp7EJnfWaaEScVygKv3e20udoRz+LbtxScsuTkakAxfXmt+ReC6WyPW2nINRAGvd+hG9odwcjBLyOTPjH5pBRA==", "license": "MIT", "dependencies": { - "motion-dom": "^12.41.0", + "motion-dom": "^12.42.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, @@ -6758,6 +7426,17 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/gifwrap": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", + "integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6974,6 +7653,23 @@ "node": ">= 4" } }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "dev": true, + "license": "MIT" + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -7011,8 +7707,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/internal-slot": { "version": "1.1.0", @@ -7524,6 +8219,45 @@ "react": "^19.0.0" } }, + "node_modules/jimp": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.1.tgz", + "integrity": "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/diff": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-gif": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-blur": "1.6.1", + "@jimp/plugin-circle": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-contain": "1.6.1", + "@jimp/plugin-cover": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-displace": "1.6.1", + "@jimp/plugin-dither": "1.6.1", + "@jimp/plugin-fisheye": "1.6.1", + "@jimp/plugin-flip": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/plugin-mask": "1.6.1", + "@jimp/plugin-print": "1.6.1", + "@jimp/plugin-quantize": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/plugin-rotate": "1.6.1", + "@jimp/plugin-threshold": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -7534,6 +8268,13 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7633,7 +8374,6 @@ "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "node-addon-api": "^2.0.0", "node-gyp-build": "^4.2.0", @@ -8093,6 +8833,19 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -8137,12 +8890,12 @@ } }, "node_modules/motion": { - "version": "12.41.0", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.41.0.tgz", - "integrity": "sha512-avEDKE22rFPJqDr3Ttk7gMQpeaOmNik60NoJ5T0tj+RBCNvz21D3ArY3l4uitoeQ7eIpDqueWaO3pPYFv8JOVA==", + "version": "12.42.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.0.tgz", + "integrity": "sha512-Qhwvu9sVl5/URSq5CNzwMCpSKK8Uhnrwb6VO977kZyj/wOCS7mWebJUnBoHx5cZU1Zv8a9BD5CSICWKAlrLJgA==", "license": "MIT", "dependencies": { - "framer-motion": "^12.41.0", + "framer-motion": "^12.42.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -8163,9 +8916,9 @@ } }, "node_modules/motion-dom": { - "version": "12.41.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.41.0.tgz", - "integrity": "sha512-Lk3J39fOGg6xNr1KRZsN6usDyBf8aP7MEbUPez1VCughHt79OrP7VGqNrPyFL0riaT7WS8t9DRw1M3BHtM/xKw==", + "version": "12.42.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.0.tgz", + "integrity": "sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w==", "license": "MIT", "dependencies": { "motion-utils": "^12.39.0" @@ -8292,8 +9045,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/node-exports-info": { "version": "1.6.0", @@ -8319,7 +9071,6 @@ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "license": "MIT", - "peer": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -8340,15 +9091,13 @@ "version": "1.4.0-alpha.3", "resolved": "https://registry.npmjs.org/node-tfhe/-/node-tfhe-1.4.0-alpha.3.tgz", "integrity": "sha512-oTcWL0OFA6t6BhScmDiGQ3VA8tU8T3EXCzIzpNxQxcuJDgQtiUF5CV6dgJLOrpWck4KCp1Bo/xLhv07uwn3q6Q==", - "license": "BSD-3-Clause-Clear", - "peer": true + "license": "BSD-3-Clause-Clear" }, "node_modules/node-tkms": { "version": "0.12.8", "resolved": "https://registry.npmjs.org/node-tkms/-/node-tkms-0.12.8.tgz", "integrity": "sha512-4erFxgbSVm1HCohIN2qijDfQL2GoIGaBve7SDeIKTu2bNBZZdTRKatcW+ExwHZF5MC6CzGDTvJQhEnG9LD7T3w==", - "license": "BSD-3-Clause-Clear", - "peer": true + "license": "BSD-3-Clause-Clear" }, "node_modules/object-assign": { "version": "4.1.1", @@ -8487,6 +9236,13 @@ "node": ">=12.20.0" } }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "dev": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -8585,6 +9341,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8598,6 +9361,31 @@ "node": ">=6" } }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8650,6 +9438,39 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pixelmatch": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", + "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^6.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8960,7 +9781,6 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -9180,8 +10000,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/safe-push-apply": { "version": "1.0.0", @@ -9218,6 +10037,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -9445,6 +10274,16 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-xml-to-json": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.7.tgz", + "integrity": "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.12.2" + } + }, "node_modules/simplex-noise": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-4.0.3.tgz", @@ -9526,7 +10365,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -9668,6 +10506,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -9761,8 +10616,7 @@ "version": "1.4.0-alpha.3", "resolved": "https://registry.npmjs.org/tfhe/-/tfhe-1.4.0-alpha.3.tgz", "integrity": "sha512-xdla7hi2WzLFIdAx2/ihRZ/bKlKcgDDabTJGtoqp1E5oqhLM1PzTXsJE0p7tW8+ebrvxiMGfbgMAWnU3f2ZAIQ==", - "license": "BSD-3-Clause-Clear", - "peer": true + "license": "BSD-3-Clause-Clear" }, "node_modules/three": { "version": "0.184.0", @@ -9809,6 +10663,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", @@ -9881,8 +10742,7 @@ "version": "0.12.8", "resolved": "https://registry.npmjs.org/tkms/-/tkms-0.12.8.tgz", "integrity": "sha512-iXS8wxz3jhx3JlKVJiBZUOibGtP69lC2H9I120EfhAI5amc/4xW/HCM7tmMtBJEuqdCoN5ssnHvfGog8gZ6UKg==", - "license": "BSD-3-Clause-Clear", - "peer": true + "license": "BSD-3-Clause-Clear" }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -9897,6 +10757,25 @@ "node": ">=8.0" } }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/troika-three-text": { "version": "0.52.4", "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", @@ -10138,6 +11017,19 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -10295,12 +11187,21 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utif2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", + "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.11" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/utility-types": { "version": "3.11.0", @@ -10704,8 +11605,7 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/webgl-constants": { "version": "1.1.1", @@ -10870,6 +11770,37 @@ } } }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 1d38274..a308bfc 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "zamavault", + "name": "shadowline", "version": "0.1.0", "private": true, "scripts": { @@ -7,7 +7,10 @@ "build": "next build", "start": "next start", "lint": "eslint", - "test": "vitest run" + "test": "vitest run", + "setup": "node scripts/setup.js", + "deploy": "node scripts/setup.js", + "docker:up": "docker compose up -d --build" }, "dependencies": { "@radix-ui/react-icons": "^1.3.2", @@ -15,6 +18,7 @@ "@react-three/fiber": "^9.6.1", "@tanstack/react-query": "^5.101.0", "@zama-fhe/react-sdk": "^3.0.1", + "@zama-fhe/sdk": "^3.0.1", "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -44,6 +48,7 @@ "@types/three": "^0.184.1", "eslint": "^9", "eslint-config-next": "16.2.9", + "jimp": "^1.6.1", "tailwindcss": "^4.3.1", "typescript": "^5", "vitest": "^4.1.9" diff --git a/public/.well-known/ai-plugin.json b/public/.well-known/ai-plugin.json new file mode 100644 index 0000000..392e4a2 --- /dev/null +++ b/public/.well-known/ai-plugin.json @@ -0,0 +1,17 @@ +{ + "schema_version": "v1", + "name_for_human": "ShadowLine FHE Confidential Token Registry", + "name_for_model": "shadowline_fhe_registry", + "description_for_human": "Discover and query ERC-7984 confidential token wrapper pairs on Zama FHEVM.", + "description_for_model": "Plugin for querying verified ERC-20 to ERC-7984 confidential wrapper token pairs on Zama FHEVM (Ethereum Sepolia Testnet and Ethereum Mainnet). Use this to find confidential token addresses, underlying token symbols, decimals, and scaling rules for shielding and unshielding assets.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "https://shadow-line.vercel.app/openapi.json" + }, + "logo_url": "https://shadow-line.vercel.app/file.svg", + "contact_email": "dev@shadowline.protocol", + "legal_info_url": "https://shadow-line.vercel.app/app/docs" +} diff --git a/public/brands/blockscout-logo.svg b/public/brands/blockscout-logo.svg new file mode 100644 index 0000000..734b731 --- /dev/null +++ b/public/brands/blockscout-logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/brands/openzeppelin-logo.svg b/public/brands/openzeppelin-logo.svg new file mode 100644 index 0000000..5bb452e --- /dev/null +++ b/public/brands/openzeppelin-logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/brands/steakhouse-logo.png b/public/brands/steakhouse-logo.png new file mode 100644 index 0000000..5ec9716 Binary files /dev/null and b/public/brands/steakhouse-logo.png differ diff --git a/public/brands/zama-logo.svg b/public/brands/zama-logo.svg new file mode 100644 index 0000000..b4fd205 --- /dev/null +++ b/public/brands/zama-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..84b4c92 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/icon.jpg b/public/icon.jpg new file mode 100644 index 0000000..84b4c92 Binary files /dev/null and b/public/icon.jpg differ diff --git a/public/images/abstract-glass-artwork.jpg b/public/images/abstract-glass-artwork.jpg new file mode 100644 index 0000000..0c24c44 Binary files /dev/null and b/public/images/abstract-glass-artwork.jpg differ diff --git a/public/images/abstract-gold-torus.jpg b/public/images/abstract-gold-torus.jpg new file mode 100644 index 0000000..4a1ff47 Binary files /dev/null and b/public/images/abstract-gold-torus.jpg differ diff --git a/public/images/confidential-token-crystal.jpg b/public/images/confidential-token-crystal.jpg new file mode 100644 index 0000000..da36924 Binary files /dev/null and b/public/images/confidential-token-crystal.jpg differ diff --git a/public/images/crystal-shield-decor.jpg b/public/images/crystal-shield-decor.jpg new file mode 100644 index 0000000..8cba9c1 Binary files /dev/null and b/public/images/crystal-shield-decor.jpg differ diff --git a/public/images/defi-interface-mockup.jpg b/public/images/defi-interface-mockup.jpg new file mode 100644 index 0000000..ffad97c Binary files /dev/null and b/public/images/defi-interface-mockup.jpg differ diff --git a/public/images/encrypted-lock-prism.jpg b/public/images/encrypted-lock-prism.jpg new file mode 100644 index 0000000..aa4246e Binary files /dev/null and b/public/images/encrypted-lock-prism.jpg differ diff --git a/public/images/flowing-amber-aurora.jpg b/public/images/flowing-amber-aurora.jpg new file mode 100644 index 0000000..1602e92 Binary files /dev/null and b/public/images/flowing-amber-aurora.jpg differ diff --git a/public/images/obsidian-glass-sculpture.jpg b/public/images/obsidian-glass-sculpture.jpg new file mode 100644 index 0000000..39ff550 Binary files /dev/null and b/public/images/obsidian-glass-sculpture.jpg differ diff --git a/public/images/privacy-boundary-diagram.jpg b/public/images/privacy-boundary-diagram.jpg new file mode 100644 index 0000000..fb07eca Binary files /dev/null and b/public/images/privacy-boundary-diagram.jpg differ diff --git a/public/llms-full.txt b/public/llms-full.txt new file mode 100644 index 0000000..69347c1 --- /dev/null +++ b/public/llms-full.txt @@ -0,0 +1,170 @@ +# ShadowLine Complete AI Agent & Developer Reference (`llms-full.txt`) + +> Comprehensive technical specification, contract ABIs, REST API documentation, and code integration guide for autonomous AI agents and web3 developers building on ShadowLine. + +--- + +## 1. Protocol Architecture & Overview +ShadowLine is a decentralized asset shielding protocol built on Zama's Fully Homomorphic Encryption Virtual Machine (fhEVM). It acts as a confidential wrapper layer for ERC-20 tokens, adhering to the ERC-7984 confidential token standard. + +### Why Autonomous AI Agents Need ShadowLine +Autonomous AI agents executing DeFi trading strategies, payroll, or DAO treasury management face severe MEV (Maximal Extractable Value) exploitation and strategy front-running when holding public ERC-20 tokens. By shielding tokens into ERC-7984 confidential wrappers (`cTokens`), an AI agent can: +1. Hold encrypted treasury balances on-chain. +2. Execute confidential peer-to-peer transfers without revealing transaction amounts to blockchain observers or MEV searchers. +3. Decrypt holdings locally inside trusted memory using EIP-712 read-only permits. + +--- + +## 2. REST API Reference + +ShadowLine provides a wallet-free, public HTTP REST API for discovering verified ERC-20 ↔ ERC-7984 wrapper pairs. + +### Endpoint: `GET /api/registry` +Queries the on-chain `WrappersRegistry` contract and returns all registered token pairs. + +#### Query Parameters: +- `chain` (optional): `"sepolia"` (default) or `"mainnet"`. + +#### Example Request: +```bash +curl -s "https://shadow-line.vercel.app/api/registry?chain=sepolia" +``` + +#### Response JSON Schema: +```json +{ + "pairs": [ + { + "tokenAddress": "0x9b5Cd13b8eFbB58Dc25A05CF411D8056058aDFfF", + "confidentialTokenAddress": "0x7c5BF43B851c1dff1a4feE8dB225b87f2C223639", + "symbol": "USDC", + "confidentialSymbol": "cUSDC", + "name": "USD Coin", + "decimals": 6, + "wrapperDecimals": 6, + "isValid": true, + "source": "registry" + } + ], + "total": 1, + "chain": "sepolia", + "registryAddress": "0x2f0750Bbb0A246059d80e94c454586a7F27a128e", + "timestamp": 1751740000000, + "source": "on-chain" +} +``` + +--- + +## 3. Decimal Scaling Rules (CRITICAL FOR AGENTS) +When building automated transactions, agents must adhere to strict decimal scaling: +- **Shield (Deposit):** The input amount MUST be formatted using the **underlying ERC-20 token's decimals** (e.g., 6 for USDC, 18 for WETH/ZAMA). +- **Unshield (Withdraw) & Confidential Transfers:** The input amount MUST be formatted using the **wrapper's fixed 6-decimal scale (`euint64`)**, regardless of what decimals the underlying ERC-20 uses. +- **Why?** Zama's fhEVM represents encrypted token balances as 64-bit unsigned homomorphic integers (`euint64`). To prevent overflow and maintain uniform computation costs across all assets, all ERC-7984 wrappers normalize balances to 6 decimal places. + +--- + +## 4. Smart Contract ABIs for AI Agents + +### WrappersRegistry Contract +- **Sepolia Address:** `0x2f0750Bbb0A246059d80e94c454586a7F27a128e` +- **Mainnet Address:** `0xeb5015fF021DB115aCe010f23F55C2591059bBA0` + +```json +[ + { + "name": "getTokenConfidentialTokenPairsLength", + "type": "function", + "stateMutability": "view", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "name": "getTokenConfidentialTokenPairsSlice", + "type": "function", + "stateMutability": "view", + "inputs": [ + { "name": "fromIndex", "type": "uint256" }, + { "name": "toIndex", "type": "uint256" } + ], + "outputs": [ + { + "type": "tuple[]", + "components": [ + { "name": "tokenAddress", "type": "address" }, + { "name": "confidentialTokenAddress", "type": "address" }, + { "name": "isValid", "type": "bool" } + ] + } + ] + } +] +``` + +### ERC-7984 Confidential Wrapper Contract (`cToken`) +> **WARNING FOR AI AGENTS:** Do NOT call `transfer` or `unwrap` using raw plaintext numbers or padded hex strings in Viem/Ethers! In FHEVM, confidential transfers and unshielding require client-side WASM encryption (`euint64`) and FHE input proof generation. Always use `ShadowlineAgentHarness` or `@zama-fhe/sdk` to execute these methods. + +```json +[ + { + "name": "wrap", + "type": "function", + "stateMutability": "nonpayable", + "inputs": [ + { "name": "to", "type": "address" }, + { "name": "amount", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "bytes32" }] + }, + { + "name": "confidentialBalanceOf", + "type": "function", + "stateMutability": "view", + "inputs": [{ "name": "account", "type": "address" }], + "outputs": [{ "name": "", "type": "bytes32" }] + } +] +``` + +--- + +## 5. Autonomous Agent Integration Code (Headless Node.js Harness) + +To enable true autonomous execution without a browser, ShadowLine provides a headless execution harness (`ShadowlineAgentHarness`) in `@shadowline/agent-tools` (`src/lib/agent-harness.ts`). It wraps `@zama-fhe/sdk` in Node worker pool mode (`node()`) and handles EIP-712 permit signing, KMS decryption, and FHE client-side encryption automatically. + +```typescript +import { ShadowlineAgentHarness } from '@/lib/agent-harness'; + +// 1. Initialize Headless Agent Harness +const agent = new ShadowlineAgentHarness({ + privateKey: process.env.AGENT_PRIVATE_KEY as `0x${string}`, + relayerApiKey: process.env.ZAMA_RELAYER_API_KEY || 'YOUR_API_KEY', +}); + +await agent.init(); + +// 2. Discover Verified Pairs +const pairs = await agent.getPairs(); +const usdcPair = pairs.find(p => p.symbol === 'USDC') || pairs[0]; + +// 3. Shield (Wrap) ERC-20 -> ERC-7984 +// NOTE: Shielding amount MUST use underlying ERC-20 decimals (e.g. 6 for USDC) +const shieldHash = await agent.shield(usdcPair.confidentialTokenAddress, '10.5'); +console.log('Shielded in tx:', shieldHash); + +// 4. Check Confidential Balance +// Automatically generates EIP-712 permit, queries Zama KMS, and decrypts in WASM +const balance = await agent.getConfidentialBalance(usdcPair.confidentialTokenAddress); +console.log('Decrypted Confidential Balance:', balance); + +// 5. Confidential Transfer +// Automatically encrypts amount into an FHE euint64 handle and generates FHE input proof +// NOTE: Transfer amount MUST use fixed 6 decimals scale! +const transferHash = await agent.transfer(usdcPair.confidentialTokenAddress, '0xRecipientAddress', '5.0'); +console.log('Confidential transfer tx:', transferHash); + +// 6. Unshield (Unwrap) ERC-7984 -> ERC-20 +// NOTE: Unshield amount MUST use fixed 6 decimals scale! +const unshieldHash = await agent.unshield(usdcPair.confidentialTokenAddress, '5.0'); +console.log('Unshield requested in tx:', unshieldHash); +``` diff --git a/public/llms.txt b/public/llms.txt new file mode 100644 index 0000000..688854b --- /dev/null +++ b/public/llms.txt @@ -0,0 +1,31 @@ +# ShadowLine + +> Privacy-first asset shielding protocol built on Zama FHEVM. Confidentially shield, transfer, and unshield ERC-20 tokens using the ERC-7984 standard. + +ShadowLine enables users, DAOs, and autonomous AI agents to wrap standard public ERC-20 tokens into confidential ERC-7984 tokens (`cTokens`). On-chain balances and transfer amounts are encrypted using Fully Homomorphic Encryption (FHE) via Zama's fhEVM, preventing front-running, strategy copy-trading, and wallet tracking. + +## Core Capabilities for AI Agents +- **Shield (Wrap):** Lock public ERC-20 tokens in a wrapper contract to mint an encrypted balance (`euint64`) on-chain. +- **Confidential Transfer:** Send confidential tokens to any address. The transfer amount is encrypted client-side; on-chain observers see sender and receiver addresses but never the token amount. +- **Unshield (Unwrap):** Request withdrawal of confidential tokens back to public ERC-20 tokens via Zama Gateway threshold decryption. +- **REST API:** Query verified wrapper pairs without a wallet or web3 provider via `GET /api/registry`. + +## Key Resources +- [Live Web App & Registry](https://shadow-line.vercel.app/app) +- [Developer Documentation](https://shadow-line.vercel.app/app/docs) +- [REST API Reference](https://shadow-line.vercel.app/app/docs/rest-api) +- [GitHub Repository](https://github.com/hosein-ul/ShadowLine) +- [Zama FHEVM Documentation](https://docs.zama.org/protocol) + +## Network & Contract Architecture +- **Supported Networks:** Ethereum Sepolia Testnet (Chain ID: 11155111), Ethereum Mainnet (Chain ID: 1) +- **Encryption Scale:** All ERC-7984 confidential ciphertexts use a fixed 6-decimal scale (`euint64`), regardless of the underlying ERC-20 decimals. +- **Decryption Security:** Balance decryption requires an EIP-712 read-only permit signature (`FHE.allowThis`/`allow`). Private keys and plaintext balances never leave the client/agent memory. + +## AI Agent Integration (Drop-in Hook & API) +AI agents can fetch live verified token pairs via HTTP: +```http +GET https://shadow-line.vercel.app/api/registry?chain=sepolia +``` + +For headless Node.js or Python agent execution (without a browser), import `ShadowlineAgentHarness` from `src/lib/agent-harness.ts` (also exported in `@shadowline/agent-tools`). For React dApps, reference `src/lib/use-shadowline.ts` or view the full technical specification in `/llms-full.txt`. diff --git a/public/logo-icon.jpg b/public/logo-icon.jpg new file mode 100644 index 0000000..84b4c92 Binary files /dev/null and b/public/logo-icon.jpg differ diff --git a/public/logo-text-transparent.png b/public/logo-text-transparent.png new file mode 100644 index 0000000..9543e9f Binary files /dev/null and b/public/logo-text-transparent.png differ diff --git a/public/logo-text.jpg b/public/logo-text.jpg new file mode 100644 index 0000000..9d650d5 Binary files /dev/null and b/public/logo-text.jpg differ diff --git a/public/openapi.json b/public/openapi.json new file mode 100644 index 0000000..9b13286 --- /dev/null +++ b/public/openapi.json @@ -0,0 +1,112 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "ShadowLine FHE Confidential Token Registry API", + "description": "Public REST API for querying on-chain ERC-20 to ERC-7984 confidential wrapper pairs on Zama FHEVM.", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://shadow-line.vercel.app" + } + ], + "paths": { + "/api/registry": { + "get": { + "operationId": "getConfidentialTokenPairs", + "summary": "Get all verified confidential token wrapper pairs", + "description": "Returns a list of all registered ERC-20 to ERC-7984 confidential token wrapper pairs from the on-chain WrappersRegistry contract.", + "parameters": [ + { + "name": "chain", + "in": "query", + "description": "Blockchain network to query ('sepolia' or 'mainnet'). Defaults to 'sepolia'.", + "required": false, + "schema": { + "type": "string", + "enum": ["sepolia", "mainnet"], + "default": "sepolia" + } + } + ], + "responses": { + "200": { + "description": "Successful response containing wrapper pairs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "RegistryResponse": { + "type": "object", + "properties": { + "pairs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PairResult" + } + }, + "total": { + "type": "integer" + }, + "chain": { + "type": "string" + }, + "registryAddress": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "source": { + "type": "string" + } + } + }, + "PairResult": { + "type": "object", + "properties": { + "tokenAddress": { + "type": "string", + "description": "Underlying public ERC-20 contract address" + }, + "confidentialTokenAddress": { + "type": "string", + "description": "Confidential ERC-7984 wrapper contract address" + }, + "symbol": { + "type": "string", + "description": "Normalized underlying token symbol (e.g. USDC)" + }, + "confidentialSymbol": { + "type": "string", + "description": "Confidential token symbol (e.g. cUSDC)" + }, + "name": { + "type": "string" + }, + "decimals": { + "type": "integer", + "description": "Underlying ERC-20 decimals" + }, + "wrapperDecimals": { + "type": "integer", + "description": "Fixed 6 decimals for FHE euint64 ciphertexts" + }, + "isValid": { + "type": "boolean" + } + } + } + } + } +} diff --git a/public/tokens/bron.png b/public/tokens/bron.png new file mode 100644 index 0000000..0c01d8d Binary files /dev/null and b/public/tokens/bron.png differ diff --git a/public/tokens/eth.png b/public/tokens/eth.png new file mode 100644 index 0000000..e29c0b3 Binary files /dev/null and b/public/tokens/eth.png differ diff --git a/public/tokens/usdc.png b/public/tokens/usdc.png new file mode 100644 index 0000000..d81127d Binary files /dev/null and b/public/tokens/usdc.png differ diff --git a/public/tokens/usdt.png b/public/tokens/usdt.png new file mode 100644 index 0000000..898c0e5 Binary files /dev/null and b/public/tokens/usdt.png differ diff --git a/public/tokens/weth.png b/public/tokens/weth.png new file mode 100644 index 0000000..0fda675 Binary files /dev/null and b/public/tokens/weth.png differ diff --git a/public/tokens/zama.png b/public/tokens/zama.png new file mode 100644 index 0000000..7c8159b Binary files /dev/null and b/public/tokens/zama.png differ diff --git a/scripts/remove-bg.js b/scripts/remove-bg.js new file mode 100644 index 0000000..419ad0e --- /dev/null +++ b/scripts/remove-bg.js @@ -0,0 +1,48 @@ +const jimpLib = require("jimp"); + +async function run() { + const JimpClass = jimpLib.Jimp || jimpLib; + console.log("Reading image..."); + const image = await JimpClass.read("public/logo-text.jpg"); + const width = image.bitmap.width; + const height = image.bitmap.height; + console.log("Image loaded:", width, "x", height); + + // Sample top-left corner as background color reference + const bgR = image.bitmap.data[0]; + const bgG = image.bitmap.data[1]; + const bgB = image.bitmap.data[2]; + console.log("Background color sample:", bgR, bgG, bgB); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = (width * y + x) << 2; + const r = image.bitmap.data[idx + 0]; + const g = image.bitmap.data[idx + 1]; + const b = image.bitmap.data[idx + 2]; + + // Euclidean distance from background color + const dist = Math.sqrt((r - bgR)**2 + (g - bgG)**2 + (b - bgB)**2); + + if (dist < 40) { + image.bitmap.data[idx + 3] = 0; // 100% transparent + } else if (dist < 80) { + // Smooth alpha edge transition + image.bitmap.data[idx + 3] = Math.floor(((dist - 40) / 40) * 255); + } + } + } + + console.log("Saving transparent PNG..."); + if (typeof image.writeAsync === "function") { + await image.writeAsync("public/logo-text-transparent.png"); + } else if (typeof image.write === "function") { + await image.write("public/logo-text-transparent.png"); + } + console.log("Successfully created public/logo-text-transparent.png!"); +} + +run().catch(err => { + console.error("Error processing image:", err); + process.exit(1); +}); diff --git a/scripts/setup.js b/scripts/setup.js new file mode 100644 index 0000000..69e909b --- /dev/null +++ b/scripts/setup.js @@ -0,0 +1,213 @@ +#!/usr/bin/env node +/** + * ShadowLine 0-to-100 Automated Setup & Launcher + * + * Cross-platform CLI wizard for Linux Ubuntu, Windows, and macOS. + * Handles environment configuration, dependency installation, and + * launching local dev/prod servers or cloud deployments. + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const readline = require('readline'); + +const ROOT_DIR = path.resolve(__dirname, '..'); +const ENV_LOCAL_PATH = path.join(ROOT_DIR, '.env.local'); + +// ANSI Color codes for terminal formatting +const colors = { + reset: '\x1b[0m', + bold: '\x1b[1m', + cyan: '\x1b[36m', + green: '\x1b[32m', + yellow: '\x1b[33m', + red: '\x1b[31m', + magenta: '\x1b[35m', +}; + +function printBanner() { + console.clear(); + console.log(`${colors.cyan}${colors.bold}`); + console.log(' ███████╗██╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ██╗██╗ ██╗███╗ ██╗███████╗'); + console.log(' ██╔════╝██║ ██║██╔══██╗██╔══██╗██╔═══██╗██║ ██║██║ ██║████╗ ██║██╔════╝'); + console.log(' ███████╗███████║███████║██║ ██║██║ ██║██║ █╗ ██║██║ ██║██╔██╗ ██║█████╗ '); + console.log(' ╚════██║██╔══██║██╔══██║██║ ██║██║ ██║██║███╗██║██║ ██║██║╚██╗██║██╔══╝ '); + console.log(' ███████║██║ ██║██║ ██║██████╔╝╚██████╔╝╚███╔███╔╝███████╗██║██║ ╚████║███████╗'); + console.log(' ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝'); + console.log(`${colors.reset}`); + console.log(`${colors.green}${colors.bold} 🔒 Confidential Asset Shielding Protocol | ⚡ Powered by Zama FHEVM${colors.reset}`); + console.log(`${colors.magenta} 🌐 Open-Source Protocol (MIT License) | 💎 Powered by x.com/andy1eth${colors.reset}`); + console.log(`${colors.cyan} ────────────────────────────────────────────────────────────────────────────────────${colors.reset}\n`); +} + +function checkNodeVersion() { + const version = process.version.replace('v', '').split('.')[0]; + if (parseInt(version, 10) < 18) { + console.error(`${colors.red}[ERROR] Node.js version 18 or higher is required. You are running ${process.version}${colors.reset}`); + process.exit(1); + } + console.log(`${colors.green}✔ Node.js version check passed (${process.version})${colors.reset}`); +} + +// Single shared readline interface — created once, reused across all questions +let _rl = null; +function getRl() { + if (!_rl || _rl.closed) { + _rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + } + return _rl; +} + +function closeRl() { + if (_rl && !_rl.closed) { + _rl.close(); + _rl = null; + } +} + +function question(query) { + return new Promise((resolve) => getRl().question(query, resolve)); +} + +async function setupEnvironment() { + console.log(`\n${colors.bold}─── Step 1: Environment Configuration (.env.local) ───${colors.reset}`); + + if (fs.existsSync(ENV_LOCAL_PATH)) { + console.log(`${colors.green}✔ .env.local already exists. Using existing configuration.${colors.reset}`); + return; + } + + console.log(`${colors.yellow}ℹ .env.local not found. Generating default public configuration...${colors.reset}`); + + const customize = await question(`${colors.cyan}? Do you want to configure custom API keys (WalletConnect / Relayer)? [y/N]: ${colors.reset}`); + + let wcId = 'public-demo-project-id'; + let relayerKey = ''; + + if (customize.trim().toLowerCase() === 'y' || customize.trim().toLowerCase() === 'yes') { + const inputWc = await question(`${colors.cyan}? Enter WalletConnect Project ID (leave blank for public fallback): ${colors.reset}`); + if (inputWc.trim()) wcId = inputWc.trim(); + + const inputRelayer = await question(`${colors.cyan}? Enter Zama Relayer API Key (leave blank for public testnet mode): ${colors.reset}`); + if (inputRelayer.trim()) relayerKey = inputRelayer.trim(); + } + + const envContent = [ + '# ShadowLine Automated Configuration', + 'NEXT_PUBLIC_APP_URL="http://localhost:3000"', + 'NEXT_PUBLIC_DEFAULT_CHAIN="sepolia"', + `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="${wcId}"`, + `NEXT_PUBLIC_ZAMA_RELAYER_API_KEY="${relayerKey}"`, + '', + ].join('\n'); + + fs.writeFileSync(ENV_LOCAL_PATH, envContent, 'utf8'); + console.log(`${colors.green}✔ Created .env.local successfully!${colors.reset}`); +} + +function installDependencies() { + console.log(`\n${colors.bold}─── Step 2: Installing Dependencies ───${colors.reset}`); + const nodeModulesPath = path.join(ROOT_DIR, 'node_modules'); + if (fs.existsSync(nodeModulesPath)) { + console.log(`${colors.green}✔ node_modules found. Verifying packages are up to date...${colors.reset}`); + } else { + console.log(`${colors.yellow}ℹ node_modules not found. Installing project dependencies...${colors.reset}`); + } + try { + execSync('npm install', { stdio: 'inherit', cwd: ROOT_DIR }); + console.log(`${colors.green}✔ Project dependencies verified and ready.${colors.reset}`); + } catch (error) { + console.error(`${colors.red}[ERROR] Failed to install dependencies.${colors.reset}`); + process.exit(1); + } +} + +async function presentMenu() { + console.log(`\n${colors.bold}${colors.magenta}═══════════════════════════════════════════════════════════════════════`); + console.log(`🎉 SETUP COMPLETE! What would you like to do next?`); + console.log(`═══════════════════════════════════════════════════════════════════════${colors.reset}`); + console.log(`${colors.green}[1] 🚀 Start Local Development Server (npm run dev) [RECOMMENDED]${colors.reset}`); + console.log(`[2] 🌐 Start Production Server (npm run start)`); + console.log(`[3] ☁️ Deploy to Netlify (via Netlify CLI)`); + console.log(`[4] ☁️ Deploy to Vercel (via Vercel CLI)`); + console.log(`[5] 🐳 Launch Docker Container (docker compose up -d)`); + console.log(`[0] ❌ Exit`); + console.log(`${colors.magenta}───────────────────────────────────────────────────────────────────────${colors.reset}`); + + const choice = await question(`${colors.cyan}? Select an option [0-5]: ${colors.reset}`); + // Pause readline (do NOT close/destroy it) — closing destroys stdin fd which causes EINVAL + // when execSync tries to inherit stdio from this process. + getRl().pause(); + + switch (choice.trim()) { + case '1': + console.log(`\n${colors.green}🚀 Launching local development server on http://localhost:3000 ...${colors.reset}\n`); + try { + execSync('npm run dev', { stdio: 'inherit', cwd: ROOT_DIR }); + } catch (e) { + // Ctrl+C or dev server exit — not a fatal error, just exit cleanly + process.exit(0); + } + break; + case '2': + console.log(`\n${colors.green}🌐 Launching production server on http://localhost:3000 ...${colors.reset}\n`); + try { + execSync('npm run start', { stdio: 'inherit', cwd: ROOT_DIR }); + } catch (e) { + process.exit(0); + } + break; + case '3': + console.log(`\n${colors.cyan}☁️ Deploying to Netlify...${colors.reset}`); + console.log(`${colors.yellow}ℹ Make sure you are logged in. If not, run: npx netlify login${colors.reset}\n`); + try { + execSync('npx netlify-cli deploy --prod', { stdio: 'inherit', cwd: ROOT_DIR }); + } catch (e) { + console.error(`${colors.red}[ERROR] Netlify deployment failed.${colors.reset}`); + } + break; + case '4': + console.log(`\n${colors.cyan}☁️ Deploying to Vercel...${colors.reset}`); + console.log(`${colors.yellow}ℹ Make sure you are logged in. If not, run: npx vercel login${colors.reset}\n`); + try { + execSync('npx vercel --prod', { stdio: 'inherit', cwd: ROOT_DIR }); + } catch (e) { + console.error(`${colors.red}[ERROR] Vercel deployment failed.${colors.reset}`); + } + break; + case '5': + console.log(`\n${colors.cyan}🐳 Launching Docker container...${colors.reset}\n`); + try { + execSync('docker compose up -d --build', { stdio: 'inherit', cwd: ROOT_DIR }); + console.log(`${colors.green}✔ Docker container running on http://localhost:3000${colors.reset}`); + } catch (e) { + console.error(`${colors.red}[ERROR] Docker command failed. Is Docker running?${colors.reset}`); + } + break; + case '0': + console.log(`\n${colors.yellow}Goodbye! Re-run anytime with: npm run setup${colors.reset}\n`); + process.exit(0); + break; + default: + console.log(`${colors.red}Invalid option. Exiting.${colors.reset}`); + process.exit(0); + } +} + +async function main() { + printBanner(); + checkNodeVersion(); + await setupEnvironment(); + installDependencies(); + await presentMenu(); +} + +main().catch((err) => { + console.error(`${colors.red}[FATAL ERROR]`, err.message || err, colors.reset); + closeRl(); + process.exit(1); +}); diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 new file mode 100644 index 0000000..f06fc14 --- /dev/null +++ b/scripts/setup.ps1 @@ -0,0 +1,121 @@ +# ShadowLine 0-to-100 Quick Installer and Prerequisite Auto-Installer for Windows PowerShell +# +# Usage (one-liner in PowerShell): +# irm https://raw.githubusercontent.com/hosein-ul/ShadowLine/main/scripts/setup.ps1 | iex +# +# Or locally: +# .\scripts\setup.ps1 + +$ErrorActionPreference = "Stop" + +Write-Host ' ███████╗██╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ██╗██╗ ██╗███╗ ██╗███████╗' -ForegroundColor Cyan +Write-Host ' ██╔════╝██║ ██║██╔══██╗██╔══██╗██╔═══██╗██║ ██║██║ ██║████╗ ██║██╔════╝' -ForegroundColor Cyan +Write-Host ' ███████╗███████║███████║██║ ██║██║ ██║██║ █╗ ██║██║ ██║██╔██╗ ██║█████╗ ' -ForegroundColor Cyan +Write-Host ' ╚════██║██╔══██║██╔══██║██║ ██║██║ ██║██║███╗██║██║ ██║██║╚██╗██║██╔══╝ ' -ForegroundColor Cyan +Write-Host ' ███████║██║ ██║██║ ██║██████╔╝╚██████╔╝╚███╔███╔╝███████╗██║██║ ╚████║███████╗' -ForegroundColor Cyan +Write-Host ' ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝' -ForegroundColor Cyan +Write-Host ' 🔒 Confidential Asset Shielding Protocol | ⚡ Powered by Zama FHEVM' -ForegroundColor Green +Write-Host ' 🌐 Open-Source Protocol (MIT License) | 💎 Powered by x.com/andy1eth' -ForegroundColor Magenta +Write-Host ' ────────────────────────────────────────────────────────────────────────────────────' -ForegroundColor Cyan +Write-Host "" + +# Helper to refresh PATH in current session after winget/choco installs +function Refresh-Path { + $machinePath = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + $userPath = [System.Environment]::GetEnvironmentVariable("Path", "User") + $env:Path = "$machinePath;$userPath" +} + +# 1. Check and Auto-Install Git +if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Write-Host "ℹ Git not found. Attempting automatic installation..." -ForegroundColor Yellow + if (Get-Command winget -ErrorAction SilentlyContinue) { + winget install --id Git.Git -e --source winget --accept-package-agreements --accept-source-agreements + Refresh-Path + } elseif (Get-Command choco -ErrorAction SilentlyContinue) { + choco install git -y + Refresh-Path + } else { + Write-Host "[ERROR] Git is not installed and no package manager (winget/choco) was found." -ForegroundColor Red + Write-Host "Please install Git from https://git-scm.com/ and re-run this script." -ForegroundColor Red + exit 1 + } +} else { + Write-Host "✔ Git is already installed ($(git --version))." -ForegroundColor Green +} + +# 2. Check and Auto-Install Node.js (v18+) +if (-not (Get-Command node -ErrorAction SilentlyContinue)) { + Write-Host "ℹ Node.js not found. Attempting automatic installation of Node.js LTS..." -ForegroundColor Yellow + if (Get-Command winget -ErrorAction SilentlyContinue) { + winget install --id OpenJS.NodeJS.LTS -e --source winget --accept-package-agreements --accept-source-agreements + Refresh-Path + } elseif (Get-Command choco -ErrorAction SilentlyContinue) { + choco install nodejs-lts -y + Refresh-Path + } else { + Write-Host "[ERROR] Node.js is not installed and no package manager was found." -ForegroundColor Red + Write-Host "Please install Node.js v18+ from https://nodejs.org/ and re-run this script." -ForegroundColor Red + exit 1 + } +} else { + try { + $nodeVer = (node -v) -replace 'v','' -split '\.' | Select-Object -First 1 + if ([int]$nodeVer -lt 18) { + Write-Host "[ERROR] Node.js version 18 or higher is required. Found v$nodeVer. Please upgrade." -ForegroundColor Red + exit 1 + } + Write-Host "✔ Node.js version check passed ($(node -v))." -ForegroundColor Green + } catch { + Write-Host "[ERROR] Could not determine Node.js version. Please ensure Node.js v18+ is installed." -ForegroundColor Red + exit 1 + } +} + +# 3. Clone or locate the repository +# Bug fix: track $RepoDir explicitly rather than relying on Set-Location side-effects +$RepoDir = $null + +# Bug fix: safely check package.json content (avoid null reference if file doesn't exist) +$InRepo = $false +if (Test-Path "package.json") { + $pkgContent = Get-Content "package.json" -Raw -ErrorAction SilentlyContinue + if ($pkgContent -and ($pkgContent | Select-String '"name": "shadowline"' -Quiet)) { + $InRepo = $true + } +} + +if ($InRepo) { + $RepoDir = (Get-Location).Path + Write-Host "✔ ShadowLine repository detected in current directory." -ForegroundColor Green +} elseif ((Test-Path "shadowline\package.json") -and (Test-Path "shadowline\.git")) { + Write-Host "ℹ Directory 'shadowline' found. Updating..." -ForegroundColor Yellow + Set-Location "shadowline" + git pull origin main + $RepoDir = (Get-Location).Path +} elseif ((Test-Path "ShadowLine\package.json") -and (Test-Path "ShadowLine\.git")) { + Write-Host "ℹ Directory 'ShadowLine' found. Updating..." -ForegroundColor Yellow + Set-Location "ShadowLine" + git pull origin main + $RepoDir = (Get-Location).Path +} else { + Write-Host "Cloning ShadowLine from GitHub..." -ForegroundColor Cyan + $currName = Split-Path -Leaf (Get-Location).Path + if (($currName -ieq "shadowline") -and -not (Get-ChildItem -Force | Where-Object { $_.Name -ne "." -and $_.Name -ne ".." })) { + git clone https://github.com/hosein-ul/ShadowLine.git . + } else { + git clone https://github.com/hosein-ul/ShadowLine.git shadowline + Set-Location "shadowline" + } + $RepoDir = (Get-Location).Path +} + +Write-Host "✔ Working directory: $RepoDir" -ForegroundColor Green +Write-Host "" + +# 4. Launch interactive setup wizard +# Bug fix: When run via irm | iex, node.exe stdin is connected to the PowerShell pipe. +# We must pass the script path explicitly and rely on setup.js's /dev/tty equivalent (CONIN$). +Write-Host "✔ Environment ready! Launching interactive setup wizard..." -ForegroundColor Green +$SetupScript = Join-Path $RepoDir "scripts\setup.js" +node $SetupScript diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000..a3554b2 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# ShadowLine 0-to-100 Quick Installer for Ubuntu Linux, Debian & macOS +# +# Usage (one-liner): +# curl -sSL https://raw.githubusercontent.com/hosein-ul/ShadowLine/main/scripts/setup.sh | bash +# +# Or locally inside the repo: +# bash scripts/setup.sh + +# Bug fix: use set -euo pipefail for robust error handling +# -e: exit on error, -u: treat unset variables as error, -o pipefail: catch pipe failures +set -euo pipefail + +GREEN='\033[0;32m' +CYAN='\033[0;36m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "${CYAN}" +cat << "EOF" + ███████╗██╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ██╗██╗ ██╗███╗ ██╗███████╗ + ██╔════╝██║ ██║██╔══██╗██╔══██╗██╔═══██╗██║ ██║██║ ██║████╗ ██║██╔════╝ + ███████╗███████║███████║██║ ██║██║ ██║██║ █╗ ██║██║ ██║██╔██╗ ██║█████╗ + ╚════██║██╔══██║██╔══██║██║ ██║██║ ██║██║███╗██║██║ ██║██║╚██╗██║██╔══╝ + ███████║██║ ██║██║ ██║██████╔╝╚██████╔╝╚███╔███╔╝███████╗██║██║ ╚████║███████╗ + ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝ +EOF +echo -e "${NC}" +echo -e "${GREEN} 🔒 Confidential Asset Shielding Protocol | ⚡ Powered by Zama FHEVM${NC}" +echo -e "${YELLOW} 🌐 Open-Source Protocol (MIT License) | 💎 Powered by x.com/andy1eth${NC}" +echo -e "${CYAN} ────────────────────────────────────────────────────────────────────────────────────${NC}\n" + +# Detect root / sudo availability +SUDO="" +if [ "${EUID:-$(id -u)}" -ne 0 ] && command -v sudo &> /dev/null; then + SUDO="sudo" +elif [ "${EUID:-$(id -u)}" -ne 0 ]; then + echo -e "${YELLOW}Warning: Running without root privileges and sudo is not installed. Package installs may fail.${NC}" +fi + +# 0. Ensure curl is installed (required for NodeSource) +if ! command -v curl &> /dev/null; then + echo -e "${YELLOW}ℹ curl not found. Attempting to install...${NC}" + if command -v apt-get &> /dev/null; then + $SUDO apt-get update -y && $SUDO apt-get install -y curl ca-certificates + elif command -v brew &> /dev/null; then + brew install curl + else + echo -e "${RED}[ERROR] curl is required but could not be installed. Please install curl manually.${NC}" + exit 1 + fi +else + echo -e "${GREEN}✔ curl is available.${NC}" +fi + +# 1. Check & Auto-Install Git +if ! command -v git &> /dev/null; then + echo -e "${YELLOW}ℹ Git not found. Attempting to install...${NC}" + if command -v apt-get &> /dev/null; then + echo -e "${CYAN}Installing git via apt-get (Ubuntu/Debian)...${NC}" + $SUDO apt-get update -y && $SUDO apt-get install -y git + elif command -v brew &> /dev/null; then + echo -e "${CYAN}Installing git via Homebrew (macOS)...${NC}" + brew install git + else + echo -e "${RED}[ERROR] Git is required. Please install git and try again.${NC}" + exit 1 + fi +else + echo -e "${GREEN}✔ Git is already installed ($(git --version)).${NC}" +fi + +# 2. Check & Auto-Install Node.js (v18+) +if ! command -v node &> /dev/null; then + echo -e "${YELLOW}ℹ Node.js not found. Installing Node.js v20 (LTS)...${NC}" + if command -v apt-get &> /dev/null; then + echo -e "${CYAN}Installing Node.js via NodeSource (Ubuntu/Debian)...${NC}" + $SUDO apt-get update -y && $SUDO apt-get install -y ca-certificates gnupg + curl -fsSL https://deb.nodesource.com/setup_20.x | $SUDO -E bash - + $SUDO apt-get install -y nodejs + elif command -v brew &> /dev/null; then + echo -e "${CYAN}Installing Node.js via Homebrew (macOS)...${NC}" + brew install node + else + echo -e "${RED}[ERROR] Node.js is required. Please install Node.js v18+ and try again.${NC}" + exit 1 + fi +else + NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) + if [ "$NODE_VER" -lt 18 ]; then + echo -e "${YELLOW}ℹ Node.js v$NODE_VER is older than required v18. Upgrading to v20 LTS...${NC}" + if command -v apt-get &> /dev/null; then + curl -fsSL https://deb.nodesource.com/setup_20.x | $SUDO -E bash - + $SUDO apt-get install -y nodejs + elif command -v brew &> /dev/null; then + brew upgrade node + else + echo -e "${RED}[ERROR] Node.js version 18 or higher is required. Found v$NODE_VER.${NC}" + exit 1 + fi + else + echo -e "${GREEN}✔ Node.js version check passed ($(node -v)).${NC}" + fi +fi + +# 3. Clone or locate the repository +# Bug fix: track the repo directory explicitly instead of relying on cd side-effects +REPO_DIR="" + +if [ -f "package.json" ] && grep -q '"name": "shadowline"' package.json 2>/dev/null; then + # Already inside the repo + REPO_DIR="$(pwd)" + echo -e "${GREEN}✔ ShadowLine repository detected in current directory.${NC}" +elif [ -f "shadowline/package.json" ] && [ -d "shadowline/.git" ]; then + echo -e "${YELLOW}ℹ Directory 'shadowline' found. Updating...${NC}" + cd shadowline + # Bug fix: don't fail on git pull errors (e.g. already up to date, detached HEAD) + git pull origin main || echo -e "${YELLOW}Warning: git pull had an issue; continuing with existing code.${NC}" + REPO_DIR="$(pwd)" +elif [ -f "ShadowLine/package.json" ] && [ -d "ShadowLine/.git" ]; then + echo -e "${YELLOW}ℹ Directory 'ShadowLine' found. Updating...${NC}" + cd ShadowLine + git pull origin main || echo -e "${YELLOW}Warning: git pull had an issue; continuing with existing code.${NC}" + REPO_DIR="$(pwd)" +else + echo -e "${CYAN}Cloning ShadowLine from GitHub...${NC}" + CURR_DIR_NAME=$(basename "$(pwd)" | tr '[:upper:]' '[:lower:]') + if [ "$CURR_DIR_NAME" = "shadowline" ] && [ -z "$(ls -A 2>/dev/null)" ]; then + git clone https://github.com/hosein-ul/ShadowLine.git . + else + git clone https://github.com/hosein-ul/ShadowLine.git shadowline + cd shadowline + fi + REPO_DIR="$(pwd)" +fi + +echo -e "${GREEN}✔ Working directory: $REPO_DIR${NC}\n" + +# 4. Launch the interactive setup wizard +# Bug fix: redirect stdin from /dev/tty so readline works even when piped via curl | bash +if [ -t 0 ]; then + # stdin is already a terminal — run normally + node "$REPO_DIR/scripts/setup.js" +elif [ -e /dev/tty ]; then + # stdin is a pipe (curl | bash) — redirect from the real terminal + node "$REPO_DIR/scripts/setup.js" < /dev/tty +else + echo -e "${RED}[ERROR] No interactive terminal available. Please run the script directly (not via pipe):${NC}" + echo -e "${CYAN} bash scripts/setup.sh${NC}" + exit 1 +fi diff --git a/src/app/ClientLayout.tsx b/src/app/ClientLayout.tsx index 79b2ecf..daae74e 100644 --- a/src/app/ClientLayout.tsx +++ b/src/app/ClientLayout.tsx @@ -8,6 +8,7 @@ import Footer from '@/components/layout/Footer'; import { useAccount } from 'wagmi'; import { sepolia, mainnet } from 'wagmi/chains'; import { type SupportedChainId } from '@/config/chains'; +import { SessionResetProvider } from '@/lib/reset-session'; type Theme = 'dark' | 'light'; export type DesignTheme = 'charcoal' | 'midnight' | 'frost' | 'aurora'; @@ -53,55 +54,49 @@ export function useActiveNetwork() { function LayoutContent({ children }: { children: React.ReactNode }) { const [isTestnet, setIsTestnet] = useState(true); - // Load network preference on mount + // Load network preference on mount — force Sepolia while Mainnet relayer API key is pending useEffect(() => { - const savedNetwork = localStorage.getItem('network-preference'); - if (savedNetwork) { - setIsTestnet(savedNetwork === 'testnet'); - } + setIsTestnet(true); + localStorage.setItem('network-preference', 'testnet'); }, []); const handleSetIsTestnet = (val: boolean) => { + if (!val) return; // Mainnet is temporarily disabled setIsTestnet(val); - localStorage.setItem('network-preference', val ? 'testnet' : 'mainnet'); + localStorage.setItem('network-preference', 'testnet'); }; // Determine active chain ID dynamically (safe now that we are within Providers) - const { chain, isConnected } = useAccount(); - const activeChainId = (isConnected && chain && (chain.id === sepolia.id || chain.id === mainnet.id) - ? chain.id - : (isTestnet ? sepolia.id : mainnet.id)) as SupportedChainId; - - // Keep isTestnet in sync with connected wallet chain - useEffect(() => { - if (isConnected && chain) { - setIsTestnet(chain.id === sepolia.id); - } - }, [chain, isConnected]); + // While Mainnet relayer API key is pending, force activeChainId to Sepolia (11155111). + // If the user connects a wallet on Mainnet (chain 1), activeChainId stays Sepolia, + // prompting all UI actions (like Wrap/Transfer/Portfolio) to show "Switch to Sepolia". + const activeChainId = sepolia.id as SupportedChainId; return ( -
-
-
- {children} -
-
-
+ +
+
+
+ {children} +
+
+
+
); } export default function ClientLayout({ children }: { children: React.ReactNode }) { - const [theme, setTheme] = useState('dark'); - const [designTheme, setDesignThemeState] = useState('charcoal'); + const [theme, setTheme] = useState('light'); + const [designTheme, setDesignThemeState] = useState('midnight'); // Load theme and design direction from localStorage on mount useEffect(() => { const savedTheme = localStorage.getItem('theme') as Theme | null; - const initialTheme = savedTheme || 'dark'; + const initialTheme = savedTheme || 'light'; setTheme(initialTheme); document.documentElement.setAttribute('data-theme', initialTheme); @@ -111,8 +106,8 @@ export default function ClientLayout({ children }: { children: React.ReactNode } setDesignThemeState(savedDesign); document.documentElement.setAttribute('data-design-theme', savedDesign); } else { - setDesignThemeState('charcoal'); - document.documentElement.setAttribute('data-design-theme', 'charcoal'); + setDesignThemeState('midnight'); + document.documentElement.setAttribute('data-design-theme', 'midnight'); } }, []); diff --git a/src/app/api/registry/route.ts b/src/app/api/registry/route.ts index 649d6fb..8116c55 100644 --- a/src/app/api/registry/route.ts +++ b/src/app/api/registry/route.ts @@ -9,7 +9,7 @@ import { REGISTRY_ADDRESSES, KNOWN_WRAPPERS } from '@/config/contracts'; * Public REST API for querying the Zama WrappersRegistry. * * Returns every registered ERC-20 ↔ ERC-7984 wrapper pair with metadata. - * Falls back to the hardcoded snapshot when the on-chain read fails. + * Falls back to the hardcoded snapshot only when the on-chain read fails. * * Usage: * fetch("https://YOUR_DEPLOYMENT_URL/api/registry?chain=sepolia") @@ -17,8 +17,15 @@ import { REGISTRY_ADDRESSES, KNOWN_WRAPPERS } from '@/config/contracts'; * .then(data => console.log(data.pairs)) */ -// Minimal ABI for the WrappersRegistry — only the methods we need. -// The real contract exposes more, but these two give us the full pair list. +/** + * Real WrappersRegistry ABI — the two view functions we need to paginate the + * pair list. Verified against the on-chain contract source at + * https://github.com/zama-ai/protocol-apps/tree/main/contracts/confidential-token-wrappers-registry + * + * Note: the previous version of this route called a non-existent `listPairs` + * function which always reverted, causing every request to fall through to + * the cached snapshot. That is now fixed. + */ const REGISTRY_ABI = [ { name: 'getTokenConfidentialTokenPairsLength', @@ -28,17 +35,22 @@ const REGISTRY_ABI = [ outputs: [{ name: '', type: 'uint256' }], }, { - // listPairs(uint256 start, uint256 count) → (address[] tokens, address[] confidentialTokens) - name: 'listPairs', + name: 'getTokenConfidentialTokenPairsSlice', type: 'function', stateMutability: 'view', inputs: [ - { name: 'start', type: 'uint256' }, - { name: 'count', type: 'uint256' }, + { name: 'fromIndex', type: 'uint256' }, + { name: 'toIndex', type: 'uint256' }, ], outputs: [ - { name: 'tokens', type: 'address[]' }, - { name: 'confidentialTokens', type: 'address[]' }, + { + type: 'tuple[]', + components: [ + { name: 'tokenAddress', type: 'address' }, + { name: 'confidentialTokenAddress', type: 'address' }, + { name: 'isValid', type: 'bool' }, + ], + }, ], }, ] as const; @@ -60,10 +72,18 @@ const RPC_URLS: Record = { [mainnet.id]: process.env.NEXT_PUBLIC_MAINNET_RPC || 'https://ethereum-rpc.publicnode.com', }; -// cbbqTGBP blocklist — same as client-side, see src/lib/registry.ts -const BLOCKLISTED = new Set([ - '0xba4cff6ed6f7cb2a58776deca4e984b498446762', -]); +/** + * Registry entries that are on-chain but that our review flags as unverified + * (suspected test/placeholder deployments). Instead of hiding them — + * which would silently drop official registry coverage — we surface them + * with an `unverified` flag + rationale so consumers can display a warning. + * + * Keep in sync with `BLOCKLISTED_WRAPPERS` in `src/lib/registry.ts`. + */ +const UNVERIFIED_WRAPPERS: Record = { + '0xba4cff6ed6f7cb2a58776deca4e984b498446762': + 'Suspected test/placeholder entry (cbbqTGBP). Underlying uses a vanity address (0xbeeff…) and the asset name has no known referent. See docs.zama.org mainnet addresses page.', +}; interface PairResult { tokenAddress: string; @@ -73,6 +93,9 @@ interface PairResult { name: string; decimals: number; wrapperDecimals: number; + isValid: boolean; + unverified?: boolean; + unverifiedReason?: string; } async function readTokenMeta( @@ -121,54 +144,56 @@ export async function GET(request: NextRequest) { try { // 1. Get pair count - const totalBig = await client.readContract({ + const totalBig = (await client.readContract({ address: registryAddress, abi: REGISTRY_ABI, functionName: 'getTokenConfidentialTokenPairsLength', - }) as bigint; + })) as bigint; const total = Number(totalBig); if (total === 0) { return NextResponse.json( - { pairs: [], total: 0, chain: chainParam, registryAddress, timestamp: Date.now() }, + { pairs: [], total: 0, chain: chainParam, registryAddress, timestamp: Date.now(), source: 'on-chain' }, { headers: cacheHeaders() }, ); } - // 2. Fetch all pairs in one call - const [tokens, confidentialTokens] = await client.readContract({ + // 2. Fetch all pairs in one call (fromIndex inclusive, toIndex exclusive) + const slice = (await client.readContract({ address: registryAddress, abi: REGISTRY_ABI, - functionName: 'listPairs', - args: [0n, BigInt(total)], - }) as [readonly `0x${string}`[], readonly `0x${string}`[]]; + functionName: 'getTokenConfidentialTokenPairsSlice', + args: [0n, totalBig], + })) as readonly { + tokenAddress: `0x${string}`; + confidentialTokenAddress: `0x${string}`; + isValid: boolean; + }[]; // 3. Enrich with ERC-20 metadata (parallel) - const pairs: PairResult[] = []; - const metaPromises = tokens.map(async (tokenAddr, i) => { - const wrapper = confidentialTokens[i]; - if (BLOCKLISTED.has(wrapper.toLowerCase())) return null; - + const metaPromises = slice.map(async (pair): Promise => { const [underlyingMeta, wrapperMeta] = await Promise.all([ - readTokenMeta(client, tokenAddr), - readTokenMeta(client, wrapper), + readTokenMeta(client, pair.tokenAddress), + readTokenMeta(client, pair.confidentialTokenAddress), ]); + const wrapperKey = pair.confidentialTokenAddress.toLowerCase(); + const unverifiedReason = UNVERIFIED_WRAPPERS[wrapperKey]; + return { - tokenAddress: tokenAddr, - confidentialTokenAddress: wrapper, + tokenAddress: pair.tokenAddress, + confidentialTokenAddress: pair.confidentialTokenAddress, symbol: underlyingMeta.symbol, confidentialSymbol: `c${underlyingMeta.symbol}`, name: underlyingMeta.name, decimals: underlyingMeta.decimals, wrapperDecimals: wrapperMeta.decimals, - } satisfies PairResult; + isValid: pair.isValid, + ...(unverifiedReason ? { unverified: true, unverifiedReason } : {}), + }; }); - const results = await Promise.all(metaPromises); - for (const r of results) { - if (r) pairs.push(r); - } + const pairs = await Promise.all(metaPromises); return NextResponse.json( { @@ -182,11 +207,12 @@ export async function GET(request: NextRequest) { { headers: cacheHeaders() }, ); } catch (err) { - // Fallback to hardcoded snapshot + // Fallback to hardcoded snapshot only when the RPC read genuinely fails. console.error('Registry on-chain read failed, falling back to snapshot:', err); - const fallback = (KNOWN_WRAPPERS[chain.id as keyof typeof KNOWN_WRAPPERS] ?? []) - .filter((p) => !BLOCKLISTED.has(p.erc7984Address.toLowerCase())) - .map((p) => ({ + const fallback = (KNOWN_WRAPPERS[chain.id as keyof typeof KNOWN_WRAPPERS] ?? []).map((p): PairResult => { + const wrapperKey = p.erc7984Address.toLowerCase(); + const unverifiedReason = UNVERIFIED_WRAPPERS[wrapperKey]; + return { tokenAddress: p.erc20Address, confidentialTokenAddress: p.erc7984Address, symbol: p.symbol, @@ -194,7 +220,10 @@ export async function GET(request: NextRequest) { name: p.name, decimals: p.decimals, wrapperDecimals: p.wrapperDecimals, - })); + isValid: p.isValid ?? true, + ...(unverifiedReason ? { unverified: true, unverifiedReason } : {}), + }; + }); return NextResponse.json( { diff --git a/src/app/app/analytics/page.tsx b/src/app/app/analytics/page.tsx index de8a8b3..628f4e5 100644 --- a/src/app/app/analytics/page.tsx +++ b/src/app/app/analytics/page.tsx @@ -321,13 +321,15 @@ function RatioBar({ shields, unshields }: { shields: number; unshields: number } const BLOCK_TIME_MS = 12_000; // ~12 seconds per block -function getBlocksSinceMidnightUTC(): bigint { +function getBlocksSinceUtcMidnight(): bigint { const now = Date.now(); const midnightUTC = new Date(); midnightUTC.setUTCHours(0, 0, 0, 0); const msSinceMidnight = now - midnightUTC.getTime(); - const blocks = Math.ceil(msSinceMidnight / BLOCK_TIME_MS); - return BigInt(Math.max(blocks, 1)); + const blocksSinceMidnight = Math.ceil(msSinceMidnight / BLOCK_TIME_MS); + // Enforce a minimum of 7200 blocks (~24h) so the page always has meaningful + // data even at 00:01 UTC when only a handful of blocks have elapsed. + return BigInt(Math.max(blocksSinceMidnight, 7200)); } export default function AnalyticsPage() { @@ -352,7 +354,7 @@ export default function AnalyticsPage() { try { const latestBlock = await client.getBlockNumber(); const fetchTimestamp = Date.now(); - const blockLookback = getBlocksSinceMidnightUTC(); + const blockLookback = getBlocksSinceUtcMidnight(); const fromBlock = latestBlock > blockLookback ? latestBlock - blockLookback : 0n; diff --git a/src/app/app/developers/page.tsx b/src/app/app/developers/page.tsx deleted file mode 100644 index c1328e4..0000000 --- a/src/app/app/developers/page.tsx +++ /dev/null @@ -1,780 +0,0 @@ -'use client'; - -import React, { useState, useMemo } from 'react'; - -const APP_URL = process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, '') ?? 'https://YOUR_DEPLOYMENT_URL'; -import Card from '@/components/ui/Card'; -import Badge from '@/components/ui/Badge'; -import Button from '@/components/ui/Button'; -import CopyButton from '@/components/ui/CopyButton'; -import BlurIn from '@/components/ui/BlurIn'; -import { useActiveNetwork } from '@/app/ClientLayout'; -import { useRegistryPairs } from '@/lib/registry'; -import { type WrapperPair } from '@/config/contracts'; -import { - Code2, - Shield, - Unlock, - Eye, - List, - ExternalLink, - Terminal, - BookOpen, -} from 'lucide-react'; - -/* ─── Operation types ───────────────────────────────────────────────────────── */ - -type Operation = 'list' | 'shield' | 'unshield' | 'decrypt'; -type Framework = 'react' | 'viem' | 'ethers'; - -interface OperationMeta { - id: Operation; - label: string; - description: string; - icon: React.ReactNode; - color: string; -} - -const OPERATIONS: OperationMeta[] = [ - { - id: 'list', - label: 'List Pairs', - description: 'Query all registered ERC-20 ↔ ERC-7984 wrapper pairs from the on-chain registry', - icon: , - color: 'var(--accent)', - }, - { - id: 'shield', - label: 'Shield (Wrap)', - description: 'Convert public ERC-20 tokens into confidential ERC-7984 tokens', - icon: , - color: 'var(--success)', - }, - { - id: 'unshield', - label: 'Unshield (Unwrap)', - description: 'Convert confidential ERC-7984 tokens back to public ERC-20', - icon: , - color: 'var(--warning)', - }, - { - id: 'decrypt', - label: 'Decrypt Balance', - description: 'Read an encrypted FHE balance by signing an EIP-712 permit', - icon: , - color: '#a78bfa', - }, -]; - -const FRAMEWORKS: { id: Framework; label: string; badge: string }[] = [ - { id: 'react', label: 'React (Zama SDK)', badge: '@zama-fhe/react-sdk' }, - { id: 'viem', label: 'Viem (Raw)', badge: 'viem' }, - { id: 'ethers', label: 'Ethers.js', badge: 'ethers v6' }, -]; - -/* ─── Snippet generators ────────────────────────────────────────────────────── */ - -function generateSnippet( - operation: Operation, - framework: Framework, - pair: WrapperPair | null, - chainName: string, -): string { - const sym = pair?.symbol ?? 'USDC'; - const erc20 = pair?.erc20Address ?? '0x...underlying'; - const erc7984 = pair?.erc7984Address ?? '0x...wrapper'; - const registryAddr = chainName === 'sepolia' - ? '0x2f0750Bbb0A246059d80e94c454586a7F27a128e' - : '0xeb5015fF021DB115aCe010f23F55C2591059bBA0'; - - switch (operation) { - case 'list': - return listSnippet(framework, registryAddr, chainName); - case 'shield': - return shieldSnippet(framework, sym, erc20, erc7984); - case 'unshield': - return unshieldSnippet(framework, sym, erc7984); - case 'decrypt': - return decryptSnippet(framework, sym, erc7984); - } -} - -function listSnippet(fw: Framework, registry: string, chain: string): string { - if (fw === 'react') { - return `import { useListPairs } from '@zama-fhe/react-sdk'; - -function RegistryList() { - const { data, isLoading, error } = useListPairs({ - page: 1, - pageSize: 50, - metadata: true, - }); - - if (isLoading) return

Loading pairs...

; - if (error) return

Error: {error.message}

; - - return ( -
    - {data?.pairs.map((pair) => ( -
  • - {pair.metadata?.symbol} — {pair.token} ↔ {pair.confidentialToken} -
  • - ))} -
- ); -}`; - } - - if (fw === 'viem') { - return `import { createPublicClient, http } from 'viem'; -import { ${chain} } from 'viem/chains'; - -const REGISTRY = '${registry}'; - -const client = createPublicClient({ - chain: ${chain}, - transport: http(), -}); - -// 1. Get total pair count -const total = await client.readContract({ - address: REGISTRY, - abi: [{ - name: 'getTokenConfidentialTokenPairsLength', - type: 'function', - stateMutability: 'view', - inputs: [], - outputs: [{ name: '', type: 'uint256' }], - }], - functionName: 'getTokenConfidentialTokenPairsLength', -}); - -// 2. Fetch all pairs -const [tokens, wrappers] = await client.readContract({ - address: REGISTRY, - abi: [{ - name: 'listPairs', - type: 'function', - stateMutability: 'view', - inputs: [ - { name: 'start', type: 'uint256' }, - { name: 'count', type: 'uint256' }, - ], - outputs: [ - { name: 'tokens', type: 'address[]' }, - { name: 'confidentialTokens', type: 'address[]' }, - ], - }], - functionName: 'listPairs', - args: [0n, total], -}); - -console.log('Pairs:', tokens.map((t, i) => ({ - underlying: t, - wrapper: wrappers[i], -})));`; - } - - // ethers - return `import { ethers } from 'ethers'; - -const REGISTRY = '${registry}'; -const RPC = 'https://ethereum-${chain === 'mainnet' ? '' : 'sepolia-'}rpc.publicnode.com'; - -const provider = new ethers.JsonRpcProvider(RPC); - -const registry = new ethers.Contract(REGISTRY, [ - 'function getTokenConfidentialTokenPairsLength() view returns (uint256)', - 'function listPairs(uint256 start, uint256 count) view returns (address[], address[])', -], provider); - -// 1. Get total count -const total = await registry.getTokenConfidentialTokenPairsLength(); - -// 2. Fetch all pairs -const [tokens, wrappers] = await registry.listPairs(0, total); - -tokens.forEach((token, i) => { - console.log(\`Pair \${i}: \${token} ↔ \${wrappers[i]}\`); -});`; -} - -function shieldSnippet(fw: Framework, sym: string, erc20: string, erc7984: string): string { - if (fw === 'react') { - return `import { useShield } from '@zama-fhe/react-sdk'; -import { useAccount } from 'wagmi'; -import { parseUnits } from 'viem'; - -function Shield${sym}() { - const { address } = useAccount(); - const { mutateAsync: shield, isPending } = useShield({ - tokenAddress: '${erc7984}', // ERC-7984 wrapper - }); - - const handleShield = async () => { - try { - // Amount in underlying token decimals (e.g. 6 for USDC, 18 for WETH) - const amount = parseUnits('100', 6); // 100 ${sym} - - const tx = await shield({ amount }); - console.log('Shield tx:', tx); - } catch (err) { - console.error('Shield failed:', err); - } - }; - - return ( - - ); -}`; - } - - if (fw === 'viem') { - return `import { createWalletClient, createPublicClient, http, parseUnits } from 'viem'; -import { sepolia } from 'viem/chains'; - -const ERC20 = '${erc20}'; -const WRAPPER = '${erc7984}'; - -// 1. Approve the wrapper to spend your ERC-20 -const approveHash = await walletClient.writeContract({ - address: ERC20, - abi: [{ - name: 'approve', - type: 'function', - stateMutability: 'nonpayable', - inputs: [ - { name: 'spender', type: 'address' }, - { name: 'amount', type: 'uint256' }, - ], - outputs: [{ name: '', type: 'bool' }], - }], - functionName: 'approve', - args: [WRAPPER, parseUnits('100', 6)], // underlying decimals -}); - -await publicClient.waitForTransactionReceipt({ hash: approveHash }); - -// 2. Call wrap() on the ERC-7984 wrapper -const wrapHash = await walletClient.writeContract({ - address: WRAPPER, - abi: [{ - name: 'wrap', - type: 'function', - stateMutability: 'nonpayable', - inputs: [{ name: 'amount', type: 'uint256' }], - outputs: [], - }], - functionName: 'wrap', - args: [parseUnits('100', 6)], -}); - -console.log('Wrap tx:', wrapHash);`; - } - - // ethers - return `import { ethers } from 'ethers'; - -const ERC20 = '${erc20}'; -const WRAPPER = '${erc7984}'; - -const provider = new ethers.BrowserProvider(window.ethereum); -const signer = await provider.getSigner(); - -// 1. Approve -const erc20Contract = new ethers.Contract(ERC20, [ - 'function approve(address spender, uint256 amount) returns (bool)', -], signer); - -const amount = ethers.parseUnits('100', 6); // underlying decimals -const approveTx = await erc20Contract.approve(WRAPPER, amount); -await approveTx.wait(); - -// 2. Wrap (shield) -const wrapperContract = new ethers.Contract(WRAPPER, [ - 'function wrap(uint256 amount)', -], signer); - -const wrapTx = await wrapperContract.wrap(amount); -console.log('Wrap tx:', wrapTx.hash);`; -} - -function unshieldSnippet(fw: Framework, sym: string, erc7984: string): string { - if (fw === 'react') { - return `import { useUnshield } from '@zama-fhe/react-sdk'; -import { parseUnits } from 'viem'; - -function Unshield${sym}() { - const { mutateAsync: unshield, isPending } = useUnshield({ - tokenAddress: '${erc7984}', // ERC-7984 wrapper - }); - - const handleUnshield = async () => { - try { - // Amount in WRAPPER decimals (always 6 for FHE tokens) - const amount = parseUnits('50', 6); // 50 c${sym} - - const tx = await unshield({ amount }); - console.log('Unshield tx:', tx); - // Note: unshield is a 2-phase process. - // The SDK handles finalization automatically, - // but if the user closes the tab, use useResumeUnshield. - } catch (err) { - console.error('Unshield failed:', err); - } - }; - - return ( - - ); -}`; - } - - if (fw === 'viem') { - return `import { parseUnits } from 'viem'; - -const WRAPPER = '${erc7984}'; - -// Note: Unshielding with raw viem requires two steps: -// 1. Call unwrap() to initiate -// 2. Wait for the Zama Gateway to process, then call finalizeUnwrap() -// The Zama React SDK handles step 2 automatically. - -// Step 1: Initiate unwrap -const unwrapHash = await walletClient.writeContract({ - address: WRAPPER, - abi: [{ - name: 'unwrap', - type: 'function', - stateMutability: 'nonpayable', - inputs: [{ name: 'amount', type: 'uint256' }], - outputs: [], - }], - functionName: 'unwrap', - args: [parseUnits('50', 6)], // wrapper decimals (always 6) -}); - -console.log('Unwrap initiated:', unwrapHash); - -// Step 2: Finalization happens via the Zama Gateway. -// For production apps, use the SDK's useResumeUnshield -// to handle this automatically.`; - } - - // ethers - return `import { ethers } from 'ethers'; - -const WRAPPER = '${erc7984}'; - -const provider = new ethers.BrowserProvider(window.ethereum); -const signer = await provider.getSigner(); - -const wrapper = new ethers.Contract(WRAPPER, [ - 'function unwrap(uint256 amount)', -], signer); - -// Step 1: Initiate unwrap (wrapper decimals = 6) -const amount = ethers.parseUnits('50', 6); -const tx = await wrapper.unwrap(amount); -console.log('Unwrap initiated:', tx.hash); - -// Step 2: Finalization is handled by the Zama Gateway. -// For production usage, use the Zama React SDK's -// useResumeUnshield hook to handle interrupted flows.`; -} - -function decryptSnippet(fw: Framework, sym: string, erc7984: string): string { - if (fw === 'react') { - return `import { useConfidentialBalance } from '@zama-fhe/react-sdk'; -import { useAccount } from 'wagmi'; -import { useState } from 'react'; -import { formatUnits } from 'viem'; - -function DecryptBalance() { - const { address } = useAccount(); - const [decryptRequested, setDecryptRequested] = useState(false); - - const { data: balance, isLoading, error } = useConfidentialBalance({ - tokenAddress: '${erc7984}', - // IMPORTANT: Only enable when user explicitly clicks "Decrypt" - // Never auto-fire EIP-712 permits! - enabled: decryptRequested && !!address, - }); - - if (!decryptRequested) { - return ( - - ); - } - - if (isLoading) return

Awaiting wallet signature...

; - if (error) return

Error: {error.message}

; - - // Wrapper decimals are always 6 - const formatted = balance - ? formatUnits(balance, 6) - : '0'; - - return

c${sym} balance: {formatted}

; -}`; - } - - if (fw === 'viem') { - return `// Decryption requires the Zama SDK's EIP-712 permit flow. -// Raw viem cannot decrypt FHE ciphertexts directly — you need -// the SDK to generate the permit signature and communicate -// with the Zama Gateway. -// -// For non-React apps, use the Zama Core SDK: - -import { ZamaSDK } from '@zama-fhe/sdk'; - -const WRAPPER = '${erc7984}'; - -// Initialize the SDK (see Zama docs for full setup) -const sdk = new ZamaSDK({ /* config */ }); - -// The SDK handles: -// 1. Generating an EIP-712 typed data signature -// 2. Sending it to the Zama Gateway -// 3. Receiving the decrypted plaintext value -// -// See: https://docs.zama.org/protocol/sdk/guides/authentication`; - } - - // ethers - return `// Decryption requires the Zama SDK's EIP-712 permit flow. -// The Zama Gateway performs the decryption off-chain using -// a session key derived from your wallet signature. -// -// For ethers.js-based apps, use the Zama Core SDK alongside -// your ethers provider: - -import { ZamaSDK } from '@zama-fhe/sdk'; - -const WRAPPER = '${erc7984}'; - -// The recommended approach for production apps is to use -// the Zama React SDK (useConfidentialBalance hook) which -// handles the full permit → decrypt → display flow. -// -// For custom integrations, refer to: -// https://docs.zama.org/protocol/sdk/guides/authentication -// -// Key principle: NEVER auto-fire EIP-712 permits. -// Always require an explicit user action (button click) -// before requesting a wallet signature.`; -} - -/* ─── REST API snippet (special — framework-independent) ── */ - -function restApiSnippet(chain: string): string { - return `// ZamaVault Public REST API — no SDK required! -// Returns all registered wrapper pairs with metadata. - -const response = await fetch( - '${APP_URL}/api/registry?chain=${chain}' -); - -const data = await response.json(); - -// Response shape: -// { -// pairs: [{ -// tokenAddress: "0x...", -// confidentialTokenAddress: "0x...", -// symbol: "USDC", -// confidentialSymbol: "cUSDC", -// name: "USD Coin", -// decimals: 6, -// wrapperDecimals: 6 -// }, ...], -// total: 8, -// chain: "${chain}", -// source: "on-chain", -// timestamp: 1719100000000 -// } - -console.log(\`Found \${data.total} pairs on \${data.chain}\`); -data.pairs.forEach(p => { - console.log(\` \${p.symbol} → \${p.confidentialSymbol}\`); -});`; -} - -/* ─── Doc links per operation ───────────────────────────────────────────────── */ - -const DOC_LINKS: Record = { - list: { - label: 'WrappersRegistry API Reference', - url: 'https://docs.zama.org/protocol/sdk/api-references/sdk/wrappersregistry', - }, - shield: { - label: 'useShield Hook Reference', - url: 'https://docs.zama.org/protocol/sdk/overview', - }, - unshield: { - label: 'useResumeUnshield Reference', - url: 'https://docs.zama.org/protocol/sdk/api-references/react/useresumeunshield', - }, - decrypt: { - label: 'Authentication & Permits Guide', - url: 'https://docs.zama.org/protocol/sdk/guides/authentication', - }, -}; - -/* ─── Main page ─────────────────────────────────────────────────────────────── */ - -export default function DevelopersPage() { - const { activeChainId, isTestnet } = useActiveNetwork(); - const { pairs, isLoading } = useRegistryPairs(activeChainId); - - const [selectedOp, setSelectedOp] = useState('list'); - const [selectedFw, setSelectedFw] = useState('react'); - const [selectedPairIdx, setSelectedPairIdx] = useState(0); - const [showRestApi, setShowRestApi] = useState(false); - - const chainName = isTestnet ? 'sepolia' : 'mainnet'; - const selectedPair: WrapperPair | null = pairs[selectedPairIdx] ?? null; - - const snippet = useMemo(() => { - if (showRestApi) return restApiSnippet(chainName); - return generateSnippet(selectedOp, selectedFw, selectedPair, chainName); - }, [selectedOp, selectedFw, selectedPair, chainName, showRestApi]); - - const currentOpMeta = OPERATIONS.find((o) => o.id === selectedOp)!; - const docLink = DOC_LINKS[selectedOp]; - - return ( -
- {/* ── Header ── */} -
- - Developer Tools - -

- -

-

- Select a token, pick an operation, choose your framework — get - copy-paste ready code for Zama's confidential token ecosystem. -

-
- -
- {/* ── Left panel: Controls ── */} -
- {/* Operation selector */} - -

- - Operation -

-
- {OPERATIONS.map((op) => ( - - ))} -
- - {/* REST API toggle */} - -
- - {/* Token selector */} - {!showRestApi && selectedOp !== 'list' && ( - -

- Token -

- {isLoading ? ( -
Loading pairs...
- ) : ( - - )} - {selectedPair && ( -
-
ERC-20: {selectedPair.erc20Address.slice(0, 10)}...{selectedPair.erc20Address.slice(-6)}
-
ERC-7984: {selectedPair.erc7984Address.slice(0, 10)}...{selectedPair.erc7984Address.slice(-6)}
-
- )} -
- )} - - {/* Framework selector */} - {!showRestApi && ( - -

- Framework -

-
- {FRAMEWORKS.map((fw) => ( - - ))} -
-
- )} - - {/* Docs link */} - {!showRestApi && ( - - -
- - {docLink.label} - -
-
-
- )} -
- - {/* ── Right panel: Code output ── */} -
- - {/* Code header */} -
-
- {showRestApi ? ( - <> - REST API - fetch() — No SDK required - - ) : ( - <> - - {currentOpMeta.icon} - - {currentOpMeta.label} - - - - {FRAMEWORKS.find((f) => f.id === selectedFw)?.badge} - - {selectedOp !== 'list' && selectedPair && ( - {selectedPair.symbol} - )} - - )} -
- -
- - {/* Code block */} -
-
-                {snippet}
-              
-
-
- - {/* Usage notes */} - -
- Note -

- {showRestApi ? ( - <> - The REST API endpoint reads directly from the on-chain registry - and caches results for 60 seconds. No authentication or SDK - installation required — use it from any language or platform. - - ) : selectedOp === 'decrypt' && selectedFw !== 'react' ? ( - <> - Balance decryption requires the Zama SDK's EIP-712 permit - flow. Raw contract calls alone cannot decrypt FHE ciphertexts. - For the best developer experience, use the React SDK hooks. - - ) : selectedOp === 'unshield' ? ( - <> - Unshielding is a two-phase process: the on-chain unwrap request - is followed by Zama Gateway finalization (~30-60s). If the user - closes their browser during this window, use{' '} - useResumeUnshield to complete the operation later. - - ) : selectedOp === 'shield' ? ( - <> - The wrapper always uses 6 decimals (FHE euint64 constraint). - When shielding, parse the amount using the underlying{' '} - token's decimals. The wrapper contract handles the scaling - automatically. - - ) : ( - <> - The on-chain WrappersRegistry is the canonical source for all - registered token pairs. Use listPairs(start, count){' '} - to paginate through entries. Each pair maps an ERC-20 underlying - to its ERC-7984 confidential wrapper. - - )} -

-
-
-
-
-
- ); -} diff --git a/src/app/app/docs/[slug]/page.tsx b/src/app/app/docs/[slug]/page.tsx new file mode 100644 index 0000000..11e5424 --- /dev/null +++ b/src/app/app/docs/[slug]/page.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import DocRenderer from '../_docs/DocRenderer'; +import { SUBPAGE_SLUGS } from '../_docs/nav'; + +/** Pre-render every known subpage; reject anything else with a 404. */ +export const dynamicParams = false; + +export function generateStaticParams() { + return SUBPAGE_SLUGS.map((slug) => ({ slug })); +} + +export default async function DocSlugPage({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + return ; +} diff --git a/src/app/app/docs/_docs/DocRenderer.tsx b/src/app/app/docs/_docs/DocRenderer.tsx new file mode 100644 index 0000000..f86dce2 --- /dev/null +++ b/src/app/app/docs/_docs/DocRenderer.tsx @@ -0,0 +1,22 @@ +'use client'; + +/** + * Client-side resolver for a docs subpage. The `[slug]` route is a server + * component (so it can own generateStaticParams + dynamicParams), but the + * slug→component map lives in a 'use client' module and must be read on the + * client — hence this thin wrapper. + */ + +import React from 'react'; +import { DocPage } from './components'; +import { DOC_CONTENT } from './content'; + +export default function DocRenderer({ slug }: { slug: string }) { + const Body = DOC_CONTENT[slug]; + if (!Body) return null; // unknown slugs are already 404'd by dynamicParams=false + return ( + + + + ); +} diff --git a/src/app/app/docs/_docs/Sidebar.tsx b/src/app/app/docs/_docs/Sidebar.tsx new file mode 100644 index 0000000..95b9183 --- /dev/null +++ b/src/app/app/docs/_docs/Sidebar.tsx @@ -0,0 +1,81 @@ +'use client'; + +/** + * Docs sidebar — grouped, route-aware navigation. Lives in the docs layout so it + * persists across page transitions (only the content re-animates). Collapses into + * a slide-over drawer on mobile. + */ + +import React, { useState } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { BookOpen, ExternalLink, Menu, X } from 'lucide-react'; +import { DOC_ENTRIES, DOC_GROUPS, hrefForSlug } from './nav'; + +export default function Sidebar() { + const pathname = usePathname(); + const [open, setOpen] = useState(false); + + return ( + <> + + + + + {open &&
setOpen(false)} />} + + ); +} diff --git a/src/app/app/docs/_docs/components.tsx b/src/app/app/docs/_docs/components.tsx new file mode 100644 index 0000000..9274cbd --- /dev/null +++ b/src/app/app/docs/_docs/components.tsx @@ -0,0 +1,577 @@ +'use client'; + +/** + * Shared building blocks for every docs subpage: page shell, prose helpers, + * tables, code blocks, the prev/next pager, motion reveals, and a small set of + * hand-built SVG diagrams. All styling comes from the existing `.docs-*` design + * tokens in globals.css — no new palette. + */ + +import React from 'react'; +import Link from 'next/link'; +import { motion } from 'framer-motion'; +import Badge from '@/components/ui/Badge'; +import CopyButton from '@/components/ui/CopyButton'; +import { ArrowLeft, ArrowRight, ExternalLink } from 'lucide-react'; +import { getEntry, getNeighbours, hrefForSlug } from './nav'; + +/* ─── Motion reveal ──────────────────────────────────────────────────────── */ + +/** Fades + lifts its children in on mount. Respects the template's page-level + * entrance by staggering slightly after it. */ +export function Reveal({ + children, + delay = 0, + className, +}: { + children: React.ReactNode; + delay?: number; + className?: string; +}) { + return ( + + {children} + + ); +} + +/* ─── Page shell (header + body + pager) ─────────────────────────────────── */ + +export function DocPage({ slug, children }: { slug: string; children: React.ReactNode }) { + const entry = getEntry(slug); + return ( +
+ {entry && ( + + {entry.eyebrow} +

{entry.label}

+

{entry.description}

+
+ )} + {children} + +
+ ); +} + +/* ─── Prev / Next pager ──────────────────────────────────────────────────── */ + +export function DocsPager({ slug }: { slug: string }) { + const { prev, next } = getNeighbours(slug); + return ( + + ); +} + +/* ─── Prose helpers ──────────────────────────────────────────────────────── */ + +export function Lead({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +export function P({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +export function H2({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +export function H4({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +export function UL({ children }: { children: React.ReactNode }) { + return
    {children}
; +} + +/* ─── Code block ─────────────────────────────────────────────────────────── */ + +export function CodeBlock({ + code, + lang = 'ts', + filename, +}: { + code: string; + lang?: string; + filename?: string; +}) { + return ( +
+
+ {filename ?? lang} + +
+
+        {code}
+      
+
+ ); +} + +/* ─── Callouts & info boxes ──────────────────────────────────────────────── */ + +export function Callout({ + variant = 'info', + children, +}: { + variant?: 'info' | 'warning' | 'error' | 'success'; + children: React.ReactNode; +}) { + const cls = + variant === 'warning' + ? 'docs-callout docs-callout-warning' + : variant === 'error' + ? 'docs-callout docs-callout-error' + : variant === 'success' + ? 'docs-callout docs-callout-success' + : 'docs-info-box'; + return
{children}
; +} + +/* ─── Feature grid ───────────────────────────────────────────────────────── */ + +export function FeatureGrid({ + items, +}: { + items: { icon: string; title: string; desc: string }[]; +}) { + return ( +
+ {items.map((f) => ( +
+
{f.icon}
+
+ {f.title} +

+ {f.desc} +

+
+
+ ))} +
+ ); +} + +/* ─── Numbered steps ─────────────────────────────────────────────────────── */ + +export function StepList({ steps }: { steps: { t: string; d: React.ReactNode }[] }) { + return ( +
+ {steps.map((s, i) => ( +
+
{i + 1}
+
+ {s.t} +

+ {s.d} +

+
+
+ ))} +
+ ); +} + +/* ─── Tables ─────────────────────────────────────────────────────────────── */ + +export function PropTable({ + columns = ['Field', 'Type', 'Description'], + children, +}: { + columns?: string[]; + children: React.ReactNode; +}) { + return ( + + + + {columns.map((c) => ( + + ))} + + + {children} +
{c}
+ ); +} + +export function PropRow({ + name, + type, + required, + description, +}: { + name: string; + type: string; + required?: boolean; + description: string; +}) { + return ( + + + {name} + {required && required} + + + {type} + + {description} + + ); +} + +export function EndpointBadge({ method, path }: { method: string; path: string }) { + return ( +
+ {method} + {path} +
+ ); +} + +export function HookCard({ + name, + pkg, + description, + signature, + example, +}: { + name: string; + pkg: string; + description: string; + signature: string; + example: string; +}) { + return ( +
+
+
+
+ {name} + + {pkg} + +
+

{description}

+
+
+ + +
+ ); +} + +export function ErrorRow({ + code, + title, + description, + retryable, +}: { + code: string; + title: string; + description: string; + retryable: boolean; +}) { + return ( + + + + {code} + + + {title} + {description} + + + {retryable ? 'Retryable' : 'Terminal'} + + + + ); +} + +export function AddressTable({ + network, + registry, + pairs, +}: { + network: string; + registry: string; + pairs: { symbol: string; erc20: string; wrapper: string; decimals: number }[]; +}) { + const explorerBase = + network === 'Sepolia' + ? 'https://eth-sepolia.blockscout.com/address' + : 'https://eth.blockscout.com/address'; + + return ( +
+
+ WrappersRegistry +
+ {registry} + + + +
+
+ + + + + + + + + + + {pairs.map((p) => ( + + + + + + + ))} + +
TokenDecimalsERC-20 AddressERC-7984 Wrapper
+ {p.symbol} + + c{p.symbol} + + {p.decimals} / 6 +
+ + {p.erc20.slice(0, 10)}…{p.erc20.slice(-6)} + + + + +
+
+
+ + {p.wrapper.slice(0, 10)}…{p.wrapper.slice(-6)} + + + + +
+
+
+ ); +} + +/* ─── SVG diagrams ───────────────────────────────────────────────────────── */ +/* Themed via CSS variables so they track light/dark automatically. Each is a + labelled box-and-arrow schematic — no external assets. */ + +function DiagramFrame({ + title, + viewBox, + children, +}: { + title: string; + viewBox: string; + children: React.ReactNode; +}) { + return ( +
+ + + {children} + + +
{title}
+
+ ); +} + +/** Reusable rounded node. */ +function Node({ + x, + y, + w, + h, + label, + sub, + accent, +}: { + x: number; + y: number; + w: number; + h: number; + label: string; + sub?: string; + accent?: boolean; +}) { + return ( + + + + {label} + + {sub && ( + + {sub} + + )} + + ); +} + +const ARROW = 'var(--text-muted)'; + +export function ArchitectureDiagram() { + return ( + + + + + + + + + + + {/* Two backend lanes */} + + + + + + + {/* Arrows */} + + + + + + {/* Gateway settles to fhEVM */} + + settles + + ); +} + +export function ShieldFlowDiagram() { + return ( + + + + + + + + SHIELD (wrap) + + + + + + + UNSHIELD (unwrap) + + + + + + + ); +} + +export function PermitFlowDiagram() { + return ( + + + + + + + + + + + + {[140, 310, 480, 650].map((x, i) => ( + + ))} + + ); +} + +export function FheConceptDiagram() { + return ( + + + Public ERC-20 + balanceOf = 1000 + uint256 · readable by anyone + + + Confidential ERC-7984 + 0x9f3a…e1c7 + euint64 handle · only you can decrypt + + ); +} diff --git a/src/app/app/docs/_docs/content/addresses.tsx b/src/app/app/docs/_docs/content/addresses.tsx new file mode 100644 index 0000000..36b02b9 --- /dev/null +++ b/src/app/app/docs/_docs/content/addresses.tsx @@ -0,0 +1,64 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, AddressTable } from '../components'; +import { useRegistryPairs } from '@/lib/registry'; +import { sepolia, mainnet } from 'viem/chains'; +import { REGISTRY_ADDRESSES } from '@/config/contracts'; + +export default function Addresses() { + const { pairs: sepoliaPairs } = useRegistryPairs(sepolia.id); + const { pairs: mainnetPairs } = useRegistryPairs(mainnet.id); + + const sepoliaFormatted = sepoliaPairs + .filter((p) => p.source !== 'custom' && !p.unverified) + .map((p) => ({ + symbol: p.symbol, + erc20: p.erc20Address, + wrapper: p.erc7984Address, + decimals: p.decimals, + })); + + const mainnetFormatted = mainnetPairs + .filter((p) => p.source !== 'custom' && !p.unverified) + .map((p) => ({ + symbol: p.symbol, + erc20: p.erc20Address, + wrapper: p.erc7984Address, + decimals: p.decimals, + })); + + return ( + <> + + All addresses below are sourced directly from the official on-chain WrappersRegistry and verified + in real-time. This ensures every registered pair is always present and up-to-date. + + +

Sepolia Testnet

+ +

+ The pairs above are mock tokens with a public mint() — grab + free test tokens from the Faucet page. Addresses read live from the on-chain registry. +

+ +

Ethereum Mainnet

+ +

+ These pairs reflect the live on-chain registry on Ethereum Mainnet. You can also query this list + programmatically via the REST API or view live analytics on the{' '} + Registry page. +

+ + ); +} + diff --git a/src/app/app/docs/_docs/content/ai-agents.tsx b/src/app/app/docs/_docs/content/ai-agents.tsx new file mode 100644 index 0000000..088ce64 --- /dev/null +++ b/src/app/app/docs/_docs/content/ai-agents.tsx @@ -0,0 +1,116 @@ +'use client'; + +import React from 'react'; +import { Lead, P, H2, H4, CodeBlock, EndpointBadge, UL, Callout } from '../components'; + +export default function AiAgents() { + return ( + <> + + ShadowLine is built from the ground up for autonomous AI agents, LLMs, and programmatic + wallets. Discover verified asset pairs via standard AI manifests and execute MEV-resistant + confidential DeFi operations. + + +

Why Autonomous AI Agents Need ShadowLine

+

+ AI agents executing on-chain trading strategies, DAO treasury management, or automated payroll + face critical vulnerabilities when holding public ERC-20 tokens: +

+
    +
  • + MEV Exploitation & Sandwiche Attacks: Searchers monitor public mempools and agent balances to front-run automated trades. +
  • +
  • + Strategy Copy-Trading: Observers can copy or counter-trade an AI agent's portfolio rebalancing in real time. +
  • +
  • + Treasury Exposure: DAO and agent operational wallets reveal sensitive cash flow and runway data. +
  • +
+

+ By wrapping public tokens into ERC-7984 confidential wrappers (cTokens) on Zama's + fhEVM, AI agents hold encrypted balances (euint64) and execute confidential transfers + completely hidden from public scrutiny. +

+ +

1. llms.txt — AI Discovery Standard

+

+ ShadowLine adheres to the emerging llmstxt.org specification. LLMs and autonomous coding agents can read our structured summaries directly: +

+
    +
  • + /llms.txt — High-level protocol summary, capabilities, and core concepts. +
  • +
  • + /llms-full.txt — Complete developer reference including smart contract ABIs, Viem code patterns, and decimal scaling rules. +
  • +
+ +

2. OpenAI & Universal AI Plugin Manifests

+

+ ShadowLine hosts standard discovery manifests, allowing AI frameworks (ChatGPT plugins, LangChain, Vercel AI SDK, Eliza) to auto-discover our REST API and query confidential asset pairs without manual schema configuration: +

+
    +
  • + /.well-known/ai-plugin.json — Plugin metadata and authentication spec. +
  • +
  • + /openapi.json — OpenAPI 3.0 specification for the /api/registry endpoint. +
  • +
+ +

3. Agent Tools SDK (`@shadowline/agent-tools`)

+

+ For developers building AI agents with TypeScript, we provide pre-built tool definitions in{' '} + src/lib/agent-tools.ts. These tools can be plugged directly into LangChain or Vercel AI SDK: +

+ + +

4. Headless Execution Harness (`ShadowlineAgentHarness`)

+

+ To enable autonomous AI agents to execute real FHE asset shielding, confidential transfers, and balance decryption in a backend or script environment (without a web browser), use the headless execution harness in src/lib/agent-harness.ts: +

+ p.symbol === 'USDC')?.confidentialTokenAddress || pairs[0].confidentialTokenAddress; + +// Shield (wrap) public USDC into confidential cUSDC +const shieldHash = await agent.shield(usdcWrapper, '10.0'); + +// Decrypt balance via EIP-712 permit & WASM decryption +const balance = await agent.getConfidentialBalance(usdcWrapper); + +// Send confidential cUSDC (client-side WASM encryption + FHE input proof) +const transferHash = await agent.transfer(usdcWrapper, '0xRecipient', '5.0');`} + /> + + + Critical Rule for AI Agents: When shielding (depositing), input amounts MUST use the underlying token's decimals (e.g. 6 for USDC, 18 for WETH). When unshielding (withdrawing) or transferring, amounts MUST always use the wrapper's fixed euint64 6-decimal scale. + + + ); +} diff --git a/src/app/app/docs/_docs/content/architecture.tsx b/src/app/app/docs/_docs/content/architecture.tsx new file mode 100644 index 0000000..c408cdd --- /dev/null +++ b/src/app/app/docs/_docs/content/architecture.tsx @@ -0,0 +1,70 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, UL, Callout, ArchitectureDiagram, Reveal } from '../components'; + +export default function Architecture() { + return ( + <> + + ShadowLine is a non-custodial frontend. There is no backend that holds keys or funds — + everything happens between your browser, your wallet, the public RPC, and Zama's FHE + infrastructure. + + + + + + +

The four moving parts

+

+ Every action in the app resolves to a combination of these four lanes. Reads and writes to + public state go through Wagmi; anything involving an encrypted value goes through the Zama + SDK. +

+
    +
  • + ShadowLine UI — a Next.js app. It renders the registry, forms, and + balances. It never sees your private key and stores no secrets server-side. +
  • +
  • + Wagmi + viem — public RPC for standard EVM reads (allowances, ERC-20 + balances, registry pairs) and for sending transactions your wallet signs. +
  • +
  • + Zama React SDK — client-side FHE: it encrypts inputs before they hit the + chain, requests EIP-712 permits, and asks the Gateway to decrypt values that belong to + you. +
  • +
  • + Relayer / Gateway (KMS) — Zama's off-chain coprocessor. It performs + the heavy FHE work and produces the decryption proofs the fhEVM contracts verify on-chain. +
  • +
+ +

A shield, traced end-to-end

+

+ When you shield 100 USDC: the UI reads your allowance via Wagmi, sends an{' '} + approve() if needed, waits for the receipt, then calls the wrapper's{' '} + wrap(). The wrapper locks the ERC-20 and mints an encrypted euint64{' '} + balance to you. Nothing about the amount is readable on-chain afterward — only a ciphertext + handle exists. +

+ +

A decrypt, traced end-to-end

+

+ When you reveal a balance: the SDK asks your wallet for an off-chain EIP-712 signature (no + gas), derives a session key scoped to your address and that contract, and hands it to the + Gateway. The Gateway decrypts only your ciphertext and returns the plaintext to the + browser session. See EIP-712 Permits for the full sequence. +

+ + + Non-custodial by construction: there is no ShadowLine server in any of these + paths. If this site disappeared tomorrow, your tokens and wrappers would remain fully usable + directly against the on-chain contracts. + + + ); +} diff --git a/src/app/app/docs/_docs/content/decimal-scaling.tsx b/src/app/app/docs/_docs/content/decimal-scaling.tsx new file mode 100644 index 0000000..a2d95bd --- /dev/null +++ b/src/app/app/docs/_docs/content/decimal-scaling.tsx @@ -0,0 +1,85 @@ +'use client'; + +import React from 'react'; +import { Lead, P, H2, CodeBlock, Callout, PropTable } from '../components'; + +export default function DecimalScaling() { + return ( + <> + + This is the most common source of bugs when integrating Zama FHE tokens. Read it carefully — + it is short. + + +

+ + FHE operates on euint64 + {' '} + — a 64-bit unsigned integer with a maximum of ~1.84 × 10¹⁹. A standard 18-decimal ERC-20 + represents 1.0 token as 10¹⁸; multiplied by any meaningful amount that overflows 64 bits + quickly. +

+

+ Therefore all ERC-7984 wrapper tokens use 6 decimals, regardless of the + underlying token's precision. The wrapper scales amounts automatically during shielding + and unshielding. +

+ + + Critical rule: when calling useShield, parse the amount with + the underlying token's decimals. When calling useUnshield, always + use 6 decimals (the wrapper decimals). When displaying a confidential + balance, always format with 6. + + +

Decision table

+ + + Shield (wrap) + + parseUnits(amount, underlyingDecimals) + + + {"parseUnits('1', 18)"} → 10¹⁸ + + + + Unshield (unwrap) + + parseUnits(amount, 6) + + + {"parseUnits('1', 6)"} → 10⁶ + + + + Display balance + + formatUnits(balance, 6) + + + {"formatUnits(1_000_000n, 6)"} → '1.0' + + + + + + + ); +} diff --git a/src/app/app/docs/_docs/content/errors.tsx b/src/app/app/docs/_docs/content/errors.tsx new file mode 100644 index 0000000..50995af --- /dev/null +++ b/src/app/app/docs/_docs/content/errors.tsx @@ -0,0 +1,69 @@ +'use client'; + +import React from 'react'; +import { Lead, CodeBlock, Callout, ErrorRow } from '../components'; + +export default function Errors() { + return ( + <> + + Use matchZamaError from @zama-fhe/sdk to classify SDK errors into + user-friendly messages. ShadowLine re-exports this via the classifyError(err){' '} + utility in src/lib/errors.ts. + + + ({ title: 'Declined', message: 'You cancelled the signature.' }), + INSUFFICIENT_ERC20_BALANCE: () => ({ title: 'Low Balance', message: 'Not enough tokens.' }), + _: (e) => ({ title: 'Error', message: e.message }), + }); + showToast(result); +}`} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Error CodeTitleDescriptionRetry?
+ + + Wallet errors (non-SDK): common rejection strings like{' '} + user rejected, User denied, ACTION_REJECTED, and{' '} + user cancelled are caught by the fallback in classifyError() and + mapped to "Request Cancelled." + + + ); +} diff --git a/src/app/app/docs/_docs/content/faq.tsx b/src/app/app/docs/_docs/content/faq.tsx new file mode 100644 index 0000000..8a1dac1 --- /dev/null +++ b/src/app/app/docs/_docs/content/faq.tsx @@ -0,0 +1,64 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2 } from '../components'; + +function QA({ q, children }: { q: string; children: React.ReactNode }) { + return ( +
+

{q}

+

{children}

+
+ ); +} + +export default function Faq() { + return ( + <> + Short answers to the questions people ask most about confidential tokens. + + + Yes — on-chain it exists only as an encrypted euint64 handle. Validators, + indexers, and explorers see ciphertext, not a number. Only you, after signing a permit, can + decrypt it. What stays public is the interaction graph: that your address touched a wrapper, + and when. + + + + No. Decryption uses an off-chain EIP-712 signature. There is no transaction and no gas — + you're just proving to the Gateway that the ciphertext is yours. + + + + FHE works on 64-bit integers (euint64), which would overflow with 18-decimal + amounts. Wrappers standardize on 6 decimals and scale automatically. See{' '} + Decimal Scaling for the exact shield/unshield rule. + + + + Nothing is lost. The unwrap request is on-chain and the pending tx hash is saved locally. + Next visit, ShadowLine detects it and offers a Resume action to finalize. See{' '} + Shield & Unshield. + + + + Yes. It's non-custodial and open-source — every operation maps to public contract calls. + The REST API and the documented{' '} + SDK hooks let you build your own interface against the same + registry. + + + + Sepolia registry pairs are Zama-deployed mock tokens with a public mint(). Grab + free ones from the Faucet page, then shield them to try the full flow. + + + + Likely a USDT-style token that rejects changing a non-zero allowance directly to another + non-zero value. Zero the allowance first, then approve the real amount. ShadowLine does this + automatically; details are on Shield & Unshield. + + + ); +} diff --git a/src/app/app/docs/_docs/content/fhe.tsx b/src/app/app/docs/_docs/content/fhe.tsx new file mode 100644 index 0000000..008fbe5 --- /dev/null +++ b/src/app/app/docs/_docs/content/fhe.tsx @@ -0,0 +1,58 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, UL, Callout, FheConceptDiagram, Reveal } from '../components'; + +export default function Fhe() { + return ( + <> + + The whole system rests on two ideas: a way to compute on encrypted data (FHE), and a token + standard that stores balances as ciphertext (ERC-7984). + + +

Fully Homomorphic Encryption

+

+ Fully Homomorphic Encryption (FHE) is a cryptographic scheme that allows + arbitrary computation on encrypted data without decrypting it first. Zama's{' '} + fhEVM is a modified Ethereum Virtual Machine that supports FHE operations + natively in Solidity — a contract can add two encrypted balances and get an encrypted sum, + never seeing either plaintext. +

+ +

The ERC-7984 standard

+

+ ERC-7984 is the confidential token standard built on the fhEVM. Instead of + storing balances as a public uint256, a wrapper contract stores them as{' '} + euint64 — an encrypted 64-bit integer. The plaintext is never on-chain; only the + token owner can decrypt it. +

+ + + + + + + Key properties of ERC-7984 tokens: +
    +
  • + Balances are on-chain ciphertexts — unreadable by validators, indexers, or block + explorers. +
  • +
  • Transfer amounts are encrypted — confidential even from recipients until decrypted.
  • +
  • Decryption requires the owner's EIP-712 permit.
  • +
  • The underlying ERC-20 is always 1:1 collateralized inside the wrapper contract.
  • +
+
+ +

What FHE does and does not hide

+

+ FHE hides values — balances and transfer amounts. It does not hide the{' '} + graph: the fact that address A interacted with a given wrapper contract, and + when, is still public, because transactions and their senders are public on Ethereum. See the{' '} + Security Model for the precise trust boundaries. +

+ + ); +} diff --git a/src/app/app/docs/_docs/content/index.tsx b/src/app/app/docs/_docs/content/index.tsx new file mode 100644 index 0000000..173b301 --- /dev/null +++ b/src/app/app/docs/_docs/content/index.tsx @@ -0,0 +1,41 @@ +'use client'; + +import React from 'react'; +import Overview from './overview'; +import QuickStart from './quickstart'; +import Architecture from './architecture'; +import Fhe from './fhe'; +import DecimalScaling from './decimal-scaling'; +import Permits from './permits'; +import Shield from './shield'; +import Transfer from './transfer'; +import Registry from './registry'; +import Portfolio from './portfolio'; +import RestApi from './rest-api'; +import AiAgents from './ai-agents'; +import UseCases from './use-cases'; +import Addresses from './addresses'; +import Errors from './errors'; +import Security from './security'; +import Faq from './faq'; + +/** slug → body component. Keys must match `slug` values in nav.ts. */ +export const DOC_CONTENT: Record = { + overview: Overview, + quickstart: QuickStart, + architecture: Architecture, + fhe: Fhe, + 'decimal-scaling': DecimalScaling, + permits: Permits, + shield: Shield, + transfer: Transfer, + registry: Registry, + portfolio: Portfolio, + 'rest-api': RestApi, + 'ai-agents': AiAgents, + 'use-cases': UseCases, + addresses: Addresses, + errors: Errors, + security: Security, + faq: Faq, +}; diff --git a/src/app/app/docs/_docs/content/overview.tsx b/src/app/app/docs/_docs/content/overview.tsx new file mode 100644 index 0000000..77f45df --- /dev/null +++ b/src/app/app/docs/_docs/content/overview.tsx @@ -0,0 +1,65 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, Callout, FeatureGrid, Reveal } from '../components'; + +export default function Overview() { + return ( + <> + + ShadowLine is the canonical interface and developer toolkit for Zama's confidential + token ecosystem. It lets users and developers discover, wrap, unwrap, transfer, and decrypt + ERC-20 tokens that have been converted into confidential ERC-7984 wrappers using{' '} + Fully Homomorphic Encryption (FHE). + + + + + + +

Who this is for

+

+ Users get a simple UI to privatize on-chain balances: shield a token, send + it confidentially, and reveal balances only to yourself. Developers get a + public REST API, a documented set of React SDK hooks, and verified contract addresses so + they can build on the same registry ShadowLine does. +

+ +

How to read these docs

+

+ Start with the Quick Start if you want to ship + something today, or the Architecture and{' '} + FHE & ERC-7984 pages if you want to understand the + model first. The Guides walk through each product feature; the{' '} + Developers and Reference sections are the lookup material + you'll come back to. Use the Next button at the bottom of any page to read + straight through. +

+ + + ); +} diff --git a/src/app/app/docs/_docs/content/permits.tsx b/src/app/app/docs/_docs/content/permits.tsx new file mode 100644 index 0000000..1138727 --- /dev/null +++ b/src/app/app/docs/_docs/content/permits.tsx @@ -0,0 +1,87 @@ +'use client'; + +import React from 'react'; +import { Lead, P, H2, CodeBlock, Callout, StepList, PermitFlowDiagram, Reveal } from '../components'; + +export default function Permits() { + return ( + <> + + Reading a confidential balance requires an EIP-712 typed-data signature from the token + owner's wallet. This signature authorizes the Zama Gateway to decrypt the ciphertext and + return the plaintext to the frontend session. It is off-chain — no gas, no transaction. + + + + Security rule — never auto-fire permits. Every call to{' '} + useConfidentialBalance or useConfidentialBalances with{' '} + enabled: true immediately requests a wallet signature. Always gate it behind an + explicit decryptRequested boolean that is only set true on a user + click. + + + + + + +

How it works

+ + + + Reset on token change: when the user switches the selected token, reset{' '} + decryptRequested synchronously in the onChange handler — not only + in a useEffect. A one-frame delay in the effect can let the old true{' '} + combine with the new token address and auto-fire a permit. + + + { + setSelectedToken(newToken); + setDecryptRequested(false); // ← same handler, not a useEffect +}; + +const { data: balance } = useConfidentialBalance({ + tokenAddress: selectedToken.erc7984Address, + enabled: decryptRequested && !!address, // ← explicit gate +});`} + /> + +

Rejections are terminal — do not retry

+

+ If the user declines the signature, treat it as done: re-arm the button and wait for another + click. Do not re-fire the query on error, on window focus, or on remount — that produces the + "wallet keeps popping up" loop. ShadowLine disables the query after any decrypt + error and only re-enables it on a fresh click. +

+ + ); +} diff --git a/src/app/app/docs/_docs/content/portfolio.tsx b/src/app/app/docs/_docs/content/portfolio.tsx new file mode 100644 index 0000000..a5b17aa --- /dev/null +++ b/src/app/app/docs/_docs/content/portfolio.tsx @@ -0,0 +1,70 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, UL, CodeBlock, Callout } from '../components'; + +export default function Portfolio() { + return ( + <> + + The Portfolio shows everything you hold across the registry and + lets you reveal every confidential balance with a single signature. + + +

Batch decryption

+

+ Decrypting balances one-by-one would prompt your wallet once per token. Instead, the + portfolio uses useConfidentialBalances to cover many wrappers under a single + EIP-712 permit — one click, one signature, every official balance revealed. +

+ setDecryptRequested(true)}>Decrypt All; + } + return ( +
    + {wrappers.map((w) => ( +
  • {formatUnits(balances?.[w.toLowerCase()] ?? 0n, 6)}
  • + ))} +
+ ); +}`} + /> + +

Three kinds of holdings

+
    +
  • + Official wrappers — verified registry pairs, batch-decrypted together. +
  • +
  • + Custom wrappers — your locally-added pairs that support shield/unshield. +
  • +
  • + Custom decrypt-only — confidential tokens with no ERC-20 underlying; each + card runs its own per-row decrypt. +
  • +
+ + + Session reset: the wallet menu's "Reset Decryption Session" + wipes cached FHE permits app-wide. The next decrypt then prompts for a fresh wallet + signature — useful if you switch accounts or want to re-arm every gate at once. + + + ); +} diff --git a/src/app/app/docs/_docs/content/quickstart.tsx b/src/app/app/docs/_docs/content/quickstart.tsx new file mode 100644 index 0000000..c690dfe --- /dev/null +++ b/src/app/app/docs/_docs/content/quickstart.tsx @@ -0,0 +1,161 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, H2, P, UL, Callout, StepList, CodeBlock } from '../components'; + +export default function QuickStart() { + return ( + <> + + ShadowLine is ready to use — no installation required. Connect a wallet, pick a token pair, + and your first confidential balance is on-chain in under a minute. + + +

1. Connect your wallet

+

+ Open the Registry and click Connect in the top + right. ShadowLine works with any EIP-1193 wallet (MetaMask, Rabby, WalletConnect). Switch + the network toggle to Sepolia to use free test tokens, or{' '} + Mainnet for real assets. +

+ + First time on Sepolia? Head to the Faucet page and mint free + mock tokens — no ETH required, one click per token. + + +

2. Browse the Registry and pick a pair

+

+ The Registry lists every verified ERC-20 ↔ ERC-7984 wrapper pair + registered on-chain. Each card shows the public token on top and its confidential wrapper + below. Use the search bar to find a token by symbol or address. +

+

+ Click Shield on any public-token row to go directly to that pair on the{' '} + Wrapper page. +

+ +

3. Shield — wrap ERC-20 into a confidential token

+

+ On the Wrapper page, enter an amount and click{' '} + Shield. Two wallet prompts follow: +

+ +

+ The Zama Gateway finalizes the ciphertext in a few seconds. Your confidential balance appears + in the Registry after you click Decrypt and sign a + read-only EIP-712 permit. +

+ +

4. Transfer or decrypt

+

+ Once shielded, you have two options: +

+
    +
  • + Confidential Transfer — go to Transfer, + pick the confidential token, enter a recipient and amount. The amount is encrypted + client-side before submission. On-chain observers see the addresses but never the value. +
  • +
  • + Decrypt balance — on the Registry or Portfolio page, click{' '} + Decrypt next to any confidential row. Sign the EIP-712 permit in your + wallet. No tokens move; the balance is decrypted only inside your browser session. +
  • +
+ +

5. Unshield when you are done

+

+ To recover your original ERC-20, go to the Wrapper page, + switch to Unshield, and enter the amount. The Zama Gateway decrypts + on-chain and releases the underlying ERC-20 back to your wallet (typically 30–60 seconds). + If the page closes mid-flow, the Resume banner re-appears automatically on + your next visit. +

+ + + Decimal note: shield amounts use the underlying token's + decimals (e.g. 6 for USDC, 18 for WETH). Unshield always uses the wrapper's fixed{' '} + 6-decimal scale. See Decimal Scaling for the + full rule. + + +

For Developers: Drop-in SDK Hook

+

+ Want to integrate confidential asset shielding into your own dApp without writing boilerplate contract or relayer code? We created a zero-boilerplate drop-in React hook: useShadowline(). +

+

+ Simply copy src/lib/use-shadowline.ts into your React, Next.js, or Wagmi project to get instant access to verified contract pairs, automatic ERC-20 allowances, and one-click shielding/unshielding: +

+ +

Available Confidential Assets ({pairs.length})

+ {pairs.map((pair) => ( +
+ {pair.symbol} ↔ c{pair.symbol} + +
+ ))} +
+ ); +}`} + /> +

0-to-100 Automated Setup & Deployment

+

+ Want to run ShadowLine locally or deploy to a cloud node / VPS in under 1 minute? We built an automated cross-platform wizard that handles dependency checking, environment configuration (.env.local), production build verification, and server launching. +

+

+ Linux Ubuntu & macOS (Bash / Zsh): +

+ +

+ Windows (PowerShell & CMD): +

+ +

+ 1-Line Auto-Installers (with Automatic Prerequisite Installation): +

+ +

+ Docker & VPS Self-Hosting: +

+ + + ); +} diff --git a/src/app/app/docs/_docs/content/registry.tsx b/src/app/app/docs/_docs/content/registry.tsx new file mode 100644 index 0000000..999a200 --- /dev/null +++ b/src/app/app/docs/_docs/content/registry.tsx @@ -0,0 +1,55 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, UL, Callout } from '../components'; + +export default function Registry() { + return ( + <> + + The Registry is the front door: a live view of every ERC-20 ↔ ERC-7984 + pair, read straight from the on-chain WrappersRegistry for the selected network. + + +

Official vs. custom pairs

+

+ ShadowLine keeps two kinds of pairs strictly separated so a token you added locally can never + be mistaken for a verified one. +

+
    +
  • + Official Registry — pairs verified on-chain by the WrappersRegistry + contract. On Sepolia these are Zama-deployed mock tokens (each carries a{' '} + Mock badge and has a public mint() you can use from the Faucet). +
  • +
  • + Custom / dev-only — pairs you add yourself, stored locally in your + browser and scoped per chain. They never mix into the official list. +
  • +
+ +

Network scoping

+

+ The Testnet/Mainnet switch in the header controls which registry is shown. Sepolia pairs and + Mainnet pairs are never displayed together, and only addresses for the active network appear + on each row — no cross-network placeholders. +

+ +

Adding a custom token

+

+ You can register any ERC-7984 token by address. ShadowLine validates it is genuinely + confidential: it tries ERC-165 first, and falls back to a behavioral probe of{' '} + confidentialBalanceOf() for tokens that don't implement ERC-165. A wrapper + (one with an underlying()) gets Shield/Unshield actions; a decrypt-only + confidential token gets its own per-row decrypt. +

+ + + Prefer to build against the registry programmatically? The{' '} + REST API returns the same official pairs as JSON with no + wallet required, and useListPairs gives you the live list inside a React app. + + + ); +} diff --git a/src/app/app/docs/_docs/content/rest-api.tsx b/src/app/app/docs/_docs/content/rest-api.tsx new file mode 100644 index 0000000..1b3b296 --- /dev/null +++ b/src/app/app/docs/_docs/content/rest-api.tsx @@ -0,0 +1,108 @@ +'use client'; + +import React from 'react'; +import { Lead, P, H2, H4, CodeBlock, EndpointBadge, PropTable, PropRow } from '../components'; + +const APP_URL = process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, '') ?? 'https://YOUR_DEPLOYMENT_URL'; + +export default function RestApi() { + return ( + <> + + ShadowLine exposes a public REST API for querying the on-chain registry. No SDK, no wallet, + no authentication — just a fetch(). + + +

GET /api/registry

+ +

+ Returns all registered ERC-20 ↔ ERC-7984 wrapper pairs for the specified chain. Data is read + directly from the on-chain WrappersRegistry and cached for 60 seconds + (stale-while-revalidate 300s). +

+ +

Query parameters

+ + + + +

Response schema

+ + + + + + + + + + +

PairResult object

+ + + + + + + + + + +

Examples

+ + + c{pair['symbol']:8} | decimals: {pair['decimals']}/{pair['wrapperDecimals']}")`} + /> + +

HTTP headers

+ + + + Cache-Control + + public, s-maxage=60, stale-while-revalidate=300 + + + + Access-Control-Allow-Origin + + * (CORS open) + + + + ); +} diff --git a/src/app/app/docs/_docs/content/security.tsx b/src/app/app/docs/_docs/content/security.tsx new file mode 100644 index 0000000..4f2cbcb --- /dev/null +++ b/src/app/app/docs/_docs/content/security.tsx @@ -0,0 +1,87 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, H2, UL, Callout, PropTable } from '../components'; + +export default function Security() { + return ( + <> + + ShadowLine is a non-custodial interface over audited, open-source contracts. Understanding + exactly what is private, what is public, and who you trust is the point of this page. + + +

What stays private

+
    +
  • + Balances — stored on-chain as euint64 ciphertext. Not + readable by validators, indexers, or explorers. +
  • +
  • + Transfer amounts — encrypted before submission; the transaction carries a + ciphertext, not a number. +
  • +
+ +

What is public

+
    +
  • + Addresses and the interaction graph — that your address interacted with a + given wrapper, and when. FHE hides values, not the fact that a transaction happened. +
  • +
  • + The underlying ERC-20 movements at shield/unshield boundaries: the moment + you wrap or unwrap, the public leg (the ERC-20 lock or release) is a normal, visible + transfer. +
  • +
+ +

Trust boundaries

+ + + ShadowLine frontend + Only after you sign a permit, in your session + No — every transfer is signed by your wallet + + + Zama Gateway / Relayer + Decrypts only ciphertext your session key authorizes + No custody of funds + + + Public RPC / validators + No — only ciphertext handles are on-chain + No + + + +

Design guarantees

+
    +
  • + Non-custodial: no ShadowLine server holds keys or funds. Tokens are locked + inside the open-source ERC-7984 wrapper contracts. +
  • +
  • + 1:1 collateralization: every confidential unit is backed by an underlying + ERC-20 held in the wrapper. +
  • +
  • + Explicit decryption: balances are only revealed via an EIP-712 permit you + sign — the app never auto-decrypts. +
  • +
  • + Private keys never leave your wallet: the frontend requests signatures; it + never sees your key. +
  • +
+ + + What ShadowLine does not claim: it is an interface, not a new protocol. + Confidentiality guarantees come from Zama's fhEVM and the ERC-7984 contracts. Always + verify contract addresses (see Contract Addresses) and + never enter seed phrases or private keys into any website. + + + ); +} diff --git a/src/app/app/docs/_docs/content/shield.tsx b/src/app/app/docs/_docs/content/shield.tsx new file mode 100644 index 0000000..0e1b236 --- /dev/null +++ b/src/app/app/docs/_docs/content/shield.tsx @@ -0,0 +1,78 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, UL, CodeBlock, Callout, ShieldFlowDiagram, Reveal } from '../components'; + +export default function Shield() { + return ( + <> + + Shielding wraps a public ERC-20 into its confidential ERC-7984 form; unshielding does the + reverse. In the app both live on the Wrap page. + + + + + + +

Shield (wrap)

+

+ A shield is a two-transaction dance: an ERC-20 approve() so the wrapper can pull + your tokens, then wrap(), which locks the ERC-20 and mints you an encrypted{' '} + euint64 balance. ShadowLine runs the approval, waits for its receipt, refreshes + the allowance, then wraps — passing approvalStrategy: 'skip' so the SDK + doesn't try to approve again. +

+ + + + USDT-style tokens: some ERC-20s (real USDT, and this app's USDTMock + which replicates it) revert an approve() that changes a non-zero allowance + straight to another non-zero value. If an allowance is already outstanding, zero it first + (approve(spender, 0), await the receipt), then approve the real amount. Standard + tokens with a zero allowance are unaffected. + + +

Unshield (unwrap)

+

+ Unshielding is two phases. First an on-chain unwrap() request burns your + ciphertext and registers the intent. Then Zama's Gateway produces a decryption proof and + finalizes the unwrap — typically ~30–60 seconds later — releasing the underlying ERC-20 back + to your address. +

+
    +
  • Amounts for unshield always use 6 decimals (wrapper decimals).
  • +
  • + Because finalization is asynchronous, the pending unwrap tx hash is persisted so the + operation can be resumed if you close the tab. +
  • +
+ +

Resuming an interrupted unshield

+

+ The SDK does not auto-persist the pending unwrap. ShadowLine saves the unwrap tx hash the + moment it's submitted; on the next visit it reads it back with{' '} + loadPendingUnshield and offers a Resume action wired to{' '} + useResumeUnshield. See the SDK Hooks page for the + exact signatures. +

+ + + The activity feed on the Wrap page auto-refreshes a few seconds after a shield or unshield + confirms, so a fresh transaction appears without a manual reload. + + + ); +} diff --git a/src/app/app/docs/_docs/content/transfer.tsx b/src/app/app/docs/_docs/content/transfer.tsx new file mode 100644 index 0000000..09c8398 --- /dev/null +++ b/src/app/app/docs/_docs/content/transfer.tsx @@ -0,0 +1,74 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, P, H2, UL, CodeBlock, Callout } from '../components'; + +export default function Transfer() { + return ( + <> + + A confidential transfer moves ERC-7984 tokens where the amount is encrypted on-chain — hidden + from block explorers and even from the recipient until they decrypt it. In the app this is + the Transfer page. + + +

How it differs from a normal transfer

+
    +
  • + The amount is encrypted client-side before it's submitted, so the transaction carries + a ciphertext, not a number. +
  • +
  • + The sender and recipient addresses are still public — FHE hides the value, not the graph. +
  • +
  • + The recipient must run their own decrypt (an EIP-712 permit) to learn how much they + received. +
  • +
+ +

Using the SDK

+

+ useConfidentialTransfer takes the wrapper address and returns a mutation. The + SDK encrypts the amount, then submits — you get lifecycle callbacks for each phase. +

+ { + await transfer({ + to, + // wrapper decimals are always 6 + amount: parseUnits('25', 6), + onEncryptComplete: () => console.log('amount encrypted, submitting…'), + onTransferSubmitted: (hash) => console.log('submitted:', hash), + }); + }; + + return ; +}`} + /> + + + The Transfer page also supports a standard public ERC-20 transfer mode for the underlying + token, so you can move either the public or the confidential side from one place. + + + + You can only send what you hold confidentially. If your confidential balance is lower than + the amount, the transfer reverts with{' '} + INSUFFICIENT_CONFIDENTIAL_BALANCE — see the{' '} + Error Reference. + + + ); +} diff --git a/src/app/app/docs/_docs/content/use-cases.tsx b/src/app/app/docs/_docs/content/use-cases.tsx new file mode 100644 index 0000000..715336b --- /dev/null +++ b/src/app/app/docs/_docs/content/use-cases.tsx @@ -0,0 +1,114 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Lead, H2, P, UL, Callout, FeatureGrid } from '../components'; + +export default function UseCases() { + return ( + <> + + ERC-7984 confidential tokens make on-chain amounts invisible to everyone except the holder. + Here are the real-world patterns ShadowLine is designed to enable. + + +

Private payroll & compensation

+

+ Companies paying contributors on-chain today expose every salary to public scrutiny — anyone + can track an address and reconstruct the full comp structure. Wrapping payroll tokens into + confidential wrappers keeps amounts encrypted on-chain. The recipient holds the ciphertext; + only they can decrypt the value with an EIP-712 permit. Attestations (hire date, role) can + remain on-chain without leaking the number itself. +

+ +

Sealed-bid auctions

+

+ Traditional on-chain auctions require bids to be public, enabling sniping and last-second + manipulation. With ERC-7984 wrappers, each bid is an encrypted amount submitted to a smart + contract. The contract performs comparisons on ciphertext — no participant learns another + bid until the auctioneer chooses to finalize. ShadowLine's shield flow handles the + ERC-20 → confidential conversion that feeds into such contracts. +

+ +

DAO treasury & budget privacy

+

+ DAOs frequently need to approve grants or operational spending without surfacing exact + numbers to competitors or exploiters before execution. Confidential token flows let a + multi-sig hold and transfer budget allocations as encrypted balances. The DAO's + governance rules stay on-chain; the amounts move privately until finalization. +

+ +

Front-run resistant DeFi

+

+ Any large swap, liquidity provision, or liquidation on a public mempool is visible before + it lands. Wrapping the input amount keeps MEV bots blind to the size of the upcoming + trade. The ciphertext is only decrypted inside the EVM at execution time — by then the + block is already sealed. +

+ +

Private P2P payments

+

+ Sending money between wallets reveals the amount to every block explorer, data aggregator, + and anyone who knows either address. A confidential transfer (see{' '} + Confidential Transfer) submits an encrypted amount — + the recipient must run their own decrypt to learn what they received, and observers see + only that a transaction occurred. +

+ +

Vesting & lockup schedules

+

+ Token vesting contracts that hold large allocations are targets for social engineering and + market manipulation once balances are known. Wrapping vested amounts as confidential tokens + removes the live balance signal. The cliff and linear schedule logic stays on-chain; only + the holder can reveal what has vested so far. +

+ + + All of these patterns share one foundation: ERC-20 tokens are locked inside the ERC-7984 + wrapper (1:1 collateralized) and the encrypted handle is what moves on-chain. ShadowLine is + the interface that makes shielding, unshielding, and transferring those handles easy. + + +

Building on ShadowLine

+ + +
    +
  • + REST API — wallet-free pair discovery +
  • +
  • + Contract Addresses — Sepolia and Mainnet +
  • +
  • + Registry & Discovery — adding custom pairs +
  • +
  • + Security Model — trust boundaries +
  • +
+ + ); +} diff --git a/src/app/app/docs/_docs/nav.ts b/src/app/app/docs/_docs/nav.ts new file mode 100644 index 0000000..0a53272 --- /dev/null +++ b/src/app/app/docs/_docs/nav.ts @@ -0,0 +1,215 @@ +/** + * Single source of truth for the docs navigation. + * + * Every doc "page" is one entry here. The sidebar renders them grouped; the + * prev/next pager walks the FLAT order below. Adding a page = adding one entry + * (plus its content component in `content/` and a case in the `[slug]` route). + * + * Routing: the first item (`overview`) is the index route `/app/docs`. Every + * other item is a real subpage at `/app/docs/`. + */ + +export type DocGroup = + | 'Getting Started' + | 'Core Concepts' + | 'Guides' + | 'Developers' + | 'Reference'; + +export interface DocEntry { + /** URL slug. `overview` maps to the index route `/app/docs`. */ + slug: string; + /** Sidebar + page title. */ + label: string; + /** Sidebar group heading. */ + group: DocGroup; + /** Small eyebrow shown above the page title. */ + eyebrow: string; + /** One-line page summary rendered under the title. */ + description: string; +} + +/** + * FLAT, ordered list — drives both the grouped sidebar and the prev/next pager. + * Order here IS the reading order. + */ +export const DOC_ENTRIES: DocEntry[] = [ + // ── Getting Started ───────────────────────────────────────────── + { + slug: 'overview', + label: 'Overview', + group: 'Getting Started', + eyebrow: 'Introduction', + description: + 'What ShadowLine is, who it is for, and how the confidential-token pieces fit together.', + }, + { + slug: 'quickstart', + label: 'Quick Start', + group: 'Getting Started', + eyebrow: 'Getting Started', + description: + 'Connect a wallet, browse the Registry, shield your first token, and transfer confidentially.', + }, + { + slug: 'architecture', + label: 'Architecture', + group: 'Getting Started', + eyebrow: 'Getting Started', + description: + 'How the browser, wallet, Zama Relayer/Gateway, and the fhEVM contracts talk to each other.', + }, + + // ── Core Concepts ─────────────────────────────────────────────── + { + slug: 'fhe', + label: 'FHE & ERC-7984', + group: 'Core Concepts', + eyebrow: 'Concept', + description: + 'Fully Homomorphic Encryption, the fhEVM, and the confidential-token standard ShadowLine is built on.', + }, + { + slug: 'decimal-scaling', + label: 'Decimal Scaling', + group: 'Core Concepts', + eyebrow: 'Concept', + description: + 'Why every confidential wrapper is 6 decimals, and the exact rule for shield vs. unshield amounts.', + }, + { + slug: 'permits', + label: 'EIP-712 Permits', + group: 'Core Concepts', + eyebrow: 'Concept', + description: + 'How a read-only signature lets only you decrypt your own balance — and how to never fire it by accident.', + }, + + // ── Guides ────────────────────────────────────────────────────── + { + slug: 'shield', + label: 'Shield & Unshield', + group: 'Guides', + eyebrow: 'Guide', + description: + 'The full wrap/unwrap lifecycle: approval, shielding, the two-phase unshield, and interrupted-op resume.', + }, + { + slug: 'transfer', + label: 'Confidential Transfer', + group: 'Guides', + eyebrow: 'Guide', + description: + 'Send confidential tokens where the amount is encrypted on-chain — hidden even from the recipient.', + }, + { + slug: 'registry', + label: 'Registry & Discovery', + group: 'Guides', + eyebrow: 'Guide', + description: + 'How pairs are discovered on-chain, official vs. custom tokens, and adding your own wrapper.', + }, + { + slug: 'portfolio', + label: 'Portfolio & Decryption', + group: 'Guides', + eyebrow: 'Guide', + description: + 'View your holdings and batch-decrypt every confidential balance with a single signature.', + }, + + // ── Developers ────────────────────────────────────────────────── + { + slug: 'ai-agents', + label: 'AI Agents & LLMs', + group: 'Developers', + eyebrow: 'Developers', + description: + 'Discover verified asset pairs via llms.txt and OpenAI manifests, and execute MEV-resistant confidential DeFi operations.', + }, + { + slug: 'rest-api', + label: 'REST API', + group: 'Developers', + eyebrow: 'Developers', + description: + 'A public, wallet-free GET endpoint that returns every registered wrapper pair as JSON.', + }, + { + slug: 'use-cases', + label: 'Use Cases', + group: 'Developers', + eyebrow: 'Developers', + description: + 'Real-world patterns enabled by ERC-7984 confidential tokens: payroll, auctions, DAO treasury, and more.', + }, + + // ── Reference ─────────────────────────────────────────────────── + { + slug: 'addresses', + label: 'Contract Addresses', + group: 'Reference', + eyebrow: 'Reference', + description: + 'Registry and wrapper-pair addresses for Sepolia and Ethereum Mainnet, with explorer links.', + }, + { + slug: 'errors', + label: 'Error Reference', + group: 'Reference', + eyebrow: 'Reference', + description: + 'Every Zama SDK error code, whether it is retryable, and how ShadowLine maps it to a message.', + }, + { + slug: 'security', + label: 'Security Model', + group: 'Reference', + eyebrow: 'Reference', + description: + 'Trust boundaries, what stays private, what is public, and the guarantees ShadowLine does — and does not — make.', + }, + { + slug: 'faq', + label: 'FAQ', + group: 'Reference', + eyebrow: 'Reference', + description: 'Short answers to the questions people ask most about confidential tokens.', + }, +]; + +/** Ordered list of group headings, matching first appearance in DOC_ENTRIES. */ +export const DOC_GROUPS: DocGroup[] = [ + 'Getting Started', + 'Core Concepts', + 'Guides', + 'Developers', + 'Reference', +]; + +/** Route href for a slug. `overview` is the index route. */ +export function hrefForSlug(slug: string): string { + return slug === 'overview' ? '/app/docs' : `/app/docs/${slug}`; +} + +/** Look up an entry by slug. */ +export function getEntry(slug: string): DocEntry | undefined { + return DOC_ENTRIES.find((e) => e.slug === slug); +} + +/** Prev/next neighbours in reading order, for the pager. */ +export function getNeighbours(slug: string): { prev?: DocEntry; next?: DocEntry } { + const i = DOC_ENTRIES.findIndex((e) => e.slug === slug); + if (i === -1) return {}; + return { + prev: i > 0 ? DOC_ENTRIES[i - 1] : undefined, + next: i < DOC_ENTRIES.length - 1 ? DOC_ENTRIES[i + 1] : undefined, + }; +} + +/** Every slug except the index — used to generate the [slug] subpages. */ +export const SUBPAGE_SLUGS: string[] = DOC_ENTRIES.filter((e) => e.slug !== 'overview').map( + (e) => e.slug, +); diff --git a/src/app/app/docs/layout.tsx b/src/app/app/docs/layout.tsx new file mode 100644 index 0000000..d8a93d6 --- /dev/null +++ b/src/app/app/docs/layout.tsx @@ -0,0 +1,19 @@ +'use client'; + +/** + * Persistent docs shell: a sticky grouped sidebar + the scrollable content + * column. `children` is the current subpage (wrapped by template.tsx, which + * handles the per-navigation entrance animation). + */ + +import React from 'react'; +import Sidebar from './_docs/Sidebar'; + +export default function DocsLayout({ children }: { children: React.ReactNode }) { + return ( +
+ +
{children}
+
+ ); +} diff --git a/src/app/app/docs/page.tsx b/src/app/app/docs/page.tsx index b5fd9f5..b569166 100644 --- a/src/app/app/docs/page.tsx +++ b/src/app/app/docs/page.tsx @@ -1,1157 +1,12 @@ -'use client'; +import React from 'react'; +import { DocPage } from './_docs/components'; +import Overview from './_docs/content/overview'; -import React, { useState, useEffect, useRef, useCallback } from 'react'; - -// Base URL for API examples in docs — set NEXT_PUBLIC_APP_URL in your deployment. -const APP_URL = process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, '') ?? 'https://YOUR_DEPLOYMENT_URL'; -import Link from 'next/link'; -import Badge from '@/components/ui/Badge'; -import CopyButton from '@/components/ui/CopyButton'; -import { - BookOpen, - Zap, - Globe, - Code2, - Cpu, - MapPin, - AlertCircle, - ChevronRight, - ExternalLink, - Shield, - Menu, - X, -} from 'lucide-react'; - -/* ─── Sidebar nav structure ──────────────────────────────────────────────────── */ - -const SIDEBAR_SECTIONS = [ - { - group: 'Getting Started', - items: [ - { id: 'overview', label: 'Overview', icon: }, - { id: 'quickstart', label: 'Quick Start', icon: }, - ], - }, - { - group: 'API Reference', - items: [ - { id: 'rest-api', label: 'REST API', icon: }, - { id: 'sdk-hooks', label: 'React SDK Hooks', icon: }, - ], - }, - { - group: 'Concepts', - items: [ - { id: 'concepts', label: 'Core Concepts', icon: }, - { id: 'decimal-scaling', label: 'Decimal Scaling', icon: }, - { id: 'permit-flow', label: 'EIP-712 Permits', icon: }, - ], - }, - { - group: 'Reference', - items: [ - { id: 'addresses', label: 'Contract Addresses', icon: }, - { id: 'errors', label: 'Error Reference', icon: }, - ], - }, -]; - -/* ─── CodeBlock component ────────────────────────────────────────────────────── */ - -function CodeBlock({ - code, - lang = 'ts', - filename, -}: { - code: string; - lang?: string; - filename?: string; -}) { - return ( -
-
- {filename ?? lang} - -
-
{code}
-
- ); -} - -/* ─── Section wrapper ────────────────────────────────────────────────────────── */ - -function Section({ - id, - title, - children, -}: { - id: string; - title: string; - children: React.ReactNode; -}) { - return ( -
-

{title}

- {children} -
- ); -} - -function SubSection({ - id, - title, - children, -}: { - id: string; - title: string; - children: React.ReactNode; -}) { - return ( -
-

{title}

- {children} -
- ); -} - -/* ─── Endpoint badge ─────────────────────────────────────────────────────────── */ - -function EndpointBadge({ method, path }: { method: string; path: string }) { - return ( -
- {method} - {path} -
- ); -} - -/* ─── Property row (for response schemas) ────────────────────────────────────── */ - -function PropRow({ - name, - type, - required, - description, -}: { - name: string; - type: string; - required?: boolean; - description: string; -}) { - return ( - - - {name} - {required && required} - - {type} - {description} - - ); -} - -/* ─── Hook row ───────────────────────────────────────────────────────────────── */ - -function HookCard({ - name, - pkg, - description, - signature, - example, -}: { - name: string; - pkg: string; - description: string; - signature: string; - example: string; -}) { - return ( -
-
-
-
- {name} - {pkg} -
-

{description}

-
-
- - -
- ); -} - -/* ─── Error row ──────────────────────────────────────────────────────────────── */ - -function ErrorRow({ - code, - title, - description, - retryable, -}: { - code: string; - title: string; - description: string; - retryable: boolean; -}) { - return ( - - {code} - {title} - {description} - - - {retryable ? 'Retryable' : 'Terminal'} - - - - ); -} - -/* ─── Address table ──────────────────────────────────────────────────────────── */ - -function AddressTable({ - network, - registry, - pairs, -}: { - network: string; - registry: string; - pairs: { symbol: string; erc20: string; wrapper: string; decimals: number }[]; -}) { - const explorerBase = network === 'Sepolia' - ? 'https://eth-sepolia.blockscout.com/address' - : 'https://eth.blockscout.com/address'; - - return ( -
-
- WrappersRegistry -
- {registry} - - - -
-
- - - - - - - - - - - {pairs.map((p) => ( - - - - - - - ))} - -
TokenDecimalsERC-20 AddressERC-7984 Wrapper
- {p.symbol} - - c{p.symbol} - - {p.decimals} / 6 -
- {p.erc20.slice(0, 10)}…{p.erc20.slice(-6)} - - - -
-
-
- {p.wrapper.slice(0, 10)}…{p.wrapper.slice(-6)} - - - -
-
-
- ); -} - -/* ─── Main page ──────────────────────────────────────────────────────────────── */ - -export default function DocsPage() { - const [activeSection, setActiveSection] = useState('overview'); - const [sidebarOpen, setSidebarOpen] = useState(false); - const observerRef = useRef(null); - - // Track which section is visible - useEffect(() => { - const allIds = SIDEBAR_SECTIONS.flatMap((g) => g.items.map((i) => i.id)); - - observerRef.current = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - if (entry.isIntersecting) { - setActiveSection(entry.target.id); - } - } - }, - { rootMargin: '-20% 0px -70% 0px', threshold: 0 }, - ); - - for (const id of allIds) { - const el = document.getElementById(id); - if (el) observerRef.current.observe(el); - } - - return () => observerRef.current?.disconnect(); - }, []); - - const scrollTo = useCallback((id: string) => { - const el = document.getElementById(id); - if (el) { - const offset = 90; // header height + padding - const top = el.getBoundingClientRect().top + window.scrollY - offset; - window.scrollTo({ top, behavior: 'smooth' }); - } - setSidebarOpen(false); - }, []); - - return ( -
- {/* Mobile sidebar toggle */} - - - {/* ── Sidebar ── */} - - - {/* Overlay for mobile */} - {sidebarOpen && ( -
setSidebarOpen(false)} /> - )} - - {/* ── Main content ── */} -
- - {/* ════════════════════════════════════════════════════════════════ */} -
-

- ZamaVault is the canonical interface and developer toolkit for - Zama's confidential token ecosystem. It lets users and developers - discover, wrap, unwrap, and decrypt ERC-20 tokens that have been - converted into confidential ERC-7984 wrappers using{' '} - Fully Homomorphic Encryption (FHE). -

- -
- {[ - { - icon: '🔍', - title: 'Registry Explorer', - desc: 'Live on-chain discovery of all registered ERC-20 ↔ ERC-7984 wrapper pairs via the WrappersRegistry contract on Sepolia and Mainnet.', - }, - { - icon: '🛡️', - title: 'Shield & Unshield', - desc: 'Wrap public ERC-20 tokens into encrypted confidential tokens. Unwrap them back — with automatic resume for interrupted operations.', - }, - { - icon: '👁️', - title: 'Confidential Balances', - desc: 'Decrypt your encrypted portfolio balance using an EIP-712 permit signed in your wallet. Never auto-fires — always explicit user action.', - }, - { - icon: '🔌', - title: 'Public REST API', - desc: 'Fetch all wrapper pairs from any language with a simple GET request — no SDK or wallet connection required.', - }, - ].map((f) => ( -
-
{f.icon}
-
- {f.title} -

{f.desc}

-
-
- ))} -
- -
- Judging context: Built for the Zama Developer Program Mainnet Season 3 Bounty Track. - The goal is to turn the WrappersRegistry into a product every developer and user can point to. -
-
- - {/* ════════════════════════════════════════════════════════════════ */} -
-

- Integrate Zama confidential tokens into your app in three steps. -

- - - - - - -

- ZamaVault uses Wagmi for wallet connections and the Zama React SDK for FHE operations. - Both must be initialized at the root of your app. -

- - - - {children} - - - - ); -}`} - /> -
- - -

- Use the live registry to get all wrapper pairs, then call useShield{' '} - to wrap your first token. -

- { - // amount uses underlying token's decimals (e.g. 6 for USDC) - await shield({ amount: parseUnits('100', 6) }); - }; - - return ( - - ); -}`} - /> -
-
- - {/* ════════════════════════════════════════════════════════════════ */} -
-

- ZamaVault exposes a public REST API for querying the on-chain registry. - No SDK, no wallet, no authentication — just a fetch() call. -

- - - - -

- Returns all registered ERC-20 ↔ ERC-7984 wrapper pairs for the specified chain. - Data is read directly from the on-chain WrappersRegistry contract and - cached for 60 seconds (stale-while-revalidate 300s). -

- -

Query Parameters

- - - - - - - - - - - -
ParameterTypeDescription
- -

Response Schema

- - - - - - - - - - - - - - - - - -
FieldTypeDescription
- -

PairResult object

- - - - - - - - - - - - - - - - - -
FieldTypeDescription
- -

Examples

- - - c{pair['symbol']:8} | decimals: {pair['decimals']}/{pair['wrapperDecimals']}")`} - /> - -

HTTP Headers

- - - - - - - - - - - - - - -
HeaderValue
Cache-Controlpublic, s-maxage=60, stale-while-revalidate=300
Access-Control-Allow-Origin* (CORS open)
-
-
- - {/* ════════════════════════════════════════════════════════════════ */} -
-

- All confidential token operations are exposed as React hooks from{' '} - @zama-fhe/react-sdk. Install the package and wrap your app with - the providers shown in Quick Start. -

- - Loading…

: ( -
    - {data?.pairs.map(p => ( -
  • - {p.metadata?.symbol} ↔ c{p.metadata?.symbol} -
  • - ))} -
- ); -}`} - /> - - Promise }`} - example={`import { useShield } from '@zama-fhe/react-sdk'; -import { parseUnits } from 'viem'; - -function ShieldForm() { - const { mutateAsync: shield, isPending } = useShield({ - tokenAddress: '0x7c5BF43B851c1dff1a4feE8dB225b87f2C223639', // cUSDC on Sepolia - }); - - const handleShield = async () => { - // Parse using UNDERLYING decimals (6 for USDC, 18 for WETH) - const amount = parseUnits('100', 6); - const txHash = await shield({ amount }); - console.log('Shielded:', txHash); - }; - - return ; -}`} - /> - - Promise }`} - example={`import { useUnshield } from '@zama-fhe/react-sdk'; -import { parseUnits } from 'viem'; - -function UnshieldForm() { - const { mutateAsync: unshield, isPending } = useUnshield({ - tokenAddress: '0x7c5BF43B851c1dff1a4feE8dB225b87f2C223639', // cUSDC - }); - - const handleUnshield = async () => { - // Always use WRAPPER decimals (always 6) for unshield amounts - const amount = parseUnits('50', 6); - await unshield({ amount }); - // Zama Gateway will finalize the unwrap (~30-60s) - // Use useResumeUnshield if the user navigates away - }; - - return ; -}`} - /> - - Promise }`} - example={`import { useResumeUnshield, useZamaSDK, loadPendingUnshield } from '@zama-fhe/react-sdk'; -import { useEffect, useState } from 'react'; - -function ResumeBanner({ tokenAddress }: { tokenAddress: \`0x\${string}\` }) { - const sdk = useZamaSDK(); - const [pendingTx, setPendingTx] = useState<\`0x\${string}\` | null>(null); - const { mutateAsync: resume } = useResumeUnshield({ tokenAddress }); - - useEffect(() => { - if (!sdk?.storage) return; - loadPendingUnshield(sdk.storage, tokenAddress) - .then(tx => { if (tx) setPendingTx(tx as \`0x\${string}\`); }); - }, [sdk?.storage, tokenAddress]); - - if (!pendingTx) return null; - - return ( -
- Pending unshield detected! - -
- ); -}`} - /> - - setDecryptRequested(true)}>Decrypt Balance; - } - if (isLoading) return Awaiting signature…; - - // Wrapper decimals are always 6 - return {balance ? formatUnits(balance, 6) : '0'}; -}`} - /> - - , isLoading, error }`} - example={`import { useConfidentialBalances } from '@zama-fhe/react-sdk'; - -const WRAPPERS = [ - '0x7c5BF43B851c1dff1a4feE8dB225b87f2C223639', // cUSDC - '0x46208622DA27d91db4f0393733C8BA082ed83158', // cWETH -]; - -function Portfolio() { - const [decryptRequested, setDecryptRequested] = useState(false); - - const { data: balances, isLoading } = useConfidentialBalances({ - tokenAddresses: WRAPPERS, - // One EIP-712 permit covers all tokens — no permit spam - enabled: decryptRequested, - }); - - return ( -
- {!decryptRequested && ( - - )} - {balances && WRAPPERS.map(addr => ( -
Balance: {formatUnits(balances[addr.toLowerCase()] ?? 0n, 6)}
- ))} -
- ); -}`} - /> -
- - {/* ════════════════════════════════════════════════════════════════ */} -
- -

- Fully Homomorphic Encryption (FHE) is a cryptographic scheme that - allows arbitrary computations on encrypted data without decrypting it first. Zama's - fhEVM is a modified Ethereum Virtual Machine that supports FHE - operations natively in Solidity smart contracts. -

-

- ERC-7984 is the confidential token standard built on fhEVM. Instead - of storing balances as public uint256, wrapper contracts store them as - euint64 — encrypted 64-bit integers. The plaintext is never visible on-chain; - only the token owner can decrypt it. -

-
- Key properties of ERC-7984 tokens: -
    -
  • Balances are on-chain ciphertexts — unreadable by validators, indexers, or block explorers
  • -
  • Transfer amounts are encrypted — confidential even from recipients until decrypted
  • -
  • Decryption requires the owner's EIP-712 permit (see below)
  • -
  • Underlying ERC-20 is always 1:1 collateralized in the wrapper contract
  • -
-
-
-
- - {/* ════════════════════════════════════════════════════════════════ */} -
-

- This is the most common source of bugs when integrating Zama FHE tokens. - Read carefully. -

-

- FHE operates on euint64 — a 64-bit unsigned integer - with a maximum value of ~1.84 × 10¹⁹. A standard 18-decimal ERC-20 token - represents 1.0 ETH as 10¹⁸. Multiplied by any meaningful token amount, this - would overflow the 64-bit limit quickly. -

-

- Therefore, all ERC-7984 wrapper tokens use 6 decimals, regardless - of the underlying token's precision. The wrapper contract scales amounts - automatically during shielding and unshielding. -

- -
- ⚠️ Critical rule: When calling useShield, parse the - amount using the underlying token's decimals. When calling{' '} - useUnshield, always use 6 decimals (wrapper decimals). -
- -

Decision table

- - - - - - - - - - - - - - - - - - - - - - - - - -
OperationDecimals to useExample (1.0 WETH)
Shield (wrap)parseUnits(amount, underlyingDecimals)parseUnits("1", 18) → 10¹⁸
Unshield (unwrap)parseUnits(amount, 6)parseUnits("1", 6) → 10⁶
Display confidential balanceformatUnits(balance, 6)formatUnits(1_000_000n, 6) → "1.0"
- - -
- - {/* ════════════════════════════════════════════════════════════════ */} -
-

- Reading a confidential balance requires an EIP-712 typed-data signature from the - token owner's wallet. This signature authorizes the Zama Gateway to decrypt the - ciphertext and return the plaintext value to the frontend session. -

- -
- 🚨 Security rule — NEVER auto-fire permits. Every call to{' '} - useConfidentialBalance or useConfidentialBalances with{' '} - enabled: true will immediately request a wallet signature. Always gate it - behind an explicit decryptRequested boolean state that is only set{' '} - true on user click. -
- -

How it works

-
- {[ - { n: '1', t: 'User clicks "Decrypt"', d: 'Set decryptRequested = true in your component state.' }, - { n: '2', t: 'SDK requests EIP-712 signature', d: 'The hook constructs a typed data payload and asks MetaMask/Rabby to sign it. This is off-chain — no gas, no transaction.' }, - { n: '3', t: 'Session key derived', d: 'The signature is used to derive a short-lived session key scoped to your wallet address and the specific contract.' }, - { n: '4', t: 'Zama Gateway decrypts', d: 'The Gateway uses the session key to decrypt the on-chain ciphertext. Only your account\'s ciphertexts can be decrypted with your key.' }, - { n: '5', t: 'Plaintext returned to browser', d: 'The decrypted bigint balance is returned to your component. It is never stored on-chain in plaintext.' }, - ].map((s) => ( -
-
{s.n}
-
- {s.t} -

{s.d}

-
-
- ))} -
- -
- Token selector reset: When the user changes the selected token in - your UI, reset decryptRequested synchronously in the{' '} - onChange handler — not only in a useEffect. A one-frame - delay in the effect can cause the old true value to combine with the - new token address and auto-fire a permit. -
- - { - setSelectedToken(newToken); - setDecryptRequested(false); // ← must happen in same handler, not useEffect -}; - -const { data: balance } = useConfidentialBalance({ - tokenAddress: selectedToken.erc7984Address, - enabled: decryptRequested && !!address, // ← explicit gate -});`} - /> -
- - {/* ════════════════════════════════════════════════════════════════ */} -
-

- All addresses below are sourced from the official Zama documentation and verified - against the on-chain WrappersRegistry. Blocklisted entries (suspected test/placeholder - contracts with vanity addresses) are excluded. -

- - - -

- The first 7 pairs are mock tokens with a public mint(). - The 8th (ctGBP restricted) is a non-mintable pair — it does not have - a public mint function. -

-
- - - - -
- - {/* ════════════════════════════════════════════════════════════════ */} -
-

- Use matchZamaError from @zama-fhe/sdk to classify SDK - errors into user-friendly messages. ZamaVault re-exports this via the{' '} - classifyError(err) utility in src/lib/errors.ts. -

- - ({ title: 'Declined', message: 'You cancelled the signature.' }), - INSUFFICIENT_ERC20_BALANCE: () => ({ title: 'Low Balance', message: 'Not enough tokens.' }), - _: (e) => ({ title: 'Error', message: e.message }), - }); - showToast(result); -}`} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Error CodeTitleDescriptionRetry?
- -
- Wallet errors (non-SDK): Common wallet rejection strings like{' '} - user rejected, User denied, ACTION_REJECTED, - and user cancelled are caught by the fallback handler in{' '} - classifyError() and mapped to "Request Cancelled". -
-
- - {/* ── Footer ── */} -
-
- - Zama SDK Docs - - - GitHub - - - REST API (Sepolia) - - - Interactive Tutorial - -
-

- Contract addresses verified against{' '} - - Zama official docs - - . Registry entries are live on-chain — always use the REST API or{' '} - useListPairs for the most current data. -

-
- -
-
+ + + ); } diff --git a/src/app/app/docs/template.tsx b/src/app/app/docs/template.tsx new file mode 100644 index 0000000..ed12b65 --- /dev/null +++ b/src/app/app/docs/template.tsx @@ -0,0 +1,26 @@ +'use client'; + +/** + * Next.js re-mounts a `template` on every navigation — perfect for a fresh + * entrance animation per docs page. We also reset scroll to the top so a long + * page doesn't open half-way down after clicking Next. + */ + +import React, { useEffect } from 'react'; +import { motion } from 'framer-motion'; + +export default function DocsTemplate({ children }: { children: React.ReactNode }) { + useEffect(() => { + window.scrollTo({ top: 0, behavior: 'auto' }); + }, []); + + return ( + + {children} + + ); +} diff --git a/src/app/app/faucet/page.tsx b/src/app/app/faucet/page.tsx index 46823b6..1769e93 100644 --- a/src/app/app/faucet/page.tsx +++ b/src/app/app/faucet/page.tsx @@ -141,7 +141,7 @@ export default function FaucetPage() { setIsRequestPending(false); const classified = classifyError(err); addToast({ - variant: 'error', + variant: classified.retryable ? 'warning' : 'error', title: classified.title, message: classified.message, }); diff --git a/src/app/app/learn/page.tsx b/src/app/app/learn/page.tsx index 75e0dd0..3061e5d 100644 --- a/src/app/app/learn/page.tsx +++ b/src/app/app/learn/page.tsx @@ -172,7 +172,7 @@ function StepFaucet() { return (

- Before you can shield tokens, you need some test tokens. ZamaVault + Before you can shield tokens, you need some test tokens. ShadowLine includes a Faucet page that lets you mint free mock tokens on the Sepolia testnet — no cost, no limits.

@@ -285,7 +285,7 @@ function StepShield() {
- + - + @@ -743,7 +743,7 @@ export default function LearnPage() { `Convert public ${sym} into encrypted c${sym}. Requires ERC-20 approval then the shield transaction.`, - unshield: (sym: string) => `Burn encrypted c${sym} and retrieve public ${sym}. Two-step: on-chain unwrap + Gateway proof finalization.`, - permit: 'Read-only off-chain signature (EIP-712). Authorises Zama Gateway to decrypt your balance for this session. Does not spend tokens or approve contracts.', }; // ─── Per-row component ──────────────────────────────────────────────────────── +function shortName(name: string, maxLen = 24): string { + if (name.length <= maxLen) return name; + return name.slice(0, maxLen).trimEnd() + '…'; +} + function RegistryTokenRow({ wrapper, explorerBase, isTestnet, + batchedValue, + batchedError, }: { wrapper: WrapperPair; explorerBase: string; isTestnet: boolean; + /** Value from the parent's batched useConfidentialBalances (one signature for all). */ + batchedValue?: bigint; + /** Per-token error from the batched decrypt, if any. */ + batchedError?: Error; }) { const { address, isConnected } = useAccount(); const [decryptRequested, setDecryptRequested] = useState(false); + // When true, the per-row Decrypt was clicked — bypass any batched value from + // Decrypt-All so this click is authoritative. `??` preserves 0n on the left, + // so a stale batched 0n would otherwise short-circuit the per-row query and + // the button would feel unresponsive. + const [preferSingle, setPreferSingle] = useState(false); + const decryptErrorRef = useRef(null); + const { resetToken } = useSessionReset(); // Public ERC-20 balance const { data: rawPublicBalance } = useReadContract({ @@ -68,57 +97,101 @@ function RegistryTokenRow({ }); const publicBalance = rawPublicBalance as bigint | undefined; - // Confidential balance — only fires after explicit user click + // Per-row single-token decrypt — only fires after explicit user click. + // retry: false — a rejected permit signature must NOT re-prompt the wallet. const { - data: confidentialBalance, - isLoading: isDecrypting, - error: decryptError, + data: singleBalance, + isLoading: isSingleDecrypting, + error: singleError, refetch: refetchConfidential, } = useConfidentialBalance( { tokenAddress: wrapper.erc7984Address }, - { enabled: decryptRequested && isConnected && !!address }, + { + enabled: decryptRequested && isConnected && !!address && (preferSingle || batchedValue === undefined), + retry: false, + refetchOnWindowFocus: false, + }, ); + // preferSingle overrides the batched value once the user clicks the per-row + // Decrypt button. Otherwise, batched value from useConfidentialBalances wins + // (one Decrypt-All signature covers everything). + const confidentialBalance = preferSingle ? singleBalance : (batchedValue ?? singleBalance); + const isDecrypting = isSingleDecrypting; + const decryptError = preferSingle ? singleError : (batchedError ?? singleError); + const isRevoked = wrapper.isValid === false; - const cleanName = wrapper.name.replace(/\s*\(Mock\)\s*/gi, '').trim(); - const isMock = isMintablePair(wrapper) && isTestnet; + const cleanName = shortName(wrapper.name.replace(/\s*\(Mock\)\s*/gi, '').trim()); + // Every Sepolia registry pair is a Zama-deployed testnet mock — real mainnet + // assets don't exist on Sepolia. (isMintablePair's on-chain symbol() check is + // kept for the Faucet's actual mint-button gating, a separate concern; it's + // unreliable as a *label* since some mocks' symbol() doesn't end in "Mock".) + const isMock = isTestnet && !wrapper.name.toLowerCase().includes('restricted') && !wrapper.symbol.toLowerCase().includes('restricted'); const confidentialSymbol = `c${wrapper.symbol}`; + // App-wide session reset — re-arm the button so the next click prompts for + // a fresh EIP-712 signature (IndexedDB is empty after reset). + useEffect(() => { + if (resetToken > 0) { + setDecryptRequested(false); + setPreferSingle(false); + } + }, [resetToken]); + + // Fire-once: on decrypt error (incl. signature rejection), disable the query + // so it can't re-fire on remount/focus. The Decrypt button re-arms itself. + const { addToast } = useToast(); + useEffect(() => { + if (!singleError) { + decryptErrorRef.current = null; + return; + } + const msg = singleError.message ?? ''; + if (decryptErrorRef.current === msg) return; + decryptErrorRef.current = msg; + setDecryptRequested(false); + const classified = classifyError(singleError); + addToast({ variant: 'warning', title: classified.title, message: classified.message }); + }, [singleError, addToast]); + const handleDecrypt = () => { + setPreferSingle(true); setDecryptRequested(true); - refetchConfidential(); + void refetchConfidential(); }; - return ( - + const rowOpacity = isRevoked ? { opacity: 0.55 } : undefined; - {/* ── Token ─────────────────────────────────────────────────────────── */} - + return ( +
+ {/* ── Public token row ──────────────────────────────────────────────── */} +
-
- {/* Name + badges on one line */} +
- {cleanName} - {isMock && ( -
- Mock - -
- )} + {wrapper.symbol} + {isMock && Mock} {isRevoked && ( Revoked )} + {wrapper.unverified && ( + + Unverified + + )} +
+
+ {cleanName}
-
{wrapper.symbol}
- - - {/* ── ERC-20 Address ────────────────────────────────────────────────── */} - - - - {/* ── ERC-7984 Wrapper ──────────────────────────────────────────────── */} - -
-
- - Confidential - - -
-
+ + {/* ── Confidential wrapper row ──────────────────────────────────────── */} +
+ - - - {/* ── Public Balance ────────────────────────────────────────────────── */} - - {!isConnected ? ( - - ) : publicBalance !== undefined ? ( - - {formatAmount(publicBalance, wrapper.decimals)}{' '} - {wrapper.symbol} - - ) : ( - - )} - - - {/* ── Confidential Balance ──────────────────────────────────────────── */} - - {!isConnected ? ( - - ) : confidentialBalance !== undefined && confidentialBalance !== null ? ( - - {formatAmount(confidentialBalance, wrapper.wrapperDecimals)}{' '} - {confidentialSymbol} - - - ) : isDecrypting ? ( - Awaiting signature… - ) : decryptError ? ( - - ) : ( - + {formatAddress(wrapper.erc7984Address, 6)} + + + +
+ + {wrapper.wrapperDecimals} + FHE + + +
+ {!isConnected ? ( + + ) : confidentialBalance !== undefined && confidentialBalance !== null ? ( + confidentialBalance === 0n ? ( + + + No confidential balance yet + + + + ) : ( + + {formatAmount(confidentialBalance, wrapper.wrapperDecimals)}{' '} + {confidentialSymbol} + + + ) + ) : isDecrypting ? ( + Awaiting signature… + ) : decryptError ? ( + ) : ( + - - - )} - + )} +
+
+ {isRevoked ? ( + + Unavailable + + ) : ( +
+ + + +
+ )} +
+
+
+ ); +} - {/* ── Actions ───────────────────────────────────────────────────────── */} - - {isRevoked ? ( - - Unavailable - +// ─── Detected Token Row (For Wallet Scan Auto-Detection) ────────────────────── + +interface CustomTokenEntry { + address: `0x${string}`; + symbol: string; + name: string; + decimals: number; + isAutoDetected: boolean; +} + +function DetectedTokenRow({ + token, + explorerBase, + onRemove, + batchedValue, + batchedError, +}: { + token: CustomTokenEntry; + explorerBase: string; + onRemove?: () => void; + batchedValue?: bigint; + batchedError?: Error; +}) { + const { address, isConnected } = useAccount(); + const [decryptRequested, setDecryptRequested] = useState(false); + // Force the per-row query to win over a stale batched 0n — same rationale as + // in RegistryTokenRow. Clicking Decrypt is authoritative. + const [preferSingle, setPreferSingle] = useState(false); + const { resetToken } = useSessionReset(); + + // 1. Read the underlying ERC-20 via the canonical `underlying()` getter + // (OpenZeppelin IERC7984ERC20Wrapper — verified on-chain: real registry + // wrappers revert on the legacy `underlyingToken()` alias). + const { data: rawUnderlyingAddress } = useReadContract({ + abi: WRAPPER_ABI, + address: token.address, + functionName: 'underlying', + query: { enabled: isConnected && !!address }, + }); + const underlyingAddress = rawUnderlyingAddress as `0x${string}` | undefined; + const isWrapper = !!underlyingAddress && underlyingAddress !== '0x0000000000000000000000000000000000000000'; + + // 2. Query public balance of the underlying token (if it is a wrapper) + const { data: rawPublicBalance } = useReadContract({ + abi: ERC20_ABI, + address: isWrapper ? underlyingAddress : undefined, + functionName: 'balanceOf', + args: address ? [address] : undefined, + query: { enabled: isConnected && !!address && isWrapper }, + }); + const publicBalance = rawPublicBalance as bigint | undefined; + + // 3. Confidential balance — only decrypted on explicit user action. + // Prefer the parent's batched value (one signature covers everything). + // retry: false — a rejected permit signature must NOT re-prompt the wallet. + const { + data: singleBalance, + isLoading: isSingleDecrypting, + error: singleError, + } = useConfidentialBalance( + { tokenAddress: token.address }, + { + enabled: decryptRequested && isConnected && !!address && (preferSingle || batchedValue === undefined), + retry: false, + refetchOnWindowFocus: false, + }, + ); + const confidentialBalance = preferSingle ? singleBalance : (batchedValue ?? singleBalance); + const isDecrypting = isSingleDecrypting; + const decryptError = preferSingle ? singleError : (batchedError ?? singleError); + + const cleanName = shortName(token.name.replace(/\s*\(Mock\)\s*/gi, '').trim()); + const confidentialSymbol = isWrapper ? (token.symbol.startsWith('c') ? token.symbol : `c${token.symbol}`) : token.symbol; + + // App-wide reset — re-arm the button. + useEffect(() => { + if (resetToken > 0) { + setDecryptRequested(false); + setPreferSingle(false); + } + }, [resetToken]); + + // Fire-once: on decrypt error, disable the query so it can't re-fire. + const { addToast } = useToast(); + const decryptErrorRef = useRef(null); + useEffect(() => { + if (!singleError) { + decryptErrorRef.current = null; + return; + } + const msg = singleError.message ?? ''; + if (decryptErrorRef.current === msg) return; + decryptErrorRef.current = msg; + setDecryptRequested(false); + const classified = classifyError(singleError); + addToast({ variant: 'warning', title: classified.title, message: classified.message }); + }, [singleError, addToast]); + + const handleDecrypt = () => { + setPreferSingle(true); + setDecryptRequested(true); + // State transition alone enables the query; calling refetch() before the + // re-render means it fires against the still-disabled query and is a no-op. + }; + + return ( +
+ {/* ── Public token row (only shown if token wraps an underlying ERC-20) ── */} + {isWrapper && ( +
+ {isWrapper ? ( +
+ +
+
+ {token.symbol} + + {token.isAutoDetected ? 'Detected' : 'Custom'} + +
+
{cleanName}
+
+
) : ( +
+ +
+
+ {token.symbol} + + Decrypt-only + +
+ Native FHE asset — no ERC-20 +
+
+ )} + + {isWrapper ? {token.decimals} : } +
+ {!isConnected ? ( + + ) : !isWrapper ? ( + + ) : publicBalance !== undefined ? ( + + {formatAmount(publicBalance, token.decimals)}{' '} + {token.symbol} + + ) : ( + + )} +
+
+ {isWrapper ? ( +
+ + + +
+ ) : ( + Direct Transfer Only + )} +
+
+ )} + + {/* ── Confidential wrapper row ──────────────────────────────────────── */} +
+
+ +
+
+ {confidentialSymbol} + {!isWrapper && ( + + {token.isAutoDetected ? 'Detected' : 'Custom'} + + )} +
+
{isWrapper ? `Confidential ${cleanName}` : `${cleanName} (Standalone ERC-7984)`}
+
+
+ + + 6 + FHE + + +
+ {!isConnected ? ( + + ) : confidentialBalance !== undefined && confidentialBalance !== null ? ( + confidentialBalance === 0n ? ( + + + No confidential balance yet + + + + ) : ( + + {/* wrapperDecimals (euint64 = 6) not token.decimals — the encrypted + balance is in wrapper units, and the underlying scale would + show 0 for high-decimal underlyings. */} + {formatAmount(confidentialBalance, 6)}{' '} + {confidentialSymbol} + + + ) + ) : isDecrypting ? ( + Awaiting signature… + ) : decryptError ? ( + + ) : ( + + )} +
+
- - - - - - - - - - + + )} + {onRemove && ( + - + )}
- )} - - +
+
+
); } + // ─── Page ───────────────────────────────────────────────────────────────────── export default function HomePage() { const [searchQuery, setSearchQuery] = useState(''); const [showRevoked, setShowRevoked] = useState(false); + const customTokenSectionRef = useRef(null); const { isTestnet, activeChainId } = useActiveNetwork(); + const { addToast } = useToast(); + const { resetToken } = useSessionReset(); - const { pairs, isLoading, isFromCache, total }: RegistryPairsResult = + const { pairs, isLoading, isFromCache, officialTotal, customTotal }: RegistryPairsResult = useRegistryPairs(activeChainId); - const visibleWrappers = useMemo( - () => (showRevoked ? pairs : pairs.filter(p => p.isValid !== false)), - [pairs, showRevoked], + const { address, isConnected } = useAccount(); + // Pin the client to the app's active network — without the explicit chainId, + // wagmi follows the wallet's chain, so validation reads could silently hit + // the wrong network (e.g. wallet on Mainnet while the UI shows Sepolia). + const client = usePublicClient({ chainId: activeChainId }); + + const registryAddresses = useMemo(() => { + return new Set(pairs.map((p) => p.erc7984Address.toLowerCase())); + }, [pairs]); + + const { + extra: detectedExtras, + status: scanStatus, + rescan, + } = useWalletErc7984Scan(address, client, registryAddresses); + + // === Persistent Local Custom Pairs (chain-scoped, versioned) === + const [customPairs, setCustomPairs] = useState([]); + + // Client-side load (SSR-safe — window is only touched in the effect). + useEffect(() => { + setCustomPairs(loadCustomPairs(activeChainId)); + }, [activeChainId]); + + // Adapt CustomPairRecord[] → CustomTokenEntry[] for the existing row UI. + const localCustomTokens = useMemo( + () => customPairs.map((p) => ({ + address: p.erc7984Address, + symbol: p.symbol, + name: p.name, + decimals: p.wrapperDecimals, + isAutoDetected: false, + })), + [customPairs], ); + + // === Add Custom Pair — one input, on-chain validation === + const [inputAddress, setInputAddress] = useState(''); + const [addressError, setAddressError] = useState(''); + const [addressInfo, setAddressInfo] = useState(null); + const [previewPair, setPreviewPair] = useState(null); + const [isValidating, setIsValidating] = useState(false); + const inputRef = useRef(null); + const validationTokenRef = useRef(0); + + // Duplicate-check data is read through a ref so the validation effect below + // does NOT depend on `pairs` / `customPairs` / `detectedExtras` identities. + // With them in the dependency array, any unrelated re-render (balance + // refetch, scan status, decrypt state) re-ran the effect, cancelled the + // debounce timer, discarded the in-flight validation (token bump) and wiped + // the preview — the visible symptom was a stuck "Checking wrapper on-chain…" + // and a pair that could never be added. + const dedupDataRef = useRef({ pairs, customPairs, detectedExtras }); + useEffect(() => { + dedupDataRef.current = { pairs, customPairs, detectedExtras }; + }, [pairs, customPairs, detectedExtras]); + + // Debounced on-chain validation. Runs against a wallet-independent viem + // PublicClient — works even before a wallet is connected. + useEffect(() => { + setAddressError(''); + setAddressInfo(null); + setPreviewPair(null); + const paste = inputAddress.trim(); + if (!paste) return; + if (!isAddress(paste)) { + // Distinguish a checksum failure (right shape, wrong EIP-55 casing — + // viem's strict isAddress rejects it) from a malformed string, so the + // user gets an actionable message instead of a generic one. + setAddressError( + /^0x[0-9a-fA-F]{40}$/.test(paste) + ? 'Address checksum is invalid (EIP-55 mixed-case mismatch) — re-copy it from the explorer or paste it in all-lowercase.' + : 'Not a valid Ethereum address (0x-prefixed, 42 chars).', + ); + return; + } + const wrapperAddr = getAddress(paste) as `0x${string}`; + if (wrapperAddr === '0x0000000000000000000000000000000000000000') { + setAddressError('Cannot add the zero address.'); + return; + } + if (!client) { + setAddressError('No RPC client available for this network — try reloading.'); + return; + } + + const token = ++validationTokenRef.current; + const timer = setTimeout(async () => { + setIsValidating(true); + try { + // 1) Contract check. + const code = await client.getCode({ address: wrapperAddr }); + if (token !== validationTokenRef.current) return; + if (!code || code === '0x') { + setAddressError('Not a contract on this network.'); + return; + } + + // 2) ERC-7984 check — robust: ERC-165 fast path, then a behavioral + // `confidentialBalanceOf` probe so tokens that don't implement ERC-165 + // (but ARE real ERC-7984s) are still accepted. See isErc7984Contract. + const isErc7984 = await isErc7984Contract(client, wrapperAddr); + if (token !== validationTokenRef.current) return; + if (!isErc7984) { + setAddressError('Not an ERC-7984 confidential token (no ERC-165 support and no confidentialBalanceOf).'); + return; + } + + // 3) underlying() — canonical name, with the legacy underlyingToken() + // alias as a fallback. A token with NO underlying is a valid + // confidential-only ERC-7984 (not a wrapper): we still add it, as a + // decrypt-only token with no shield/unshield side. + let underlyingAddr: `0x${string}` | null = null; + try { + underlyingAddr = (await client.readContract({ + address: wrapperAddr, + abi: WRAPPER_ABI, + functionName: 'underlying', + })) as `0x${string}`; + } catch { + try { + underlyingAddr = (await client.readContract({ + address: wrapperAddr, + abi: WRAPPER_ABI, + functionName: 'underlyingToken', + })) as `0x${string}`; + } catch { underlyingAddr = null; } + } + if (token !== validationTokenRef.current) return; + + const isWrapper = + !!underlyingAddr && + underlyingAddr !== '0x0000000000000000000000000000000000000000' && + underlyingAddr.toLowerCase() !== wrapperAddr.toLowerCase(); + + // 4) Wrapper metadata is always read; underlying metadata only when it's + // an actual wrapper. + const [wSymRaw, wNameRaw, wDecRaw] = await Promise.all([ + client.readContract({ address: wrapperAddr, abi: WRAPPER_ABI, functionName: 'symbol' }).catch(() => null), + client.readContract({ address: wrapperAddr, abi: WRAPPER_ABI, functionName: 'name' }).catch(() => null), + client.readContract({ address: wrapperAddr, abi: WRAPPER_ABI, functionName: 'decimals' }).catch(() => null), + ]); + if (token !== validationTokenRef.current) return; + if (wSymRaw == null || wNameRaw == null || wDecRaw == null) { + setAddressError('Failed to read token metadata from the contract.'); + return; + } + const wrapperSymbol = String(wSymRaw); + const wrapperName = String(wNameRaw); + const wrapperDecimals = Number(wDecRaw); + + let underlyingSymbol = ''; + let underlyingName = ''; + let underlyingDecimals = wrapperDecimals; + if (isWrapper && underlyingAddr) { + const [uSymRaw, uNameRaw, uDecRaw] = await Promise.all([ + client.readContract({ address: underlyingAddr, abi: ERC20_ABI, functionName: 'symbol' }).catch(() => null), + client.readContract({ address: underlyingAddr, abi: ERC20_ABI, functionName: 'name' }).catch(() => null), + client.readContract({ address: underlyingAddr, abi: ERC20_ABI, functionName: 'decimals' }).catch(() => null), + ]); + if (token !== validationTokenRef.current) return; + if (uSymRaw == null || uNameRaw == null || uDecRaw == null) { + setAddressError('Failed to read underlying ERC-20 metadata.'); + return; + } + underlyingSymbol = String(uSymRaw); + underlyingName = String(uNameRaw); + underlyingDecimals = Number(uDecRaw); + } + + // 5) Duplicate check (latest data via ref — see above). + const { pairs: allPairs, customPairs: existingCustom } = dedupDataRef.current; + const wLower = wrapperAddr.toLowerCase(); + const uLower = underlyingAddr?.toLowerCase(); + const registryHitByWrapper = allPairs.find( + (p) => p.source !== 'custom' && p.erc7984Address.toLowerCase() === wLower, + ); + const registryHitByUnderlying = isWrapper + ? allPairs.find((p) => p.source !== 'custom' && p.erc20Address.toLowerCase() === uLower) + : undefined; + if (registryHitByWrapper) { + if (registryHitByWrapper.isValid === false) { + setAddressError('This wrapper is revoked in the on-chain registry.'); + return; + } + setAddressInfo(`This pair is already Official (${registryHitByWrapper.symbol}) — no need to add it.`); + setPreviewPair(null); + return; + } + if (registryHitByUnderlying) { + setAddressError(`The underlying ${underlyingSymbol} is already in the Official registry (paired with ${registryHitByUnderlying.symbol}).`); + return; + } + if (existingCustom.some((p) => { + if (p.erc7984Address.toLowerCase() === wLower) return true; + // erc20 collision check only when both sides have a non-zero underlying — + // avoids false collisions between distinct confidential-only tokens + // (both stored with erc20 = zero address). + if (!isWrapper || !uLower) return false; + const pu = p.erc20Address.toLowerCase(); + if (pu === '0x0000000000000000000000000000000000000000') return false; + return pu === uLower; + })) { + setAddressError('This token has already been added.'); + return; + } + // Scanner-detected tokens are NOT a blocker for adding — the user is + // promoting an auto-detected token to a first-class custom pair, which + // saves it to localStorage and gets it out of the "detected" bucket. + // (Duplicate row prevention lives in allCustomTokens dedup.) + + // All checks passed — build the preview. + setPreviewPair({ + erc7984Address: wrapperAddr, + erc20Address: isWrapper && underlyingAddr + ? (getAddress(underlyingAddr) as `0x${string}`) + : '0x0000000000000000000000000000000000000000', + symbol: wrapperSymbol, + name: wrapperName, + decimals: underlyingDecimals, + wrapperDecimals, + underlyingSymbol, + underlyingName, + addedAt: Date.now(), + source: 'custom', + isWrapper, + }); + } catch (err) { + if (token !== validationTokenRef.current) return; + const classified = classifyError(err); + setAddressError(classified.message || 'Validation failed.'); + } finally { + if (token === validationTokenRef.current) setIsValidating(false); + } + }, 500); + + return () => clearTimeout(timer); + // Dedup data intentionally read via dedupDataRef (kept in sync above) so + // identity churn on pairs/scan results can't cancel an in-flight check. + }, [inputAddress, client]); + + const handleAddCustomToken = () => { + if (!previewPair) return; + // Backstop dedup at add time — the validation snapshot may be stale if the + // registry/scan updated while the preview was showing. + // + // Comparing erc20Address only makes sense when it's non-zero. For a + // confidential-only ERC-7984 the underlying is the zero address, and + // comparing zero-vs-zero against ANY prior confidential-only entry would + // false-collide — that was the "This pair already exists" bug on a second + // confidential-only add. + const wLower = previewPair.erc7984Address.toLowerCase(); + const uLower = previewPair.erc20Address.toLowerCase(); + const hasUnderlying = uLower !== '0x0000000000000000000000000000000000000000'; + const collides = (p: { erc7984Address: string; erc20Address: string }) => { + if (p.erc7984Address.toLowerCase() === wLower) return true; + if (!hasUnderlying) return false; + const pu = p.erc20Address.toLowerCase(); + if (pu === '0x0000000000000000000000000000000000000000') return false; + return pu === uLower; + }; + if ( + customPairs.some(collides) || + pairs.some((p) => p.source !== 'custom' && collides(p)) + ) { + setPreviewPair(null); + setAddressError('This pair already exists in the registry or your custom list.'); + return; + } + const next = [...customPairs, previewPair]; + setCustomPairs(next); + saveCustomPairs(activeChainId, next); + setInputAddress(''); + setPreviewPair(null); + setAddressError(''); + setAddressInfo(null); + inputRef.current?.focus(); + addToast({ + variant: 'success', + title: previewPair.isWrapper === false ? 'Confidential Token Added' : 'Custom Pair Added', + message: previewPair.isWrapper === false + ? `${previewPair.symbol} is now available to decrypt in the Custom / Dev-only section.` + : `${previewPair.symbol} ↔ ${previewPair.underlyingSymbol} is now available for shield/unshield/decrypt.`, + }); + }; + + const handleRemoveCustomToken = (tokenAddress: string) => { + const next = customPairs.filter( + (p) => p.erc7984Address.toLowerCase() !== tokenAddress.toLowerCase(), + ); + setCustomPairs(next); + saveCustomPairs(activeChainId, next); + }; + + // ── Export / Import custom pairs (JSON) — survives a browser-cache wipe ── + const importFileRef = useRef(null); + + const handleExportCustomPairs = () => { + const payload = JSON.stringify( + { app: 'shadowline', kind: 'custom-pairs', version: 1, chainId: activeChainId, pairs: customPairs }, + null, + 2, + ); + const blob = new Blob([payload], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `shadowline-custom-pairs-${activeChainId}.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const handleImportCustomPairs = (file: File) => { + const reader = new FileReader(); + reader.onload = () => { + try { + const parsed = JSON.parse(String(reader.result)) as { + chainId?: number; + pairs?: CustomPairRecord[]; + }; + const incoming = Array.isArray(parsed.pairs) ? parsed.pairs : []; + // Schema check per record — drop anything malformed instead of crashing. + const valid = incoming.filter( + (p) => + p && + isAddress(p.erc7984Address ?? '') && + isAddress(p.erc20Address ?? '') && + typeof p.symbol === 'string' && + typeof p.decimals === 'number' && + typeof p.wrapperDecimals === 'number', + ); + if (parsed.chainId !== undefined && parsed.chainId !== activeChainId) { + addToast({ + variant: 'warning', + title: 'Chain Mismatch', + message: `This file was exported for chain ${parsed.chainId}, but the active network is ${activeChainId}. Import skipped.`, + }); + return; + } + const known = new Set([ + ...customPairs.map((p) => p.erc7984Address.toLowerCase()), + ...pairs.map((p) => p.erc7984Address.toLowerCase()), + ]); + const fresh = valid.filter((p) => !known.has(p.erc7984Address.toLowerCase())); + if (fresh.length === 0) { + addToast({ + variant: 'info', + title: 'Nothing to Import', + message: valid.length > 0 ? 'All pairs in the file already exist.' : 'No valid pair records found in the file.', + }); + return; + } + const next = [...customPairs, ...fresh]; + setCustomPairs(next); + saveCustomPairs(activeChainId, next); + addToast({ + variant: 'success', + title: 'Pairs Imported', + message: `${fresh.length} custom pair${fresh.length === 1 ? '' : 's'} restored from file.`, + }); + } catch { + addToast({ variant: 'error', title: 'Import Failed', message: 'The file is not valid ShadowLine custom-pairs JSON.' }); + } + }; + reader.readAsText(file); + }; + + // Combine auto-detected extras and manual custom tokens, deduplicating by address + const allCustomTokens = useMemo(() => { + const merged = localCustomTokens.map((t) => ({ ...t, isAutoDetected: false })); + const existing = new Set(merged.map((t) => t.address.toLowerCase())); + + for (const token of detectedExtras) { + if (!existing.has(token.address.toLowerCase())) { + merged.push({ + address: token.address, + symbol: token.symbol, + name: token.name, + decimals: token.decimals, + isAutoDetected: true, + }); + } + } + return merged; + }, [localCustomTokens, detectedExtras]); + + // The main table is the "Official — Zama Registry" section: custom pairs are + // excluded here and rendered exclusively in the "Custom / Dev-only" section + // below (they still flow to wrap/transfer/portfolio via useRegistryPairs). + const visibleWrappers = useMemo(() => { + const official = pairs.filter(p => p.source !== 'custom'); + return showRevoked ? official : official.filter(p => p.isValid !== false); + }, [pairs, showRevoked]); const revokedCount = useMemo(() => pairs.filter(p => p.isValid === false).length, [pairs]); const filteredWrappers = useMemo(() => { @@ -290,6 +1114,80 @@ export default function HomePage() { const explorerBase = isTestnet ? 'https://eth-sepolia.blockscout.com' : 'https://eth.blockscout.com'; + // ── Batched Decrypt All (ONE EIP-712 signature covers every listed token) ── + // useConfidentialBalances (plural) calls credentials.allow(...addresses) once. + // Populate `batchAddresses` on click; empty array = disabled (no popup). + // Result gets distributed to both RegistryTokenRow and DetectedTokenRow via + // the `batchedValue` / `batchedError` props — rows fall back to their own + // single-token decrypt for the per-row "Decrypt" button. + const [batchAddresses, setBatchAddresses] = useState<`0x${string}`[]>([]); + const batchErrorRef = useRef(null); + + const { + data: batchResult, + isFetching: isBatchDecrypting, + error: batchError, + } = useConfidentialBalances( + { tokenAddresses: batchAddresses }, + { enabled: batchAddresses.length > 0, retry: false, refetchOnWindowFocus: false }, + ); + + // Reset on app-wide session reset — disarm the batch query. + useEffect(() => { + if (resetToken > 0) { + setBatchAddresses([]); + batchErrorRef.current = null; + } + }, [resetToken]); + + // Fire-once on batch failure (incl. rejected signature): disarm + one toast. + useEffect(() => { + if (!batchError) { + batchErrorRef.current = null; + return; + } + const msg = batchError.message ?? ''; + if (batchErrorRef.current === msg) return; + batchErrorRef.current = msg; + setBatchAddresses([]); + const classified = classifyError(batchError); + addToast({ variant: 'warning', title: classified.title, message: classified.message }); + }, [batchError, addToast]); + + // Fast address→bigint lookup for the row prop. + const batchValueByAddress = useMemo(() => { + const map = new Map(); + if (batchResult?.results instanceof Map) { + for (const [addr, val] of batchResult.results.entries()) { + if (typeof val === 'bigint') map.set(addr.toLowerCase(), val); + } + } + return map; + }, [batchResult]); + + const batchErrorByAddress = useMemo(() => { + const map = new Map(); + if (batchResult?.errors instanceof Map) { + for (const [addr, err] of batchResult.errors.entries()) { + if (err instanceof Error) map.set(addr.toLowerCase(), err); + } + } + return map; + }, [batchResult]); + + const handleDecryptAll = () => { + if (!isConnected) return; + const addresses: `0x${string}`[] = []; + for (const w of filteredWrappers) { + if (w.isValid !== false) addresses.push(w.erc7984Address); + } + for (const t of allCustomTokens) { + addresses.push(t.address); + } + if (addresses.length === 0) return; + setBatchAddresses(addresses); + }; + return (
{/* Header */} @@ -320,32 +1218,74 @@ export default function HomePage() { )} - {/* Stats */} -
- -
Registered Pairs
-
- {isLoading && pairs.length === 0 ? : total} + {/* Stats Bar */} +
+ {/* Pairs */} +
+ +
+
+ {isLoading ? : officialTotal} +
+
+ Wrapper Pairs + {revokedCount > 0 && · {revokedCount} revoked} +
- {revokedCount > 0 &&
{revokedCount} revoked
} - - -
Active Network
- - {isTestnet ? 'Sepolia Testnet' : 'Ethereum Mainnet'} - -
- -
FHE Security
-
- ERC-7984 Standard - +
+ + {/* Network */} +
+
+
+
+ {isTestnet ? 'Sepolia Testnet' : 'Ethereum Mainnet'} +
+
Active Network
- +
+ + {/* Standard */} +
+ +
+
+ ERC-7984 + +
+
Confidential Standard
+
+
+ + {/* Encryption layer */} +
+ +
+
fhEVM · Zama
+
Encryption Layer
+
+
- {/* Search */} -
+ {/* Search + Actions */} +
@@ -360,75 +1300,278 @@ export default function HomePage() { aria-label="Search registered wrapper pairs" />
- {revokedCount > 0 && ( - +
+ {revokedCount > 0 && ( + + )} + {isConnected && filteredWrappers.length > 0 && ( + + )} + {isConnected && ( + + )} +
+
+ + {/* ── Section: Official — Zama Registry ─────────────────────────────── */} +
+

+ + Official Registry +

+

+ Verified on-chain by the Confidential Token Wrappers Registry. + {isTestnet && ' Most Sepolia pairs are Zama mock tokens with a public mint — grab free test tokens from the Faucet page.'} +

+
+ + {/* Pair list */} +
+
+ Token + Address + Decimals + + Balance + + + Actions +
+ + {isLoading && filteredWrappers.length === 0 ? ( +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ ) : filteredWrappers.length === 0 ? ( +
+
+ +
+
+ {searchQuery ? 'No tokens match your search query' : 'No registered wrappers found on this network'} +
+
+ ) : ( +
+ {filteredWrappers.map(wrapper => ( + + ))} +
)}
- {/* Table */} -
- - - - - - - - - - - - - {isLoading && filteredWrappers.length === 0 ? ( - Array.from({ length: 5 }).map((_, i) => ( - - - - )) - ) : filteredWrappers.length === 0 ? ( - - - - ) : ( - filteredWrappers.map(wrapper => ( - - )) + + {previewPair.isWrapper === false ? ( + + Decrypt-only · no ERC-20 wrapper + + ) : ( + <> +
+
+ +
+
Underlying {previewPair.underlyingSymbol}
+
{formatAddress(previewPair.erc20Address, 6)} · {previewPair.decimals} dec
+
+
+ + )} + )} -
-
TokenERC-20 Address - - ERC-7984 Wrapper - - - - - Public Balance - - - - - Confidential Balance - - - Actions
-
-
- -
-
- {searchQuery ? 'No tokens match your search query' : 'No registered wrappers found on this network'} + {/* Auto-Detected & Custom Tokens Section */} + {isConnected && ( +
+
+
+

+ + Custom / Dev-only Tokens +

+

+ Added locally in this browser · not in the official registry. Includes ERC-7984 tokens auto-detected from your wallet history. +

+
+
+ {customPairs.length > 0 && ( + + )} + + { + const file = e.target.files?.[0]; + if (file) handleImportCustomPairs(file); + e.target.value = ''; + }} + /> + {scanStatus !== 'scanning' && ( + + )} +
+
+ + {/* Add-Custom-Pair form — one input, on-chain validated */} + +
+
+ +
+ + + + setInputAddress(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && previewPair && handleAddCustomToken()} + spellCheck={false} + autoComplete="off" + /> +
+ {isValidating && ( +
+ Checking wrapper on-chain… +
+ )} + {!isValidating && addressError && ( +
+ {addressError} +
+ )} + {!isValidating && addressInfo && ( +
+ {addressInfo} +
+ )} +
+
+
Add
+ +
+
+ + {previewPair && ( +
+
+ +
+
+ {previewPair.isWrapper === false ? 'Confidential' : 'Wrapper'} {previewPair.symbol}
+
{formatAddress(previewPair.erc7984Address, 6)} · {previewPair.wrapperDecimals} dec
-
-
+ +
+ + Custom pairs are stored locally in this browser (chain-scoped) and go through the same shield / unshield / decrypt paths as Official pairs — just without the on-chain registry endorsement. +
+ + + {/* Token List */} + {scanStatus === 'scanning' ? ( + +
+ + Scanning wallet transfer logs & Blockscout API for custom tokens... +
+
+ ) : allCustomTokens.length === 0 ? ( +
+
+ +
+

+ No additional custom tokens detected in wallet history. Use the form above to manually register a custom token address. +

+
+ ) : ( +
+
+ Token + Address + Decimals + Balance + Actions +
+
+ {allCustomTokens.map((token) => ( + handleRemoveCustomToken(token.address)} + batchedValue={batchValueByAddress.get(token.address.toLowerCase())} + batchedError={batchErrorByAddress.get(token.address.toLowerCase())} + /> + ))} +
+
+ )} +
+ )} {/* Info Banner */} diff --git a/src/app/app/portfolio/page.tsx b/src/app/app/portfolio/page.tsx index 8107528..f60d4b4 100644 --- a/src/app/app/portfolio/page.tsx +++ b/src/app/app/portfolio/page.tsx @@ -1,25 +1,24 @@ 'use client'; -import React, { useState, useEffect, useMemo, useCallback } from 'react'; -import Link from 'next/link'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import Badge from '@/components/ui/Badge'; import Modal from '@/components/ui/Modal'; import TokenIcon from '@/components/ui/TokenIcon'; -import Skeleton from '@/components/ui/Skeleton'; +import WalletActivityFeed from '@/components/WalletActivityFeed'; import { type WrapperPair } from '@/config/contracts'; import { formatAmount, formatAddress } from '@/lib/utils'; import { classifyError } from '@/lib/errors'; import PendingUnshieldBanner from '@/components/PendingUnshieldBanner'; import { useActiveNetwork } from '@/app/ClientLayout'; import { useRegistryPairs } from '@/lib/registry'; -import { useAccount, useConnect, usePublicClient } from 'wagmi'; -import { useConfidentialBalances, useRevokeSession } from '@zama-fhe/react-sdk'; +import { useAccount, useConnect } from 'wagmi'; +import { useConfidentialBalances, useConfidentialBalance } from '@zama-fhe/react-sdk'; +import { useQueryClient } from '@tanstack/react-query'; import { useToast } from '@/components/ui/Toast'; +import { useSessionReset } from '@/lib/reset-session'; import BlurIn from '@/components/ui/BlurIn'; -import { parseAbiItem, formatUnits } from 'viem'; -import { CHAIN_CONFIG } from '@/config/chains'; import { Lock, Unlock, @@ -27,16 +26,11 @@ import { Shield, Wallet, RefreshCw, - Clock, - ArrowUpRight, - ArrowDownLeft, - BarChart2, - ExternalLink, + AlertTriangle, + Settings2, } from 'lucide-react'; -const TRANSFER_ABI = parseAbiItem( - 'event Transfer(address indexed from, address indexed to, uint256 value)', -); +// ── Official wrapper card ──────────────────────────────────────────────────── interface TokenPositionProps { wrapper: WrapperPair; @@ -46,6 +40,8 @@ interface TokenPositionProps { decryptedBalance: bigint | undefined; decryptError: Error | null; onDecrypt: () => void; + /** When true, shows Unshield + Decrypt Again; when false only Decrypt. */ + isConfidentialOnly?: boolean; } function TokenPositionCard({ @@ -56,21 +52,26 @@ function TokenPositionCard({ decryptedBalance, decryptError, onDecrypt, + isConfidentialOnly = false, }: TokenPositionProps) { return ( - {/* Token Header */} -
- -
-
{wrapper.name}
+
+ +
+
{wrapper.name}
c{wrapper.symbol} · {formatAddress(wrapper.erc7984Address)}
- - ERC-7984 - +
+ ERC-7984 + {isConfidentialOnly && ( + + Decrypt only + + )} +
{/* Balance Display */} @@ -78,8 +79,8 @@ function TokenPositionCard({ style={{ background: 'var(--bg-input)', borderRadius: 'var(--radius-lg)', - padding: 'var(--sp-4) var(--sp-5)', - marginBottom: 'var(--sp-4)', + padding: 'var(--sp-3) var(--sp-4)', + marginBottom: 'var(--sp-3)', border: '1px solid var(--border)', }} > @@ -89,42 +90,30 @@ function TokenPositionCard({ {isDecrypting ? (
- Awaiting Permit... + Awaiting permit…
) : decryptError ? ( -
- Error: {decryptError.message || 'Decryption failed. Ensure the wrapper is deployed.'} +
+ {classifyError(decryptError).message}
) : isDecrypted ? (
{formatAmount(decryptedBalance ?? 0n, wrapper.wrapperDecimals)} - - c{wrapper.symbol} - + c{wrapper.symbol}
) : (
- + •••••• - - Encrypted + + Encrypted
- - Decrypt to View - + Click to decrypt
)}
@@ -138,22 +127,24 @@ function TokenPositionCard({ isLoading={isDecrypting} disabled={!isConnected} onClick={onDecrypt} - style={{ gap: '6px' }} + style={{ gap: 6 }} > - Decrypt Balance + Decrypt Balance ) : ( <> - - + )} + )} @@ -162,450 +153,425 @@ function TokenPositionCard({ ); } -/* ─── Wallet Activity Feed ─────────────────────────────────────────────────── */ - -interface WalletEvent { - type: 'shield' | 'unshield'; - symbol: string; - amount: bigint; - decimals: number; - counterpart: string; - txHash: string; - blockNumber: bigint; -} +// ── Confidential-only per-row decrypt (singular hook, one per card) ────────── -function WalletActivityFeed({ - address, - wrappers, - chainId, +function ConfidentialOnlyCard({ + wrapper, + isConnected, + resetToken, }: { - address: `0x${string}`; - wrappers: WrapperPair[]; - chainId: number; + wrapper: WrapperPair; + isConnected: boolean; + resetToken: number; }) { - const client = usePublicClient({ chainId }); - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(false); - const explorerBase = CHAIN_CONFIG[chainId as keyof typeof CHAIN_CONFIG]?.explorerUrl ?? 'https://eth.blockscout.com'; - - const fetchActivity = useCallback(async () => { - if (!client || wrappers.length === 0 || !address) return; - setLoading(true); - try { - const latestBlock = await client.getBlockNumber(); - const fromBlock = latestBlock > 10000n ? latestBlock - 10000n : 0n; - - const allEvents: WalletEvent[] = []; - await Promise.all( - wrappers.filter((p) => p.isValid !== false).map(async (pair) => { - try { - // Shield: ERC-20 Transfer from user to wrapper - const shields = await client.getLogs({ - address: pair.erc20Address, - event: TRANSFER_ABI, - args: { from: address, to: pair.erc7984Address }, - fromBlock, - toBlock: latestBlock, - }); - // Unshield: ERC-20 Transfer from wrapper to user - const unshields = await client.getLogs({ - address: pair.erc20Address, - event: TRANSFER_ABI, - args: { from: pair.erc7984Address, to: address }, - fromBlock, - toBlock: latestBlock, - }); - for (const log of shields) { - allEvents.push({ - type: 'shield', - symbol: pair.symbol, - amount: (log.args?.value as bigint) ?? 0n, - decimals: pair.decimals, - counterpart: pair.erc7984Address, - txHash: log.transactionHash ?? '', - blockNumber: log.blockNumber ?? 0n, - }); - } - for (const log of unshields) { - allEvents.push({ - type: 'unshield', - symbol: pair.symbol, - amount: (log.args?.value as bigint) ?? 0n, - decimals: pair.decimals, - counterpart: pair.erc7984Address, - txHash: log.transactionHash ?? '', - blockNumber: log.blockNumber ?? 0n, - }); - } - } catch { /* skip failed token */ } - }), - ); - allEvents.sort((a, b) => Number(b.blockNumber - a.blockNumber)); - setEvents(allEvents.slice(0, 20)); - } catch { /* ignore */ } - finally { setLoading(false); } - }, [client, wrappers, address]); - - useEffect(() => { fetchActivity(); }, [fetchActivity]); + const [decryptRequested, setDecryptRequested] = useState(false); - return ( - -
-

- - My Recent Activity -

-
- - - - -
-
+ const { + data: balance, + isLoading, + error, + } = useConfidentialBalance( + { tokenAddress: wrapper.erc7984Address as `0x${string}` }, + { + enabled: decryptRequested && isConnected, + retry: false, + refetchOnWindowFocus: false, + }, + ); - {loading && events.length === 0 ? ( -
- {[1, 2, 3].map((i) => )} -
- ) : events.length === 0 ? ( -

- No shield or unshield events found in the last ~34 hours for this wallet. -

- ) : ( -
- {events.map((ev, i) => { - const isShield = ev.type === 'shield'; - const color = isShield ? 'var(--success)' : 'var(--warning)'; - const Icon = isShield ? ArrowUpRight : ArrowDownLeft; - return ( -
-
- -
-
-
- {isShield ? 'Shield' : 'Unshield'} - - {formatUnits(ev.amount, ev.decimals)} {ev.symbol} - -
-
- Wrapper: {formatAddress(ev.counterpart)} -
-
- - Tx - -
- ); - })} -
- )} -
+ // Reset on app-wide session reset + useEffect(() => { + if (resetToken > 0) setDecryptRequested(false); + }, [resetToken]); + + const { addToast } = useToast(); + const lastErrorRef = useRef(null); + useEffect(() => { + if (!error) { + lastErrorRef.current = null; + return; + } + const msg = error.message ?? ''; + if (lastErrorRef.current === msg) return; + lastErrorRef.current = msg; + setDecryptRequested(false); + const classified = classifyError(error); + addToast({ variant: 'warning', title: classified.title, message: classified.message }); + }, [error, addToast]); + + return ( + setDecryptRequested(true)} + isConfidentialOnly + /> ); } +// ── Main page ──────────────────────────────────────────────────────────────── + export default function PortfolioPage() { const { activeChainId } = useActiveNetwork(); - const { address, isConnected } = useAccount(); + const { address, isConnected, chain: walletChain } = useAccount(); const { connect, connectors } = useConnect(); const { addToast } = useToast(); + const queryClient = useQueryClient(); + const { reset: resetSession, isResetting: isRevoking, resetToken } = useSessionReset(); - // Live registry read with hardcoded fallback. We deliberately keep - // revoked pairs OUT of the portfolio: a revoked wrapper cannot accept new - // shields, but a user may still hold a non-zero confidential balance in - // one and need to decrypt + unshield it. We include all pairs and let - // the per-card UI reflect the revoked state. - const { pairs: wrappers } = useRegistryPairs(activeChainId); + const { pairs, localRecords } = useRegistryPairs(activeChainId); + + // Official pairs = registry + config-file (never localStorage custom) + const officialWrappers = useMemo(() => pairs.filter((p) => p.source !== 'custom'), [pairs]); + // Custom wrapper pairs (isWrapper:true — has ERC-20 underlying, can unshield) + const customWrapperPairs = useMemo(() => pairs.filter((p) => p.source === 'custom' && p.isWrapper !== false), [pairs]); + // Confidential-only custom pairs (isWrapper:false — no underlying, decrypt only) + const customConfOnly = useMemo(() => pairs.filter((p) => p.source === 'custom' && p.isWrapper === false), [pairs]); + + // All pairs passed to the activity feed (official + custom wrappers) + const allWrappers = useMemo(() => pairs, [pairs]); const [requestedAddresses, setRequestedAddresses] = useState<`0x${string}`[]>([]); const [resolvedBalances, setResolvedBalances] = useState>({}); const [resolvedErrors, setResolvedErrors] = useState>({}); const [isConnectModalOpen, setIsConnectModalOpen] = useState(false); + const [fheWorkerFailed, setFheWorkerFailed] = useState(false); + + const resolvedBalancesRef = useRef(resolvedBalances); + useEffect(() => { resolvedBalancesRef.current = resolvedBalances; }, [resolvedBalances]); + + const lastHandledErrorMsgRef = useRef(null); + const autoRevokedForMsgRef = useRef(null); + const revokeSessionRef = useRef<(() => void) | null>(null); + + const supportedChain = + !isConnected || !walletChain || walletChain.id === 11155111 || walletChain.id === 1; - // Zama Official useConfidentialBalances hook (plural) const { data: decryptedBalances, isLoading: isDecryptingAll, error: globalError, } = useConfidentialBalances( { tokenAddresses: requestedAddresses }, - { enabled: isConnected && requestedAddresses.length > 0 } + { + enabled: isConnected && supportedChain && requestedAddresses.length > 0, + retry: false, + refetchOnWindowFocus: false, + }, ); - // Sync resolved balances and errors case-insensitively useEffect(() => { - if (decryptedBalances) { - const nextBalances = { ...resolvedBalances }; - const nextErrors = { ...resolvedErrors }; - - if (decryptedBalances.results) { - if (decryptedBalances.results instanceof Map) { - for (const [key, val] of decryptedBalances.results.entries()) { - if (val !== undefined && val !== null) { - nextBalances[key.toLowerCase()] = val; - // Clear error if successfully resolved now - delete nextErrors[key.toLowerCase()]; - } - } - } else { - for (const [key, val] of Object.entries(decryptedBalances.results)) { - if (val !== undefined && val !== null) { - nextBalances[key.toLowerCase()] = val as bigint; - delete nextErrors[key.toLowerCase()]; - } - } + if (!decryptedBalances) return; + setResolvedBalances((prev) => { + const next = { ...prev }; + if (decryptedBalances.results instanceof Map) { + for (const [key, val] of decryptedBalances.results.entries()) { + if (val != null) next[key.toLowerCase()] = val; + } + } else if (decryptedBalances.results) { + for (const [key, val] of Object.entries(decryptedBalances.results)) { + if (val != null) next[key.toLowerCase()] = val as bigint; } } - - if (decryptedBalances.errors) { - if (decryptedBalances.errors instanceof Map) { - for (const [key, val] of decryptedBalances.errors.entries()) { - if (val) { - nextErrors[key.toLowerCase()] = val; - } - } - } else { - for (const [key, val] of Object.entries(decryptedBalances.errors)) { - if (val) { - nextErrors[key.toLowerCase()] = val as Error; - } - } + return next; + }); + setResolvedErrors((prev) => { + const next = { ...prev }; + if (decryptedBalances.results instanceof Map) { + for (const [key] of decryptedBalances.results.entries()) delete next[key.toLowerCase()]; + } + if (decryptedBalances.errors instanceof Map) { + for (const [key, val] of decryptedBalances.errors.entries()) { + if (val) next[key.toLowerCase()] = val; } } - - setResolvedBalances(nextBalances); - setResolvedErrors(nextErrors); - } + return next; + }); }, [decryptedBalances]); - // Handle global permit signing errors useEffect(() => { - if (globalError) { - console.error('Batch decryption error:', globalError); - addToast({ - variant: 'error', - title: 'Decryption Failed', - message: globalError.message || 'The permit signature request was rejected or failed.', - }); - // Clear out requested addresses that weren't successfully resolved - setRequestedAddresses(prev => - prev.filter(addr => resolvedBalances[addr.toLowerCase()] !== undefined) - ); + if (!globalError) { lastHandledErrorMsgRef.current = null; return; } + const msg = globalError.message ?? ''; + if (msg === lastHandledErrorMsgRef.current) return; + lastHandledErrorMsgRef.current = msg; + const classified = classifyError(globalError); + const isStalePermit = + classified.title === 'Decryption Failed' || + classified.title === 'Session Expired' || + classified.title === 'Session Key Rejected' || + classified.title === 'Balance Check Unavailable'; + + if (classified.title === 'Configuration Error' || classified.title === 'Relayer Unavailable') { + setFheWorkerFailed(true); + addToast({ variant: 'error', title: classified.title, message: classified.message }); + } else if (isStalePermit && autoRevokedForMsgRef.current !== msg) { + autoRevokedForMsgRef.current = msg; + try { revokeSessionRef.current?.(); } catch { /* best-effort */ } + addToast({ variant: 'info', title: 'Session Permit Refreshed', message: 'Cached permit was stale — cleared. Click Decrypt again.' }); + } else { + addToast({ variant: 'error', title: classified.title, message: classified.message }); } - }, [globalError, resolvedBalances, addToast]); + queryClient.removeQueries({ queryKey: ['zama.confidentialBalances'] }); + queryClient.removeQueries({ queryKey: ['zama.confidentialBalance'] }); + setRequestedAddresses((prev) => + prev.filter((addr) => resolvedBalancesRef.current[addr.toLowerCase()] !== undefined), + ); + }, [globalError, addToast, queryClient]); const handleDecryptToken = (tokenAddress: `0x${string}`, symbol: string) => { - const lowerAddress = tokenAddress.toLowerCase(); - if (!requestedAddresses.some(addr => addr.toLowerCase() === lowerAddress)) { - setRequestedAddresses(prev => [...prev, tokenAddress]); - addToast({ - variant: 'info', - title: `Decrypting ${symbol}`, - message: 'Requesting decryption permit. Please sign in your wallet if prompted.', - }); + const lower = tokenAddress.toLowerCase(); + setResolvedErrors((prev) => { const n = { ...prev }; delete n[lower]; return n; }); + setFheWorkerFailed(false); + lastHandledErrorMsgRef.current = null; + queryClient.resetQueries({ queryKey: ['zama.confidentialBalances'] }); + if (!requestedAddresses.some((a) => a.toLowerCase() === lower)) { + setRequestedAddresses((prev) => [...prev, tokenAddress]); + addToast({ variant: 'info', title: `Decrypting ${symbol}`, message: 'Sign the EIP-712 permit in your wallet.' }); } }; const handleDecryptAll = () => { if (!address) return; - const allAddresses = wrappers.map(w => w.erc7984Address); + setResolvedErrors({}); + setFheWorkerFailed(false); + lastHandledErrorMsgRef.current = null; + queryClient.resetQueries({ queryKey: ['zama.confidentialBalances'] }); + // Batch includes official + custom wrapper pairs (conf-only have their own per-card hook) + const allAddresses = [...officialWrappers, ...customWrapperPairs].map((w) => w.erc7984Address); setRequestedAddresses(allAddresses); - addToast({ - variant: 'info', - title: 'Decrypting Portfolio', - message: 'Requesting batch permit signature. All assets will be decrypted in a single prompt.', - }); + addToast({ variant: 'info', title: 'Decrypting Portfolio', message: 'One batch EIP-712 permit for all assets.' }); }; - // Revoke session/clear permit signatures from the Zama SDK's cache - const { mutate: revokeSession, isPending: isRevoking } = useRevokeSession({ - onSuccess: () => { - setRequestedAddresses([]); - setResolvedBalances({}); - setResolvedErrors({}); - addToast({ - variant: 'success', - title: 'Decryption Session Reset', - message: 'All cached FHE permits have been cleared. Future decryptions will prompt for wallet signatures.', - }); - }, - onError: (err: unknown) => { - console.error('Error revoking session:', err); - const classified = classifyError(err); - addToast({ - variant: 'error', - title: classified.title, - message: classified.message, - }); - }, - }); + useEffect(() => { + revokeSessionRef.current = () => { void resetSession({ silent: true }); }; + }, [resetSession]); + + useEffect(() => { + if (resetToken === 0) return; + setRequestedAddresses([]); + setResolvedBalances({}); + setResolvedErrors({}); + setFheWorkerFailed(false); + autoRevokedForMsgRef.current = null; + lastHandledErrorMsgRef.current = null; + }, [resetToken]); const totalDecrypted = Object.keys(resolvedBalances).length; + const batchableCount = officialWrappers.length + customWrapperPairs.length; + + function rowProps(wrapper: WrapperPair) { + const lower = wrapper.erc7984Address.toLowerCase(); + return { + isDecrypted: resolvedBalances[lower] !== undefined, + decryptError: resolvedErrors[lower] || null, + isDecrypting: + requestedAddresses.some((a) => a.toLowerCase() === lower) && + resolvedBalances[lower] === undefined && + !resolvedErrors[lower], + decryptedBalance: resolvedBalances[lower], + onDecrypt: () => handleDecryptToken(wrapper.erc7984Address, wrapper.symbol), + }; + } return (
-

- -

+

- +

- {isConnected && totalDecrypted > 0 && ( - )} - {isConnected && wrappers.length > 0 && totalDecrypted < wrappers.length && ( - )}
- {/* Summary Card */} + {/* Summary */} {isConnected && totalDecrypted > 0 && ( -
- Decrypted Balances -
+
Decrypted Balances
{totalDecrypted} - - / {wrappers.length} assets decrypted - + / {batchableCount} assets decrypted
)} - {/* Wallet Not Connected State */} + {/* Not connected */} {!isConnected ? (
-
+

Connect Wallet

-

- Please connect your Web3 wallet to read on-chain balances and request cryptographic decryption permits. +

+ Connect your Web3 wallet to view and decrypt confidential balances.

- +
) : ( <> - {/* Pending unshield banners — one per wrapper token */} - {wrappers.map((w) => ( - - ))} - - {/* Token Positions Grid */} -
- {wrappers.map((wrapper) => { - const wrapperAddressLower = wrapper.erc7984Address.toLowerCase(); - const isDecrypted = resolvedBalances[wrapperAddressLower] !== undefined; - const isDecrypting = requestedAddresses.some(addr => addr.toLowerCase() === wrapperAddressLower) && !isDecrypted; - const decryptedBalance = resolvedBalances[wrapperAddressLower]; - const decryptError = resolvedErrors[wrapperAddressLower] || null; - - return ( - handleDecryptToken(wrapper.erc7984Address, wrapper.symbol)} - /> - ); - })} -
- - )} + {/* Pending unshield banners */} + {officialWrappers.map((w) => ( + + ))} + + {/* Unsupported chain */} + {!supportedChain && ( +
+ +
+
Unsupported Chain
+
Switch your wallet to Sepolia or Mainnet.
+
+
+ )} + + {/* FHE worker error */} + {fheWorkerFailed && ( +
+ +
+
Zama Relayer Unavailable
+
FHE network temporarily unreachable. Wait and try again.
+
+
+ )} + + {/* ── Official — Zama Registry ────────────────────────────────── */} + {officialWrappers.length > 0 && ( +
+
+

+ Official Registry +

+

+ Verified on-chain ERC-20 ↔ ERC-7984 wrapper pairs. Supports shield, unshield, and decrypt. +

+
+
+ {officialWrappers.map((wrapper) => ( + + ))} +
+
+ )} + + {/* ── Custom wrapper pairs ────────────────────────────────────── */} + {customWrapperPairs.length > 0 && ( +
+
+ +
+

+ Custom Wrappers +

+

+ Locally-added wrapper pairs. Supports shield, unshield, and decrypt. +

+
+
+
+ {customWrapperPairs.map((wrapper) => ( + + ))} +
+
+ )} + + {/* ── Custom confidential-only tokens ────────────────────────── */} + {customConfOnly.length > 0 && ( +
+
+ +
+

+ Custom Decrypt-Only +

+

+ Confidential tokens with no ERC-20 underlying. Decrypt only — no shield/unshield. +

+
+
+
+ {customConfOnly.map((wrapper) => ( + + ))} +
+
+ )} - {/* Empty State */} - {isConnected && wrappers.length === 0 && ( -
-
- -
-

No Registered Tokens

-

- There are no wrappers registered on this chain yet. Try switching to Sepolia. -

-
+ {/* No tokens at all */} + {officialWrappers.length === 0 && customWrapperPairs.length === 0 && customConfOnly.length === 0 && ( +
+
+ +
+

No Tokens

+

No registered tokens on this chain. Try switching to Sepolia.

+
+ )} + )} - {/* Wallet Activity Feed */} - {isConnected && address && wrappers.length > 0 && ( - + {/* Activity feed */} + {isConnected && address && supportedChain && allWrappers.length > 0 && ( + )} {/* Info */}
-
- +
+
- - Decrypting balances requires an EIP-712 signature (permit) to verify you are the account owner. - This creates an ephemeral session key that decrypts the on-chain ciphertext handle. - Your private key never leaves your device, and cleartext balances are never transmitted. - Permits are securely cached in your browser; use the Reset Session button to clear cached permits and force wallet signature prompts. + + Decrypting balances requires an EIP-712 permit — a read-only off-chain signature that authorises the Zama Gateway to decrypt your balance for this session. + Your private key never leaves your device. Use Reset Session to clear cached permits and force fresh signatures.
- {/* Connect Wallet Modal */} + {/* Connect modal */} {isConnectModalOpen && ( - setIsConnectModalOpen(false)} - title="Connect Wallet" - > + setIsConnectModalOpen(false)}>
Select a wallet:
{connectors.map((c) => ( - ))} diff --git a/src/app/app/transfer/page.tsx b/src/app/app/transfer/page.tsx new file mode 100644 index 0000000..c78e51d --- /dev/null +++ b/src/app/app/transfer/page.tsx @@ -0,0 +1,894 @@ +'use client'; + +/** + * Transfer page — /app/transfer + * + * Two modes: + * - Confidential: ERC-7984 transfer with FHE-encrypted amount (Zama SDK). + * - Standard: plain ERC-20 transfer via wagmi writeContract. + * + * Confidential path: docs.zama.org/protocol/sdk/api-references/react/useconfidentialtransfer + * Installed 3.0.1 uses { tokenAddress } config shape (verified against .d.ts). + */ + +import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react'; +import { useAccount, useConnect, useReadContract, useWriteContract, useWaitForTransactionReceipt, usePublicClient } from 'wagmi'; +import { useConfidentialTransfer, useConfidentialBalance } from '@zama-fhe/react-sdk'; +import { isAddress, formatUnits } from 'viem'; + +import Card from '@/components/ui/Card'; +import Button from '@/components/ui/Button'; +import Badge from '@/components/ui/Badge'; +import TokenIcon from '@/components/ui/TokenIcon'; +import TokenSelect, { type TokenSelectGroup } from '@/components/ui/TokenSelect'; +import { useToast } from '@/components/ui/Toast'; +import TransactionSuccessModal from '@/components/ui/TransactionSuccessModal'; +import { useActiveNetwork } from '@/app/ClientLayout'; +import { useRegistryPairs } from '@/lib/registry'; +import { useWalletErc7984Scan } from '@/lib/use-wallet-scan'; +import { useSessionReset } from '@/lib/reset-session'; +import { parseAmount, formatAmount, formatAddress } from '@/lib/utils'; +import { classifyError } from '@/lib/errors'; +import { ERC20_ABI } from '@/lib/wrapper-abi'; +import { CHAIN_CONFIG, type SupportedChainId } from '@/config/chains'; +import { + Send, + ShieldCheck, + Lock, + ArrowRight, + Wallet, + ExternalLink, + Zap, + Shield, + Unlock, + Clock, + AlertTriangle, + CheckCircle2, + Loader2, + X, +} from 'lucide-react'; + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as const; +const RECENT_KEY = 'shadowline-recent-recipients'; +const MAX_RECENT = 5; + +type TransferMode = 'confidential' | 'standard'; +type Step = 'idle' | 'encrypting' | 'submitting' | 'confirming' | 'done'; + +function loadRecents(): string[] { + try { + return JSON.parse(localStorage.getItem(RECENT_KEY) ?? '[]') as string[]; + } catch { + return []; + } +} +function safeParse(value: string, dec: number): bigint { + if (!value) return 0n; + try { return parseAmount(value, dec); } catch { return 0n; } +} +function saveRecent(addr: string) { + try { + const prev = loadRecents().filter((a) => a.toLowerCase() !== addr.toLowerCase()); + localStorage.setItem(RECENT_KEY, JSON.stringify([addr, ...prev].slice(0, MAX_RECENT))); + } catch { /* best-effort */ } +} + +function StepIndicator({ step }: { step: Step }) { + const steps: { id: Step; label: string }[] = [ + { id: 'encrypting', label: 'Encrypt' }, + { id: 'submitting', label: 'Submit' }, + { id: 'confirming', label: 'Confirm' }, + ]; + const activeIdx = steps.findIndex((s) => s.id === step); + + if (step === 'idle' || step === 'done') return null; + + return ( +
+ {steps.map((s, i) => { + const isDone = i < activeIdx; + const isActive = i === activeIdx; + return ( + +
+ {isDone ? ( + + ) : isActive ? ( + + ) : ( +
+ )} + {s.label} +
+ {i < steps.length - 1 && ( +
+ )} + + ); + })} +
+ ); +} + +export default function TransferPage() { + const { address, isConnected } = useAccount(); + const { connect, connectors } = useConnect(); + const { addToast } = useToast(); + const { activeChainId } = useActiveNetwork(); + const chainConfig = CHAIN_CONFIG[activeChainId]; + const publicClient = usePublicClient({ chainId: activeChainId as SupportedChainId }); + + const { pairs, isLoading: isRegistryLoading } = useRegistryPairs(activeChainId); + + const transferablePairs = useMemo( + () => pairs.filter((p) => p.isValid !== false), + [pairs], + ); + + // Auto-detected ERC-7984 tokens outside the registry + const registryAddresses = useMemo( + () => new Set(transferablePairs.map((p) => p.erc7984Address.toLowerCase())), + [transferablePairs], + ); + const { extra: extraTokens } = useWalletErc7984Scan(address, publicClient, registryAddresses); + + const [mode, setMode] = useState('confidential'); + const [selectedSymbol, setSelectedSymbol] = useState(''); + const selectedPair = useMemo( + () => transferablePairs.find((p) => p.symbol === selectedSymbol), + [transferablePairs, selectedSymbol], + ); + + // For auto-detected extra tokens not in registry + const selectedExtra = useMemo( + () => extraTokens.find((t) => t.address === selectedSymbol), + [extraTokens, selectedSymbol], + ); + + const [recipient, setRecipient] = useState(''); + const [amount, setAmount] = useState(''); + const [step, setStep] = useState('idle'); + const [finalTxHash, setFinalTxHash] = useState(undefined); + const [isSuccessOpen, setIsSuccessOpen] = useState(false); + + // Recent recipients + const [recents, setRecents] = useState([]); + useEffect(() => { setRecents(loadRecents()); }, []); + + // Confidential balance decrypt gate + const [decryptRequested, setDecryptRequested] = useState(false); + + // App-wide session reset — re-arm the Reveal button so the next click + // prompts for a fresh EIP-712 signature (IndexedDB is empty after reset). + const { resetToken } = useSessionReset(); + useEffect(() => { + if (resetToken > 0) setDecryptRequested(false); + }, [resetToken]); + + // Reset on token/mode change + const handleModeSwitch = (next: TransferMode) => { + setMode(next); + setSelectedSymbol(''); + setAmount(''); + setRecipient(''); + setStep('idle'); + setDecryptRequested(false); + }; + const handleTokenChange = (sym: string) => { + setSelectedSymbol(sym); + setAmount(''); + setDecryptRequested(false); + }; + + // Which address are we transferring from/to? + const erc7984Addr = selectedPair?.erc7984Address ?? selectedExtra?.address ?? ZERO_ADDRESS; + const erc20Addr = selectedPair?.erc20Address ?? ZERO_ADDRESS; + const wrapperDecimals = selectedPair?.wrapperDecimals ?? selectedExtra?.decimals ?? 6; + const underlyingDecimals = selectedPair?.decimals ?? 18; + + const decimals = mode === 'confidential' ? wrapperDecimals : underlyingDecimals; + const symbolDisplay = mode === 'confidential' + ? `c${selectedPair?.symbol ?? selectedExtra?.symbol ?? '…'}` + : (selectedPair?.symbol ?? '…'); + + // ── Confidential balance (decrypt-gated) ──────────────────────────────────── + // retry: false — a rejected permit signature must NOT re-prompt the wallet. + const { data: confBalRaw, isLoading: isDecrypting, error: confBalError, refetch: refetchConfBalance } = useConfidentialBalance( + { tokenAddress: erc7984Addr as `0x${string}` }, + { + enabled: decryptRequested && !!address && erc7984Addr !== ZERO_ADDRESS, + retry: false, + refetchOnWindowFocus: false, + }, + ); + + // Fire-once: on decrypt error (incl. signature rejection), disable the query + // and show one toast. The "Reveal balance" button re-arms itself. + const confBalErrorRef = useRef(null); + useEffect(() => { + if (!confBalError) { + confBalErrorRef.current = null; + return; + } + const msg = confBalError.message ?? ''; + if (confBalErrorRef.current === msg) return; + confBalErrorRef.current = msg; + setDecryptRequested(false); + const classified = classifyError(confBalError); + addToast({ variant: 'warning', title: classified.title, message: classified.message }); + }, [confBalError, addToast]); + const confBalance = confBalRaw != null + ? formatUnits(BigInt(confBalRaw), wrapperDecimals) + : null; + + // ── ERC-20 balance (standard mode, no gating needed) ─────────────────────── + const { data: erc20BalanceRaw } = useReadContract({ + address: erc20Addr as `0x${string}`, + abi: ERC20_ABI, + functionName: 'balanceOf', + args: [address!], + query: { enabled: !!address && erc20Addr !== ZERO_ADDRESS && mode === 'standard' }, + }); + const erc20Balance = erc20BalanceRaw != null + ? formatAmount(erc20BalanceRaw as bigint, underlyingDecimals) + : null; + + // ── Recipient validation ───────────────────────────────────────────────────── + const recipientTrimmed = recipient.trim(); + const recipientIsValidAddr = isAddress(recipientTrimmed); + const recipientIsZero = recipientTrimmed === ZERO_ADDRESS; + const recipientIsSelf = recipientTrimmed.toLowerCase() === address?.toLowerCase(); + const [isContract, setIsContract] = useState(null); + const contractCheckRef = useRef | null>(null); + + useEffect(() => { + setIsContract(null); + if (!recipientIsValidAddr || !publicClient) return; + if (contractCheckRef.current) clearTimeout(contractCheckRef.current); + contractCheckRef.current = setTimeout(() => { + publicClient.getCode({ address: recipientTrimmed as `0x${string}` }).then((code) => { + setIsContract(code != null && code !== '0x' && code.length > 2); + }).catch(() => setIsContract(null)); + }, 500); + return () => { if (contractCheckRef.current) clearTimeout(contractCheckRef.current); }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [recipientTrimmed, recipientIsValidAddr]); + + const recipientError = + recipientTrimmed && !recipientIsValidAddr ? 'Not a valid Ethereum address.' : + recipientIsZero ? 'Cannot send to the zero address.' : + recipientIsSelf ? 'Cannot send to yourself.' : + null; + const recipientWarning = !recipientError && isContract + ? 'This looks like a contract. Confidential tokens sent to a contract without ERC-7984 support may be permanently locked.' + : null; + + const parsedAmount = safeParse(amount, decimals); + + // Insufficient balance checks + const confBalBigint = confBalance != null ? parseAmount(confBalance, wrapperDecimals) : null; + const erc20BalBigint = erc20BalanceRaw as bigint | null ?? null; + const isConfInsufficient = mode === 'confidential' && confBalBigint != null && parsedAmount > 0n && parsedAmount > confBalBigint; + const isStdInsufficient = mode === 'standard' && erc20BalBigint != null && parsedAmount > 0n && parsedAmount > erc20BalBigint; + const isInsufficient = isConfInsufficient || isStdInsufficient; + + const hasToken = !!selectedPair || !!selectedExtra; + const isRecipientValid = recipientIsValidAddr && !recipientIsZero && !recipientIsSelf; + const isAmountValid = parsedAmount > 0n; + const canSubmit = + isConnected && + hasToken && + isRecipientValid && + isAmountValid && + !isInsufficient && + step === 'idle'; + + // Readable reason for disabled button + const disabledReason: string | null = !isConnected + ? 'Connect a wallet' + : !hasToken + ? 'Select a token' + : !isRecipientValid + ? 'Enter a valid recipient' + : !isAmountValid + ? 'Enter an amount' + : isInsufficient + ? 'Insufficient balance' + : step !== 'idle' + ? 'Transfer in progress…' + : null; + + // ── Confidential transfer ─────────────────────────────────────────────────── + const isPending = step !== 'idle' && step !== 'done'; + const { mutateAsync: transfer } = useConfidentialTransfer({ + tokenAddress: erc7984Addr as `0x${string}`, + }); + + // ── Standard ERC-20 transfer ──────────────────────────────────────────────── + const { writeContractAsync } = useWriteContract(); + const [pendingTxHash, setPendingTxHash] = useState(undefined); + useWaitForTransactionReceipt({ + hash: pendingTxHash as `0x${string}` | undefined, + query: { enabled: !!pendingTxHash }, + }); + + const explorerBase = chainConfig?.explorerUrl ?? 'https://etherscan.io'; + + const handleConfidentialTransfer = useCallback(async () => { + if (!canSubmit || !hasToken) return; + try { + setStep('encrypting'); + const res = await transfer({ + to: recipientTrimmed as `0x${string}`, + amount: parsedAmount, + onEncryptComplete: () => { + setStep('submitting'); + addToast({ variant: 'info', title: 'Amount Encrypted', message: 'Submitting to the network…' }); + }, + onTransferSubmitted: (hash) => { + setStep('confirming'); + setPendingTxHash(hash); + addToast({ variant: 'info', title: 'Transfer Submitted', message: 'Waiting for on-chain confirmation…' }); + }, + }); + saveRecent(recipientTrimmed); + setRecents(loadRecents()); + setFinalTxHash(res.txHash); + setIsSuccessOpen(true); + setStep('done'); + addToast({ + variant: 'success', + title: 'Confidential Transfer Confirmed', + message: `Sent ${amount} ${symbolDisplay} to ${formatAddress(recipientTrimmed)}.`, + }); + // Note: amount/recipient are cleared in modal onClose so the modal + // can display the correct sent amount (React batches these state updates). + } catch (err: unknown) { + console.error('Confidential transfer failed:', err); + const classified = classifyError(err); + addToast({ variant: classified.retryable ? 'warning' : 'error', title: classified.title, message: classified.message }); + setStep('idle'); + } + }, [canSubmit, hasToken, transfer, recipientTrimmed, parsedAmount, amount, symbolDisplay, addToast]); + + const handleStandardTransfer = useCallback(async () => { + if (!canSubmit || !selectedPair) return; + try { + setStep('submitting'); + addToast({ variant: 'info', title: 'Confirm in wallet', message: 'Approve the ERC-20 transfer in your wallet.' }); + const hash = await writeContractAsync({ + address: selectedPair.erc20Address as `0x${string}`, + abi: ERC20_ABI, + functionName: 'transfer', + args: [recipientTrimmed as `0x${string}`, parsedAmount], + }); + setStep('confirming'); + setPendingTxHash(hash); + addToast({ variant: 'info', title: 'Transfer Submitted', message: 'Waiting for confirmation…' }); + saveRecent(recipientTrimmed); + setRecents(loadRecents()); + setFinalTxHash(hash); + setIsSuccessOpen(true); + setStep('done'); + addToast({ + variant: 'success', + title: 'ERC-20 Transfer Confirmed', + message: `Sent ${amount} ${selectedPair.symbol} to ${formatAddress(recipientTrimmed)}.`, + }); + // Note: amount/recipient are cleared in modal onClose (see above). + } catch (err: unknown) { + console.error('ERC-20 transfer failed:', err); + const classified = classifyError(err); + addToast({ variant: classified.retryable ? 'warning' : 'error', title: classified.title, message: classified.message }); + setStep('idle'); + } + }, [canSubmit, selectedPair, writeContractAsync, recipientTrimmed, parsedAmount, amount, addToast]); + + const rawModalSym = selectedPair?.symbol ?? selectedExtra?.symbol ?? ''; + const tokenSymbolForModal = mode === 'confidential' + ? (selectedPair?.isWrapper === false || rawModalSym.startsWith('c') ? rawModalSym : `c${rawModalSym}`) + : rawModalSym; + + const isConfMode = mode === 'confidential'; + + const tokenGroups = useMemo(() => { + const official = transferablePairs.filter((p) => p.source !== 'custom' && (isConfMode || p.isWrapper !== false)); + const custom = transferablePairs.filter((p) => p.source === 'custom' && (isConfMode || p.isWrapper !== false)); + + const groups: TokenSelectGroup[] = []; + if (official.length > 0) { + groups.push({ + label: 'Official Registry', + options: official.map((p) => { + const sym = isConfMode ? (p.isWrapper === false || p.symbol.startsWith('c') ? p.symbol : `c${p.symbol}`) : p.symbol; + return { + value: p.symbol, + symbol: sym, + name: p.name, + iconSymbol: p.symbol, + address: p.erc7984Address, + }; + }), + }); + } + if (custom.length > 0) { + groups.push({ + label: 'Custom / Dev-only', + options: custom.map((p) => { + const sym = isConfMode ? (p.isWrapper === false || p.symbol.startsWith('c') ? p.symbol : `c${p.symbol}`) : p.symbol; + return { + value: p.symbol, + symbol: sym, + name: p.name, + iconSymbol: p.symbol, + badge: { text: 'Custom', variant: 'accent' }, + address: p.erc7984Address, + }; + }), + }); + } + if (extraTokens.length > 0 && isConfMode) { + groups.push({ + label: 'Detected (unverified)', + options: extraTokens.map((t) => ({ + value: t.address, + symbol: t.symbol, + name: t.name, + iconSymbol: t.symbol, + badge: { text: 'Unverified', variant: 'warning' }, + address: t.address, + })), + }); + } + return groups; + }, [transferablePairs, extraTokens, isConfMode]); + + const noTokensAvailable = + !isRegistryLoading && + transferablePairs.length === 0 && + extraTokens.length === 0 && + isConnected; + + return ( +
+ {/* Header */} +
+
+ +

+ Transfer +

+ + FHE + +
+

+ Send tokens to any recipient. Choose Confidential (ERC-7984, amount encrypted) + or Standard (plain ERC-20 transfer). +

+
+ + {/* Mode tabs */} +
+ + +
+ + {/* Mode description */} + {isConfMode ? ( +
+ + The amount is encrypted client-side via FHE before submission. + On-chain observers see who sent to whom, but never the value. + Gas cost is higher than a standard ERC-20 transfer. +
+ ) : ( +
+ + Standard ERC-20 transfer — amount and recipient are fully public on-chain. + Uses the underlying token, not the confidential wrapper. +
+ )} + + {!isConnected ? ( + +
+ +
+
Connect a wallet to continue
+
+ {isConfMode + ? 'Confidential transfers require a signer to encrypt the amount.' + : 'Connect your wallet to sign the ERC-20 transfer.'} +
+
+
+ {connectors.map((c) => ( + + ))} +
+
+
+ ) : noTokensAvailable ? ( + +
+ +
+
No confidential tokens detected
+
+ Your wallet doesn't hold any ERC-7984 tokens yet. + Shield some ERC-20 tokens first, or get test tokens from the Faucet. +
+
+ +
+
+ ) : ( + + {/* Step indicator (confidential only) */} + {isConfMode && } + + {/* Token select */} + + + {/* Recipient */} +
+
Recipient address
+ {/* Recent recipient chips */} + {recents.length > 0 && ( +
+ + Recent: + + {recents.map((r) => ( + + ))} +
+ )} + setRecipient(e.target.value.trim())} + disabled={isPending} + spellCheck={false} + style={{ + width: '100%', + padding: 'var(--sp-2) var(--sp-3)', + borderRadius: 'var(--radius-md)', + border: `1px solid ${recipientError ? 'var(--error)' : 'var(--border)'}`, + background: 'var(--bg-elevated)', + color: 'var(--text-primary)', + fontFamily: 'var(--font-mono, monospace)', + fontSize: 'var(--text-sm)', + }} + /> + {recipientError && ( +
+ {recipientError} +
+ )} + {recipientWarning && ( +
+ + {recipientWarning} +
+ )} +
+ + {/* Amount */} +
+
+ Amount + {hasToken && ( + + {isConfMode ? ( + decryptRequested && confBalance !== null ? ( + <> + Available:  + + {confBalance} {symbolDisplay} + + + + ) : ( + + ) + ) : erc20Balance !== null ? ( + <> + Available:  + + {erc20Balance} {selectedPair?.symbol ?? ''} + + + + ) : null} + + )} +
+ setAmount(e.target.value.replace(/[^0-9.]/g, ''))} + disabled={!hasToken || isPending} + style={{ + width: '100%', + padding: 'var(--sp-3) var(--sp-4)', + borderRadius: 'var(--radius-md)', + border: `1px solid ${isInsufficient ? 'var(--error)' : 'var(--border)'}`, + background: 'var(--bg-elevated)', + color: 'var(--text-primary)', + fontSize: 'var(--text-xl)', + fontFamily: 'var(--font-mono, monospace)', + }} + /> + {isInsufficient && ( +
+ Amount exceeds your available balance. +
+ )} +
+ + {/* CTA */} + + + {/* Disabled reason */} + {!canSubmit && !isPending && disabledReason && ( +
+ {disabledReason} +
+ )} + + {pendingTxHash && ( + + )} +
+ )} + + {finalTxHash && (selectedPair || selectedExtra) && ( + { + setIsSuccessOpen(false); + setFinalTxHash(undefined); + setPendingTxHash(undefined); + setStep('idle'); + setAmount(''); + setRecipient(''); + }} + action="transfer" + amount={amount || '0'} + tokenSymbol={tokenSymbolForModal} + txHash={finalTxHash} + /> + )} +
+ ); +} diff --git a/src/app/app/wrap/page.tsx b/src/app/app/wrapper/page.tsx similarity index 68% rename from src/app/app/wrap/page.tsx rename to src/app/app/wrapper/page.tsx index dc93151..9b7fc3f 100644 --- a/src/app/app/wrap/page.tsx +++ b/src/app/app/wrapper/page.tsx @@ -1,39 +1,49 @@ 'use client'; -import React, { useState, useMemo, useEffect, Suspense } from 'react'; +import React, { useState, useMemo, useEffect, useRef, Suspense } from 'react'; import { useSearchParams } from 'next/navigation'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import Badge from '@/components/ui/Badge'; import Modal from '@/components/ui/Modal'; import TokenIcon from '@/components/ui/TokenIcon'; +import TokenSelect, { type TokenSelectGroup } from '@/components/ui/TokenSelect'; +import WalletActivityFeed from '@/components/WalletActivityFeed'; import { formatAddress, formatAmount, parseAmount } from '@/lib/utils'; import { classifyError } from '@/lib/errors'; import PendingUnshieldBanner from '@/components/PendingUnshieldBanner'; import { useActiveNetwork } from '@/app/ClientLayout'; import { useRegistryPairs, findPairBySymbol } from '@/lib/registry'; import { useToast } from '@/components/ui/Toast'; +import { useSessionReset } from '@/lib/reset-session'; import { useAccount, useReadContract, useConnect, useSwitchChain, + usePublicClient, + useWriteContract, } from 'wagmi'; -import { useConfidentialBalance, useShield, useUnshield } from '@zama-fhe/react-sdk'; -import { ERC20_ABI } from '@/lib/wrapper-abi'; +import { + useConfidentialBalance, + useShield, + useUnshield, + useZamaSDK, + savePendingUnshield, + clearPendingUnshield, +} from '@zama-fhe/react-sdk'; +import { ERC20_ABI, WRAPPER_ABI } from '@/lib/wrapper-abi'; +import { isAddress } from 'viem'; import BlurIn from '@/components/ui/BlurIn'; import TypingAnimation from '@/components/ui/TypingAnimation'; -import confetti from 'canvas-confetti'; import { CHAIN_CONFIG } from '@/config/chains'; import TransactionSuccessModal from '@/components/ui/TransactionSuccessModal'; import { - Shield, ArrowUpDown, Lock, Unlock, Check, Info, - Wallet, ExternalLink, AlertCircle, } from 'lucide-react'; @@ -99,19 +109,21 @@ function ConfidentialBalanceInline({ ); } - // Default: explicit decrypt button — never auto-fires + // Default: explicit decrypt button — never auto-fires. Neutral gray palette + // so it doesn't compete with the primary Shield/Unshield CTA (which owns the + // accent color). return (
- {/* Pending unshield banner for the currently selected token */} - {selectedWrapper && ( -
- + {/* Pending unshield banners — one per wrapper; each self-hides if nothing is pending */} + {isConnected && ( +
+ {wrappers.map((w) => ( + + ))}
)} @@ -410,7 +634,7 @@ function WrapPageContent() { isDecrypting={isDecryptingWrapper} error={decryptWrapperError} wrapperDecimals={wrapperDecimals} - onDecrypt={() => { setDecryptRequested(true); refetchWrapperBalance(); }} + onDecrypt={() => setDecryptRequested(true)} /> )} @@ -431,37 +655,20 @@ function WrapPageContent() { {/* Token Display / Selector */}
- {selectedToken && } - + groups={wrapperGroups} + placeholder="Select Token" + disabled={txStep > 0} + size="sm" + style={{ minWidth: '160px' }} + />
@@ -543,7 +750,7 @@ function WrapPageContent() { isDecrypting={isDecryptingWrapper} error={decryptWrapperError} wrapperDecimals={wrapperDecimals} - onDecrypt={() => { setDecryptRequested(true); refetchWrapperBalance(); }} + onDecrypt={() => setDecryptRequested(true)} /> )} @@ -562,14 +769,19 @@ function WrapPageContent() {
{selectedToken && (
- + {/* Display the resolved symbol from selectedWrapper — never + the raw ?token= address (that's how "c0xfF89…" leaked into + the UI when navigated from a custom row). */} + {action === 'wrap' && ( )} - {action === 'wrap' ? `c${selectedToken}` : selectedToken} + {selectedWrapper + ? (action === 'wrap' ? `c${selectedWrapper.symbol}` : selectedWrapper.symbol) + : 'Loading…'}
)} @@ -625,7 +837,7 @@ function WrapPageContent() { {/* Primary Action Button — ALWAYS in this slot */}
{!isConnected ? ( - ) : isChainMismatch ? ( @@ -659,8 +871,9 @@ function WrapPageContent() { isLoading={txStep === 1 || txStep === 2} disabled={!selectedToken || !amount || amount === '0' || parsedInputAmount > hasPublicBalance} onClick={handleAction} + className="btn-primary-black" > - {parsedInputAmount > hasPublicBalance ? 'Insufficient Balance' : `Approve & Shield ${selectedToken}`} + {parsedInputAmount > hasPublicBalance ? 'Insufficient Balance' : `Approve & Shield ${selectedWrapper?.symbol ?? ''}`} ) : ( )}
@@ -775,6 +989,18 @@ function WrapPageContent() {
+ + {/* Compact wallet activity feed */} + {isConnected && address && wrappers.length > 0 && ( + + )}
{/* Connect Wallet Modal */} @@ -814,7 +1040,7 @@ function WrapPageContent() { }} action={action} amount={amount} - tokenSymbol={selectedToken} + tokenSymbol={selectedWrapper?.symbol ?? selectedToken} txHash={finalTxHash} /> )} diff --git a/src/app/globals.css b/src/app/globals.css index 65b2db3..867d95e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,5 +1,5 @@ /* ========================================================================== - ZAMAVAULT — PREMIUM UNIVERSAL DESIGN SYSTEM + SHADOWLINE — PREMIUM UNIVERSAL DESIGN SYSTEM Multi-Theme Design Directions: Cyber, Nebula, Nordic, Emerald ========================================================================== */ @@ -15,7 +15,7 @@ ul, ol { list-style: none; } input, select, textarea { font: inherit; border: none; outline: none; background: none; } table { border-collapse: collapse; } -/* ---------- Global Tokens & Defaults (Nordic Charcoal Default) ---------- */ +/* ---------- Global Tokens & Defaults (Nordic Midnight Default) ---------- */ :root { /* Layout & Defaults */ --font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif; @@ -53,7 +53,65 @@ table { border-collapse: collapse; } --sp-16: 64px; --sp-20: 80px; - /* Nordic Charcoal (Default) variables */ + /* Nordic Midnight (Default - Clean Black & White) */ + --bg-base: #000000; + --bg-surface: #09090b; + --bg-elevated: #141418; + --bg-card: rgba(20, 20, 24, 0.95); + --bg-card-hover: rgba(30, 30, 36, 0.98); + --bg-input: #000000; + + --accent: #f4f4f5; + --accent-hover: #ffffff; + --accent-muted: rgba(244, 244, 245, 0.18); + --accent-subtle: rgba(244, 244, 245, 0.04); + --accent-glow: rgba(255, 255, 255, 0.1); + --zama-gold: #FFD208; + --zama-gold-glow: rgba(255, 210, 8, 0.15); + + --text-primary: #ffffff; + --text-secondary: #e4e4e7; + --text-muted: #71717a; + --text-accent: #ffffff; + --text-inverse: #000000; + + --success: #10b981; + --success-muted: rgba(16, 185, 129, 0.1); + --warning: #f59e0b; + --warning-muted: rgba(245, 158, 11, 0.1); + --error: #ef4444; + --error-muted: rgba(239, 68, 68, 0.1); + --info: #3b82f6; + --info-muted: rgba(59, 130, 246, 0.1); + + --border: rgba(255, 255, 255, 0.35); + --border-hover: rgba(255, 255, 255, 0.55); + --border-accent: rgba(255, 255, 255, 0.8); + --border-focus: #ffffff; + + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 18px; + --radius-2xl: 24px; + --radius-full: 9999px; + + --shadow-sm: 0 2px 4px rgba(0,0,0,0.6); + --shadow-md: 0 6px 18px rgba(0,0,0,0.7); + --shadow-lg: 0 20px 48px rgba(0,0,0,0.9); + --shadow-glow: 0 0 25px rgba(255, 255, 255, 0.08); + + --radial-glow-1: rgba(255, 255, 255, 0.02); + --radial-glow-2: rgba(255, 255, 255, 0.01); + --grid-line: rgba(255, 255, 255, 0.022); +} + +/* ========================================================================== + THEME DIRECTIONS + ========================================================================== */ + +/* ---------- 1. NORDIC CHARCOAL THEME (DARK - BLUE ACCENT) ---------- */ +html[data-design-theme='charcoal'] { --bg-base: #0b0b0f; --bg-surface: #121217; --bg-elevated: #1a1a22; @@ -66,8 +124,6 @@ table { border-collapse: collapse; } --accent-muted: rgba(56, 189, 248, 0.18); --accent-subtle: rgba(56, 189, 248, 0.05); --accent-glow: rgba(56, 189, 248, 0.2); - --zama-gold: #FFD208; - --zama-gold-glow: rgba(255, 210, 8, 0.15); --text-primary: #f8f9fa; --text-secondary: #a0a5b5; @@ -75,15 +131,6 @@ table { border-collapse: collapse; } --text-accent: #38bdf8; --text-inverse: #0b0b0f; - --success: #10b981; - --success-muted: rgba(16, 185, 129, 0.1); - --warning: #f59e0b; - --warning-muted: rgba(245, 158, 11, 0.1); - --error: #ef4444; - --error-muted: rgba(239, 68, 68, 0.1); - --info: #3b82f6; - --info-muted: rgba(59, 130, 246, 0.1); - --border: rgba(255, 255, 255, 0.3); --border-hover: rgba(255, 255, 255, 0.45); --border-accent: rgba(56, 189, 248, 0.6); @@ -94,67 +141,20 @@ table { border-collapse: collapse; } --radius-lg: 14px; --radius-xl: 20px; --radius-2xl: 26px; - --radius-full: 9999px; --shadow-sm: 0 2px 4px rgba(0,0,0,0.5); --shadow-md: 0 4px 14px rgba(0,0,0,0.6); --shadow-lg: 0 16px 36px rgba(0,0,0,0.8); --shadow-glow: 0 0 20px rgba(56, 189, 248, 0.15); - + --radial-glow-1: rgba(56, 189, 248, 0.04); --radial-glow-2: rgba(255, 255, 255, 0.02); --grid-line: rgba(255, 255, 255, 0.012); } -/* ========================================================================== - THEME DIRECTIONS - ========================================================================== */ - -/* ---------- 1. NORDIC CHARCOAL THEME (DARK - DEFAULT) ---------- */ -html[data-design-theme='charcoal'] { - /* Inherits directly from :root */ -} - -/* ---------- 2. NORDIC MIDNIGHT THEME (DARK) ---------- */ +/* ---------- 2. NORDIC MIDNIGHT THEME (DARK - DEFAULT CLEAN) ---------- */ html[data-design-theme='midnight'] { - --bg-base: #000000; - --bg-surface: #09090b; - --bg-elevated: #141418; - --bg-card: rgba(20, 20, 24, 0.95); - --bg-card-hover: rgba(30, 30, 36, 0.98); - --bg-input: #000000; - - --accent: #f4f4f5; - --accent-hover: #ffffff; - --accent-muted: rgba(244, 244, 245, 0.18); - --accent-subtle: rgba(244, 244, 245, 0.04); - --accent-glow: rgba(255, 255, 255, 0.1); - - --text-primary: #ffffff; - --text-secondary: #e4e4e7; - --text-muted: #71717a; - --text-accent: #ffffff; - --text-inverse: #000000; - - --border: rgba(255, 255, 255, 0.35); - --border-hover: rgba(255, 255, 255, 0.55); - --border-accent: rgba(255, 255, 255, 0.8); - --border-focus: #ffffff; - - --radius-sm: 4px; - --radius-md: 8px; - --radius-lg: 12px; - --radius-xl: 18px; - --radius-2xl: 24px; - - --shadow-sm: 0 2px 4px rgba(0,0,0,0.6); - --shadow-md: 0 6px 18px rgba(0,0,0,0.7); - --shadow-lg: 0 20px 48px rgba(0,0,0,0.9); - --shadow-glow: 0 0 25px rgba(255, 255, 255, 0.08); - - --radial-glow-1: rgba(255, 255, 255, 0.02); - --radial-glow-2: rgba(255, 255, 255, 0.01); - --grid-line: rgba(255, 255, 255, 0.022); + /* Inherits directly from :root - Clean black & white Nordic design */ } /* ---------- 3. NORDIC FROST THEME (DARK) ---------- */ @@ -243,38 +243,38 @@ html[data-design-theme='aurora'] { /* ---------- 5. LIGHT MODE OVERRIDES ---------- */ html[data-theme='light'] { - --bg-base: #f4f4f5; + --bg-base: #fafafa; --bg-surface: #ffffff; - --bg-elevated: #e4e4e7; + --bg-elevated: #f4f4f5; --bg-card: #ffffff; --bg-card-hover: #ffffff; --bg-input: #ffffff; - --accent: #09090b; - --accent-hover: #18181b; - --accent-muted: rgba(9, 9, 11, 0.08); - --accent-subtle: rgba(9, 9, 11, 0.03); - --accent-glow: rgba(9, 9, 11, 0.04); - - --text-primary: #09090b; - --text-secondary: #3f3f46; - --text-muted: #71717a; - --text-accent: #09090b; - --text-inverse: #ffffff; + --accent: #FFD208; + --accent-hover: #e0b806; + --accent-muted: rgba(255, 210, 8, 0.15); + --accent-subtle: rgba(255, 210, 8, 0.04); + --accent-glow: rgba(255, 210, 8, 0.2); + + --text-primary: #000000; + --text-secondary: #27272a; + --text-muted: #52525b; + --text-accent: #000000; + --text-inverse: #000000; - --border: #d4d4d8; + --border: #e4e4e7; --border-hover: #a1a1aa; - --border-accent: rgba(9, 9, 11, 0.4); - --border-focus: #09090b; + --border-accent: #FFD208; + --border-focus: #000000; --shadow-sm: 0 1px 3px rgba(0,0,0,0.05); --shadow-md: 0 4px 10px rgba(0,0,0,0.06); --shadow-lg: 0 10px 25px rgba(0,0,0,0.08); - --shadow-glow: 0 0 12px rgba(9, 9, 11, 0.02); + --shadow-glow: 0 0 16px rgba(255, 210, 8, 0.18); - --radial-glow-1: rgba(9, 9, 11, 0.01); - --radial-glow-2: rgba(113, 128, 150, 0.01); - --grid-line: rgba(0, 0, 0, 0.025); + --radial-glow-1: rgba(255, 210, 8, 0.03); + --radial-glow-2: rgba(0, 0, 0, 0.01); + --grid-line: rgba(0, 0, 0, 0.015); } /* ========================================================================== @@ -876,6 +876,53 @@ h4 { font-size: var(--text-xl); } gap: var(--sp-2); } +/* ---------- Registry pair cards ---------- */ +/* Each token pair is a single rounded/bordered card holding exactly two rows + (public token, confidential wrapper) laid out on the same grid as the column + header bar above the list. Reuses existing tokens only — same border, radius, + surface, and spacing scale as the rest of the app; no new palette. */ +.registry-pair-columns { + grid-template-columns: minmax(210px, 1.7fr) minmax(170px, 1.3fr) 90px minmax(170px, 1.2fr) minmax(220px, 1fr); +} +.registry-grid-wrap { overflow-x: auto; } +.registry-grid-header { + display: grid; + min-width: 780px; + padding: var(--sp-4) var(--sp-5); + font-weight: 600; + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: rgba(0,0,0,0.15); + margin-bottom: var(--sp-3); + align-items: center; + gap: var(--sp-3); +} +.registry-pair-list { + display: flex; + flex-direction: column; + gap: var(--sp-3); + min-width: 780px; +} +.registry-pair-card { + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--bg-card); + backdrop-filter: blur(12px); + overflow: hidden; +} +.registry-pair-row { + display: grid; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-4) var(--sp-5); +} +.registry-pair-row + .registry-pair-row { border-top: 1px dashed var(--border); } +.registry-pair-row:hover { background: rgba(255,255,255,0.015); } + /* ---------- Network Badge ---------- */ .network-switcher { display: flex; @@ -1090,8 +1137,8 @@ h4 { font-size: var(--text-xl); } z-index: 100; height: var(--header-h); background: rgba(var(--bg-base), 0.85); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); + backdrop-filter: blur(80px) saturate(200%); + -webkit-backdrop-filter: blur(80px) saturate(200%); border-bottom: 1px solid var(--border); display: flex; align-items: center; @@ -1111,7 +1158,8 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } width: 100%; max-width: var(--container-max); margin: 0 auto; - padding: 0 var(--sp-6); + padding: 0 var(--sp-4); + gap: var(--sp-2); } .header-logo { @@ -1121,12 +1169,30 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } font-size: var(--text-xl); font-weight: 800; letter-spacing: -0.03em; + flex-shrink: 0; } -.header-nav { display: flex; align-items: center; gap: var(--sp-1); } +/* Nav sits between the logo and the actions cluster. `min-width: 0` lets it + shrink instead of forcing header-inner to overflow the viewport. All the app + routes are shown directly, so link padding is kept tight to fit the row at + common desktop widths (1280px+); below the tablet breakpoint it collapses + into the hamburger drawer. */ +.header-nav { + display: flex; + align-items: center; + gap: 2px; + min-width: 0; + flex-shrink: 1; + overflow-x: auto; + scrollbar-width: none; + -ms-overflow-style: none; +} +.header-nav::-webkit-scrollbar { + display: none; +} .header-link { - padding: var(--sp-2) var(--sp-3); - font-size: var(--text-sm); + padding: 6px 8px; + font-size: 13.5px; font-weight: 500; color: var(--text-secondary); border-radius: var(--radius-md); @@ -1135,8 +1201,110 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } } .header-link:hover { color: var(--text-primary); background: var(--bg-elevated); } -.header-link.active { color: var(--accent); background: var(--accent-subtle); } -.header-actions { display: flex; align-items: center; gap: var(--sp-3); } +.header-link.active { color: var(--text-primary) !important; background: var(--bg-elevated) !important; } +.header-actions { display: flex; align-items: center; gap: var(--sp-3); flex-shrink: 0; } + +/* ---------- "More" nav dropdown ---------- */ +.nav-more-wrapper { position: relative; } +.nav-more-trigger { + display: inline-flex; + align-items: center; + background: none; + border: none; + cursor: pointer; + font-family: inherit; +} +.nav-more-menu { + position: absolute; + left: 0; + top: calc(100% + var(--sp-2)); + width: 200px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + padding: 4px; + z-index: 200; +} +.nav-more-item { + width: 100%; + padding: 8px 12px; + display: flex; + align-items: center; + justify-content: space-between; + font-size: var(--text-sm); + font-weight: 500; + color: var(--text-secondary); + border-radius: var(--radius-sm); + transition: all var(--t-fast); +} +.nav-more-item:hover { background: var(--bg-elevated); color: var(--text-primary); } +.nav-more-item.active { color: var(--accent); background: var(--accent-subtle); } + +/* ---------- Mobile hamburger + drawer ---------- */ +.nav-hamburger { display: none; } + +.mobile-nav-overlay { + position: fixed; + inset: 0; + z-index: 300; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); +} + +.mobile-nav-panel { + position: absolute; + top: 0; + right: 0; + height: 100%; + width: min(320px, 85vw); + background: var(--bg-surface); + border-left: 1px solid var(--border); + box-shadow: var(--shadow-lg); + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.mobile-nav-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--sp-4); + border-bottom: 1px solid var(--border); +} + +.mobile-nav-list { + display: flex; + flex-direction: column; + padding: var(--sp-2); +} + +.mobile-nav-link { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--sp-3) var(--sp-4); + font-size: var(--text-base); + font-weight: 500; + color: var(--text-secondary); + border-radius: var(--radius-md); +} + +.mobile-nav-link:hover { background: var(--bg-elevated); color: var(--text-primary); } +.mobile-nav-link.active { color: var(--text-primary); background: var(--bg-elevated); } + +.btn-primary-black { + background: var(--text-primary) !important; + color: var(--bg-surface) !important; + box-shadow: var(--shadow-sm); + border: none; +} +.btn-primary-black:hover { + background: var(--accent-hover) !important; + opacity: 0.95; + box-shadow: var(--shadow-md); +} /* ---------- Footer ---------- */ .footer { @@ -2292,6 +2460,145 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } border-top: 1px solid var(--border); } +/* ── Multi-page docs: page header ── */ +.docs-page-header { + margin-bottom: var(--sp-8); + padding-bottom: var(--sp-6); + border-bottom: 1px solid var(--border); +} +.docs-eyebrow { + display: inline-block; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: var(--sp-3); +} +.docs-page-title { + font-size: var(--text-3xl); + font-weight: 800; + letter-spacing: -0.03em; + color: var(--text-primary); + margin: 0 0 var(--sp-3); + line-height: 1.1; +} +.docs-page-desc { + font-size: var(--text-md); + color: var(--text-secondary); + line-height: 1.6; + margin: 0; + max-width: 62ch; +} + +/* Section heading inside a page body */ +.docs-h2 { + font-size: var(--text-xl); + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.02em; + margin: var(--sp-10) 0 var(--sp-4); + padding-bottom: var(--sp-3); + border-bottom: 1px solid var(--border); +} + +/* Prose links */ +.docs-p a, +.docs-lead a, +.docs-list a, +.docs-callout a, +.docs-info-box a, +.docs-faq-item a, +.docs-page-desc a { + color: var(--text-primary); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: var(--border-hover); + font-weight: 600; +} +.docs-p a:hover, +.docs-lead a:hover, +.docs-list a:hover, +.docs-callout a:hover, +.docs-info-box a:hover, +.docs-faq-item a:hover, +.docs-page-desc a:hover { + text-decoration-color: var(--text-primary); +} + +/* Sidebar items are now s */ +a.docs-nav-item { text-decoration: none; } + +/* Success callout (info=neutral, warning=amber, error=red already exist) */ +.docs-callout-success { + border-left: 3px solid var(--success); + background: color-mix(in srgb, var(--success) 8%, transparent); +} + +/* FAQ items reuse .docs-h2 for questions but tighten the spacing */ +.docs-faq-item + .docs-faq-item { margin-top: var(--sp-2); } +.docs-faq-item .docs-h2 { font-size: var(--text-lg); } + +/* SVG diagrams */ +.docs-diagram { + margin: var(--sp-6) 0; + padding: var(--sp-5); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--bg-surface); +} +.docs-diagram svg { display: block; } +.docs-diagram figcaption { + margin-top: var(--sp-3); + text-align: center; + font-size: var(--text-xs); + color: var(--text-muted); +} + +/* Prev / Next pager */ +.docs-pager { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--sp-4); + margin-top: var(--sp-16); + padding-top: var(--sp-8); + border-top: 1px solid var(--border); +} +.docs-pager-link { + display: flex; + flex-direction: column; + gap: 4px; + padding: var(--sp-4); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + text-decoration: none; + transition: all var(--t-fast); + background: var(--bg-surface); +} +.docs-pager-link:hover { + border-color: var(--accent); + background: var(--bg-elevated); +} +.docs-pager-next { text-align: right; align-items: flex-end; } +.docs-pager-dir { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: var(--text-xs); + font-weight: 600; + color: var(--text-muted); +} +.docs-pager-label { + font-size: var(--text-sm); + font-weight: 700; + color: var(--text-primary); +} + +@media (max-width: 640px) { + .docs-pager { grid-template-columns: 1fr; } + .docs-pager-next { text-align: left; align-items: flex-start; } +} + /* ── Mobile toggle ── */ .docs-mobile-toggle { display: none; @@ -2379,6 +2686,18 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } } } +/* ========================================================================== + TABLET — trim non-essential header chrome before the hamburger kicks in + ========================================================================== */ + +@media (max-width: 1100px) { + .header-inner { padding: 0 var(--sp-4); } + /* Design theme swapper is a nice-to-have — first thing to go under + pressure so Registry/Wrap/.../Faucet + network switcher + wallet + always have room. */ + .theme-selector-dropdown { display: none; } +} + /* ========================================================================== MOBILE RESPONSIVE — Global fixes for 360px–768px ========================================================================== */ @@ -2391,7 +2710,15 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } } .header-nav { - display: none; /* hidden on mobile — user scrolls or uses links */ + display: none; /* replaced by the hamburger drawer below this width */ + } + + .nav-hamburger { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; } .header-actions { @@ -2440,9 +2767,32 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } } } +/* ── Mobile: hide registry address columns ─────────────────────────── */ +@media (max-width: 768px) { + .registry-addr-col { display: none; } + + /* Prevent iOS auto-zoom on inputs inside swap/transfer panels */ + .swap-panel select, + .swap-panel input[type="text"], + .swap-panel input[type="number"] { + font-size: 16px; + } +} + +/* ── Grid-3: responsive breakpoints (previously missing) ───────────── */ +@media (max-width: 900px) { + .grid-3 { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 480px) { + .grid-3 { grid-template-columns: 1fr; } +} + @media (max-width: 480px) { .header-logo span { display: none; } /* hide text, keep logo icon */ - .header-logo svg { margin: 0; } + .header-logo svg, .header-logo img { margin: 0; } + + /* Collapse "Connect Wallet" to icon-only so header fits in 375px */ + .btn-connect-label { display: none; } /* Network switcher: compact */ .network-switcher { @@ -2483,3 +2833,46 @@ html[data-theme='light'] .header { background: rgba(255, 255, 255, 0.85); } padding: var(--sp-3); } } + +/* ---------- LANDING PAGE DARK MODE OVERRIDES ---------- */ +.landing-page-container[data-theme='dark'] { + background: var(--bg-base) !important; + color: var(--text-primary) !important; +} +.landing-page-container[data-theme='dark'] section[style*="background: #fff"], +.landing-page-container[data-theme='dark'] section[style*="background: rgb(255, 255, 255)"], +.landing-page-container[data-theme='dark'] section[style*="background: #ffffff"] { + background: var(--bg-surface) !important; + border-color: var(--border) !important; +} +.landing-page-container[data-theme='dark'] section[style*="background: #fafafa"], +.landing-page-container[data-theme='dark'] section[style*="background: rgb(250, 250, 250)"] { + background: var(--bg-base) !important; + border-color: var(--border) !important; +} +.landing-page-container[data-theme='dark'] [style*="color: #000"], +.landing-page-container[data-theme='dark'] [style*="color: rgb(0, 0, 0)"], +.landing-page-container[data-theme='dark'] [style*="color: #000000"] { + color: var(--text-primary) !important; +} +.landing-page-container[data-theme='dark'] [style*="color: #71717a"], +.landing-page-container[data-theme='dark'] [style*="color: rgb(113, 113, 122)"], +.landing-page-container[data-theme='dark'] [style*="color: #52525b"] { + color: var(--text-muted) !important; +} +.landing-page-container[data-theme='dark'] [style*="border: 1px solid #e4e4e7"], +.landing-page-container[data-theme='dark'] [style*="border-top: 1px solid #e4e4e7"], +.landing-page-container[data-theme='dark'] [style*="border-bottom: 1px solid #e4e4e7"], +.landing-page-container[data-theme='dark'] [style*="border: 1px solid rgb(228, 228, 231)"] { + border-color: var(--border) !important; +} +.landing-page-container[data-theme='dark'] [style*="background: #f4f4f5"], +.landing-page-container[data-theme='dark'] .pill { + background: var(--bg-elevated) !important; + color: var(--text-secondary) !important; + border-color: var(--border) !important; +} +.landing-page-container[data-theme='dark'] nav a[style*="color: #52525b"] { + color: var(--text-secondary) !important; +} + diff --git a/src/app/icon.jpg b/src/app/icon.jpg new file mode 100644 index 0000000..84b4c92 Binary files /dev/null and b/src/app/icon.jpg differ diff --git a/src/app/landing.css b/src/app/landing.css deleted file mode 100644 index 47c60e7..0000000 --- a/src/app/landing.css +++ /dev/null @@ -1,159 +0,0 @@ -/* ============================================================================ - * Landing page stylesheet — Tailwind v4 + brand tokens. - * - * ONLY imported from src/app/page.tsx. The /app/* subtree uses globals.css. - * Palette: warm cream default (light) · gold accent · dark for contrast sections. - * ============================================================================ */ - -@import 'tailwindcss'; - -@custom-variant dark (&:is(.dark *)); - -/* --------------------------------------------------------------------------- - * @theme — design tokens exposed as Tailwind utilities - * -------------------------------------------------------------------------- */ -@theme { - /* Brand gold */ - --color-gold-50: #fff8d4; - --color-gold-100: #fff0a8; - --color-gold-400: #ffdc4d; - --color-gold-500: #ffd208; - --color-gold-600: #e6b800; - --color-gold-700: #a87a00; - - /* Cream scale — warm off-whites for light sections */ - --color-cream-50: #fefdfb; - --color-cream-100: #f8f5f0; - --color-cream-200: #ede9e0; - --color-cream-300: #dcd7cc; - - /* Ink scale — warm blacks/grays */ - --color-ink-50: #fafaf7; - --color-ink-100: #e8e6df; - --color-ink-200: #c8c4bc; - --color-ink-300: #7a7872; - --color-ink-400: #5a5850; - --color-ink-500: #38362f; - --color-ink-700: #1c1b16; - --color-ink-800: #131210; - --color-ink-900: #0a0908; - --color-ink-950: #050504; - - /* Typography — next/font/google injects these CSS vars on */ - --font-display: var(--font-fraunces, 'Fraunces'), Georgia, serif; - --font-sans: var(--font-jakarta, 'Plus Jakarta Sans'), system-ui, sans-serif; - --font-mono: 'JetBrains Mono', ui-monospace, monospace; - - /* shadcn/MagicCard compat — defaults to light; overridden by .section-dark */ - --color-background: #f8f5f0; - --color-border: rgba(15, 14, 12, 0.10); - - /* Motion */ - --animate-aurora: aurora 20s ease-in-out infinite alternate; - --animate-marquee: marquee var(--duration, 30s) linear infinite; - --animate-marquee-vertical: marquee-vertical var(--duration, 30s) linear infinite; - --animate-shimmer-slide: shimmer-slide var(--speed, 2s) ease-in-out infinite alternate; - --animate-spin-around: spin-around calc(var(--speed, 2s) * 2) infinite linear; - --animate-shine: shine var(--duration, 8s) ease-in-out infinite; - --animate-grid: grid 15s linear infinite; - --animate-border-beam: border-beam calc(var(--duration, 8s) * 1s) infinite linear; - --animate-blink-cursor: blink-cursor 1.2s step-end infinite; -} - -/* --------------------------------------------------------------------------- - * Keyframes - * -------------------------------------------------------------------------- */ -@keyframes aurora { - 0% { background-position: 0% 50%; transform: rotate(-5deg) scale(0.9); } - 25% { background-position: 50% 100%; transform: rotate(5deg) scale(1.1); } - 50% { background-position: 100% 50%; transform: rotate(-3deg) scale(0.95); } - 75% { background-position: 50% 0%; transform: rotate(3deg) scale(1.05); } - 100% { background-position: 0% 50%; transform: rotate(-5deg) scale(0.9); } -} -@keyframes marquee { - from { transform: translateX(0); } - to { transform: translateX(calc(-100% - var(--gap, 1rem))); } -} -@keyframes marquee-vertical { - from { transform: translateY(0); } - to { transform: translateY(calc(-100% - var(--gap, 1rem))); } -} -@keyframes shimmer-slide { - to { transform: translate(calc(100cqw - 100%), 0); } -} -@keyframes spin-around { - 0% { transform: translateZ(0) rotate(0); } - 15%, 35% { transform: translateZ(0) rotate(90deg); } - 65%, 85% { transform: translateZ(0) rotate(270deg); } - 100% { transform: translateZ(0) rotate(360deg); } -} -@keyframes shine { - 0% { background-position: 0% 0%; } - 50% { background-position: 100% 100%; } - 100% { background-position: 0% 0%; } -} -@keyframes grid { - 0% { transform: translateY(-50%); } - 100% { transform: translateY(0); } -} -@keyframes border-beam { - 100% { offset-distance: 100%; } -} -@keyframes blink-cursor { - 0%, 49% { opacity: 1; } - 50%, 100% { opacity: 0; } -} - -/* --------------------------------------------------------------------------- - * Landing root — light cream default. Never bleeds outside .landing-root. - * -------------------------------------------------------------------------- */ -.landing-root { - font-family: var(--font-sans); - background: var(--color-cream-100); - color: var(--color-ink-900); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - min-height: 100vh; -} - -.landing-root ::selection { - background: var(--color-gold-500); - color: var(--color-ink-950); -} - -.landing-root h1, -.landing-root h2, -.landing-root h3 { - font-family: var(--font-display); - letter-spacing: -0.02em; -} - -/* Dark variant — used on FHE explainer, final CTA, footer */ -.section-dark { - --color-background: #050504; - --color-border: rgba(255, 255, 255, 0.08); -} - -/* Grain overlay for hero only */ -.landing-grain::before { - content: ''; - position: fixed; - inset: 0; - pointer-events: none; - z-index: 1; - opacity: 0.03; - mix-blend-mode: multiply; - background-image: url("data:image/svg+xml;utf8,"); -} - -/* Gold rule divider */ -.landing-rule { - height: 1px; - background: linear-gradient( - to right, - transparent 0%, - var(--color-gold-500) 50%, - transparent 100% - ); - opacity: 0.4; -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b993e2b..52a68fa 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -18,7 +18,7 @@ const jakarta = Plus_Jakarta_Sans({ }); export const metadata: Metadata = { - title: 'ZamaVault — Confidential Tokens, Made Simple', + title: 'ShadowLine — Confidential Tokens, Made Simple', description: "Discover, shield, and manage confidential ERC-7984 tokens on Zama's fully-homomorphic encryption protocol. The canonical interface for the Confidential Wrappers Registry.", keywords: [ @@ -26,7 +26,7 @@ export const metadata: Metadata = { 'confidential tokens', 'wrapper registry', 'privacy', 'Ethereum', 'DeFi', ], openGraph: { - title: 'ZamaVault — Confidential Tokens, Made Simple', + title: 'ShadowLine — Confidential Tokens, Made Simple', description: "The definitive interface for managing confidential ERC-7984 tokens powered by Zama's Fully Homomorphic Encryption.", type: 'website', @@ -35,19 +35,19 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - +