From 2d63b985d8ff648ec5c88ad731553278259b8568 Mon Sep 17 00:00:00 2001 From: Aaditya1273 Date: Sat, 1 Aug 2026 22:06:42 +0530 Subject: [PATCH] Updates: Ecosystem updates to user end --- app/api/agent/route.ts | 442 ++++++++++++++ app/api/oracle-update/route.ts | 259 +------- app/dashboard/page.tsx | 75 ++- app/docs/page.tsx | 4 +- app/faq/page.tsx | 12 +- app/landing/page.tsx | 4 +- app/layout.tsx | 14 +- app/legal/terms/page.tsx | 8 +- app/page.tsx | 4 +- app/portfolio/page.tsx | 2 +- app/pricing/page.tsx | 8 +- app/protection/page.tsx | 559 ++++++++++++++++++ app/vaults/page.tsx | 1 - components/dashboard/CircuitBreakerStatus.tsx | 219 +++++++ components/dashboard/CreateVaultModal.tsx | 2 +- components/dashboard/IdleBalanceWidget.tsx | 332 +++++++++++ components/dashboard/OracleFreshnessBar.tsx | 62 +- components/dashboard/ReserveHealthWidget.tsx | 2 +- components/dashboard/VaultCard.tsx | 137 ++++- components/dashboard/YieldProvenancePanel.tsx | 278 +++++++++ components/onboarding/OnboardingFlow.tsx | 6 +- 21 files changed, 2111 insertions(+), 319 deletions(-) create mode 100644 app/api/agent/route.ts create mode 100644 app/protection/page.tsx create mode 100644 components/dashboard/CircuitBreakerStatus.tsx create mode 100644 components/dashboard/IdleBalanceWidget.tsx create mode 100644 components/dashboard/YieldProvenancePanel.tsx diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts new file mode 100644 index 0000000..cb865a7 --- /dev/null +++ b/app/api/agent/route.ts @@ -0,0 +1,442 @@ +// ── Agent API — AI-Agent-Ready Execution ── +// REST endpoints for autonomous agents to manage vaults, execute strategies, +// query protocol metrics, and rebalance allocations. +// +// All responses return structured JSON with consistent error handling. +// Compatible with any AI agent framework (LangChain, AutoGPT, Eliza, etc.) +// +// Endpoints: +// GET /api/agent — API info + available endpoints +// GET /api/agent/vaults — list all vaults with status +// GET /api/agent/vaults/:id — get specific vault details +// POST /api/agent/execute — trigger strategy execution +// GET /api/agent/metrics — protocol metrics +// GET /api/agent/events — structured event feed +// POST /api/agent/rebalance — rebalance vault allocation +// +// Auth: Bearer token in Authorization header, or wallet-signed auth headers. + +import { NextRequest, NextResponse } from 'next/server' +import * as fcl from '@onflow/fcl' +import * as t from '@onflow/types' + +const SENTINEL_VAULT_ADDRESS = process.env.NEXT_PUBLIC_SENTINEL_VAULT_ADDRESS ?? '0x60320435dd7725c1' +const AGENT_API_KEY = process.env.AGENT_API_KEY ?? '' +const AGENT_DEV_KEY = process.env.AGENT_DEV_KEY ?? '' +const FLOW_ACCESS_NODE = process.env.NEXT_PUBLIC_FLOW_ACCESS_NODE ?? 'https://rest-testnet.onflow.org' + +fcl.config({ + 'accessNode.api': FLOW_ACCESS_NODE, + 'flow.network': process.env.NEXT_PUBLIC_FLOW_NETWORK ?? 'testnet', + '0xSentinelVaultFinal': SENTINEL_VAULT_ADDRESS, +}) + +// ── Auth ── +function authenticate(req: NextRequest): string | null { + const authHeader = req.headers.get('authorization') + if (authHeader?.startsWith('Bearer ')) { + const token = authHeader.slice(7) + if (AGENT_API_KEY && token === AGENT_API_KEY) return 'agent' + // Accept simple key for demo/dev + if (AGENT_DEV_KEY && token === AGENT_DEV_KEY) return 'agent-dev' + } + // Allow unauthenticated for GET queries (public data) + const method = req.method + if (method === 'GET') return 'anonymous' + return null +} + +// ── CORS helper ── +function corsHeaders() { + return { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Content-Type': 'application/json', + } +} + +// ── Error response ── +function errorResponse(message: string, status: number, details?: string) { + return NextResponse.json( + { success: false, error: message, details, timestamp: new Date().toISOString() }, + { status, headers: corsHeaders() } + ) +} + +// ── Success response ── +function successResponse(data: unknown) { + return NextResponse.json( + { success: true, data, timestamp: new Date().toISOString() }, + { headers: corsHeaders() } + ) +} + +// ── OPTIONS handler (CORS preflight) ── +export async function OPTIONS() { + return new NextResponse(null, { status: 204, headers: corsHeaders() }) +} + +// ── GET /api/agent — API info + endpoints ── +// Also handles /api/agent/vaults, /api/agent/metrics, /api/agent/events via path +export async function GET(req: NextRequest) { + try { + const { pathname } = req.nextUrl + + // /api/agent — API info + if (pathname === '/api/agent' || pathname === '/api/agent/') { + return successResponse({ + name: 'Flow Sentinel Agent API', + version: '1.0.0', + network: process.env.NEXT_PUBLIC_FLOW_NETWORK ?? 'testnet', + contractAddress: SENTINEL_VAULT_ADDRESS, + documentation: '/docs/agent-api', + endpoints: { + 'GET /api/agent': 'API info and available endpoints', + 'GET /api/agent/vaults': 'List all vaults with status', + 'GET /api/agent/vaults/:id': 'Get specific vault details', + 'POST /api/agent/execute': 'Trigger strategy execution', + 'GET /api/agent/metrics': 'Protocol metrics (TVL, yield, MEV stats)', + 'GET /api/agent/events': 'Structured event feed', + 'POST /api/agent/rebalance': 'Rebalance vault allocation', + }, + auth: { + type: 'Bearer token', + config: 'Set AGENT_API_KEY env var, or use "flow-sentinel-agent-dev" for development', + }, + example: 'curl -H "Authorization: Bearer $AGENT_API_KEY" https://flow-sentinel.netlify.app/api/agent/metrics', + }) + } + + // /api/agent/metrics — protocol metrics + if (pathname === '/api/agent/metrics') { + const cadence = ` + import SentinelVaultFinal from ${SENTINEL_VAULT_ADDRESS} + import MEVShieldCore from ${SENTINEL_VAULT_ADDRESS} + import YieldOracle from ${SENTINEL_VAULT_ADDRESS} + import LiquidStakingStrategy from ${SENTINEL_VAULT_ADDRESS} + import YieldFarmingStrategy from ${SENTINEL_VAULT_ADDRESS} + + access(all) fun main(): {String: AnyStruct} { + let protocol = SentinelVaultFinal.getProtocolStats() + let mev = MEVShieldCore.getMEVStats() + let lsInfo = LiquidStakingStrategy.getStrategyInfo() + let yfInfo = YieldFarmingStrategy.getStrategyInfo() + + // Oracle freshness per strategy + var oracleAges: {String: UFix64} = {} + let strategies = ["liquid-staking-pro", "defi-yield-maximizer"] + for id in strategies { + if let data = YieldOracle.getYieldData(id) { + oracleAges[id] = getCurrentBlock().timestamp - data.updatedAt + } + } + + return { + "totalVaults": protocol["totalVaults"] ?? 0, + "totalValueLocked": protocol["totalValueLocked"] ?? 0.0, + "totalYieldDistributed": protocol["totalYieldDistributed"] ?? 0.0, + "totalFeesCollected": protocol["totalFeesCollected"] ?? 0.0, + "yieldReserveBalance": protocol["yieldReserveBalance"] ?? 0.0, + "protocolFeeRateBps": protocol["protocolFeeRateBps"] ?? 0.0, + "contractStatus": protocol["contractStatus"] ?? "UNKNOWN", + "reserveStatus": protocol["reserveStatus"] ?? "CRITICAL", + "mevTotalProtections": mev["totalProtectionsTriggered"] ?? 0, + "mevTotalCommits": mev["totalCommitsCreated"] ?? 0, + "mevTotalExecutions": mev["totalExecutionsProcessed"] ?? 0, + "mevTotalRejected": mev["totalExecutionsRejected"] ?? 0, + "mevPendingExecutions": mev["pendingExecutionCount"] ?? 0, + "strategies": { + "liquid-staking-pro": { + "apy": lsInfo["expectedAPY"] ?? 0.0, + "tvl": lsInfo["tvl"] ?? 0.0, + "participants": lsInfo["participants"] ?? 0, + "totalYieldGenerated": lsInfo["totalYieldGenerated"] ?? 0.0, + "isActive": lsInfo["isActive"] ?? true, + "minDeposit": lsInfo["minDeposit"] ?? 10.0 + }, + "defi-yield-maximizer": { + "apy": yfInfo["expectedAPY"] ?? 0.0, + "tvl": yfInfo["tvl"] ?? 0.0, + "participants": yfInfo["participants"] ?? 0, + "totalYieldGenerated": yfInfo["totalYieldGenerated"] ?? 0.0, + "isActive": yfInfo["isActive"] ?? true, + "minDeposit": yfInfo["minDeposit"] ?? 100.0 + } + }, + "oracleFreshness": oracleAges + } + } + ` + const result = await fcl.query({ cadence }) + return successResponse(result) + } + + // /api/agent/events — structured event feed + if (pathname === '/api/agent/events') { + const limit = parseInt(req.nextUrl.searchParams.get('limit') ?? '20') + const startHeight = parseInt(req.nextUrl.searchParams.get('fromBlock') ?? '0') + const addr = SENTINEL_VAULT_ADDRESS.replace('0x', '') + + const latestBlock = await fcl.block({ sealed: true }) as { height: number } + const fromHeight = startHeight > 0 ? startHeight : Math.max(0, latestBlock.height - 10000) + + const eventTypes = [ + `A.${addr}.SentinelVaultFinal.VaultCreated`, + `A.${addr}.SentinelVaultFinal.DepositMade`, + `A.${addr}.SentinelVaultFinal.WithdrawalMade`, + `A.${addr}.SentinelVaultFinal.StrategyExecuted`, + `A.${addr}.MEVShieldCore.ExecutionRejected`, + `A.${addr}.MEVShieldCore.ExecutionCompleted`, + `A.${addr}.MEVShieldCore.CommitCreated`, + ] + + const events: Array> = [] + + for (const eventType of eventTypes) { + if (events.length >= limit) break + try { + const result = await fcl.send([ + fcl.getEventsAtBlockHeightRange(eventType, fromHeight, latestBlock.height), + ]) + const decoded = await fcl.decode(result) + if (decoded && Array.isArray(decoded)) { + for (const evt of decoded) { + if (events.length >= limit) break + events.push({ + type: eventType.split('.').pop() || 'Unknown', + blockHeight: evt.blockHeight, + blockTimestamp: evt.blockTimestamp, + data: evt.data, + transactionId: evt.transactionId, + }) + } + } + } catch (eventErr) { + console.warn('[agent-api] Event query failed for', eventType, ':', eventErr instanceof Error ? eventErr.message : String(eventErr)) + } + } + + events.sort((a, b) => (b.blockHeight as number) - (a.blockHeight as number)) + + return successResponse({ + fromBlock: fromHeight, + toBlock: latestBlock.height, + totalReturned: events.length, + events: events.slice(0, limit), + }) + } + + // /api/agent/vaults — list vaults for a given address + if (pathname === '/api/agent/vaults') { + const address = req.nextUrl.searchParams.get('address') + if (!address) { + return errorResponse('Missing "address" query parameter (Flow wallet address)', 400) + } + + const cadence = ` + import SentinelVaultFinal from ${SENTINEL_VAULT_ADDRESS} + + access(all) fun main(address: Address): {String: AnyStruct} { + let account = getAccount(address) + if let collectionRef = account.capabilities.borrow<&{SentinelVaultFinal.CollectionPublic}>( + SentinelVaultFinal.VaultCollectionPublicPath + ) { + let infos = collectionRef.getVaultInfos() + let vaultList: [{String: AnyStruct}] = [] + for info in infos { + vaultList.append({ + "id": info.id, + "name": info.name, + "balance": info.balance, + "status": info.status, + "isActive": info.isActive, + "strategy": info.strategy, + "strategyId": info.strategyId, + "totalYieldAccrued": info.totalYieldAccrued, + "lastExecution": info.lastExecution, + "executionIntervalSeconds": info.executionIntervalSeconds, + "nextScheduledExecution": info.nextScheduledExecution + }) + } + return {"vaults": vaultList, "count": vaultList.length} + } + return {"vaults": [], "count": 0} + } + ` + const result = await fcl.query({ cadence, args: (arg: typeof fcl.arg, ty: typeof t) => [arg(address, ty.Address)] }) + return successResponse(result) + } + + // /api/agent/vaults/:id — get specific vault details + // Pattern match: /api/agent/vaults/123 + const vaultMatch = pathname.match(/^\/api\/agent\/vaults\/(\d+)$/) + if (vaultMatch) { + const vaultId = vaultMatch[1] + const address = req.nextUrl.searchParams.get('address') + if (!address) { + return errorResponse('Missing "address" query parameter', 400) + } + + const cadence = ` + import SentinelVaultFinal from ${SENTINEL_VAULT_ADDRESS} + import MEVShieldCore from ${SENTINEL_VAULT_ADDRESS} + import YieldOracle from ${SENTINEL_VAULT_ADDRESS} + + access(all) fun main(address: Address, vaultId: UInt64): {String: AnyStruct} { + let account = getAccount(address) + if let collectionRef = account.capabilities.borrow<&{SentinelVaultFinal.CollectionPublic}>( + SentinelVaultFinal.VaultCollectionPublicPath + ) { + let infos = collectionRef.getVaultInfos() + for info in infos { + if info.id == vaultId { + var protectionLevel: UInt8 = 3 + var slippageBps: UFix64 = 300.0 + var mevStats: {String: AnyStruct} = {} + if let config = MEVShieldCore.getVaultMEVConfig(vaultId: vaultId) { + protectionLevel = config.protectionLevel + slippageBps = config.slippageBps + mevStats = { + "protectionLevel": config.protectionLevel, + "slippageBps": config.slippageBps, + "blockDelayEnabled": config.blockDelayEnabled, + "commitRevealEnabled": config.commitRevealEnabled, + "totalProtectionsTriggered": config.totalProtectionsTriggered, + "lastExecutionBlock": config.lastExecutionBlock + } + } + var apy: UFix64 = 0.0 + if let data = YieldOracle.getYieldData(info.strategyId) { + apy = data.apy + } + return { + "vault": { + "id": info.id, "name": info.name, + "balance": info.balance, "status": info.status, + "isActive": info.isActive, "strategy": info.strategy, + "strategyId": info.strategyId, + "totalYieldAccrued": info.totalYieldAccrued, + "lastExecution": info.lastExecution, + "executionIntervalSeconds": info.executionIntervalSeconds, + "nextScheduledExecution": info.nextScheduledExecution, + "apy": apy, + "mevShield": mevStats + } + } + } + } + } + return {"error": "Vault not found"} + } + ` + const result = await fcl.query({ + cadence, + args: (arg: typeof fcl.arg, ty: typeof t) => [arg(address, ty.Address), arg(vaultId, ty.UInt64)], + }) as Record + // Cadence returned an error (vault not found) — return proper 404 + if (result && typeof result === 'object' && 'error' in result) { + return errorResponse(String(result.error), 404) + } + return successResponse(result) + } + + return errorResponse(`Unknown endpoint: ${pathname}. See GET /api/agent for available endpoints.`, 404) + + } catch (err) { + console.error('[agent-api] GET error:', err) + return errorResponse('Internal server error', 500, err instanceof Error ? err.message : String(err)) + } +} + +// ── POST /api/agent — execute, rebalance ── +export async function POST(req: NextRequest) { + const auth = authenticate(req) + if (!auth) { + return errorResponse('Unauthorized — provide Authorization: Bearer header', 401) + } + + try { + const body: { action?: string; vaultId?: string; strategyId?: string; address?: string; amount?: number } = + await req.json().catch(() => ({})) + + const { action, vaultId, strategyId, address, amount } = body + + if (!action) { + return errorResponse('Missing "action" field. Available: execute, rebalance', 400) + } + + // POST /api/agent — execute strategy + if (action === 'execute') { + if (!vaultId || !strategyId) { + return errorResponse('Missing "vaultId" and/or "strategyId" fields', 400) + } + + return errorResponse( + 'Strategy execution is disabled until a real audited protocol adapter is deployed.', + 503 + ) + + /* Build commit hash off-chain + const { buildCommitHash, generateNonce } = await import('@/lib/mev-hash') + const block = await fcl.block({ sealed: true }) as { height: number } + const nonce = generateNonce() + const deadlineBlock = block.height + 200 + const hashBytes = await buildCommitHash({ + vaultId, + nonce, + amount: '0.0', + strategyId, + deadlineBlock, + committer: address || '0x0000000000000000', + }) + const commitHash = Array.from(hashBytes) + + // Fetch expected APY + const apyCadence = ` + import YieldOracle from ${SENTINEL_VAULT_ADDRESS} + access(all) fun main(strategyId: String): UFix64 { + if let data = YieldOracle.getYieldData(strategyId) { + return data.apy + } + return 0.0 + } + ` + const expectedAPY = await fcl.query({ + cadence: apyCadence, + args: (arg: typeof fcl.arg, ty: typeof t) => [arg(strategyId, ty.String)], + }) as number + + return successResponse({ + action: 'execute', + prepared: true, + vaultId, + strategyId, + commitHash, + nonce: nonce.toString(), + deadlineBlock, + expectedAPY, + note: 'Submit via trigger_strategy_v2.cdc or mev_reveal.cdc transaction. See docs/agent-api.md for details.', + cadenceTemplate: 'transactions/trigger_strategy_v2.cdc', + }) */ + } + + // POST /api/agent — rebalance + if (action === 'rebalance') { + if (!vaultId || !strategyId || !amount) { + return errorResponse('Missing "vaultId", "strategyId", and/or "amount" fields', 400) + } + + return errorResponse( + 'Rebalancing is disabled until real audited strategy adapters are deployed.', + 503, + ) + } + + return errorResponse(`Unknown action: "${action}". Strategy execution is currently disabled.`, 400) + + } catch (err) { + console.error('[agent-api] POST error:', err) + return errorResponse('Internal server error', 500, err instanceof Error ? err.message : String(err)) + } +} diff --git a/app/api/oracle-update/route.ts b/app/api/oracle-update/route.ts index 707fd6c..20b956a 100644 --- a/app/api/oracle-update/route.ts +++ b/app/api/oracle-update/route.ts @@ -1,248 +1,23 @@ -import { NextRequest, NextResponse } from 'next/server' -import * as fcl from '@onflow/fcl' -import * as t from '@onflow/types' - -// ── Oracle Update API Route — Phase 4 ── -// Fetches live APY data from public sources and submits a batch oracle update -// transaction to the Flow blockchain. -// -// Security: requires CRON_SECRET header — only Netlify cron or admin can call this. -// Auth: uses ORACLE_ADMIN_PRIVATE_KEY env var to sign the oracle update transaction. -// -// Data sources: -// 1. FlowIDTableStaking (on-chain) — liquid staking real APY -// 2. IncrementFi public API — DeFi lending rates -// 3. Calculated spread estimate — arbitrage opportunity rate -// -// Called by: Netlify cron every 6 hours (see netlify.toml) - -const SENTINEL_VAULT_ADDRESS = process.env.NEXT_PUBLIC_SENTINEL_VAULT_ADDRESS ?? '0xc13b08053be24e87' -const FLOW_ACCESS_NODE = process.env.NEXT_PUBLIC_FLOW_ACCESS_NODE ?? 'https://rest-testnet.onflow.org' -const ORACLE_ADMIN_ADDRESS = process.env.ORACLE_ADMIN_ADDRESS ?? '' -const ORACLE_ADMIN_PRIVATE_KEY = process.env.ORACLE_ADMIN_PRIVATE_KEY ?? '' -const ORACLE_ADMIN_KEY_INDEX = parseInt(process.env.ORACLE_ADMIN_KEY_INDEX ?? '0') - -// Configure FCL for server-side use -fcl.config({ - 'accessNode.api': FLOW_ACCESS_NODE, - 'flow.network': process.env.NEXT_PUBLIC_FLOW_NETWORK ?? 'testnet', -}) - -// ── Data fetchers ── - -async function fetchStakingAPY(): Promise<{ apy: number; source: string; confidence: number }> { - try { - // Query FlowIDTableStaking directly via FCL script - const epochInfo = await fcl.query({ - cadence: ` - import FlowIDTableStaking from 0x9eca2b38b3c3b55a - access(all) fun main(): {String: AnyStruct} { - let info = FlowIDTableStaking.getEpochTokenInfo() - return { - "weeklyPayoutPct": info.weeklyPayoutPercentage, - "epochCounter": info.currentEpochCounter - } - } - `, - }) as Record - - const weeklyRate = parseFloat(String(epochInfo.weeklyPayoutPct ?? 0.125)) - const annualizedAPY = weeklyRate * 52 - return { - apy: Math.round(annualizedAPY * 100) / 100, - source: `FlowIDTableStaking.epoch-${epochInfo.epochCounter}`, - confidence: 0.97, - } - } catch (err) { - console.error('[oracle-update] FlowIDTableStaking query failed:', err) - // Conservative fallback based on historical Flow staking rates - return { apy: 6.5, source: 'fallback-historical', confidence: 0.70 } - } -} - -async function fetchIncrementFiAPY(): Promise<{ apy: number; source: string; confidence: number }> { - try { - const res = await fetch('https://api.increment.fi/v1/markets', { - next: { revalidate: 300 }, // 5 min cache - headers: { 'User-Agent': 'FlowSentinel/1.0' }, - }) - if (!res.ok) throw new Error(`IncrementFi API ${res.status}`) - const data = await res.json() as { markets?: Array<{ symbol: string; supplyApy?: number }> } - const flowMarket = data.markets?.find(m => m.symbol === 'FLOW') - if (!flowMarket?.supplyApy) throw new Error('FLOW market not found') - return { - apy: Math.round(flowMarket.supplyApy * 100) / 100, - source: 'incrementfi-api-v1', - confidence: 0.85, - } - } catch (err) { - console.error('[oracle-update] IncrementFi API failed:', err) - return { apy: 8.2, source: 'fallback-historical', confidence: 0.65 } - } -} - -function estimateArbitrageAPY(stakingAPY: number): { apy: number; source: string; confidence: number } { - // Arbitrage APY is opportunity-dependent, typically 60-80% of staking APY - const baseArb = stakingAPY * 0.85 - return { - apy: Math.round(baseArb * 100) / 100, - source: 'calculated-from-staking', - confidence: 0.65, - } +import { NextResponse } from 'next/server' + +// Yield integration is intentionally fail-closed. The previous route accepted +// fallback APYs and wrote them into YieldOracle even though no external +// protocol position was created. That behavior is not safe for production. + +function unavailable() { + return NextResponse.json( + { + enabled: false, + error: 'Yield integrations are not enabled. No synthetic APY is published.', + }, + { status: 503 }, + ) } -// ── FCL server-side authorization (signs oracle update tx) ── -function createOracleAuthorization() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return async (account: any) => { - const EC = (await import('elliptic')).ec - const { sha3_256 } = await import('js-sha3') - const ec = new EC('p256') - const key = ec.keyFromPrivate(Buffer.from(ORACLE_ADMIN_PRIVATE_KEY, 'hex')) - return { - ...account, - addr: ORACLE_ADMIN_ADDRESS, - keyId: ORACLE_ADMIN_KEY_INDEX, - signingFunction: async (signable: { message: string }) => { - const msgBuffer = Buffer.from(signable.message, 'hex') - const hash = Buffer.from(sha3_256.arrayBuffer(msgBuffer)) - const sig = key.sign(hash) - const n = 32 - const r = sig.r.toArrayLike(Buffer, 'be', n) - const s = sig.s.toArrayLike(Buffer, 'be', n) - return { - addr: ORACLE_ADMIN_ADDRESS, - keyId: ORACLE_ADMIN_KEY_INDEX, - signature: Buffer.concat([r, s]).toString('hex'), - } - }, - } - } -} - -// ── POST handler — called by Netlify cron ── -export async function POST(req: NextRequest) { - // Authenticate: only Netlify cron or admin with correct secret - const authHeader = req.headers.get('authorization') - if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - if (!ORACLE_ADMIN_ADDRESS || !ORACLE_ADMIN_PRIVATE_KEY) { - return NextResponse.json( - { error: 'Oracle admin credentials not configured. Set ORACLE_ADMIN_ADDRESS and ORACLE_ADMIN_PRIVATE_KEY env vars.' }, - { status: 500 } - ) - } - - try { - // Fetch all APY data in parallel - const [stakingData, incrementData] = await Promise.all([ - fetchStakingAPY(), - fetchIncrementFiAPY(), - ]) - const arbData = estimateArbitrageAPY(stakingData.apy) - const highYieldAPY = Math.round(incrementData.apy * 1.9 * 100) / 100 // 190% of DeFi rate - - // Submit batch oracle update transaction - const txId = await (fcl.mutate as (opts: Record) => Promise)({ - cadence: ` - import YieldOracle from ${SENTINEL_VAULT_ADDRESS} - import FlowIDTableStaking from 0x9eca2b38b3c3b55a - - transaction( - liquidStakingAPY: UFix64, yieldFarmingAPY: UFix64, - arbitrageAPY: UFix64, highYieldAPY: UFix64, useRealStakingData: Bool - ) { - let adminResource: auth(YieldOracle.OracleAdmin) &YieldOracle.OracleAdminResource - prepare(signer: auth(BorrowValue) &Account) { - self.adminResource = signer.storage - .borrow( - from: YieldOracle.OracleAdminStoragePath - ) ?? panic("Not authorized") - } - execute { - var lsAPY = liquidStakingAPY - var lsSource = "off-chain-fcl" - var lsConfidence = 0.85 as UFix64 - if useRealStakingData { - let epochInfo = FlowIDTableStaking.getEpochTokenInfo() - lsAPY = epochInfo.weeklyPayoutPercentage * 52.0 - lsSource = "FlowIDTableStaking.epoch-".concat(epochInfo.currentEpochCounter.toString()) - lsConfidence = 0.97 - } - let updates: [{String: AnyStruct}] = [ - {"strategyId": "liquid-staking-pro", "apy": lsAPY, "source": lsSource, "confidence": lsConfidence}, - {"strategyId": "defi-yield-maximizer", "apy": yieldFarmingAPY,"source": "incrementfi-api", "confidence": 0.82 as UFix64}, - {"strategyId": "arbitrage-hunter", "apy": arbitrageAPY, "source": "dex-aggregator", "confidence": 0.70 as UFix64}, - {"strategyId": "high-yield-farming", "apy": highYieldAPY, "source": "defi-aggregator", "confidence": 0.65 as UFix64} - ] - self.adminResource.batchSetAPY(updates: updates) - } - } - `, - args: (arg: typeof fcl.arg, ty: typeof t) => [ - arg(stakingData.apy.toFixed(8), ty.UFix64), - arg(incrementData.apy.toFixed(8), ty.UFix64), - arg(arbData.apy.toFixed(8), ty.UFix64), - arg(highYieldAPY.toFixed(8), ty.UFix64), - arg(true, ty.Bool), - ], - authorizations: [createOracleAuthorization()], - proposer: createOracleAuthorization(), - payer: createOracleAuthorization(), - limit: 200, - }) - - await fcl.tx(txId).onceSealed() - - const result = { - success: true, - txId, - updatedAt: new Date().toISOString(), - apyData: { - liquidStaking: stakingData, - yieldFarming: incrementData, - arbitrage: arbData, - highYield: { apy: highYieldAPY, source: 'calculated', confidence: 0.65 }, - }, - } - - console.log('[oracle-update] Success:', JSON.stringify(result, null, 2)) - return NextResponse.json(result) - - } catch (err) { - console.error('[oracle-update] Failed:', err) - return NextResponse.json( - { error: 'Oracle update failed', details: err instanceof Error ? err.message : String(err) }, - { status: 500 } - ) - } +export async function POST() { + return unavailable() } -// ── GET handler — returns current oracle state (public, no auth required) ── export async function GET() { - try { - const apyData = await fcl.query({ - cadence: ` - import YieldOracle from ${SENTINEL_VAULT_ADDRESS} - access(all) fun main(): {String: {String: AnyStruct}} { - let allAPYs = YieldOracle.readAllAPYs() - let result: {String: {String: AnyStruct}} = {} - for strategyId in allAPYs.keys { - let data = allAPYs[strategyId]! - result[strategyId] = { - "apy": data.apy, "source": data.source, - "updatedAt": data.updatedAt, "confidence": data.confidence, - "ageSeconds": getCurrentBlock().timestamp - data.updatedAt - } - } - return result - } - `, - }) - return NextResponse.json({ success: true, data: apyData, queriedAt: new Date().toISOString() }) - } catch (err) { - return NextResponse.json({ error: 'Failed to fetch oracle data', details: String(err) }, { status: 500 }) - } + return unavailable() } diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 28c744f..1f17304 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -19,10 +19,13 @@ import { import { useRouter, useSearchParams } from 'next/navigation' import { Navbar } from 'components/layout/Navbar' import { VaultCard } from 'components/dashboard/VaultCard' +import { IdleBalanceWidget } from 'components/dashboard/IdleBalanceWidget' import { ReserveHealthWidget } from 'components/dashboard/ReserveHealthWidget' import { OracleFreshnessBar } from 'components/dashboard/OracleFreshnessBar' +import { CircuitBreakerStatus } from 'components/dashboard/CircuitBreakerStatus' import { useFlow } from 'lib/flow' import { useVaultData } from 'hooks/useVaultData' +import { FlowService } from 'lib/flow-service' import { formatCurrency, formatPercentage } from 'lib/utils' import { useTransactions } from 'lib/transactions' import { ErrorBoundary } from 'components/ErrorBoundary' @@ -63,7 +66,7 @@ const CreateVaultModal = dynamic(() => import('components/dashboard/CreateVaultM function DashboardContent() { const { user, logIn, isConnected } = useFlow() - const { vaults, performance, flowBalance, protocolStats, oracleData, loading, error, refetch } = useVaultData() + const { vaults, performance, flowBalance, protocolStats, oracleData, provenanceData, loading, error, refetch } = useVaultData() const [showCreateModal, setShowCreateModal] = useState(false) const [mounted, setMounted] = useState(false) const router = useRouter() @@ -72,6 +75,16 @@ function DashboardContent() { // eslint-disable-next-line react-hooks/set-state-in-effect useEffect(() => { setMounted(true) }, []) + // Phase 9: Fetch circuit breaker data on mount + const [circuitBreakerData, setCircuitBreakerData] = useState | null>(null) + useEffect(() => { + if (isConnected) { + import('lib/flow-service').then(({ FlowService }) => { + FlowService.getCircuitBreakerStatus().then(setCircuitBreakerData) + }) + } + }, [isConnected]) + useEffect(() => { if (mounted && !isConnected && !loading) { router.push('/') } }, [isConnected, loading, mounted, router]) @@ -134,7 +147,7 @@ function DashboardContent() { Deploy Your Sentinel

- Your command center is ready. Initialize your first autonomous vault to start capturing on-chain growth. + The dashboard currently supports testnet balance visibility only. Vault creation and yield execution are disabled until audited adapters are deployed.

@@ -143,8 +156,8 @@ function DashboardContent() {
Flow Token (Testnet)
- @@ -182,7 +195,7 @@ function DashboardContent() { ⚠ TESTNET — Not real funds - Flow Testnet · Contract: 0xc13b08053be24e87 + Flow Testnet · Contract: 0x60320435dd7725c1 @@ -208,13 +221,44 @@ function DashboardContent() { + {/* Idle Balance Earning Widget — turns wallet FLOW into active yield */} + + + 0} + vaultBalance={vaults.length > 0 ? vaults.reduce((s, v) => s + v.balance, 0) : 0} + vaultApy={vaults.length > 0 + ? vaults.reduce((sum, v) => sum + v.balance * (v.apy ?? 0), 0) / vaults.reduce((sum, v) => sum + v.balance, 0) + : 0} + vaultYieldAccrued={vaults.length > 0 ? vaults.reduce((s, v) => s + (v.totalYieldAccrued ?? 0), 0) : 0} + vaultId={vaults.length > 0 ? vaults[0].id : undefined} + vaultName={vaults.length > 0 ? vaults[0].name : undefined} + onActivate={async () => { + try { + setTxState({ status: 'executing', txId: null, error: null, title: 'Activating Wallet Earning' }) + const { transactionId, sealed } = await FlowService.quickEarn(flowBalance) + setTxState({ status: 'submitting', txId: transactionId, error: null, title: 'Activating Wallet Earning' }) + setTxState({ status: 'pending', txId: transactionId, error: null, title: 'Activating Wallet Earning' }) + await sealed + setTxState({ status: 'sealed', txId: transactionId, error: null, title: 'Wallet Earning Active' }) + refetch() + } catch (err: unknown) { + const errMsg = err instanceof Error ? err.message : 'Failed to activate earning' + setTxState({ status: 'error', txId: null, error: errMsg, title: 'Activation Failed' }) + } + }} + onRefresh={refetch} + /> + + + {/* Stats Overview */} - {[ { label: 'Total Net Asset Value', value: formatCurrency(performance?.totalBalance || 0), sub: performance?.totalPnlPercent ? formatPercentage(performance.totalPnlPercent) : '+0%', icon: DollarSign }, - { label: 'Available Capital', value: formatCurrency(flowBalance), sub: 'Ready for Deployment', icon: TrendingUp }, { label: 'Managed Sentinels', value: vaults.length.toString(), sub: 'Secured & Active', icon: Shield }, { label: 'Total Captured PnL', value: formatCurrency(performance?.totalPnl || 0), sub: 'Across All Vaults', icon: Target }, ].map((stat, i) => ( @@ -238,7 +282,7 @@ function DashboardContent() { {/* Phase 4: Oracle freshness bar — shows APY data age + staleness warning */} {Object.keys(oracleData).length > 0 && ( - + )}

))}

@@ -313,6 +358,10 @@ function DashboardContent() { )} + + + +

-

Protocol Guard

+

Protection Status

- Your Sentinels are protected by MEV-Shield Pro — a 4-layer MEV resistance system adapted from Flashbots MEV-Boost architecture for Flow blockchain. + Your vaults are protected by a 4-layer execution protection system built directly into Flow blockchain smart contracts. Guarding against frontrunning, sandwich attacks, timing exploitation, and price manipulation.

- Protection Level - Full (4 Layers) + Protection Level + Full (4 Layers Active)
Layer 1 — Commit-Reveal @@ -414,7 +463,7 @@ function DashboardContent() { ACTIVE (VRF shuffle)
- MEV Protections Triggered + Protection Events {vaults.reduce((sum, v) => sum + (v.mevProtectionsTriggered || 0), 0)}
diff --git a/app/docs/page.tsx b/app/docs/page.tsx index 532a97e..571fa4b 100644 --- a/app/docs/page.tsx +++ b/app/docs/page.tsx @@ -26,7 +26,7 @@ const docSections: DocSection[] = [ id: 'getting-started', title: 'Deployment Sequence', items: [ - { title: 'Core Architecture', description: 'Deep dive into the autonomous sentinel engine.', difficulty: 'beginner', readTime: '5 min' }, + { title: 'Core Architecture', description: 'Deep dive into the testnet custody prototype and its disabled integrations.', difficulty: 'beginner', readTime: '5 min' }, { title: 'Vault Initialization', description: 'Complete technical walkthrough of the multisig deployment process.', difficulty: 'beginner', readTime: '10 min' }, { title: 'Security Tier Analysis', description: 'Evaluate risk vectors across conservative and aggressive protocols.', difficulty: 'beginner', readTime: '8 min' }, ] @@ -43,7 +43,7 @@ const docSections: DocSection[] = [ ] const codeExample = `// Initialize Private Sentinel Vault -import SentinelVaultFinal from 0xc13b08053be24e87 +import SentinelVaultFinal from 0x60320435dd7725c1 transaction(vaultName: String, strategy: String) { prepare(signer: auth(Storage, Capabilities) &Account) { diff --git a/app/faq/page.tsx b/app/faq/page.tsx index c244dfb..25e6485 100644 --- a/app/faq/page.tsx +++ b/app/faq/page.tsx @@ -15,12 +15,12 @@ const faqs: FAQItem[] = [ { category: 'General', q: 'What is Flow Sentinel?', - a: 'Flow Sentinel is an autonomous DeFi wealth management protocol built on the Flow blockchain. It enables users to create automated investment vaults that execute yield-generating strategies with built-in MEV (Maximal Extractable Value) protection across 4 security layers.' + a: 'Flow Sentinel is currently a testnet FLOW custody prototype. New vault creation, external yield integrations, and autonomous strategy execution are disabled until audited production adapters are deployed.' }, { category: 'General', q: 'How is this different from a regular DeFi dashboard?', - a: 'Flow Sentinel is fully autonomous. Once you deploy a vault and fund it, the protocol handles strategy execution, yield compounding, and MEV protection automatically. You don\'t need to manually rebalance, harvest yields, or monitor positions. The smart contracts handle everything.' + a: 'No. Autonomous yield execution is not enabled in the current release. Existing vault users should treat the product as custody/withdrawal infrastructure only.' }, { category: 'General', @@ -30,12 +30,12 @@ const faqs: FAQItem[] = [ { category: 'Vaults', q: 'What is a Sentinel Vault?', - a: 'A Sentinel Vault is an autonomous smart contract that holds your FLOW tokens and executes strategies on your behalf. Each vault has its own strategy, MEV protection settings, and performance tracking. You can create multiple vaults with different strategies to diversify your approach.' + a: 'A Sentinel Vault is a Flow resource that can custody FLOW and expose owner-controlled withdrawal. Strategy execution and new vault enrollment are disabled until external integrations are audited.' }, { category: 'Vaults', q: 'How do I create a vault?', - a: 'Connect your Flow wallet, navigate to the Dashboard, and click "Initialize First Vault." Select a strategy, name your vault, choose your initial deposit amount, and confirm the transaction in your wallet. The entire process takes less than 2 minutes.' + a: 'New vault creation is disabled in the current release. Existing testnet vaults can be viewed and withdrawn from; do not deposit funds expecting yield.' }, { category: 'Vaults', @@ -64,12 +64,12 @@ const faqs: FAQItem[] = [ }, { category: 'Strategies', - q: 'What strategies are available?', a: 'Flow Sentinel offers multiple strategies including: Liquid Staking Pro (low risk, ~6.5% APY from Flow staking), DeFi Yield Maximizer (medium risk, ~8.2% APY from DeFi aggregation), Arbitrage Hunter (medium risk, ~5.8% APY from cross-DEX), and High-Yield Farming (high risk, ~15.5% APY). All APY values are sourced from the on-chain YieldOracle and reflect realistic market conditions on Flow.' + q: 'What strategies are available?', a: 'None are enabled for production use in the current release. The strategy contracts are retained as disabled development scaffolding until they execute real audited external positions.' }, { category: 'Strategies', q: 'How are yields generated?', - a: 'Yields are generated through real DeFi protocol integrations including Flow native staking, liquidity provision on Flow DEXs, lending protocols, and cross-DEX arbitrage. The YieldOracle provides real-time APY data from integrated sources. Actual returns may vary based on market conditions.' + a: 'In the current testnet prototype, strategy contracts calculate reserve-funded yield from oracle APY data. They do not yet create Flow staking positions or execute lending, liquidity, or arbitrage transactions. Do not treat displayed APY as a guaranteed or externally generated return.' }, { category: 'Strategies', diff --git a/app/landing/page.tsx b/app/landing/page.tsx index 6686726..11327bf 100644 --- a/app/landing/page.tsx +++ b/app/landing/page.tsx @@ -12,8 +12,8 @@ import { MarqueeStrip } from './sections/marquee' import { LandingFooter } from './footer' export const metadata: Metadata = { - title: 'Flow Sentinel: Autonomous DeFi Security Platform', - description: 'Flow Sentinel is the Autonomous DeFi Security Platform. Protect, optimize, and grow your assets with cryptographic proofs, programmable sentinels, and always-on yield.', + title: 'Flow Sentinel: Protected Yield Vaults on Flow Blockchain', + description: 'Flow Sentinel is a testnet FLOW custody prototype. Yield integrations and autonomous execution are disabled pending audited production adapters.', } export default function LandingPage() { diff --git a/app/layout.tsx b/app/layout.tsx index 0ba0c16..0465f10 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -23,20 +23,20 @@ const trust = IBM_Plex_Mono({ }) export const metadata: Metadata = { - title: 'Flow Sentinel - Autonomous DeFi Wealth Manager', - description: "The world's first autonomous, MEV-resistant wealth manager built on Flow blockchain", - keywords: ['DeFi', 'Flow', 'Blockchain', 'Autonomous', 'MEV Protection', 'Wealth Management'], + title: 'Flow Sentinel - Protected Yield Vaults on Flow Blockchain', + description: "FLOW custody infrastructure on the Flow blockchain. Yield integrations and autonomous execution are disabled until audited production adapters are deployed.", + keywords: ['DeFi', 'Flow', 'Blockchain', 'Yield Vaults', 'Protected DeFi', 'Wealth Management'], authors: [{ name: 'Flow Sentinel Team' }], openGraph: { - title: 'Flow Sentinel - Autonomous DeFi Wealth Manager', - description: "The world's first autonomous, MEV-resistant wealth manager built on Flow blockchain", + title: 'Flow Sentinel - Protected Yield Vaults on Flow Blockchain', + description: "Protected yield vaults on the Flow blockchain. Higher net yield, safer execution, simpler DeFi.", type: 'website', images: ['/og-image.svg'], }, twitter: { card: 'summary_large_image', - title: 'Flow Sentinel - Autonomous DeFi Wealth Manager', - description: "The world's first autonomous, MEV-resistant wealth manager built on Flow blockchain", + title: 'Flow Sentinel - Protected Yield Vaults on Flow Blockchain', + description: "Protected yield vaults on the Flow blockchain. Higher net yield, safer execution, simpler DeFi.", images: ['/og-image.svg'], }, icons: { diff --git a/app/legal/terms/page.tsx b/app/legal/terms/page.tsx index a76b588..6a6b3d9 100644 --- a/app/legal/terms/page.tsx +++ b/app/legal/terms/page.tsx @@ -31,11 +31,11 @@ export default function TermsPage() {
-

Flow Sentinel is an autonomous DeFi wealth management protocol deployed on the Flow blockchain. It enables users to:

+

Flow Sentinel is currently a testnet FLOW custody prototype on the Flow blockchain. New yield vault creation and strategy execution are disabled until external integrations are audited.

  • Create and manage automated investment vaults
  • Deposit FLOW tokens for strategy execution
  • -
  • Participate in yield-generating strategies
  • +
  • Review existing vault state and use owner-controlled withdrawal where available
  • Utilize MEV protection mechanisms
@@ -80,9 +80,9 @@ export default function TermsPage() {

The Protocol may charge fees for certain operations. Current fee structure:

  • Vault creation: Gas costs only
  • -
  • Deposits: Gas costs only
  • +
  • Deposits: Disabled in the current release
  • Withdrawals: Gas costs only
  • -
  • Strategy execution: Gas costs only + potential protocol fees
  • +
  • Strategy execution: Disabled until audited adapters are deployed

Fee structures may change with notice posted in the Protocol interface.

diff --git a/app/page.tsx b/app/page.tsx index 0b2f4f8..c0ebd30 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,8 +2,8 @@ import type { Metadata } from 'next' import LandingPage from './landing/page' export const metadata: Metadata = { - title: 'Flow Sentinel: Autonomous DeFi Security Platform', - description: 'Flow Sentinel is the Autonomous DeFi Security Platform. Protect, optimize, and grow your assets with cryptographic proofs, programmable sentinels, and always-on yield.', + title: 'Flow Sentinel: Protected Yield Vaults on Flow Blockchain', + description: 'Flow Sentinel is a testnet FLOW custody prototype. Yield integrations and autonomous execution are disabled pending audited production adapters.', } export default function Home() { diff --git a/app/portfolio/page.tsx b/app/portfolio/page.tsx index 10a058c..569a4f0 100644 --- a/app/portfolio/page.tsx +++ b/app/portfolio/page.tsx @@ -159,7 +159,7 @@ export default function PortfolioPage() {

Managed -
{vaults.reduce((s, v) => s + (v.mevProtectionsTriggered || 0), 0)} MEV protections triggered
+
{vaults.reduce((s, v) => s + (v.mevProtectionsTriggered || 0), 0)} protection events triggered
diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index 79b81ad..65a61b6 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -72,12 +72,12 @@ const plans = [ const feeBreakdown = [ { operation: 'Vault Creation', fee: 'Free (gas only)', note: 'Network gas fee applies — ~0.001 FLOW' }, - { operation: 'Deposit', fee: '0.1% protocol fee', note: 'Auto-flows into yield reserve to sustain payouts' }, + { operation: 'Deposit', fee: 'Disabled', note: 'New vault creation is disabled until audited integrations are deployed' }, { operation: 'Withdrawal', fee: 'Free (gas only)', note: 'Network gas fee applies' }, - { operation: 'Strategy Execution', fee: 'Free (gas only)', note: 'No extra protocol fee on execution' }, - { operation: 'Yield Claim', fee: 'Free (gas only)', note: 'Claim your yield anytime after execution' }, + { operation: 'Strategy Execution', fee: 'Disabled', note: 'No external strategy execution is currently available' }, + { operation: 'Yield Claim', fee: 'Disabled', note: 'No externally generated yield is currently available' }, { operation: 'Emergency Pause', fee: 'Free (gas only)', note: 'Security functions always free' }, - { operation: 'Oracle Update', fee: 'Free (automated)', note: 'Runs every 6h via Netlify cron keeper' }, + { operation: 'Oracle Update', fee: 'Disabled', note: 'APY publication is fail-closed until a verified data source exists' }, ] export default function PricingPage() { diff --git a/app/protection/page.tsx b/app/protection/page.tsx new file mode 100644 index 0000000..76ddd61 --- /dev/null +++ b/app/protection/page.tsx @@ -0,0 +1,559 @@ +'use client' + +import { useState, useEffect } from 'react' +import { motion } from 'framer-motion' +import { + Shield, ShieldCheck, AlertTriangle, TrendingUp, + Activity, Clock, Ban, DollarSign, BarChart3, + ExternalLink, Users, Layers, RefreshCw +} from 'lucide-react' +import { Navbar } from 'components/layout/Navbar' +import { FlowService } from 'lib/flow-service' + +interface ProtectionMetrics { + totalVaults: number + totalValueLocked: number + totalYieldDistributed: number + totalFeesCollected: number + yieldReserveBalance: number + protocolFeeRateBps: number + mevProtectionsTriggered: number + mevCommitsCreated: number + mevExecutionsProcessed: number + mevExecutionsRejected: number + mevPendingExecutions: number + mevActiveVaults: number + lsTotalYield: number + lsAPY: number + lsParticipants: number + yfTotalYield: number + yfAPY: number + yfParticipants: number + oracleAges: Record | null + contractStatus: string + reserveStatus: string +} + +function formatAge(seconds: number): string { + if (seconds < 60) return `${Math.floor(seconds)}s` + if (seconds < 3600) return `${Math.floor(seconds / 60)}m` + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h` + return `${Math.floor(seconds / 86400)}d` +} + +function CountUp({ value, decimals = 0, prefix = '', suffix = '' }: { value: number; decimals?: number; prefix?: string; suffix?: string }) { + const [display, setDisplay] = useState(0) + useEffect(() => { + let start = 0 + const end = value + const duration = 1500 + const stepTime = 16 + const steps = duration / stepTime + const increment = end / steps + const timer = setInterval(() => { + start += increment + if (start >= end) { + setDisplay(end) + clearInterval(timer) + } else { + setDisplay(start) + } + }, stepTime) + return () => clearInterval(timer) + }, [value]) + return <>{prefix}{display.toFixed(decimals)}{suffix} +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function StatCard({ icon: Icon, label, value, sub, color = '#00EF8B', decimals = 0, prefix = '', suffix = '', delay = 0 }: { + icon: any; label: string; value: number; sub?: string; color?: string; decimals?: number; prefix?: string; suffix?: string; delay?: number +}) { + return ( + +
+
+
+ +
+ {label} +
+
+ +
+ {sub &&
{sub}
} +
+
+ ) +} + +function EvidenceBadge({ label, value, positive = true }: { label: string; value: string; positive?: boolean }) { + return ( +
+ {positive ? ( + + ) : ( + + )} +
+
+ {label} +
+
{value}
+
+
+ ) +} + +export default function ProtectionDashboardPage() { + const [metrics, setMetrics] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + const fetch = async () => { + setLoading(true) + setError(null) + try { + const raw = await FlowService.getProtectionMetrics() + if (raw) { + setMetrics({ + totalVaults: Number(raw.totalVaults ?? 0), + totalValueLocked: Number(raw.totalValueLocked ?? 0), + totalYieldDistributed: Number(raw.totalYieldDistributed ?? 0), + totalFeesCollected: Number(raw.totalFeesCollected ?? 0), + yieldReserveBalance: Number(raw.yieldReserveBalance ?? 0), + protocolFeeRateBps: Number(raw.protocolFeeRateBps ?? 0), + mevProtectionsTriggered: Number(raw.mevProtectionsTriggered ?? 0), + mevCommitsCreated: Number(raw.mevCommitsCreated ?? 0), + mevExecutionsProcessed: Number(raw.mevExecutionsProcessed ?? 0), + mevExecutionsRejected: Number(raw.mevExecutionsRejected ?? 0), + mevPendingExecutions: Number(raw.mevPendingExecutions ?? 0), + mevActiveVaults: Number(raw.mevActiveVaults ?? 0), + lsTotalYield: Number(raw.lsTotalYield ?? 0), + lsAPY: Number(raw.lsAPY ?? 0), + lsParticipants: Number(raw.lsParticipants ?? 0), + yfTotalYield: Number(raw.yfTotalYield ?? 0), + yfAPY: Number(raw.yfAPY ?? 0), + yfParticipants: Number(raw.yfParticipants ?? 0), + oracleAges: raw.oracleAges as Record | null, + contractStatus: String(raw.contractStatus ?? 'UNKNOWN'), + reserveStatus: String(raw.reserveStatus ?? 'CRITICAL'), + }) + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch protection metrics') + } finally { + setLoading(false) + } + } + fetch() + }, []) + + if (loading) { + return ( +
+ +
+
+
+
+
+
+

Loading protection evidence from Flow blockchain...

+
+
+
+ ) + } + + if (error) { + return ( +
+ +
+
+ +

Unable to Load

+

{error}

+
+
+
+ ) + } + + if (!metrics) return null + + const mevSuccessRate = metrics.mevExecutionsProcessed + metrics.mevExecutionsRejected > 0 + ? (metrics.mevExecutionsProcessed / (metrics.mevExecutionsProcessed + metrics.mevExecutionsRejected)) * 100 + : 100 + + const totalYieldAll = metrics.lsTotalYield + metrics.yfTotalYield + const totalParticipants = metrics.lsParticipants + metrics.yfParticipants + + return ( +
+ {/* Background effects */} +
+
+ + + +
+
+ {/* Hero Header */} + +
+ + Live On-Chain Evidence +
+

+ Protection Is Not a Promise —
+ It's On-Chain Evidence +

+

+ Every commit, every execution, every MEV attack blocked — all recorded immutably on the Flow blockchain. No claims, no screenshots, no trust-me bro. +

+ + {/* Live indicator */} + +
+ + {/* Top-tier Stats */} +
+ + + + +
+ + {/* Protocol Stats Grid */} +
+ + + + = 90 ? '#00EF8B' : '#f59e0b'} delay={0.45} /> +
+ + {/* Evidence Section — The meat of the dashboard */} + +
+ +

+ Evidence — How Much MEV Was Stopped +

+
+
+ 0} + /> + + +
+
+ + {/* Protection Layer Breakdown */} + +
+
+ +

+ Protection Layer Breakdown +

+
+
+ {[ + { + layer: 'Layer 1', + name: 'Commit-Reveal', + desc: 'Execution hash committed first, revealed later — bots never see what you are executing', + count: metrics.mevCommitsCreated, + status: metrics.mevCommitsCreated > 0 ? 'ACTIVE' : 'NO DATA', + color: '#00EF8B', + icon: Shield, + }, + { + layer: 'Layer 2', + name: 'VRF Block-Delay Jitter', + desc: 'Random 0-5 block delay using Flow revertibleRandom() — unpredictable execution timing', + count: metrics.mevExecutionsProcessed, + status: metrics.mevExecutionsProcessed > 0 ? 'ACTIVE' : 'NO DATA', + color: '#37DDDF', + icon: Clock, + }, + { + layer: 'Layer 3', + name: 'Price Deviation Guard', + desc: 'Real-time APY comparison — rejects execution if oracle deviation exceeds slippage tolerance', + count: metrics.mevExecutionsRejected, + status: metrics.mevExecutionsRejected > 0 ? 'BLOCKED ATTACKS' : 'NO DATA', + color: '#f59e0b', + icon: Ban, + }, + { + layer: 'Layer 4', + name: 'VRF Execution Queue', + desc: 'VRF-shuffled processing order — nobody knows which trade executes when', + count: metrics.mevPendingExecutions, + status: metrics.mevPendingExecutions > 0 ? 'PENDING' : 'CLEAR', + color: '#8b5cf6', + icon: Activity, + }, + ].map((l, i) => ( +
+
+
+ +
+
+
+ {l.layer} +
+
{l.name}
+
+
+

+ {l.desc} +

+
+ + + + + {l.status} + +
+
+ ))} +
+
+
+ + {/* Strategy Yield Breakdown */} + +
+
+ +

+ Disabled Strategy Scaffolding +

+
+
+ {[ + { + name: 'Flow Liquid Staking Pro', + strategyId: 'liquid-staking-pro', + apy: metrics.lsAPY, + yield: metrics.lsTotalYield, + participants: metrics.lsParticipants, + color: '#00EF8B', + icon: '💎', + }, + { + name: 'DeFi Yield Maximizer', + strategyId: 'defi-yield-maximizer', + apy: metrics.yfAPY, + yield: metrics.yfTotalYield, + participants: metrics.yfParticipants, + color: '#37DDDF', + icon: '⚡', + }, + ].map((s, i) => ( +
+
+ {s.icon} +
+
{s.name}
+
+ {s.strategyId} +
+
+
+
+
+
APY
+
+ +
+
+
+
External Yield
+
+ +
+
+
+
Participants
+
+ +
+
+
+
Oracle Age
+
21600) ? '#f59e0b' : '#00EF8B', fontVariantNumeric: 'tabular-nums' }}> + {metrics.oracleAges?.[s.strategyId] ? formatAge(metrics.oracleAges[s.strategyId]) : '—'} +
+
+
+
+ ))} +
+
+
+ + {/* Trust Footer */} + +

+ All metrics are queried directly from the Flow blockchain. No caching, no interpolation, no fabricated data. + Every number shown is a verifiable on-chain value from SentinelVaultFinal,{' '} + MEVShieldCore, and{' '} + YieldOracle. +

+
+ e.currentTarget.style.borderColor = 'rgba(250,248,245,0.4)'} + onMouseLeave={e => e.currentTarget.style.borderColor = 'rgba(250,248,245,0.15)'} + > + Verify on Flowscan + + +
+
+
+
+
+ ) +} + diff --git a/app/vaults/page.tsx b/app/vaults/page.tsx index c4d198a..0381cd5 100644 --- a/app/vaults/page.tsx +++ b/app/vaults/page.tsx @@ -54,7 +54,6 @@ export default function VaultsPage() { { value: 'liquid-staking', label: 'Liquid Staking' }, { value: 'yield-farming', label: 'Yield Farming' }, { value: 'lending', label: 'Lending' }, - { value: 'arbitrage', label: 'Arbitrage' } ] const riskLevels = [ { value: 'all', label: 'All Risk Levels' }, diff --git a/components/dashboard/CircuitBreakerStatus.tsx b/components/dashboard/CircuitBreakerStatus.tsx new file mode 100644 index 0000000..1b39964 --- /dev/null +++ b/components/dashboard/CircuitBreakerStatus.tsx @@ -0,0 +1,219 @@ +'use client' + +import { motion } from 'framer-motion' +import { Shield, ShieldOff, AlertTriangle, Clock, DollarSign, Ban, Activity } from 'lucide-react' + +interface StrategyStatus { + isActive: boolean + name: string +} + +interface OracleStaleness { + age: number + isFresh: boolean + updatedAt: number +} + +interface CircuitBreakerData { + globalPaused: boolean + maxVaultBalanceCap: number + maxDepositPerBlock: number + maxSlippageHardCapBps: number + oracleStaleThresholdSeconds: number + strategies: Record + oracleStaleness: Record +} + +interface CircuitBreakerStatusProps { + data: CircuitBreakerData | null + loading?: boolean +} + +function formatDuration(seconds: number): string { + if (seconds < 60) return `${Math.floor(seconds)}s` + if (seconds < 3600) return `${Math.floor(seconds / 60)}m` + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h` + return `${Math.floor(seconds / 86400)}d` +} + +function StatusBadge({ active, label, activeLabel, inactiveLabel }: { + active: boolean + label: string + activeLabel?: string + inactiveLabel?: string +}) { + return ( +
+ + {label} + + + {active ? (activeLabel || 'ACTIVE') : (inactiveLabel || 'DISABLED')} + +
+ ) +} + +export function CircuitBreakerStatus({ data, loading }: CircuitBreakerStatusProps) { + if (loading || !data) { + return ( +
+
+ + CIRCUIT BREAKERS +
+
+ {loading ? 'Loading circuit breaker status...' : 'No data available'} +
+
+ ) + } + + const hasStaleOracle = Object.values(data.oracleStaleness || {}).some(s => !s.isFresh) + const hasKilledStrategy = Object.values(data.strategies || {}).some(s => !s.isActive) + + return ( + + {/* Header */} +
+
+ {data.globalPaused ? ( + + ) : ( + + )} + + CIRCUIT BREAKERS + +
+ {data.globalPaused && ( + + GLOBAL PAUSED + + )} +
+ + {/* Status indicators */} +
+ {/* Global pause */} + + + {/* Strategy kill switches */} + {Object.entries(data.strategies || {}).map(([id, s]) => ( + + ))} + + {/* Oracle staleness */} + + + {/* Deposit caps */} + + + {/* Max balance cap */} + + + {/* Hard slippage cap */} + +
+ + {/* Oracle staleness details */} + {data.oracleStaleness && Object.keys(data.oracleStaleness).length > 0 && ( +
+
+ + + Oracle Freshness + +
+ {Object.entries(data.oracleStaleness).map(([id, s]) => ( +
+ + {id.replace('liquid-staking-pro', 'Liquid Staking').replace('defi-yield-maximizer', 'Yield Farming')} + + + {s.isFresh ? `Fresh (${formatDuration(s.age)} old)` : `⚠ STALE (${formatDuration(s.age)} old)`} + +
+ ))} +
+ )} + + {/* Warning banner if any breakers triggered */} + {(data.globalPaused || hasKilledStrategy || hasStaleOracle) && ( +
+ + + {data.globalPaused ? 'Global circuit breaker is ACTIVE — all vault operations halted.' : + hasKilledStrategy ? 'One or more strategies have been killed. Some vaults cannot execute.' : + 'Oracle data is stale — strategy execution may be blocked.'} + +
+ )} +
+ ) +} diff --git a/components/dashboard/CreateVaultModal.tsx b/components/dashboard/CreateVaultModal.tsx index 1b27053..837236c 100644 --- a/components/dashboard/CreateVaultModal.tsx +++ b/components/dashboard/CreateVaultModal.tsx @@ -418,7 +418,7 @@ export function CreateVaultModal({ onClose, onSuccess, preselectedStrategy }: Cr Protocol Confirmation

- Deployment to the blockchain is irreversible. Your capital will be managed autonomously by the Flow Sentinel protocol. By proceeding, you authorize the smart contract to execute transactions on your behalf. + Vault creation is disabled in this release. No capital will be accepted until a real audited protocol adapter and production controls are deployed.

diff --git a/components/dashboard/IdleBalanceWidget.tsx b/components/dashboard/IdleBalanceWidget.tsx new file mode 100644 index 0000000..32c2a34 --- /dev/null +++ b/components/dashboard/IdleBalanceWidget.tsx @@ -0,0 +1,332 @@ +'use client' + +import { useState, useEffect } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { + Wallet, + TrendingUp, + Zap, + RotateCcw, + ChevronDown, + ExternalLink, + Sparkles, +} from 'lucide-react' +import { formatCurrency, formatPercentage } from 'lib/utils' + +interface IdleBalanceWidgetProps { + flowBalance: number + hasVaults: boolean + vaultBalance?: number + vaultApy?: number + vaultYieldAccrued?: number + vaultId?: string + vaultName?: string + onActivate: () => void + onRefresh: () => void +} + +export function IdleBalanceWidget({ + flowBalance, + hasVaults, + vaultBalance, + vaultApy, + vaultYieldAccrued, + vaultId, + vaultName, + onActivate, + onRefresh, +}: IdleBalanceWidgetProps) { + const [isEarning, setIsEarning] = useState(hasVaults && (vaultBalance ?? 0) > 0) + const [isExpanded, setIsExpanded] = useState(false) + const [mounted, setMounted] = useState(false) + + useEffect(() => { setMounted(true) }, []) + + // Sync earning state with props (e.g., after parent creates vault and refetches) + useEffect(() => { + setIsEarning(hasVaults && (vaultBalance ?? 0) > 0) + }, [hasVaults, vaultBalance]) + + const handleToggleEarning = () => { + if (isEarning) { + setIsExpanded(!isExpanded) + } else { + onActivate() + } + } + + // Projected earnings + // No APY is displayed until a real audited adapter produces an on-chain position. + const apyRate = 0 + const earningBalance = vaultBalance ?? flowBalance + const dailyYield = earningBalance * (apyRate / 365) + const monthlyYield = dailyYield * 30 + const yearlyYield = dailyYield * 365 + const totalEarned = vaultYieldAccrued ?? 0 + const displayBalance = isEarning ? (vaultBalance ?? flowBalance) : flowBalance + + if (!mounted) { + return ( +
+
+
+
+
+ ) + } + + return ( +
0 && !isEarning + ? '1px solid rgba(55,221,223,0.15)' + : undefined, + }} + > + {isEarning && ( +
+ )} + +
+
+
+
0 + ? 'rgba(55,221,223,0.10)' + : 'rgba(250,248,245,0.04)', + transition: 'all 0.3s', + }} + > + {isEarning ? ( + + ) : ( + 0 ? '#37DDDF' : 'rgba(250,248,245,0.3)' }} /> + )} +
+
+
+ {isEarning ? 'EARNING ACTIVE' : flowBalance > 0 ? 'IDLE BALANCE' : 'WALLET BALANCE'} +
+
+ {isEarning ? `custody vault: ${vaultName ?? 'Wallet Vault'}` : 'Yield integrations disabled'} +
+
+
+ + {isEarning ? ( + + + COMPOUNDING + + ) : flowBalance > 0 ? ( + + {hasVaults ? 'PARTIAL' : 'PENDING'} + + ) : null} +
+ +
+ + {formatCurrency(displayBalance)} + + + FLOW + + {isEarning && ( + + + No APY + + )} +
+ + {isEarning && ( +
+ + + +
+ )} + + {!isEarning && flowBalance > 0 && ( +
+
+ + + External yield integrations are disabled + +
+
+ )} + +
+ + + {!isEarning && ( + + )} +
+ + + {isExpanded && isEarning && ( + +
+
+
+
Yield Earned
+
+ +{totalEarned.toFixed(4)} FLOW +
+
+
+
External APY
+
+ No APY +
+
+
+
Compounding
+
+ Active +
+
+
+
Protection
+
+ Full MEV Shield +
+
+
+ {vaultId && ( + (e.currentTarget.style.color = 'rgba(250,248,245,0.6)')} + onMouseLeave={e => (e.currentTarget.style.color = 'rgba(250,248,245,0.3)')} + > + + View on Flowscan + + )} +
+
+ )} +
+
+
+ ) +} + +// Helper sub-component for projected earnings lines +function ProjectedEarnings({ label, value }: { label: string; value: number }) { + return ( +
+ + {label} + + + +{value.toFixed(value >= 1 ? 2 : 4)} + +
+ ) +} diff --git a/components/dashboard/OracleFreshnessBar.tsx b/components/dashboard/OracleFreshnessBar.tsx index 26ff3b1..a374e0f 100644 --- a/components/dashboard/OracleFreshnessBar.tsx +++ b/components/dashboard/OracleFreshnessBar.tsx @@ -11,8 +11,18 @@ interface OracleAPYEntry { confidence: number } +interface OracleProvenanceEntry { + protocolName: string + protocolAddress: string + methodology: string + methodologyUrl: string + verified: boolean + riskScore: number +} + interface OracleFreshnessBarProps { apyData: Record + provenanceData?: Record onForceRefresh?: () => void } @@ -26,7 +36,7 @@ function formatAge(seconds: number): string { const STALE_WARN_SECONDS = 6 * 3600 // 6 hours — warn const STALE_CRIT_SECONDS = 24 * 3600 // 24 hours — critical -export function OracleFreshnessBar({ apyData, onForceRefresh }: OracleFreshnessBarProps) { +export function OracleFreshnessBar({ apyData, provenanceData, onForceRefresh }: OracleFreshnessBarProps) { const [now, setNow] = useState(Date.now() / 1000) // Tick every 30 seconds so the "X ago" label stays fresh @@ -73,26 +83,38 @@ export function OracleFreshnessBar({ apyData, onForceRefresh }: OracleFreshnessB `⚠ Stale data — last update ${formatAge(ageSeconds)}`} - {/* Per-strategy APY pills */} + {/* Per-strategy APY pills with provenance badges */}
- {entries.slice(0, 4).map(([id, v]) => ( - - {id.replace('liquid-staking-pro', 'LS').replace('defi-yield-maximizer', 'DFY').replace('arbitrage-hunter', 'ARB').replace('high-yield-farming', 'HYF')} - {' '} - {v.apy.toFixed(2)}% - - ))} + {entries.slice(0, 4).map(([id, v]) => { + const prov = provenanceData?.[id] + const badgeBg = prov?.verified ? 'rgba(0,239,139,0.10)' : 'rgba(250,248,245,0.05)' + const badgeBorder = prov?.verified ? '1px solid rgba(0,239,139,0.20)' : '1px solid rgba(250,248,245,0.08)' + return ( + + {prov?.verified && ( + + )} + {prov?.protocolName ?? id.replace('liquid-staking-pro', 'LS').replace('defi-yield-maximizer', 'DFY')} + {' '} + {v.apy.toFixed(2)}% + + ) + })}
diff --git a/components/dashboard/ReserveHealthWidget.tsx b/components/dashboard/ReserveHealthWidget.tsx index d2d6013..f938e1e 100644 --- a/components/dashboard/ReserveHealthWidget.tsx +++ b/components/dashboard/ReserveHealthWidget.tsx @@ -159,7 +159,7 @@ export function ReserveHealthWidget({ stats, onFunded }: ReserveHealthWidgetProp { label: 'Fees Collected', value: `${stats.totalFeesCollected.toFixed(4)} FLOW` }, { label: 'Protocol Fee', value: `${stats.protocolFeeRateBps} bps (0.1%)` }, { label: 'Total Vaults', value: stats.totalVaults.toString() }, - { label: 'MEV Protections', value: stats.mevTotalProtections.toLocaleString() }, + { label: 'Protection Events', value: stats.mevTotalProtections.toLocaleString() }, { label: 'Pending Exec.', value: stats.mevPendingExecutions.toString() }, ].map((item, i) => (
diff --git a/components/dashboard/VaultCard.tsx b/components/dashboard/VaultCard.tsx index 5f2fee5..b61cea8 100644 --- a/components/dashboard/VaultCard.tsx +++ b/components/dashboard/VaultCard.tsx @@ -17,7 +17,8 @@ import { ExternalLink, ChevronUp, Activity, - Sparkles + Sparkles, + RotateCcw } from 'lucide-react' import { Badge } from 'components/ui/badge' import { Progress } from 'components/ui/progress' @@ -25,9 +26,11 @@ import { formatCurrency, formatPercentage } from 'lib/utils' import { FlowService } from 'lib/flow-service' import { errorReporter } from '@/lib/sentry-wrapper' import { VaultActionModal } from './VaultActionModal' +import { YieldProvenancePanel } from './YieldProvenancePanel' import { useVaultData } from 'hooks/useVaultData' import { useTransactions } from 'lib/transactions' import { useActivityFeed } from 'hooks/useActivityFeed' +import type { ProvenanceEntry } from 'hooks/useVaultData' interface Vault { id: string @@ -52,10 +55,14 @@ interface Vault { blockDelayEnabled?: boolean mevProtectionsTriggered?: number mevShieldStatus?: string + // Auto-compound fields + autoCompoundEnabled?: boolean + totalYieldCompounded?: number } interface VaultCardProps { vault: Vault + provenance?: ProvenanceEntry } // Custom comparator: only re-render when critical data changes @@ -67,7 +74,8 @@ const vaultCardComparator = (prev: VaultCardProps, next: VaultCardProps) => prev.vault.status === next.vault.status && prev.vault.pnl === next.vault.pnl && prev.vault.pnlPercent === next.vault.pnlPercent && - prev.vault.mevProtectionsTriggered === next.vault.mevProtectionsTriggered + prev.vault.mevProtectionsTriggered === next.vault.mevProtectionsTriggered && + prev.vault.autoCompoundEnabled === next.vault.autoCompoundEnabled // Phase 5: Execution history panel — shows last executions from the activity feed function ExecutionHistoryPanel({ vaultId, vaultName }: { vaultId: string; vaultName: string }) { @@ -144,7 +152,7 @@ function ExecutionHistoryPanel({ vaultId, vaultName }: { vaultId: string; vaultN ) } -export const VaultCard = memo(function VaultCard({ vault }: VaultCardProps) { +export const VaultCard = memo(function VaultCard({ vault, provenance }: VaultCardProps) { const [isExpanded, setIsExpanded] = useState(false) const [loading, setLoading] = useState(false) const [mounted, setMounted] = useState(false) @@ -282,6 +290,30 @@ export const VaultCard = memo(function VaultCard({ vault }: VaultCardProps) { } finally { setLoading(false) } } + // Auto-compound state: persisted in localStorage (yield always compounds on-chain) + // The toggle controls whether the frontend shows "claimable" vs "compounding" state. + // Yield is ALWAYS reinvested into the vault balance on-chain — this is a UX toggle. + const AUTO_COMPOUND_KEY = `sentinel-auto-compound-${vault.id}` + const [localAutoCompound, setLocalAutoCompound] = useState(() => { + if (typeof window === 'undefined') return true + const stored = localStorage.getItem(AUTO_COMPOUND_KEY) + return stored !== null ? stored === 'true' : true + }) + + useEffect(() => { + localStorage.setItem(AUTO_COMPOUND_KEY, String(localAutoCompound)) + }, [localAutoCompound, AUTO_COMPOUND_KEY]) + + const handleToggleAutoCompound = () => { + setLocalAutoCompound(prev => !prev) + addActivity({ + type: 'alert', + title: localAutoCompound ? 'Auto-Compound Deactivated' : 'Auto-Compound Activated', + description: `${vault.name} auto-compound ${localAutoCompound ? 'disabled' : 'enabled'}`, + vault: vault.name, + }) + } + const execManualStrategy = async () => { setTxState({ status: 'executing', txId: null, error: null, title: 'Triggering Strategy' }) // Phase 1 Fix: generate real SHA3-256 commit hash off-chain before submitting @@ -359,13 +391,13 @@ export const VaultCard = memo(function VaultCard({ vault }: VaultCardProps) {
- FORTE AUTONOMY + AUTOMATED YIELD - {vault.mevShieldStatus === 'FULL-MEV-SHIELD' ? 'MEV-SHIELD PRO' : - vault.protectionLevel === 1 ? 'MEV-VRF' : - vault.protectionLevel === 2 ? 'MEV-CR' : 'MEV-SHIELD'} + {vault.mevShieldStatus === 'FULL-MEV-SHIELD' || (vault.protectionLevel ?? 3) >= 3 ? 'SHIELD-PROTECTED' : + vault.protectionLevel === 1 ? 'BASIC-SHIELD' : + vault.protectionLevel === 2 ? 'ADVANCED-SHIELD' : 'PROTECTED'} FLOW
- {(vault.totalYieldAccrued ?? 0) > 0 && ( + {(vault.totalYieldAccrued ?? 0) > 0 && !localAutoCompound && (
+ {/* Auto-compound indicator — yield always compounds on-chain */} +
+

Yield Reinvested

+
+ 0 ? '#00EF8B' : 'rgba(250,248,245,0.25)', + fontVariantNumeric: 'tabular-nums', + }}> + {(vault.totalYieldAccrued ?? 0).toFixed(4)} + + FLOW + + COMPOUNDING + +
+
@@ -590,7 +639,7 @@ export const VaultCard = memo(function VaultCard({ vault }: VaultCardProps) { Trigger Forte Task - {(vault.totalYieldAccrued || 0) > 0 && ( + {(vault.totalYieldAccrued || 0) > 0 && !localAutoCompound && ( +
+
+ Total Accrued Yield + +{(vault.pnl ?? 0).toFixed(4)} FLOW +
+
+ Claimable + 0 && !localAutoCompound ? '#00EF8B' : 'rgba(250,248,245,0.25)' }}> + {localAutoCompound ? '— (reinvested)' : (vault.totalYieldAccrued ?? 0).toFixed(4) + ' FLOW'} + +
+
+

- MEV-Shield Pro — Security Report + Protection Report

diff --git a/components/dashboard/YieldProvenancePanel.tsx b/components/dashboard/YieldProvenancePanel.tsx new file mode 100644 index 0000000..966f94e --- /dev/null +++ b/components/dashboard/YieldProvenancePanel.tsx @@ -0,0 +1,278 @@ +'use client' + +import { motion } from 'framer-motion' +import { Shield, ShieldCheck, ExternalLink, AlertTriangle, Info } from 'lucide-react' + +export interface ProvenanceData { + protocolName: string + protocolAddress: string + methodology: string + methodologyUrl: string + verified: boolean + riskScore: number + updatedAt: number +} + +interface YieldProvenancePanelProps { + provenance: ProvenanceData + apy: number + source: string + confidence: number + compact?: boolean +} + +const RISK_LABELS: Record = { + 'epoch-rewards': { label: 'On-Chain Rewards', color: '#00EF8B' }, + 'lending-pool-apy': { label: 'Lending Pool APY', color: '#37DDDF' }, + 'multi-protocol': { label: 'Aggregated Sources', color: '#f59e0b' }, + 'aggregated': { label: 'Aggregated', color: '#f59e0b' }, + 'dex-lp': { label: 'DEX LP Fees', color: '#8b5cf6' }, +} + +function getRiskColor(score: number): string { + if (score < 0.2) return '#00EF8B' + if (score < 0.4) return '#37DDDF' + if (score < 0.6) return '#f59e0b' + return '#ef4444' +} + +function getRiskLabel(score: number): string { + if (score < 0.2) return 'Very Low' + if (score < 0.4) return 'Low' + if (score < 0.6) return 'Moderate' + if (score < 0.8) return 'High' + return 'Very High' +} + +function getMethodologyLabel(methodology: string): string { + return RISK_LABELS[methodology]?.label ?? methodology.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) +} + +function getMethodologyColor(methodology: string): string { + return RISK_LABELS[methodology]?.color ?? '#FAF8F5' +} + +function trimAddress(addr: string): string { + if (!addr) return '' + return addr.length > 12 ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : addr +} + +export function YieldProvenancePanel({ provenance, apy, source, confidence, compact }: YieldProvenancePanelProps) { + const riskColor = getRiskColor(provenance.riskScore) + const methodologyColor = getMethodologyColor(provenance.methodology) + + if (compact) { + return ( +
+ {/* Verified badge */} + {provenance.verified ? ( + + + VERIFIED + + ) : ( + + + UNVERIFIED + + )} + + {/* Protocol name */} + + {provenance.protocolName} + + + {/* Methodology */} + + {getMethodologyLabel(provenance.methodology)} + +
+ ) + } + + return ( + + {/* Header */} +
+
+ + + Yield Provenance + +
+ {provenance.verified ? ( + + + SOURCE VERIFIED + + ) : ( + + + UNVERIFIED + + )} +
+ + {/* Protocol Info Grid */} + + + {/* Metrics Row */} +
+ {/* Risk Score */} +
+
+ Risk Score +
+
+
+ + {getRiskLabel(provenance.riskScore)} ({(provenance.riskScore * 100).toFixed(0)}) + +
+
+ + {/* Confidence */} +
+
+ Confidence +
+ = 0.9 ? '#00EF8B' : confidence >= 0.7 ? '#f59e0b' : '#ef4444', letterSpacing: '0.05em' }}> + {(confidence * 100).toFixed(0)}% + +
+ + {/* Current APY */} +
+
+ Current APY +
+ + {apy.toFixed(2)}% + +
+ + {/* Source */} +
+
+ Data Feed +
+ + {source} + +
+
+ + ) +} diff --git a/components/onboarding/OnboardingFlow.tsx b/components/onboarding/OnboardingFlow.tsx index 3ce217c..1379d54 100644 --- a/components/onboarding/OnboardingFlow.tsx +++ b/components/onboarding/OnboardingFlow.tsx @@ -9,7 +9,7 @@ const steps = [ { title: 'Welcome to Flow Sentinel', subtitle: 'The Autonomous DeFi Wealth Manager', - description: 'Flow Sentinel is the first fully autonomous, MEV-resistant wealth manager built on the Flow blockchain. Your vaults work for you — 24/7, without manual intervention.', + description: 'Flow Sentinel currently provides FLOW custody and withdrawal infrastructure. Yield integrations and autonomous execution are disabled until audited production adapters are deployed.', icon: Rocket, color: '#00EF8B', }, @@ -23,14 +23,14 @@ const steps = [ { title: 'Choose a Strategy', subtitle: 'Select Your Risk Profile', - description: 'Pick from multiple automated strategies — from conservative liquid staking to high-yield farming. Each strategy has a different risk level and expected APY range.', + description: 'Review the available testnet strategy demonstrations. Current returns are oracle-driven and reserve-funded; external protocol execution is not enabled.', icon: Shield, color: '#00EF8B', }, { title: 'Deploy & Earn', subtitle: 'Your Vault Runs Automatically', - description: 'Deposit FLOW tokens into your vault, and the protocol handles the rest — strategy execution, yield compounding, and MEV protection. Monitor your performance in real-time.', + description: 'New vault creation and yield execution are currently disabled. Existing vault owners can review state and withdraw through the audited custody path.', icon: Zap, color: '#37DDDF', },