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 00000000..718d6fea Binary files /dev/null and b/assignments/7-block-explorer/block-explorer/app/favicon.ico differ 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"] +} 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" + } +} 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/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" + } +} 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" + } +}