From 31e72bf05435a046f2bc2f93a3c0abfa3cc9b3e9 Mon Sep 17 00:00:00 2001 From: yahia008 Date: Tue, 3 Feb 2026 16:00:09 +0100 Subject: [PATCH 1/4] myblock_explorer --- .../block-explorer/.gitignore | 41 +++ .../7-block-explorer/block-explorer/README.md | 36 +++ .../app/block/[blocknumber]/page.tsx | 49 ++++ .../block-explorer/app/components/Navbar.tsx | 20 ++ .../app/components/OverviewItem.tsx | 16 ++ .../block-explorer/app/components/header.tsx | 36 +++ .../app/components/overview.tsx | 116 ++++++++ .../app/components/searchbar.tsx | 68 +++++ .../block-explorer/app/components/section.tsx | 120 +++++++++ .../app/components/transaction.tsx | 138 ++++++++++ .../block-explorer/app/favicon.ico | Bin 0 -> 25931 bytes .../block-explorer/app/globals.css | 125 +++++++++ .../block-explorer/app/layout.tsx | 34 +++ .../block-explorer/app/page.tsx | 15 ++ .../block-explorer/app/utils/jsonrpc.tsx | 27 ++ .../block-explorer/app/utils/obj.tsx | 251 ++++++++++++++++++ .../block-explorer/components.json | 22 ++ .../block-explorer/components/ui/card.tsx | 92 +++++++ .../block-explorer/components/ui/table.tsx | 116 ++++++++ .../block-explorer/eslint.config.mjs | 18 ++ .../block-explorer/lib/utils.ts | 6 + .../block-explorer/next.config.ts | 7 + .../block-explorer/package.json | 33 +++ .../block-explorer/postcss.config.mjs | 7 + .../block-explorer/public/file.svg | 1 + .../block-explorer/public/globe.svg | 1 + .../block-explorer/public/next.svg | 1 + .../block-explorer/public/vercel.svg | 1 + .../block-explorer/public/window.svg | 1 + .../block-explorer/tsconfig.json | 34 +++ 30 files changed, 1432 insertions(+) create mode 100644 assignments/7-block-explorer/block-explorer/.gitignore create mode 100644 assignments/7-block-explorer/block-explorer/README.md create mode 100644 assignments/7-block-explorer/block-explorer/app/block/[blocknumber]/page.tsx create mode 100644 assignments/7-block-explorer/block-explorer/app/components/Navbar.tsx create mode 100644 assignments/7-block-explorer/block-explorer/app/components/OverviewItem.tsx create mode 100644 assignments/7-block-explorer/block-explorer/app/components/header.tsx create mode 100644 assignments/7-block-explorer/block-explorer/app/components/overview.tsx create mode 100644 assignments/7-block-explorer/block-explorer/app/components/searchbar.tsx create mode 100644 assignments/7-block-explorer/block-explorer/app/components/section.tsx create mode 100644 assignments/7-block-explorer/block-explorer/app/components/transaction.tsx create mode 100644 assignments/7-block-explorer/block-explorer/app/favicon.ico create mode 100644 assignments/7-block-explorer/block-explorer/app/globals.css create mode 100644 assignments/7-block-explorer/block-explorer/app/layout.tsx create mode 100644 assignments/7-block-explorer/block-explorer/app/page.tsx create mode 100644 assignments/7-block-explorer/block-explorer/app/utils/jsonrpc.tsx create mode 100644 assignments/7-block-explorer/block-explorer/app/utils/obj.tsx create mode 100644 assignments/7-block-explorer/block-explorer/components.json create mode 100644 assignments/7-block-explorer/block-explorer/components/ui/card.tsx create mode 100644 assignments/7-block-explorer/block-explorer/components/ui/table.tsx create mode 100644 assignments/7-block-explorer/block-explorer/eslint.config.mjs create mode 100644 assignments/7-block-explorer/block-explorer/lib/utils.ts create mode 100644 assignments/7-block-explorer/block-explorer/next.config.ts create mode 100644 assignments/7-block-explorer/block-explorer/package.json create mode 100644 assignments/7-block-explorer/block-explorer/postcss.config.mjs create mode 100644 assignments/7-block-explorer/block-explorer/public/file.svg create mode 100644 assignments/7-block-explorer/block-explorer/public/globe.svg create mode 100644 assignments/7-block-explorer/block-explorer/public/next.svg create mode 100644 assignments/7-block-explorer/block-explorer/public/vercel.svg create mode 100644 assignments/7-block-explorer/block-explorer/public/window.svg create mode 100644 assignments/7-block-explorer/block-explorer/tsconfig.json diff --git a/assignments/7-block-explorer/block-explorer/.gitignore b/assignments/7-block-explorer/block-explorer/.gitignore new file mode 100644 index 00000000..5ef6a520 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/assignments/7-block-explorer/block-explorer/README.md b/assignments/7-block-explorer/block-explorer/README.md new file mode 100644 index 00000000..e215bc4c --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/assignments/7-block-explorer/block-explorer/app/block/[blocknumber]/page.tsx b/assignments/7-block-explorer/block-explorer/app/block/[blocknumber]/page.tsx new file mode 100644 index 00000000..d9cc49ad --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/block/[blocknumber]/page.tsx @@ -0,0 +1,49 @@ +import { Header } from '@/app/components/header' +import Overview from '@/app/components/overview' + +type BlockTag = "latest" | "pending" | "finalized" | "safe" | string; + +async function getBlockByNumber( + blocknumber: BlockTag, + fullTxObjects: boolean +) { + const res = await fetch('https://ethereum-rpc.publicnode.com', { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "eth_getBlockByNumber", + params: [blocknumber, fullTxObjects], + }), + }); + + const response = await res.json(); + console.log('Fetched Block Data:', response); + return response.result; +} + +function toHexBlockNumber(decimal: string) { + return "0x" + Number(decimal).toString(16); +} + +const Blockpage = async ({ params }: { params: Promise<{ blocknumber: string }> }) => { + const { blocknumber } = await params; + console.log('Requested Block Number:', blocknumber); + console.log('Type of Block Number:', typeof blocknumber); + const hexBlockNumber = toHexBlockNumber(blocknumber); + console.log('Hex Block Number:', hexBlockNumber); + + const block = await getBlockByNumber(hexBlockNumber, true); + + return ( +
+
+
+ +
+
+ ); +}; + +export default Blockpage; diff --git a/assignments/7-block-explorer/block-explorer/app/components/Navbar.tsx b/assignments/7-block-explorer/block-explorer/app/components/Navbar.tsx new file mode 100644 index 00000000..2b7edb43 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/components/Navbar.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { Searchbar } from './searchbar' +import { FaHome } from "react-icons/fa"; + + + +export const Navbar = () => { + return ( +
+
+

+ Block Explorer +

+
+ +
+
+
+ ) +} diff --git a/assignments/7-block-explorer/block-explorer/app/components/OverviewItem.tsx b/assignments/7-block-explorer/block-explorer/app/components/OverviewItem.tsx new file mode 100644 index 00000000..f4c942e8 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/components/OverviewItem.tsx @@ -0,0 +1,16 @@ +const Item = ({ + label, + value, +}: { + label: string + value: React.ReactNode +}) => ( +
+ {label}: + + {value} + +
+) + +export default Item diff --git a/assignments/7-block-explorer/block-explorer/app/components/header.tsx b/assignments/7-block-explorer/block-explorer/app/components/header.tsx new file mode 100644 index 00000000..03457c0f --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/components/header.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { Navbar } from './Navbar'; + +export const Header = () => { + return ( +
+
+
+ ETH Price: $20000 +
+ +
+ Market Cap: $20000 +
+ +
+ Transactions: $20000 +
+
+ +
+ +
+ +
+ ) +}; diff --git a/assignments/7-block-explorer/block-explorer/app/components/overview.tsx b/assignments/7-block-explorer/block-explorer/app/components/overview.tsx new file mode 100644 index 00000000..f4b522ee --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/components/overview.tsx @@ -0,0 +1,116 @@ +'use client' + +import Item from "./OverviewItem" + +const weiToEth = (wei: string) => + Number(BigInt(wei)) / 1e18 + +const gweiFromWei = (wei: string) => + Number(BigInt(wei)) / 1e9 + +const formatTimestamp = (timestampHex: string) => { + const ts = parseInt(timestampHex, 16) * 1000 + const date = new Date(ts) + return date.toUTCString() +} + +const Overview = ({ block }: { block: any }) => { + if (!block) return null + + const burntFeesWei = (BigInt(block.baseFeePerGas) * BigInt(block.gasUsed)).toString(); + const burntFeesEth = weiToEth(burntFeesWei); + + const blockNumber = parseInt(block.number, 16) + const gasUsed = parseInt(block.gasUsed, 16) + const gasLimit = parseInt(block.gasLimit, 16) + const size = parseInt(block.size, 16) + const txCount = block.transactions.length + + return ( +
+ + {/* Header */} +
+

+ Block #{blockNumber} +

+
+ + {/* Grid */} +
+ + + + + Unfinalized + + } + /> + + + + + + + + + {block.miner} + + } + /> + + + + + + + + + + 🔥{burntFeesEth.toFixed(6)} ETH + + } + /> + + + {block.extraData} +
+ } + /> + +
+ + ) +} + +export default Overview diff --git a/assignments/7-block-explorer/block-explorer/app/components/searchbar.tsx b/assignments/7-block-explorer/block-explorer/app/components/searchbar.tsx new file mode 100644 index 00000000..361e65a7 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/components/searchbar.tsx @@ -0,0 +1,68 @@ +"use client"; +import { useState } from "react"; +import { obg } from "../utils/obj"; + +export const Searchbar = () => { + const [open, setOpen] = useState(false); + const [category, setCategory] = useState(obg[0].name); + + const toggleDropdown = () => setOpen((prev) => !prev); + + return ( +
+
+ + {/* Category dropdown */} + + + {/* Dropdown menu */} + {open && ( +
+ {obg.map((item) => ( + + ))} +
+ )} + + {/* Search input */} + + + {/* Search button */} + +
+
+ ); +}; diff --git a/assignments/7-block-explorer/block-explorer/app/components/section.tsx b/assignments/7-block-explorer/block-explorer/app/components/section.tsx new file mode 100644 index 00000000..01ec8348 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/components/section.tsx @@ -0,0 +1,120 @@ +'use client' + +import React, { useEffect, useState } from 'react' +import axios from 'axios' +import Link from 'next/link' +import { latestTransactions } from '../utils/obj' // keep static transactions for now +import { getBlockByNumber } from '../utils/jsonrpc' +import { FaCube } from 'react-icons/fa' +import { LatestTransactions } from './transaction' + +const Section = () => { + const [latestBlocks, setLatestBlocks] = useState([]) + //const [latestTransactions, setLatestTransactions] = useState([]) + + useEffect(() => { + const fetchLatestBlocks = async () => { + try { + + const res = await axios.post('https://ethereum-rpc.publicnode.com', { + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + id: 1 + }) + + const latestHex = res.data.result + console.log('Latest Block Hex:', latestHex) + const latestDecimal = parseInt(latestHex, 16) + + + const blockPromises = [] + for (let i = 0; i < 12; i++) { + const blockNumberHex = '0x' + (latestDecimal - i).toString(16) + console.log('Fetching block number (hex):', blockNumberHex) + blockPromises.push(getBlockByNumber(blockNumberHex, false, i+1) + ) + } + + const blocksResponses = await Promise.all(blockPromises) + + + const formattedBlocks = blocksResponses + .filter(b => b !== null && b?.data?.result) + .map((b: any, index: number) => { + const block = b.data.result + console.log('Block Data:', block) + const txTime = new Date().toLocaleTimeString() // optional: approximate + return { + id: index, + blockNumber: parseInt(block.number, 16), + time: new Date(parseInt(block.timestamp, 16) * 1000).toLocaleString(), + miner: block.miner, + txCount: block.transactions.length, + txTime: txTime, + reward: '0.0 ETH', + icon: , // optional icon + } + }) + + setLatestBlocks(formattedBlocks) + } catch (error) { + console.error('Error fetching latest blocks:', error) + } + } + //const data = getBlockByNumber(latestHex, true, 1) + + fetchLatestBlocks() + + //const interval = setInterval(fetchLatestBlocks, 5000) + //return () => clearInterval(interval) + }, []) + + return ( +
+ {/* Section 1: Latest Blocks */} +
+
Latest Blocks
+ + {latestBlocks.map((block) => ( + + {/* Icon */} +
{block.icon}
+ + {/* Block Number & Time */} +
+
{block.blockNumber}
+
{block.time}
+
+ + {/* Miner & Txns */} +
+
Miner: {block.miner}
+
+ Txns: {block.txCount} in {block.txTime} +
+
+ + {/* Reward */} +
+
Reward: {block.reward}
+
+ + ))} +
+ + {/* Section 2: Latest Transactions (static for now) */} +
+
Latest Transactions
+ + +
+
+ ) +} + +export default Section diff --git a/assignments/7-block-explorer/block-explorer/app/components/transaction.tsx b/assignments/7-block-explorer/block-explorer/app/components/transaction.tsx new file mode 100644 index 00000000..72ca0ea6 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/components/transaction.tsx @@ -0,0 +1,138 @@ +import { useState, useEffect, JSX } from 'react'; +import { FaFileContract } from 'react-icons/fa'; +import axios from 'axios'; +import { getBlockByNumber } from '../utils/jsonrpc'; + +interface Transaction { + id: number; + hash: string; + from: string; + to: string; + value: string; + blockNumber: number; + timestamp: number; + time: string; + icon: JSX.Element; +} + +export const LatestTransactions = () => { + const [latestTransactions, setLatestTransactions] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchLatestTransactions = async () => { + try { + setLoading(true); + + + const blockNumberRes = await axios.post( + 'https://ethereum-rpc.publicnode.com', + { + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + id: 1, + } + ); + + const latestBlockHex = blockNumberRes.data.result; + const latestBlock = parseInt(latestBlockHex, 16); + + + const blockRequests = Array.from({ length: 19 }, (_, i) => { + const hex = '0x' + (latestBlock - i).toString(16); + return getBlockByNumber(hex, true, i + 1); + }); + + const blockResponses = await Promise.all(blockRequests); + + + let txs: Transaction[] = []; + let idCounter = 0; + + blockResponses.forEach((res: any) => { + const block = res?.data?.result; + if (!block?.transactions) return; + + const timestamp = parseInt(block.timestamp, 16); + + block.transactions.forEach((tx: any) => { + const valueWei = BigInt(tx.value); + const valueEth = Number(valueWei) / 1e18; + + txs.push({ + id: idCounter++, + hash: tx.hash, + from: tx.from, + to: tx.to ?? 'Contract Creation', + value: `${valueEth.toFixed(4)} ETH`, + blockNumber: parseInt(block.number, 16), + timestamp, + time: getTimeAgo(timestamp), + icon: , + }); + }); + }); + + + txs.sort((a, b) => b.timestamp - a.timestamp); + + + setLatestTransactions(txs.slice(0, 15)); + } catch (err) { + console.error('Failed to fetch latest transactions:', err); + } finally { + setLoading(false); + } + }; + + const getTimeAgo = (timestamp: number) => { + const diff = Math.floor(Date.now() / 1000) - timestamp; + if (diff < 60) return `${diff}s ago`; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + return `${Math.floor(diff / 3600)}h ago`; + }; + + const formatAddress = (addr: string) => + addr === 'Contract Creation' + ? addr + : `${addr.slice(0, 8)}...${addr.slice(-6)}`; + + useEffect(() => { + fetchLatestTransactions(); + + + }, []); + + if (loading) { + return
Loading latest transactions…
; + } + + return ( +
+
Latest Transactions
+ + {latestTransactions.map((tx) => ( +
+ {tx.icon} + +
+
+ {formatAddress(tx.hash)} +
+
{tx.time}
+
+ +
+
From: {formatAddress(tx.from)}
+
To: {formatAddress(tx.to)}
+
+ +
{tx.value}
+
+ ))} +
+ ); +}; diff --git a/assignments/7-block-explorer/block-explorer/app/favicon.ico b/assignments/7-block-explorer/block-explorer/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/assignments/7-block-explorer/block-explorer/app/globals.css b/assignments/7-block-explorer/block-explorer/app/globals.css new file mode 100644 index 00000000..0e0bca06 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/globals.css @@ -0,0 +1,125 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.129 0.042 264.695); + --card: oklch(1 0 0); + --card-foreground: oklch(0.129 0.042 264.695); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.129 0.042 264.695); + --primary: oklch(0.208 0.042 265.755); + --primary-foreground: oklch(0.984 0.003 247.858); + --secondary: oklch(0.968 0.007 247.896); + --secondary-foreground: oklch(0.208 0.042 265.755); + --muted: oklch(0.968 0.007 247.896); + --muted-foreground: oklch(0.554 0.046 257.417); + --accent: oklch(0.968 0.007 247.896); + --accent-foreground: oklch(0.208 0.042 265.755); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.929 0.013 255.508); + --input: oklch(0.929 0.013 255.508); + --ring: oklch(0.704 0.04 256.788); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.984 0.003 247.858); + --sidebar-foreground: oklch(0.129 0.042 264.695); + --sidebar-primary: oklch(0.208 0.042 265.755); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.968 0.007 247.896); + --sidebar-accent-foreground: oklch(0.208 0.042 265.755); + --sidebar-border: oklch(0.929 0.013 255.508); + --sidebar-ring: oklch(0.704 0.04 256.788); +} + +.dark { + --background: oklch(0.129 0.042 264.695); + --foreground: oklch(0.984 0.003 247.858); + --card: oklch(0.208 0.042 265.755); + --card-foreground: oklch(0.984 0.003 247.858); + --popover: oklch(0.208 0.042 265.755); + --popover-foreground: oklch(0.984 0.003 247.858); + --primary: oklch(0.929 0.013 255.508); + --primary-foreground: oklch(0.208 0.042 265.755); + --secondary: oklch(0.279 0.041 260.031); + --secondary-foreground: oklch(0.984 0.003 247.858); + --muted: oklch(0.279 0.041 260.031); + --muted-foreground: oklch(0.704 0.04 256.788); + --accent: oklch(0.279 0.041 260.031); + --accent-foreground: oklch(0.984 0.003 247.858); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.551 0.027 264.364); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.208 0.042 265.755); + --sidebar-foreground: oklch(0.984 0.003 247.858); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.279 0.041 260.031); + --sidebar-accent-foreground: oklch(0.984 0.003 247.858); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.551 0.027 264.364); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/assignments/7-block-explorer/block-explorer/app/layout.tsx b/assignments/7-block-explorer/block-explorer/app/layout.tsx new file mode 100644 index 00000000..f63b5d9c --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "block-explorer", + description: "block explorer for various blockchains", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/assignments/7-block-explorer/block-explorer/app/page.tsx b/assignments/7-block-explorer/block-explorer/app/page.tsx new file mode 100644 index 00000000..7595d0ce --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/page.tsx @@ -0,0 +1,15 @@ +import Image from "next/image"; +import { Header } from "./components/header"; +import Section from "./components/section"; + +export default function Home() { + return ( + +
+
+
+
+ + + ); +} diff --git a/assignments/7-block-explorer/block-explorer/app/utils/jsonrpc.tsx b/assignments/7-block-explorer/block-explorer/app/utils/jsonrpc.tsx new file mode 100644 index 00000000..6192ec02 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/utils/jsonrpc.tsx @@ -0,0 +1,27 @@ +import axios from "axios"; + +export const getBlockByNumber= async(number: string, bol: boolean, id:number) => { + try { + const response = await axios.post('https://ethereum-rpc.publicnode.com', { + jsonrpc: "2.0", + method: "eth_getBlockByNumber", + params: [number, bol], + id: id + }); + console.log('Block Data for', number, ':', response); + return response + } catch (error) { + console.error("Error fetching block by number:", error); + throw error; + } +} + +export const getBlockNumber = async() => { + const res = await axios.post('https://ethereum-rpc.publicnode.com', { + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + id: 1 + }) + return res +} \ No newline at end of file diff --git a/assignments/7-block-explorer/block-explorer/app/utils/obj.tsx b/assignments/7-block-explorer/block-explorer/app/utils/obj.tsx new file mode 100644 index 00000000..8ad5a68d --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/app/utils/obj.tsx @@ -0,0 +1,251 @@ +import { FaCube, FaExchangeAlt } from "react-icons/fa"; + +export const obg = [ + { id: 1, name: "All", link: "#" }, + { id: 2, name: "Blocks", link: "#" }, + { id: 3, name: "Transactions", link: "#" }, + { id: 4, name: "Addresses", link: "#" }, + { id: 5, name: "Tokens", link: "#" }, + { id: 6, name: "Contracts", link: "#" }, +]; + +export const latestBlocks = [ + { + id: 1, + icon: , + blockNumber: 24353978, + time: "9 secs ago", + miner: "Titan Builder", + txCount: 294, + txTime: "12 secs", + reward: "0.0044 ETH", + }, + { + id: 2, + icon: , + blockNumber: 24353977, + time: "18 secs ago", + miner: "EtherNode", + txCount: 210, + txTime: "10 secs", + reward: "0.0039 ETH", + }, + { + id: 3, + icon: , + blockNumber: 24353976, + time: "27 secs ago", + miner: "BlockForge", + txCount: 180, + txTime: "15 secs", + reward: "0.0050 ETH", + }, + { + id: 4, + icon: , + blockNumber: 24353975, + time: "35 secs ago", + miner: "Green Miner", + txCount: 250, + txTime: "13 secs", + reward: "0.0047 ETH", + }, + { + id: 5, + icon: , + blockNumber: 24353974, + time: "45 secs ago", + miner: "NodeX", + txCount: 310, + txTime: "18 secs", + reward: "0.0052 ETH", + }, + { + id: 6, + icon: , + blockNumber: 24353973, + time: "55 secs ago", + miner: "BlockChainers", + txCount: 280, + txTime: "14 secs", + reward: "0.0049 ETH", + }, + { + id: 7, + icon: , + blockNumber: 24353972, + time: "1 min ago", + miner: "AlphaMiner", + txCount: 200, + txTime: "12 secs", + reward: "0.0042 ETH", + }, +]; + + +export const latestTransactions = [ + { + id: 1, + icon: , + hash: "0x91c0679d0eb5fdda39efcf44022852fdd5d6e1874925e2fef1573643449a4f97", + time: "9 secs ago", + from: "0x4838B106...B0BAD5f97", + to: "0x22eEC85b...D9c6fa778", + value: "0.01082 ETH", + }, + { + id: 2, + icon: , + hash: "0xcda8cbbd2cec32e9a003fd9520e6ad752c857a43271a16089a0459cd3daba288", + time: "12 secs ago", + from: "0x6bc727Ab...2cbF35748", + to: "0x7df9415B...8d526D1a4", + value: "0 ETH", + }, + { + id: 3, + icon: , + hash: "0xf467b67b2370a5400d881894bfc1440a88d493d99f641d3234cb3b32d855eb6e", + time: "15 secs ago", + from: "0x67dD6f6A...bfF629203", + to: "0xD4416b13...E25686401", + value: "0 ETH", + }, + { + id: 4, + icon: , + hash: "0x5cf5207804ebcb99143266bd660e3898bd9246cd55a18001444cc903b03c6ff5", + time: "18 secs ago", + from: "0x8C8D7C46...D564d7465", + to: "0x5A22E074...576ee14b4", + value: "0.06485 ETH", + }, + { + id: 5, + icon: , + hash: "0x48599d0846584359e34ae08d015a7b22641bf9b639ba7550b9f5f3b611ac9058", + time: "21 secs ago", + from: "0x4945cE2d...19Cab982b", + to: "0xc57853C8...a28387abb", + value: "0.00002 ETH", + }, + { + id: 6, + icon: , + hash: "0xc2a7f68ccceb5ba41f0883c6ea00f22b0fba645285b7bbf992e15436d6618fee", + time: "24 secs ago", + from: "0xc6d77CD1...5BD459d9f", + to: "0xdAC17F95...13D831ec7", + value: "0 ETH", + }, + { + id: 7, + icon: , + hash: "0x74d9bcbb7fae9cbbd4e0df7c69c61e5b87a96c431c0fcb0f6f3dbdd4c91e3c18", + time: "27 secs ago", + from: "0xA91d3C92...9dC771e23", + to: "0x4d8cB8B2...eBf67112A", + value: "1.204 ETH", + }, + { + id: 8, + icon: , + hash: "0x7fbc9c918b8f1d63dca52c20bb3a53a2ef65d5e457f37b76f19b05db43c9f3c9", + time: "31 secs ago", + from: "0x8cE9A5e1...bA45f9a21", + to: "0xF977814e...5cE2d63F9", + value: "0.52 ETH", + }, + { + id: 9, + icon: , + hash: "0x6bb91e65a1a01c6f2b99e4a3f4f960a6f62b7e2e3f1a24a8d0d5a3b2f9c4b8a2", + time: "35 secs ago", + from: "0x2f318C5D...d9f92E1B3", + to: "0x1a3F45C9...91aA0f83C", + value: "0 ETH", + }, + { + id: 10, + icon: , + hash: "0x4f93cbb6fcd9b5d75f13d8f1c2e64b41bde1e0a84a7c7b7a1fd14f52cb4ad91e", + time: "40 secs ago", + from: "0xE592427A...05861564", + to: "0x88e6A0c2...5640F5d7", + value: "0.003 ETH", + }, + { + id: 11, + icon: , + hash: "0x92c8a4b78bde79dcae8e6cbe45a52b95dbcf53c2e68e6a8d9d3b0b45c51f4a33", + time: "45 secs ago", + from: "0x9A4f2C9A...1Ebd89aC2", + to: "0x3fC91A3a...F0c26c9B", + value: "2.45 ETH", + }, + { + id: 12, + icon: , + hash: "0x19a3d4b5a1e77a8c8dbe7ef5bbad2d1a92e0c5b5f7e3b1eac6db9f1c38d44c61", + time: "51 secs ago", + from: "0x742d35Cc...f44e", + to: "0xBcd4042D...F2dAE", + value: "0 ETH", + }, + { + id: 13, + icon: , + hash: "0xa91b5f8d8d7b3d72a91a5b6d7c2b6a9f4a3b1d8f6c2a8d5f9e4c1b8a3d7c5f", + time: "58 secs ago", + from: "0xAb5801a7...bA7", + to: "0x00000000...dEaD", + value: "0 ETH", + }, + { + id: 14, + icon: , + hash: "0x5c9f1a9a7f3e8b1c4d2e7a6b9f5a3d8c6b4e2a1f9d7c8b3a6e5d4f2a9c7b1", + time: "1 min ago", + from: "0x3C44CdDd...9cAE", + to: "0x90F79bf6...dC42", + value: "0.89 ETH", + }, + { + id: 15, + icon: , + hash: "0xb4c2f7d8a3e9c5d6b1f4a8e7c9d2b5a6f1e3d4c8a9b7f5e6d2c1a4b3e8d9", + time: "1 min ago", + from: "0x15d34AAf...a65", + to: "0x9965507D...e0C", + value: "0.12 ETH", + }, +]; + + + +export type Block = { + height: number; + status: "Finalized" | "Pending"; + timestamp: string; + proposedOn: string; + + transactions: number; + internalTransactions: number; + withdrawals: number; + + feeRecipient: string; + reward: string; + totalDifficulty: string; + size: string; + gasUsed: string; + gasLimit: string; + baseFee: string; + burntFees: string; + extraData: string; + + hash: string; + parentHash: string; + stateRoot: string; + withdrawalsRoot: string; + nonce: string; +}; diff --git a/assignments/7-block-explorer/block-explorer/components.json b/assignments/7-block-explorer/block-explorer/components.json new file mode 100644 index 00000000..2c2c3f18 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/assignments/7-block-explorer/block-explorer/components/ui/card.tsx b/assignments/7-block-explorer/block-explorer/components/ui/card.tsx new file mode 100644 index 00000000..681ad980 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/assignments/7-block-explorer/block-explorer/components/ui/table.tsx b/assignments/7-block-explorer/block-explorer/components/ui/table.tsx new file mode 100644 index 00000000..51b74dd5 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/components/ui/table.tsx @@ -0,0 +1,116 @@ +"use client" + +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/assignments/7-block-explorer/block-explorer/eslint.config.mjs b/assignments/7-block-explorer/block-explorer/eslint.config.mjs new file mode 100644 index 00000000..05e726d1 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/assignments/7-block-explorer/block-explorer/lib/utils.ts b/assignments/7-block-explorer/block-explorer/lib/utils.ts new file mode 100644 index 00000000..bd0c391d --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/assignments/7-block-explorer/block-explorer/next.config.ts b/assignments/7-block-explorer/block-explorer/next.config.ts new file mode 100644 index 00000000..e9ffa308 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/assignments/7-block-explorer/block-explorer/package.json b/assignments/7-block-explorer/block-explorer/package.json new file mode 100644 index 00000000..cd89fc42 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/package.json @@ -0,0 +1,33 @@ +{ + "name": "block-explorer", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "axios": "^1.13.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", + "next": "16.1.6", + "react": "19.2.3", + "react-dom": "19.2.3", + "react-icons": "^5.5.0", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.1.6", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", + "typescript": "^5" + } +} diff --git a/assignments/7-block-explorer/block-explorer/postcss.config.mjs b/assignments/7-block-explorer/block-explorer/postcss.config.mjs new file mode 100644 index 00000000..61e36849 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/assignments/7-block-explorer/block-explorer/public/file.svg b/assignments/7-block-explorer/block-explorer/public/file.svg new file mode 100644 index 00000000..004145cd --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assignments/7-block-explorer/block-explorer/public/globe.svg b/assignments/7-block-explorer/block-explorer/public/globe.svg new file mode 100644 index 00000000..567f17b0 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assignments/7-block-explorer/block-explorer/public/next.svg b/assignments/7-block-explorer/block-explorer/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assignments/7-block-explorer/block-explorer/public/vercel.svg b/assignments/7-block-explorer/block-explorer/public/vercel.svg new file mode 100644 index 00000000..77053960 --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assignments/7-block-explorer/block-explorer/public/window.svg b/assignments/7-block-explorer/block-explorer/public/window.svg new file mode 100644 index 00000000..b2b2a44f --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assignments/7-block-explorer/block-explorer/tsconfig.json b/assignments/7-block-explorer/block-explorer/tsconfig.json new file mode 100644 index 00000000..3a13f90a --- /dev/null +++ b/assignments/7-block-explorer/block-explorer/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} From af79b9d228902b74ef487f621b327be7e4d15dec Mon Sep 17 00:00:00 2001 From: yahia008 Date: Tue, 24 Feb 2026 12:49:34 +0100 Subject: [PATCH 2/4] excrow --- excrowv1/.gitignore | 20 +++++++ excrowv1/README.md | 90 ++++++++++++++++++++++++++++ excrowv1/contracts/Counter.sol | 19 ++++++ excrowv1/contracts/Counter.t.sol | 32 ++++++++++ excrowv1/contracts/Excrow1.sol | 0 excrowv1/hardhat.config.ts | 38 ++++++++++++ excrowv1/ignition/modules/Counter.ts | 9 +++ excrowv1/package.json | 20 +++++++ excrowv1/scripts/send-op-tx.ts | 22 +++++++ excrowv1/test/Counter.ts | 36 +++++++++++ excrowv1/tsconfig.json | 13 ++++ 11 files changed, 299 insertions(+) create mode 100644 excrowv1/.gitignore create mode 100644 excrowv1/README.md create mode 100644 excrowv1/contracts/Counter.sol create mode 100644 excrowv1/contracts/Counter.t.sol create mode 100644 excrowv1/contracts/Excrow1.sol create mode 100644 excrowv1/hardhat.config.ts create mode 100644 excrowv1/ignition/modules/Counter.ts create mode 100644 excrowv1/package.json create mode 100644 excrowv1/scripts/send-op-tx.ts create mode 100644 excrowv1/test/Counter.ts create mode 100644 excrowv1/tsconfig.json diff --git a/excrowv1/.gitignore b/excrowv1/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/excrowv1/.gitignore @@ -0,0 +1,20 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# pnpm deploy output +/bundle + +# Hardhat Build Artifacts +/artifacts + +# Hardhat compilation (v2) support directory +/cache + +# Typechain output +/types + +# Hardhat coverage reports +/coverage diff --git a/excrowv1/README.md b/excrowv1/README.md new file mode 100644 index 00000000..6d3b5b8f --- /dev/null +++ b/excrowv1/README.md @@ -0,0 +1,90 @@ +Escrow Smart Contract + +A simple escrow system written in Solidity (^0.8.28). + +It allows a buyer to deposit ETH, and the seller receives payment only after delivery is confirmed. + +The project also includes an EscrowFactory contract to create multiple escrow contracts. + +How It Works +1️⃣ Create Escrow + +Using EscrowFactory, a new escrow contract is created with: + +Buyer address + +Seller address + +Each escrow is a separate contract. + +2️⃣ Buyer Deposits ETH + +deposit() + +Only buyer can call + +Must send ETH + +Escrow status changes from PENDING → PAID + +3️⃣ Confirm Delivery + +confirmDelivery() + +Only buyer can call + +ETH is sent to seller + +Status changes to COMPLETE + +4️⃣ Refund Buyer + +refundBuyer() + +Only buyer can call + +ETH is returned to buyer + +Status changes to REFUNDED + +Escrow Status + +PENDING → Waiting for payment + +PAID → Buyer has deposited ETH + +COMPLETE → Seller has been paid + +REFUNDED → Buyer received refund + +EscrowFactory + +The factory contract allows you to: + +Create new escrow contracts + +Store all escrow addresses + +Get total number of escrows + +Retrieve all escrows + +Simple Flow + +Create Escrow → Buyer deposits → +Either: + +Confirm delivery → Seller gets paid 💰 +OR + +Refund → Buyer gets money back + +Use Cases + +Online purchases + +Freelance payments + +Peer-to-peer transactions + +Secure ETH transfers between two parties \ No newline at end of file diff --git a/excrowv1/contracts/Counter.sol b/excrowv1/contracts/Counter.sol new file mode 100644 index 00000000..8d00cb7c --- /dev/null +++ b/excrowv1/contracts/Counter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Counter { + uint public x; + + event Increment(uint by); + + function inc() public { + x++; + emit Increment(1); + } + + function incBy(uint by) public { + require(by > 0, "incBy: increment should be positive"); + x += by; + emit Increment(by); + } +} diff --git a/excrowv1/contracts/Counter.t.sol b/excrowv1/contracts/Counter.t.sol new file mode 100644 index 00000000..ac71d5b8 --- /dev/null +++ b/excrowv1/contracts/Counter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; + +// Solidity tests are compatible with foundry, so they +// use the same syntax and offer the same functionality. + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_InitialValue() public view { + require(counter.x() == 0, "Initial value should be 0"); + } + + function testFuzz_Inc(uint8 x) public { + for (uint8 i = 0; i < x; i++) { + counter.inc(); + } + require(counter.x() == x, "Value after calling inc x times should be x"); + } + + function test_IncByZero() public { + vm.expectRevert(); + counter.incBy(0); + } +} diff --git a/excrowv1/contracts/Excrow1.sol b/excrowv1/contracts/Excrow1.sol new file mode 100644 index 00000000..e69de29b diff --git a/excrowv1/hardhat.config.ts b/excrowv1/hardhat.config.ts new file mode 100644 index 00000000..7092b852 --- /dev/null +++ b/excrowv1/hardhat.config.ts @@ -0,0 +1,38 @@ +import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers"; +import { configVariable, defineConfig } from "hardhat/config"; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + solidity: { + profiles: { + default: { + version: "0.8.28", + }, + production: { + version: "0.8.28", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + }, + }, + networks: { + hardhatMainnet: { + type: "edr-simulated", + chainType: "l1", + }, + hardhatOp: { + type: "edr-simulated", + chainType: "op", + }, + sepolia: { + type: "http", + chainType: "l1", + url: configVariable("SEPOLIA_RPC_URL"), + accounts: [configVariable("SEPOLIA_PRIVATE_KEY")], + }, + }, +}); diff --git a/excrowv1/ignition/modules/Counter.ts b/excrowv1/ignition/modules/Counter.ts new file mode 100644 index 00000000..042e61c8 --- /dev/null +++ b/excrowv1/ignition/modules/Counter.ts @@ -0,0 +1,9 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("CounterModule", (m) => { + const counter = m.contract("Counter"); + + m.call(counter, "incBy", [5n]); + + return { counter }; +}); diff --git a/excrowv1/package.json b/excrowv1/package.json new file mode 100644 index 00000000..8a3a0667 --- /dev/null +++ b/excrowv1/package.json @@ -0,0 +1,20 @@ +{ + "name": "excrowv1", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.4", + "@nomicfoundation/hardhat-ignition": "^3.0.7", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.11", + "chai": "^5.3.3", + "ethers": "^6.16.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "hardhat": "^3.1.8", + "mocha": "^11.7.5", + "typescript": "~5.8.0" + } +} diff --git a/excrowv1/scripts/send-op-tx.ts b/excrowv1/scripts/send-op-tx.ts new file mode 100644 index 00000000..c10a2360 --- /dev/null +++ b/excrowv1/scripts/send-op-tx.ts @@ -0,0 +1,22 @@ +import { network } from "hardhat"; + +const { ethers } = await network.connect({ + network: "hardhatOp", + chainType: "op", +}); + +console.log("Sending transaction using the OP chain type"); + +const [sender] = await ethers.getSigners(); + +console.log("Sending 1 wei from", sender.address, "to itself"); + +console.log("Sending L2 transaction"); +const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, +}); + +await tx.wait(); + +console.log("Transaction sent successfully"); diff --git a/excrowv1/test/Counter.ts b/excrowv1/test/Counter.ts new file mode 100644 index 00000000..f8c38986 --- /dev/null +++ b/excrowv1/test/Counter.ts @@ -0,0 +1,36 @@ +import { expect } from "chai"; +import { network } from "hardhat"; + +const { ethers } = await network.connect(); + +describe("Counter", function () { + it("Should emit the Increment event when calling the inc() function", async function () { + const counter = await ethers.deployContract("Counter"); + + await expect(counter.inc()).to.emit(counter, "Increment").withArgs(1n); + }); + + it("The sum of the Increment events should match the current value", async function () { + const counter = await ethers.deployContract("Counter"); + const deploymentBlockNumber = await ethers.provider.getBlockNumber(); + + // run a series of increments + for (let i = 1; i <= 10; i++) { + await counter.incBy(i); + } + + const events = await counter.queryFilter( + counter.filters.Increment(), + deploymentBlockNumber, + "latest", + ); + + // check that the aggregated events match the current value + let total = 0n; + for (const event of events) { + total += event.args.by; + } + + expect(await counter.x()).to.equal(total); + }); +}); diff --git a/excrowv1/tsconfig.json b/excrowv1/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/excrowv1/tsconfig.json @@ -0,0 +1,13 @@ +/* Based on https://github.com/tsconfig/bases/blob/501da2bcd640cf95c95805783e1012b992338f28/bases/node22.json */ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "outDir": "dist" + } +} From 68dfb06e24635bfa4fb5a5fa0122efbd76b14d6b Mon Sep 17 00:00:00 2001 From: yahia008 Date: Tue, 24 Feb 2026 13:03:40 +0100 Subject: [PATCH 3/4] submit --- excrowv1/crowd/.gitignore | 20 +++ excrowv1/crowd/README.md | 89 +++++++++++ excrowv1/crowd/contracts/Counter.sol | 19 +++ excrowv1/crowd/contracts/Counter.t.sol | 32 ++++ excrowv1/crowd/contracts/crowd.sol | 166 +++++++++++++++++++++ excrowv1/crowd/hardhat.config.ts | 38 +++++ excrowv1/crowd/ignition/modules/Counter.ts | 9 ++ excrowv1/crowd/package.json | 20 +++ excrowv1/crowd/scripts/send-op-tx.ts | 22 +++ excrowv1/crowd/test/Counter.ts | 36 +++++ excrowv1/crowd/tsconfig.json | 13 ++ 11 files changed, 464 insertions(+) create mode 100644 excrowv1/crowd/.gitignore create mode 100644 excrowv1/crowd/README.md create mode 100644 excrowv1/crowd/contracts/Counter.sol create mode 100644 excrowv1/crowd/contracts/Counter.t.sol create mode 100644 excrowv1/crowd/contracts/crowd.sol create mode 100644 excrowv1/crowd/hardhat.config.ts create mode 100644 excrowv1/crowd/ignition/modules/Counter.ts create mode 100644 excrowv1/crowd/package.json create mode 100644 excrowv1/crowd/scripts/send-op-tx.ts create mode 100644 excrowv1/crowd/test/Counter.ts create mode 100644 excrowv1/crowd/tsconfig.json diff --git a/excrowv1/crowd/.gitignore b/excrowv1/crowd/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/excrowv1/crowd/.gitignore @@ -0,0 +1,20 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# pnpm deploy output +/bundle + +# Hardhat Build Artifacts +/artifacts + +# Hardhat compilation (v2) support directory +/cache + +# Typechain output +/types + +# Hardhat coverage reports +/coverage diff --git a/excrowv1/crowd/README.md b/excrowv1/crowd/README.md new file mode 100644 index 00000000..a6893ebd --- /dev/null +++ b/excrowv1/crowd/README.md @@ -0,0 +1,89 @@ +CrowdFunding Smart Contract + +A simple crowdfunding smart contract written in Solidity (^0.8.28). + +Users can create campaigns, contribute ETH, request refunds if the goal is not met, and creators can withdraw funds if the campaign succeeds. + +How It Works +1️⃣ Create Campaign + +Anyone can create a campaign by providing: + +Title + +Funding goal (in wei) + +Duration (in seconds) + +The campaign becomes ACTIVE immediately. + +2️⃣ Contribute + +Users send ETH to a campaign using contribute(). + +Must send ETH + +Campaign must still be active + +Cannot contribute after deadline + +If user sends more than needed, extra ETH is refunded automatically + +If goal is reached → campaign becomes SUCCESSFUL + +3️⃣ Refund + +If: + +Deadline has passed + +Goal was NOT reached + +Contributors can call requestRefund() to get their money back. + +Campaign becomes UNSUCCEEDED. + +4️⃣ Withdraw (Creator Only) + +If campaign is SUCCESSFUL, +the campaign creator can call withdraw() to collect the funds. + +Funds can only be withdrawn once. + +Campaign Status + +ACTIVE + +SUCCESSFUL + +UNSUCCEEDED + +DELETED + +Security + +Uses nonReentrant modifier to prevent reentrancy attacks + +Uses safe call for ETH transfers + +Prevents double withdrawals + +Main Functions + +createCampaign() + +contribute() + +requestRefund() + +withdraw() + +Use Cases + +Fundraising + +Charity campaigns + +Startup funding + +Community projects \ No newline at end of file diff --git a/excrowv1/crowd/contracts/Counter.sol b/excrowv1/crowd/contracts/Counter.sol new file mode 100644 index 00000000..8d00cb7c --- /dev/null +++ b/excrowv1/crowd/contracts/Counter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Counter { + uint public x; + + event Increment(uint by); + + function inc() public { + x++; + emit Increment(1); + } + + function incBy(uint by) public { + require(by > 0, "incBy: increment should be positive"); + x += by; + emit Increment(by); + } +} diff --git a/excrowv1/crowd/contracts/Counter.t.sol b/excrowv1/crowd/contracts/Counter.t.sol new file mode 100644 index 00000000..ac71d5b8 --- /dev/null +++ b/excrowv1/crowd/contracts/Counter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; + +// Solidity tests are compatible with foundry, so they +// use the same syntax and offer the same functionality. + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_InitialValue() public view { + require(counter.x() == 0, "Initial value should be 0"); + } + + function testFuzz_Inc(uint8 x) public { + for (uint8 i = 0; i < x; i++) { + counter.inc(); + } + require(counter.x() == x, "Value after calling inc x times should be x"); + } + + function test_IncByZero() public { + vm.expectRevert(); + counter.incBy(0); + } +} diff --git a/excrowv1/crowd/contracts/crowd.sol b/excrowv1/crowd/contracts/crowd.sol new file mode 100644 index 00000000..84a86bb1 --- /dev/null +++ b/excrowv1/crowd/contracts/crowd.sol @@ -0,0 +1,166 @@ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +contract CrowdFunding{ + + address private immutable owner; + + + uint private nextId; + bool private locked; + + modifier nonReentrant() { + require(!locked, "Reentrant call"); + locked = true; + _; + locked = false; + } + + struct Campaign { + uint id; + string title; + address creator; + uint goal; + uint deadline; + uint amountRaised; + STATUS status; + uint startsAt; + uint endsAt; + uint totalContributions; + bool claimed; + mapping(address => uint) contributions; + //uint[] contributionAmounts; + } + + enum STATUS { + ACTIVE, + DELETED, + SUCCESSFUL, + UNSUCCEEDED + } + + mapping(uint => Campaign) public campaigns; + uint public campaignCount; + + event CampaignCreated(uint indexed campaignId, address campaignCreator, string title, STATUS status); + event CampaignDeleted(uint indexed campaignId, address campaignCreator, STATUS status); + event ContributionMade(uint indexed campaignId, address contributor, uint amount); + event RefundMade(uint indexed campaignId, address contributor, uint amount); + + function createCampaign( + string memory _title, + uint _goal, + uint _duration)public { + require(bytes(_title).length > 0, "Title must not be empty"); + require(_goal > 0, "Goal must be greater than zero"); + require(_duration > 0, "Ends time must be greater than zero"); + + campaignCount++; + nextId++; + Campaign storage campaign = campaigns[campaignCount]; + campaign.id = nextId; + campaign.creator = msg.sender; + campaign.title =_title; + campaign.goal = _goal; + campaign.startsAt = block.timestamp; + campaign.status = STATUS.ACTIVE; + campaign.endsAt = block.timestamp + _duration; + + emit CampaignCreated(nextId , msg.sender, _title, STATUS.ACTIVE); + } + + + function contribute(uint _id) + public + payable + nonReentrant +{ + Campaign storage campaign = campaigns[_id]; + + require(msg.value > 0, "Must send ETH"); + require(block.timestamp < campaign.endsAt, "Campaign ended"); + require(campaign.status == STATUS.ACTIVE, "Not active"); + + uint remaining = campaign.goal - campaign.totalContributions; + + uint acceptedAmount = msg.value; + + if (msg.value > remaining) { + acceptedAmount = remaining; + + uint excess = msg.value - remaining; + + (bool success, ) = payable(msg.sender).call{value: excess}(""); + require(success, "Refund failed"); + } + + campaign.totalContributions += acceptedAmount; + campaign.contributions[msg.sender] += acceptedAmount; + + if (campaign.totalContributions >= campaign.goal) { + campaign.status = STATUS.SUCCESSFUL; + } + + emit ContributionMade(_id, msg.sender, acceptedAmount); +} + + function requestRefund(uint _id) + public + nonReentrant +{ + Campaign storage campaign = campaigns[_id]; + + + if ( + block.timestamp >= campaign.endsAt && + campaign.totalContributions < campaign.goal + ) { + campaign.status = STATUS.UNSUCCEEDED; + } + + require(campaign.status == STATUS.UNSUCCEEDED, "Refund not allowed"); + + uint contributedAmount = campaign.contributions[msg.sender]; + require(contributedAmount > 0, "No contribution found"); + + + campaign.contributions[msg.sender] = 0; + campaign.totalContributions -= contributedAmount; + + (bool success, ) = payable(msg.sender).call{value: contributedAmount}(""); + require(success, "Refund failed"); + + emit RefundMade(_id, msg.sender, contributedAmount); +} + + function withdraw(uint _id) + public + nonReentrant +{ + Campaign storage campaign = campaigns[_id]; + + + require(msg.sender == campaign.creator, "Not campaign creator"); + + + require(campaign.status == STATUS.SUCCESSFUL, "Campaign not successful"); + + + require(!campaign.claimed, "Funds already withdrawn"); + + uint amount = campaign.totalContributions; + require(amount > 0, "No funds to withdraw"); + + campaign.claimed = true; + + (bool success, ) = payable(msg.sender).call{value: amount}(""); + require(success, "Withdrawal failed"); + + emit RefundMade(_id, msg.sender, amount); +} + + + +} + diff --git a/excrowv1/crowd/hardhat.config.ts b/excrowv1/crowd/hardhat.config.ts new file mode 100644 index 00000000..7092b852 --- /dev/null +++ b/excrowv1/crowd/hardhat.config.ts @@ -0,0 +1,38 @@ +import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers"; +import { configVariable, defineConfig } from "hardhat/config"; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + solidity: { + profiles: { + default: { + version: "0.8.28", + }, + production: { + version: "0.8.28", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + }, + }, + networks: { + hardhatMainnet: { + type: "edr-simulated", + chainType: "l1", + }, + hardhatOp: { + type: "edr-simulated", + chainType: "op", + }, + sepolia: { + type: "http", + chainType: "l1", + url: configVariable("SEPOLIA_RPC_URL"), + accounts: [configVariable("SEPOLIA_PRIVATE_KEY")], + }, + }, +}); diff --git a/excrowv1/crowd/ignition/modules/Counter.ts b/excrowv1/crowd/ignition/modules/Counter.ts new file mode 100644 index 00000000..042e61c8 --- /dev/null +++ b/excrowv1/crowd/ignition/modules/Counter.ts @@ -0,0 +1,9 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("CounterModule", (m) => { + const counter = m.contract("Counter"); + + m.call(counter, "incBy", [5n]); + + return { counter }; +}); diff --git a/excrowv1/crowd/package.json b/excrowv1/crowd/package.json new file mode 100644 index 00000000..ababa8dc --- /dev/null +++ b/excrowv1/crowd/package.json @@ -0,0 +1,20 @@ +{ + "name": "crowd", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.4", + "@nomicfoundation/hardhat-ignition": "^3.0.7", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.11", + "chai": "^5.3.3", + "ethers": "^6.16.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "hardhat": "^3.1.8", + "mocha": "^11.7.5", + "typescript": "~5.8.0" + } +} diff --git a/excrowv1/crowd/scripts/send-op-tx.ts b/excrowv1/crowd/scripts/send-op-tx.ts new file mode 100644 index 00000000..c10a2360 --- /dev/null +++ b/excrowv1/crowd/scripts/send-op-tx.ts @@ -0,0 +1,22 @@ +import { network } from "hardhat"; + +const { ethers } = await network.connect({ + network: "hardhatOp", + chainType: "op", +}); + +console.log("Sending transaction using the OP chain type"); + +const [sender] = await ethers.getSigners(); + +console.log("Sending 1 wei from", sender.address, "to itself"); + +console.log("Sending L2 transaction"); +const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, +}); + +await tx.wait(); + +console.log("Transaction sent successfully"); diff --git a/excrowv1/crowd/test/Counter.ts b/excrowv1/crowd/test/Counter.ts new file mode 100644 index 00000000..f8c38986 --- /dev/null +++ b/excrowv1/crowd/test/Counter.ts @@ -0,0 +1,36 @@ +import { expect } from "chai"; +import { network } from "hardhat"; + +const { ethers } = await network.connect(); + +describe("Counter", function () { + it("Should emit the Increment event when calling the inc() function", async function () { + const counter = await ethers.deployContract("Counter"); + + await expect(counter.inc()).to.emit(counter, "Increment").withArgs(1n); + }); + + it("The sum of the Increment events should match the current value", async function () { + const counter = await ethers.deployContract("Counter"); + const deploymentBlockNumber = await ethers.provider.getBlockNumber(); + + // run a series of increments + for (let i = 1; i <= 10; i++) { + await counter.incBy(i); + } + + const events = await counter.queryFilter( + counter.filters.Increment(), + deploymentBlockNumber, + "latest", + ); + + // check that the aggregated events match the current value + let total = 0n; + for (const event of events) { + total += event.args.by; + } + + expect(await counter.x()).to.equal(total); + }); +}); diff --git a/excrowv1/crowd/tsconfig.json b/excrowv1/crowd/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/excrowv1/crowd/tsconfig.json @@ -0,0 +1,13 @@ +/* Based on https://github.com/tsconfig/bases/blob/501da2bcd640cf95c95805783e1012b992338f28/bases/node22.json */ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "outDir": "dist" + } +} From 1e3e9cd1e4e84a18acffcadc6953e8070f27f7af Mon Sep 17 00:00:00 2001 From: yahia008 Date: Tue, 24 Feb 2026 13:34:59 +0100 Subject: [PATCH 4/4] submit --- crowd/.gitignore | 20 ++++ crowd/README.md | 89 ++++++++++++++++ crowd/contracts/Counter.sol | 19 ++++ crowd/contracts/Counter.t.sol | 32 ++++++ crowd/contracts/crowd.sol | 166 ++++++++++++++++++++++++++++++ crowd/hardhat.config.ts | 38 +++++++ crowd/ignition/modules/Counter.ts | 9 ++ crowd/package.json | 20 ++++ crowd/scripts/send-op-tx.ts | 22 ++++ crowd/test/Counter.ts | 36 +++++++ crowd/tsconfig.json | 13 +++ 11 files changed, 464 insertions(+) create mode 100644 crowd/.gitignore create mode 100644 crowd/README.md create mode 100644 crowd/contracts/Counter.sol create mode 100644 crowd/contracts/Counter.t.sol create mode 100644 crowd/contracts/crowd.sol create mode 100644 crowd/hardhat.config.ts create mode 100644 crowd/ignition/modules/Counter.ts create mode 100644 crowd/package.json create mode 100644 crowd/scripts/send-op-tx.ts create mode 100644 crowd/test/Counter.ts create mode 100644 crowd/tsconfig.json diff --git a/crowd/.gitignore b/crowd/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/crowd/.gitignore @@ -0,0 +1,20 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# pnpm deploy output +/bundle + +# Hardhat Build Artifacts +/artifacts + +# Hardhat compilation (v2) support directory +/cache + +# Typechain output +/types + +# Hardhat coverage reports +/coverage diff --git a/crowd/README.md b/crowd/README.md new file mode 100644 index 00000000..a6893ebd --- /dev/null +++ b/crowd/README.md @@ -0,0 +1,89 @@ +CrowdFunding Smart Contract + +A simple crowdfunding smart contract written in Solidity (^0.8.28). + +Users can create campaigns, contribute ETH, request refunds if the goal is not met, and creators can withdraw funds if the campaign succeeds. + +How It Works +1️⃣ Create Campaign + +Anyone can create a campaign by providing: + +Title + +Funding goal (in wei) + +Duration (in seconds) + +The campaign becomes ACTIVE immediately. + +2️⃣ Contribute + +Users send ETH to a campaign using contribute(). + +Must send ETH + +Campaign must still be active + +Cannot contribute after deadline + +If user sends more than needed, extra ETH is refunded automatically + +If goal is reached → campaign becomes SUCCESSFUL + +3️⃣ Refund + +If: + +Deadline has passed + +Goal was NOT reached + +Contributors can call requestRefund() to get their money back. + +Campaign becomes UNSUCCEEDED. + +4️⃣ Withdraw (Creator Only) + +If campaign is SUCCESSFUL, +the campaign creator can call withdraw() to collect the funds. + +Funds can only be withdrawn once. + +Campaign Status + +ACTIVE + +SUCCESSFUL + +UNSUCCEEDED + +DELETED + +Security + +Uses nonReentrant modifier to prevent reentrancy attacks + +Uses safe call for ETH transfers + +Prevents double withdrawals + +Main Functions + +createCampaign() + +contribute() + +requestRefund() + +withdraw() + +Use Cases + +Fundraising + +Charity campaigns + +Startup funding + +Community projects \ No newline at end of file diff --git a/crowd/contracts/Counter.sol b/crowd/contracts/Counter.sol new file mode 100644 index 00000000..8d00cb7c --- /dev/null +++ b/crowd/contracts/Counter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Counter { + uint public x; + + event Increment(uint by); + + function inc() public { + x++; + emit Increment(1); + } + + function incBy(uint by) public { + require(by > 0, "incBy: increment should be positive"); + x += by; + emit Increment(by); + } +} diff --git a/crowd/contracts/Counter.t.sol b/crowd/contracts/Counter.t.sol new file mode 100644 index 00000000..ac71d5b8 --- /dev/null +++ b/crowd/contracts/Counter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; + +// Solidity tests are compatible with foundry, so they +// use the same syntax and offer the same functionality. + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_InitialValue() public view { + require(counter.x() == 0, "Initial value should be 0"); + } + + function testFuzz_Inc(uint8 x) public { + for (uint8 i = 0; i < x; i++) { + counter.inc(); + } + require(counter.x() == x, "Value after calling inc x times should be x"); + } + + function test_IncByZero() public { + vm.expectRevert(); + counter.incBy(0); + } +} diff --git a/crowd/contracts/crowd.sol b/crowd/contracts/crowd.sol new file mode 100644 index 00000000..84a86bb1 --- /dev/null +++ b/crowd/contracts/crowd.sol @@ -0,0 +1,166 @@ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +contract CrowdFunding{ + + address private immutable owner; + + + uint private nextId; + bool private locked; + + modifier nonReentrant() { + require(!locked, "Reentrant call"); + locked = true; + _; + locked = false; + } + + struct Campaign { + uint id; + string title; + address creator; + uint goal; + uint deadline; + uint amountRaised; + STATUS status; + uint startsAt; + uint endsAt; + uint totalContributions; + bool claimed; + mapping(address => uint) contributions; + //uint[] contributionAmounts; + } + + enum STATUS { + ACTIVE, + DELETED, + SUCCESSFUL, + UNSUCCEEDED + } + + mapping(uint => Campaign) public campaigns; + uint public campaignCount; + + event CampaignCreated(uint indexed campaignId, address campaignCreator, string title, STATUS status); + event CampaignDeleted(uint indexed campaignId, address campaignCreator, STATUS status); + event ContributionMade(uint indexed campaignId, address contributor, uint amount); + event RefundMade(uint indexed campaignId, address contributor, uint amount); + + function createCampaign( + string memory _title, + uint _goal, + uint _duration)public { + require(bytes(_title).length > 0, "Title must not be empty"); + require(_goal > 0, "Goal must be greater than zero"); + require(_duration > 0, "Ends time must be greater than zero"); + + campaignCount++; + nextId++; + Campaign storage campaign = campaigns[campaignCount]; + campaign.id = nextId; + campaign.creator = msg.sender; + campaign.title =_title; + campaign.goal = _goal; + campaign.startsAt = block.timestamp; + campaign.status = STATUS.ACTIVE; + campaign.endsAt = block.timestamp + _duration; + + emit CampaignCreated(nextId , msg.sender, _title, STATUS.ACTIVE); + } + + + function contribute(uint _id) + public + payable + nonReentrant +{ + Campaign storage campaign = campaigns[_id]; + + require(msg.value > 0, "Must send ETH"); + require(block.timestamp < campaign.endsAt, "Campaign ended"); + require(campaign.status == STATUS.ACTIVE, "Not active"); + + uint remaining = campaign.goal - campaign.totalContributions; + + uint acceptedAmount = msg.value; + + if (msg.value > remaining) { + acceptedAmount = remaining; + + uint excess = msg.value - remaining; + + (bool success, ) = payable(msg.sender).call{value: excess}(""); + require(success, "Refund failed"); + } + + campaign.totalContributions += acceptedAmount; + campaign.contributions[msg.sender] += acceptedAmount; + + if (campaign.totalContributions >= campaign.goal) { + campaign.status = STATUS.SUCCESSFUL; + } + + emit ContributionMade(_id, msg.sender, acceptedAmount); +} + + function requestRefund(uint _id) + public + nonReentrant +{ + Campaign storage campaign = campaigns[_id]; + + + if ( + block.timestamp >= campaign.endsAt && + campaign.totalContributions < campaign.goal + ) { + campaign.status = STATUS.UNSUCCEEDED; + } + + require(campaign.status == STATUS.UNSUCCEEDED, "Refund not allowed"); + + uint contributedAmount = campaign.contributions[msg.sender]; + require(contributedAmount > 0, "No contribution found"); + + + campaign.contributions[msg.sender] = 0; + campaign.totalContributions -= contributedAmount; + + (bool success, ) = payable(msg.sender).call{value: contributedAmount}(""); + require(success, "Refund failed"); + + emit RefundMade(_id, msg.sender, contributedAmount); +} + + function withdraw(uint _id) + public + nonReentrant +{ + Campaign storage campaign = campaigns[_id]; + + + require(msg.sender == campaign.creator, "Not campaign creator"); + + + require(campaign.status == STATUS.SUCCESSFUL, "Campaign not successful"); + + + require(!campaign.claimed, "Funds already withdrawn"); + + uint amount = campaign.totalContributions; + require(amount > 0, "No funds to withdraw"); + + campaign.claimed = true; + + (bool success, ) = payable(msg.sender).call{value: amount}(""); + require(success, "Withdrawal failed"); + + emit RefundMade(_id, msg.sender, amount); +} + + + +} + diff --git a/crowd/hardhat.config.ts b/crowd/hardhat.config.ts new file mode 100644 index 00000000..7092b852 --- /dev/null +++ b/crowd/hardhat.config.ts @@ -0,0 +1,38 @@ +import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers"; +import { configVariable, defineConfig } from "hardhat/config"; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + solidity: { + profiles: { + default: { + version: "0.8.28", + }, + production: { + version: "0.8.28", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + }, + }, + networks: { + hardhatMainnet: { + type: "edr-simulated", + chainType: "l1", + }, + hardhatOp: { + type: "edr-simulated", + chainType: "op", + }, + sepolia: { + type: "http", + chainType: "l1", + url: configVariable("SEPOLIA_RPC_URL"), + accounts: [configVariable("SEPOLIA_PRIVATE_KEY")], + }, + }, +}); diff --git a/crowd/ignition/modules/Counter.ts b/crowd/ignition/modules/Counter.ts new file mode 100644 index 00000000..042e61c8 --- /dev/null +++ b/crowd/ignition/modules/Counter.ts @@ -0,0 +1,9 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("CounterModule", (m) => { + const counter = m.contract("Counter"); + + m.call(counter, "incBy", [5n]); + + return { counter }; +}); diff --git a/crowd/package.json b/crowd/package.json new file mode 100644 index 00000000..ababa8dc --- /dev/null +++ b/crowd/package.json @@ -0,0 +1,20 @@ +{ + "name": "crowd", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.4", + "@nomicfoundation/hardhat-ignition": "^3.0.7", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.11", + "chai": "^5.3.3", + "ethers": "^6.16.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "hardhat": "^3.1.8", + "mocha": "^11.7.5", + "typescript": "~5.8.0" + } +} diff --git a/crowd/scripts/send-op-tx.ts b/crowd/scripts/send-op-tx.ts new file mode 100644 index 00000000..c10a2360 --- /dev/null +++ b/crowd/scripts/send-op-tx.ts @@ -0,0 +1,22 @@ +import { network } from "hardhat"; + +const { ethers } = await network.connect({ + network: "hardhatOp", + chainType: "op", +}); + +console.log("Sending transaction using the OP chain type"); + +const [sender] = await ethers.getSigners(); + +console.log("Sending 1 wei from", sender.address, "to itself"); + +console.log("Sending L2 transaction"); +const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, +}); + +await tx.wait(); + +console.log("Transaction sent successfully"); diff --git a/crowd/test/Counter.ts b/crowd/test/Counter.ts new file mode 100644 index 00000000..f8c38986 --- /dev/null +++ b/crowd/test/Counter.ts @@ -0,0 +1,36 @@ +import { expect } from "chai"; +import { network } from "hardhat"; + +const { ethers } = await network.connect(); + +describe("Counter", function () { + it("Should emit the Increment event when calling the inc() function", async function () { + const counter = await ethers.deployContract("Counter"); + + await expect(counter.inc()).to.emit(counter, "Increment").withArgs(1n); + }); + + it("The sum of the Increment events should match the current value", async function () { + const counter = await ethers.deployContract("Counter"); + const deploymentBlockNumber = await ethers.provider.getBlockNumber(); + + // run a series of increments + for (let i = 1; i <= 10; i++) { + await counter.incBy(i); + } + + const events = await counter.queryFilter( + counter.filters.Increment(), + deploymentBlockNumber, + "latest", + ); + + // check that the aggregated events match the current value + let total = 0n; + for (const event of events) { + total += event.args.by; + } + + expect(await counter.x()).to.equal(total); + }); +}); diff --git a/crowd/tsconfig.json b/crowd/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/crowd/tsconfig.json @@ -0,0 +1,13 @@ +/* Based on https://github.com/tsconfig/bases/blob/501da2bcd640cf95c95805783e1012b992338f28/bases/node22.json */ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "outDir": "dist" + } +}