├── .env.sample ├── .eslintrc.json ├── .gitignore ├── .gitmodules ├── LICENSE.md ├── README.md ├── app ├── api │ ├── aiSummary │ │ └── route.ts │ ├── frame │ │ └── home │ │ │ └── route.ts │ ├── getTrendingEIPs │ │ └── route.ts │ ├── logPageVisit │ │ └── route.ts │ ├── og │ │ └── route.tsx │ └── webhook │ │ └── route.ts ├── caip │ └── [eipOrNo] │ │ ├── layout.tsx │ │ └── page.tsx ├── eip │ └── [eipOrNo] │ │ ├── layout.tsx │ │ └── page.tsx ├── fonts.ts ├── graph │ ├── layout.tsx │ └── page.tsx ├── icon.png ├── layout.tsx ├── lib │ └── neynar.ts ├── page.tsx ├── providers.tsx ├── rip │ └── [eipOrNo] │ │ ├── layout.tsx │ │ └── page.tsx └── shared │ ├── layout.tsx │ └── page.tsx ├── assets └── Poppins-Bold.ttf ├── components ├── Analytics.tsx ├── CodeBlock.tsx ├── CopyToClipboard.tsx ├── EIPGraph.tsx ├── EIPGraphSection.tsx ├── EIPGraphWrapper.tsx ├── EIPOfTheDay.tsx ├── Footer.tsx ├── Layout.tsx ├── Markdown.tsx ├── Navbar.tsx ├── NotificationBar.tsx ├── PectraEIPs.tsx ├── ScrollToTopButton.tsx ├── Searchbox.tsx └── TrendingEIPs.tsx ├── data ├── eip-graph-data.json ├── eipGraphData.ts ├── schemas.ts ├── valid-caips.json ├── valid-eips.json ├── valid-rips.json ├── validCAIPs.ts ├── validEIPs.ts └── validRIPs.ts ├── funding.json ├── hooks └── useTopLoaderRouter.tsx ├── middleware.ts ├── models ├── aiSummary.ts └── pageVisit.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── public ├── eth.png └── og │ ├── base.png │ ├── graph.png │ └── index.png ├── scripts ├── create-webhook.ts ├── fetchValidEIPs.ts ├── genEIPDependencyGraph.ts ├── getWIPEIPsFromPRs.ts └── test-webhook.ts ├── style └── theme.ts ├── styles └── globals.css ├── temp └── eip-7830.md ├── tsconfig.json ├── types.ts └── utils └── index.ts /.env.sample: -------------------------------------------------------------------------------- 1 | # .env.local 2 | GITHUB_TOKEN= 3 | MONGODB_URL= 4 | NEXT_PUBLIC_DEVELOPMENT=true 5 | HOST=http://localhost:3000 6 | OPENAI_API_KEY= 7 | OPENAI_ORG_ID= 8 | 9 | NEYNAR_API_KEY= 10 | WEBHOOK_URL=https://eip.tools/api/webhook 11 | NEYNAR_SIGNER_UUID= 12 | NEYNAR_WEBHOOK_SECRET= 13 | 14 | NEXT_PUBLIC_GITCOIN_GRANTS_LINK= 15 | NEXT_PUBLIC_GITCOIN_GRANTS_ACTIVE="false" 16 | NEXT_PUBLIC_VERCEL_URL=localhost:3000 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/ERCs"] 2 | path = submodules/ERCs 3 | url = https://github.com/ethereum/ERCs.git 4 | [submodule "submodules/EIPs"] 5 | path = submodules/EIPs 6 | url = https://github.com/ethereum/EIPs.git 7 | [submodule "submodules/RIPs"] 8 | path = submodules/RIPs 9 | url = https://github.com/ethereum/RIPs.git 10 | [submodule "submodules/CAIPs"] 11 | path = submodules/CAIPs 12 | url = https://github.com/ChainAgnostic/CAIPs 13 | branch = main 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Apoorv Lathey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eip.tools 2 | -------------------------------------------------------------------------------- /app/api/aiSummary/route.ts: -------------------------------------------------------------------------------- 1 | // add this to prevent the build command from static generating this page 2 | export const dynamic = "force-dynamic"; 3 | 4 | import mongoose from "mongoose"; 5 | import OpenAI from "openai"; 6 | 7 | import { AISummary } from "@/models/aiSummary"; 8 | import { AISummaryRequest, AISummaryRequestSchema } from "@/data/schemas"; 9 | import { validEIPs } from "@/data/validEIPs"; 10 | import { validRIPs } from "@/data/validRIPs"; 11 | import { validCAIPs } from "@/data/validCAIPs"; 12 | import { EIPStatus } from "@/utils"; 13 | 14 | const openai = new OpenAI({ 15 | organization: process.env.OPENAI_ORG_ID, 16 | project: null, // Default Project 17 | apiKey: process.env.OPENAI_API_KEY, 18 | }); 19 | 20 | export const POST = async (request: Request) => { 21 | // validate request body 22 | let body: AISummaryRequest; 23 | try { 24 | const requestBody = await request.json(); 25 | body = AISummaryRequestSchema.parse(requestBody); 26 | } catch { 27 | return new Response("Invalid request body", { status: 400 }); 28 | } 29 | 30 | const eipNo = body.eipNo; 31 | const type = body.type ?? "EIP"; 32 | 33 | await mongoose.connect(process.env.MONGODB_URL!); 34 | 35 | // query "summaries" mongodb for this eipNo 36 | const summaryRes = await AISummary.findOne({ eipNo }); 37 | 38 | // If summary exists, return 39 | if (summaryRes) { 40 | return new Response(JSON.stringify(summaryRes.summary), { 41 | status: 200, 42 | }); 43 | } 44 | 45 | // If not, then send EIP markdown to chatgpt api 46 | const { status, markdownPath } = 47 | type === "RIP" 48 | ? validRIPs[eipNo] 49 | : type === "CAIP" 50 | ? validCAIPs[eipNo] 51 | : validEIPs[eipNo]; 52 | const markdown = await fetch(markdownPath).then((res) => res.text()); 53 | 54 | try { 55 | const stream = await openai.chat.completions.create({ 56 | model: "gpt-4o", 57 | messages: [ 58 | { 59 | role: "user", 60 | content: `Summarize the following EIP/ERC no: ${eipNo} into easy to understand & just in few lines. 61 | Don't start with any other extra stuff, only output concise 4-6 lines. I need to directly display this output on a website. 62 | Here's the markdown to summarize: 63 | ${markdown}`, 64 | }, 65 | ], 66 | stream: true, 67 | }); 68 | 69 | let response = ""; 70 | for await (const chunk of stream) { 71 | const content = chunk.choices[0]?.delta?.content || ""; 72 | response += content; 73 | } 74 | 75 | // save the response in mongodb 76 | await AISummary.create({ 77 | eipNo, 78 | summary: response, 79 | eipStatus: status, 80 | timestamp: new Date(), 81 | }); 82 | 83 | // return string summary 84 | return new Response(JSON.stringify(response), { 85 | status: 200, 86 | }); 87 | } catch (e) { 88 | // send error 89 | return new Response(JSON.stringify(e), { status: 500 }); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /app/api/frame/home/route.ts: -------------------------------------------------------------------------------- 1 | // add this to prevent the build command from static generating this page 2 | export const dynamic = "force-dynamic"; 3 | 4 | import { validEIPs } from "@/data/validEIPs"; 5 | import { NextRequest, NextResponse } from "next/server"; 6 | 7 | export async function POST(req: NextRequest) { 8 | const { untrustedData } = await req.json(); 9 | 10 | if (untrustedData.inputText && untrustedData.inputText.length > 0) { 11 | try { 12 | const eipNo = parseInt(untrustedData.inputText as string); 13 | const validEIP = validEIPs[eipNo]; 14 | 15 | const imageUrl = `${process.env["HOST"]}/api/og?eipNo=${eipNo}`; 16 | const postUrl = `${process.env["HOST"]}/api/frame/home`; 17 | 18 | return new NextResponse( 19 | ` 20 | 21 | 22 | EIP.tools 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 53 | 54 | 57 | 58 | 59 | `, 60 | { 61 | status: 200, 62 | headers: { 63 | "Content-Type": "text/html", 64 | }, 65 | } 66 | ); 67 | } catch {} 68 | } 69 | 70 | // return back the same frame if no / invalid input received 71 | const imageUrl = `${process.env["HOST"]}/og/index.png?date=${Date.now()}`; 72 | const postUrl = `${process.env["HOST"]}/api/frame/home`; 73 | 74 | return new NextResponse( 75 | ` 76 | 77 | 78 | EIP.tools 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | `, 98 | { 99 | status: 200, 100 | headers: { 101 | "Content-Type": "text/html", 102 | }, 103 | } 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /app/api/getTrendingEIPs/route.ts: -------------------------------------------------------------------------------- 1 | // add this to prevent the build command from static generating this page 2 | export const dynamic = "force-dynamic"; 3 | 4 | import { NextRequest } from "next/server"; 5 | import mongoose from "mongoose"; 6 | import { subDays } from "date-fns"; 7 | import { PageVisit } from "@/models/pageVisit"; 8 | 9 | export const GET = async (req: NextRequest) => { 10 | await mongoose.connect(process.env.MONGODB_URL!); 11 | 12 | const sevenDaysAgo = subDays(new Date(), 7); 13 | const trendingCount = 5; 14 | 15 | const topPages = await PageVisit.aggregate([ 16 | { 17 | $match: { 18 | timestamp: { $gte: sevenDaysAgo }, 19 | }, 20 | }, 21 | { 22 | $group: { 23 | _id: "$eipNo", 24 | count: { $sum: 1 }, 25 | type: { $first: "$type" }, // Assuming 'type' is a field in the same collection 26 | }, 27 | }, 28 | { 29 | $sort: { count: -1 }, 30 | }, 31 | { 32 | $limit: trendingCount, 33 | }, 34 | ]); 35 | 36 | return new Response(JSON.stringify(topPages), { 37 | status: 200, 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /app/api/logPageVisit/route.ts: -------------------------------------------------------------------------------- 1 | // add this to prevent the build command from static generating this page 2 | export const dynamic = "force-dynamic"; 3 | 4 | import mongoose from "mongoose"; 5 | import { PageVisit } from "@/models/pageVisit"; 6 | import { LogPageVisitRequest, logPageVisitRequestSchema } from "@/data/schemas"; 7 | 8 | export const POST = async (request: Request) => { 9 | // validate request body 10 | let body: LogPageVisitRequest; 11 | try { 12 | const requestBody = await request.json(); 13 | body = logPageVisitRequestSchema.parse(requestBody); 14 | } catch { 15 | return new Response("Invalid request body", { status: 400 }); 16 | } 17 | 18 | const eipNo = body.eipNo; 19 | const type = body.type; 20 | 21 | await mongoose.connect(process.env.MONGODB_URL!); 22 | 23 | const pageVisit = new PageVisit({ 24 | eipNo, 25 | type, 26 | }); 27 | await pageVisit.save(); 28 | 29 | return new Response("Page visit logged", { status: 200 }); 30 | }; 31 | -------------------------------------------------------------------------------- /app/api/og/route.tsx: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from "next/og"; 2 | import { validEIPs } from "@/data/validEIPs"; 3 | import { validRIPs } from "@/data/validRIPs"; 4 | import { validCAIPs } from "@/data/validCAIPs"; 5 | import { EIPStatus } from "@/utils"; 6 | 7 | export const runtime = "edge"; 8 | 9 | export async function GET(req: Request) { 10 | try { 11 | const { searchParams } = new URL(req.url); 12 | 13 | const eipNo = parseInt(searchParams.get("eipNo")!); 14 | const type = searchParams.get("type") ?? "EIP"; 15 | const eipData = 16 | type === "RIP" 17 | ? validRIPs[eipNo] 18 | : type === "CAIP" 19 | ? validCAIPs[eipNo] 20 | : validEIPs[eipNo]; 21 | const statusInfo = EIPStatus[eipData.status ?? "Draft"]; 22 | 23 | const fontData = await fetch( 24 | new URL("../../../assets/Poppins-Bold.ttf", import.meta.url) 25 | ).then((res) => res.arrayBuffer()); 26 | 27 | const imgArrayBuffer = await fetch( 28 | new URL("../../../public/og/base.png", import.meta.url) 29 | ).then((res) => res.arrayBuffer()); 30 | const buffer = Buffer.from(imgArrayBuffer); 31 | const imgUrl = `data:image/png;base64,${buffer.toString("base64")}`; 32 | 33 | return new ImageResponse( 34 | ( 35 |
46 |
54 | {eipData.status && ( 55 |
65 | {statusInfo.prefix} {eipData.status} 66 |
67 | )} 68 |
77 | {type === "RIP" 78 | ? "RIP" 79 | : type === "CAIP" 80 | ? "CAIP" 81 | : eipData.isERC 82 | ? "ERC" 83 | : "EIP"} 84 | -{eipNo} 85 |
86 |
93 |
99 | {eipData.title} 100 |
101 |
102 |
103 |
104 | ), 105 | { 106 | width: 2144, 107 | height: 1122, 108 | fonts: [ 109 | { 110 | name: "Poppins", 111 | data: fontData, 112 | style: "normal", 113 | }, 114 | ], 115 | } 116 | ); 117 | } catch (e: any) { 118 | return new Response(e.message, { status: 500 }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { validEIPsArray } from "@/data/validEIPs"; 3 | import neynarClient from "@/app/lib/neynar"; 4 | import crypto from "crypto"; 5 | 6 | // Define allowed methods 7 | export const dynamic = "force-dynamic"; 8 | 9 | // EIP numbers to ignore 10 | const IGNORED_EIPS = [ 11 | "1", 12 | "2", 13 | "3", 14 | "4", 15 | "5", 16 | "6", 17 | "7", 18 | "8", 19 | "9", 20 | "20", 21 | "155", 22 | "721", 23 | "1155", 24 | "2025", 25 | ]; 26 | 27 | interface WebhookData { 28 | type: string; 29 | data: { 30 | text: string; 31 | hash: string; 32 | }; 33 | } 34 | 35 | // This is required to handle OPTIONS requests from CORS preflight 36 | export async function OPTIONS() { 37 | return new NextResponse(null, { 38 | status: 200, 39 | headers: { 40 | Allow: "POST", 41 | }, 42 | }); 43 | } 44 | 45 | export async function POST(req: Request) { 46 | try { 47 | // Get the signature from header 48 | const signature = req.headers.get("x-neynar-signature"); 49 | if (!signature) { 50 | return NextResponse.json( 51 | { error: "Missing signature header" }, 52 | { status: 401 } 53 | ); 54 | } 55 | 56 | // Get the raw request body as a string 57 | const rawBody = await req.text(); 58 | 59 | // Create signature using shared secret 60 | const webhookSecret = process.env.NEYNAR_WEBHOOK_SECRET; 61 | if (!webhookSecret) { 62 | throw new Error("NEYNAR_WEBHOOK_SECRET not set in environment variables"); 63 | } 64 | 65 | // Convert webhook secret to Uint8Array 66 | const encoder = new TextEncoder(); 67 | const keyData = encoder.encode(webhookSecret); 68 | const messageData = encoder.encode(rawBody); 69 | 70 | // Create HMAC using Web Crypto API 71 | const key = await crypto.subtle.importKey( 72 | "raw", 73 | keyData, 74 | { name: "HMAC", hash: "SHA-512" }, 75 | false, 76 | ["sign"] 77 | ); 78 | 79 | const signature_array = await crypto.subtle.sign("HMAC", key, messageData); 80 | 81 | // Convert to hex string 82 | const computedSignature = Array.from(new Uint8Array(signature_array)) 83 | .map((b) => b.toString(16).padStart(2, "0")) 84 | .join(""); 85 | 86 | // Compare signatures 87 | if (signature !== computedSignature) { 88 | return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); 89 | } 90 | 91 | // Parse the body after verification 92 | const body = JSON.parse(rawBody) as WebhookData; 93 | 94 | if (body.type === "cast.created") { 95 | const text = body.data.text; 96 | const originalCastHash = body.data.hash; 97 | 98 | // Match patterns for numbers with optional EIP/ERC prefix 99 | const pattern = /(?:eip[-\s]?(\d+)|erc[-\s]?(\d+)|(? 0) { 118 | if (!process.env.NEYNAR_SIGNER_UUID) { 119 | throw new Error( 120 | "Make sure you set NEYNAR_SIGNER_UUID in your .env file" 121 | ); 122 | } 123 | 124 | // Create the reply text based on number of URLs 125 | let replyText = "Explore the EIPs / ERCs mentioned in this cast:"; 126 | if (urls.length >= 2) { 127 | const additionalUrls = urls.slice(1); 128 | replyText += `\n\n${additionalUrls.join("\n")}`; 129 | } 130 | 131 | // Post the reply using Neynar client 132 | const reply = await neynarClient.publishCast({ 133 | signerUuid: process.env.NEYNAR_SIGNER_UUID, 134 | text: replyText, 135 | embeds: [ 136 | { 137 | url: urls[0], 138 | }, 139 | ], 140 | parent: originalCastHash, 141 | }); 142 | 143 | console.log("Posted reply:", reply.cast); 144 | } 145 | } 146 | 147 | return NextResponse.json({ success: true }); 148 | } catch (error) { 149 | console.error("Webhook error:", error); 150 | return NextResponse.json( 151 | { error: "Internal server error" }, 152 | { status: 500 } 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /app/caip/[eipOrNo]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from "@/components/Layout"; 2 | import { 3 | convertMetadataToJson, 4 | extractEipNumber, 5 | extractMetadata, 6 | getMetadata, 7 | } from "@/utils"; 8 | import { validCAIPs } from "@/data/validCAIPs"; 9 | 10 | export async function generateMetadata({ 11 | params: { eipOrNo }, 12 | }: { 13 | params: { eipOrNo: string }; 14 | }) { 15 | const eipNo = extractEipNumber(eipOrNo, "caip"); 16 | const validEIPData = validCAIPs[parseInt(eipNo)]; 17 | 18 | if (!validEIPData) { 19 | return; 20 | } 21 | 22 | const eipMarkdownRes = await fetch(validEIPData.markdownPath).then( 23 | (response) => response.text() 24 | ); 25 | const { metadata } = extractMetadata(eipMarkdownRes); 26 | const metadataJson = convertMetadataToJson(metadata); 27 | 28 | const imageUrl = `${process.env["HOST"]}/api/og?eipNo=${eipNo}&type=CAIP`; 29 | const postUrl = `${process.env["HOST"]}/api/frame/home`; 30 | 31 | const generated = getMetadata({ 32 | title: `CAIP-${eipNo}: ${validEIPData.title} | EIP.tools`, 33 | description: metadataJson.description, 34 | images: imageUrl, 35 | }); 36 | 37 | return { 38 | ...generated, 39 | other: { 40 | "fc:frame": "vNext", 41 | "fc:frame:image": imageUrl, 42 | "fc:frame:post_url": postUrl, 43 | "fc:frame:input:text": "Enter EIP/ERC No", 44 | "fc:frame:button:1": "Search 🔎", 45 | "fc:frame:button:2": `📙 ${validEIPData.isERC ? "ERC" : "EIP"}-${eipNo}`, 46 | "fc:frame:button:2:action": "link", 47 | "fc:frame:button:2:target": `${process.env["HOST"]}/eip/${eipNo}`, 48 | "of:version": "vNext", 49 | "of:accepts:anonymous": "true", 50 | "of:image": imageUrl, 51 | "of:post_url": postUrl, 52 | "of:input:text": "Enter EIP/ERC No", 53 | "of:button:1": "Search 🔎", 54 | "of:button:2": `📙 ${validEIPData.isERC ? "ERC" : "EIP"}-${eipNo}`, 55 | "of:button:2:action": "link", 56 | "of:button:2:target": `${process.env["HOST"]}/eip/${eipNo}`, 57 | }, 58 | }; 59 | } 60 | 61 | export default function EIPLayout({ children }: { children: React.ReactNode }) { 62 | return {children}; 63 | } 64 | -------------------------------------------------------------------------------- /app/caip/[eipOrNo]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import NLink from "next/link"; 4 | import { useCallback, useEffect, useState } from "react"; 5 | import { Markdown } from "@/components/Markdown"; 6 | import { 7 | Container, 8 | Heading, 9 | Center, 10 | Text, 11 | Table, 12 | Tr, 13 | Td, 14 | Th, 15 | Link, 16 | HStack, 17 | Badge, 18 | Tooltip, 19 | Box, 20 | Button, 21 | Spacer, 22 | Skeleton, 23 | SkeletonText, 24 | useDisclosure, 25 | Collapse, 26 | IconButton, 27 | } from "@chakra-ui/react"; 28 | import { 29 | ChevronLeftIcon, 30 | ChevronRightIcon, 31 | ChevronUpIcon, 32 | ChevronDownIcon, 33 | } from "@chakra-ui/icons"; 34 | import { FaBookmark, FaRegBookmark } from "react-icons/fa"; 35 | import { useLocalStorage } from "usehooks-ts"; 36 | import Typewriter from "typewriter-effect"; 37 | import { 38 | EIPStatus, 39 | convertMetadataToJson, 40 | extractEipNumber, 41 | extractMetadata, 42 | } from "@/utils"; 43 | import { EIPType } from "@/types"; 44 | import { validCAIPs, validCAIPsArray } from "@/data/validCAIPs"; 45 | import { EipMetadataJson } from "@/types"; 46 | import { useTopLoaderRouter } from "@/hooks/useTopLoaderRouter"; 47 | import { ScrollToTopButton } from "@/components/ScrollToTopButton"; 48 | 49 | const CAIP = ({ 50 | params: { eipOrNo }, 51 | }: { 52 | params: { 53 | eipOrNo: string; // can be of the form `1234`, `eip-1234` or `eip-1234.md` (standard followed by official EIP) 54 | }; 55 | }) => { 56 | const router = useTopLoaderRouter(); 57 | 58 | const eipNo = extractEipNumber(eipOrNo, "caip"); 59 | 60 | const [markdownFileURL, setMarkdownFileURL] = useState(""); 61 | const [metadataJson, setMetadataJson] = useState(); 62 | const [markdown, setMarkdown] = useState(""); 63 | const [isERC, setIsERC] = useState(true); 64 | 65 | const [bookmarks, setBookmarks] = useLocalStorage< 66 | { eipNo: string; title: string; type?: EIPType; status?: string }[] 67 | >("eip-bookmarks", []); 68 | const [isBookmarked, setIsBookmarked] = useState(false); 69 | 70 | const [aiSummary, setAiSummary] = useState(""); 71 | 72 | const currentEIPArrayIndex = validCAIPsArray.indexOf(eipNo); 73 | 74 | const { 75 | isOpen: aiSummaryIsOpen, 76 | onOpen: aiSummaryOnOpen, 77 | onToggle: aiSummaryOnToggle, 78 | } = useDisclosure(); 79 | 80 | const handlePrevEIP = () => { 81 | if (currentEIPArrayIndex > 0) { 82 | setMetadataJson(undefined); 83 | router.push(`/caip/${validCAIPsArray[currentEIPArrayIndex - 1]}`); 84 | } 85 | }; 86 | 87 | const handleNextEIP = () => { 88 | if (currentEIPArrayIndex < validCAIPsArray.length - 1) { 89 | setMetadataJson(undefined); 90 | router.push(`/caip/${validCAIPsArray[currentEIPArrayIndex + 1]}`); 91 | } 92 | }; 93 | 94 | const fetchEIPData = useCallback(async () => { 95 | const validEIPData = validCAIPs[parseInt(eipNo)]; 96 | let _isERC = true; 97 | 98 | let _markdownFileURL = ""; 99 | let eipMarkdownRes = ""; 100 | 101 | if (validEIPData) { 102 | _markdownFileURL = validEIPData.markdownPath; 103 | eipMarkdownRes = await fetch(_markdownFileURL).then((response) => 104 | response.text() 105 | ); 106 | } else { 107 | _markdownFileURL = `https://raw.githubusercontent.com/ChainAgnostic/CAIPs/main/CAIPs/caip-${eipNo}.md`; 108 | eipMarkdownRes = await fetch(_markdownFileURL).then((response) => 109 | response.text() 110 | ); 111 | } 112 | setMarkdownFileURL(_markdownFileURL); 113 | 114 | const { metadata, markdown: _markdown } = extractMetadata(eipMarkdownRes); 115 | setMetadataJson(convertMetadataToJson(metadata)); 116 | setMarkdown(_markdown); 117 | setIsERC(_isERC); 118 | 119 | // only add to trending if it's a valid EIP 120 | if ( 121 | eipMarkdownRes !== "404: Not Found" && 122 | process.env.NEXT_PUBLIC_DEVELOPMENT !== "true" 123 | ) { 124 | fetch("/api/logPageVisit", { 125 | method: "POST", 126 | body: JSON.stringify({ eipNo, type: "CAIP" }), 127 | headers: { 128 | "Content-Type": "application/json", 129 | }, 130 | }); 131 | } 132 | }, [eipNo]); 133 | 134 | const fetchAISummary = useCallback(async () => { 135 | fetch("/api/aiSummary", { 136 | method: "POST", 137 | body: JSON.stringify({ eipNo, type: "CAIP" }), 138 | headers: { 139 | "Content-Type": "application/json", 140 | }, 141 | }).then((response) => { 142 | response.json().then((data) => { 143 | setAiSummary(data); 144 | }); 145 | }); 146 | }, [eipNo]); 147 | 148 | useEffect(() => { 149 | fetchEIPData(); 150 | }, [eipNo, fetchEIPData]); 151 | 152 | // Fetch AI Summary when clicked 153 | useEffect(() => { 154 | if (aiSummaryIsOpen && !aiSummary) { 155 | fetchAISummary(); 156 | } 157 | }, [aiSummaryIsOpen, aiSummary]); 158 | 159 | useEffect(() => { 160 | setIsBookmarked(bookmarks.some((item) => item.eipNo === eipNo)); 161 | }, [bookmarks, eipNo]); 162 | 163 | const toggleBookmark = () => { 164 | if (isBookmarked) { 165 | const updatedBookmarks = bookmarks.filter( 166 | (item: any) => item.eipNo !== eipNo 167 | ); 168 | setBookmarks(updatedBookmarks); 169 | } else { 170 | const newBookmark = { 171 | eipNo, 172 | title: metadataJson?.title || "", 173 | type: EIPType.CAIP, 174 | status: metadataJson?.status || "", 175 | }; 176 | setBookmarks([...bookmarks, newBookmark]); 177 | } 178 | setIsBookmarked(!isBookmarked); 179 | }; 180 | 181 | return ( 182 |
183 | {!metadataJson && ( 184 | <> 185 | 195 | {currentEIPArrayIndex > 0 && ( 196 | 197 | 200 | 201 | )} 202 | 203 | {currentEIPArrayIndex < validCAIPsArray.length - 1 && ( 204 | 205 | 208 | 209 | )} 210 | 211 | 220 | 221 | 222 | 223 | Draft 224 | 225 | 226 | 227 | 228 | Standards Track: ERC 229 | 230 | 231 | 232 | 233 | TITLE 234 | 235 | 236 | some description about the EIP 237 | 238 | 239 | 240 | )} 241 | {metadataJson && ( 242 | 251 | {/* Navigation Arrows */} 252 | 253 | {currentEIPArrayIndex > 0 && ( 254 | 255 | 258 | 259 | )} 260 | 261 | {currentEIPArrayIndex < validCAIPsArray.length - 1 && ( 262 | 263 | 266 | 267 | )} 268 | 269 | {/* AI Summary */} 270 | 286 | 287 | 💡 EIP-GPT: 288 | 289 | 290 | {aiSummaryIsOpen ? : } 291 | 292 | 293 | 294 | {aiSummary ? ( 295 | 296 | { 298 | typewriter.typeString(`${aiSummary}`).start(); 299 | }} 300 | options={{ 301 | delay: 5, 302 | }} 303 | /> 304 | 305 | ) : ( 306 | 307 | )} 308 | 309 | 310 | {/* Metadata Badges */} 311 | 312 | 313 | 319 | {EIPStatus[metadataJson.status]?.prefix} {metadataJson.status} 320 | 321 | 322 | 323 | {metadataJson.type}: {metadataJson.category} 324 | 325 | 347 | 348 | 349 | 350 | CAIP-{eipNo}: {metadataJson.title} 351 | 352 | {metadataJson.description} 353 | 354 | 355 | {metadataJson.author && ( 356 | 357 | 358 | 380 | 381 | )} 382 | {metadataJson.created && ( 383 | 384 | 385 | 386 | 387 | )} 388 | {metadataJson["discussions-to"] && ( 389 | 390 | 391 | 400 | 401 | )} 402 | {metadataJson.requires && metadataJson.requires.length > 0 && ( 403 | 404 | 405 | 419 | 420 | )} 421 |
Authors 359 | 377 | {metadataJson.author.join(", ")} 378 | 379 |
Created{metadataJson.created}
Discussion Link 392 | 397 | {metadataJson["discussions-to"]} 398 | 399 |
Requires 406 | 407 | {metadataJson.requires.map((req, i) => ( 408 | 409 | 413 | CAIP-{req} 414 | 415 | 416 | ))} 417 | 418 |
422 |
423 | {markdown === "404: Not Found" ? ( 424 |
{markdown}
425 | ) : ( 426 | 427 | )} 428 |
429 | )} 430 | 431 |
432 | ); 433 | }; 434 | 435 | export default CAIP; 436 | -------------------------------------------------------------------------------- /app/eip/[eipOrNo]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from "@/components/Layout"; 2 | import { 3 | convertMetadataToJson, 4 | extractEipNumber, 5 | extractMetadata, 6 | getMetadata, 7 | } from "@/utils"; 8 | import { validEIPs } from "@/data/validEIPs"; 9 | 10 | export async function generateMetadata({ 11 | params: { eipOrNo }, 12 | }: { 13 | params: { eipOrNo: string }; 14 | }) { 15 | const eipNo = extractEipNumber(eipOrNo, "eip"); 16 | const validEIPData = validEIPs[parseInt(eipNo)]; 17 | 18 | if (!validEIPData) { 19 | return; 20 | } 21 | 22 | const eipMarkdownRes = await fetch(validEIPData.markdownPath).then( 23 | (response) => response.text() 24 | ); 25 | const { metadata } = extractMetadata(eipMarkdownRes); 26 | const metadataJson = convertMetadataToJson(metadata); 27 | 28 | const imageUrl = `${process.env["HOST"]}/api/og?eipNo=${eipNo}`; 29 | const postUrl = `${process.env["HOST"]}/api/frame/home`; 30 | 31 | const generated = getMetadata({ 32 | title: `${validEIPData.isERC ? "ERC" : "EIP"}-${eipNo}: ${ 33 | validEIPData.title 34 | } | EIP.tools`, 35 | description: metadataJson.description, 36 | images: imageUrl, 37 | }); 38 | 39 | return { 40 | ...generated, 41 | other: { 42 | "fc:frame": "vNext", 43 | "fc:frame:image": imageUrl, 44 | "fc:frame:post_url": postUrl, 45 | "fc:frame:input:text": "Enter EIP/ERC No", 46 | "fc:frame:button:1": "Search 🔎", 47 | "fc:frame:button:2": `📙 ${validEIPData.isERC ? "ERC" : "EIP"}-${eipNo}`, 48 | "fc:frame:button:2:action": "link", 49 | "fc:frame:button:2:target": `${process.env["HOST"]}/eip/${eipNo}`, 50 | "of:version": "vNext", 51 | "of:accepts:anonymous": "true", 52 | "of:image": imageUrl, 53 | "of:post_url": postUrl, 54 | "of:input:text": "Enter EIP/ERC No", 55 | "of:button:1": "Search 🔎", 56 | "of:button:2": `📙 ${validEIPData.isERC ? "ERC" : "EIP"}-${eipNo}`, 57 | "of:button:2:action": "link", 58 | "of:button:2:target": `${process.env["HOST"]}/eip/${eipNo}`, 59 | }, 60 | }; 61 | } 62 | 63 | export default function EIPLayout({ children }: { children: React.ReactNode }) { 64 | return {children}; 65 | } 66 | -------------------------------------------------------------------------------- /app/eip/[eipOrNo]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import NLink from "next/link"; 4 | import { useCallback, useEffect, useState } from "react"; 5 | import { Markdown } from "@/components/Markdown"; 6 | import { 7 | Container, 8 | Heading, 9 | Center, 10 | Text, 11 | Table, 12 | Tr, 13 | Td, 14 | Th, 15 | Link, 16 | HStack, 17 | Badge, 18 | Tooltip, 19 | Box, 20 | Button, 21 | Spacer, 22 | Skeleton, 23 | SkeletonText, 24 | useDisclosure, 25 | Collapse, 26 | IconButton, 27 | } from "@chakra-ui/react"; 28 | import { 29 | ChevronLeftIcon, 30 | ChevronRightIcon, 31 | ChevronUpIcon, 32 | ChevronDownIcon, 33 | } from "@chakra-ui/icons"; 34 | import { FaBookmark, FaRegBookmark } from "react-icons/fa"; 35 | import { useLocalStorage } from "usehooks-ts"; 36 | import Typewriter from "typewriter-effect"; 37 | import { 38 | EIPStatus, 39 | convertMetadataToJson, 40 | extractEipNumber, 41 | extractMetadata, 42 | } from "@/utils"; 43 | import { EIPType } from "@/types"; 44 | import { validEIPs, validEIPsArray } from "@/data/validEIPs"; 45 | import { EipMetadataJson } from "@/types"; 46 | import { useTopLoaderRouter } from "@/hooks/useTopLoaderRouter"; 47 | import { ScrollToTopButton } from "@/components/ScrollToTopButton"; 48 | import { CopyToClipboard } from "@/components/CopyToClipboard"; 49 | 50 | const EIP = ({ 51 | params: { eipOrNo }, 52 | }: { 53 | params: { 54 | eipOrNo: string; // can be of the form `1234`, `eip-1234` or `eip-1234.md` (standard followed by official EIP) 55 | }; 56 | }) => { 57 | const router = useTopLoaderRouter(); 58 | 59 | const eipNo = extractEipNumber(eipOrNo, "eip"); 60 | 61 | const [markdownFileURL, setMarkdownFileURL] = useState(""); 62 | const [metadataJson, setMetadataJson] = useState(); 63 | const [markdown, setMarkdown] = useState(""); 64 | const [isERC, setIsERC] = useState(true); 65 | 66 | const [bookmarks, setBookmarks] = useLocalStorage< 67 | { eipNo: string; title: string; type?: EIPType; status?: string }[] 68 | >("eip-bookmarks", []); 69 | const [isBookmarked, setIsBookmarked] = useState(false); 70 | 71 | const [aiSummary, setAiSummary] = useState(""); 72 | 73 | const currentEIPArrayIndex = validEIPsArray.indexOf(eipNo); 74 | 75 | const { 76 | isOpen: aiSummaryIsOpen, 77 | onOpen: aiSummaryOnOpen, 78 | onToggle: aiSummaryOnToggle, 79 | } = useDisclosure(); 80 | 81 | const handlePrevEIP = () => { 82 | if (currentEIPArrayIndex > 0) { 83 | setMetadataJson(undefined); 84 | router.push(`/eip/${validEIPsArray[currentEIPArrayIndex - 1]}`); 85 | } 86 | }; 87 | 88 | const handleNextEIP = () => { 89 | if (currentEIPArrayIndex < validEIPsArray.length - 1) { 90 | setMetadataJson(undefined); 91 | router.push(`/eip/${validEIPsArray[currentEIPArrayIndex + 1]}`); 92 | } 93 | }; 94 | 95 | const fetchEIPData = useCallback(async () => { 96 | const validEIPData = validEIPs[parseInt(eipNo)]; 97 | let _isERC = true; 98 | 99 | let _markdownFileURL = ""; 100 | let eipMarkdownRes = ""; 101 | 102 | if (validEIPData) { 103 | _markdownFileURL = validEIPData.markdownPath; 104 | eipMarkdownRes = await fetch(_markdownFileURL).then((response) => 105 | response.text() 106 | ); 107 | _isERC = validEIPData.isERC ?? false; 108 | } else { 109 | _markdownFileURL = `https://raw.githubusercontent.com/ethereum/ERCs/master/ERCS/erc-${eipNo}.md`; 110 | eipMarkdownRes = await fetch(_markdownFileURL).then((response) => 111 | response.text() 112 | ); 113 | 114 | if (eipMarkdownRes === "404: Not Found") { 115 | _markdownFileURL = `https://raw.githubusercontent.com/ethereum/EIPs/master/EIPS/eip-${eipNo}.md`; 116 | eipMarkdownRes = await fetch(_markdownFileURL).then((response) => 117 | response.text() 118 | ); 119 | _isERC = false; 120 | } 121 | } 122 | setMarkdownFileURL(_markdownFileURL); 123 | 124 | const { metadata, markdown: _markdown } = extractMetadata(eipMarkdownRes); 125 | setMetadataJson(convertMetadataToJson(metadata)); 126 | setMarkdown(_markdown); 127 | setIsERC(_isERC); 128 | 129 | // only add to trending if it's a valid EIP 130 | if ( 131 | eipMarkdownRes !== "404: Not Found" && 132 | process.env.NEXT_PUBLIC_DEVELOPMENT !== "true" 133 | ) { 134 | fetch("/api/logPageVisit", { 135 | method: "POST", 136 | body: JSON.stringify({ eipNo, type: "EIP" }), 137 | headers: { 138 | "Content-Type": "application/json", 139 | }, 140 | }); 141 | } 142 | }, [eipNo]); 143 | 144 | const fetchAISummary = useCallback(async () => { 145 | fetch("/api/aiSummary", { 146 | method: "POST", 147 | body: JSON.stringify({ eipNo }), 148 | headers: { 149 | "Content-Type": "application/json", 150 | }, 151 | }).then((response) => { 152 | response.json().then((data) => { 153 | setAiSummary(data); 154 | }); 155 | }); 156 | }, [eipNo]); 157 | 158 | useEffect(() => { 159 | fetchEIPData(); 160 | }, [eipNo, fetchEIPData]); 161 | 162 | // Fetch AI Summary when clicked 163 | useEffect(() => { 164 | if (aiSummaryIsOpen && !aiSummary) { 165 | fetchAISummary(); 166 | } 167 | }, [aiSummaryIsOpen, aiSummary]); 168 | 169 | useEffect(() => { 170 | setIsBookmarked(bookmarks.some((item) => item.eipNo === eipNo)); 171 | }, [bookmarks, eipNo]); 172 | 173 | const toggleBookmark = () => { 174 | if (isBookmarked) { 175 | const updatedBookmarks = bookmarks.filter( 176 | (item: any) => item.eipNo !== eipNo 177 | ); 178 | setBookmarks(updatedBookmarks); 179 | } else { 180 | const newBookmark = { 181 | eipNo, 182 | title: metadataJson?.title || "", 183 | status: metadataJson?.status || "", 184 | }; 185 | setBookmarks([...bookmarks, newBookmark]); 186 | } 187 | setIsBookmarked(!isBookmarked); 188 | }; 189 | 190 | return ( 191 |
192 | {!metadataJson && ( 193 | <> 194 | 204 | {currentEIPArrayIndex > 0 && ( 205 | 206 | 209 | 210 | )} 211 | 212 | {currentEIPArrayIndex < validEIPsArray.length - 1 && ( 213 | 214 | 217 | 218 | )} 219 | 220 | 229 | 230 | 231 | 232 | Draft 233 | 234 | 235 | 236 | 237 | Standards Track: ERC 238 | 239 | 240 | 241 | 242 | TITLE 243 | 244 | 245 | some description about the EIP 246 | 247 | 248 | 249 | )} 250 | {metadataJson && ( 251 | 260 | {/* Navigation Arrows */} 261 | 262 | {currentEIPArrayIndex > 0 && ( 263 | 264 | 267 | 268 | )} 269 | 270 | {currentEIPArrayIndex < validEIPsArray.length - 1 && ( 271 | 272 | 275 | 276 | )} 277 | 278 | {/* AI Summary */} 279 | 295 | 296 | 💡 EIP-GPT: 297 | 298 | 299 | {aiSummaryIsOpen ? : } 300 | 301 | 302 | 303 | {aiSummary ? ( 304 | 305 | { 307 | typewriter.typeString(`${aiSummary}`).start(); 308 | }} 309 | options={{ 310 | delay: 5, 311 | }} 312 | /> 313 | 314 | ) : ( 315 | 316 | )} 317 | 318 | 319 | {/* Metadata Badges */} 320 | 321 | 322 | 328 | {EIPStatus[metadataJson.status]?.prefix} {metadataJson.status} 329 | 330 | 331 | 332 | {metadataJson.type}: {metadataJson.category} 333 | 334 | 356 | 357 | 358 | {isERC ? "ERC" : "EIP"}-{eipNo}: {metadataJson.title} 359 | 360 | {metadataJson.description} 361 | 362 | 363 | {metadataJson.author && ( 364 | 365 | 366 | 388 | 389 | )} 390 | {metadataJson.created && ( 391 | 392 | 393 | 394 | 395 | )} 396 | {metadataJson["discussions-to"] && ( 397 | 398 | 399 | 408 | 409 | )} 410 | {metadataJson.requires && metadataJson.requires.length > 0 && ( 411 | 412 | 413 | 427 | 428 | )} 429 | {markdownFileURL && ( 430 | 431 | 441 | 454 | 455 | )} 456 |
Authors 367 | 385 | {metadataJson.author.join(", ")} 386 | 387 |
Created{metadataJson.created}
Discussion Link 400 | 405 | {metadataJson["discussions-to"]} 406 | 407 |
Requires 414 | 415 | {metadataJson.requires.map((req, i) => ( 416 | 417 | 421 | {validEIPs[req].isERC ? "ERC" : "EIP"}-{req} 422 | 423 | 424 | ))} 425 | 426 |
432 | 433 | Markdown 434 | 439 | 440 | 442 | 443 | 448 | {markdownFileURL.length > 50 449 | ? `${markdownFileURL.substring(0, 50)}...` 450 | : markdownFileURL} 451 | 452 | 453 |
457 |
458 | {markdown === "404: Not Found" ? ( 459 |
{markdown}
460 | ) : ( 461 | 462 | )} 463 |
464 | )} 465 | 466 |
467 | ); 468 | }; 469 | 470 | export default EIP; 471 | -------------------------------------------------------------------------------- /app/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Poppins } from "next/font/google"; 2 | 3 | export const poppins = Poppins({ 4 | weight: ["200", "400", "700"], 5 | subsets: ["latin"], 6 | }); 7 | -------------------------------------------------------------------------------- /app/graph/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getMetadata } from "@/utils"; 2 | 3 | export const metadata = getMetadata({ 4 | title: "EIP Dependency Graph | EIP.Tools", 5 | description: 6 | "Visualize dependecies between EIPs & ERCs with this interactive graph.", 7 | images: "https://eip.tools/og/graph.png", 8 | }); 9 | 10 | const EIPGraphLayout = ({ children }: { children: React.ReactNode }) => { 11 | return <>{children}; 12 | }; 13 | 14 | export default EIPGraphLayout; 15 | -------------------------------------------------------------------------------- /app/graph/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import dynamic from "next/dynamic"; 4 | 5 | const EIPGraph = dynamic(() => import("@/components/EIPGraph"), { 6 | ssr: false, 7 | }); 8 | 9 | export default function EIPGraphPage() { 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EIPTools/eip-tools/28ddf9430fd67df2626bb27f4df08daf01a0310f/app/icon.png -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { poppins } from "./fonts"; 2 | import { Providers } from "./providers"; 3 | import { Analytics } from "@/components/Analytics"; 4 | 5 | export default function RootLayout({ 6 | children, 7 | }: Readonly<{ 8 | children: React.ReactNode; 9 | }>) { 10 | return ( 11 | 12 | 13 | 14 | {children} 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/lib/neynar.ts: -------------------------------------------------------------------------------- 1 | import { NeynarAPIClient, Configuration } from "@neynar/nodejs-sdk"; 2 | 3 | if (!process.env.NEYNAR_API_KEY) { 4 | throw new Error("Make sure you set NEYNAR_API_KEY in your .env file"); 5 | } 6 | 7 | const config = new Configuration({ 8 | apiKey: process.env.NEYNAR_API_KEY, 9 | }); 10 | 11 | const neynarClient = new NeynarAPIClient(config); 12 | 13 | export default neynarClient; 14 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { getMetadata } from "@/utils"; 3 | import { EIPOfTheDay } from "@/components/EIPOfTheDay"; 4 | import { Layout } from "@/components/Layout"; 5 | import { TrendingEIPs } from "@/components/TrendingEIPs"; 6 | import { PectraEIPs } from "@/components/PectraEIPs"; 7 | import { EIPGraphSection } from "@/components/EIPGraphSection"; 8 | 9 | export async function generateMetadata(): Promise { 10 | const imageUrl = `${process.env["HOST"]}/og/index.png?date=${Date.now()}`; 11 | const postUrl = `${process.env["HOST"]}/api/frame/home`; 12 | 13 | const metadata = getMetadata({ 14 | title: "EIP.tools", 15 | description: "Explore all EIPs, ERCs, RIPs and CAIPs easily!", 16 | images: imageUrl, 17 | }); 18 | 19 | return { 20 | ...metadata, 21 | other: { 22 | "fc:frame": "vNext", 23 | "fc:frame:image": imageUrl, 24 | "fc:frame:post_url": postUrl, 25 | "fc:frame:input:text": "Enter EIP/ERC No", 26 | "fc:frame:button:1": "Search 🔎", 27 | "of:version": "vNext", 28 | "of:accepts:anonymous": "true", 29 | "of:image": imageUrl, 30 | "of:post_url": postUrl, 31 | "of:input:text": "Enter EIP/ERC No", 32 | "of:button:1": "Search 🔎", 33 | }, 34 | }; 35 | } 36 | 37 | export default function Home() { 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ChakraProvider } from "@chakra-ui/react"; 4 | import { CacheProvider } from "@chakra-ui/next-js"; 5 | import NextTopLoader from "nextjs-toploader"; 6 | import theme from "@/style/theme"; 7 | import "@fortawesome/fontawesome-svg-core/styles.css"; 8 | 9 | export const Providers = ({ children }: { children: React.ReactNode }) => { 10 | return ( 11 | 12 | 13 | 14 | {children} 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /app/rip/[eipOrNo]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from "@/components/Layout"; 2 | import { 3 | convertMetadataToJson, 4 | extractEipNumber, 5 | extractMetadata, 6 | getMetadata, 7 | } from "@/utils"; 8 | import { validRIPs } from "@/data/validRIPs"; 9 | 10 | export async function generateMetadata({ 11 | params: { eipOrNo }, 12 | }: { 13 | params: { eipOrNo: string }; 14 | }) { 15 | const eipNo = extractEipNumber(eipOrNo, "rip"); 16 | const validEIPData = validRIPs[parseInt(eipNo)]; 17 | 18 | if (!validEIPData) { 19 | return; 20 | } 21 | 22 | const eipMarkdownRes = await fetch(validEIPData.markdownPath).then( 23 | (response) => response.text() 24 | ); 25 | const { metadata } = extractMetadata(eipMarkdownRes); 26 | const metadataJson = convertMetadataToJson(metadata); 27 | 28 | const imageUrl = `${process.env["HOST"]}/api/og?eipNo=${eipNo}&type=RIP`; 29 | const postUrl = `${process.env["HOST"]}/api/frame/home`; 30 | 31 | const generated = getMetadata({ 32 | title: `RIP-${eipNo}: ${validEIPData.title} | EIP.tools`, 33 | description: metadataJson.description, 34 | images: imageUrl, 35 | }); 36 | 37 | return { 38 | ...generated, 39 | other: { 40 | "fc:frame": "vNext", 41 | "fc:frame:image": imageUrl, 42 | "fc:frame:post_url": postUrl, 43 | "fc:frame:input:text": "Enter EIP/ERC No", 44 | "fc:frame:button:1": "Search 🔎", 45 | "fc:frame:button:2": `📙 ${validEIPData.isERC ? "ERC" : "EIP"}-${eipNo}`, 46 | "fc:frame:button:2:action": "link", 47 | "fc:frame:button:2:target": `${process.env["HOST"]}/eip/${eipNo}`, 48 | "of:version": "vNext", 49 | "of:accepts:anonymous": "true", 50 | "of:image": imageUrl, 51 | "of:post_url": postUrl, 52 | "of:input:text": "Enter EIP/ERC No", 53 | "of:button:1": "Search 🔎", 54 | "of:button:2": `📙 ${validEIPData.isERC ? "ERC" : "EIP"}-${eipNo}`, 55 | "of:button:2:action": "link", 56 | "of:button:2:target": `${process.env["HOST"]}/eip/${eipNo}`, 57 | }, 58 | }; 59 | } 60 | 61 | export default function EIPLayout({ children }: { children: React.ReactNode }) { 62 | return {children}; 63 | } 64 | -------------------------------------------------------------------------------- /app/rip/[eipOrNo]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import NLink from "next/link"; 4 | import { useCallback, useEffect, useState } from "react"; 5 | import { Markdown } from "@/components/Markdown"; 6 | import { 7 | Container, 8 | Heading, 9 | Center, 10 | Text, 11 | Table, 12 | Tr, 13 | Td, 14 | Th, 15 | Link, 16 | HStack, 17 | Badge, 18 | Tooltip, 19 | Box, 20 | Button, 21 | Spacer, 22 | Skeleton, 23 | SkeletonText, 24 | useDisclosure, 25 | Collapse, 26 | IconButton, 27 | } from "@chakra-ui/react"; 28 | import { 29 | ChevronLeftIcon, 30 | ChevronRightIcon, 31 | ChevronUpIcon, 32 | ChevronDownIcon, 33 | } from "@chakra-ui/icons"; 34 | import { FaBookmark, FaRegBookmark } from "react-icons/fa"; 35 | import { useLocalStorage } from "usehooks-ts"; 36 | import Typewriter from "typewriter-effect"; 37 | import { 38 | EIPStatus, 39 | convertMetadataToJson, 40 | extractEipNumber, 41 | extractMetadata, 42 | } from "@/utils"; 43 | import { EIPType } from "@/types"; 44 | import { validRIPs, validRIPsArray } from "@/data/validRIPs"; 45 | import { EipMetadataJson } from "@/types"; 46 | import { useTopLoaderRouter } from "@/hooks/useTopLoaderRouter"; 47 | import { ScrollToTopButton } from "@/components/ScrollToTopButton"; 48 | 49 | const RIP = ({ 50 | params: { eipOrNo }, 51 | }: { 52 | params: { 53 | eipOrNo: string; // can be of the form `1234`, `eip-1234` or `eip-1234.md` (standard followed by official EIP) 54 | }; 55 | }) => { 56 | const router = useTopLoaderRouter(); 57 | 58 | const eipNo = extractEipNumber(eipOrNo, "rip"); 59 | 60 | const [markdownFileURL, setMarkdownFileURL] = useState(""); 61 | const [metadataJson, setMetadataJson] = useState(); 62 | const [markdown, setMarkdown] = useState(""); 63 | const [isERC, setIsERC] = useState(true); 64 | 65 | const [bookmarks, setBookmarks] = useLocalStorage< 66 | { eipNo: string; title: string; type?: EIPType; status?: string }[] 67 | >("eip-bookmarks", []); 68 | const [isBookmarked, setIsBookmarked] = useState(false); 69 | 70 | const [aiSummary, setAiSummary] = useState(""); 71 | 72 | const currentEIPArrayIndex = validRIPsArray.indexOf(eipNo); 73 | 74 | const { 75 | isOpen: aiSummaryIsOpen, 76 | onOpen: aiSummaryOnOpen, 77 | onToggle: aiSummaryOnToggle, 78 | } = useDisclosure(); 79 | 80 | const handlePrevEIP = () => { 81 | if (currentEIPArrayIndex > 0) { 82 | setMetadataJson(undefined); 83 | router.push(`/rip/${validRIPsArray[currentEIPArrayIndex - 1]}`); 84 | } 85 | }; 86 | 87 | const handleNextEIP = () => { 88 | if (currentEIPArrayIndex < validRIPsArray.length - 1) { 89 | setMetadataJson(undefined); 90 | router.push(`/rip/${validRIPsArray[currentEIPArrayIndex + 1]}`); 91 | } 92 | }; 93 | 94 | const fetchEIPData = useCallback(async () => { 95 | const validEIPData = validRIPs[parseInt(eipNo)]; 96 | let _isERC = true; 97 | 98 | let _markdownFileURL = ""; 99 | let eipMarkdownRes = ""; 100 | 101 | if (validEIPData) { 102 | _markdownFileURL = validEIPData.markdownPath; 103 | eipMarkdownRes = await fetch(_markdownFileURL).then((response) => 104 | response.text() 105 | ); 106 | } else { 107 | _markdownFileURL = `https://raw.githubusercontent.com/ethereum/RIPs/master/RIPS/rip-${eipNo}.md`; 108 | eipMarkdownRes = await fetch(_markdownFileURL).then((response) => 109 | response.text() 110 | ); 111 | } 112 | setMarkdownFileURL(_markdownFileURL); 113 | 114 | const { metadata, markdown: _markdown } = extractMetadata(eipMarkdownRes); 115 | setMetadataJson(convertMetadataToJson(metadata)); 116 | setMarkdown(_markdown); 117 | setIsERC(_isERC); 118 | 119 | // only add to trending if it's a valid EIP 120 | if ( 121 | eipMarkdownRes !== "404: Not Found" && 122 | process.env.NEXT_PUBLIC_DEVELOPMENT !== "true" 123 | ) { 124 | fetch("/api/logPageVisit", { 125 | method: "POST", 126 | body: JSON.stringify({ eipNo, type: "RIP" }), 127 | headers: { 128 | "Content-Type": "application/json", 129 | }, 130 | }); 131 | } 132 | }, [eipNo]); 133 | 134 | const fetchAISummary = useCallback(async () => { 135 | fetch("/api/aiSummary", { 136 | method: "POST", 137 | body: JSON.stringify({ eipNo, type: "RIP" }), 138 | headers: { 139 | "Content-Type": "application/json", 140 | }, 141 | }).then((response) => { 142 | response.json().then((data) => { 143 | setAiSummary(data); 144 | }); 145 | }); 146 | }, [eipNo]); 147 | 148 | useEffect(() => { 149 | fetchEIPData(); 150 | }, [eipNo, fetchEIPData]); 151 | 152 | // Fetch AI Summary when clicked 153 | useEffect(() => { 154 | if (aiSummaryIsOpen && !aiSummary) { 155 | fetchAISummary(); 156 | } 157 | }, [aiSummaryIsOpen, aiSummary]); 158 | 159 | useEffect(() => { 160 | setIsBookmarked(bookmarks.some((item) => item.eipNo === eipNo)); 161 | }, [bookmarks, eipNo]); 162 | 163 | const toggleBookmark = () => { 164 | if (isBookmarked) { 165 | const updatedBookmarks = bookmarks.filter( 166 | (item: any) => item.eipNo !== eipNo 167 | ); 168 | setBookmarks(updatedBookmarks); 169 | } else { 170 | const newBookmark = { 171 | eipNo, 172 | title: metadataJson?.title || "", 173 | type: EIPType.RIP, 174 | status: metadataJson?.status || "", 175 | }; 176 | setBookmarks([...bookmarks, newBookmark]); 177 | } 178 | setIsBookmarked(!isBookmarked); 179 | }; 180 | 181 | return ( 182 |
183 | {!metadataJson && ( 184 | <> 185 | 195 | {currentEIPArrayIndex > 0 && ( 196 | 197 | 200 | 201 | )} 202 | 203 | {currentEIPArrayIndex < validRIPsArray.length - 1 && ( 204 | 205 | 208 | 209 | )} 210 | 211 | 220 | 221 | 222 | 223 | Draft 224 | 225 | 226 | 227 | 228 | Standards Track: ERC 229 | 230 | 231 | 232 | 233 | TITLE 234 | 235 | 236 | some description about the EIP 237 | 238 | 239 | 240 | )} 241 | {metadataJson && ( 242 | 251 | {/* Navigation Arrows */} 252 | 253 | {currentEIPArrayIndex > 0 && ( 254 | 255 | 258 | 259 | )} 260 | 261 | {currentEIPArrayIndex < validRIPsArray.length - 1 && ( 262 | 263 | 266 | 267 | )} 268 | 269 | {/* AI Summary */} 270 | 286 | 287 | 💡 EIP-GPT: 288 | 289 | 290 | {aiSummaryIsOpen ? : } 291 | 292 | 293 | 294 | {aiSummary ? ( 295 | 296 | { 298 | typewriter.typeString(`${aiSummary}`).start(); 299 | }} 300 | options={{ 301 | delay: 5, 302 | }} 303 | /> 304 | 305 | ) : ( 306 | 307 | )} 308 | 309 | 310 | {/* Metadata Badges */} 311 | 312 | 313 | 319 | {EIPStatus[metadataJson.status]?.prefix} {metadataJson.status} 320 | 321 | 322 | 323 | {metadataJson.type}: {metadataJson.category} 324 | 325 | 347 | 348 | 349 | RIP-{eipNo}: {metadataJson.title} 350 | 351 | {metadataJson.description} 352 | 353 | 354 | {metadataJson.author && ( 355 | 356 | 357 | 379 | 380 | )} 381 | {metadataJson.created && ( 382 | 383 | 384 | 385 | 386 | )} 387 | {metadataJson["discussions-to"] && ( 388 | 389 | 390 | 399 | 400 | )} 401 | {metadataJson.requires && metadataJson.requires.length > 0 && ( 402 | 403 | 404 | 418 | 419 | )} 420 |
Authors 358 | 376 | {metadataJson.author.join(", ")} 377 | 378 |
Created{metadataJson.created}
Discussion Link 391 | 396 | {metadataJson["discussions-to"]} 397 | 398 |
Requires 405 | 406 | {metadataJson.requires.map((req, i) => ( 407 | 408 | 412 | RIP-{req} 413 | 414 | 415 | ))} 416 | 417 |
421 |
422 | {markdown === "404: Not Found" ? ( 423 |
{markdown}
424 | ) : ( 425 | 426 | )} 427 |
428 | )} 429 | 430 |
431 | ); 432 | }; 433 | 434 | export default RIP; 435 | -------------------------------------------------------------------------------- /app/shared/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import { getMetadata } from "@/utils"; 3 | import { Layout } from "@/components/Layout"; 4 | 5 | export async function generateMetadata(): Promise { 6 | const imageUrl = `${process.env["HOST"]}/og/index.png?date=${Date.now()}`; 7 | const postUrl = `${process.env["HOST"]}/api/frame/home`; 8 | 9 | const metadata = getMetadata({ 10 | title: "Shared Bookmarks - EIP.tools", 11 | description: "Explore the EIPs, ERCs, RIPs and CAIPs shared with you!", 12 | images: imageUrl, 13 | }); 14 | 15 | return { 16 | ...metadata, 17 | other: { 18 | "fc:frame": "vNext", 19 | "fc:frame:image": imageUrl, 20 | "fc:frame:post_url": postUrl, 21 | "fc:frame:input:text": "Enter EIP/ERC No", 22 | "fc:frame:button:1": "Search 🔎", 23 | "of:version": "vNext", 24 | "of:accepts:anonymous": "true", 25 | "of:image": imageUrl, 26 | "of:post_url": postUrl, 27 | "of:input:text": "Enter EIP/ERC No", 28 | "of:button:1": "Search 🔎", 29 | }, 30 | }; 31 | } 32 | 33 | export default function SharedLayout({ 34 | children, 35 | }: { 36 | children: React.ReactNode; 37 | }) { 38 | return {children}; 39 | } 40 | -------------------------------------------------------------------------------- /app/shared/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSearchParams } from "next/navigation"; 4 | import { 5 | Button, 6 | Box, 7 | Heading, 8 | Text, 9 | Container, 10 | Center, 11 | VStack, 12 | useToast, 13 | HStack, 14 | Spacer, 15 | } from "@chakra-ui/react"; 16 | import { useLocalStorage } from "usehooks-ts"; 17 | import { EIPType } from "@/types"; 18 | import React, { useMemo, Suspense } from "react"; 19 | import { validEIPs } from "@/data/validEIPs"; 20 | import { validRIPs } from "@/data/validRIPs"; 21 | import { validCAIPs } from "@/data/validCAIPs"; 22 | import { EIPGridItem } from "@/components/TrendingEIPs"; 23 | import { FaRegBookmark } from "react-icons/fa"; 24 | 25 | interface Bookmark { 26 | eipNo: string; 27 | type?: EIPType; 28 | title?: string; 29 | status?: string; 30 | } 31 | 32 | const SharedList = () => { 33 | return ( 34 | Loading...}> 35 | 36 | 37 | ); 38 | }; 39 | 40 | const SharedListContent = () => { 41 | const searchParams = useSearchParams(); 42 | const toast = useToast(); 43 | 44 | const queryKeys = ["eip", "erc", "caip", "rip"]; 45 | const [bookmarks, setBookmarks] = useLocalStorage( 46 | "eip-bookmarks", 47 | [] 48 | ); 49 | 50 | const paramKey = queryKeys.find((key) => searchParams.has(key)); 51 | const paramValue = paramKey ? searchParams.get(paramKey) : null; 52 | 53 | const parsedItems = useMemo(() => { 54 | if (!paramValue || !paramKey) return []; 55 | 56 | try { 57 | return paramValue 58 | .toString() 59 | .split(",") 60 | .map((item) => { 61 | const [type, eipNo] = item.includes("=") 62 | ? item.split("=") 63 | : [paramKey, item]; 64 | 65 | return { 66 | type: type.toUpperCase() as EIPType, 67 | eipNo, 68 | }; 69 | }); 70 | } catch (error) { 71 | if (error instanceof Error) { 72 | console.error("Error parsing items:", error.message); 73 | } else { 74 | console.error("Error parsing items:", error); 75 | } 76 | return []; 77 | } 78 | }, [paramValue, paramKey]); 79 | 80 | const addToReadingList = () => { 81 | console.log({ 82 | bookmarks, 83 | parsedItems, 84 | }); 85 | 86 | const newBookmarks = parsedItems 87 | .filter((item) => { 88 | return !bookmarks.some( 89 | (bookmark) => 90 | bookmark.eipNo === item.eipNo && 91 | (bookmark.type ? bookmark.type === item.type.toUpperCase() : true) 92 | ); 93 | }) 94 | .map((item) => { 95 | const type = item.type.toUpperCase() as EIPType; 96 | let dataSource; 97 | 98 | if (type === EIPType.EIP) { 99 | dataSource = validEIPs; 100 | } else if (type === EIPType.RIP) { 101 | dataSource = validRIPs; 102 | } else if (type === EIPType.CAIP) { 103 | dataSource = validCAIPs; 104 | } 105 | 106 | const entry: { title?: string; status?: string } = 107 | dataSource?.[item.eipNo] || {}; 108 | const title = entry.title || `Title for ${type}-${item.eipNo}`; 109 | const status = entry.status || "Unknown"; 110 | 111 | return { 112 | eipNo: item.eipNo, 113 | type, 114 | title, 115 | status, 116 | }; 117 | }); 118 | 119 | setBookmarks([...bookmarks, ...newBookmarks]); 120 | 121 | toast({ 122 | title: "Action Successful.", 123 | description: "Added new items to your reading list!", 124 | status: "success", 125 | duration: 5000, 126 | isClosable: true, 127 | }); 128 | }; 129 | 130 | return ( 131 | 132 | 133 | Shared Reading List 134 | 135 | 138 | 139 | {parsedItems.length > 0 ? ( 140 | <> 141 | {parsedItems.map((item, index) => ( 142 | 143 | 144 | 145 | ))} 146 | 147 | ) : ( 148 | 149 | No items to display. Ensure the shared link contains valid data. 150 | 151 | )} 152 | 153 | ); 154 | }; 155 | 156 | export default SharedList; 157 | -------------------------------------------------------------------------------- /assets/Poppins-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EIPTools/eip-tools/28ddf9430fd67df2626bb27f4df08daf01a0310f/assets/Poppins-Bold.ttf -------------------------------------------------------------------------------- /components/Analytics.tsx: -------------------------------------------------------------------------------- 1 | import Script from "next/script"; 2 | 3 | const GA_ID = "G-H37RDW7NTT"; 4 | 5 | export const Analytics = () => { 6 | return ( 7 | <> 8 |