Initial commit
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
.react-router
|
||||
build
|
||||
node_modules
|
||||
README.md
|
||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
.env
|
||||
/node_modules/
|
||||
|
||||
# React Router
|
||||
/.react-router/
|
||||
/build/
|
||||
|
||||
# Archives
|
||||
*.zip
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM node:24-alpine AS development-dependencies-env
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN npm ci
|
||||
|
||||
FROM node:24-alpine AS production-dependencies-env
|
||||
COPY ./package.json package-lock.json /app/
|
||||
WORKDIR /app
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
FROM node:24-alpine AS build-env
|
||||
COPY . /app/
|
||||
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||
WORKDIR /app
|
||||
RUN npm run build
|
||||
|
||||
FROM node:24-alpine
|
||||
COPY ./package.json package-lock.json /app/
|
||||
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||
COPY --from=build-env /app/build /app/build
|
||||
WORKDIR /app
|
||||
CMD ["npm", "run", "start"]
|
||||
22
README.md
Normal file
22
README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Lab 6
|
||||
|
||||
This is a Hardhat 3 and React project. To get started, please read the instructions below.
|
||||
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
To start the lab, run:
|
||||
|
||||
```shell
|
||||
npm run start
|
||||
```
|
||||
|
||||
|
||||
## Finishing up
|
||||
|
||||
When you're done, run the following command to generate a zip archive.
|
||||
|
||||
```shell
|
||||
npm run zip
|
||||
```
|
||||
23
app/app.css
Normal file
23
app/app.css
Normal file
@@ -0,0 +1,23 @@
|
||||
@import "tailwindcss";
|
||||
@import "react-toastify/dist/ReactToastify.min.css";
|
||||
|
||||
|
||||
table {
|
||||
@apply w-full text-left;
|
||||
}
|
||||
|
||||
table thead {
|
||||
@apply bg-slate-100;
|
||||
}
|
||||
|
||||
table tbody {
|
||||
@apply bg-white;
|
||||
}
|
||||
|
||||
table tbody tr {
|
||||
@apply border-b border-slate-200;
|
||||
}
|
||||
|
||||
table thead th, table tbody td {
|
||||
@apply px-4 py-2;
|
||||
}
|
||||
7
app/components/error/ErrorView.tsx
Normal file
7
app/components/error/ErrorView.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export function ErrorView() {
|
||||
return (
|
||||
<div>
|
||||
<p>Ups, an unexpected error occurred.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
app/components/layout/Footer.tsx
Normal file
13
app/components/layout/Footer.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="max-w-6xl mx-auto px-4 py-12 border-t border-slate-200 mt-12">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-slate-500">
|
||||
Copyright © {new Date().getFullYear()} Brainster Next College.
|
||||
All rights reserved.<br />
|
||||
Icons by <a href="https://icons8.com/" target="_blank" rel="noopener noreferrer">Icons8</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
33
app/components/layout/Header.tsx
Normal file
33
app/components/layout/Header.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Link } from "react-router";
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="bg-white border-b border-slate-200 sticky top-0 z-10">
|
||||
<div className="max-w-6xl mx-auto px-4 py-3 h-auto min-h-16 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-blue-600 p-2 rounded-lg shrink-0">
|
||||
<img
|
||||
src="https://img.icons8.com/ios-filled/50/ffffff/flash-on.png"
|
||||
className="w-5 h-5 object-contain"
|
||||
alt="logo"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-bold text-lg leading-tight">
|
||||
Distributed Systems and Blockchain
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500">Brainster Next College</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<p className="text-sm text-slate-500 hidden md:block">Chain: Next Testnet</p>
|
||||
|
||||
<Link to="/status" className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-xl text-sm font-semibold transition-all shadow-sm">
|
||||
Connect Wallet
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
39
app/components/layout/Sidebar.tsx
Normal file
39
app/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Link, useLocation } from "react-router";
|
||||
|
||||
const navItems = [
|
||||
{ route: "/", label: "Explorer", icon: "home" },
|
||||
{ route: "/status", label: "Status", icon: "notepad" }
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<aside className="lg:col-span-3">
|
||||
<nav className="space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.route}
|
||||
to={item.route}
|
||||
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-colors ${
|
||||
location.pathname === item.route
|
||||
? "bg-blue-50 text-blue-700 border border-blue-100"
|
||||
: "text-slate-600 hover:bg-slate-100"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
location.pathname === item.route
|
||||
? `https://img.icons8.com/ios-filled/50/2563eb/${item.icon}.png`
|
||||
: `https://img.icons8.com/ios/50/64748b/${item.icon}.png`
|
||||
}
|
||||
className="w-5 h-5 shrink-0 object-contain"
|
||||
alt={item.label}
|
||||
/>
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
7
app/components/loading/LoadingView.tsx
Normal file
7
app/components/loading/LoadingView.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export function LoadingView() {
|
||||
return (
|
||||
<div className="flex justify-center items-center">
|
||||
<p className="sr-only">Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
app/config/wagmi.ts
Normal file
25
app/config/wagmi.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createConfig, http } from "wagmi";
|
||||
import { metaMask } from "wagmi/connectors";
|
||||
|
||||
import { type Chain } from "viem";
|
||||
|
||||
export const nextChain = {
|
||||
id: 1337,
|
||||
name: "Next",
|
||||
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
||||
rpcUrls: {
|
||||
default: { http: ["https://eth.code-camp.org"] }
|
||||
},
|
||||
blockExplorers: {
|
||||
default: { name: "Otterscan", url: "https://etherscan.code-camp.org" }
|
||||
},
|
||||
testnet: true
|
||||
} as const satisfies Chain;
|
||||
|
||||
export const config = createConfig({
|
||||
chains: [nextChain],
|
||||
transports: {
|
||||
[nextChain.id]: http()
|
||||
},
|
||||
connectors: [metaMask()]
|
||||
});
|
||||
7
app/lib/fetch.ts
Normal file
7
app/lib/fetch.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const fetchApi = async <T = any>(url: string): Promise<T> => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
7
app/pages/home/HomePage.tsx
Normal file
7
app/pages/home/HomePage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export function HomePage() {
|
||||
return (
|
||||
<div>
|
||||
<p>Hello Wagmi!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
79
app/pages/status/StatusPage.tsx
Normal file
79
app/pages/status/StatusPage.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { useEffect } from "react";
|
||||
import { formatEther, type Address } from "viem";
|
||||
import { useBalance, useBlockNumber, useConnect, useConnection, useConnectors, useDisconnect } from "wagmi";
|
||||
|
||||
function WalletDetails({ address }: { address: Address }) {
|
||||
const { data: blockNumber } = useBlockNumber({ watch: true });
|
||||
const { data: balance, isPending, error, refetch: refetchBalance } = useBalance({ address });
|
||||
const { mutate: disconnect } = useDisconnect();
|
||||
|
||||
useEffect(() => {
|
||||
refetchBalance();
|
||||
}, [blockNumber]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<h1 className="text-lg font-bold">You are connected</h1>
|
||||
|
||||
<p className="break-all">Your address is: {address}</p>
|
||||
|
||||
{isPending && <p>Loading balance...</p>}
|
||||
{error && <p>An error occurred.</p>}
|
||||
|
||||
{blockNumber && <p>Block number: {blockNumber.toLocaleString()}</p>}
|
||||
{balance && <p>Balance: {formatEther(balance.value)} ETH</p>}
|
||||
|
||||
<div className="flex flex-row">
|
||||
<button
|
||||
className="bg-black text-white py-2 px-6 rounded-xl cursor-pointer"
|
||||
onClick={() => disconnect()}
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
function ConnectWallet() {
|
||||
const connectors = useConnectors();
|
||||
const { mutate: connect } = useConnect();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<h1 className="text-lg font-bold">Connect your wallet</h1>
|
||||
|
||||
<p>Choose an option from the list below:</p>
|
||||
<div className="flex flex-row gap-6">
|
||||
{connectors.map(connector => (
|
||||
<button
|
||||
key={connector.id}
|
||||
onClick={() => connect({ connector })}
|
||||
className="bg-black text-white px-6 py-2 rounded-lg cursor-pointer"
|
||||
>
|
||||
Connect {connector.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function StatusPage() {
|
||||
const { address, isConnected } = useConnection();
|
||||
|
||||
return (
|
||||
<div className="bg-white p-5 border border-slate-200 shadow-sm rounded-xl flex flex-col gap-6">
|
||||
{isConnected && address ? (
|
||||
<WalletDetails address={address} />
|
||||
) : (
|
||||
<ConnectWallet />
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
app/root.tsx
Normal file
103
app/root.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration
|
||||
} from "react-router";
|
||||
|
||||
import { ToastContainer } from "react-toastify";
|
||||
|
||||
import { WagmiProvider } from "wagmi";
|
||||
import { config } from "./config/wagmi";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
import type { Route } from "./+types/root";
|
||||
import "./app.css";
|
||||
import { Header } from "./components/layout/Header";
|
||||
import { Footer } from "./components/layout/Footer";
|
||||
import { Sidebar } from "./components/layout/Sidebar";
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
{
|
||||
rel: "preconnect",
|
||||
href: "https://fonts.gstatic.com",
|
||||
crossOrigin: "anonymous"
|
||||
},
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
|
||||
}
|
||||
];
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="bg-slate-50 text-slate-900 font-sans">
|
||||
<div>
|
||||
<Header />
|
||||
<main className="max-w-6xl mx-auto px-4 py-8 min-h-[calc(100vh-15rem)]">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
<Sidebar />
|
||||
|
||||
<div className="lg:col-span-9 space-y-6">{children}</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<ToastContainer position="top-center" autoClose={5000} theme="colored" />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<WagmiProvider config={config}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Outlet />
|
||||
</QueryClientProvider>
|
||||
</WagmiProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
let message = "Oops!";
|
||||
let details = "An unexpected error occurred.";
|
||||
let stack: string | undefined;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? "404" : "Error";
|
||||
details =
|
||||
error.status === 404
|
||||
? "The requested page could not be found."
|
||||
: error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="pt-16 p-4 container mx-auto">
|
||||
<h1>{message}</h1>
|
||||
<p>{details}</p>
|
||||
{stack && (
|
||||
<pre className="w-full p-4 overflow-x-auto">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
6
app/routes.ts
Normal file
6
app/routes.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
index("routes/home.tsx"),
|
||||
route("status", "routes/status.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
12
app/routes/home.tsx
Normal file
12
app/routes/home.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { HomePage } from "../pages/home/HomePage";
|
||||
|
||||
export function meta() {
|
||||
return [
|
||||
{ title: "Wagmi Starter" },
|
||||
{ name: "description", content: "Welcome to Wagmi!" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return <HomePage />;
|
||||
}
|
||||
6
app/routes/status.tsx
Normal file
6
app/routes/status.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { StatusPage } from "../pages/status/StatusPage";
|
||||
|
||||
export default function Status() {
|
||||
return <StatusPage />;
|
||||
}
|
||||
|
||||
55
exam/common.js
Normal file
55
exam/common.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { exec, execSync } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
export function installDeps() {
|
||||
let parentDirectory = process.cwd();
|
||||
if (parentDirectory.includes('exam') && !fs.existsSync(path.join(parentDirectory, 'exam'))) {
|
||||
parentDirectory = path.join(parentDirectory, '..');
|
||||
}
|
||||
|
||||
const examDirectory = path.join(parentDirectory, 'exam');
|
||||
|
||||
// install starter/exam dependencies
|
||||
if (!fs.existsSync(path.join(examDirectory, 'node_modules'))) {
|
||||
console.log('Installing starter dependencies...');
|
||||
execSync('npm install', { stdio: 'inherit', cwd: examDirectory });
|
||||
console.log('Starter dependencies installed');
|
||||
}
|
||||
|
||||
// install dev dependencies
|
||||
if (!fs.existsSync(path.join(parentDirectory, 'node_modules'))) {
|
||||
// install dev dependencies (in the background)
|
||||
console.log(); console.log(); console.log();
|
||||
console.log('Installing dev dependencies...');
|
||||
installDevDeps(parentDirectory);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.join(parentDirectory, 'hardhat', 'node_modules'))) {
|
||||
// install hardhat dependencies (in the background)
|
||||
console.log(); console.log(); console.log();
|
||||
console.log('Installing hardhat dependencies...');
|
||||
installDevDeps(path.join(parentDirectory, 'hardhat'));
|
||||
}
|
||||
}
|
||||
|
||||
async function installDevDeps(workingDirectory, attempt = 1) {
|
||||
try {
|
||||
await execPromise('npm install', { stdio: 'inherit', cwd: workingDirectory });
|
||||
console.log('Dev dependencies installed');
|
||||
} catch (error) {
|
||||
console.error('Error installing dev dependencies:', error.message);
|
||||
|
||||
if (attempt < 3) {
|
||||
const retryDelay = 5000 * attempt;
|
||||
console.log(`Retrying in ${Math.round(retryDelay / 1000)} seconds...`);
|
||||
setTimeout(() => installDevDeps(workingDirectory, attempt + 1), retryDelay);
|
||||
} else {
|
||||
console.error('Failed to install dev dependencies.');
|
||||
console.error('Please try again manually, by running "npm install" in the project directory.');
|
||||
}
|
||||
}
|
||||
}
|
||||
58
exam/exam.js
Normal file
58
exam/exam.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { installDeps } from './common.js';
|
||||
|
||||
const SERVER_PORT = 3018;
|
||||
|
||||
async function main() {
|
||||
installDeps();
|
||||
|
||||
let parentDirectory = process.cwd();
|
||||
if (parentDirectory.includes('exam') && !fs.existsSync(path.join(parentDirectory, 'exam'))) {
|
||||
parentDirectory = path.join(parentDirectory, '..');
|
||||
}
|
||||
|
||||
const indexHtml = fs.readFileSync(path.join(parentDirectory, 'exam', 'index.html'), 'utf8');
|
||||
const examTheory = fs.readFileSync(path.join(parentDirectory, 'exam', 'theory.json'), 'utf8');
|
||||
let examTheoryData = JSON.parse(examTheory);
|
||||
|
||||
// import express and start the server
|
||||
const express = await import('express');
|
||||
const app = express.default();
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Pragma', 'no-cache');
|
||||
res.setHeader('Expires', '0');
|
||||
res.send(indexHtml);
|
||||
});
|
||||
|
||||
app.get('/theory', (req, res) => {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.json(examTheoryData);
|
||||
});
|
||||
|
||||
app.post('/theory-answer', (req, res) => {
|
||||
const questionId = req.body.questionId;
|
||||
const answer = req.body.answer;
|
||||
const index = examTheoryData.findIndex(q => q.id === questionId);
|
||||
if (index !== -1) {
|
||||
examTheoryData[index].answer = answer;
|
||||
fs.writeFileSync(path.join(parentDirectory, 'exam', 'theory.json'), JSON.stringify(examTheoryData, null, 2));
|
||||
res.status(200).json({ success: true });
|
||||
} else {
|
||||
res.status(404).json({ success: false, error: 'Question not found' });
|
||||
}
|
||||
});
|
||||
|
||||
const open = await import('open');
|
||||
app.listen(SERVER_PORT, () => {
|
||||
console.log(`Server is running on port ${SERVER_PORT}`);
|
||||
open.default(`http://localhost:${SERVER_PORT}/`);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
417
exam/index.html
Normal file
417
exam/index.html
Normal file
@@ -0,0 +1,417 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
|
||||
>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.colors.min.css"
|
||||
>
|
||||
<style>
|
||||
[data-theme="dark"] {
|
||||
--pico-background-color: #282a36; /* Custom color */
|
||||
}
|
||||
|
||||
.app-header {
|
||||
border-bottom: 1px solid var(--pico-muted-border-color);
|
||||
box-shadow: 0 1px 0 var(--pico-muted-border-color);
|
||||
}
|
||||
|
||||
[data-theme="light"] .app-header {
|
||||
background-color: #fcfcfc;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .app-header {
|
||||
background-color: rgba(9, 21, 21, 0.6);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sidebar {
|
||||
border-right: 3px solid rgba(9, 21, 21, 0.6);
|
||||
}
|
||||
|
||||
.app-header nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.app-header hgroup {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-header #theme-icon {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.app-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.app-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sidebar {
|
||||
width: 315px;
|
||||
flex-shrink: 0;
|
||||
padding: 1.5rem;
|
||||
border-right: 1px solid var(--pico-muted-border-color);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.sidebar nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.sidebar .sidebar-button {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem 0.75rem 0;
|
||||
border-bottom: 1px solid var(--pico-muted-border-color);
|
||||
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] hgroup>:not(:first-child):last-child {
|
||||
--pico-color: revert;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.main-content section.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.main-content section p.mt-3 {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.main-content section h3.mt-3 {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.main-content section .theory-question:first-of-type {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.main-content section .theory-question {
|
||||
border-top: 1px solid var(--pico-muted-border-color);
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.main-content section .theory-question p.question {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.main-content section .theory-question p.extra-details {
|
||||
margin-bottom: 1.5rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.main-content section hgroup p {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
/* Mobile: hide sidebar, show desktop-only message */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
.main-content {
|
||||
display: none;
|
||||
}
|
||||
.mobile-message {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
.mobile-message {
|
||||
display: none;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.mobile-message article {
|
||||
max-width: 28rem;
|
||||
}
|
||||
</style>
|
||||
<title>Lab 6</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-root">
|
||||
<header class="app-header">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<hgroup>
|
||||
<h2>Lab 6</h2>
|
||||
<p>Distributed Systems and Blockchain</p>
|
||||
</hgroup>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<details class="dropdown">
|
||||
<summary>
|
||||
Documentation
|
||||
</summary>
|
||||
<ul>
|
||||
<li><a href="https://api.next.code-camp.org/cdn/viem-docs.pdf" target="_blank">Viem</a></li>
|
||||
<li><a href="https://api.next.code-camp.org/cdn/solidity-docs.pdf" target="_blank">Solidity</a></li>
|
||||
<li><a href="https://api.next.code-camp.org/cdn/crowdfunding-contract.html" target="_blank">Contract example</a></li>
|
||||
<li><a href="https://api.next.code-camp.org/cdn/crowdfunding-tests.html" target="_blank">Tests example</a></li>
|
||||
<li><a href="https://gitea.next.code-camp.org/dsb/next-coins" target="_blank">React example app</a></li>
|
||||
<li><a href="https://gitea.next.code-camp.org/dsb/wagmi-lesson" target="_blank">Wagmi example app</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span id="theme-icon" aria-hidden="true" onclick="cycleTheme()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 32 32" fill="currentColor" class="icon-theme-toggle "><clipPath id="theme-toggle-cutout"><path d="M0-11h25a1 1 0 0017 13v30H0Z"></path></clipPath><g clip-path="url(#theme-toggle-cutout)"><circle cx="16" cy="16" r="8.4"></circle><path d="M18.3 3.2c0 1.3-1 2.3-2.3 2.3s-2.3-1-2.3-2.3S14.7.9 16 .9s2.3 1 2.3 2.3zm-4.6 25.6c0-1.3 1-2.3 2.3-2.3s2.3 1 2.3 2.3-1 2.3-2.3 2.3-2.3-1-2.3-2.3zm15.1-10.5c-1.3 0-2.3-1-2.3-2.3s1-2.3 2.3-2.3 2.3 1 2.3 2.3-1 2.3-2.3 2.3zM3.2 13.7c1.3 0 2.3 1 2.3 2.3s-1 2.3-2.3 2.3S.9 17.3.9 16s1-2.3 2.3-2.3zm5.8-7C9 7.9 7.9 9 6.7 9S4.4 8 4.4 6.7s1-2.3 2.3-2.3S9 5.4 9 6.7zm16.3 21c-1.3 0-2.3-1-2.3-2.3s1-2.3 2.3-2.3 2.3 1 2.3 2.3-1 2.3-2.3 2.3zm2.4-21c0 1.3-1 2.3-2.3 2.3S23 7.9 23 6.7s1-2.3 2.3-2.3 2.4 1 2.4 2.3zM6.7 23C8 23 9 24 9 25.3s-1 2.3-2.3 2.3-2.3-1-2.3-2.3 1-2.3 2.3-2.3z"></path></g></svg>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="app-body">
|
||||
<aside class="sidebar">
|
||||
<nav>
|
||||
<a href="#theory" class="sidebar-button">Questions</a>
|
||||
<a href="#contract" class="sidebar-button">Coding - Solidity</a>
|
||||
<a href="#react" class="sidebar-button">Coding - Wagmi</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="main-content container" id="main-content">
|
||||
<section id="theory">
|
||||
<hgroup>
|
||||
<h2>Theory</h2>
|
||||
<p>Answer the following questions to the best of your ability. Each question has exactly one correct answer. There are no negative marks for incorrect answers.</p>
|
||||
</hgroup>
|
||||
|
||||
|
||||
<div id="theory-questions"></div>
|
||||
</section>
|
||||
|
||||
|
||||
<section id="contract" class="hidden">
|
||||
<h2>Coding - Solidity: Powers of Two</h2>
|
||||
|
||||
<p>The repository you downloaded contains a Hardhat starter project. Open the file in hardhat/contracts/PowersOfTwo.sol (that's where you will write your solution for this task). The file already has the boilerplate code written for you.</p>
|
||||
|
||||
<p>Your job is to write a contract for playing a game called "Powers of Two". The user should guess successive powers of two (2, 4, 8, 16, 32, etc), and in each step, if they answer correctly they get an extra 1 point, and if they answer incorrectly they lose a point (but if they already have 0 points, they can't go negative, i.e. the score will still remain 0 for that user). The contract should use a mapping, so that multiple addresses can play individually (i.e. we should keep track of each address's points and last correct guess).</p>
|
||||
|
||||
<p>For example, if we know the last correct guess for address X was the number 8 and that they have 3 points, then if they answer 16 we can save that number as their last correct guess and increase their points to 4. We keep those details for everyone that plays our game in the same contract.</p>
|
||||
|
||||
<p>To be more specific, the contract should have the following functions:</p>
|
||||
<ul>
|
||||
<li>points(addr) - which should return the number of points for the account with address <strong>addr</strong></li>
|
||||
<li>last(addr) - which should return the last correctly guessed power of two by address <strong>addr</strong> (it should start with 1, if the user hasn't guessed anything yet)</li>
|
||||
<li>answer(num) - for sending a number, which is the user's guess for the next power of two (this function should also update the points we store for the user)</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p class="mt-3">You can build the contract by going into the hardhat directory, and running the following command:</p>
|
||||
<pre><code>npx hardhat build</code></pre>
|
||||
|
||||
<p class="mt-3">Hint: you have an example contract (see Documentation links on the top) to help you get started.</p>
|
||||
</section>
|
||||
|
||||
|
||||
<section id="react" class="hidden">
|
||||
<h2>Coding - React: Powers of Two</h2>
|
||||
|
||||
<p>The repository you downloaded contains a React+Wagmi starter project. You can start the development server by running <strong>npm run dev</strong>. Your job is to create a simple page that shows the balance of the connected wallet account, and use a contract called "Powers of Two" which allows the account to play a simple game. The user interface should show the number of points, the last power of two provided correctly by the user, and to allow the user to submit an answer for the next power of two. A designer on our team has already created the design for the web app. Your job is to create the frontend in React.</p>
|
||||
|
||||
<h3 class="mt-3">The page (<a href="https://api.next.code-camp.org/cdn/next-powers-of-two/index.html" target="_blank" style="text-decoration: none;">design</a>)</h3>
|
||||
<p>The page should retrieve the necessary information from the blockchain, including the wallet balance in the first card, the number of points in the second card, and the last correct number (which is shown in the last card as "X*2="). It should also enable the user to answer the question for the next power of two (the answer to the question X*2). If the wallet is not connected, the page should just redirect the user to the Status tab, so the user can connect their wallet.</p>
|
||||
<p>Keep in mind that you shouldn't recreate the whole design, as the layout is already part of the starter template (this includes the header, the footer, and the sidebar). You should only add the missing component(s) from the main section (in the HTML source code, this will be available under the "main-content-container" element). You can create the page in "pages/home".</p>
|
||||
|
||||
<p class="mt-3">If you solved the previous task by writing a valid contract in Solidity, you can use that contract for testing. However, it's recommended that you use the contract deployed at address 0x5B495d0e92A8bC50b72D8502c5EfDaBea104E4a0 instead (since it's already verified and tested), and to use the ABI available below (which you can save in a file and use it by importing it whenever you need to access the contract functionality).</p>
|
||||
|
||||
<p>The contract has the following functions:</p>
|
||||
<ul>
|
||||
<li>points(addr) - which returns the number of points for the account with address <strong>addr</strong></li>
|
||||
<li>last(addr) - which returns the last correctly guessed power of two by address <strong>addr</strong> (and starts with 1)</li>
|
||||
<li>answer(num) - for sending a number, which is the guess for the next power of two</li>
|
||||
</ul>
|
||||
|
||||
<p class="mt-3">Important links:</p>
|
||||
<ul>
|
||||
<li><a href="https://api.next.code-camp.org/cdn/next-powers-of-two/index.html" target="_blank">The page design</a></li>
|
||||
<li><a href="https://api.next.code-camp.org/cdn/next-powers-of-two/abi.html" target="_blank">The ABI</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="mobile-message">
|
||||
<article>
|
||||
<header>
|
||||
<strong>Desktop required</strong>
|
||||
</header>
|
||||
<p>This exam layout needs a larger screen. Please open it on a desktop computer.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
function setupTheme() {
|
||||
var THEME_KEY = 'dsb-exam-theme';
|
||||
var html = document.documentElement;
|
||||
var icon = document.getElementById('theme-icon');
|
||||
|
||||
function applyTheme(theme) {
|
||||
html.setAttribute('data-theme', theme);
|
||||
try { localStorage.setItem(THEME_KEY, theme); } catch (e) {}
|
||||
}
|
||||
|
||||
function cycleTheme(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
var next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
applyTheme(next);
|
||||
}
|
||||
|
||||
try {
|
||||
var saved = localStorage.getItem(THEME_KEY);
|
||||
if (saved === 'light' || saved === 'dark') applyTheme(saved);
|
||||
} catch (e) {}
|
||||
|
||||
if (icon) icon.addEventListener('click', cycleTheme);
|
||||
}
|
||||
|
||||
function setupSidebar() {
|
||||
var sidebar = document.querySelector('.sidebar');
|
||||
var sidebarButtons = sidebar.querySelectorAll('.sidebar-button');
|
||||
sidebarButtons.forEach(function(button) {
|
||||
button.addEventListener('click', function() {
|
||||
var target = button.getAttribute('href');
|
||||
var targetSection = document.querySelector(target);
|
||||
|
||||
var sections = document.querySelectorAll('.main-content section');
|
||||
|
||||
if (targetSection) {
|
||||
document.body.style.opacity = 0;
|
||||
sections.forEach(function(section) {
|
||||
section.classList.add('hidden');
|
||||
});
|
||||
|
||||
targetSection.classList.remove('hidden');
|
||||
setTimeout(function() {
|
||||
window.scrollTo(0, 0);
|
||||
document.body.style.opacity = 1;
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupTheory() {
|
||||
|
||||
function setupTheoryCheckboxes() {
|
||||
var theoryCheckboxes = document.querySelectorAll('.theory-question input[type="checkbox"]');
|
||||
theoryCheckboxes.forEach(function(checkbox) {
|
||||
checkbox.addEventListener('change', function() {
|
||||
var question = checkbox.closest('.theory-question');
|
||||
var questionId = question.getAttribute('id').split('-')[1];
|
||||
var answer = checkbox.getAttribute('name').split('-')[2];
|
||||
var otherCheckboxes = question.querySelectorAll('input[type="checkbox"]');
|
||||
otherCheckboxes.forEach(function(otherCheckbox) {
|
||||
const otherAnswer = otherCheckbox.getAttribute('name').split('-')[2];
|
||||
if (otherAnswer !== answer) {
|
||||
otherCheckbox.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
checkbox.checked = true;
|
||||
fetch('/theory-answer', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
questionId: Number(questionId),
|
||||
answer: answer
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(function(response) {
|
||||
if (!response.ok) {
|
||||
alert('Failed to submit answer.');
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
alert('Failed to submit answer.');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
fetch('/theory')
|
||||
.then(response => response.json())
|
||||
.then(function(data) {
|
||||
var theory = document.querySelector('#theory');
|
||||
|
||||
var html = '';
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var questionId = data[i].id;
|
||||
var question = data[i].question;
|
||||
var details = data[i].details;
|
||||
var options = data[i].options;
|
||||
var answer = data[i].answer;
|
||||
|
||||
html += '<div class="theory-question" id="question-' + questionId + '">';
|
||||
html += '<p class="question">' + (i + 1) + '. ' + question + "</p>";
|
||||
if (details) {
|
||||
html += '<p class="extra-details">' + details + "</p>";
|
||||
}
|
||||
const sortedOptionKeys = Object.keys(options).sort();
|
||||
|
||||
html += '<fieldset>';
|
||||
sortedOptionKeys.forEach(function(optionKey) {
|
||||
html += '<label>';
|
||||
html += '<input type="checkbox" name="question-' + questionId + '-' + optionKey + '"' + (answer === optionKey ? ' checked' : '') +'>';
|
||||
html += ' ' + options[optionKey];
|
||||
html += '</label>';
|
||||
});
|
||||
html += '</fieldset>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
document.querySelector('#theory-questions').innerHTML = html;
|
||||
setupTheoryCheckboxes();
|
||||
});
|
||||
}
|
||||
|
||||
setupTheme();
|
||||
setupSidebar();
|
||||
setupTheory();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
17
exam/install-deps.js
Normal file
17
exam/install-deps.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import fs from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
|
||||
const parentDirectory = process.cwd();
|
||||
if (parentDirectory.includes('exam') && !fs.existsSync(path.join(parentDirectory, 'exam'))) {
|
||||
parentDirectory = path.join(parentDirectory, '..');
|
||||
}
|
||||
|
||||
const examDirectory = path.join(parentDirectory, 'exam');
|
||||
|
||||
if (!fs.existsSync(path.join(examDirectory, 'node_modules'))) {
|
||||
console.log('Exam directory node_modules not found. Installing...');
|
||||
execSync('npm install', { stdio: 'inherit', cwd: examDirectory });
|
||||
} else {
|
||||
console.log('Exam directory node_modules already exists. Skipping.');
|
||||
}
|
||||
2068
exam/package-lock.json
generated
Normal file
2068
exam/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
exam/package.json
Normal file
18
exam/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "dsb-lab6-exam",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "echo 'Run this command in the parent directory...'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.19.11",
|
||||
"@types/prompts": "^2.4.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"express": "^5.2.1",
|
||||
"open": "^11.0.0",
|
||||
"prompts": "^2.4.2"
|
||||
}
|
||||
}
|
||||
122
exam/theory.json
Normal file
122
exam/theory.json
Normal file
@@ -0,0 +1,122 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"question": "What is horizontal scaling?",
|
||||
"options": {
|
||||
"A": "Adding more CPU and RAM to a single machine",
|
||||
"B": "Replacing hardware with faster SSDs",
|
||||
"C": "Adding more machines to improve performance",
|
||||
"D": "Upgrading the operating system"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"question": "Which statement about distributed vs decentralized systems is correct?",
|
||||
"options": {
|
||||
"A": "Every distributed system is decentralized",
|
||||
"B": "A decentralized system must be distributed",
|
||||
"C": "A distributed system must have no central authority",
|
||||
"D": "They are identical concepts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"question": "In a CP (Consistency + Partition tolerance) system, what happens during a network partition?",
|
||||
"options": {
|
||||
"A": "The system sacrifices Consistency to stay online",
|
||||
"B": "The system ignores the partition and continues normally",
|
||||
"C": "The system shuts down entirely",
|
||||
"D": "The system sacrifices Availability to ensure data accuracy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"question": "If gas runs out during the execution of a transaction:",
|
||||
"options": {
|
||||
"A": "Execution stops and state changes from the current call revert",
|
||||
"B": "Only part of the smart contract storage updates",
|
||||
"C": "Changes to state are partially applied",
|
||||
"D": "Transaction becomes free"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"question": "A mnemonic (seed phrase) allows a user to:",
|
||||
"options": {
|
||||
"A": "Mine faster",
|
||||
"B": "Recover private keys",
|
||||
"C": "Increase gas fees",
|
||||
"D": "Modify blockchain rules"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"question": "What is the role of the constructor() in a Solidity contract?",
|
||||
"options": {
|
||||
"A": "It is called every time a function is executed",
|
||||
"B": "It is used to delete the contract from the blockchain",
|
||||
"C": "Its code is run only once when the contract is created",
|
||||
"D": "It is used to calculate the gas price of the contract"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"question": "If someone has your private key, they:",
|
||||
"options": {
|
||||
"A": "Can view your transactions only",
|
||||
"B": "Cannot do anything without your password",
|
||||
"C": "Have total control over your funds",
|
||||
"D": "Cannot do anything without the mnemonics"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"question": "How does Raft differ from basic Paxos in its approach to reaching consensus?",
|
||||
"options": {
|
||||
"A": "Raft was designed to work via a single elected leader",
|
||||
"B": "Raft is used for strong consistency, while Paxos is for eventual consistency",
|
||||
"C": "Raft does not require a majority of nodes to agree",
|
||||
"D": "Raft is mathematically impossible to implement"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"question": "What is the main difference between props and state in React?",
|
||||
"options": {
|
||||
"A": "Props are internal to a component; state is passed from parents",
|
||||
"B": "Props are handled by the browser; state is handled by the server",
|
||||
"C": "Props are passed from above; state is managed within the component",
|
||||
"D": "Props are only for strings; state is only for numbers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"question": "Why should you use className instead of class in JSX?",
|
||||
"options": {
|
||||
"A": "class is a reserved word in JavaScript",
|
||||
"B": "className is faster to process",
|
||||
"C": "className allows for more CSS features",
|
||||
"D": "class only works with class-based components"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"question": "Which of the following is a rule of React hooks?",
|
||||
"options": {
|
||||
"A": "Hooks must be called inside loops",
|
||||
"B": "Hooks should only be called at the top level of a component",
|
||||
"C": "Hooks can be used inside regular JavaScript functions",
|
||||
"D": "Hooks must always be defined inside a try-catch block"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"question": "What is sharding in distributed databases?",
|
||||
"options": {
|
||||
"A": "Data encryption",
|
||||
"B": "Backup process",
|
||||
"C": "Data duplication",
|
||||
"D": "Splitting data across machines"
|
||||
}
|
||||
}
|
||||
]
|
||||
79
exam/zip.js
Normal file
79
exam/zip.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { installDeps } from './common.js';
|
||||
import packageJson from '../package.json' with { type: 'json' };
|
||||
|
||||
async function main() {
|
||||
installDeps();
|
||||
|
||||
const prompts = await import('prompts');
|
||||
console.log(`Lets create a zip archive for your submission.`);
|
||||
const { studentId } = await prompts.default({
|
||||
type: 'text',
|
||||
name: 'studentId',
|
||||
message: 'What is your student ID?',
|
||||
format: (value) => value.trim().replace(/[^A-Za-z0-9]/g, ''),
|
||||
});
|
||||
|
||||
if (!studentId) {
|
||||
console.error('Student ID is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let parentDirectory = process.cwd();
|
||||
if (parentDirectory.includes('exam') && !fs.existsSync(path.join(parentDirectory, 'exam'))) {
|
||||
parentDirectory = path.join(parentDirectory, '..');
|
||||
}
|
||||
|
||||
const archiveName = `${packageJson.name}-${studentId}.zip`;
|
||||
const zipPath = path.join(parentDirectory, archiveName);
|
||||
|
||||
if (fs.existsSync(zipPath)) {
|
||||
console.log(`Removing existing archive at ${zipPath}`);
|
||||
fs.unlinkSync(zipPath);
|
||||
}
|
||||
|
||||
const archiver = await import('archiver');
|
||||
const archive = archiver.default('zip');
|
||||
archive.on('error', (err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
archive.glob('**/*', {
|
||||
cwd: parentDirectory,
|
||||
ignore: [
|
||||
'node_modules/**',
|
||||
'exam/node_modules/**',
|
||||
'hardhat/node_modules/**',
|
||||
'.git/**',
|
||||
'.react-router/**',
|
||||
'hardhat/artifacts/**',
|
||||
'hardhat/ignition/deployments/**',
|
||||
'hardhat/build/**',
|
||||
'hardhat/cache/**',
|
||||
'build/**',
|
||||
'dist/**',
|
||||
'.env',
|
||||
'hardhat/.env',
|
||||
'hardhat/install-deps.js',
|
||||
'.DS_Store',
|
||||
'*.zip',
|
||||
'exam/common.js',
|
||||
'exam/exam.js',
|
||||
'exam/install-deps.js',
|
||||
'exam/index.html',
|
||||
'exam/zip.js',
|
||||
'package-lock.json',
|
||||
'exam/package-lock.json',
|
||||
'hardhat/package-lock.json'
|
||||
]
|
||||
|
||||
});
|
||||
archive.pipe(fs.createWriteStream(zipPath));
|
||||
await archive.finalize();
|
||||
|
||||
console.log();
|
||||
console.log(`Archive created: ${zipPath}`);
|
||||
}
|
||||
|
||||
main();
|
||||
26
hardhat/.gitignore
vendored
Normal file
26
hardhat/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Node modules
|
||||
/node_modules
|
||||
|
||||
# Compilation output
|
||||
/dist
|
||||
|
||||
# pnpm deploy output
|
||||
/bundle
|
||||
|
||||
# Hardhat Build Artifacts
|
||||
/artifacts
|
||||
|
||||
# Deployments by students
|
||||
/ignition/deployments
|
||||
|
||||
# Hardhat compilation (v2) support directory
|
||||
/cache
|
||||
|
||||
# Typechain output
|
||||
/types
|
||||
|
||||
# Hardhat coverage reports
|
||||
/coverage
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
41
hardhat/README.md
Normal file
41
hardhat/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Hardhat 3 Project
|
||||
|
||||
This is a Hardhat 3 project, which uses the native Node.js test runner (`node:test`) and the `viem` library for Ethereum interactions.
|
||||
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
To build, run:
|
||||
|
||||
```shell
|
||||
npx hardhat build
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Running Tests
|
||||
|
||||
To run all the tests in the project, execute the following command:
|
||||
|
||||
```shell
|
||||
npx hardhat test
|
||||
```
|
||||
|
||||
You can also selectively run the `node:test` tests:
|
||||
|
||||
```shell
|
||||
npx hardhat test nodejs
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Make a deployment to Next Testnet
|
||||
|
||||
This project includes an example Ignition module to deploy a contract to our testnet.
|
||||
|
||||
To run the deployment to Next Testnet, set your mnemonics in an .env file (MNEMONICS=...) and run:
|
||||
|
||||
```shell
|
||||
npx hardhat ignition deploy --network next ignition/modules/PowersOfTwo.ts
|
||||
```
|
||||
16
hardhat/chains/next.ts
Normal file
16
hardhat/chains/next.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineChain } from 'viem';
|
||||
|
||||
export const nextChain = defineChain({
|
||||
id: 1337,
|
||||
name: 'Next',
|
||||
nativeCurrency: {
|
||||
name: 'Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
rpcUrls: {
|
||||
default: {
|
||||
http: ['https://eth.code-camp.org']
|
||||
}
|
||||
}
|
||||
});
|
||||
7
hardhat/contracts/PowersOfTwo.sol
Normal file
7
hardhat/contracts/PowersOfTwo.sol
Normal file
@@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.28;
|
||||
|
||||
contract PowersOfTwo {
|
||||
|
||||
// your code goes here...
|
||||
}
|
||||
49
hardhat/hardhat.config.ts
Normal file
49
hardhat/hardhat.config.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import hardhatToolboxViemPlugin from "@nomicfoundation/hardhat-toolbox-viem";
|
||||
import { configVariable, defineConfig } from "hardhat/config";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [hardhatToolboxViemPlugin],
|
||||
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")],
|
||||
},
|
||||
next: {
|
||||
type: "http",
|
||||
chainId: 1337,
|
||||
chainType: "l1",
|
||||
url: "https://eth.code-camp.org",
|
||||
accounts: {
|
||||
mnemonic: configVariable("MNEMONICS"),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
6
hardhat/ignition/modules/PowersOfTwo.ts
Normal file
6
hardhat/ignition/modules/PowersOfTwo.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
|
||||
|
||||
export default buildModule("PowersOfTwoModule", (m) => {
|
||||
const powersOfTwo = m.contract("PowersOfTwo");
|
||||
return { powersOfTwo };
|
||||
});
|
||||
17
hardhat/install-deps.js
Normal file
17
hardhat/install-deps.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import fs from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
|
||||
const parentDirectory = process.cwd();
|
||||
if (parentDirectory.includes('hardhat') && !fs.existsSync(path.join(parentDirectory, 'hardhat'))) {
|
||||
parentDirectory = path.join(parentDirectory, '..');
|
||||
}
|
||||
|
||||
const hardhatDirectory = path.join(parentDirectory, 'hardhat');
|
||||
|
||||
if (!fs.existsSync(path.join(hardhatDirectory, 'node_modules'))) {
|
||||
console.log('Hardhat directory node_modules not found. Installing...');
|
||||
execSync('npm install', { stdio: 'inherit', cwd: hardhatDirectory });
|
||||
} else {
|
||||
console.log('Hardhat directory node_modules already exists. Skipping.');
|
||||
}
|
||||
2948
hardhat/package-lock.json
generated
Normal file
2948
hardhat/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
hardhat/package.json
Normal file
17
hardhat/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "hardhat-code",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@nomicfoundation/hardhat-ignition": "^3.0.7",
|
||||
"@nomicfoundation/hardhat-toolbox-viem": "^5.0.2",
|
||||
"@types/node": "^22.19.11",
|
||||
"forge-std": "github:foundry-rs/forge-std#v1.9.4",
|
||||
"hardhat": "^3.1.8",
|
||||
"typescript": "~5.8.0",
|
||||
"viem": "^2.46.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^17.3.1"
|
||||
}
|
||||
}
|
||||
17
hardhat/test/PowersOfTwo.ts
Normal file
17
hardhat/test/PowersOfTwo.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { network } from "hardhat";
|
||||
|
||||
describe("PowersOfTwo", async function () {
|
||||
const { viem } = await network.connect();
|
||||
const publicClient = await viem.getPublicClient();
|
||||
|
||||
it("The test name goes here...", async function () {
|
||||
const contract = await viem.deployContract("PowersOfTwo");
|
||||
|
||||
// you can write commands/asserts/etc here
|
||||
// ...
|
||||
// ...
|
||||
});
|
||||
});
|
||||
13
hardhat/tsconfig.json
Normal file
13
hardhat/tsconfig.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
6111
package-lock.json
generated
Normal file
6111
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "dsb-lab6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "react-router build",
|
||||
"dev": "react-router dev",
|
||||
"start": "node exam/exam.js",
|
||||
"zip": "node exam/zip.js",
|
||||
"typecheck": "react-router typegen && tsc",
|
||||
"postinstall": "node exam/install-deps.js && node hardhat/install-deps.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@metamask/sdk": "^0.33.1",
|
||||
"@react-router/node": "7.12.0",
|
||||
"@react-router/serve": "7.12.0",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"clsx": "^2.1.1",
|
||||
"isbot": "^5.1.31",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-is": "^19.2.4",
|
||||
"react-router": "7.12.0",
|
||||
"react-toastify": "^10.0.6",
|
||||
"recharts": "^3.7.0",
|
||||
"viem": "^2.47.1",
|
||||
"wagmi": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "7.12.0",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.7",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
7
react-router.config.ts
Normal file
7
react-router.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { Config } from "@react-router/dev/config";
|
||||
|
||||
export default {
|
||||
// Config options...
|
||||
// Server-side render by default, to enable SPA mode set this to `false`
|
||||
ssr: true,
|
||||
} satisfies Config;
|
||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"include": [
|
||||
"**/*",
|
||||
"**/.server/**/*",
|
||||
"**/.client/**/*",
|
||||
".react-router/types/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["node", "vite/client"],
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"noImplicitAny": false,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
8
vite.config.ts
Normal file
8
vite.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
|
||||
});
|
||||
Reference in New Issue
Block a user