diff --git a/.gitignore b/.gitignore index 00c88a671..9722eeec3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,3 @@ -<<<<<<< HEAD -# Compiler files -cache/ -out/ - -# Ignores development broadcast logs -======= # ============================================================================= # Environment Variables - CRITICAL: Never commit .env files # ============================================================================= @@ -26,23 +19,14 @@ contracts/foundry-out/ contracts/out/ # Development broadcast logs (keep broadcast structure, ignore specific chains) ->>>>>>> 8b13a35cb03fb14cdb9f34a7b7b429971bb197b8 !/broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ -<<<<<<< HEAD -# Docs -docs/ - -# Dotenv file -.env -======= # ============================================================================= # Hardhat Build Artifacts (if used) # ============================================================================= artifacts/ -cache/ typechain/ typechain-types/ @@ -141,4 +125,3 @@ agents/ *.temp .cache/ .temp/ ->>>>>>> 8b13a35cb03fb14cdb9f34a7b7b429971bb197b8 diff --git a/README.md b/README.md index 8e03d8083..952c2ca8a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,54 @@ # Hook Bazaar Monorepo +
+ + Solidity + Ethereum + Uniswap + TypeScript + React + Vite + Tailwind CSS + Foundry + Node.js + PostgreSQL + Subsquid + +
+ +

+ Description +

+ + + +## Table of Contents +- [Hook Bazaar Monorepo](#Hook Bazaar Monorepo) + - [Table of Contents](#table-of-contents) + - [Demo](#demo) + - [Problem Description](#problem-description) + - [Solution Overview](#solution-overview) + - [Arquitecture](#arquitecture) + - [Setup](#setup) + - [Build](#build) + - [Test](#test) + - [Deploy](#deploy) + - [References](#references) + + +# Setup + Minimal setup to run the frontend and indexer locally. ## Prerequisites - Node.js v18+ - Docker (for indexer PostgreSQL) +- Foundry ## Quick Start + ### 1. Install Dependencies ```bash @@ -28,51 +68,8 @@ Frontend runs on: **http://localhost:3000** ### 3. Indexer -#### Setup (first time only) - -1. Set RPC endpoint in root `.env`: - ```bash - RPC_UNI_TESTNET_HTTP=https://your-rpc-endpoint-here - ``` - (The indexer reads from the root `.env` file) - -2. Start PostgreSQL: - ```bash - npm run indexer:db:up - ``` - -3. Build and migrate: - ```bash - npm run indexer:build - npm run indexer:migration - ``` - -#### Run Indexer - -```bash -npm run indexer -``` - -Indexer runs on: **PostgreSQL port 5432** - -(Optional) GraphQL server on port 4350: -```bash -npm run indexer:graphql -``` -**Note:** Requires `@subsquid/openreader` package (install with `npm install`) - -## Available Commands - -### Frontend -- `npm run dev` - Start Vite dev server (port 3000) -- `npm run build` - Build for production - -### Indexer -- `npm run indexer:build` - Build TypeScript -- `npm run indexer` - Run indexer processor -- `npm run indexer:graphql` - Start GraphQL server (port 4350) - -### Contracts +### 4. Contracts - `forge build` - Compile contracts - `forge test` - Run tests + diff --git a/client2/docs/protocol-admin-management-spec.md b/client2/docs/protocol-admin-management-spec.md new file mode 100644 index 000000000..835fda33c --- /dev/null +++ b/client2/docs/protocol-admin-management-spec.md @@ -0,0 +1,284 @@ +# Protocol Admin Management Page - Frontend Specification + +## Overview + +This specification details the Protocol Admin Management Page, a comprehensive interface for protocol administrators to manage their protocols through the `IProtocolAdminManager` and `IProtocolAdminClient` smart contract interfaces. + +## Purpose + +Protocol administrators need a dedicated interface to: +1. View and manage protocol details +2. Create new pools for their protocol +3. Set protocol-wide and pool-specific revenue configurations +4. Delegate pool creator roles +5. Manage protocol metadata (URIs) + +## Contract Interfaces + +### IProtocolAdminManager Functions (All exposed) + +| Function | Parameters | Returns | Description | +|----------|------------|---------|-------------| +| `isCreator(address)` | `_account: address` | `bool` | Check if address is protocol creator | +| `setPool(PoolId)` | `_poolId: PoolId` | `void` | Set pool (called through admin panel) | +| `getPools()` | - | `PoolId[]` | Get all pools for protocol | +| `delegatePoolCreatorRole(address)` | `_account: address` | `void` | Delegate pool creation rights | +| `isPoolCreator(address)` | `_account: address` | `bool` | Check if address can create pools | +| `protocolId()` | - | `uint256` | Get protocol token ID | +| `protocolName()` | - | `string` | Get protocol name | +| `setURI(URI_TYPE, string)` | `_uriType: enum, _uri: string` | `void` | Set protocol URI metadata | +| `getURI(URI_TYPE)` | `_uriType: enum` | `string` | Get protocol URI by type | + +### URI_TYPE Enum +- `ZORA` (0) - Zora NFT metadata URI +- `WEBSITE` (1) - Protocol website +- `X` (2) - X/Twitter handle +- `FARCASTER` (3) - Farcaster handle + +### IProtocolAdminClient Functions (Selective exposure) + +| Function | Parameters | Returns | Description | Exposed | +|----------|------------|---------|-------------|---------| +| `create_pool` | `protocolId: uint256, _encoded_pool_key: bytes, initialSqrtPrice: uint160` | `(PoolId, int24)` | Create new pool | Yes | +| `setProtocolRevenue` | `protocolId: uint256` | `uint256` | Set protocol revenue | Yes | +| `setPoolRevenue` | `protocolId: uint256, poolId: PoolId` | `uint256` | Set pool revenue | Yes | +| `getProtocols` | - | `uint256[]` | Get user's protocols | No | +| `getProtocolRevenue` | `protocolId: uint256` | `uint256` | Get protocol revenue | No (view only) | +| `getPoolRevenue` | `protocolId: uint256, poolId: PoolId` | `uint256` | Get pool revenue | No (view only) | + +## Page Architecture + +### Route +`/ProtocolDashboard/protocol/:protocolId/admin` + +### URL Parameters +- `protocolId`: The protocol token ID from the contract + +### Navigation +- Access via "View Details" button on Protocol Designer Dashboard +- Back navigation to Protocol Designer Dashboard + +## Component Structure + +``` +ProtocolAdminPage/ +├── ProtocolAdminPage.tsx # Main page orchestrator +├── ProtocolHeader.tsx # Protocol info header +├── ManagerSection.tsx # IProtocolAdminManager functions +│ ├── PoolsTable.tsx # Display pools with getPools() +│ ├── DelegateRoleForm.tsx # delegatePoolCreatorRole() +│ └── URIManager.tsx # setURI() and getURI() +├── ClientSection.tsx # IProtocolAdminClient functions +│ ├── CreatePoolForm.tsx # create_pool() +│ ├── SetProtocolRevenueForm.tsx # setProtocolRevenue() +│ └── SetPoolRevenueForm.tsx # setPoolRevenue() +└── index.ts # Exports +``` + +## UI Sections + +### 1. Protocol Header +- Protocol Name (from `protocolName()`) +- Protocol ID (from `protocolId()`) +- Creator verification badge (from `isCreator()`) +- Chain information + +### 2. Protocol Admin Manager Section + +#### 2.1 Pools Overview +- Table displaying all pools from `getPools()` +- Columns: Pool ID, Status, Actions + +#### 2.2 Role Management +- Form to delegate pool creator role via `delegatePoolCreatorRole()` +- Check role status via `isPoolCreator()` +- Input: Ethereum address + +#### 2.3 URI Management +- Forms to set/update protocol URIs via `setURI()` +- Display current URIs via `getURI()` +- URI types: Website, X/Twitter, Farcaster, Zora + +### 3. Protocol Admin Client Section + +#### 3.1 Create Pool Form +**Function:** `create_pool(uint256 protocolId, bytes _encoded_pool_key, uint160 initialSqrtPrice)` + +**Form Fields:** +| Field | Type | Description | Validation | +|-------|------|-------------|------------| +| Token 0 Address | `address` | First token in pair | Valid checksum address | +| Token 1 Address | `address` | Second token in pair | Valid checksum address | +| Fee | `uint24` | Pool fee in bps | 100, 500, 3000, or 10000 | +| Tick Spacing | `int24` | Price tick spacing | Positive integer | +| Hook Address | `address` | Hook contract address | Valid address or zero | +| Initial Price | `uint160` | sqrtPriceX96 format | Positive number | + +**PoolKey Structure (for encoding):** +```typescript +interface PoolKey { + currency0: Address; + currency1: Address; + fee: number; + tickSpacing: number; + hooks: Address; +} +``` + +#### 3.2 Set Protocol Revenue Form +**Function:** `setProtocolRevenue(uint256 protocolId)` + +**Form Fields:** +| Field | Type | Description | +|-------|------|-------------| +| Revenue Amount | `uint256` | Revenue in wei | + +#### 3.3 Set Pool Revenue Form +**Function:** `setPoolRevenue(uint256 protocolId, PoolId poolId)` + +**Form Fields:** +| Field | Type | Description | +|-------|------|-------------| +| Pool ID | `PoolId` | Select from available pools | +| Revenue Amount | `uint256` | Revenue in wei | + +## State Management + +### React Hooks Required +- `useProtocolAdminManager` - Interact with IProtocolAdminManager +- `useProtocolAdminClient` - Interact with IProtocolAdminClient (existing, extend if needed) + +### Wagmi Hooks +- `useReadContract` - Read contract state +- `useWriteContract` - Execute transactions +- `useWaitForTransactionReceipt` - Track transaction status + +## TypeScript Types + +```typescript +// URI Types enum matching contract +export enum URIType { + ZORA = 0, + WEBSITE = 1, + X = 2, + FARCASTER = 3, +} + +// Pool Key structure for encoding +export interface PoolKey { + currency0: Address; + currency1: Address; + fee: number; + tickSpacing: number; + hooks: Address; +} + +// Create Pool parameters +export interface CreatePoolParams { + protocolId: bigint; + poolKey: PoolKey; + initialSqrtPrice: bigint; +} + +// Revenue parameters +export interface SetRevenueParams { + protocolId: bigint; + poolId?: string; // For pool-specific revenue + amount: bigint; +} + +// Protocol Admin state +export interface ProtocolAdminState { + protocolId: bigint | null; + protocolName: string; + isCreator: boolean; + pools: string[]; + uris: Record; +} +``` + +## Transaction Flow + +### Create Pool Flow +1. User fills pool key form +2. Frontend encodes PoolKey to bytes using `abi.encode` +3. Call `create_pool(protocolId, encodedPoolKey, initialSqrtPrice)` +4. Wait for transaction confirmation +5. Display pool ID and initial tick from return values +6. Refresh pools list + +### Set Revenue Flow +1. User enters revenue amount +2. Convert to wei if needed +3. Call `setProtocolRevenue()` or `setPoolRevenue()` +4. Wait for confirmation +5. Update displayed revenue + +## Error Handling + +| Error | Contract | User Message | +|-------|----------|--------------| +| `ProtocolAdminManagerCallerIsNotCreator` | Manager | "You are not the protocol creator" | +| `ProtocolAdminClientUnauthorizedCaller` | Client | "You are not authorized to create pools" | +| `ProtocolAdminManagerNotClone` | Manager | "Invalid protocol instance" | +| `ProtocolAdminManagerInvalidContextCall` | Manager | "Invalid call context" | + +## Responsive Design + +- Desktop: Full 3-column layout +- Tablet: 2-column layout with stacked sections +- Mobile: Single column with collapsible sections + +## Accessibility + +- Form labels with proper `for` attributes +- ARIA labels on action buttons +- Keyboard navigation support +- Screen reader friendly error messages +- Focus management after transactions + +## Mock Data (Development) + +```typescript +const mockProtocolAdmin = { + protocolId: 1n, + protocolName: 'My DeFi Protocol', + isCreator: true, + pools: [ + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000000000000000000000000002', + ], + uris: { + [URIType.WEBSITE]: 'https://myprotocol.io', + [URIType.X]: '@myprotocol', + [URIType.FARCASTER]: 'myprotocol.eth', + [URIType.ZORA]: 'ipfs://...', + }, +}; +``` + +## Implementation Priority + +1. **Phase 1 - Core Functions (Required)** + - ProtocolAdminPage layout + - Create Pool form + - Set Protocol Revenue form + - Set Pool Revenue form + +2. **Phase 2 - Manager Functions** + - Pools table display + - Delegate role form + - URI management + +3. **Phase 3 - Polish** + - Transaction status toasts + - Loading states + - Error boundaries + +## Dependencies + +- React Router v6 (navigation) +- wagmi v2 (contract interactions) +- viem (encoding/decoding) +- lucide-react (icons) +- Existing design system components diff --git a/client2/package-lock.json b/client2/package-lock.json new file mode 100644 index 000000000..90fd62a42 --- /dev/null +++ b/client2/package-lock.json @@ -0,0 +1,139 @@ +{ + "name": "client2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "katex": "^0.16.27", + "react-katex": "^3.1.0" + }, + "devDependencies": { + "@types/katex": "^0.16.7", + "@types/react-katex": "^3.0.4" + } + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-katex": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/react-katex/-/react-katex-3.0.4.tgz", + "integrity": "sha512-aLkykKzSKLpXI6REJ3uClao6P47HAFfR1gcHOZwDeTuALsyjgMhz+oynLV4gX0kiJVnvHrBKF/TLXqyNTpHDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT", + "peer": true + }, + "node_modules/katex": { + "version": "0.16.27", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.27.tgz", + "integrity": "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "license": "MIT", + "peer": true + }, + "node_modules/react": { + "version": "18.3.1", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-katex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-katex/-/react-katex-3.1.0.tgz", + "integrity": "sha512-At9uLOkC75gwn2N+ZXc5HD8TlATsB+3Hkp9OGs6uA8tM3dwZ3Wljn74Bk3JyHFPgSnesY/EMrIAB1WJwqZqejA==", + "license": "MIT", + "dependencies": { + "katex": "^0.16.0" + }, + "peerDependencies": { + "prop-types": "^15.8.1", + "react": ">=15.3.2 <20" + } + } + } +} diff --git a/client2/package.json b/client2/package.json new file mode 100644 index 000000000..1f7443caa --- /dev/null +++ b/client2/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "katex": "^0.16.27", + "react-katex": "^3.1.0" + }, + "devDependencies": { + "@types/katex": "^0.16.7", + "@types/react-katex": "^3.0.4" + } +} diff --git a/client2/src/App.tsx b/client2/src/App.tsx index ca8cc3611..eb483463b 100644 --- a/client2/src/App.tsx +++ b/client2/src/App.tsx @@ -5,8 +5,11 @@ import ContactPage from './components/ContactPage'; import HookDeveloperDashboard from './components/HookDeveloperDashboard'; import ProtocolDesignerDashboard from './components/ProtocolDesignerDashboard'; import CreateProtocolPage from './components/protocol/CreateProtocolPage'; +import { ProtocolAdminPage } from './components/protocol/admin'; import IntegratorPortal from './components/IntegratorPortal'; import SkipLink from './components/common/SkipLink'; +import { CreateHookPage, CodeVerificationPage } from './components/hook'; +import { HookMarketPage } from './components/market'; export default function App() { return ( @@ -19,8 +22,12 @@ export default function App() { {}} />} /> {}} />} /> {}} />} /> + } /> + } /> {}} />} /> } /> + } /> + } /> {}} />} /> } /> diff --git a/client2/src/assets/d9960bdb814135603341883999ea9dc547d831b8.png b/client2/src/assets/d9960bdb814135603341883999ea9dc547d831b8.png index c71293e14..51e89b382 100644 Binary files a/client2/src/assets/d9960bdb814135603341883999ea9dc547d831b8.png and b/client2/src/assets/d9960bdb814135603341883999ea9dc547d831b8.png differ diff --git a/client2/src/components/HookDeveloperDashboard.tsx b/client2/src/components/HookDeveloperDashboard.tsx index 3bc2c4769..042a3f2dc 100644 --- a/client2/src/components/HookDeveloperDashboard.tsx +++ b/client2/src/components/HookDeveloperDashboard.tsx @@ -1,4 +1,5 @@ import { ArrowLeft, Code, DollarSign, Package, Plus, TrendingUp } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; import Navigation from './Navigation'; import Footer from './Footer'; @@ -7,6 +8,12 @@ interface HookDeveloperDashboardProps { } export default function HookDeveloperDashboard({ onNavigate }: HookDeveloperDashboardProps) { + const navigate = useNavigate(); + + const handleCreateHook = () => { + navigate('/hook-developer/create'); + }; + const stats = [ { icon: Package, label: 'Total Hooks', value: '3', color: 'primary' }, { icon: DollarSign, label: 'Total Revenue', value: '$12,450', color: 'secondary' }, @@ -95,6 +102,7 @@ export default function HookDeveloperDashboard({ onNavigate }: HookDeveloperDash diff --git a/client2/src/components/ProtocolDesignerDashboard.tsx b/client2/src/components/ProtocolDesignerDashboard.tsx index 41f8e9422..6947183b9 100644 --- a/client2/src/components/ProtocolDesignerDashboard.tsx +++ b/client2/src/components/ProtocolDesignerDashboard.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; -import { ArrowLeft, DollarSign, Layers, Plus, TrendingUp } from 'lucide-react'; +import { ArrowLeft, DollarSign, Layers, Plus, Search, TrendingUp } from 'lucide-react'; import Navigation from './Navigation'; import Footer from './Footer'; import CreateProtocolDialog from './protocol/CreateProtocolDialog'; @@ -126,8 +126,16 @@ export default function ProtocolDesignerDashboard({ onNavigate }: ProtocolDesign }, [location.state, address, protocols, navigate, location.pathname]); const handleViewDetails = (protocol: Protocol) => { - setSelectedProtocol(protocol); - setDetailsDialogOpen(true); + // Navigate to the Protocol Admin page with the protocol data + if (protocol.protocolId) { + navigate(`/ProtocolDashboard/protocol/${protocol.protocolId}/admin`, { + state: { protocol }, + }); + } else { + // Fallback to dialog for protocols without protocolId + setSelectedProtocol(protocol); + setDetailsDialogOpen(true); + } }; const handleSaveProtocolDetails = ( @@ -195,33 +203,56 @@ export default function ProtocolDesignerDashboard({ onNavigate }: ProtocolDesign

- +
+ + +
diff --git a/client2/src/components/common/LatexRenderer.tsx b/client2/src/components/common/LatexRenderer.tsx new file mode 100644 index 000000000..6ed0bc013 --- /dev/null +++ b/client2/src/components/common/LatexRenderer.tsx @@ -0,0 +1,136 @@ +import { useMemo } from 'react'; +import katex from 'katex'; +import 'katex/dist/katex.min.css'; + +interface LatexRendererProps { + latex: string; + displayMode?: boolean; + className?: string; + style?: React.CSSProperties; +} + +/** + * Renders LaTeX mathematical expressions using KaTeX + * Handles both inline ($...$) and display ($$...$$) math + */ +export default function LatexRenderer({ + latex, + displayMode = false, + className = '', + style = {} +}: LatexRendererProps) { + const renderedHtml = useMemo(() => { + try { + // Clean up the LaTeX string - handle common subscript/superscript patterns + let cleanLatex = latex; + + // If it doesn't look like LaTeX, wrap simple subscripts + if (!latex.includes('\\') && !latex.includes('{')) { + // Convert simple patterns like L_k to L_{k} + cleanLatex = latex + .replace(/([A-Za-z])_([A-Za-z0-9])/g, '$1_{$2}') + .replace(/([A-Za-z])\^([A-Za-z0-9])/g, '$1^{$2}'); + } + + return katex.renderToString(cleanLatex, { + displayMode, + throwOnError: false, + strict: false, + trust: true, + macros: { + // Common macros for DeFi/AMM notation + '\\R': '\\mathbb{R}', + '\\N': '\\mathbb{N}', + '\\Z': '\\mathbb{Z}', + } + }); + } catch (error) { + // Fallback to plain text if LaTeX parsing fails + return `${latex}`; + } + }, [latex, displayMode]); + + return ( + + ); +} + +/** + * Parse and render a string that may contain mixed text and LaTeX + * Handles inline $...$ and display $$...$$ delimiters + */ +export function MixedLatexRenderer({ + content, + className = '', + style = {} +}: { + content: string; + className?: string; + style?: React.CSSProperties; +}) { + const parts = useMemo(() => { + const result: { type: 'text' | 'latex' | 'displayLatex'; content: string }[] = []; + + // Match $$...$$ (display) and $...$ (inline) + const regex = /(\$\$[\s\S]*?\$\$|\$[^$]+\$)/g; + let lastIndex = 0; + let match; + + while ((match = regex.exec(content)) !== null) { + // Add text before this match + if (match.index > lastIndex) { + result.push({ + type: 'text', + content: content.slice(lastIndex, match.index) + }); + } + + // Add the LaTeX part + const matched = match[0]; + if (matched.startsWith('$$')) { + result.push({ + type: 'displayLatex', + content: matched.slice(2, -2) + }); + } else { + result.push({ + type: 'latex', + content: matched.slice(1, -1) + }); + } + + lastIndex = regex.lastIndex; + } + + // Add remaining text + if (lastIndex < content.length) { + result.push({ + type: 'text', + content: content.slice(lastIndex) + }); + } + + return result; + }, [content]); + + return ( + + {parts.map((part, index) => { + if (part.type === 'text') { + return {part.content}; + } + return ( + + ); + })} + + ); +} diff --git a/client2/src/components/hook/AVSVerificationResults.tsx b/client2/src/components/hook/AVSVerificationResults.tsx new file mode 100644 index 000000000..dd62c7aff --- /dev/null +++ b/client2/src/components/hook/AVSVerificationResults.tsx @@ -0,0 +1,547 @@ +import { useState } from 'react'; +import { + Shield, + CheckCircle, + AlertTriangle, + AlertCircle, + Info, + ChevronDown, + ChevronRight, + ExternalLink, + FileCode +} from 'lucide-react'; +import type { AVSVerificationResult, AVSFinding, CompilationResult } from '../../types/hookSpec'; + +interface AVSVerificationResultsProps { + compilationResult: CompilationResult; + avsResult: AVSVerificationResult; +} + +export default function AVSVerificationResults({ + compilationResult, + avsResult +}: AVSVerificationResultsProps) { + const [expandedFindings, setExpandedFindings] = useState>(new Set()); + + const toggleFinding = (id: string) => { + setExpandedFindings(prev => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + const getScoreColor = (score: number) => { + if (score >= 95) return 'var(--color-primary)'; + if (score >= 80) return '#FFA500'; // Orange + return 'var(--color-accent)'; + }; + + const getSeverityIcon = (severity: AVSFinding['severity']) => { + switch (severity) { + case 'critical': + return ; + case 'warning': + return ; + case 'info': + return ; + } + }; + + const getSeverityBg = (severity: AVSFinding['severity']) => { + switch (severity) { + case 'critical': + return 'rgba(232, 90, 79, 0.1)'; + case 'warning': + return 'rgba(255, 165, 0, 0.1)'; + case 'info': + return 'rgba(100, 100, 100, 0.1)'; + } + }; + + const criticalCount = avsResult.findings.filter(f => f.severity === 'critical').length; + const warningCount = avsResult.findings.filter(f => f.severity === 'warning').length; + const infoCount = avsResult.findings.filter(f => f.severity === 'info').length; + + return ( +
+ {/* Header */} +
+
+ +
+
+

+ AVS Verification Results +

+

+ Code compliance checked by staked operators +

+
+
+ + {/* Compilation Status */} +
+
+ + + Compilation {compilationResult.success ? 'Successful' : 'Failed'} + +
+ {compilationResult.success && ( +
+
+ + Bytecode Hash + + + {compilationResult.bytecodeHash?.slice(0, 18)}... + +
+
+ + Deployed Bytecode Hash + + + {compilationResult.deployedBytecodeHash?.slice(0, 18)}... + +
+
+ )} +
+ + {/* Compliance Score */} +
+
+ + Compliance Score + + + {avsResult.complianceScore.toFixed(1)}% + +
+ + {/* Score Bar */} +
+
+
+ + {/* Status Badge */} +
+ {avsResult.compliant ? ( + <> + + + Compliant with Specification + + + ) : ( + <> + + + Non-Compliant (Review Findings) + + + )} +
+
+ + {/* Attestation Info */} +
+
+
+ + Attestation ID + +
+ + {avsResult.attestationId.slice(0, 20)}... + + + + +
+
+
+ + Verified At + + + {new Date(avsResult.verificationTimestamp).toLocaleString()} + +
+
+
+ + {/* Findings Summary */} + {avsResult.findings.length > 0 && ( + <> +
+ + Findings ({avsResult.findings.length}) + + {criticalCount > 0 && ( + + + {criticalCount} Critical + + )} + {warningCount > 0 && ( + + + {warningCount} Warning + + )} + {infoCount > 0 && ( + + + {infoCount} Info + + )} +
+ + {/* Findings List */} +
+ {avsResult.findings.map((finding, index) => { + const findingId = `${finding.ruleId}-${index}`; + const isExpanded = expandedFindings.has(findingId); + + return ( +
+ + + {isExpanded && ( +
+

+ {finding.description} +

+ + {finding.codeLocation && ( +
+ + Code Location + + + Lines {finding.codeLocation.startLine}-{finding.codeLocation.endLine} + {finding.codeLocation.functionName && ` in ${finding.codeLocation.functionName}()`} + +
+ )} + + {finding.specReference && ( +
+ + Spec Reference: + + + {finding.specReference} + +
+ )} +
+ )} +
+ ); + })} +
+ + )} + + {/* No Findings */} + {avsResult.findings.length === 0 && ( +
+ + + No Issues Found + +

+ Your code fully complies with the HookSpec specification +

+
+ )} +
+ ); +} diff --git a/client2/src/components/hook/CFHEDeploymentStatus.tsx b/client2/src/components/hook/CFHEDeploymentStatus.tsx new file mode 100644 index 000000000..91295f03c --- /dev/null +++ b/client2/src/components/hook/CFHEDeploymentStatus.tsx @@ -0,0 +1,270 @@ +import { + Lock, + CheckCircle, + ExternalLink, + Copy, + Shield, + Cpu, + Hash +} from 'lucide-react'; +import type { CFHEDeploymentResult } from '../../types/hookSpec'; + +interface CFHEDeploymentStatusProps { + cfheResult: CFHEDeploymentResult; +} + +export default function CFHEDeploymentStatus({ cfheResult }: CFHEDeploymentStatusProps) { + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + }; + + return ( +
+ {/* Header */} +
+
+ +
+
+

+ CFHE Deployment +

+

+ Encrypted bytecode deployed to confidential FHE network +

+
+
+ + {/* Success Banner */} +
+ +
+ + Deployment Successful + +

+ Your hook bytecode has been encrypted and deployed +

+
+
+ + {/* Deployment Details */} +
+ {/* Encrypted Contract Address */} +
+
+ + + Encrypted Contract Address + +
+
+ + {cfheResult.encryptedContractAddress} + + +
+
+ + {/* Transaction Hash */} +
+
+ + + Deployment Transaction + +
+
+ + {cfheResult.deploymentTxHash} + + + + +
+
+ + {/* Grid Info */} +
+ {/* Encrypted Bytecode Hash */} +
+
+ + + Encrypted Bytecode Hash + +
+ + {cfheResult.encryptedBytecodeHash.slice(0, 22)}... + +
+ + {/* Block Number */} +
+ + Block Number + + + #{cfheResult.blockNumber.toLocaleString()} + +
+
+
+ + {/* View on Explorer Button */} + + + View on CFHE Explorer + +
+ ); +} diff --git a/client2/src/components/hook/CodeSubmissionFlow.tsx b/client2/src/components/hook/CodeSubmissionFlow.tsx new file mode 100644 index 000000000..b3b2d1a76 --- /dev/null +++ b/client2/src/components/hook/CodeSubmissionFlow.tsx @@ -0,0 +1,843 @@ +import { useState, useCallback } from 'react'; +import { + Github, + Code, + Terminal, + AlertCircle, + Check, + GitBranch, + FileCode, + Settings, + ExternalLink, + Loader2 +} from 'lucide-react'; +import type { + SubmissionResult, + CodeVerificationResult +} from '../../types/hookSpec'; + +interface CodeSubmissionFlowProps { + licenseInfo: { + tokenId: number; + hookName: string; + hookVersion: string; + specIpfsCid: string; + }; + submissionResult: SubmissionResult; + walletConnected: boolean; + walletAddress?: string; + onConnectWallet: () => void; + onSubmitCode: (params: { + repoUrl: string; + branch?: string; + contractPath?: string; + compilerVersion: string; + optimizerRuns: number; + verifyOnly: boolean; + }) => Promise; +} + +type SubmissionStep = 'input' | 'configure' | 'submitting' | 'complete'; + +const COMPILER_VERSIONS = [ + '0.8.26', + '0.8.25', + '0.8.24', + '0.8.23', + '0.8.22', + '0.8.21', + '0.8.20', + '0.8.19', +]; + +// GitHub URL validation regex +const GITHUB_URL_REGEX = /^https?:\/\/(www\.)?github\.com\/[\w.-]+\/[\w.-]+\/?$/; + +export default function CodeSubmissionFlow({ + licenseInfo, + submissionResult, + walletConnected, + walletAddress, + onConnectWallet, + onSubmitCode +}: CodeSubmissionFlowProps) { + const [step, setStep] = useState('input'); + const [repoUrl, setRepoUrl] = useState(''); + const [branch, setBranch] = useState(''); + const [contractPath, setContractPath] = useState(''); + const [compilerVersion, setCompilerVersion] = useState('0.8.26'); + const [optimizerRuns, setOptimizerRuns] = useState(200); + const [verifyOnly, setVerifyOnly] = useState(false); + const [urlError, setUrlError] = useState(null); + const [verificationResult, setVerificationResult] = useState(null); + const [submissionError, setSubmissionError] = useState(null); + const [isValidating, setIsValidating] = useState(false); + + const validateGitHubUrl = (url: string): boolean => { + return GITHUB_URL_REGEX.test(url.trim()); + }; + + const handleUrlChange = (url: string) => { + setRepoUrl(url); + setUrlError(null); + }; + + const handleContinue = async () => { + if (!repoUrl.trim()) { + setUrlError('Please enter a GitHub repository URL'); + return; + } + + if (!validateGitHubUrl(repoUrl)) { + setUrlError('Invalid GitHub URL. Please use format: https://github.com/username/repo'); + return; + } + + setIsValidating(true); + // In production, this would validate the repo exists + await new Promise(resolve => setTimeout(resolve, 500)); + setIsValidating(false); + setStep('configure'); + }; + + const handleSubmit = useCallback(async () => { + if (!repoUrl) return; + + setStep('submitting'); + setSubmissionError(null); + + try { + const result = await onSubmitCode({ + repoUrl: repoUrl.trim(), + branch: branch.trim() || undefined, + contractPath: contractPath.trim() || undefined, + compilerVersion, + optimizerRuns, + verifyOnly + }); + + setVerificationResult(result); + setStep('complete'); + } catch (error) { + setSubmissionError(error instanceof Error ? error.message : 'Submission failed'); + setStep('configure'); + } + }, [repoUrl, branch, contractPath, compilerVersion, optimizerRuns, verifyOnly, onSubmitCode]); + + // Extract repo info from URL for display + const getRepoInfo = (url: string) => { + const match = url.match(/github\.com\/([\w.-]+)\/([\w.-]+)/); + if (match) { + return { owner: match[1], repo: match[2] }; + } + return null; + }; + + const repoInfo = repoUrl ? getRepoInfo(repoUrl) : null; + + return ( +
+ {/* Header */} +
+

+ + Submit Hook Code +

+

+ Submit your GitHub repository for AVS verification and CFHE deployment +

+
+ + {/* License Info Banner */} +
+
+
+ + Hook License + +

+ {licenseInfo.hookName} v{licenseInfo.hookVersion} +

+
+
+ + Token ID + +

+ #{licenseInfo.tokenId} +

+
+
+ + IPFS TX + +

+ {submissionResult.transactionHash.slice(0, 10)}... +

+
+
+
+ + {/* CLI Command Alternative */} +
+
+ +
+ + Or use CLI + + + hook-bazaar submit-code --license {licenseInfo.tokenId} --repo https://github.com/your-repo + +
+
+
+ + {/* Input Step */} + {step === 'input' && ( +
+ {/* GitHub URL Input */} +
+ +
+
+ +
+ handleUrlChange(e.target.value)} + placeholder="https://github.com/username/hook-implementation" + className="angular-clip w-full pl-12 pr-4 py-3 font-mono" + style={{ + background: 'var(--color-marble-light)', + border: urlError ? '2px solid var(--color-accent)' : '2px solid var(--color-secondary)', + color: 'var(--color-secondary)', + fontSize: 'var(--font-size-body-sm)' + }} + /> +
+ {urlError && ( +
+ + + {urlError} + +
+ )} +
+ + {/* Help Text */} +
+

+ Your repository should contain a Uniswap V4 hook implementation. + The system will automatically detect hook contracts that extend BaseHook. +

+
    +
  • - Supports Foundry and Hardhat projects
  • +
  • - Auto-detects main hook contract
  • +
  • - Verifies compliance with your HookSpec
  • +
+
+ + {/* Continue Button */} + +
+ )} + + {/* Configure Step */} + {step === 'configure' && ( +
+ {/* Repo Info */} +
+ +
+ + {repoInfo ? `${repoInfo.owner}/${repoInfo.repo}` : repoUrl} + + + + View on GitHub + + + +
+ +
+ + {/* Optional Settings */} +
+
+ + + Repository Options + + + (Optional) + +
+ +
+
+ + setBranch(e.target.value)} + placeholder="main (default)" + className="angular-clip w-full px-3 py-2 font-mono" + style={{ + background: 'var(--color-marble-light)', + border: '2px solid var(--color-secondary)', + color: 'var(--color-secondary)', + fontSize: 'var(--font-size-body-sm)' + }} + /> +
+ +
+ + setContractPath(e.target.value)} + placeholder="Auto-detect" + className="angular-clip w-full px-3 py-2 font-mono" + style={{ + background: 'var(--color-marble-light)', + border: '2px solid var(--color-secondary)', + color: 'var(--color-secondary)', + fontSize: 'var(--font-size-body-sm)' + }} + /> + + e.g., src/DynamicFeeHook.sol + +
+
+
+ + {/* Compiler Settings */} +
+
+ + + Compiler Settings + +
+ +
+
+ + +
+ +
+ + setOptimizerRuns(parseInt(e.target.value) || 200)} + min={0} + max={10000} + className="angular-clip w-full px-3 py-2 font-mono" + style={{ + background: 'var(--color-marble-light)', + border: '2px solid var(--color-secondary)', + color: 'var(--color-secondary)', + fontSize: 'var(--font-size-body-sm)' + }} + /> +
+
+
+ + {/* Verify Only Option */} +
+ +
+ + {/* Submission Error */} + {submissionError && ( +
+ + + {submissionError} + +
+ )} + + {/* Wallet Connection / Submit Button */} + {!walletConnected ? ( + + ) : ( + + )} +
+ )} + + {/* Submitting Step */} + {step === 'submitting' && ( +
+
+ +
+

+ Processing... +

+

+ Analyzing repository, verifying with AVS operators, and deploying to CFHE... +

+ + {/* Progress indicators */} +
+ {['Cloning Repository', 'Analyzing Contracts', 'Compiling Solidity', 'Submitting to AVS', verifyOnly ? null : 'Deploying to CFHE'].filter(Boolean).map((stepName, i) => ( +
+
+ + {stepName} + +
+ ))} +
+
+ )} + + {/* Complete Step - Pass to parent to show results components */} + {step === 'complete' && verificationResult && ( +
+
+ +
+

+ {verificationResult.avsResult.compliant ? 'Verification Complete!' : 'Verification Complete (Issues Found)'} +

+

+ View the detailed results below +

+ + {/* Repo Info Summary */} + {verificationResult.repoAnalysis && ( +
+
+ + + Repository Analyzed + +
+
+
+ + Commit + + + {verificationResult.repoAnalysis.commitHash.slice(0, 8)} + +
+
+ + Contracts Found + + + {verificationResult.repoAnalysis.contractsFound} + +
+
+
+ )} +
+ )} +
+ ); +} diff --git a/client2/src/components/hook/CodeVerificationPage.tsx b/client2/src/components/hook/CodeVerificationPage.tsx new file mode 100644 index 000000000..9a13ebdd6 --- /dev/null +++ b/client2/src/components/hook/CodeVerificationPage.tsx @@ -0,0 +1,522 @@ +import { useState, useCallback, useEffect } from 'react'; +import { ArrowLeft, ChevronRight, Github, Shield, Lock, CheckCircle } from 'lucide-react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import Navigation from '../Navigation'; +import Footer from '../Footer'; +import CodeSubmissionFlow from './CodeSubmissionFlow'; +import AVSVerificationResults from './AVSVerificationResults'; +import CFHEDeploymentStatus from './CFHEDeploymentStatus'; +import type { + SubmissionResult, + CodeVerificationResult +} from '../../types/hookSpec'; + +interface CodeVerificationPageProps { + onNavigate?: (page: string) => void; +} + +type VerificationStep = 'submit-code' | 'verifying' | 'results'; + +const STEPS: { id: VerificationStep; title: string; description: string; icon: React.ReactNode }[] = [ + { + id: 'submit-code', + title: 'Submit Code', + description: 'Link GitHub repository', + icon: + }, + { + id: 'verifying', + title: 'AVS Verification', + description: 'Compliance check', + icon: + }, + { + id: 'results', + title: 'Deploy', + description: 'CFHE deployment', + icon: + } +]; + +export default function CodeVerificationPage({ onNavigate }: CodeVerificationPageProps) { + const navigate = useNavigate(); + const location = useLocation(); + + // Get license info from navigation state (passed from CreateHookPage after minting) + const locationState = location.state as { + licenseInfo?: { + tokenId: number; + hookName: string; + hookVersion: string; + specIpfsCid: string; + }; + submissionResult?: SubmissionResult; + } | null; + + const [currentStep, setCurrentStep] = useState('submit-code'); + const [codeVerificationResult, setCodeVerificationResult] = useState(null); + + // Mock license info if not passed (for direct navigation/testing) + const [licenseInfo] = useState(locationState?.licenseInfo || { + tokenId: 42, + hookName: 'DynamicFeeHook', + hookVersion: '1.0.0', + specIpfsCid: 'QmMockSpecCid123456789' + }); + + const [submissionResult] = useState(locationState?.submissionResult || { + success: true, + ipfsCid: 'QmMockSpecCid123456789', + tokenId: 42, + transactionHash: '0xmock1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + ownerAddress: '0x1234567890abcdef1234567890abcdef12345678', + ipfsGatewayUrl: 'https://ipfs.io/ipfs/QmMockSpecCid123456789', + openSeaUrl: 'https://opensea.io/assets/ethereum/...' + }); + + // Mock wallet state + const [walletConnected, setWalletConnected] = useState(true); + const [walletAddress] = useState('0x1234567890abcdef1234567890abcdef12345678'); + + const handleNavigate = (page: string) => { + if (onNavigate) { + onNavigate(page); + } else { + navigate(`/${page}`); + } + }; + + const handleConnectWallet = useCallback(() => { + setWalletConnected(true); + }, []); + + const handleCodeSubmit = useCallback(async (params: { + repoUrl: string; + branch?: string; + contractPath?: string; + compilerVersion: string; + optimizerRuns: number; + verifyOnly: boolean; + }): Promise => { + // Move to verifying step + setCurrentStep('verifying'); + + // Simulate code verification + await new Promise(resolve => setTimeout(resolve, 3000)); + + const mockResult: CodeVerificationResult = { + success: true, + repoAnalysis: { + repoUrl: params.repoUrl, + branch: params.branch || 'main', + commitHash: 'a1b2c3d4e5f6g7h8i9j0' + Math.random().toString(36).slice(2, 6), + mainContract: params.contractPath || 'src/DynamicFeeHook.sol', + contractsFound: 3, + dependencies: ['@uniswap/v4-core', '@openzeppelin/contracts'] + }, + compilation: { + success: true, + bytecodeHash: '0x' + Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2), + deployedBytecodeHash: '0x' + Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2) + }, + avsResult: { + compliant: true, + complianceScore: 96.5, + attestationId: '0xattest' + Math.random().toString(16).slice(2, 18), + attestationTxHash: '0x' + Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2), + findings: [ + { + severity: 'info', + ruleId: 'GAS-01', + ruleName: 'Gas Optimization', + description: 'Consider using unchecked blocks for arithmetic operations that cannot overflow', + codeLocation: { startLine: 45, endLine: 48, functionName: 'beforeSwap' }, + specReference: 'Section 3.2 - State Transitions' + }, + { + severity: 'warning', + ruleId: 'SEC-02', + ruleName: 'Reentrancy Check', + description: 'External call detected before state update. Ensure reentrancy protection is in place.', + codeLocation: { startLine: 72, endLine: 75, functionName: 'afterSwap' } + } + ], + verificationTimestamp: Date.now() + }, + cfheResult: params.verifyOnly ? undefined : { + encryptedBytecodeHash: '0xenc' + Math.random().toString(16).slice(2, 30), + deploymentTxHash: '0x' + Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2), + encryptedContractAddress: '0x' + Math.random().toString(16).slice(2, 42), + blockNumber: 18500000 + Math.floor(Math.random() * 10000), + explorerUrl: 'https://explorer.inco.org/tx/0x...' + }, + summary: { + hookName: licenseInfo.hookName, + hookVersion: licenseInfo.hookVersion, + licenseTokenId: licenseInfo.tokenId, + specIpfsCid: licenseInfo.specIpfsCid, + repoUrl: params.repoUrl, + commitHash: 'a1b2c3d4', + submittedAt: Date.now() - 3000, + completedAt: Date.now() + } + }; + + setCodeVerificationResult(mockResult); + setCurrentStep('results'); + return mockResult; + }, [licenseInfo]); + + const goToStep = (step: VerificationStep) => { + const stepOrder: VerificationStep[] = ['submit-code', 'verifying', 'results']; + const currentIndex = stepOrder.indexOf(currentStep); + const targetIndex = stepOrder.indexOf(step); + + if (targetIndex <= currentIndex) { + setCurrentStep(step); + } + }; + + return ( +
+ + + {/* Hero Section */} +
+
+ + +

+ Verify & Deploy Code +

+

+ Link your GitHub repository for AVS verification and CFHE deployment +

+
+
+ + {/* Step Indicator */} +
+
+
+ {STEPS.map((step, index) => { + const stepOrder: VerificationStep[] = ['submit-code', 'verifying', 'results']; + const currentIndex = stepOrder.indexOf(currentStep); + const stepIndex = stepOrder.indexOf(step.id); + const isActive = step.id === currentStep; + const isCompleted = stepIndex < currentIndex; + const isAccessible = stepIndex <= currentIndex; + + return ( +
+ + + {index < STEPS.length - 1 && ( + + )} +
+ ); + })} +
+
+
+ + {/* Main Content */} +
+
+ {/* Code Submission Step */} + {currentStep === 'submit-code' && ( + + )} + + {/* Verifying Step */} + {currentStep === 'verifying' && ( +
+
+ +
+

+ Verification in Progress +

+

+ AVS operators are verifying your code against the HookSpec specification... +

+ + {/* Progress Steps */} +
+ {[ + 'Cloning Repository', + 'Analyzing Contracts', + 'Compiling Solidity', + 'AVS Consensus', + 'CFHE Encryption', + 'Deploying' + ].map((stepName, i) => ( +
+
+ + {stepName} + +
+ ))} +
+
+ )} + + {/* Results Step */} + {currentStep === 'results' && codeVerificationResult && ( +
+ {/* AVS Verification Results */} + + + {/* CFHE Deployment Status */} + {codeVerificationResult.cfheResult && ( + + )} + + {/* Success Summary */} +
+
+ +

+ Verification Complete! +

+
+ +
+
+ + Hook Name + + + {codeVerificationResult.summary.hookName} v{codeVerificationResult.summary.hookVersion} + +
+
+ + License Token ID + + + #{codeVerificationResult.summary.licenseTokenId} + +
+
+ + Compliance Score + + + {codeVerificationResult.avsResult.complianceScore.toFixed(1)}% + +
+
+ + +
+
+ )} +
+
+ +
+
+ ); +} diff --git a/client2/src/components/hook/CreateHookPage.tsx b/client2/src/components/hook/CreateHookPage.tsx new file mode 100644 index 000000000..aa008acc3 --- /dev/null +++ b/client2/src/components/hook/CreateHookPage.tsx @@ -0,0 +1,560 @@ +import { useState, useCallback } from 'react'; +import { ArrowLeft, ChevronRight } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import Navigation from '../Navigation'; +import Footer from '../Footer'; +import StateSpaceModelViewer from './StateSpaceModelViewer'; +import HookSpecEditor from './HookSpecEditor'; +import ValidationResults from './ValidationResults'; +import SubmissionFlow from './SubmissionFlow'; +import type { + HookSpec, + StateSpaceModel, + ValidationResult, + SubmissionResult, + SystemStateVariable +} from '../../types/hookSpec'; + +interface CreateHookPageProps { + onNavigate?: (page: string) => void; +} + +type FlowStep = 'view-model' | 'edit-spec' | 'validate' | 'submit'; + +const STEPS: { id: FlowStep; title: string; description: string }[] = [ + { + id: 'view-model', + title: 'View State Model', + description: 'Review available pool state variables' + }, + { + id: 'edit-spec', + title: 'Write HookSpec', + description: 'Define your hook behavior' + }, + { + id: 'validate', + title: 'Validate', + description: 'Check compatibility' + }, + { + id: 'submit', + title: 'Mint License', + description: 'Mint HookLicense NFT' + } +]; + +// Mock data - replace with API calls +const MOCK_STATE_SPACE_MODEL: StateSpaceModel = { + version: '1.0.0', + ipfsCid: 'QmSystemStateModelV1abc123def456', + uniswapVersion: 'v4', + updatedAt: '2025-12-09', + indices: { + lp: [ + { + name: 'position_liquidity', + symbol: 'L_k', + type: 'uint128', + getter: 'getPositionLiquidity(poolId, positionId)', + description: 'Liquidity amount for a specific position' + }, + { + name: 'tick_lower', + symbol: 't_l^k', + type: 'int24', + getter: 'getPositionInfo(poolId, positionId)', + description: 'Lower tick bound of position' + }, + { + name: 'tick_upper', + symbol: 't_u^k', + type: 'int24', + getter: 'getPositionInfo(poolId, positionId)', + description: 'Upper tick bound of position' + }, + { + name: 'fee_growth_inside_0', + symbol: 'f_{0,k}^{in}', + type: 'uint256', + getter: 'getPositionInfo(poolId, positionId)', + description: 'Fee growth inside position for token0' + }, + { + name: 'fee_growth_inside_1', + symbol: 'f_{1,k}^{in}', + type: 'uint256', + getter: 'getPositionInfo(poolId, positionId)', + description: 'Fee growth inside position for token1' + }, + { + name: 'liquidity_gross', + symbol: 'L_g^{tick}', + type: 'uint128', + getter: 'getTickLiquidity(poolId, tick)', + description: 'Gross liquidity at tick' + }, + { + name: 'liquidity_net', + symbol: 'L_n^{tick}', + type: 'int128', + getter: 'getTickLiquidity(poolId, tick)', + description: 'Net liquidity change at tick' + } + ], + trader: [ + { + name: 'sqrt_price', + symbol: '\\sqrt{P}', + type: 'uint160', + getter: 'getSlot0(poolId)', + description: 'Current sqrt price in Q64.96 format' + }, + { + name: 'current_tick', + symbol: 't_c', + type: 'int24', + getter: 'getSlot0(poolId)', + description: 'Current tick index' + }, + { + name: 'lp_fee', + symbol: '\\phi_{lp}', + type: 'uint24', + getter: 'getSlot0(poolId)', + description: 'Fee paid to liquidity providers' + }, + { + name: 'protocol_fee', + symbol: '\\phi_{proto}', + type: 'uint24', + getter: 'getSlot0(poolId)', + description: 'Protocol fee percentage' + }, + { + name: 'active_liquidity', + symbol: 'L_{act}', + type: 'uint128', + getter: 'getLiquidity(poolId)', + description: 'Currently active liquidity for swaps' + }, + { + name: 'tick_bitmap', + symbol: 'B_{tick}', + type: 'uint256', + getter: 'getTickBitmap(poolId, wordPos)', + description: 'Bitmap for initialized ticks' + } + ], + shared: [ + { + name: 'fee_growth_global_0', + symbol: 'f_0^{global}', + type: 'uint256', + getter: 'getFeeGrowthGlobals(poolId)', + description: 'Cumulative fee growth for token0' + }, + { + name: 'fee_growth_global_1', + symbol: 'f_1^{global}', + type: 'uint256', + getter: 'getFeeGrowthGlobals(poolId)', + description: 'Cumulative fee growth for token1' + } + ] + } +}; + +export default function CreateHookPage({ onNavigate }: CreateHookPageProps) { + const navigate = useNavigate(); + const [currentStep, setCurrentStep] = useState('view-model'); + const [hookSpec, setHookSpec] = useState(null); + const [validationResults, setValidationResults] = useState([]); + const [isValidating, setIsValidating] = useState(false); + const [validationPassed, setValidationPassed] = useState(false); + const [submissionResult, setSubmissionResult] = useState(null); + + // Mock wallet state - replace with actual wallet integration + const [walletConnected, setWalletConnected] = useState(false); + const [walletAddress, setWalletAddress] = useState(undefined); + + const handleNavigate = (page: string) => { + if (onNavigate) { + onNavigate(page); + } else { + navigate(`/${page}`); + } + }; + + const handleSelectVariable = useCallback((variable: SystemStateVariable) => { + // Could open a tooltip or copy to clipboard + console.log('Selected variable:', variable); + }, []); + + const handleSpecChange = useCallback((spec: HookSpec) => { + setHookSpec(spec); + // Reset validation when spec changes + setValidationResults([]); + setValidationPassed(false); + }, []); + + const handleValidate = useCallback(async () => { + if (!hookSpec) return; + + setIsValidating(true); + setValidationResults([]); + + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Mock validation results + const mockResults: ValidationResult[] = [ + { + ruleId: 'V1', + ruleName: 'ValidPoolStateReferences', + passed: true, + errors: [], + warnings: [] + }, + { + ruleId: 'V2', + ruleName: 'UniqueHookStateNames', + passed: hookSpec.hook_state.length === new Set(hookSpec.hook_state.map(v => v.name)).size, + errors: hookSpec.hook_state.length !== new Set(hookSpec.hook_state.map(v => v.name)).size + ? ['Duplicate hook state variable names detected'] + : [], + warnings: [] + }, + { + ruleId: 'V3', + ruleName: 'ValidCallbacks', + passed: true, + errors: [], + warnings: [] + }, + { + ruleId: 'V4', + ruleName: 'ValidEquationSymbols', + passed: true, + errors: [], + warnings: hookSpec.system_functions.some(f => !f.transition.equation) + ? ['Some functions are missing transition equations'] + : [] + }, + { + ruleId: 'V5', + ruleName: 'ValidSolidityTypes', + passed: true, + errors: [], + warnings: [] + }, + { + ruleId: 'V6', + ruleName: 'WritesMatchHookState', + passed: true, + errors: [], + warnings: [] + } + ]; + + setValidationResults(mockResults); + setValidationPassed(mockResults.every(r => r.passed)); + setIsValidating(false); + setCurrentStep('validate'); + }, [hookSpec]); + + const handleSubmit = useCallback(async (): Promise => { + // Simulate submission + await new Promise(resolve => setTimeout(resolve, 2000)); + + const result: SubmissionResult = { + success: true, + ipfsCid: 'QmNewHookSpec' + Math.random().toString(36).slice(2, 10), + tokenId: Math.floor(Math.random() * 1000) + 1, + transactionHash: '0x' + Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2), + ownerAddress: walletAddress || '0x0000000000000000000000000000000000000000', + ipfsGatewayUrl: 'https://ipfs.io/ipfs/QmNewHookSpec...', + openSeaUrl: 'https://opensea.io/assets/ethereum/...' + }; + + setSubmissionResult(result); + return result; + }, [walletAddress]); + + const handleConnectWallet = useCallback(() => { + // Simulate wallet connection + setWalletConnected(true); + setWalletAddress('0x1234567890abcdef1234567890abcdef12345678'); + }, []); + + const goToStep = (step: FlowStep) => { + const stepOrder: FlowStep[] = ['view-model', 'edit-spec', 'validate', 'submit']; + const currentIndex = stepOrder.indexOf(currentStep); + const targetIndex = stepOrder.indexOf(step); + + // Allow going back, or forward if prerequisites are met + if (targetIndex <= currentIndex) { + setCurrentStep(step); + } else if (step === 'submit' && validationPassed) { + setCurrentStep(step); + } + }; + + const nextStep = () => { + const stepOrder: FlowStep[] = ['view-model', 'edit-spec', 'validate', 'submit']; + const currentIndex = stepOrder.indexOf(currentStep); + if (currentIndex < stepOrder.length - 1) { + setCurrentStep(stepOrder[currentIndex + 1]); + } + }; + + const handleContinueToVerification = () => { + // Navigate to CodeVerificationPage with license info + if (submissionResult && hookSpec) { + navigate('/hook-developer/verify', { + state: { + licenseInfo: { + tokenId: submissionResult.tokenId, + hookName: hookSpec.metadata.name, + hookVersion: hookSpec.metadata.version, + specIpfsCid: submissionResult.ipfsCid + }, + submissionResult: submissionResult + } + }); + } + }; + + return ( +
+ + + {/* Hero Section */} +
+
+ + +

+ Create New Hook +

+

+ Define your hook specification, mint license, and deploy verified code +

+
+
+ + {/* Step Indicator */} +
+
+
+ {STEPS.map((step, index) => { + const stepOrder: FlowStep[] = ['view-model', 'edit-spec', 'validate', 'submit']; + const currentIndex = stepOrder.indexOf(currentStep); + const stepIndex = stepOrder.indexOf(step.id); + const isActive = step.id === currentStep; + const isCompleted = stepIndex < currentIndex; + const isAccessible = stepIndex <= currentIndex || + (step.id === 'submit' && validationPassed); + + return ( +
+ + + {index < STEPS.length - 1 && ( + + )} +
+ ); + })} +
+
+
+ + {/* Main Content */} +
+
+
+ {/* Left Panel - State Space Model */} +
+ + + {currentStep === 'view-model' && ( + + )} +
+ + {/* Right Panel - Editor/Validation/Submission */} + {currentStep !== 'view-model' && ( +
+ {(currentStep === 'edit-spec' || currentStep === 'validate') && ( + + )} + + {currentStep === 'validate' && ( + <> + + + {validationPassed && ( + + )} + + )} + + {currentStep === 'submit' && hookSpec && ( + <> + + + {submissionResult && submissionResult.success && ( + + )} + + )} +
+ )} +
+
+
+ +
+
+ ); +} diff --git a/client2/src/components/hook/HookSpecEditor.tsx b/client2/src/components/hook/HookSpecEditor.tsx new file mode 100644 index 000000000..a3cac51e4 --- /dev/null +++ b/client2/src/components/hook/HookSpecEditor.tsx @@ -0,0 +1,898 @@ +import { useState, useCallback } from 'react'; +import { Upload, FileText, AlertCircle, CheckCircle, Info, Eye } from 'lucide-react'; +import type { HookSpec, StateSpaceModel, HookCallback } from '../../types/hookSpec'; +import LatexRenderer from '../common/LatexRenderer'; + +interface HookSpecEditorProps { + initialSpec?: HookSpec; + stateSpaceModel: StateSpaceModel; + onChange: (spec: HookSpec) => void; + onValidate: () => void; + isValidating?: boolean; +} + +const VALID_CALLBACKS: HookCallback[] = [ + 'beforeInitialize', 'afterInitialize', + 'beforeAddLiquidity', 'afterAddLiquidity', + 'beforeRemoveLiquidity', 'afterRemoveLiquidity', + 'beforeSwap', 'afterSwap', + 'beforeDonate', 'afterDonate' +]; + +const SOLIDITY_TYPES = [ + 'uint8', 'uint16', 'uint24', 'uint32', 'uint64', 'uint128', 'uint256', + 'int8', 'int16', 'int24', 'int32', 'int64', 'int128', 'int256', + 'address', 'bool', 'bytes32' +]; + +const DEFAULT_SPEC: HookSpec = { + metadata: { + name: '', + version: '1.0.0', + author: '', + description: '', + license: 'MIT' + }, + hook_state: [], + system_functions: [], + invariants: [] +}; + +export default function HookSpecEditor({ + initialSpec, + stateSpaceModel, + onChange, + onValidate, + isValidating = false +}: HookSpecEditorProps) { + const [spec, setSpec] = useState(initialSpec || DEFAULT_SPEC); + const [activeTab, setActiveTab] = useState<'metadata' | 'state' | 'functions' | 'invariants'>('metadata'); + const [uploadError, setUploadError] = useState(null); + + // Get all available state variables for autocomplete + const availableStateVars = [ + ...stateSpaceModel.indices.lp.map(v => v.name), + ...stateSpaceModel.indices.trader.map(v => v.name), + ...stateSpaceModel.indices.shared.map(v => v.name) + ]; + + const updateSpec = useCallback((updates: Partial) => { + const newSpec = { ...spec, ...updates }; + setSpec(newSpec); + onChange(newSpec); + }, [spec, onChange]); + + const handleFileUpload = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + setUploadError(null); + + try { + // Handle PDF files + if (file.name.endsWith('.pdf')) { + // For PDF, we can't parse it directly in browser without a library + // Inform user that PDF upload will store reference for manual processing + setUploadError( + 'PDF files are accepted for reference. The PDF will be stored alongside your HookSpec. ' + + 'Please also fill in the form fields below to create the machine-readable specification.' + ); + // Store PDF reference in metadata + const pdfUrl = URL.createObjectURL(file); + updateSpec({ + metadata: { + ...spec.metadata, + description: `${spec.metadata.description || ''}\n\n[Uploaded PDF: ${file.name}]`.trim(), + attachments: [{ type: 'pdf', name: file.name, url: pdfUrl }] + } + }); + return; + } + + const content = await file.text(); + let parsed: HookSpec; + + if (file.name.endsWith('.json')) { + parsed = JSON.parse(content); + } else if (file.name.endsWith('.yaml') || file.name.endsWith('.yml')) { + // Simple YAML parsing (in production, use a proper YAML library) + setUploadError('YAML parsing requires a YAML library. Please use JSON format.'); + return; + } else { + setUploadError('Unsupported file format. Supported formats: .json, .yaml, .yml, .pdf'); + return; + } + + // Basic validation + if (!parsed.metadata?.name || !parsed.metadata?.version) { + setUploadError('Invalid HookSpec: missing required metadata fields'); + return; + } + + setSpec(parsed); + onChange(parsed); + } catch (error) { + setUploadError(`Failed to parse file: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }; + + const addHookStateVariable = () => { + updateSpec({ + hook_state: [ + ...spec.hook_state, + { name: '', symbol: '', type: 'uint256', description: '' } + ] + }); + }; + + const removeHookStateVariable = (index: number) => { + updateSpec({ + hook_state: spec.hook_state.filter((_, i) => i !== index) + }); + }; + + const updateHookStateVariable = (index: number, field: string, value: string) => { + const newState = [...spec.hook_state]; + newState[index] = { ...newState[index], [field]: value }; + updateSpec({ hook_state: newState }); + }; + + const addSystemFunction = () => { + updateSpec({ + system_functions: [ + ...spec.system_functions, + { + callback: 'afterSwap', + reads: [], + writes: [], + transition: { + preconditions: [], + equation: '', + postconditions: [] + } + } + ] + }); + }; + + const removeSystemFunction = (index: number) => { + updateSpec({ + system_functions: spec.system_functions.filter((_, i) => i !== index) + }); + }; + + const addInvariant = () => { + updateSpec({ + invariants: [ + ...(spec.invariants || []), + { name: '', expression: '', description: '' } + ] + }); + }; + + const removeInvariant = (index: number) => { + updateSpec({ + invariants: (spec.invariants || []).filter((_, i) => i !== index) + }); + }; + + return ( +
+ {/* Header */} +
+
+

+ HookSpec Editor +

+

+ Define your hook's state variables and system functions +

+
+ + {/* Upload Button */} + +
+ + {/* Upload Error */} + {uploadError && ( +
+ + + {uploadError} + +
+ )} + + {/* Tabs */} +
+ {(['metadata', 'state', 'functions', 'invariants'] as const).map(tab => ( + + ))} +
+ + {/* Metadata Tab */} + {activeTab === 'metadata' && ( +
+
+
+ + updateSpec({ + metadata: { ...spec.metadata, name: e.target.value } + })} + placeholder="e.g., DynamicFeeHook" + className="angular-clip w-full px-3 py-2 font-body" + style={{ + background: 'var(--color-marble-light)', + border: '2px solid var(--color-secondary)', + color: 'var(--color-secondary)', + fontSize: 'var(--font-size-body-sm)' + }} + /> +
+ +
+ + updateSpec({ + metadata: { ...spec.metadata, version: e.target.value } + })} + placeholder="1.0.0" + className="angular-clip w-full px-3 py-2 font-body" + style={{ + background: 'var(--color-marble-light)', + border: '2px solid var(--color-secondary)', + color: 'var(--color-secondary)', + fontSize: 'var(--font-size-body-sm)' + }} + /> +
+ +
+ + updateSpec({ + metadata: { ...spec.metadata, author: e.target.value } + })} + placeholder="0x..." + className="angular-clip w-full px-3 py-2 font-mono" + style={{ + background: 'var(--color-marble-light)', + border: '2px solid var(--color-secondary)', + color: 'var(--color-secondary)', + fontSize: 'var(--font-size-body-sm)' + }} + /> +
+ +
+ + +
+
+ +
+ +