Initial commit

This commit is contained in:
2026-03-25 21:47:57 +01:00
commit 477741b545
44 changed files with 12611 additions and 0 deletions

55
exam/common.js Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

18
exam/package.json Normal file
View 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
View 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
View 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();