diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56f92b9..d68d5e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,9 @@ jobs: with: version: nightly + - name: Generate Prisma client + run: pnpm db:generate + - name: Lint run: pnpm lint diff --git a/README.md b/README.md index 7726f26..fb1a544 100644 --- a/README.md +++ b/README.md @@ -14,44 +14,14 @@ CAST QUEST Frames is a Web3-native social photo protocol that feels like Instagr - Builder-first: Frames, SDK, and a Remix-style module builder - AI-native: Smart Brain agents for pricing, previews, tagging, and system optimization -## 🧱 Monorepo Structure - -apps/ - web/ - Next.js app (feed, profiles, frames) - admin/ - Next.js admin app (fees, templates, AI settings) - mobile/ - React Native / Expo app - -packages/ - contracts/ - Solidity contracts (Base + EVM) - sdk/ - CAST QUEST SDK integration (used here) - ai-brain/ - Multi-agent Smart Brain orchestration - ui-kit/ - Shared UI components & design system - -infra/ - api-gateway/ - API entrypoint (frames, mint, collect) - indexer/ - Onchain event indexer - workers/ - Background jobs (AI, notifications, analytics) - k8s/ - Kubernetes manifests - -docs/ - whitepaper/ - Vision & protocol - architecture/- Diagrams & technical design - sdk/ - Dev docs & examples - product/ - User & admin guides - -.github/ - CI/CD, issue templates, PR templates -scripts/ - Dev, deploy, Smart Brain operator -examples/frames/ - Example frame definitions - -## 🀝 Contributing - -See CONTRIBUTING.md. +## πŸš€ Quick Start -## οΏ½ Prerequisites +### Prerequisites **Required:** - **Node.js 20+** (to prevent ERR_INVALID_THIS errors) - **pnpm 9+** (package manager) +- **PostgreSQL 14+** (for database) **Install with nvm:** ```bash @@ -60,179 +30,245 @@ nvm use 20 npm install -g pnpm@9 ``` -**Or use the .nvmrc file:** -```bash -nvm use # Automatically uses Node 20.19.6 -``` +### Installation -## 🎨 Dashboards +```bash +# Clone the repository +git clone https://github.com/CastQuest/castquest-frames.git +cd castquest-frames -CastQuest Frames includes two production-ready dashboards with **neo-glow theme** for creators and administrators. +# Install dependencies +pnpm install -### πŸ‘€ User Dashboard -**Port:** 3000 | **URL:** http://localhost:3000/dashboard +# Set up environment variables +cp .env.example .env.local +# Edit .env.local with your values -A creator-focused dashboard with AI tools and community features: -- ✨ **AI Frame Builder** - Generate frames with natural language -- πŸ“Š **Analytics** - Track views, engagement, and revenue -- πŸͺ **Marketplace** - Browse and purchase frame templates -- πŸ’¬ **Community Hub** - Social feed with interactions -- 🎯 **Frame Management** - Create and monitor frames -- πŸ† **Leaderboard** - Global rankings and achievements -- ⚑ **Quest System** - Daily/weekly challenges -- πŸ’Ž **NFT Mints** - Manage collectible mints +# Initialize database +pnpm db:push -```bash -# Start user dashboard -cd apps/web && pnpm dev -# Access: http://localhost:3000/dashboard +# Start development +pnpm dev ``` -### πŸ‘‘ Admin Dashboard -**Port:** 3010 | **URL:** http://localhost:3010/dashboard +### Environment Variables -A protocol management console with comprehensive monitoring: -- πŸ’Ž **Token Management** - Monitor $CAST, $PIC, $VID, $AUDIO -- πŸ” **Permission System** - Role-based access control -- πŸ’° **Fee Controls** - Adjustable protocol fees -- πŸ›‘οΈ **Risk Management** - AI-powered detection (98% accuracy) -- πŸ“Š **Protocol Metrics** - TVL, volume, active users -- πŸ“‘ **System Health** - Real-time monitoring -- πŸ–ΌοΈ **Frame Monitoring** - Track all frame activity -- πŸ“‹ **Activity Logs** - Complete audit trail +Copy `.env.example` to `.env.local` and configure: ```bash -# Start admin dashboard -cd apps/admin && pnpm dev -- -p 3010 -# Access: http://localhost:3010/dashboard +# Database +DATABASE_URL="postgresql://user:password@localhost:5432/castquest" + +# Auth.js +NEXTAUTH_SECRET="your-secret-key" # Generate: openssl rand -base64 32 +NEXTAUTH_URL="http://localhost:3000" + +# Web3 +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="your-project-id" +NEXT_PUBLIC_ALCHEMY_ID="your-alchemy-key" + +# Contract Deployment (Admin only) +ADMIN_PRIVATE_KEY="your-deployer-private-key" + +# AI Agents +OPENAI_API_KEY="sk-your-api-key" + +# Feature Flags +NEXT_PUBLIC_WEB3_ENABLED="true" +NEXT_PUBLIC_AGENTS_ENABLED="true" ``` -### πŸš€ Quick Start - Both Dashboards +### Database Setup ```bash -# Install dependencies -pnpm install +# Generate Prisma client +pnpm db:generate -# Run both dashboards using self-healing script (recommended) -chmod +x scripts/self-healing-ui.sh -./scripts/self-healing-ui.sh +# Push schema to database +pnpm db:push -# Or run manually in separate terminals: -# Terminal 1: User Dashboard -cd apps/web && pnpm dev +# Run migrations (development) +pnpm db:migrate:dev -# Terminal 2: Admin Dashboard -cd apps/admin && pnpm dev -- -p 3010 +# Run migrations (production β€” applies pending migrations without prompting) +pnpm db:migrate + +# Open Prisma Studio +pnpm db:studio ``` -πŸ“– **Full Documentation:** See [docs/DASHBOARDS.md](./docs/DASHBOARDS.md) for complete setup, configuration, deployment, and troubleshooting guides. +## 🧱 Monorepo Structure -## πŸ₯ Repository Health +``` +apps/ + web/ - Next.js 15 user app (feed, profiles, frames) + admin/ - Next.js 15 admin app (fees, templates, AI settings) + mobile/ - React Native / Expo app (future) -The CastQuest Frames repository includes a comprehensive dependency health monitoring system to ensure consistency, security, and reliability. +packages/ + contracts/ - Solidity contracts (Base + EVM, Foundry) + sdk/ - CAST QUEST SDK integration + neo-ux-core/ - Shared UI components & Neon Glass design system + ai-brain/ - Multi-agent Smart Brain orchestration + core-services/- Backend services and database layer + frames/ - Farcaster Frame protocol support + strategy-worker/ - Background job processing -### Health Check Commands +prisma/ + schema.prisma - Database schema (Users, Roles, Permissions, Feature Flags, Agents, Contracts) -```bash -# Run comprehensive health check -bash scripts/master.sh health +scripts/ - Development and deployment automation +docs/ - Documentation, whitepapers, technical guides +examples/ - Example frame definitions and usage +``` -# Run automated repair -bash scripts/repair-dependencies.sh +## 🎨 Dashboards -# Get AI-powered insights -.smartbrain/oracle.sh analyze +### πŸ‘€ User Dashboard (Port 3000) + +```bash +cd apps/web && pnpm dev +# Access: http://localhost:3000 +``` + +Features: +- ✨ AI Frame Builder +- πŸ“Š Analytics & Metrics +- πŸͺ Frame Marketplace +- πŸ’¬ Community Hub +- 🎯 Frame Management +- ⚑ Quest System +- πŸ’Ž NFT Mints -# Get upgrade recommendations -.smartbrain/oracle.sh recommend-upgrades +### πŸ‘‘ Admin Dashboard (Port 3001) -# Security vulnerability scan -.smartbrain/oracle.sh security-scan +```bash +cd apps/admin && pnpm dev +# Access: http://localhost:3001 ``` -### Automated Monitoring +Features: +- πŸ” User & Role Management +- 🚩 Feature Flags Control +- πŸ“œ Contract Deployment +- πŸ€– Agent Configuration +- πŸ“Š System Monitoring +- πŸ›‘οΈ RBAC Permissions -- **CI/CD Health Checks**: Automated health checks run on every push, PR, and daily at 6 AM UTC -- **Pre-commit Hooks**: Validate changes before they reach the repository -- **Smart Brain Oracle**: AI-powered dependency intelligence and predictive maintenance +## πŸ” Authentication -### Key Features +CastQuest uses **Auth.js (NextAuth v5)** for authentication: -- βœ… **Version Harmonization**: TypeScript 5.3.3, @types/node 20.10.6, Next.js 14.2.35 (secure) -- πŸ”’ **Security Scanning**: Automated vulnerability detection with pnpm audit -- πŸ“Š **Health Scoring**: Real-time repository health metrics -- πŸ€– **AI Insights**: Smart Brain Oracle for predictive maintenance -- πŸ› οΈ **Auto-Repair**: One-command dependency repair script -- πŸ“ **Comprehensive Reports**: JSON output for CI/CD integration +- Credentials-based login (email/password) +- JWT session strategy +- Role-based access control (RBAC) +- Protected routes via middleware -πŸ“– **Full Documentation:** See [docs/DEPENDENCY-HEALTH.md](./docs/DEPENDENCY-HEALTH.md) for detailed health monitoring guide. +### User Roles -## πŸ’Έ Sponsors & Partners +| Role | Access | +|------|--------| +| ADMIN | Full system access, user management, deployments | +| OPERATOR | Manage quests, frames, run agents | +| CREATOR | Create content, manage own frames | +| VIEWER | Read-only access | -Site: https://castquest.xyz (placeholder) -Docs: https://docs.castquest.xyz (placeholder) -Sponsors: GitHub Sponsors / custom page +## 🌐 Web3 Integration -## 🌌 Vision +- **RainbowKit** for wallet connection +- **Wagmi** for Ethereum interactions +- **viem** for low-level blockchain operations -Social feeds will be onchain. -Creators will own their rails. -Builders will extend everything through Frames and SDKs. -AI will act as the invisible Smart Brain across the entire stack. +### Supported Chains +- Ethereum Mainnet (1) +- Base (8453) ← Primary +- Optimism (10) +- Arbitrum (42161) +- Polygon (137) +- Base Sepolia (84532) ← Testnet +- Sepolia (11155111) ← Testnet -# CastQuest Protocol β€” Operator Console +## πŸ€– Smart Brain Agents -A sovereign, media‑first automation protocol combining: +AI-powered automation agents: -- Frame Template Engine (Module 6 MEGA) -- Quest Engine (Module 5B MEGA) -- Mint Engine + Renderer + Automation (Module 7 MEGA) -- BASE Mock Onchain Layer (Module 4 MEGA) -- Mobile‑Optimized Admin Console -- Strategy Worker + Logs Dashboard +| Agent | Function | +|-------|----------| +| FramePricingAgent | Market analysis, pricing recommendations | +| ContentModerationAgent | Content scanning, policy enforcement | +| QuestCompletionAgent | Quest validation, reward distribution | +| SmartBrainOrchestrator | Agent coordination, workflow optimization | -## Contributors +## πŸ“œ Smart Contract Deployment -- **Yosef (Founder / Protocol Architect)** - Vision, architecture, automation engine, operator console design. +Deploy contracts directly from the Admin dashboard: -- **SMSDAO (Core Contributor)** - Execution, module integration, system orchestration. +1. Navigate to `/admin/contracts` +2. Enter Solidity source code or paste pre-compiled bytecode +3. Select target chain +4. Deploy with one click -- **AI Automation Partner** - Script generation, module scaffolding, error‑resilient workflows. +Supports verification on Etherscan/Basescan. -## Modules Installed +## πŸ₯ Health Monitoring -### Module 4 β€” BASE API + Mobile Admin + Strategy Dashboard -- `/api/base/*` -- `/strategy` -- ShellLayout mobile UI +```bash +# Run comprehensive health check +bash scripts/master.sh health -### Module 5B β€” Quest Engine MEGA -- `/quests` -- `/api/quests/*` -- `data/quests.json`, `quest-steps.json`, `quest-progress.json`, `quest-rewards.json` +# Run automated repair +bash scripts/repair-dependencies.sh -### Module 6 β€” Frame Template Engine MEGA -- `/frame-templates` -- `/api/frame-templates/*` -- `data/frame-templates.json` +# AI-powered insights +.smartbrain/oracle.sh analyze +``` + +## πŸ“ Available Scripts + +```bash +# Development +pnpm dev # Start web app +pnpm dev:admin # Start admin app +pnpm dev:all # Start both apps + +# Building +pnpm build # Build all packages +pnpm lint # Run linters +pnpm typecheck # Type checking + +# Database +pnpm db:generate # Generate Prisma client +pnpm db:push # Push schema to DB +pnpm db:migrate:dev # Run migrations (development) +pnpm db:migrate # Run migrations (production) +pnpm db:studio # Open Prisma Studio +``` -### Module 7 β€” Mint + Render + Automation MEGA -- `/mints` -- `/api/mints/*` -- `/api/frames/render` -- `/api/strategy/worker/*` -- `data/mints.json`, `mint-events.json`, `frames.json`, `worker-events.json` +## 🀝 Contributing -## Vision +See [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines. -CastQuest is a sovereign automation protocol that turns: +## πŸ“– Documentation -**Media β†’ Templates β†’ Frames β†’ Mints β†’ Quests β†’ Strategy β†’ Onchain** +- [Architecture Overview](./docs/architecture/) +- [SDK Documentation](./docs/sdk/) +- [API Reference](./docs/api/) +- [Dashboard Guide](./docs/DASHBOARDS.md) +- [Dependency Health](./docs/DEPENDENCY-HEALTH.md) + +## πŸ’Έ Sponsors & Partners -into a single expressive pipeline. +- Site: https://castquest.xyz +- Docs: https://docs.castquest.xyz + +## 🌌 Vision + +Social feeds will be onchain. +Creators will own their rails. +Builders will extend everything through Frames and SDKs. +AI will act as the invisible Smart Brain across the entire stack. + +**Media β†’ Templates β†’ Frames β†’ Mints β†’ Quests β†’ Strategy β†’ Onchain** You are early. diff --git a/apps/admin/.env.example b/apps/admin/.env.example index 98db285..b1aa875 100644 --- a/apps/admin/.env.example +++ b/apps/admin/.env.example @@ -7,3 +7,10 @@ DATABASE_URL= # API Configuration NEXT_PUBLIC_API_URL=http://localhost:4000 + +# Admin API key β€” required for all /api/contracts and /api/feature-flags routes +# Used to authenticate admin API route requests β€” keep this value secret +ADMIN_API_KEY=change-me-to-a-secure-random-value + +# Contract deployer private key (hex, without 0x prefix) +ADMIN_PRIVATE_KEY= diff --git a/apps/admin/actions/agents.ts b/apps/admin/actions/agents.ts new file mode 100644 index 0000000..8450e3e --- /dev/null +++ b/apps/admin/actions/agents.ts @@ -0,0 +1,297 @@ +"use server" + +import { revalidatePath } from "next/cache" + +// Types +interface AgentConfig { + id: string + name: string + description: string | null + enabled: boolean + config: Record + schedule: string | null + lastRun: Date | null + lastResult: Record | null +} + +interface AgentExecution { + id: string + agentId: string + status: "pending" | "running" | "completed" | "failed" + input: Record | null + output: Record | null + error: string | null + duration: number | null + triggeredBy: "manual" | "schedule" | "webhook" + userId: string | null + startedAt: Date + completedAt: Date | null +} + +// In-memory store for demo (replace with Prisma in production) +const agentStore = new Map([ + ["frame-pricing", { + id: "frame-pricing", + name: "FramePricingAgent", + description: "AI agent that analyzes market data and suggests optimal pricing for frame mints", + enabled: true, + config: { model: "gpt-4-turbo", maxTokens: 2000, analysisDepth: "full" }, + schedule: "0 */6 * * *", + lastRun: new Date("2024-03-15T18:00:00Z"), + lastResult: { status: "success", framesAnalyzed: 150, recommendationsUpdated: 23 }, + }], + ["content-moderation", { + id: "content-moderation", + name: "ContentModerationAgent", + description: "Scans uploaded media for policy violations and inappropriate content", + enabled: true, + config: { model: "gpt-4-vision", confidenceThreshold: 0.85, categories: ["nsfw", "violence", "spam"] }, + schedule: "*/15 * * * *", + lastRun: new Date("2024-03-15T20:15:00Z"), + lastResult: { status: "success", reviewed: 45, flagged: 2 }, + }], + ["quest-completion", { + id: "quest-completion", + name: "QuestCompletionAgent", + description: "Validates quest completion criteria and triggers rewards distribution", + enabled: true, + config: { batchSize: 100, retryAttempts: 3, rewardMultiplier: 1.0 }, + schedule: "*/5 * * * *", + lastRun: new Date("2024-03-15T20:20:00Z"), + lastResult: { status: "success", processed: 89, rewardsDistributed: "2.5 ETH" }, + }], +]) + +const executionStore: AgentExecution[] = [] + +// ============================================================================ +// AGENT CONFIGURATION +// ============================================================================ + +export async function getAgents(): Promise<{ agents: AgentConfig[] }> { + return { agents: Array.from(agentStore.values()) } +} + +export async function getAgent(id: string): Promise<{ agent: AgentConfig | null }> { + return { agent: agentStore.get(id) || null } +} + +export async function createAgent( + name: string, + description: string, + config: Record, + schedule?: string +): Promise<{ success: boolean; agent?: AgentConfig; error?: string }> { + const id = name.toLowerCase().replace(/[^a-z0-9]/g, "-") + + if (agentStore.has(id)) { + return { success: false, error: "Agent with this name already exists" } + } + + const agent: AgentConfig = { + id, + name, + description, + enabled: false, + config, + schedule: schedule || null, + lastRun: null, + lastResult: null, + } + + agentStore.set(id, agent) + revalidatePath("/admin/agents") + + return { success: true, agent } +} + +export async function updateAgent( + id: string, + updates: Partial> +): Promise<{ success: boolean; error?: string }> { + const agent = agentStore.get(id) + if (!agent) { + return { success: false, error: "Agent not found" } + } + + agentStore.set(id, { ...agent, ...updates }) + revalidatePath("/admin/agents") + + return { success: true } +} + +export async function toggleAgent(id: string, enabled: boolean): Promise<{ success: boolean; error?: string }> { + const agent = agentStore.get(id) + if (!agent) { + return { success: false, error: "Agent not found" } + } + + agent.enabled = enabled + agentStore.set(id, agent) + revalidatePath("/admin/agents") + + return { success: true } +} + +export async function deleteAgent(id: string): Promise<{ success: boolean; error?: string }> { + if (!agentStore.has(id)) { + return { success: false, error: "Agent not found" } + } + + agentStore.delete(id) + revalidatePath("/admin/agents") + + return { success: true } +} + +// ============================================================================ +// AGENT EXECUTION +// ============================================================================ + +export async function runAgent( + agentId: string, + input?: Record, + triggeredBy: "manual" | "schedule" | "webhook" = "manual", + userId?: string +): Promise<{ success: boolean; executionId?: string; error?: string }> { + const agent = agentStore.get(agentId) + if (!agent) { + return { success: false, error: "Agent not found" } + } + + if (!agent.enabled) { + return { success: false, error: "Agent is disabled" } + } + + const execution: AgentExecution = { + id: `exec-${Date.now()}`, + agentId, + status: "running", + input: input || null, + output: null, + error: null, + duration: null, + triggeredBy, + userId: userId || null, + startedAt: new Date(), + completedAt: null, + } + + executionStore.unshift(execution) + + // Execute synchronously within the request lifecycle to avoid dropped work + // in serverless environments where the process may be frozen after the response. + try { + const startTime = Date.now() + const result = await executeAgentLogic(agent, input) + const duration = Date.now() - startTime + + execution.status = "completed" + execution.output = result + execution.duration = duration + execution.completedAt = new Date() + + agent.lastRun = new Date() + agent.lastResult = { status: "success", ...result } + agentStore.set(agentId, agent) + } catch (error) { + execution.status = "failed" + execution.error = error instanceof Error ? error.message : "Unknown error" + execution.completedAt = new Date() + + agent.lastRun = new Date() + agent.lastResult = { status: "failed", error: execution.error } + agentStore.set(agentId, agent) + } + + return { success: true, executionId: execution.id } +} + +async function executeAgentLogic( + agent: AgentConfig, + input?: Record +): Promise> { + // Simulate different agent behaviors + switch (agent.name) { + case "FramePricingAgent": + return { + framesAnalyzed: Math.floor(Math.random() * 100) + 50, + recommendationsUpdated: Math.floor(Math.random() * 30), + averagePriceChange: `${(Math.random() * 10 - 5).toFixed(2)}%`, + } + + case "ContentModerationAgent": + return { + reviewed: Math.floor(Math.random() * 50) + 20, + flagged: Math.floor(Math.random() * 5), + categories: { nsfw: 0, violence: 1, spam: 2 }, + } + + case "QuestCompletionAgent": + return { + processed: Math.floor(Math.random() * 100) + 50, + rewardsDistributed: `${(Math.random() * 5).toFixed(2)} ETH`, + failedValidations: Math.floor(Math.random() * 5), + } + + default: + return { + executed: true, + timestamp: new Date().toISOString(), + input: input || {}, + } + } +} + +export async function getExecutions( + agentId?: string, + limit: number = 50 +): Promise<{ executions: AgentExecution[] }> { + let executions = executionStore + + if (agentId) { + executions = executions.filter(e => e.agentId === agentId) + } + + return { executions: executions.slice(0, limit) } +} + +export async function getExecution(id: string): Promise<{ execution: AgentExecution | null }> { + return { execution: executionStore.find(e => e.id === id) || null } +} + +// ============================================================================ +// AGENT ANALYTICS +// ============================================================================ + +export async function getAgentStats(): Promise<{ + totalAgents: number + activeAgents: number + totalExecutions: number + successRate: number + averageDuration: number +}> { + const agents = Array.from(agentStore.values()) + const completedExecutions = executionStore.filter(e => e.status === "completed") + const failedExecutions = executionStore.filter(e => e.status === "failed") + + const totalExecutions = completedExecutions.length + failedExecutions.length + const successRate = totalExecutions > 0 + ? (completedExecutions.length / totalExecutions) * 100 + : 100 + + const durations = completedExecutions + .filter(e => e.duration !== null) + .map(e => e.duration as number) + const averageDuration = durations.length > 0 + ? durations.reduce((a, b) => a + b, 0) / durations.length + : 0 + + return { + totalAgents: agents.length, + activeAgents: agents.filter(a => a.enabled).length, + totalExecutions, + successRate: Math.round(successRate * 100) / 100, + averageDuration: Math.round(averageDuration), + } +} diff --git a/apps/admin/actions/deploy-contract.ts b/apps/admin/actions/deploy-contract.ts new file mode 100644 index 0000000..6bf8c6f --- /dev/null +++ b/apps/admin/actions/deploy-contract.ts @@ -0,0 +1,221 @@ +"use server" + +import { createPublicClient, createWalletClient, http, encodeAbiParameters } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import { mainnet, base, optimism, arbitrum, polygon, baseSepolia, sepolia } from "viem/chains" + +// Chain configurations +const chainConfigs: Record = { + 1: { chain: mainnet, rpcUrl: process.env.ETH_RPC_URL || "https://eth.llamarpc.com" }, + 8453: { chain: base, rpcUrl: process.env.BASE_RPC_URL || "https://mainnet.base.org" }, + 10: { chain: optimism, rpcUrl: process.env.OP_RPC_URL || "https://mainnet.optimism.io" }, + 42161: { chain: arbitrum, rpcUrl: process.env.ARB_RPC_URL || "https://arb1.arbitrum.io/rpc" }, + 137: { chain: polygon, rpcUrl: process.env.POLYGON_RPC_URL || "https://polygon-rpc.com" }, + 84532: { chain: baseSepolia, rpcUrl: "https://sepolia.base.org" }, + 11155111: { chain: sepolia, rpcUrl: process.env.SEPOLIA_RPC_URL || "https://rpc.sepolia.org" }, +} + +interface DeployContractInput { + name: string + chainId: number + bytecode?: string + constructorArgs?: unknown[] +} + +interface DeploymentResult { + success: boolean + deployment?: { + address: string + txHash: string + chainId: number + blockNumber: number + gasUsed: string + } + error?: string +} + +// Simple ERC20 bytecode (pre-compiled) for demo purposes +// In production, you would compile using solc or use Hardhat/Foundry +const DEMO_ERC20_BYTECODE = "0x608060405234801561001057600080fd5b506040516109e93803806109e983398101604081905261002f916100f8565b604051806040016040528060098152602001684361737451756573746360b81b815250600090816100609190610205565b5060408051808201909152600481526321a4a9a360e11b602082015260019061008990826102c4565b506002805460ff191660121790556100ab81670de0b6b3a764000061038e565b60038190553360009081526004602052604081209190915560038054909161010d913391907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9061010590565b0390a3506103b5565b60006020828403121561010a57600080fd5b5051919050565b634e487b7160e01b600052604160045260246000fd5b600181811c9082168061013b57607f821691505b60208210810361015b57634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156101aa57600081815260208120601f850160051c810160208610156101895750805b601f850160051c820191505b818110156101a857828155600101610195565b505b505050565b81516001600160401b038111156101c8576101c8610111565b6101dc816101d68454610127565b84610161565b602080601f83116001811461021157600084156101f95750858301515b600019600386901b1c1916600185901b1785556101a8565b600085815260208120601f198616915b8281101561024057888601518255948401946001909101908401610221565b508582101561025e5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b634e487b7160e01b600052601160045260246000fd5b80820281158282048414176102a9576102a961027e565b92915050565b634e487b7160e01b600052601260045260246000fd5b818103818111156102a9576102a961027e565b6106258061039e6000396000f3fe" + +const DEMO_ERC20_ABI = [ + { inputs: [{ name: "_initialSupply", type: "uint256" }], stateMutability: "nonpayable", type: "constructor" }, + { inputs: [], name: "name", outputs: [{ type: "string" }], stateMutability: "view", type: "function" }, + { inputs: [], name: "symbol", outputs: [{ type: "string" }], stateMutability: "view", type: "function" }, + { inputs: [], name: "decimals", outputs: [{ type: "uint8" }], stateMutability: "view", type: "function" }, + { inputs: [], name: "totalSupply", outputs: [{ type: "uint256" }], stateMutability: "view", type: "function" }, + { inputs: [{ name: "owner", type: "address" }], name: "balanceOf", outputs: [{ type: "uint256" }], stateMutability: "view", type: "function" }, + { inputs: [{ name: "to", type: "address" }, { name: "value", type: "uint256" }], name: "transfer", outputs: [{ type: "bool" }], stateMutability: "nonpayable", type: "function" }, + { anonymous: false, inputs: [{ indexed: true, name: "from", type: "address" }, { indexed: true, name: "to", type: "address" }, { indexed: false, name: "value", type: "uint256" }], name: "Transfer", type: "event" }, +] + +export async function deployContract(input: DeployContractInput): Promise { + // Defense-in-depth: this action uses ADMIN_PRIVATE_KEY and must only be called + // from authenticated admin contexts. Verify the ADMIN_API_KEY is set to ensure + // the action is not accidentally invoked in an unauthenticated path. + if (!process.env.ADMIN_API_KEY) { + return { success: false, error: "Admin API key not configured β€” deployment disabled" } + } + + const { chainId, bytecode, constructorArgs = [] } = input + + // Validate chain + const chainConfig = chainConfigs[chainId] + if (!chainConfig) { + return { success: false, error: `Unsupported chain ID: ${chainId}` } + } + + // Get private key from environment + const privateKey = process.env.ADMIN_PRIVATE_KEY + if (!privateKey) { + return { success: false, error: "ADMIN_PRIVATE_KEY not configured" } + } + + try { + // Create account from private key + const account = privateKeyToAccount(`0x${privateKey.replace("0x", "")}`) + + // Create clients + const publicClient = createPublicClient({ + chain: chainConfig.chain, + transport: http(chainConfig.rpcUrl), + }) + + const walletClient = createWalletClient({ + account, + chain: chainConfig.chain, + transport: http(chainConfig.rpcUrl), + }) + + // Use provided bytecode or demo bytecode + const deployBytecode = bytecode || DEMO_ERC20_BYTECODE + + // Encode constructor arguments + let encodedArgs = "" + if (constructorArgs.length > 0) { + // Simple encoding for uint256 constructor arg + const argValue = BigInt(constructorArgs[0] as string || "1000000") + encodedArgs = encodeAbiParameters( + [{ type: "uint256" }], + [argValue] + ).slice(2) // Remove 0x prefix + } + + // Combine bytecode and constructor args + const deployData = `${deployBytecode}${encodedArgs}` as `0x${string}` + + // Check balance + const balance = await publicClient.getBalance({ address: account.address }) + if (balance === BigInt(0)) { + return { + success: false, + error: `Deployer account ${account.address} has no funds on chain ${chainId}` + } + } + + // Estimate gas and add 20% safety margin + const gasEstimate = await publicClient.estimateGas({ + account: account.address, + data: deployData, + }) + const gasWithMargin = (gasEstimate * 120n) / 100n + + // Deploy contract + const hash = await walletClient.deployContract({ + abi: DEMO_ERC20_ABI, + bytecode: deployBytecode as `0x${string}`, + args: constructorArgs.length > 0 ? [BigInt(constructorArgs[0] as string)] : undefined, + gas: gasWithMargin, + }) + + // Wait for receipt + const receipt = await publicClient.waitForTransactionReceipt({ hash }) + + if (!receipt.contractAddress) { + return { success: false, error: "Contract deployment failed - no address returned" } + } + + return { + success: true, + deployment: { + address: receipt.contractAddress, + txHash: hash, + chainId, + blockNumber: Number(receipt.blockNumber), + gasUsed: receipt.gasUsed.toString(), + }, + } + } catch (error) { + console.error("Contract deployment error:", error) + return { + success: false, + error: error instanceof Error ? error.message : "Unknown deployment error", + } + } +} + +export async function getDeploymentStatus(txHash: string, chainId: number) { + const chainConfig = chainConfigs[chainId] + if (!chainConfig) { + return { success: false, error: "Unsupported chain" } + } + + try { + const publicClient = createPublicClient({ + chain: chainConfig.chain, + transport: http(chainConfig.rpcUrl), + }) + + const receipt = await publicClient.getTransactionReceipt({ hash: txHash as `0x${string}` }) + + return { + success: true, + status: receipt.status === "success" ? "deployed" : "failed", + contractAddress: receipt.contractAddress, + blockNumber: Number(receipt.blockNumber), + gasUsed: receipt.gasUsed.toString(), + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Failed to get deployment status", + } + } +} + +export async function verifyContract( + chainId: number, + address: string, + sourceCode: string, + constructorArgs?: string +) { + // This would integrate with Etherscan/Basescan API for verification + // For now, return a placeholder response + + const apiKeys: Record = { + 1: process.env.ETHERSCAN_API_KEY, + 8453: process.env.BASESCAN_API_KEY, + 10: process.env.OPTIMISM_API_KEY, + 42161: process.env.ARBISCAN_API_KEY, + 137: process.env.POLYGONSCAN_API_KEY, + } + + const apiKey = apiKeys[chainId] + if (!apiKey) { + return { + success: false, + error: `API key not configured for chain ${chainId}. Set the appropriate *SCAN_API_KEY environment variable.` + } + } + + // In production, you would: + // 1. Submit verification request to block explorer API + // 2. Poll for verification status + // 3. Return verification result + + return { + success: true, + message: "Verification request submitted. Check block explorer for status.", + verificationId: `verify-${Date.now()}`, + } +} diff --git a/apps/admin/actions/feature-flags.ts b/apps/admin/actions/feature-flags.ts new file mode 100644 index 0000000..f72badc --- /dev/null +++ b/apps/admin/actions/feature-flags.ts @@ -0,0 +1,44 @@ +"use server" + +// In-memory store for demo (replace with Prisma in production) +// Shared with the API route β€” both are on the same server process. +export const flagStore = new Map([ + ["WEB3_ENABLED", { id: "1", key: "WEB3_ENABLED", enabled: true, description: "Enable Web3 wallet features" }], + ["AGENTS_ENABLED", { id: "2", key: "AGENTS_ENABLED", enabled: true, description: "Enable AI agent workflows" }], + ["CONTRACT_DEPLOY_ENABLED", { id: "3", key: "CONTRACT_DEPLOY_ENABLED", enabled: true, description: "Enable contract deployment" }], + ["QUESTS_ENABLED", { id: "4", key: "QUESTS_ENABLED", enabled: true, description: "Enable quest system" }], + ["FRAMES_V2_ENABLED", { id: "5", key: "FRAMES_V2_ENABLED", enabled: false, description: "Enable Frames v2 features" }], + ["ANALYTICS_ENABLED", { id: "6", key: "ANALYTICS_ENABLED", enabled: true, description: "Enable analytics" }], +]) + +export type FeatureFlag = { id: string; key: string; enabled: boolean; description: string } + +export async function getFeatureFlags(): Promise<{ flags: FeatureFlag[] }> { + return { flags: Array.from(flagStore.values()) } +} + +export async function toggleFeatureFlag(key: string, enabled: boolean): Promise<{ flag?: FeatureFlag; error?: string }> { + const flag = flagStore.get(key) + if (!flag) return { error: "Flag not found" } + flag.enabled = enabled + flagStore.set(key, flag) + return { flag } +} + +export async function createFeatureFlag(data: { key: string; description: string; enabled: boolean }): Promise<{ flag?: FeatureFlag; error?: string }> { + if (!data.key || !/^[A-Z][A-Z0-9_]*$/.test(data.key)) { + return { error: "Invalid key format" } + } + if (flagStore.has(data.key)) { + return { error: "Flag already exists" } + } + const flag: FeatureFlag = { id: `${Date.now()}`, key: data.key, description: data.description || "", enabled: data.enabled || false } + flagStore.set(data.key, flag) + return { flag } +} + +export async function deleteFeatureFlag(key: string): Promise<{ success?: boolean; error?: string }> { + if (!flagStore.has(key)) return { error: "Flag not found" } + flagStore.delete(key) + return { success: true } +} diff --git a/apps/admin/app/admin/agents/page.tsx b/apps/admin/app/admin/agents/page.tsx new file mode 100644 index 0000000..839f9e2 --- /dev/null +++ b/apps/admin/app/admin/agents/page.tsx @@ -0,0 +1,468 @@ +"use client" + +import { useState, useEffect } from "react" +import { + Bot, + Play, + Pause, + RefreshCw, + Settings, + Activity, + Clock, + CheckCircle2, + XCircle, + AlertTriangle, + Loader2, + Terminal, + Zap, + BarChart3 +} from "lucide-react" + +interface AgentConfig { + id: string + name: string + description: string | null + enabled: boolean + config: Record + schedule: string | null + lastRun: string | null + lastResult: { status: string; message?: string } | null +} + +interface AgentExecution { + id: string + agentId: string + status: "pending" | "running" | "completed" | "failed" + input: Record | null + output: Record | null + error: string | null + duration: number | null + triggeredBy: string + startedAt: string + completedAt: string | null +} + +// Demo agents +const demoAgents: AgentConfig[] = [ + { + id: "a1", + name: "FramePricingAgent", + description: "AI agent that analyzes market data and suggests optimal pricing for frame mints", + enabled: true, + config: { model: "gpt-4-turbo", maxTokens: 2000 }, + schedule: "0 */6 * * *", + lastRun: "2024-03-15T18:00:00Z", + lastResult: { status: "success", message: "Analyzed 150 frames, updated 23 pricing recommendations" }, + }, + { + id: "a2", + name: "ContentModerationAgent", + description: "Scans uploaded media for policy violations and inappropriate content", + enabled: true, + config: { model: "gpt-4-vision", confidenceThreshold: 0.85 }, + schedule: "*/15 * * * *", + lastRun: "2024-03-15T20:15:00Z", + lastResult: { status: "success", message: "Reviewed 45 new uploads, flagged 2 for review" }, + }, + { + id: "a3", + name: "QuestCompletionAgent", + description: "Validates quest completion criteria and triggers rewards distribution", + enabled: true, + config: { batchSize: 100, retryAttempts: 3 }, + schedule: "*/5 * * * *", + lastRun: "2024-03-15T20:20:00Z", + lastResult: { status: "success", message: "Processed 89 quest completions, distributed 2.5 ETH in rewards" }, + }, + { + id: "a4", + name: "AnalyticsAggregationAgent", + description: "Aggregates platform metrics and generates daily reports", + enabled: false, + config: { reportFormat: "json", destinations: ["db", "s3"] }, + schedule: "0 0 * * *", + lastRun: "2024-03-15T00:00:00Z", + lastResult: { status: "failed", message: "S3 bucket write permission denied" }, + }, + { + id: "a5", + name: "SmartBrainOrchestrator", + description: "Master agent that coordinates other agents and optimizes workflow execution", + enabled: true, + config: { maxConcurrency: 5, priorityQueue: true }, + schedule: "*/30 * * * *", + lastRun: "2024-03-15T20:00:00Z", + lastResult: { status: "success", message: "Orchestrated 12 agent runs, 100% success rate" }, + }, +] + +const demoExecutions: AgentExecution[] = [ + { + id: "e1", + agentId: "a3", + status: "completed", + input: { batchId: "b-123" }, + output: { processed: 89, rewards: "2.5 ETH" }, + error: null, + duration: 4500, + triggeredBy: "schedule", + startedAt: "2024-03-15T20:20:00Z", + completedAt: "2024-03-15T20:20:04Z", + }, + { + id: "e2", + agentId: "a2", + status: "completed", + input: { scanType: "full" }, + output: { scanned: 45, flagged: 2 }, + error: null, + duration: 12300, + triggeredBy: "schedule", + startedAt: "2024-03-15T20:15:00Z", + completedAt: "2024-03-15T20:15:12Z", + }, + { + id: "e3", + agentId: "a4", + status: "failed", + input: { reportDate: "2024-03-15" }, + output: null, + error: "S3 bucket write permission denied", + duration: 850, + triggeredBy: "schedule", + startedAt: "2024-03-15T00:00:00Z", + completedAt: "2024-03-15T00:00:01Z", + }, +] + +export default function AgentsPage() { + const [agents, setAgents] = useState([]) + const [executions, setExecutions] = useState([]) + const [loading, setLoading] = useState(true) + const [runningAgents, setRunningAgents] = useState>(new Set()) + const [selectedAgent, setSelectedAgent] = useState(null) + const [notification, setNotification] = useState<{ type: "success" | "error"; message: string } | null>(null) + + useEffect(() => { + setTimeout(() => { + setAgents(demoAgents) + setExecutions(demoExecutions) + setLoading(false) + }, 500) + }, []) + + const showNotification = (type: "success" | "error", message: string) => { + setNotification({ type, message }) + setTimeout(() => setNotification(null), 3000) + } + + const toggleAgent = (agentId: string, currentEnabled: boolean) => { + setAgents(prev => prev.map(a => + a.id === agentId ? { ...a, enabled: !currentEnabled } : a + )) + showNotification("success", `Agent ${currentEnabled ? "disabled" : "enabled"}`) + } + + const runAgent = async (agent: AgentConfig) => { + setRunningAgents(prev => new Set(prev).add(agent.id)) + + // Add pending execution + const execution: AgentExecution = { + id: `e${Date.now()}`, + agentId: agent.id, + status: "running", + input: null, + output: null, + error: null, + duration: null, + triggeredBy: "manual", + startedAt: new Date().toISOString(), + completedAt: null, + } + setExecutions(prev => [execution, ...prev]) + + // Simulate execution + setTimeout(() => { + const success = Math.random() > 0.2 + setExecutions(prev => prev.map(e => + e.id === execution.id + ? { + ...e, + status: success ? "completed" : "failed", + output: success ? { result: "Execution completed" } : null, + error: success ? null : "Simulated error for demo", + duration: Math.floor(Math.random() * 5000) + 1000, + completedAt: new Date().toISOString(), + } + : e + )) + + setAgents(prev => prev.map(a => + a.id === agent.id + ? { + ...a, + lastRun: new Date().toISOString(), + lastResult: success + ? { status: "success", message: "Manual execution completed" } + : { status: "failed", message: "Simulated error" }, + } + : a + )) + + setRunningAgents(prev => { + const next = new Set(prev) + next.delete(agent.id) + return next + }) + + showNotification(success ? "success" : "error", + success ? `${agent.name} completed successfully` : `${agent.name} failed` + ) + }, 2000 + Math.random() * 3000) + } + + const getStatusIcon = (status: string) => { + switch (status) { + case "completed": return + case "running": return + case "failed": return + default: return + } + } + + const getResultColor = (status: string | undefined) => { + switch (status) { + case "success": return "text-emerald-400" + case "failed": return "text-red-400" + default: return "text-slate-400" + } + } + + return ( +
+
+ {/* Header */} +
+
+

+ Agent Management +

+

Configure and monitor AI agents

+
+
+
+ + + {agents.filter(a => a.enabled).length}/{agents.length} Active + +
+
+
+ + {/* Notification */} + {notification && ( +
+ {notification.type === "success" ? : } + {notification.message} +
+ )} + + {/* Stats */} +
+ {[ + { label: "Total Agents", value: agents.length, icon: Bot, color: "emerald" }, + { label: "Active", value: agents.filter(a => a.enabled).length, icon: Activity, color: "cyan" }, + { label: "Last Hour Runs", value: 24, icon: BarChart3, color: "purple" }, + { label: "Success Rate", value: "94%", icon: CheckCircle2, color: "green" }, + ].map((stat, i) => { + const colorClassMap: Record = { + emerald: "text-emerald-400", + cyan: "text-cyan-400", + purple: "text-purple-400", + green: "text-green-400", + } + return ( +
+
+ + {stat.value} +
+

{stat.label}

+
+ ) + })} +
+ +
+ {/* Agents List */} +
+
+

+ + Configured Agents +

+
+ + {loading ? ( +
+ + Loading agents... +
+ ) : ( +
+ {agents.map((agent) => ( +
setSelectedAgent(agent)} + > +
+
+
+ +
+
+
{agent.name}
+
+ {agent.schedule ? `Schedule: ${agent.schedule}` : "Manual only"} +
+
+
+
+ + +
+
+ + {agent.lastResult && ( +
+ Last: {agent.lastResult.message} +
+ )} +
+ ))} +
+ )} +
+ + {/* Agent Details / Recent Executions */} +
+ {/* Selected Agent Details */} + {selectedAgent && ( +
+
+

+ + {selectedAgent.name} +

+ + {selectedAgent.enabled ? "ACTIVE" : "DISABLED"} + +
+ +

{selectedAgent.description}

+ +
+
+ Schedule + {selectedAgent.schedule || "None"} +
+
+ Last Run + + {selectedAgent.lastRun + ? new Date(selectedAgent.lastRun).toLocaleString() + : "Never"} + +
+
+ Config + + {JSON.stringify(selectedAgent.config).slice(0, 50)}... + +
+
+
+ )} + + {/* Recent Executions */} +
+
+

+ + Recent Executions +

+
+ +
+ {executions.map((exec) => { + const agent = agents.find(a => a.id === exec.agentId) + return ( +
+
+
+ {getStatusIcon(exec.status)} + {agent?.name} +
+ + {new Date(exec.startedAt).toLocaleTimeString()} + +
+
+ + + {exec.duration ? `${exec.duration}ms` : "In progress"} + + + {exec.triggeredBy} + +
+ {exec.error && ( +
+ {exec.error} +
+ )} +
+ ) + })} +
+
+
+
+
+
+ ) +} diff --git a/apps/admin/app/admin/contracts/page.tsx b/apps/admin/app/admin/contracts/page.tsx new file mode 100644 index 0000000..fe80c86 --- /dev/null +++ b/apps/admin/app/admin/contracts/page.tsx @@ -0,0 +1,455 @@ +"use client" + +import { useState, useEffect } from "react" +import { + Code2, + Rocket, + RefreshCw, + ExternalLink, + Copy, + Check, + AlertCircle, + FileCode, + Cpu, + Link2, + Clock, + CheckCircle2, + XCircle, + Loader2 +} from "lucide-react" + +interface ContractDeployment { + id: string + name: string | null + chainId: number + address: string | null + txHash: string | null + compilerVersion: string | null + status: "pending" | "deploying" | "deployed" | "failed" + errorMessage: string | null + createdAt: string +} + +const chains = [ + { id: 1, name: "Ethereum Mainnet", icon: "Ξ", color: "#627EEA", explorer: "https://etherscan.io" }, + { id: 8453, name: "Base", icon: "B", color: "#0052FF", explorer: "https://basescan.org" }, + { id: 10, name: "Optimism", icon: "OP", color: "#FF0420", explorer: "https://optimistic.etherscan.io" }, + { id: 42161, name: "Arbitrum", icon: "A", color: "#28A0F0", explorer: "https://arbiscan.io" }, + { id: 137, name: "Polygon", icon: "P", color: "#8247E5", explorer: "https://polygonscan.com" }, + { id: 84532, name: "Base Sepolia", icon: "BS", color: "#0052FF", explorer: "https://sepolia.basescan.org" }, + { id: 11155111, name: "Sepolia", icon: "S", color: "#CFB5F0", explorer: "https://sepolia.etherscan.io" }, +] + +const sampleContract = `// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract CastQuestToken { + string public name = "CastQuest"; + string public symbol = "CQST"; + uint8 public decimals = 18; + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + constructor(uint256 _initialSupply) { + totalSupply = _initialSupply * 10 ** decimals; + balanceOf[msg.sender] = totalSupply; + emit Transfer(address(0), msg.sender, totalSupply); + } + + function transfer(address to, uint256 value) public returns (bool) { + require(balanceOf[msg.sender] >= value, "Insufficient balance"); + balanceOf[msg.sender] -= value; + balanceOf[to] += value; + emit Transfer(msg.sender, to, value); + return true; + } + + function approve(address spender, uint256 value) public returns (bool) { + allowance[msg.sender][spender] = value; + emit Approval(msg.sender, spender, value); + return true; + } + + function transferFrom(address from, address to, uint256 value) public returns (bool) { + require(balanceOf[from] >= value, "Insufficient balance"); + require(allowance[from][msg.sender] >= value, "Insufficient allowance"); + balanceOf[from] -= value; + balanceOf[to] += value; + allowance[from][msg.sender] -= value; + emit Transfer(from, to, value); + return true; + } +}` + +// Demo deployments +const demoDeployments: ContractDeployment[] = [ + { + id: "d1", + name: "CastQuestToken", + chainId: 8453, + address: "0x1234567890abcdef1234567890abcdef12345678", + txHash: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + compilerVersion: "0.8.20", + status: "deployed", + errorMessage: null, + createdAt: "2024-01-15T10:30:00Z" + }, + { + id: "d2", + name: "FrameRegistry", + chainId: 84532, + address: "0xabcdef1234567890abcdef1234567890abcdef12", + txHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + compilerVersion: "0.8.20", + status: "deployed", + errorMessage: null, + createdAt: "2024-02-20T14:45:00Z" + }, +] + +export default function ContractsPage() { + const [deployments, setDeployments] = useState([]) + const [loading, setLoading] = useState(true) + const [deploying, setDeploying] = useState(false) + const [sourceCode, setSourceCode] = useState(sampleContract) + const [contractName, setContractName] = useState("CastQuestToken") + const [selectedChain, setSelectedChain] = useState(84532) // Base Sepolia default + const [constructorArgs, setConstructorArgs] = useState("1000000") + const [notification, setNotification] = useState<{ type: "success" | "error"; message: string } | null>(null) + const [copiedAddress, setCopiedAddress] = useState(null) + + useEffect(() => { + setTimeout(() => { + setDeployments(demoDeployments) + setLoading(false) + }, 500) + }, []) + + const showNotification = (type: "success" | "error", message: string) => { + setNotification({ type, message }) + setTimeout(() => setNotification(null), 5000) + } + + const copyToClipboard = async (text: string, id: string) => { + await navigator.clipboard.writeText(text) + setCopiedAddress(id) + setTimeout(() => setCopiedAddress(null), 2000) + } + + const handleDeployContract = async () => { + if (!sourceCode.trim()) { + showNotification("error", "Please enter contract source code") + return + } + + setDeploying(true) + + // Create pending deployment + const pendingDeployment: ContractDeployment = { + id: `d${Date.now()}`, + name: contractName, + chainId: selectedChain, + address: null, + txHash: null, + compilerVersion: "0.8.20", + status: "deploying", + errorMessage: null, + createdAt: new Date().toISOString(), + } + + setDeployments(prev => [pendingDeployment, ...prev]) + + try { + const result = await deployContract({ + name: contractName, + chainId: selectedChain, + constructorArgs: constructorArgs ? constructorArgs.split(",").map(a => a.trim()) : [], + }) + + if (result.success && result.deployment) { + setDeployments(prev => prev.map(d => + d.id === pendingDeployment.id + ? { ...d, ...result.deployment, status: "deployed" } + : d + )) + showNotification("success", `Contract deployed to ${result.deployment.address}`) + } else { + setDeployments(prev => prev.map(d => + d.id === pendingDeployment.id + ? { ...d, status: "failed", errorMessage: result.error ?? "Deployment failed" } + : d + )) + showNotification("error", result.error || "Deployment failed") + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Deployment failed" + setDeployments(prev => prev.map(d => + d.id === pendingDeployment.id + ? { ...d, status: "failed", errorMessage } + : d + )) + showNotification("error", errorMessage) + } finally { + setDeploying(false) + } + } + + const getChain = (chainId: number) => chains.find(c => c.id === chainId) + + const getStatusIcon = (status: string) => { + switch (status) { + case "deployed": return + case "deploying": return + case "failed": return + default: return + } + } + + return ( +
+
+ {/* Header */} +
+
+

+ Contract Deployment +

+

Deploy smart contracts to supported EVM chains

+
+
+ + {/* Notification */} + {notification && ( +
+ {notification.type === "success" ? : } + {notification.message} +
+ )} + +
+ {/* Deployment Form */} +
+

+ + Deploy New Contract +

+ +
+ {/* Contract Name */} +
+ + setContractName(e.target.value)} + placeholder="MyContract" + className="w-full px-4 py-2 bg-slate-800/50 border border-slate-600/50 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/50" + /> +
+ + {/* Chain Selector */} +
+ + +
+ + {/* Source Code */} +
+ +