diff --git a/1. b/1. new file mode 100644 index 00000000..e69de29b diff --git a/assignments/1-block-observation/README.md b/assignments/1-block-observation/README.md deleted file mode 100644 index a832de62..00000000 --- a/assignments/1-block-observation/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# The Ethereum Blockchain Explorer (Etherscan) & Transaction Hash - -## The Ethereum Blockchain Explorer (Etherscan) - -### Overview Page Observations -On the Etherscan overview homepage, I observed the following key metrics in the network: -1. **Live Ether Price**: In real-time, this displays the current price of ETH. -2. **Market Capitalization**: Represents the total value of all ETH in circulation. -3. **Transactions**: This displays the total number of transactions processed on the Ethereum network, increasing continuously as new transactions are confirmed. -4. **Last Finalized Block**: This shows us the most recent block that has achieved finality. - - -#### A table that display the lastest blocks -After clicking on the latest block at the time, there are several details I observed about the block and they are: - -1. **Block Height**: This indicates the length of the blockchain, increases after the addition of the new block. -2. **Status**: As at the time I checked it, it was finalized. This shows the status of the finality of the block. -3. **Proposed On** and **Timestamps**: Basically, this gives us information about when the transaction started. -4. Withdrawals -5. **Fee Recipient**: This gives us basic information about the receiver of the transaction. -6. **Block Reward**: This is the total reward earned from the miner. -7. Gas information -8. Block Size -9. **Extra Data**: More information about the recipient - - -#### A table that display the lastest transactions - -After clicking on the latest transaction at the time, there are several transaction details and they are: - -##### Transaction Overview -1. **Transaction Hash**: A unique 64-bit character id for ay given transaction. -2. **Status**: This shows us the progress of the transaction. -3. Block -4. Timestamp**: Giving us information about when the transaction took place. - -##### Transaction Cost Details -This gives us information about the cost of transaction like its fee and gas price. - -Etherscan serves as a powerful blockchain explorer for transparently analyzing Ethereum's blocks, transactions, and validator behavior. - -## Transaction Hash - -### Overview Page Observations - -Similar to the overview page of the etherscan of several transactions and blocks, this page gives us a keen view of the information that pertains to a particular transaction. - -We have the transaction hash of this transaction, the status, spacific block and the amount of block confirmations, timestamp from the time it was proposed (I think), the sender and receiver's wallet address, value of eth sent, transaction fee and gas price. - -There's also a button that when clicked, will show us a dropdown of more information about the transaction - -## Conclusion -In conclusion, I think the Etherscan is a powerful blockchain explorer for analyzing Etherum blocks, transactions, and validator behaviour, transparently. \ No newline at end of file diff --git a/assignments/2-gas-hash/README.md b/assignments/2-gas-hash/README.md deleted file mode 100644 index 6f04904e..00000000 --- a/assignments/2-gas-hash/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# Ethereum Gas Fee and Merkle Root Calculator - - -## Overview -This repo contains: -- Simple explanations and calculation of key concepts around gas. -- TypeScript scripts for computing the Merkle roots of transactions using SHA256 and Keccak256. - -## Gas Fees in Ethereum: Explanation and Formula - -In Ethereum (and other EVM-based blockchains), gas is a unit that measures the computational work required to execute a transaction or smart contract. -It prevents spam and ensures fair use of network resources. - -**Gas fees** are the cost paid for this computation and are paid in ETH (or the chain’s native token). -I'll like to explain some concepts that are closesly associated with gas. - -### Key Concepts -- **Gas Limit**: This is the maximum amount of gas a transaction is allowed to use/consume, usually set by the user when submitting the transaction. If the transaction exceeds the limit in for a transaction, it fails and the gas is used. It is set by the user or wallet. If execution requires more gas than the limit, the transaction fails and is not refunded. -- **Gas Price (Gwei)**: This is the price/unit (Gwei, where 1 Gwei = 10^-9 ETH) of gas that the user bids. Miners will usually prefer a higher gas price. -The price per unit of gas, denominated in Gwei - -1 Gwei = 10⁻⁹ ETH -- **Gas Used**: The actual amount of gas consumed by the transaction after it has been executed. Simple ETH transfers use a fixed amount. Smart contracts consume variable gas depending on logic. - -- **Base Fee**: This fee is dynamically adjusted by the protocol depending on how the network is congested. It would watch for the block to be ~50%. This fee is burned, and ETH suply is reduced. Dynamically adjusted by the protocol based on network congestion. Target block usage is ~50%. Burned (removed from circulation), reducing ETH supply. -- **Priority Fee (Tip)**: An optional extra fee to incentivize miners/validators to include your transaction faster. -Optional extra fee paid to validators. Incentivizes faster inclusion in a block. -- **Effective Gas Price**: The total price per gas unit paid = Base Fee + Priority Fee. - -### Gas Fee Formula -**Total Gas Fee = Gas Used x Effective Gas Price** - - -#### Example Calculation - -If you send **2 ETH** and the transaction uses 4 units of gas, with: - -Base Fee = 11 Gwei - -Priority Fee = 3 Gwei - -Total Fee = 4 × (11 + 3) - = 4 × 14 - = 56 Gwei - - -Convert to ETH: - -56 Gwei = 56 × 10⁻⁹ ETH - = 0.000000056 ETH - - -**Total sent:** -2.000000056 ETH - -### What Affects Gas Fees? - -- **Network congestion:** Higher demand → higher base fee -- **Transaction complexity:** More computation → more gas used -- **User tip:** Higher priority fee → faster confirmation - -Gas fees are **market-driven**, not fixed. - - -## Markle Root (Root Hash) -This is a cryptographic hash that serves as a unique fingerprint for all transactions within a blockchain block. It is the final code in a markle tree. - - -### Setup -run -```bash - git clone https://github.com/Olorunshogo/blockheader-web3-assignments.git - cd gas-hash - npm install - node hash.ts -``` - -## Merkle Root -- Prompts for tx count and strings, derives hashes, computes roots with both algorithms. -- Leaves are 64-char hex (32 bytes). - -## Notes -- Tested on Node.js 24.11.1. \ No newline at end of file diff --git a/assignments/2-gas-hash/hash.ts b/assignments/2-gas-hash/hash.ts deleted file mode 100644 index cd282994..00000000 --- a/assignments/2-gas-hash/hash.ts +++ /dev/null @@ -1,166 +0,0 @@ - -import { ethers } from "ethers"; -import * as crypto from 'crypto'; -import * as readline from 'readline-sync'; - -// Gas Price Calculation -function calculateGasFee() { - // Prompt for gas price (in Gwei) - const gasPriceGwei = parseFloat( - readline.question("Enter gas price in Gwei: "), - ); - - // Prompt for gas used (in units) - const gasUsed = parseFloat(readline.question("Enter gas used: ")); - - // Calculate total gas fee in ETH - const totalGasFee = (gasPriceGwei * gasUsed) / 1e9; // Convert Gwei to ETH - console.log(`Total Gas Fee: ${totalGasFee} ETH`); -} - -// Merkle Root Calculations - -// Helper function to hash using the SHA256 algorithm -function sha256Hash(first: Buffer, second: Buffer): string { - const combinedLeaf = Buffer.concat([first, second]); - return crypto.createHash("sha256").update(combinedLeaf).digest("hex"); - -} - -// Similarly, KACCAK256 helper function to combine two leafs -function keccak256Hash(first: Buffer, second: Buffer): string { - const combinedLeaf = Buffer.concat([first, second]); - return ethers.keccak256(combinedLeaf); -} - -// Function to build Merkle Root -function buildMerkleRoot(leaves: string[], hashFunc: (first: Buffer, second: Buffer) => string,): string { - // Check for leaves - if (leaves.length === 0) { - throw new Error("No leaves provided"); - } - - // Convert hex leaves to Buffers: So we dont have to use the 0x prefix - let level: Buffer[] = leaves.map((leaf) => Buffer.from(leaf.slice(2), "hex")); - - while (level.length > 1) { - const nextLevel: Buffer[] = []; - for (let i = 0; i < level.length; i += 2) { - const first = level[i]; - const second = i + 1 < level.length ? level[i + 1] : first; - const parentHex = hashFunc(first, second); - nextLevel.push(Buffer.from(parentHex.slice(2), "hex")); - } - // Log the current level's hashes - console.log( - "Current level hashes: ", - nextLevel.map((hash) => hash.toString("hex")), - ); - - level = nextLevel; - } - - return "Ox" + level[0].toString("hex"); -} - -// Main hash function -function shaHashingFunction() { - // Prompt User for the Number of Transactions - const numTxs = parseInt( - readline.question("Enter number of transactions: "), - 10, - ); - - // Check if the user input character (s) or a number less than 0 - if (isNaN(numTxs) || numTxs < 0) { - console.error("Invalid number"); - return; - } - - // Check if the number of transaction is equal to zero - if (numTxs === 0) { - console.error("Please input a number greater than 0"); - return; - } - - // Initiate the transaction hashes array - const transactionHashes: string[] = []; - - for (let i = 0; i < numTxs; i++) { - // Prompt the user to enter a string to generate each the transaction hash - const inputStr = readline.question( - `Enter string for transaction ${i + 1}: `, - ); - // Derive transaction hash from input string using Keccak256 (simulating tx hash derivation) - const txHash = ethers.sha256(ethers.toUtf8Bytes(inputStr)); - console.log(`Derived Tx Hash ${i + 1}: ${txHash}`); - console.log(`The length of the Tx Hash is: ${i + 1}: ${txHash.length}`); - - // Append transaction hash to the array - transactionHashes.push(txHash); - - console.log(""); - } - - // Merkle Root Calculation - const sha256Root = buildMerkleRoot(transactionHashes, sha256Hash); - console.log("\n Merkle Root with SHA256: ", sha256Root); -} - -function keccakHashingFunction() { - // Prompt User for the Number of Transactions - const numTxs = parseInt( - readline.question("Enter number of transactions: "), - 10, - ); - - // Check if the user input character (s) or a number less than 0 - if (isNaN(numTxs) || numTxs < 0) { - console.error("Invalid number"); - return; - } - - // Check if the number of transaction is equal to zero - if (numTxs === 0) { - console.error("Please input a number greater than 0"); - return; - } - - // Initiate the transaction hashes array - const transactionHashes: string[] = []; - - for (let i = 0; i < numTxs; i++) { - // Prompt the user to enter a string to generate each the transaction hash - const inputStr = readline.question( - `Enter string for transaction ${i + 1}: `, - ); - // Derive transaction hash from input string using Keccak256 (simulating tx hash derivation) - const txHash = ethers.keccak256(ethers.toUtf8Bytes(inputStr)); - console.log(`Derived Tx Hash ${i + 1}: ${txHash}`); - console.log(`The length of the Tx Hash is: ${i + 1}: ${txHash.length}`); - - // Append transaction hash to the array - transactionHashes.push(txHash); - - console.log(""); - } - - // Merkle Root Calculation - const keccak256Root = buildMerkleRoot(transactionHashes, keccak256Hash); - console.log("\n Merkle Root with Keccak256: ", keccak256Root); -} - -// Run the gas fee calculator -calculateGasFee(); - -// Run the hashing functions -shaHashingFunction(); -keccakHashingFunction(); - - - - - - - - diff --git a/assignments/2-gas-hash/package.json b/assignments/2-gas-hash/package.json deleted file mode 100644 index 59854dad..00000000 --- a/assignments/2-gas-hash/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "module", - "dependencies": { - "@types/node": "^25.0.9", - "crypto": "^1.0.1", - "ethers": "^6.16.0", - "readline-sync": "^1.4.10", - "ts-node": "^10.9.2" - }, - "devDependencies": { - "@types/readline-sync": "^1.4.8" - } -} diff --git a/assignments/6-wallet-address/README.md b/assignments/6-wallet-address/README.md new file mode 100644 index 00000000..e8c53c3b --- /dev/null +++ b/assignments/6-wallet-address/README.md @@ -0,0 +1,78 @@ + +# BIP32 Wallet + +A simple, lightweight utility for deriving Ethereum wallets from BIP39 mnemonics using hierarchical deterministic (HD) wallets. + +## What is this? + +This is a straightforward Node.js module that helps you work with Ethereum wallets. It lets you generate mnemonic phrases and derive multiple wallet addresses from a single seed phrase—useful if you want to manage several wallets without storing separate private keys. + +## Features + +- **Generate BIP39 mnemonics** - Create secure seed phrases +- **Derive Ethereum wallets** - Get address and private key from a mnemonic +- **Batch derivation** - Generate multiple wallets from a single phrase +- **HD wallet support** - Uses standard Ethereum derivation paths (BIP44) + +## Installation + +First, make sure you have Node.js installed. Then grab the dependencies: + +```bash +npm install +``` + +## Usage + +Here's how to get started: + +```javascript +const { deriveWallet, generateMnemonic, deriveMultiple } = require('./bip32wallet'); + +// Create a new random mnemonic +const mnemonic = generateMnemonic(); +console.log(mnemonic); // 12 or 24 word seed phrase + +// Derive a wallet from it +const wallet = deriveWallet(mnemonic); +console.log(wallet.address); // Your wallet address +console.log(wallet.privateKey); // Your private key + +// Or generate multiple wallets at once +const wallets = deriveMultiple(mnemonic, 5); // Creates 5 wallets +wallets.forEach((w, i) => { + console.log(`Wallet ${i + 1}: ${w.address}`); +}); +``` + +## API + +### `generateMnemonic(strength)` +Generates a random BIP39 mnemonic phrase. +- **strength** (optional): Number of bits (default: 128 = 12 words, 256 = 24 words) +- **Returns**: A string mnemonic phrase + +### `deriveWallet(mnemonic, path)` +Derives a single wallet from a mnemonic. +- **mnemonic**: Your seed phrase +- **path** (optional): HD derivation path (default: `m/44'/60'/0'/0/0`) +- **Returns**: Object with `address`, `publicKey`, and `privateKey` + +### `deriveMultiple(mnemonic, count, basePath)` +Generate multiple wallets from a single mnemonic. +- **mnemonic**: Your seed phrase +- **count** (optional): How many wallets to create (default: 5) +- **basePath** (optional): Base derivation path (default: `m/44'/60'/0'/0`) +- **Returns**: Array of wallet objects + +## Requirements + +- Node.js (v12 or higher) +- `bip39` - BIP39 mnemonic utilities +- `ethers` - Ethereum library for wallet operations + +## Security Note + +Never share your private keys or mnemonic phrases! This code is meant for development and testing. For production use, consider using hardware wallets or proper key management systems. + + diff --git a/assignments/6-wallet-address/bip32wallet.js b/assignments/6-wallet-address/bip32wallet.js new file mode 100644 index 00000000..24b7934a --- /dev/null +++ b/assignments/6-wallet-address/bip32wallet.js @@ -0,0 +1,42 @@ +// const bip39 = require('bip39'); +// const { Wallet, HDNodeWallet } = require('ethers'); + +import bip39 from 'bip39'; +import { HDNodeWallet } from "ethers"; + +const ETH_DERIVATION_PATH = "m/44'/60'/0'/0/0"; + +const deriveWallet = ( + mnemonic, + path = ETH_DERIVATION_PATH +) => { + if (!bip39.validateMnemonic(mnemonic)) { + throw new Error('Invalid mnemonic'); + } + + const wallet = HDNodeWallet.fromPhrase(mnemonic, undefined, path); + + return { + path, + address: wallet.address, + publicKey: wallet.publicKey, + privateKey: wallet.privateKey + }; +}; + +const generateMnemonic = (strength = 128) => + bip39.generateMnemonic(strength); + +const deriveMultiple = ( + mnemonic, + count = 5, + basePath = "m/44'/60'/0'/0" +) => + Array.from({ length: count }, (_, i) => + deriveWallet(mnemonic, `${basePath}/${i}`) + ); + +/* Example */ +const mnemonic = generateMnemonic(); +console.log(deriveWallet(mnemonic)); +console.log(deriveMultiple(mnemonic, 3)); diff --git a/assignments/6-wallet-address/package-lock.json b/assignments/6-wallet-address/package-lock.json new file mode 100644 index 00000000..8baf108b --- /dev/null +++ b/assignments/6-wallet-address/package-lock.json @@ -0,0 +1,156 @@ +{ + "name": "ethereum-hd-wallet", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ethereum-hd-wallet", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "bip39": "^3.1.0", + "ethers": "^6.13.4" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "license": "ISC", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/assignments/6-wallet-address/package.json b/assignments/6-wallet-address/package.json new file mode 100644 index 00000000..8b3d7872 --- /dev/null +++ b/assignments/6-wallet-address/package.json @@ -0,0 +1,24 @@ +{ + "name": "ethereum-hd-wallet", + "version": "1.0.0", + "description": "Ethereum HD wallet derivation using BIP39 and ethers.js", + "main": "index.js", + "type": "module", + "scripts": { + "wallet": "node ./bip32wallet.js" + }, + "keywords": [ + "ethereum", + "ethers", + "bip39", + "hd-wallet", + "web3", + "crypto" + ], + "author": "Farouq Alhassan", + "license": "MIT", + "dependencies": { + "bip39": "^3.1.0", + "ethers": "^6.13.4" + } +} diff --git a/assignments/NFT-Marketplace/.gitignore b/assignments/NFT-Marketplace/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/assignments/NFT-Marketplace/.gitignore @@ -0,0 +1,20 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# pnpm deploy output +/bundle + +# Hardhat Build Artifacts +/artifacts + +# Hardhat compilation (v2) support directory +/cache + +# Typechain output +/types + +# Hardhat coverage reports +/coverage diff --git a/assignments/NFT-Marketplace/README.md b/assignments/NFT-Marketplace/README.md new file mode 100644 index 00000000..968246e9 --- /dev/null +++ b/assignments/NFT-Marketplace/README.md @@ -0,0 +1,57 @@ +# Sample Hardhat 3 Beta Project (`mocha` and `ethers`) + +This project showcases a Hardhat 3 Beta project using `mocha` for tests and the `ethers` library for Ethereum interactions. + +To learn more about the Hardhat 3 Beta, please visit the [Getting Started guide](https://hardhat.org/docs/getting-started#getting-started-with-hardhat-3). To share your feedback, join our [Hardhat 3 Beta](https://hardhat.org/hardhat3-beta-telegram-group) Telegram group or [open an issue](https://github.com/NomicFoundation/hardhat/issues/new) in our GitHub issue tracker. + +## Project Overview + +This example project includes: + +- A simple Hardhat configuration file. +- Foundry-compatible Solidity unit tests. +- TypeScript integration tests using `mocha` and ethers.js +- Examples demonstrating how to connect to different types of networks, including locally simulating OP mainnet. + +## Usage + +### Running Tests + +To run all the tests in the project, execute the following command: + +```shell +npx hardhat test +``` + +You can also selectively run the Solidity or `mocha` tests: + +```shell +npx hardhat test solidity +npx hardhat test mocha +``` + +### Make a deployment to Sepolia + +This project includes an example Ignition module to deploy the contract. You can deploy this module to a locally simulated chain or to Sepolia. + +To run the deployment to a local chain: + +```shell +npx hardhat ignition deploy ignition/modules/Counter.ts +``` + +To run the deployment to Sepolia, you need an account with funds to send the transaction. The provided Hardhat configuration includes a Configuration Variable called `SEPOLIA_PRIVATE_KEY`, which you can use to set the private key of the account you want to use. + +You can set the `SEPOLIA_PRIVATE_KEY` variable using the `hardhat-keystore` plugin or by setting it as an environment variable. + +To set the `SEPOLIA_PRIVATE_KEY` config variable using `hardhat-keystore`: + +```shell +npx hardhat keystore set SEPOLIA_PRIVATE_KEY +``` + +After setting the variable, you can run the deployment with the Sepolia network: + +```shell +npx hardhat ignition deploy --network sepolia ignition/modules/Counter.ts +``` diff --git a/assignments/NFT-Marketplace/contracts/Counter.sol b/assignments/NFT-Marketplace/contracts/Counter.sol new file mode 100644 index 00000000..8d00cb7c --- /dev/null +++ b/assignments/NFT-Marketplace/contracts/Counter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Counter { + uint public x; + + event Increment(uint by); + + function inc() public { + x++; + emit Increment(1); + } + + function incBy(uint by) public { + require(by > 0, "incBy: increment should be positive"); + x += by; + emit Increment(by); + } +} diff --git a/assignments/NFT-Marketplace/contracts/Counter.t.sol b/assignments/NFT-Marketplace/contracts/Counter.t.sol new file mode 100644 index 00000000..ac71d5b8 --- /dev/null +++ b/assignments/NFT-Marketplace/contracts/Counter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; + +// Solidity tests are compatible with foundry, so they +// use the same syntax and offer the same functionality. + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_InitialValue() public view { + require(counter.x() == 0, "Initial value should be 0"); + } + + function testFuzz_Inc(uint8 x) public { + for (uint8 i = 0; i < x; i++) { + counter.inc(); + } + require(counter.x() == x, "Value after calling inc x times should be x"); + } + + function test_IncByZero() public { + vm.expectRevert(); + counter.incBy(0); + } +} diff --git a/assignments/NFT-Marketplace/contracts/track-a.sol b/assignments/NFT-Marketplace/contracts/track-a.sol new file mode 100644 index 00000000..8322dea7 --- /dev/null +++ b/assignments/NFT-Marketplace/contracts/track-a.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +poragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract StakingRewards is ReentrancyGuard, Ownable { + IERC20 public immutable stakingToken; + IERC20 public immutable rewardsToken; + + uint256 public rewardRate = 100; // Tokens per second + uint256 public lastUpdateTime; + uint256 public rewardPerTokenStored; + + mapping(address => uint256) public userRewardPerTokenPaid; + mapping(address => uint256) public rewards; + + uint256 private _totalSupply; + mapping(address => uint256) private _balances; + + constructor(address _stakingToken, address _rewardsToken) { + stakingToken = IERC20(_stakingToken); + rewardsToken = IERC20(_rewardsToken); + } + + // --- Modifiers --- + + modifier updateReward(address account) { + rewardPerTokenStored = rewardPerToken(); + lastUpdateTime = block.timestamp; + if (account != address(0)) { + rewards[account] = earned(account); + userRewardPerTokenPaid[account] = rewardPerTokenStored; + } + _; + } + + // --- Logic Functions --- + + function rewardPerToken() public view returns (uint256) { + if (_totalSupply == 0) { + return rewardPerTokenStored; + } + return rewardPerTokenStored + (rewardRate * (block.timestamp - lastUpdateTime) * 1e18 / _totalSupply); + } + + function earned(address account) public view returns (uint256) { + return (_balances[account] * (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18) + rewards[account]; + } + + // --- Functional Requirements --- + + function stake(uint256 amount) external nonReentrant updateReward(msg.sender) { + require(amount > 0, "Cannot stake 0"); + _totalSupply += amount; + _balances[msg.sender] += amount; + stakingToken.transferFrom(msg.sender, address(this), amount); + } + + function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) { + require(amount > 0 && _balances[msg.sender] >= amount, "Invalid amount"); + _totalSupply -= amount; + _balances[msg.sender] -= amount; + stakingToken.transfer(msg.sender, amount); + } + + function claimRewards() external nonReentrant updateReward(msg.sender) { + uint256 reward = rewards[msg.sender]; + if (reward > 0) { + rewards[msg.sender] = 0; + rewardsToken.transfer(msg.sender, reward); + } + } +} \ No newline at end of file diff --git a/assignments/NFT-Marketplace/contracts/track-b.sol b/assignments/NFT-Marketplace/contracts/track-b.sol new file mode 100644 index 00000000..ac743dc3 --- /dev/null +++ b/assignments/NFT-Marketplace/contracts/track-b.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract MinimalMarketplace is ReentrancyGuard, Ownable { + + struct Listing { + address seller; + address nftContract; + uint256 tokenId; + uint256 price; + bool active; + } + + uint256 public feeBps = 250; // 2.5% + address public treasury; + + // nftContract => tokenId => Listing + mapping(address => mapping(uint256 => Listing)) public listings; + + event Listed(address indexed seller, address indexed nft, uint256 indexed tokenId, uint256 price); + event Sale(address indexed buyer, address indexed nft, uint256 indexed tokenId, uint256 price); + event Canceled(address indexed seller, address indexed nft, uint256 indexed tokenId); + + constructor(address _treasury) { + treasury = _treasury; + } + + function listNft(address _nftContract, uint256 _tokenId, uint256 _price) external nonReentrant { + require(_price > 0, "Price must be > 0"); + IERC721 nft = IERC721(_nftContract); + + require(nft.ownerOf(_tokenId) == msg.sender, "Not the owner"); + require(nft.isApprovedForAll(msg.sender, address(this)), "Marketplace not approved"); + + // Escrow the NFT + nft.transferFrom(msg.sender, address(this), _tokenId); + + listings[_nftContract][_tokenId] = Listing({ + seller: msg.sender, + nftContract: _nftContract, + tokenId: _tokenId, + price: _price, + active: true + }); + + emit Listed(msg.sender, _nftContract, _tokenId, _price); + } + + function cancelListing(address _nftContract, uint256 _tokenId) external nonReentrant { + Listing storage listing = listings[_nftContract][_tokenId]; + require(listing.active, "Not active"); + require(listing.seller == msg.sender, "Not the seller"); + + listing.active = false; + IERC721(_nftContract).transferFrom(address(this), msg.sender, _tokenId); + + emit Canceled(msg.sender, _nftContract, _tokenId); + } + + function buyNft(address _nftContract, uint256 _tokenId) external payable nonReentrant { + Listing storage listing = listings[_nftContract][_tokenId]; + require(listing.active, "Listing not active"); + require(msg.value >= listing.price, "Insufficient ETH"); + + listing.active = false; // Prevent reentrancy/double buy + + uint256 fee = (listing.price * feeBps) / 10000; + uint256 sellerProceeds = listing.price - fee; + + // 1. Pay Treasury + (bool feeSuccess, ) = payable(treasury).call{value: fee}(""); + require(feeSuccess, "Fee transfer failed"); + + // 2. Pay Seller + (bool sellerSuccess, ) = payable(listing.seller).call{value: sellerProceeds}(""); + require(sellerSuccess, "Seller transfer failed"); + + // 3. Transfer NFT to Buyer + IERC721(_nftContract).transferFrom(address(this), msg.sender, _tokenId); + + // 4. Refund overpayment if any + if (msg.value > listing.price) { + payable(msg.sender).transfer(msg.value - listing.price); + } + + emit Sale(msg.sender, _nftContract, _tokenId, listing.price); + } + + function updateFee(uint256 _newFeeBps) external onlyOwner { + require(_newFeeBps <= 1000, "Fee too high"); // Cap at 10% + feeBps = _newFeeBps; + } +} \ No newline at end of file diff --git a/assignments/NFT-Marketplace/hardhat.config.ts b/assignments/NFT-Marketplace/hardhat.config.ts new file mode 100644 index 00000000..7092b852 --- /dev/null +++ b/assignments/NFT-Marketplace/hardhat.config.ts @@ -0,0 +1,38 @@ +import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers"; +import { configVariable, defineConfig } from "hardhat/config"; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + 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")], + }, + }, +}); diff --git a/assignments/NFT-Marketplace/ignition/modules/Counter.ts b/assignments/NFT-Marketplace/ignition/modules/Counter.ts new file mode 100644 index 00000000..042e61c8 --- /dev/null +++ b/assignments/NFT-Marketplace/ignition/modules/Counter.ts @@ -0,0 +1,9 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("CounterModule", (m) => { + const counter = m.contract("Counter"); + + m.call(counter, "incBy", [5n]); + + return { counter }; +}); diff --git a/assignments/NFT-Marketplace/package.json b/assignments/NFT-Marketplace/package.json new file mode 100644 index 00000000..c60cfa70 --- /dev/null +++ b/assignments/NFT-Marketplace/package.json @@ -0,0 +1,20 @@ +{ + "name": "Track-A", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.4", + "@nomicfoundation/hardhat-ignition": "^3.0.7", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.11", + "chai": "^5.3.3", + "ethers": "^6.16.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "hardhat": "^3.1.9", + "mocha": "^11.7.5", + "typescript": "~5.8.0" + } +} diff --git a/assignments/NFT-Marketplace/scripts/send-op-tx.ts b/assignments/NFT-Marketplace/scripts/send-op-tx.ts new file mode 100644 index 00000000..c10a2360 --- /dev/null +++ b/assignments/NFT-Marketplace/scripts/send-op-tx.ts @@ -0,0 +1,22 @@ +import { network } from "hardhat"; + +const { ethers } = await network.connect({ + network: "hardhatOp", + chainType: "op", +}); + +console.log("Sending transaction using the OP chain type"); + +const [sender] = await ethers.getSigners(); + +console.log("Sending 1 wei from", sender.address, "to itself"); + +console.log("Sending L2 transaction"); +const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, +}); + +await tx.wait(); + +console.log("Transaction sent successfully"); diff --git a/assignments/NFT-Marketplace/test/Counter.ts b/assignments/NFT-Marketplace/test/Counter.ts new file mode 100644 index 00000000..f8c38986 --- /dev/null +++ b/assignments/NFT-Marketplace/test/Counter.ts @@ -0,0 +1,36 @@ +import { expect } from "chai"; +import { network } from "hardhat"; + +const { ethers } = await network.connect(); + +describe("Counter", function () { + it("Should emit the Increment event when calling the inc() function", async function () { + const counter = await ethers.deployContract("Counter"); + + await expect(counter.inc()).to.emit(counter, "Increment").withArgs(1n); + }); + + it("The sum of the Increment events should match the current value", async function () { + const counter = await ethers.deployContract("Counter"); + const deploymentBlockNumber = await ethers.provider.getBlockNumber(); + + // run a series of increments + for (let i = 1; i <= 10; i++) { + await counter.incBy(i); + } + + const events = await counter.queryFilter( + counter.filters.Increment(), + deploymentBlockNumber, + "latest", + ); + + // check that the aggregated events match the current value + let total = 0n; + for (const event of events) { + total += event.args.by; + } + + expect(await counter.x()).to.equal(total); + }); +}); diff --git a/assignments/NFT-Marketplace/tsconfig.json b/assignments/NFT-Marketplace/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/assignments/NFT-Marketplace/tsconfig.json @@ -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" + } +} diff --git a/assignments/Solidity-Assignment/Todo-list/.gitignore b/assignments/Solidity-Assignment/Todo-list/.gitignore new file mode 100644 index 00000000..3fe15ba0 --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/.gitignore @@ -0,0 +1,23 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# pnpm deploy output +/bundle + +# Hardhat Build Artifacts +/artifacts + +# Hardhat compilation (v2) support directory +/cache + +# Typechain output +/types + +# Hardhat coverage reports +/coverage + +# dotenv environment variables file +.env diff --git a/assignments/Solidity-Assignment/Todo-list/README.md b/assignments/Solidity-Assignment/Todo-list/README.md new file mode 100644 index 00000000..968246e9 --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/README.md @@ -0,0 +1,57 @@ +# Sample Hardhat 3 Beta Project (`mocha` and `ethers`) + +This project showcases a Hardhat 3 Beta project using `mocha` for tests and the `ethers` library for Ethereum interactions. + +To learn more about the Hardhat 3 Beta, please visit the [Getting Started guide](https://hardhat.org/docs/getting-started#getting-started-with-hardhat-3). To share your feedback, join our [Hardhat 3 Beta](https://hardhat.org/hardhat3-beta-telegram-group) Telegram group or [open an issue](https://github.com/NomicFoundation/hardhat/issues/new) in our GitHub issue tracker. + +## Project Overview + +This example project includes: + +- A simple Hardhat configuration file. +- Foundry-compatible Solidity unit tests. +- TypeScript integration tests using `mocha` and ethers.js +- Examples demonstrating how to connect to different types of networks, including locally simulating OP mainnet. + +## Usage + +### Running Tests + +To run all the tests in the project, execute the following command: + +```shell +npx hardhat test +``` + +You can also selectively run the Solidity or `mocha` tests: + +```shell +npx hardhat test solidity +npx hardhat test mocha +``` + +### Make a deployment to Sepolia + +This project includes an example Ignition module to deploy the contract. You can deploy this module to a locally simulated chain or to Sepolia. + +To run the deployment to a local chain: + +```shell +npx hardhat ignition deploy ignition/modules/Counter.ts +``` + +To run the deployment to Sepolia, you need an account with funds to send the transaction. The provided Hardhat configuration includes a Configuration Variable called `SEPOLIA_PRIVATE_KEY`, which you can use to set the private key of the account you want to use. + +You can set the `SEPOLIA_PRIVATE_KEY` variable using the `hardhat-keystore` plugin or by setting it as an environment variable. + +To set the `SEPOLIA_PRIVATE_KEY` config variable using `hardhat-keystore`: + +```shell +npx hardhat keystore set SEPOLIA_PRIVATE_KEY +``` + +After setting the variable, you can run the deployment with the Sepolia network: + +```shell +npx hardhat ignition deploy --network sepolia ignition/modules/Counter.ts +``` diff --git a/assignments/Solidity-Assignment/Todo-list/contracts/Average2.sol b/assignments/Solidity-Assignment/Todo-list/contracts/Average2.sol new file mode 100644 index 00000000..4e0dbb52 --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/contracts/Average2.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + + +contract Calculator { + uint public a; + uint public b; + uint public c; + + constructor(uint _a, uint _b){ + a = _a; + b = _b; + } + + + function sub() public returns(uint){ + c = a - b; + + return c; + } + + function add() public returns(uint){ + c = a + b; + + return c; + } + + function div() public returns(uint){ + c = a / b; + + return c; + } + + function mul() public returns(uint){ + c = a * b; + + return c; + } + + function set(uint _a, uint _b) public{ + a = _a; + b = _b; + } + + +} + +interface ICalculation { + function add() external returns(uint); + function sub() external returns(uint); + function mul() external returns(uint); + function div() external returns(uint); + +} + +contract InteractWithCalculatorContract{ + + ICalculation calculationContract; + + constructor(address _calculationContractAddress){ + calculationContract = ICalculation(_calculationContractAddress); + } + + function addInCalculationContract() public { + calculationContract.add(); + } + +} + + + +contract AverageInheritance is Calculator(20, 4){ + + function average() public returns(uint){ + uint d = div(); + c = d / 2; + + return c; + } + + +} +// contract SubFactory{ +// Sub sub; + +// function createNewSub() public returns(address){ +// Sub _sub = new Sub(); + +// return address(_sub); +// } +// } \ No newline at end of file diff --git a/assignments/Solidity-Assignment/Todo-list/contracts/Counter.sol b/assignments/Solidity-Assignment/Todo-list/contracts/Counter.sol new file mode 100644 index 00000000..8d00cb7c --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/contracts/Counter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Counter { + uint public x; + + event Increment(uint by); + + function inc() public { + x++; + emit Increment(1); + } + + function incBy(uint by) public { + require(by > 0, "incBy: increment should be positive"); + x += by; + emit Increment(by); + } +} diff --git a/assignments/Solidity-Assignment/Todo-list/contracts/Counter.t.sol b/assignments/Solidity-Assignment/Todo-list/contracts/Counter.t.sol new file mode 100644 index 00000000..ac71d5b8 --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/contracts/Counter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; + +// Solidity tests are compatible with foundry, so they +// use the same syntax and offer the same functionality. + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_InitialValue() public view { + require(counter.x() == 0, "Initial value should be 0"); + } + + function testFuzz_Inc(uint8 x) public { + for (uint8 i = 0; i < x; i++) { + counter.inc(); + } + require(counter.x() == x, "Value after calling inc x times should be x"); + } + + function test_IncByZero() public { + vm.expectRevert(); + counter.incBy(0); + } +} diff --git a/assignments/Solidity-Assignment/Todo-list/contracts/todo.sol b/assignments/Solidity-Assignment/Todo-list/contracts/todo.sol new file mode 100644 index 00000000..1701ddca --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/contracts/todo.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Todo{ + uint256 todoCounter; + + enum Status{ + Pending, + Done, + Cancelled, + Defaulted + } + + struct TodoList{ + uint id; + address owner; + string text; + Status status; + uint256 deadline; + } + + mapping(uint => TodoList) public todos; + + event TodoCreated(string text, uint deadline); + + + +function createTodo(string memory _text, uint _deadline) external returns(uint){ + require(bytes(_text).length > 0, "Empty text"); + require(_deadline > (block.timestamp + 600), "Invalid deadline"); + require(msg.sender != address(0), "Zero address"); + + todoCounter++; + + todos[todoCounter] = TodoList(todoCounter, msg.sender, _text, Status.Pending, _deadline); + + emit TodoCreated(_text, _deadline); + return todoCounter; +} + +function markAsDone(uint _id) external { + require((_id > 0) && (_id <= todoCounter) , 'invalid id'); + TodoList storage todo = todos[_id]; + require(todo.status == Status.Pending, "Not pending"); + require(msg.sender == todo.owner, "unauthorized Caller"); + + if(block.timestamp > todo.deadline){ + todo.status = Status.Defaulted; + } + else{ + todo.status = Status.Done; + } + +} + +} \ No newline at end of file diff --git a/assignments/Solidity-Assignment/Todo-list/hardhat.config.ts b/assignments/Solidity-Assignment/Todo-list/hardhat.config.ts new file mode 100644 index 00000000..dcaa784d --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/hardhat.config.ts @@ -0,0 +1,42 @@ +import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers"; +import { configVariable, defineConfig } from "hardhat/config"; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + solidity: { + compilers: [ + { + 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")], + }, + }, + + verify: { + etherscan: { + apiKey: configVariable("ETHERSCAN_API_KEY"), + }, + }, + +}); diff --git a/assignments/Solidity-Assignment/Todo-list/ignition/modules/Counter.ts b/assignments/Solidity-Assignment/Todo-list/ignition/modules/Counter.ts new file mode 100644 index 00000000..042e61c8 --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/ignition/modules/Counter.ts @@ -0,0 +1,9 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("CounterModule", (m) => { + const counter = m.contract("Counter"); + + m.call(counter, "incBy", [5n]); + + return { counter }; +}); diff --git a/assignments/Solidity-Assignment/Todo-list/ignition/modules/todo.ts b/assignments/Solidity-Assignment/Todo-list/ignition/modules/todo.ts new file mode 100644 index 00000000..7f1b42b1 --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/ignition/modules/todo.ts @@ -0,0 +1,9 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("TodoModule", (m) => { + const todo = m.contract("Todo"); + + m.call(todo, "incBy", [5n]); + + return { todo }; +}); \ No newline at end of file diff --git a/assignments/Solidity-Assignment/Todo-list/package.json b/assignments/Solidity-Assignment/Todo-list/package.json new file mode 100644 index 00000000..ca53ceda --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/package.json @@ -0,0 +1,9 @@ +{ + "name": "todo-list", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "hardhat": "^2.22.0", + "@nomicfoundation/hardhat-toolbox": "^6.1.0" + } +} diff --git a/assignments/Solidity-Assignment/Todo-list/scripts/send-op-tx.ts b/assignments/Solidity-Assignment/Todo-list/scripts/send-op-tx.ts new file mode 100644 index 00000000..c10a2360 --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/scripts/send-op-tx.ts @@ -0,0 +1,22 @@ +import { network } from "hardhat"; + +const { ethers } = await network.connect({ + network: "hardhatOp", + chainType: "op", +}); + +console.log("Sending transaction using the OP chain type"); + +const [sender] = await ethers.getSigners(); + +console.log("Sending 1 wei from", sender.address, "to itself"); + +console.log("Sending L2 transaction"); +const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, +}); + +await tx.wait(); + +console.log("Transaction sent successfully"); diff --git a/assignments/Solidity-Assignment/Todo-list/test/Counter.ts b/assignments/Solidity-Assignment/Todo-list/test/Counter.ts new file mode 100644 index 00000000..f8c38986 --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/test/Counter.ts @@ -0,0 +1,36 @@ +import { expect } from "chai"; +import { network } from "hardhat"; + +const { ethers } = await network.connect(); + +describe("Counter", function () { + it("Should emit the Increment event when calling the inc() function", async function () { + const counter = await ethers.deployContract("Counter"); + + await expect(counter.inc()).to.emit(counter, "Increment").withArgs(1n); + }); + + it("The sum of the Increment events should match the current value", async function () { + const counter = await ethers.deployContract("Counter"); + const deploymentBlockNumber = await ethers.provider.getBlockNumber(); + + // run a series of increments + for (let i = 1; i <= 10; i++) { + await counter.incBy(i); + } + + const events = await counter.queryFilter( + counter.filters.Increment(), + deploymentBlockNumber, + "latest", + ); + + // check that the aggregated events match the current value + let total = 0n; + for (const event of events) { + total += event.args.by; + } + + expect(await counter.x()).to.equal(total); + }); +}); diff --git a/assignments/Solidity-Assignment/Todo-list/tsconfig.json b/assignments/Solidity-Assignment/Todo-list/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/assignments/Solidity-Assignment/Todo-list/tsconfig.json @@ -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" + } +} diff --git a/assignments/Solidity-Assignment/escrow-freelancer/.gitignore b/assignments/Solidity-Assignment/escrow-freelancer/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/.gitignore @@ -0,0 +1,20 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# pnpm deploy output +/bundle + +# Hardhat Build Artifacts +/artifacts + +# Hardhat compilation (v2) support directory +/cache + +# Typechain output +/types + +# Hardhat coverage reports +/coverage diff --git a/assignments/Solidity-Assignment/escrow-freelancer/.vscode_hardhat_config_1770802999882.ts b/assignments/Solidity-Assignment/escrow-freelancer/.vscode_hardhat_config_1770802999882.ts new file mode 100644 index 00000000..7092b852 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/.vscode_hardhat_config_1770802999882.ts @@ -0,0 +1,38 @@ +import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers"; +import { configVariable, defineConfig } from "hardhat/config"; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + 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")], + }, + }, +}); diff --git a/assignments/Solidity-Assignment/escrow-freelancer/README.md b/assignments/Solidity-Assignment/escrow-freelancer/README.md new file mode 100644 index 00000000..968246e9 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/README.md @@ -0,0 +1,57 @@ +# Sample Hardhat 3 Beta Project (`mocha` and `ethers`) + +This project showcases a Hardhat 3 Beta project using `mocha` for tests and the `ethers` library for Ethereum interactions. + +To learn more about the Hardhat 3 Beta, please visit the [Getting Started guide](https://hardhat.org/docs/getting-started#getting-started-with-hardhat-3). To share your feedback, join our [Hardhat 3 Beta](https://hardhat.org/hardhat3-beta-telegram-group) Telegram group or [open an issue](https://github.com/NomicFoundation/hardhat/issues/new) in our GitHub issue tracker. + +## Project Overview + +This example project includes: + +- A simple Hardhat configuration file. +- Foundry-compatible Solidity unit tests. +- TypeScript integration tests using `mocha` and ethers.js +- Examples demonstrating how to connect to different types of networks, including locally simulating OP mainnet. + +## Usage + +### Running Tests + +To run all the tests in the project, execute the following command: + +```shell +npx hardhat test +``` + +You can also selectively run the Solidity or `mocha` tests: + +```shell +npx hardhat test solidity +npx hardhat test mocha +``` + +### Make a deployment to Sepolia + +This project includes an example Ignition module to deploy the contract. You can deploy this module to a locally simulated chain or to Sepolia. + +To run the deployment to a local chain: + +```shell +npx hardhat ignition deploy ignition/modules/Counter.ts +``` + +To run the deployment to Sepolia, you need an account with funds to send the transaction. The provided Hardhat configuration includes a Configuration Variable called `SEPOLIA_PRIVATE_KEY`, which you can use to set the private key of the account you want to use. + +You can set the `SEPOLIA_PRIVATE_KEY` variable using the `hardhat-keystore` plugin or by setting it as an environment variable. + +To set the `SEPOLIA_PRIVATE_KEY` config variable using `hardhat-keystore`: + +```shell +npx hardhat keystore set SEPOLIA_PRIVATE_KEY +``` + +After setting the variable, you can run the deployment with the Sepolia network: + +```shell +npx hardhat ignition deploy --network sepolia ignition/modules/Counter.ts +``` diff --git a/assignments/Solidity-Assignment/escrow-freelancer/contracts/Counter.sol b/assignments/Solidity-Assignment/escrow-freelancer/contracts/Counter.sol new file mode 100644 index 00000000..8d00cb7c --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/contracts/Counter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Counter { + uint public x; + + event Increment(uint by); + + function inc() public { + x++; + emit Increment(1); + } + + function incBy(uint by) public { + require(by > 0, "incBy: increment should be positive"); + x += by; + emit Increment(by); + } +} diff --git a/assignments/Solidity-Assignment/escrow-freelancer/contracts/Counter.t.sol b/assignments/Solidity-Assignment/escrow-freelancer/contracts/Counter.t.sol new file mode 100644 index 00000000..ac71d5b8 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/contracts/Counter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; + +// Solidity tests are compatible with foundry, so they +// use the same syntax and offer the same functionality. + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_InitialValue() public view { + require(counter.x() == 0, "Initial value should be 0"); + } + + function testFuzz_Inc(uint8 x) public { + for (uint8 i = 0; i < x; i++) { + counter.inc(); + } + require(counter.x() == x, "Value after calling inc x times should be x"); + } + + function test_IncByZero() public { + vm.expectRevert(); + counter.incBy(0); + } +} diff --git a/assignments/Solidity-Assignment/escrow-freelancer/contracts/escrow-freelance.sol b/assignments/Solidity-Assignment/escrow-freelancer/contracts/escrow-freelance.sol new file mode 100644 index 00000000..4cefaecf --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/contracts/escrow-freelance.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity ^0.8.28; + +contract FreeLance { + address public client; + address payable public freelancer; + uint256 public totalMilestones; + uint256 public completed; + uint256 public perMilestone; + bool public isSubmitted; + + modifier only(address account) { + require(msg.sender == account, "Unauthorized"); + _; + } + + constructor(address payable _freelancer, uint256 _count) payable { + require(msg.value > 0 && _count > 0); + client = msg.sender; + freelancer = _freelancer; + totalMilestones = _count; + perMilestone = msg.value / _count; + } + + // Freelancer marks milestone as done + function submit() external only(freelancer) { + require(completed < totalMilestones && !isSubmitted, "Cannot submit"); + isSubmitted = true; + } + + // Client approves and releases ETH + function approve() external only(client) { + require(isSubmitted, "Nothing to approve"); + + isSubmitted = false; + completed++; + + // On final milestone, send the full remaining balance + uint256 amount = (completed == totalMilestones) ? address(this).balance : perMilestone; + + + (bool success, ) = freelancer.call{value: amount}(""); + require(success, "Transfer failed"); + } +} \ No newline at end of file diff --git a/assignments/Solidity-Assignment/escrow-freelancer/hardhat.config.ts b/assignments/Solidity-Assignment/escrow-freelancer/hardhat.config.ts new file mode 100644 index 00000000..7092b852 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/hardhat.config.ts @@ -0,0 +1,38 @@ +import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers"; +import { configVariable, defineConfig } from "hardhat/config"; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + 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")], + }, + }, +}); diff --git a/assignments/Solidity-Assignment/escrow-freelancer/ignition/modules/Counter.ts b/assignments/Solidity-Assignment/escrow-freelancer/ignition/modules/Counter.ts new file mode 100644 index 00000000..042e61c8 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/ignition/modules/Counter.ts @@ -0,0 +1,9 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("CounterModule", (m) => { + const counter = m.contract("Counter"); + + m.call(counter, "incBy", [5n]); + + return { counter }; +}); diff --git a/assignments/Solidity-Assignment/escrow-freelancer/package.json b/assignments/Solidity-Assignment/escrow-freelancer/package.json new file mode 100644 index 00000000..57b0c1bd --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/package.json @@ -0,0 +1,20 @@ +{ + "name": "Another-escrow", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.4", + "@nomicfoundation/hardhat-ignition": "^3.0.7", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.8", + "chai": "^5.3.3", + "ethers": "^6.16.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "hardhat": "^3.1.6", + "mocha": "^11.7.5", + "typescript": "~5.8.0" + } +} diff --git a/assignments/Solidity-Assignment/escrow-freelancer/scripts/send-op-tx.ts b/assignments/Solidity-Assignment/escrow-freelancer/scripts/send-op-tx.ts new file mode 100644 index 00000000..c10a2360 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/scripts/send-op-tx.ts @@ -0,0 +1,22 @@ +import { network } from "hardhat"; + +const { ethers } = await network.connect({ + network: "hardhatOp", + chainType: "op", +}); + +console.log("Sending transaction using the OP chain type"); + +const [sender] = await ethers.getSigners(); + +console.log("Sending 1 wei from", sender.address, "to itself"); + +console.log("Sending L2 transaction"); +const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, +}); + +await tx.wait(); + +console.log("Transaction sent successfully"); diff --git a/assignments/Solidity-Assignment/escrow-freelancer/test/Counter.ts b/assignments/Solidity-Assignment/escrow-freelancer/test/Counter.ts new file mode 100644 index 00000000..f8c38986 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/test/Counter.ts @@ -0,0 +1,36 @@ +import { expect } from "chai"; +import { network } from "hardhat"; + +const { ethers } = await network.connect(); + +describe("Counter", function () { + it("Should emit the Increment event when calling the inc() function", async function () { + const counter = await ethers.deployContract("Counter"); + + await expect(counter.inc()).to.emit(counter, "Increment").withArgs(1n); + }); + + it("The sum of the Increment events should match the current value", async function () { + const counter = await ethers.deployContract("Counter"); + const deploymentBlockNumber = await ethers.provider.getBlockNumber(); + + // run a series of increments + for (let i = 1; i <= 10; i++) { + await counter.incBy(i); + } + + const events = await counter.queryFilter( + counter.filters.Increment(), + deploymentBlockNumber, + "latest", + ); + + // check that the aggregated events match the current value + let total = 0n; + for (const event of events) { + total += event.args.by; + } + + expect(await counter.x()).to.equal(total); + }); +}); diff --git a/assignments/Solidity-Assignment/escrow-freelancer/test/freelance.ts b/assignments/Solidity-Assignment/escrow-freelancer/test/freelance.ts new file mode 100644 index 00000000..98d852e4 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/test/freelance.ts @@ -0,0 +1,140 @@ +import { expect } from "chai"; +import { network } from "hardhat"; + +describe("FreeLance Contract (Hardhat v3)", function () { + let client: any; + let freelancer: any; + let other: any; + let publicClient: any; + let freelance: any; + + const TOTAL_MILESTONES = 2n; + const TOTAL_PAYMENT = 2n * 10n ** 18n; // 2 ETH + + beforeEach(async function () { + const { viem } = await network.connect(); + + // Get wallet clients (accounts) + const wallets = await viem.getWalletClients(); + client = wallets[0]; + freelancer = wallets[1]; + other = wallets[2]; + + publicClient = await viem.getPublicClient(); + + // Deploy contract + freelance = await viem.deployContract("FreeLance", [ + freelancer.account.address, + TOTAL_MILESTONES, + ], { + value: TOTAL_PAYMENT, + account: client.account, + }); + }); + + it("Should initialize correctly", async function () { + expect(await freelance.read.client()).to.equal(client.account.address); + expect(await freelance.read.freelancer()).to.equal( + freelancer.account.address + ); + expect(await freelance.read.totalMilestones()).to.equal( + TOTAL_MILESTONES + ); + + const perMilestone = TOTAL_PAYMENT / TOTAL_MILESTONES; + + expect(await freelance.read.perMilestone()).to.equal(perMilestone); + }); + + it("Only freelancer can submit milestone", async function () { + await expect( + freelance.write.submit({ + account: other.account, + }) + ).to.be.rejected; + + await freelance.write.submit({ + account: freelancer.account, + }); + + expect(await freelance.read.isSubmitted()).to.equal(true); + }); + + it("Client cannot approve without submission", async function () { + await expect( + freelance.write.approve({ + account: client.account, + }) + ).to.be.rejected; + }); + + it("Should release payment per milestone", async function () { + const perMilestone = await freelance.read.perMilestone(); + + // Submit milestone + await freelance.write.submit({ + account: freelancer.account, + }); + + const balanceBefore = await publicClient.getBalance({ + address: freelancer.account.address, + }); + + await freelance.write.approve({ + account: client.account, + }); + + const balanceAfter = await publicClient.getBalance({ + address: freelancer.account.address, + }); + + expect(balanceAfter - balanceBefore).to.equal(perMilestone); + expect(await freelance.read.completed()).to.equal(1n); + }); + + it("Should send remaining balance on final milestone", async function () { + // First milestone + await freelance.write.submit({ + account: freelancer.account, + }); + + await freelance.write.approve({ + account: client.account, + }); + + // Second milestone + await freelance.write.submit({ + account: freelancer.account, + }); + + const balanceBefore = await publicClient.getBalance({ + address: freelancer.account.address, + }); + + await freelance.write.approve({ + account: client.account, + }); + + const balanceAfter = await publicClient.getBalance({ + address: freelancer.account.address, + }); + + expect(await freelance.read.completed()).to.equal(2n); + expect(balanceAfter > balanceBefore).to.equal(true); + }); + + it("Cannot submit after all milestones completed", async function () { + // Complete both milestones + await freelance.write.submit({ account: freelancer.account }); + await freelance.write.approve({ account: client.account }); + + await freelance.write.submit({ account: freelancer.account }); + await freelance.write.approve({ account: client.account }); + + await expect( + freelance.write.submit({ + account: freelancer.account, + }) + ).to.be.rejected; + }); +}); diff --git a/assignments/Solidity-Assignment/escrow-freelancer/tsconfig.json b/assignments/Solidity-Assignment/escrow-freelancer/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/assignments/Solidity-Assignment/escrow-freelancer/tsconfig.json @@ -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" + } +} diff --git a/assignments/Solidity-Assignment/escrow/.gitignore b/assignments/Solidity-Assignment/escrow/.gitignore new file mode 100644 index 00000000..991a319e --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/.gitignore @@ -0,0 +1,20 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# pnpm deploy output +/bundle + +# Hardhat Build Artifacts +/artifacts + +# Hardhat compilation (v2) support directory +/cache + +# Typechain output +/types + +# Hardhat coverage reports +/coverage diff --git a/assignments/Solidity-Assignment/escrow/README.md b/assignments/Solidity-Assignment/escrow/README.md new file mode 100644 index 00000000..968246e9 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/README.md @@ -0,0 +1,57 @@ +# Sample Hardhat 3 Beta Project (`mocha` and `ethers`) + +This project showcases a Hardhat 3 Beta project using `mocha` for tests and the `ethers` library for Ethereum interactions. + +To learn more about the Hardhat 3 Beta, please visit the [Getting Started guide](https://hardhat.org/docs/getting-started#getting-started-with-hardhat-3). To share your feedback, join our [Hardhat 3 Beta](https://hardhat.org/hardhat3-beta-telegram-group) Telegram group or [open an issue](https://github.com/NomicFoundation/hardhat/issues/new) in our GitHub issue tracker. + +## Project Overview + +This example project includes: + +- A simple Hardhat configuration file. +- Foundry-compatible Solidity unit tests. +- TypeScript integration tests using `mocha` and ethers.js +- Examples demonstrating how to connect to different types of networks, including locally simulating OP mainnet. + +## Usage + +### Running Tests + +To run all the tests in the project, execute the following command: + +```shell +npx hardhat test +``` + +You can also selectively run the Solidity or `mocha` tests: + +```shell +npx hardhat test solidity +npx hardhat test mocha +``` + +### Make a deployment to Sepolia + +This project includes an example Ignition module to deploy the contract. You can deploy this module to a locally simulated chain or to Sepolia. + +To run the deployment to a local chain: + +```shell +npx hardhat ignition deploy ignition/modules/Counter.ts +``` + +To run the deployment to Sepolia, you need an account with funds to send the transaction. The provided Hardhat configuration includes a Configuration Variable called `SEPOLIA_PRIVATE_KEY`, which you can use to set the private key of the account you want to use. + +You can set the `SEPOLIA_PRIVATE_KEY` variable using the `hardhat-keystore` plugin or by setting it as an environment variable. + +To set the `SEPOLIA_PRIVATE_KEY` config variable using `hardhat-keystore`: + +```shell +npx hardhat keystore set SEPOLIA_PRIVATE_KEY +``` + +After setting the variable, you can run the deployment with the Sepolia network: + +```shell +npx hardhat ignition deploy --network sepolia ignition/modules/Counter.ts +``` diff --git a/assignments/Solidity-Assignment/escrow/contracts/Counter.sol b/assignments/Solidity-Assignment/escrow/contracts/Counter.sol new file mode 100644 index 00000000..8d00cb7c --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/contracts/Counter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Counter { + uint public x; + + event Increment(uint by); + + function inc() public { + x++; + emit Increment(1); + } + + function incBy(uint by) public { + require(by > 0, "incBy: increment should be positive"); + x += by; + emit Increment(by); + } +} diff --git a/assignments/Solidity-Assignment/escrow/contracts/Counter.t.sol b/assignments/Solidity-Assignment/escrow/contracts/Counter.t.sol new file mode 100644 index 00000000..ac71d5b8 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/contracts/Counter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; + +// Solidity tests are compatible with foundry, so they +// use the same syntax and offer the same functionality. + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_InitialValue() public view { + require(counter.x() == 0, "Initial value should be 0"); + } + + function testFuzz_Inc(uint8 x) public { + for (uint8 i = 0; i < x; i++) { + counter.inc(); + } + require(counter.x() == x, "Value after calling inc x times should be x"); + } + + function test_IncByZero() public { + vm.expectRevert(); + counter.incBy(0); + } +} diff --git a/assignments/Solidity-Assignment/escrow/contracts/escrow.sol b/assignments/Solidity-Assignment/escrow/contracts/escrow.sol new file mode 100644 index 00000000..92f5d6dc --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/contracts/escrow.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +contract Escrow { + enum Status { + AWAITING_PAYMENT, + AWAITING_DELIVERY, + COMPLETED + } + + // Current status (public getter created by compiler) + Status public status; + + // Parties involved in the escrow + address public buyer; + address public seller; + + // Amount held in escrow (in wei) + uint256 public amount; + + // Initialize escrow with buyer and seller addresses + constructor(address _buyer, address _seller) { + buyer = _buyer; + seller = _seller; + status = Status.AWAITING_PAYMENT; + } + + /// Seller deposits payment + function deposit() external payable { + require(msg.sender == seller, "Only seller can deposit"); + require(status == Status.AWAITING_PAYMENT, "Already paid"); + require(msg.value > 0, "Amount must be more than zero"); + + amount = msg.value; + status = Status.AWAITING_DELIVERY; + } + + /// Buyer confirms goods received + function confirmReceipt() external { + require(msg.sender == buyer, "Only buyer can confirm"); + require(status == Status.AWAITING_DELIVERY, "Payment not made"); + + status = Status.COMPLETED; // set final state + + // Transfer the escrowed amount to the seller using `call` + // capture success boolean and revert if transfer fails + (bool success, ) = payable(seller).call{value: amount}(""); + require(success, "Transfer failed"); + } +} diff --git a/assignments/Solidity-Assignment/escrow/contracts/escrow2.sol b/assignments/Solidity-Assignment/escrow/contracts/escrow2.sol new file mode 100644 index 00000000..ae42b37d --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/contracts/escrow2.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "./escrow.sol"; + +contract EscrowFactory { + Escrow[] public escrows; + + // Event emitted when a new Escrow is created + event EscrowCreated( + address escrowAddress, + address buyer, + address seller + ); + + /// Buyer creates a new escrow order + function createEscrow(address seller) external { + Escrow escrow = new Escrow(msg.sender, seller); + escrows.push(escrow); + + emit EscrowCreated(address(escrow), msg.sender, seller); + } + + // Helper to get the number of escrows created + function getEscrowCount() external view returns (uint256) { + return escrows.length; + } +} diff --git a/assignments/Solidity-Assignment/escrow/contracts/escrow3.sol b/assignments/Solidity-Assignment/escrow/contracts/escrow3.sol new file mode 100644 index 00000000..4b903b06 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/contracts/escrow3.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +contract BasicEscrow { + // State Management + // Added AWAITING_RELEASE to give confirmDelivery a purpose + enum State { AWAITING_PAYMENT, + AWAITING_DELIVERY, + AWAITING_RELEASE, + COMPLETE } + State public currentState; + + // Participants + address public buyer; + address public seller; + address public escrowAgent; + uint256 public amount; + + // Events (Good for frontend tracking) + event FundsDeposited(address indexed buyer, uint256 amount); + event DeliveryConfirmed(address indexed seller); + event FundsReleased(address indexed seller, uint256 amount); + + // Modifiers for Access Control + modifier onlyBuyer() { + require(msg.sender == buyer, "Only the buyer can perform this action."); + _; + } + modifier onlyEscrowAgent() { + require(msg.sender == escrowAgent, "Only the escrow agent can perform this action."); + _; + + } + + constructor(address _buyer, address _seller) { + buyer = _buyer; + seller = _seller; + escrowAgent = msg.sender; + currentState = State.AWAITING_PAYMENT; + } + + /// Buyer deposits funds into the contract + function deposit() external payable onlyBuyer { + require(currentState == State.AWAITING_PAYMENT, "Already paid or complete."); + require(msg.value > 0, "Must deposit some Ether."); + + amount = msg.value; currentState = State.AWAITING_DELIVERY; + emit FundsDeposited(msg.sender, msg.value); } + + // Seller confirms delivery (updates state) + /// This fixes the "view" warning by changing currentState + function confirmDelivery() external { + require(msg.sender == seller, "Only the seller can confirm delivery."); + require(currentState == State.AWAITING_DELIVERY, "Not in delivery phase."); + + currentState = State.AWAITING_RELEASE; emit DeliveryConfirmed(msg.sender); + + } + + /// Agent releases funds to the seller + function releaseFunds() external onlyEscrowAgent { + + // Updated to require the new state + require(currentState == State.AWAITING_RELEASE, "Cannot release: Delivery not confirmed."); + currentState = State.COMPLETE; payable(seller).transfer(amount); + emit FundsReleased(seller, amount); + + } + /// Agent refunds the buyer + function refundBuyer() external onlyEscrowAgent { require(currentState != State.COMPLETE, "Cannot refund: Already complete."); + currentState = State.COMPLETE; payable(buyer).transfer(amount); + } + } \ No newline at end of file diff --git a/assignments/Solidity-Assignment/escrow/hardhat.config.ts b/assignments/Solidity-Assignment/escrow/hardhat.config.ts new file mode 100644 index 00000000..7092b852 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/hardhat.config.ts @@ -0,0 +1,38 @@ +import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers"; +import { configVariable, defineConfig } from "hardhat/config"; + +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + 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")], + }, + }, +}); diff --git a/assignments/Solidity-Assignment/escrow/ignition/modules/Counter.ts b/assignments/Solidity-Assignment/escrow/ignition/modules/Counter.ts new file mode 100644 index 00000000..042e61c8 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/ignition/modules/Counter.ts @@ -0,0 +1,9 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("CounterModule", (m) => { + const counter = m.contract("Counter"); + + m.call(counter, "incBy", [5n]); + + return { counter }; +}); diff --git a/assignments/Solidity-Assignment/escrow/package.json b/assignments/Solidity-Assignment/escrow/package.json new file mode 100644 index 00000000..535b8cd1 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/package.json @@ -0,0 +1,20 @@ +{ + "name": "escrow", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.4", + "@nomicfoundation/hardhat-ignition": "^3.0.7", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.8", + "chai": "^5.3.3", + "ethers": "^6.16.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "hardhat": "^3.1.6", + "mocha": "^11.7.5", + "typescript": "~5.8.0" + } +} diff --git a/assignments/Solidity-Assignment/escrow/scripts/send-op-tx.ts b/assignments/Solidity-Assignment/escrow/scripts/send-op-tx.ts new file mode 100644 index 00000000..c10a2360 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/scripts/send-op-tx.ts @@ -0,0 +1,22 @@ +import { network } from "hardhat"; + +const { ethers } = await network.connect({ + network: "hardhatOp", + chainType: "op", +}); + +console.log("Sending transaction using the OP chain type"); + +const [sender] = await ethers.getSigners(); + +console.log("Sending 1 wei from", sender.address, "to itself"); + +console.log("Sending L2 transaction"); +const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, +}); + +await tx.wait(); + +console.log("Transaction sent successfully"); diff --git a/assignments/Solidity-Assignment/escrow/test/Counter.ts b/assignments/Solidity-Assignment/escrow/test/Counter.ts new file mode 100644 index 00000000..f8c38986 --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/test/Counter.ts @@ -0,0 +1,36 @@ +import { expect } from "chai"; +import { network } from "hardhat"; + +const { ethers } = await network.connect(); + +describe("Counter", function () { + it("Should emit the Increment event when calling the inc() function", async function () { + const counter = await ethers.deployContract("Counter"); + + await expect(counter.inc()).to.emit(counter, "Increment").withArgs(1n); + }); + + it("The sum of the Increment events should match the current value", async function () { + const counter = await ethers.deployContract("Counter"); + const deploymentBlockNumber = await ethers.provider.getBlockNumber(); + + // run a series of increments + for (let i = 1; i <= 10; i++) { + await counter.incBy(i); + } + + const events = await counter.queryFilter( + counter.filters.Increment(), + deploymentBlockNumber, + "latest", + ); + + // check that the aggregated events match the current value + let total = 0n; + for (const event of events) { + total += event.args.by; + } + + expect(await counter.x()).to.equal(total); + }); +}); diff --git a/assignments/Solidity-Assignment/escrow/tsconfig.json b/assignments/Solidity-Assignment/escrow/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/assignments/Solidity-Assignment/escrow/tsconfig.json @@ -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" + } +}