This commit is contained in:
2026-02-26 16:17:30 +01:00
commit 96e58bbf62
19 changed files with 5835 additions and 0 deletions

14
exam/common.js Normal file
View 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
View 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
View 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
View 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
View 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();