Lab 2
This commit is contained in:
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# Exam archives
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
# MacOS specific files
|
||||||
|
.DS_Store
|
||||||
24
README.md
Normal file
24
README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Lab 2
|
||||||
|
|
||||||
|
This is a Viem and Hardhat 3 project. To get started, please read the instructions below.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
To start the lab, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm install
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Finishing up
|
||||||
|
|
||||||
|
When you're done, run the following command to generate a zip archive.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm run zip
|
||||||
|
```
|
||||||
16
chains/next.ts
Normal file
16
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']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
15
contracts/Counter.sol
Normal file
15
contracts/Counter.sol
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
contract Counter {
|
||||||
|
uint256 public x;
|
||||||
|
|
||||||
|
function inc() public {
|
||||||
|
x++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function incBy(uint256 by) public {
|
||||||
|
require(by > 0, "incBy: increment should be positive");
|
||||||
|
x += by;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
contracts/Gradebook.sol
Normal file
13
contracts/Gradebook.sol
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
contract Gradebook {
|
||||||
|
|
||||||
|
|
||||||
|
// addStudent(addr, name)
|
||||||
|
|
||||||
|
// submitScore(addr, points)
|
||||||
|
// this function should fire an event when points are added
|
||||||
|
|
||||||
|
// getGrade(addr)
|
||||||
|
}
|
||||||
14
exam/common.js
Normal file
14
exam/common.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
export function installDeps() {
|
||||||
|
// check if node_modules exists
|
||||||
|
if (fs.existsSync('./node_modules')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// install dependencies
|
||||||
|
console.log('Installing dependencies...');
|
||||||
|
execSync('npm install', { stdio: 'inherit' });
|
||||||
|
console.log('Dependencies installed');
|
||||||
|
}
|
||||||
54
exam/exam.js
Normal file
54
exam/exam.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { installDeps } from './common.js';
|
||||||
|
|
||||||
|
const SERVER_PORT = 3018;
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
installDeps();
|
||||||
|
|
||||||
|
const parentDirectory = process.cwd();
|
||||||
|
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();
|
||||||
424
exam/index.html
Normal file
424
exam/index.html
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
<!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 .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 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 2</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-root">
|
||||||
|
<header class="app-header">
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<hgroup>
|
||||||
|
<h2>Lab 2</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>
|
||||||
|
|
||||||
|
</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">Theory</a>
|
||||||
|
<a href="#viem" class="sidebar-button">Coding - Viem</a>
|
||||||
|
<a href="#hardhat" class="sidebar-button">Coding - Hardhat</a>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main-content container" id="main-content">
|
||||||
|
<section id="theory">
|
||||||
|
<hgroup>
|
||||||
|
<h2>Theory</h1>
|
||||||
|
<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="viem" class="hidden">
|
||||||
|
<h2>Coding - Viem: Contract Explorer</h1>
|
||||||
|
|
||||||
|
<p>In the repository you downloaded, there is a directory called <strong>/scripts</strong>. Inside, there is a file called <strong>contract-explorer.ts</strong>, which is used to analyze an ERC-20 contract (for fungible tokens) on the Next Testnet network, and to get the balance of a specific Ethereum address within that contract. Your job is to complete the script so that it prints the following information for the contract:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>The name</li>
|
||||||
|
<li>The symbol</li>
|
||||||
|
<li>The decimals (for example, for PAXG it's 18 decimals, like Wei and Ether)</li>
|
||||||
|
<li>The balance (in that contract), for a specific address</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="mt-3">You can test the script by running the following command:</p>
|
||||||
|
<pre><code>npx tsx scripts/contract-explorer.ts</code></pre>
|
||||||
|
|
||||||
|
<p class="mt-3">To understand what options are available for ERC-20 tokens, you should start by clicking the "Read contract" tab here: <a href="https://etherscan.code-camp.org/address/0x5FbDB2315678afecb367f032d93F642f64180aa3" target="_blank">view contract</a>.</p>
|
||||||
|
|
||||||
|
<p class="mt-3">For testing, you can use the following information:</p>
|
||||||
|
<div class="overflow-auto">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Address</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="word-break: break-all;">Contract address</td>
|
||||||
|
<td>0x5FbDB2315678afecb367f032d93F642f64180aa3</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="word-break: break-all;">Account address</td>
|
||||||
|
<td>0xa24d14702cef666131dd8EFC0670318CDCA4Baf3</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-3">Hint: you only need to use the readErc20Contract function, which is already provided for you in the starter template.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section id="hardhat" class="hidden">
|
||||||
|
<h2>Coding - Hardhat: Gradebook</h1>
|
||||||
|
|
||||||
|
<p>In the repository you downloaded, there is a directory called <strong>/contracts</strong> (for storing smart contracts), and a directory called <strong>/test</strong> (for test files). In the first directory, there is a file called <strong>Gradebook.sol</strong> (where you need to write a smart contract in Solidity); while in the second directory, there is a file called <strong>Gradebook.ts</strong> (where you need to write tests for the contract).</p>
|
||||||
|
<p>The contract is meant to be used by a teacher as a public Gradebook, for storing points and calculating grades for students. It should support 3 functions:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>addStudent(addr, name) - for adding a new student (with their Ethereum address and name).</li>
|
||||||
|
<li>submitScore(addr, points) - for storing the number of points earned by the student (a number between 0 and 100). Points can only be set once for a student and can't be overridden. An event should be fired with the address and the number of points.</li>
|
||||||
|
<li>getGrade(addr) - a view function that calculates a grade based on the points and returns a number between 5 and 10.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="mt-3">The contract should validate the inputs, and return errors if something is not correct. For calculating a grade based on the number of points, you can use the standard scoring system used at Brainster Next (91-100 is a 10, 81-90 is a 9, ..., and anything below 51 is a 5).</p>
|
||||||
|
|
||||||
|
<p class="mt-3">You can build the contracts by running the following command:</p>
|
||||||
|
<pre><code>npx hardhat build</code></pre>
|
||||||
|
|
||||||
|
<p class="mt-3">After you code the contract, you can start adding tests in the test/Gradebook.ts file (it already has a list of tests you need to add). To run them, you can use the following command:</p>
|
||||||
|
<pre><code>npx hardhat test</code></pre>
|
||||||
|
|
||||||
|
<p class="mt-3">Hint: you have an example contract and tests (see Documentation links on the top) to help you get started.</p>
|
||||||
|
|
||||||
|
</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 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>";
|
||||||
|
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>
|
||||||
122
exam/theory.json
Normal file
122
exam/theory.json
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"question": "Which system has a single point of failure?",
|
||||||
|
"options": {
|
||||||
|
"A": "Monolithic system",
|
||||||
|
"B": "Peer-to-peer network",
|
||||||
|
"C": "Distributed system",
|
||||||
|
"D": "Content Delivery Network"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"question": "Vertical scaling is limited because:",
|
||||||
|
"options": {
|
||||||
|
"A": "Networks are unreliable",
|
||||||
|
"B": "It depends on consensus",
|
||||||
|
"C": "A single machine has a maximum capacity",
|
||||||
|
"D": "It requires multiple data centers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"question": "Replication helps with:",
|
||||||
|
"options": {
|
||||||
|
"A": "Increasing latency",
|
||||||
|
"B": "Fault tolerance",
|
||||||
|
"C": "Reducing decentralization",
|
||||||
|
"D": "Removing consensus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"question": "RPC aims to:",
|
||||||
|
"options": {
|
||||||
|
"A": "Replace HTTP",
|
||||||
|
"B": "Make remote calls look like local function calls",
|
||||||
|
"C": "Store messages in queues",
|
||||||
|
"D": "Encrypt all communication"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"question": "A producer in a messaging queue:",
|
||||||
|
"options": {
|
||||||
|
"A": "Processes messages",
|
||||||
|
"B": "Stores blockchain blocks",
|
||||||
|
"C": "Sends messages to a broker",
|
||||||
|
"D": "Subscribes to topics"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"question": "REST is:",
|
||||||
|
"options": {
|
||||||
|
"A": "A binary serialization format",
|
||||||
|
"B": "An architectural style using HTTP methods on resources",
|
||||||
|
"C": "A consensus mechanism",
|
||||||
|
"D": "A blockchain protocol"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"question": "In Ethereum, if two conflicting transactions attempt a double spend:",
|
||||||
|
"options": {
|
||||||
|
"A": "Both are accepted",
|
||||||
|
"B": "Neither is accepted",
|
||||||
|
"C": "A globally accepted order is selected and one is rejected",
|
||||||
|
"D": "The sender asynchronously chooses which one wins"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"question": "Which of the following is required to retrieve (read) the Ether balance of a publicly known Ethereum address?",
|
||||||
|
"options": {
|
||||||
|
"A": "The private key",
|
||||||
|
"B": "The mnemonics for the wallet account",
|
||||||
|
"C": "Both the private key and the mnemonics",
|
||||||
|
"D": "None of the above"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"question": "Ethereum is Proof of Stake. It prioritizes validators based on blockchain rules and:",
|
||||||
|
"options": {
|
||||||
|
"A": "CPU power",
|
||||||
|
"B": "Network latency",
|
||||||
|
"C": "Geographic location",
|
||||||
|
"D": "Amount of cryptocurrency staked"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"question": "Solidity is primarily used to:",
|
||||||
|
"options": {
|
||||||
|
"A": "Build REST APIs",
|
||||||
|
"B": "Develop operating systems",
|
||||||
|
"C": "Mine Bitcoin",
|
||||||
|
"D": "Write smart contracts for Ethereum"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"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": 12,
|
||||||
|
"question": "Immutability in blockchain means:",
|
||||||
|
"options": {
|
||||||
|
"A": "The balance of an address cannot be updated in subsequent blocks",
|
||||||
|
"B": "Data is deleted after one confirmation by a validator",
|
||||||
|
"C": "Past blocks cannot be altered without changing subsequent blocks and gaining consensus",
|
||||||
|
"D": "Other users cannot read transactions that don't belong to them"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
62
exam/zip.js
Normal file
62
exam/zip.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentDirectory = process.cwd();
|
||||||
|
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/**',
|
||||||
|
'.git/**',
|
||||||
|
'artifacts/**',
|
||||||
|
'cache/**',
|
||||||
|
'.env',
|
||||||
|
'.DS_Store',
|
||||||
|
'*.zip',
|
||||||
|
'exam/common.js',
|
||||||
|
'exam/exam.js',
|
||||||
|
'exam/index.html',
|
||||||
|
'exam/zip.js'
|
||||||
|
]
|
||||||
|
|
||||||
|
});
|
||||||
|
archive.pipe(fs.createWriteStream(zipPath));
|
||||||
|
await archive.finalize();
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(`Archive created: ${zipPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
49
hardhat.config.ts
Normal file
49
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
ignition/modules/Counter.ts
Normal file
6
ignition/modules/Counter.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
|
||||||
|
|
||||||
|
export default buildModule("CounterModule", (m) => {
|
||||||
|
const counter = m.contract("Counter");
|
||||||
|
return { counter };
|
||||||
|
});
|
||||||
4841
package-lock.json
generated
Normal file
4841
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "dsb-lab2",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node exam/exam.js",
|
||||||
|
"zip": "node exam/zip.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nomicfoundation/hardhat-ignition": "^3.0.7",
|
||||||
|
"@nomicfoundation/hardhat-toolbox-viem": "^5.0.2",
|
||||||
|
"@types/node": "^22.19.11",
|
||||||
|
"@types/prompts": "^2.4.9",
|
||||||
|
"forge-std": "github:foundry-rs/forge-std#v1.9.4",
|
||||||
|
"hardhat": "^3.1.8",
|
||||||
|
"typescript": "~5.8.0",
|
||||||
|
"viem": "^2.46.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"archiver": "^7.0.1",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"open": "^11.0.0",
|
||||||
|
"prompts": "^2.4.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
scripts/contract-explorer.ts
Normal file
55
scripts/contract-explorer.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import prompts from 'prompts';
|
||||||
|
import { createPublicClient, erc20Abi, http } from 'viem';
|
||||||
|
import { nextChain } from '../chains/next.js';
|
||||||
|
|
||||||
|
const client = createPublicClient({
|
||||||
|
chain: nextChain,
|
||||||
|
transport: http()
|
||||||
|
});
|
||||||
|
|
||||||
|
// you don't have to touch this function, it's already good
|
||||||
|
const readErc20Contract = async (contractAddress: `0x${string}`, func: any, args: any) => {
|
||||||
|
return client.readContract({
|
||||||
|
address: contractAddress,
|
||||||
|
abi: erc20Abi,
|
||||||
|
functionName: func,
|
||||||
|
args: args
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { contractAddress, accountAddress }:
|
||||||
|
{ contractAddress: `0x${string}`, accountAddress: `0x${string}` } = await prompts([{
|
||||||
|
type: 'text',
|
||||||
|
name: 'contractAddress',
|
||||||
|
message: 'Enter the contract address to explore:',
|
||||||
|
}, {
|
||||||
|
type: 'text',
|
||||||
|
name: 'accountAddress',
|
||||||
|
message: 'Enter the account address:',
|
||||||
|
}]);
|
||||||
|
|
||||||
|
if (!contractAddress || !accountAddress) {
|
||||||
|
console.error('You need to enter both a contract address and an account address.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// you can start coding here
|
||||||
|
// ...
|
||||||
|
|
||||||
|
|
||||||
|
console.log(`Contract address: ${contractAddress}`);
|
||||||
|
console.log(`========================================`);
|
||||||
|
console.log(`Name: ...`);
|
||||||
|
console.log(`Symbol: ...`);
|
||||||
|
console.log(`Decimals: ...`);
|
||||||
|
|
||||||
|
console.log(); console.log(); console.log();
|
||||||
|
|
||||||
|
console.log(`Account address: ${accountAddress}`);
|
||||||
|
console.log(`========================================`);
|
||||||
|
console.log(`Balance: ...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
44
test/Counter.ts
Normal file
44
test/Counter.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { network } from "hardhat";
|
||||||
|
|
||||||
|
const { viem } = await network.connect();
|
||||||
|
const publicClient = await viem.getPublicClient();
|
||||||
|
|
||||||
|
describe("Counter", async function () {
|
||||||
|
it("The value should match the sum of the increments", async function () {
|
||||||
|
const counter = await viem.deployContract("Counter");
|
||||||
|
|
||||||
|
await counter.write.inc();
|
||||||
|
|
||||||
|
// run a series of increments
|
||||||
|
// 3+...+10 = 52
|
||||||
|
for (let i = 3n; i <= 10n; i++) {
|
||||||
|
await counter.write.incBy([i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// read the value
|
||||||
|
const value = await counter.read.x();
|
||||||
|
|
||||||
|
assert.equal(value, 1n + 52n);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("Demo reading from a public client with Viem", async function () {
|
||||||
|
const counter = await viem.deployContract("Counter");
|
||||||
|
|
||||||
|
await counter.write.inc();
|
||||||
|
await counter.write.inc();
|
||||||
|
await counter.write.inc();
|
||||||
|
|
||||||
|
// read the value
|
||||||
|
const value = await publicClient.readContract({
|
||||||
|
address: counter.address,
|
||||||
|
abi: counter.abi,
|
||||||
|
functionName: "x",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(value, 3n);
|
||||||
|
});
|
||||||
|
});
|
||||||
20
test/Gradebook.ts
Normal file
20
test/Gradebook.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { network } from "hardhat";
|
||||||
|
|
||||||
|
const { viem } = await network.connect();
|
||||||
|
const publicClient = await viem.getPublicClient();
|
||||||
|
|
||||||
|
describe("Gradebook", async function () {
|
||||||
|
it("Only the teacher can add students and points", async function () {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Everyone can read the grade for a student by their address", async function () {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("An event is fired when the teacher submits points for a student", async function () {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
5
test/accounts/index.ts
Normal file
5
test/accounts/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const account1 = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||||
|
export const account2 = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
||||||
|
export const account3 = '0xcccccccccccccccccccccccccccccccccccccccc';
|
||||||
|
export const account4 = '0xdddddddddddddddddddddddddddddddddddddddd';
|
||||||
|
export const account5 = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
|
||||||
13
tsconfig.json
Normal file
13
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user