Files
lab5/exam/index.html
2026-03-19 16:37:01 +01:00

494 lines
24 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 5</title>
</head>
<body>
<div class="app-root">
<header class="app-header">
<nav>
<ul>
<li>
<hgroup>
<h2>Lab 5</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="#lesson" class="sidebar-button">Lesson</a>
<a href="#theory" class="sidebar-button">Questions</a>
<a href="#react" class="sidebar-button">Coding - Wagmi</a>
</nav>
</aside>
<main class="main-content container" id="main-content">
<section id="lesson">
<h2>Lesson: Ethereum tokens</h2>
<p>A token is a representation of something on the blockchain. This can represent money, time, services, shares in a company, a virtual pet, anything. By representing things as tokens, we can allow smart contracts to interact with them, exchange them, create or destroy them.</p>
<p>A token contract is simply an Ethereum smart contract. "Sending tokens" actually means "calling a method on a smart contract that someone has written and deployed". At the end of the day, a token contract is not much more than a mapping of addresses to balances, plus some methods to add and subtract from those balances.</p>
<p>It is these balances that represent the tokens themselves. Someone "has tokens" when their balance in the token contract is non-zero. That's it! These balances could be considered money, experience points in a game, deeds of ownership, or voting rights, and each of these tokens would be stored in different token contracts.</p>
<h3>Fungible tokens</h3>
<p>Fungible tokens are tokens where any one of them is exactly equal to any other; no token has special rights or behavior associated with it. Stated differently, fungible tokens are digital assets that are interchangeable and possess equal value, making them an important component of the cryptocurrency ecosystem. Fungible tokens are useful for things like a medium of exchange currency, voting rights, staking, and more.</p>
<p>One example of fungible tokens is stablecoins such as Tether (USDT) and USD Coin (USDC), which offer a stable medium of exchange by being pegged to stable assets like the US dollar, mitigating the volatility commonly associated with other cryptocurrencies.</p>
<h3>ERC-20</h3>
<p>Because everything in Ethereum is just a smart contract, and there are no rules about what smart contracts have to do, the community has developed a variety of standards for documenting how a contract can interoperate with other contracts. These are technical documents that outline standards for programming on Ethereum, called Ethereum Request for Comment (ERC).</p>
<p>The Ethereum standard for fungible tokens is ERC-20. Technically, ERC-20 is a list of functions and events that must be implemented by a token, so that it can be considered ERC-20 compliant. Because ERC-20 tokens follow a standardized set of rules, they can be easily exchanged and used within various applications. Keep in mind that, unlike ETH (Ethereum's native cryptocurrency), ERC-20 tokens aren't directly held by accounts. The tokens only exist inside a contract, which is like a self-contained database.</p>
<p class="mt-3">An ERC-20 token must implement the following functions:</p>
<table>
<tr><td>totalSupply()</td><td>Returns the total token supply.</td></tr>
<tr><td>balanceOf(addr)</td><td>Returns the token balance of the account with address addr.</td></tr>
<tr><td>transfer(to, value)</td><td>Transfers value amount of tokens to address to.</td></tr>
<tr><td>approve(spender, value)</td><td>Allows spender to withdraw from your account multiple times, up to the value amount.</td></tr>
<tr><td>allowance(owner, spender)</td><td>Returns the amount which spender is still allowed to withdraw from owner.</td></tr>
<tr><td>transferFrom(from, to, value)</td><td>Transfers value amount of tokens from address from to address to.</td></tr>
</table>
<p class="mt-3">Additionally, we also have: decimals, name, and symbol; which are optional, but almost always included.</p>
<p>Notice how it's possible to transfer tokens that belong to someone else (of course, with a previous approval). In other words, it's possible to authorize someone or another contract to transfer funds on our behalf. A possible use case involves paying for subscription-based services, where we don't want to manually send a payment every day/week/month. Instead, we just let a program do it for us.</p>
<p>To ensure transparency and allow external applications (like wallets and explorers) to track state changes, ERC-20 contracts must emit two key events: Transfer (which logs the movement of tokens), and Approval (which logs changes in spending permissions).</p>
<h3>Decimals</h3>
<p>Recall how we discussed both Ether and Wei in our previous lectures and labs (and we only communicated with the blockchain in Wei). Often, we'll want to be able to divide our tokens into arbitrary amounts: say, if we own 5 USDX (a fictional token), we may want to send 1.5 USDX to a friend, and keep 3.5 USDX to ourselves. Unfortunately, Solidity and the EVM do not support this behavior: only integer (whole) numbers can be used, which poses an issue. We may send 1 or 2 tokens, but not 1.5.</p>
<p>To work around this, ERC-20 provides a decimals field, which is used to specify how many decimal places a token has. To be able to transfer 1.5 USDX, decimals must be at least 1, since that number has a single decimal place.</p>
<p>How can this be achieved? It's actually very simple: a token contract can use larger integer values, so that a balance of 50 will represent 5 USDX, a transfer of 15 will correspond to 1.5 USDX being sent, and so on.</p>
<p>It is important to understand that the decimals value is only used for display purposes. All arithmetic inside the contract is still performed on integers, and it is the different user interfaces (wallets, exchanges, etc.) that must adjust the displayed values according to decimals.</p>
<p>Most of the time, we want to use a decimals value of 18, just like Ether and most ERC-20 token contracts in use, unless we have a very special reason not to.</p>
<h3>Different kinds of tokens</h3>
<p>Note that there's a big difference between having two voting rights and two deeds of ownership: each vote is equal to all others, but houses usually are not! We already mentioned that this attribute is called fungibility. Fungible goods are equivalent and interchangeable, like Ether, fiat currencies, and voting rights. Non-fungible goods are unique and distinct, like deeds of ownership, or collectibles. In a nutshell, when dealing with non-fungibles (like our house) we care about which ones we have, while in fungible assets (like our bank account statement) what matters is how much we have.</p>
<p class="mt-3">The two most well-known Ethereum token standards are listed below, but note that there are other standards as well:</p>
<table>
<tr><td>ERC-20</td><td>The most widespread token standard for fungible assets.</td></tr>
<tr><td>ERC-721</td><td>The de-facto solution for non-fungible tokens, often used for collectibles, art, and games.</td></tr>
</table>
<p class="mt-3">Understanding the difference between fungible tokens and NFTs is essential for navigating the crypto landscape effectively.</p>
<h3>OpenZeppelin</h3>
<p>OpenZeppelin Contracts is a library for secure smart contract development on the Ethereum network, written in Solidity. It's built on a solid foundation of community-vetted code, and features implementations of standards like ERC-20 and ERC-721. We can use the contracts in the OpenZeppelin library by importing them, and using inheritance (which is something we learned when we studied Solidity).</p>
<p>Therefore, using smart contracts, we can easily create our own ERC-20 token contract (in the example below, a token called Pax Gold with a symbol PAXG). Additionally, we're creating/minting an initial supply of tokens, which will be assigned to the address that deploys the contract.</p>
<pre><code>// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract PaxGold is ERC20 {
constructor(uint256 initialSupply) ERC20("Pax Gold", "PAXG") {
// Mint initial supply to the deployer
_mint(msg.sender, initialSupply);
}
}</code></pre>
<p>That's it! We can now deploy a new ERC-20 fungible token to the Ethereum blockchain.</p>
</section>
<section id="theory" class="hidden">
<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="react" class="hidden">
<h2>Coding - React: ERC-20 Tokens</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 lists several ERC-20 tokens (which you will load from an API). A designer on our team has already created the design for the web app, and the backend team has created an API for fetching the list of tokens that should be shown to the user. Your job is to create the frontend for the web app.</p>
<h3 class="mt-3">The tokens page (<a href="https://api.next.code-camp.org/cdn/erc-20/index.html" target="_blank" style="text-decoration: none;">design</a>)</h3>
<p>The tokens page should show a card for each token. The API (url below) returns those tokens as a list. In each card, you should write the name of the token, the ID (number), symbol, address (of the token contract) and the balance of the user in that token (for example, 500 USDT). 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>At the bottom of each card, there should be two buttons. The first one is a link and should point to "https://etherscan.code-camp.org/address/###" where ### is the address of the contract. The second one should be a button that triggers a "Transfer funds" section in the card (see the second card in the design). You can use React state in your component to keep track if that section should be visible or not. In the "Transfer funds" section, there is a button to execute a transfer, which should only be enabled if both the address and the amount are valid (you can use isAddress to check if the address is valid or not). After the transfer is started, you should show the current status or any potential errors below the form. There is also a button to close the section on the top right.</p>
<p>For testing the transfer functionality, you can send tokens to this address: 0xa93D235435C5CaEAa3232d88dB1173977BEB695e (or use another address that you own, for example if you have several accounts in MetaMask). If you haven't set up a MetaMask account yet, visit this <a href="https://faucet.code-camp.org">link</a>.</p>
<p>Because you will be working with ERC-20 tokens, you can use "erc20Abi" (imported from viem), when executing any functions on those contracts. For each contract, you can get the decimals field, which you need to use to show the correct balance for the user (for example, by using formatUnits).</p>
<p>Important links:</p>
<ul>
<li><a href="https://api.next.code-camp.org/cdn/erc-20/index.html" target="_blank">The page design</a></li>
<li><a href="https://api.next.code-camp.org/erc20-tokens" target="_blank">The API url</a> - https://api.next.code-camp.org/erc20-tokens</li>
<li><a href="https://api.next.code-camp.org/cdn/erc-20/types.html" target="_blank">The types</a> (created by our backend team)</li>
</ul>
<p>Remember: Just open the design link in your browser and view the source code (or use the browser's Inspect tool) to copy the necessary HTML structure. You don't need to design anything!</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 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>