diff --git a/courses/advanced-foundry/4-merkle-airdrop/2-project-setup/+page.md b/courses/advanced-foundry/4-merkle-airdrop/2-project-setup/+page.md index eaf390aa3..4c35d947a 100644 --- a/courses/advanced-foundry/4-merkle-airdrop/2-project-setup/+page.md +++ b/courses/advanced-foundry/4-merkle-airdrop/2-project-setup/+page.md @@ -38,13 +38,13 @@ contract BagelToken is ERC20, Ownable { ### MerkleAirdrop -We can then create a new file named `MarkleAirdrop.sol`, where we will have a list of addresses and someone from that list who can claim ERC20 tokens. +We can then create a new file named `MerkleAirdrop.sol`, where we will have a list of addresses and someone from that list who can claim ERC20 tokens. ```js // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -contract MerkleAirdrop is EIP712 { +contract MerkleAirdrop { // list of addresses that can receive tokens // allow someone in the list to claim some tokens } diff --git a/courses/advanced-foundry/4-merkle-airdrop/4-base-airdrop-contract/+page.md b/courses/advanced-foundry/4-merkle-airdrop/4-base-airdrop-contract/+page.md index eb3ac51a7..00e4e2109 100644 --- a/courses/advanced-foundry/4-merkle-airdrop/4-base-airdrop-contract/+page.md +++ b/courses/advanced-foundry/4-merkle-airdrop/4-base-airdrop-contract/+page.md @@ -8,7 +8,7 @@ _Follow along with the video_ ### Introduction -In this lesson, we are going to implement Merkle proofs and Merkle trees in our `MerkleTree.sol` contract by setting up the _constructor_ and creating a _claim_ function. +In this lesson, we are going to implement Merkle proofs and Merkle trees in our `MerkleAirdrop.sol` contract by setting up the _constructor_ and creating a _claim_ function. ### Constructor diff --git a/courses/advanced-foundry/4-merkle-airdrop/8-deployment-script/+page.md b/courses/advanced-foundry/4-merkle-airdrop/8-deployment-script/+page.md index dcd98c15f..a3de9c409 100644 --- a/courses/advanced-foundry/4-merkle-airdrop/8-deployment-script/+page.md +++ b/courses/advanced-foundry/4-merkle-airdrop/8-deployment-script/+page.md @@ -16,10 +16,10 @@ Inside the `script` directory, we can start coding the deployment contract by im // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import "MerkleAirdrop.sol"; -import "BagelToken.sol"; -import "forge-std/Script.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {MerkleAirdrop} from "src/MerkleAirdrop.sol"; +import {BagelToken} from "src/BagelToken.sol"; +import {Script} from "forge-std/Script.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; ``` Next, create the deployment function to deploy the contracts, mint tokens, and transfer them to the airdrop contract: diff --git a/courses/foundry/1-foundry-simple-storage/10-create-a-new-foundry-project/+page.md b/courses/foundry/1-foundry-simple-storage/10-create-a-new-foundry-project/+page.md new file mode 100644 index 000000000..818e66f52 --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/10-create-a-new-foundry-project/+page.md @@ -0,0 +1,86 @@ +--- +title: Create a new Foundry project +--- + +_Follow along with this video:_ + +--- + +### More setup + +Make sure we are in the folder we created in the previous lesson. + +**Reminder**: We ran the following commands + +``` +mkdir foundry-f23 +cd foundry-f23 +``` + +Now type the following commands: + +``` +mkdir foundry-simple-storage-f23 +cd foundry-simple-storage-f23 +``` + +You can always make the `cd` command faster by pressing the `Tab` key after you type the first couple of letters from the destination name. `Tab` lets you autocomplete a lot of commands/paths. + +If you type `code .` a new instance of VS Code will open, having the `foundry-simple-storage-f23` as the default path. + +You can see the contents of this folder on the left sidebar. Try the following command: + +``` +touch randomFile.txt +``` + +This will create a `randomFile.txt` + +If you want to delete it type: + +``` +rm randomFile.txt +``` + +The terminal is pretty slick when it comes to moving/opening/creating directories/files, changing paths and generally running things. I recommend going through [this tutorial](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview) if you want to learn how to move extra fast. + +### Creating a New Project + +The way you [create a new Foundry project](https://book.getfoundry.sh/projects/creating-a-new-project) is by running the `forge init` command. This will create a new Foundry project in your current working directory. + +If you want Foundry to create the new project in a new folder type `forge init nameOfNewFolder`. + +Keep in mind that by default `forge init` expects an empty folder. If your folder is not empty you must run `forge init --force .` + +Be sure to configure your username and email if you encounter errors related to Git configuration. + +``` +git config --global user.email "yourEmail@provider.com" +git config --global user.name "yourUsername" +``` + +And that's it, your folder should look as follows: + +::image{src='/foundry-simply-storage/7-create-a-new-foundry-project/Image1.PNG' style='width: 75%; height: auto;'} + +**But what does all this mean?** + +`lib` is the folder where all your dependencies are installed, here you'll find things like: + +- `forge-std` (the forge library used for testing and scripting) +- `openzeppelin-contracts` is the most battle-tested library of smart contracts +- and many more, depending on what you need/install + +`scripts` is a folder that houses all your scripts + +`src` is the folder where you put all your smart contracts + +`test` is the folder that houses all your tests + +`foundry.toml` - gives configuration parameters for Foundry + +More on these folders and files later. + +Please right-click `src`, click on `New File` and name it `SimpleStorage.sol`. Copy the code available [here](https://github.com/Cyfrin/foundry-simple-storage-f23/blob/main/src/SimpleStorage.sol). + +One last thing, please delete `Counter.s.sol`, `Counter.sol` and `Counter.t.sol`. These files are a set of basic smart contracts that Foundry provides as a default when you create a new Foundry project. diff --git a/courses/foundry/1-foundry-simple-storage/11-create-a-new-foundry-project-wsl/+page.md b/courses/foundry/1-foundry-simple-storage/11-create-a-new-foundry-project-wsl/+page.md new file mode 100644 index 000000000..8753fcd6c --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/11-create-a-new-foundry-project-wsl/+page.md @@ -0,0 +1,31 @@ +--- +title: WSL setup +--- + +_Follow along with the video_ + +--- + +### Introduction + +Hallo πŸ‘‹πŸ», I'm Vasily and I'll be your instructor for all Windows development. You'll see me frequently as I guide you through installing, running, and configuring various Windows tools. + +### WSL setup + +Microsoft has significantly improved its development environment support in recent years. However, for _smart contract development_, installing dependencies can sometimes be tricky. To streamline this process, we will use the **Windows Subsystem for Linux (WSL)**: this is a better option because it enables a full-fledged _unix-like console_ on your Windows machine, simplifying the use of tools and utilities commonly found in unix-based environments. This setup ensures compatibility with all the code that runs on unix-based systems like macOS and Linux. + +To install WSL, you can begin by opening the Windows terminal. On Windows 11, press the Windows key, type "terminal," and hit `enter`. On Windows 10, you need to install the Windows terminal from the Microsoft Store (select the official app from Microsoft Corporation). + +Once installed, open the terminal and type `wsl --install`. This command will initiate the installation process. Afterward, restart your computer. Upon reboot, the terminal will appear again, prompting you to select a Unix username and set a password. WSL will be then successfully installed βœ…. + +### Visual Studio Code + +After installing WSL, we need to install a code editor. We will use Visual Studio Code (VS Code), and there are three different methods to install it: + +1. **Using the Terminal**: Utilize `winget`, a package manager pre-installed on Windows 11. Open the terminal and type `winget search VS Code` to find the desired package. Then, execute `winget install Microsoft.VisualStudioCode` to install VS Code. + +2. **Via Web Browser**: Search for "Visual Studio Code" in your web browser, select the official Microsoft link and download the installer. Follow the prompts, accept the user agreement and customize all the installation options. + +3. **Using VSCodium**: For those who prefer more independence and privacy, there is an open-source alternative called **VSCodium**. It is similar to VS Code but without Microsoft's telemetry. Download the [VSCodium installer](https://github.com/VSCodium/vscodium/releases) from GitHub and follow its similar installation steps. + +Choose the method that best suits your needs. For this course, I will use the official Visual Studio Code from Microsoft. diff --git a/courses/foundry/1-foundry-simple-storage/12-vscode-solidity-setup/+page.md b/courses/foundry/1-foundry-simple-storage/12-vscode-solidity-setup/+page.md new file mode 100644 index 000000000..ae3566759 --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/12-vscode-solidity-setup/+page.md @@ -0,0 +1,64 @@ +--- +title: VSCode Solidity setup +--- + +_Follow along with this video:_ + +--- + +### Improving Code Format in Visual Studio Code + +When you first start, your code might just look like a whole bunch of dull, lifeless, white text. + +This can be easily fixed by using one of the `Solidity` extensions. Out of all the Solidity extensions available in the Extensions tab (CTRL/CMD + SHIFT + X) the following are worth mentioning: + +1. [Solidity by Juan Blanco](https://marketplace.visualstudio.com/items?itemName=JuanBlanco.solidity), the most used Solidity extension out there. +2. [Solidity by Nomic Foundation](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity) is Patrick's favorite Solidity extension. The rest of the course will be displaying this extension. +3. [Solidity Visual Developer](https://marketplace.visualstudio.com/items?itemName=tintinweb.solidity-visual-auditor) is another popular choice. + +**NOTE**: If the code remains unhighlighted despite having installed the extension, there's a quick solution to that. Press `Command + Shift + P`, or `Control + Shift + P` on Windows. This opens up the command bar. In the command bar, type in "Settings" and select "Preferences: Open User Settings (JSON)". + +If you have nothing in there, create a new setting by typing in: + +``` +{ + "editor.defaultFormatter": "NomicFoundation.hardhat" +} +``` + +Use: + +`"editor.defaultFormatter": "tintinweb.solidity-visual-auditor"` for Solidity Visual Developer + +or + +`"editor.defaultFormatter": "JuanBlanco.solidity"` for Solidity by Juan Blanco + +### Other interesting extensions + +In the previous lesson, we mentioned a file called `foundry.toml`. This also has an extension that formats it to make it easier to read. Please install [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml). + +Another indispensable extension is [Inline Bookmarks](https://marketplace.visualstudio.com/items?itemName=tintinweb.vscode-inline-bookmarks). + +The Inline Bookmarks plugin facilitates bookmarking the actual code. The extension can be used for document review, auditing, log analysis, and keeping track of development notes and to-do lists. You may share your notes and bookmarks with others with ease because they are saved with your files. + +The following default trigger words/tags are configured by default: +``` +@todo - (blue) General ToDo remark. +@note - (blue) General remark. +@remind - (blue) General remark. +@follow-up - (blue) General remark. +@audit - (red) General bookmark for potential issues. +@audit-info - (blue) General bookmark for information to be noted for later use. +@audit-ok - (green) Add a note that a specific line is not an issue even though it might look like. +@audit-issue - (purple) Reference a code location an issue was filed for. +``` + +You can fully customize the colors! + +Remember these! They will be very handy in developing and especially in auditing projects. + +More details are available [here](https://github.com/tintinweb/vscode-inline-bookmarks). + +Next comes the fun part! Let's compile our contract using Foundry! + diff --git a/courses/foundry/1-foundry-simple-storage/13-compile-a-smart-contract-using-foundry/+page.md b/courses/foundry/1-foundry-simple-storage/13-compile-a-smart-contract-using-foundry/+page.md new file mode 100644 index 000000000..6b820ad04 --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/13-compile-a-smart-contract-using-foundry/+page.md @@ -0,0 +1,29 @@ +--- +title: Compile a smart contract using Foundry +--- + +_Follow along with this video:_ + +--- + +### Compiling Smart Contracts: A Guide to the Foundry Console Compilation Process + +Open a new terminal. Type in `forge build` or `forge compile` to compile the smart contracts in your project. + +Once the compiling is finished, you'll see some new folders in the Explorer tab on the left side. One of them is a folder called `out`. Here you'll be able to find the [ABI](https://docs.soliditylang.org/en/latest/abi-spec.html) of the smart contract together with the [Bytecode](https://www.geeksforgeeks.org/introduction-to-bytecode-and-opcode-in-solidity/) and a lot of useful information. + +The `cache` folder also appears. Generally, this folder is used to store temporary system files facilitating the compilation process. But for this course, you can safely ignore it. + +### More terminal wizardry + +Throughout your solidity development/audit journey you will type a lot of terminal commands, every time to make a change that you want tested you'll probably have to rerun the `forge build` then maybe you test it with `forge test` or run a script with `forge script` and many more. Typing all these over and over again is inefficient and time-consuming. The better way is to use the `up` and `down` arrow keys. Type the following commands: + +``` +echo "I like Foundry" +echo "I love Cyfrin" +echo "Auditing is great" +``` + +Now press the `up` and `down` arrow keys to cycle through the 3 commands. + +Ok, cool! We learned how to compile a contract, but how does one deploy a smart contract? \ No newline at end of file diff --git a/courses/foundry/1-foundry-simple-storage/14-deploy-a-smart-contract-locally-using-ganache/+page.md b/courses/foundry/1-foundry-simple-storage/14-deploy-a-smart-contract-locally-using-ganache/+page.md new file mode 100644 index 000000000..bc9d5ae7c --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/14-deploy-a-smart-contract-locally-using-ganache/+page.md @@ -0,0 +1,93 @@ +--- +title: Deploy a smart contract locally using Ganache +--- + +_Follow along with this video:_ + + +### Deploying a smart contract + +There are multiple ways and multiple places where you could deploy a smart contract. + +While developing using the Foundry framework the easiest and most readily available place for deployment is Anvil. + +Anvil is a local testnet node shipped with Foundry. You can use it for testing your contracts from frontends or for interacting over RPC. + +To run Anvil you simply have to type `anvil` in the terminal. + +::image{src='/foundry-simply-storage/10-deploy-a-smart-contract-locally-using-ganache/Image1.PNG' style='width: 75%; height: auto;'} + +You now have access to 10 test addresses funded with 10_000 ETH each, with their associated private keys. + +This testnet node always listens on `127.0.0.1:8545` this will be our `RPC_URL` parameter when we deploy smart contracts here. More on this later! + +More info about Anvil is available [here](https://book.getfoundry.sh/reference/anvil/). + +Please press `Ctrl/CMD + C` to close Anvil. + +Anvil will be used throughout the course to deploy and test our smart contracts, but before that, let's quickly check an intermediary step. + +### Ganache + +_Ganache is a glaze, icing, sauce, or filling for pastries usually made by heating equal parts weight of cream and chopped chocolate, warming the cream first, then pouring it over the chocolate._ + +Wait, not that ganache! The other ganache: + +Ganache is a personal blockchain for rapid Ethereum and Filecoin distributed application development. You can use Ganache across the entire development cycle; enabling you to develop, deploy, and test your dApps in a safe and deterministic environment. + +Better! + +Please download Ganache from [here](https://archive.trufflesuite.com/ganache/). + +For people using Windows WSL please read [this](https://github.com/Cyfrin/foundry-simple-storage-f23?tab=readme-ov-file#windows-wsl--ganache). Using Ganache in this environment is not the easiest thing to do. We are not going to use this in the future, so don't worry if you can't configure it properly. + +Hit `Quickstart Ethereum`. Voila! A brand new blockchain. We get some addresses, that have balances and private keys. + +### Configuring MetaMask + +To deploy to a custom network (like your localhost), you'll need MetaMask. MetaMask is a popular cryptocurrency wallet and browser extension that allows users to interact with the Ethereum blockchain and its ecosystem. If you don't have it download it from [here](https://metamask.io/download/) + +Follow these steps: + +1. Open MetaMask. + +2. Click the three little dots and select 'Expand View'. + +3. Go to 'Settings', then 'Networks'. + +4. Here, you'll see the list of networks (Ethereum, Mainnet, etc.) with plenty of details about each one. Locate the RPC URL - this is key. + +The RPC URL is essentially the endpoint we make API calls to when sending transactions. For every blockchain transaction you execute, you're making an API to whatever is in here. +To send a transaction to your custom blockchain, you need to add it as a network: + +1. Click on 'Add a Network' + +2. Scroll to the bottom of the list of networks. + +3. Hit 'Add a Network manually'. + +4. Enter the details of your local network + + Network name: `Localhost` + + New RPC URL: Ganache`http://127.0.0.1:7545` or Anvil `http://127.0.0.1:8545` (make sure you always add `http://`) - these two could differ on your machine, please consult the Ganache UI or Anvil terminal for the exact RPC URL. + + Chain ID: Ganache `5777`(sometimes `1337`) or Anvil `31337` - these two could differ on your machine, please consult the Ganache UI or Anvil terminal for the exact Chain ID. + + Currency symbol: ETH + + Block explorer URL: - (we don't have a block explorer for our newly created blockchain, which will most likely disappear when we close the VS Code / Ganache app) + +Great! Now that we configured our local network, the next step is to add one of the accounts available in Ganche or Anvil into our MetaMask. [This is done as follows](https://support.metamask.io/hc/en-us/articles/360015489331-How-to-import-an-account#h_01G01W07NV7Q94M7P1EBD5BYM4): + +1. Click the account selector at the top of your wallet. + +2. Click `Add account or hardware wallet`. + +3. Click `Import account` + +4. You will be directed to the Import page. Paste your Ganache/Anvil private key. Click `Import`. + +**NOTE: Do not use this account for anything else, do not interact with it or send things to it on mainnet or any other real blockchain, use it locally, for testing purposes. Everyone has access to it.** + +Next up we shall talk more about adding a new network to MetaMask. diff --git a/courses/foundry/1-foundry-simple-storage/15-how-to-add-a-new-network-to-metamask/+page.md b/courses/foundry/1-foundry-simple-storage/15-how-to-add-a-new-network-to-metamask/+page.md new file mode 100644 index 000000000..2e69eac1b --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/15-how-to-add-a-new-network-to-metamask/+page.md @@ -0,0 +1,24 @@ +--- +title: How to add a new network to Metamask +--- + +_Follow along with this video:_ + +--- + +### Adding New Networks Using MetaMask + +Conveniently, MetaMask provides an easy way to add EVM-compatible chains. By pre-configuring a host of them, you can add a chain such as the Arbitrum One by simply clicking on the `Networks` button on the top left, then `Add Network` and proceeding to `Add`. The pleasing part is that MetaMask does all the grunt work, filling in all the necessary information for you. A click on Approve Network ensures the successful addition of the network. + +Steps: + +1. Click on the Networks button on the top left +2. Click on Add Network +3. Choose your desired EVM-compatible chain +4. Click on Add +5. After ensuring all necessary information is already filled in, click on Approve Network + +But what can you do if the chain you want to add is not pre-configured? + +Simple! You employ the same process we just used to add our new Ganache local chain in the [previous lesson](https://updraft.cyfrin.io/courses/foundry/foundry-simple-storage/deploy-smart-contract-locally) + diff --git a/courses/foundry/1-foundry-simple-storage/16-deploy-a-smart-contract-locally-using-forge/+page.md b/courses/foundry/1-foundry-simple-storage/16-deploy-a-smart-contract-locally-using-forge/+page.md new file mode 100644 index 000000000..ab3a762ce --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/16-deploy-a-smart-contract-locally-using-forge/+page.md @@ -0,0 +1,88 @@ +--- +title: Deploy a smart contract locally using Forge +--- + +_Follow along with this video:_ + +--- + +### Deploying to a local blockchain + +To find out more about forge's capabilities type + +``` +forge --help +``` + +Out of the resulting list, we are going to use the `create` command. + +Type `forge create --help` in the terminal or go [here](https://book.getfoundry.sh/reference/forge/forge-create) to find out more about the available configuration options. + +Try running `forge create SimpleStorage`. It should fail because we haven't specified a couple of required parameters: + +1. `Where do we deploy?` + +2. `Who's paying the gas fees/signing the transaction?` + +Let's tackle both these questions. + +As you've learned in the previous lessons, each blockchain (private or public) has an RPC URL (RPC SERVER) that acts as an endpoint. When we tried to deploy our smart contract, forge tried to use `http://localhost:8545/`, which doesn't host any blockchain. Thus, let's try to deploy our smart contract specifying the place where we want to deploy it. + +Please start Ganache and press `Quickstart Ethereum`. Copy the RPC Server `HTTP://127.0.0.1:7545`. Let's run our forge create again specifying the correct rpc url. + +``` +forge create SimpleStorage --rpc-url http://127.0.0.1:7545 +``` + +This again failed, indicating the following: + +``` +Error accessing local wallet. Did you set a private key, mnemonic or keystore? +``` + +Try the following command: + +``` +forge create SimpleStorage --rpc-url http://127.0.0.1:7545 --interactive +``` + +You will be asked to enter a private key, please paste one of the private keys available in Ganache. When you paste a key you won't see the text or any placeholder symbols, just press CTRL(CMD) + V and then ENTER. + +Voila! + +::image{src='/foundry-simply-storage/12-deploy-a-smart-contract-locally-using-forge/Image1.PNG' style='width: 75%; height: auto;'} + +You can go to Ganache and check the `Blocks` and `Transactions` tabs to see more info about what you just did. + +From now on, everything we deploy shall be done on Anvil. But if you like Ganache more, feel free to use that. + +Do the following: + +1. Run `clear` +2. Run `anvil` +3. Create a new terminal by pressing the `+` button +4. Copy one of the private keys from the anvil terminal +5. Run `forge create SimpleStorage --interactive` + We don't need to specify an `--rpc-url` this time because forge defaults to Anvil's RPC URL. +6. Go to the Anvil terminal and check the deployment details: + +``` + Transaction: 0x40d2ca8f0d680f098c7d5e3c127ef1ce1207ef439ba6e163c2042483e15998a6 + Contract created: 0x5fbdb2315678afecb367f032d93f642f64180aa3 + Gas used: 357076 + + Block Number: 1 + Block Hash: 0x85a56c0b8f166e86d1cce65412615e0d9a72972e04b2488023275131ea27330a + Block Time: "Mon, 15 Apr 2024 11:50:55 +0000" + +``` + +The more explicit way to deploy using `forge create` is as follows: + +``` +forge create SimpleStorage --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +We included the `--rpc-url` to not count on the default and the `--private-key` to not use the `--interactive` option anymore. + +Pfew! That was a lot, but we learned a very important thing, how to deploy a smart contract on two local blockchains. But what comes next is one of the most important if not the **_MOST IMPORTANT_** aspects you will learn here: **_Private key safety_** diff --git a/courses/foundry/1-foundry-simple-storage/17-important-private-key-safety-pt-1/+page.md b/courses/foundry/1-foundry-simple-storage/17-important-private-key-safety-pt-1/+page.md new file mode 100644 index 000000000..45cf0530f --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/17-important-private-key-safety-pt-1/+page.md @@ -0,0 +1,43 @@ +--- +title: Important - private key safety pt.1 +--- + +_Follow along with this video:_ + +--- + +### Practicing Private Key Safety + +Having a private key in plain text is extremely bad. The private key(s) we used in the last lesson are well-known keys for local testing, you shouldn't use those on mainnet and keeping them in plain text is ok, but any other private key should be kept hidden, especially your production key or key's associated with accounts that hold crypto. + +Moreover, it's very bad to have private keys in bash history (hit the up arrow and see the key you used to deploy). + +You can delete your history by typing: + +``` +history -c +``` + +We will teach you more about how to secure private keys in one of the next lessons. + +### Your Safety Promise + +It's time now to articulate your promise for maintaining private key safety. Create a file titled `Promise.md`. In this file, make it a point to write down your promise: + +``` +I promise to never use my private key associated with real money in plain text. +``` + +If you feel comfortable doing so, consider tweeting this to affirm and secure your pledge. Make sure to tag [@PatrickAlphaC](https://twitter.com/PatrickAlphaC) and [@CyfrinUpdraft](https://twitter.com/CyfrinUpdraft) or any other professional in this field to hold yourself accountable. + +Hacking private keys is one of the most important reasons people and projects lose absurd amounts. You don't even need to look that deep to find titles like this: + +[The Ronin hack](https://www.halborn.com/blog/post/explained-the-ronin-hack-march-2022) - Social engineering of private keys + +[Early Crypto Investor Bo Shen Says He Lost $42 Million in Wallet Hack](https://www.bnnbloomberg.ca/early-crypto-investor-bo-shen-says-he-lost-42-million-in-wallet-hack-1.1850446) + +[The \$477 million FTX hack](https://www.elliptic.co/blog/the-477-million-ftx-hack-following-the-blockchain-trail) where `The new CEO of FTX revealed that private keys allowing access to the firm’s crypto assets were stored in unencrypted form, and a former employee disclosed that over $150 million was stolen from Alameda Research, due to poor security. ` + +Don't be like that! Maybe you are not holding millions, but what you hold is yours, don't let it become theirs! + +In the following lessons, we'll learn how to access RPC URLs for free using Alchemy for any blockchain. We will also delve into exploring safer methodologies for dealing with private keys. Stay tuned! diff --git a/courses/foundry/1-foundry-simple-storage/18-deploy-a-smart-contract-locally-using-anvil/+page.md b/courses/foundry/1-foundry-simple-storage/18-deploy-a-smart-contract-locally-using-anvil/+page.md new file mode 100644 index 000000000..f3106f2ce --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/18-deploy-a-smart-contract-locally-using-anvil/+page.md @@ -0,0 +1,187 @@ +--- +title: Deploy a smart contract locally using Anvil +--- + +_Follow along with this video:_ + +--- + +### Deploy a smart contract locally using Anvil via scripts + +Deploying a smart contract via scripting is particularly handy because it provides a consistent and repeatable way to deploy reliably and its features enhance the testing of both the deployment processes and the code itself. + +There's a strong chance that you like the command-line approach, but scripting enriches the whole deployment process, bringing in more functionality and an ease of use second to none. + +Foundry eases the whole process since it is written in Solidity. This means our deployment scripts will also be written in Solidity. It is essential to distinguish Solidity as a contract language from Solidity as a scripting language. Foundry also incorporates elements that enhance our Solidity experience beyond the smart contracts realm. So, let's get started on creating a script to deploy our simple storage contract. + +In Foundry we keep our scripts in the `script` folder. + +Please create a new file called `DeploySimpleStorage.s.sol`. + +Using `.s.sol` as a suffix is a naming convention for Foundry scripts, in future lessons, when we'll write Foundry tests, these will bear the suffix of `.t.sol`. + +For more best practice info regarding Foundry scripts please click [here](https://book.getfoundry.sh/tutorials/best-practices#scripts). + +Open the newly created file. Here we'll write a solidity script for deploying our SimpleStorage contract. + +Type the following: + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +contract DeploySimpleStorage { + +} +``` + +The first two lines are pretty self-explanatory. + +We declare the new contract, named `DeploySimpleStorage` + +For it to be considered a Foundry script and to be able to access the extended functionality Foundry is bringing to the table we need to import `Script` from `"forge-std/Script.sol"` and make `DeploySimpleStorage` inherit `Script`. + +**NOTE**: `forge-std` also called Forge Standard Library is a collection of pre-written Solidity contracts designed to simplify and enhance scripting and testing within the Foundry development framework. + +Furthermore, to be able to deploy `SimpleStorage` we also need to import it by typing `import {SimpleStorage} from "../src/SimpleStorage.sol";` + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Script} from "forge-std/Script.sol"; +import {SimpleStorage} from "../src/SimpleStorage.sol"; + +contract DeploySimpleStorage is Script { + +} +``` + +Every script needs a main function, which, according to the best practice linked above is called `run`. Whenever you run `forge script` this is the function that gets called. + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Script} from "forge-std/Script.sol"; +import {SimpleStorage} from "../src/SimpleStorage.sol"; + +contract DeploySimpleStorage is Script { + function run() external returns (SimpleStorage) { + vm.startBroadcast(); + + SimpleStorage simpleStorage = new SimpleStorage(); + + vm.stopBroadcast(); + return simpleStorage; + } +} +``` +`run` is an external function that will return the `SimpleStorage` contract. + +In the Run function, we are going to use a distinctive keyword: `vm`. Foundry has a distinctive feature known as cheat codes. The `vm` keyword is a cheat code in Foundry, and thereby only works in Foundry. + +`vm.startBroadcast` indicates the starting point for the list of transactions that get to be sent to the `RPC URL`; + +Similarly, `vm.stopBroadcast` indicates the ending point of the list of transactions that get to be sent to the `RPC URL`; + +Between those two we write: + +`SimpleStorage simpleStorage = new SimpleStorage();` + +The `new` keyword is used to create a new smart contract in Solidity. + +We end the function with `return simpleStorage;`. + +Please select the `Anvil` terminal and press `CTRL(CMD) + C` to stop it. Now run the following command: + +```bash +forge script script/DeploySimpleStorage.s.sol +``` + +This should go through without any errors, but if you hit some errors related to `incompatible solidity versions in various files` please ensure that both the `SimpleStorage.sol` and `DeploySimpleStorage.s.sol` use `pragma solidity 0.8.19;` + +If you want to further extend your knowledge about scripting please go [here](https://book.getfoundry.sh/tutorials/solidity-scripting?highlight=scr#solidity-scripting) + +You should get the following output: + +```text +[β †] Compiling... +[β ”] Compiling 2 files with 0.8.19 +[β ’] Solc 0.8.19 finished in 1.08s +Compiler run successful! +Script ran successfully. +Gas used: 338569 + +== Return == +0: contract SimpleStorage 0x90193C961A926261B756D1E5bb255e67ff9498A1 + +If you wish to simulate on-chain transactions pass a RPC URL. +``` + +**The million-dollar question**: If we didn't pass an RPC URL, where did this deploy to? + +If the RPC URL is not specified, Foundry automatically launches an Anvil instance, runs your script (in our case deployed the contract) and then terminates the Anvil instance. + +Run the `anvil` command in the terminal, open up a new terminal and type the following: + +```bash +forge script script/DeploySimpleStorage.s.sol --rpc-url http://127.0.0.1:8545 +``` + +To get the following output: + +```text +No files changed, compilation skipped +EIP-3855 is not supported in one or more of the RPCs used. +Unsupported Chain IDs: 31337. +Contracts deployed with a Solidity version equal or higher than 0.8.20 might not work properly. +For more information, please see https://eips.ethereum.org/EIPS/eip-3855 +Script ran successfully. + +== Return == +0: contract SimpleStorage 0x34A1D3fff3958843C43aD80F30b94c510645C316 + +## Setting up 1 EVM. + +========================== + +Chain 31337 + +Estimated gas price: 2 gwei + +Estimated total gas used for script: 464097 + +Estimated amount required: 0.000928194 ETH + +========================== + +SIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more. +``` + +**Another million-dollar question**: Is it deployed now? + +Answer: No, the output indicates this was a simulation. But, we got a new folder out of this, the `broadcast` folder contains information about different script runs in case we forget details. + +Hit the up arrow key and add `--broadcast --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` at the end. + +Our contract is now successfully deployed! Fantastic! + +Switch to the `anvil` terminal where you'll see: + +```text + Transaction: 0x73eb9fb4ef7b159e03c50d669c42e2ec4eeaa9358bea0a710cb07168e5192570 + Contract created: 0x5fbdb2315678afecb367f032d93f642f64180aa3 + Gas used: 357088 + + Block Number: 1 + Block Hash: 0x8ea564f146e04bb36fc27f0b491223a023b5882d2fcfce3ff85e0dd152e611e4 + Block Time: "Tue, 16 Apr 2024 13:39:51 +0000" +``` + +Awesome! + +Through this lesson we kept talking about a thing called `transaction`, but what is a `transaction`? Tune in the next lesson to find out. diff --git a/courses/foundry/1-foundry-simple-storage/19-what-is-a-transaction/+page.md b/courses/foundry/1-foundry-simple-storage/19-what-is-a-transaction/+page.md new file mode 100644 index 000000000..2ae821403 --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/19-what-is-a-transaction/+page.md @@ -0,0 +1,56 @@ +--- +title: What is a transaction +--- + +_Follow along with this video:_ + +--- + +### More about blockchain transactions + +In the previous lesson we kept talking about transactions, but we never explained what a transaction is. In simple terms, a transaction captures details of an activity that has taken place on a blockchain. + +On the left side of your screen, in the Explorer tab, you'll find a folder called `broadcast`. Foundry saves all your blockchain interactions here. The `dry-run` folder is used for interactions you made when you didn't have a blockchain running (remember that time when we deployed our contract without specifying an `--rpc-url`). Moreover, the recordings here are separated by `chainId`. + +**Note**: The `chainId` is a unique identifier assigned to a specific blockchain network. It is used to distinguish one blockchain from another and is a crucial parameter for ensuring the security and integrity of transactions and interactions on the blockchain. + +Click on `run-latest.json`. +Here we can find more details about the last deployment script we ran in our previous lesson. It will show things like `transactionType`, `contractName` and `contractAddress`. Moreover, in the `transaction` section, you can see what we actually sent over to the RPC URL: + +```javaScript + "transaction": { + "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "to": null, + "gas": "0x714e1", + "value": "0x0", + "input": "0x608060...c63430008130033", + "nonce": "0x0", + "chainId": "0x7a69", + "accessList": null, + "type": null + } +``` + +Let's go through each of these: + +- `from` is self-explanatory, it's the address we used to sign the transaction; +- `to` is the recipient, in our case is null or address(0), this is the standard destination for when new smart contracts are deployed; +- `gas` is the amount of gas spent. You will see the hex value `0x714e1` (or any other value represented in hex format); + +**Quick tip**: Normal humans can't understand hex values like the one indicated above, but there's a quick way to convert these into usual numbers. Run the following command in your terminal: `cast --to-base 0x714e1 dec`. `cast` is a very versatile tool provided by Foundry, type `cast --help` in your terminal to find out more, or go [here](https://book.getfoundry.sh/reference/cast/cast). + +- `value` is the transaction value, or the amount of ETH we are sending over. Given that this transaction was made to deploy a contract, the value here is `0x0` or `0`, but we could have specified a value and that would have been the initial balance of the newly deployed contract; + +- `data` in this case is the contract deployment code and the contract code. In the excerpt above this was truncated; + +- `nonce` is a unique identifier assigned to each transaction sent from a specific account. The nonce is used to ensure that each transaction is processed only once and to prevent replay attacks. `nonce` is incremented with every single transaction; + +- `accessList` is a feature of Ethereum to optimize the gas cost of transactions. It contains a list of addresses and associated storage keys that the transaction is likely to access, allowing the EVM to more efficiently compute the gas cost of storage access during the transaction's execution; + +- `type` please ignore this for now. + +There are other values that play an important part that weren't presented in that list, namely the `v`, `r`, and `s`. These are components of a transaction's signature, which are used to validate the authenticity and integrity of the transaction. + +Whenever we send a transaction over the blockchain there's a signature happening, that's where we use our `private key`. + +**Important:** Every time you change the state of the blockchain you do it using a transaction. The thing that indicates the change is the `data` field of a transaction. Deployment bytecode, contract bytecode and OPCODEs will be tackled in a future lesson. diff --git a/courses/foundry/1-foundry-simple-storage/20-important-private-key-safety-pt-2/+page.md b/courses/foundry/1-foundry-simple-storage/20-important-private-key-safety-pt-2/+page.md new file mode 100644 index 000000000..251c30c62 --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/20-important-private-key-safety-pt-2/+page.md @@ -0,0 +1,53 @@ +--- +title: Important - private key safety pt.2 +--- + +_Follow along with this video:_ + +--- + +### How to not have your private key in the command line + +Some lessons ago we deployed `SimpleStorage` using the following command: + +``` +forge script script/DeploySimpleStorage.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +Having our private key in plain text is very bad, as we've explained in [Lesson 13](https://updraft.cyfrin.io/courses/foundry/foundry-simple-storage/private-key-safety). What can we do to avoid this, except using the `--interactive` parameter, because we don't want to keep copy-pasting our private key? + +**BIG BOLDED DISCLAIMER: What we are about to do is fine for development purposes, do not put a real key here, it very terrible for production purposes.** + +Create a new file in the root of your project called `.env`. Then, go the `.gitignore` file and make sure `.env` is in there. + +The `.env` file will host environment variables. Variables that are of a sensitive nature that we don't want to expose in public. + +Open the file and put the following in it: + +``` +PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +RPC_URL=http://127.0.0.1:8545 +``` + +Next run `source .env`. This adds the above-mentioned environment variables into our shell. Now run `echo $PRIVATE_KEY` or `echo $RPC_URL` to check if the values are stored in the shell. + +Now we can safely replace the parameters in our `forge script` command: + +``` +forge script script/DeploySimpleStorage.s.sol --rpc-url $RPC_URL --broadcast --private-key $PRIVATE_KEY +``` + +This doesn't only hide your private key from plain sight in the command line but also facilitates faster terminal usage, imagine you'd have to copy-paste the `http://127.0.0.1:8545` RPC URL over and over again. It's cleaner this way. + +But yes, now we have the private key in plain text in the `.env` file, that's not good. + +### How to handle this problem with production code? + +Foundry has a very nice option called `keystore`. To read more about it type `forge script --help` in your terminal. Using `forge script --keystore ` allows you to specify a path to an encrypted store file, encrypted by a password. Thus your private key would never be available in plain text. + +Let's agree to the following: + +1. For testing purposes use a `$PRIVATE_KEY` in an `.env` file as long as you don't expose that `.env` file anywhere. +2. Where real money is involved use the `--interactive` option or a [keystore file protected by a password](https://github.com/Cyfrin/foundry-full-course-f23?tab=readme-ov-file#can-you-encrypt-a-private-key---a-keystore-in-foundry-yet). + +There's one more thing about storing keys in a `.env` file. Please take a look at the ["THE .ENV PLEDGE"](https://github.com/Cyfrin/foundry-full-course-f23/discussions/5). Read it, understand it and comment `I WILL BE SAFE`. Tweet it, Tiktok it, blog about it, make an Insta story about it, print it and put it on your fridge and share some copies with your neighbors. Please stay safe! diff --git a/courses/foundry/1-foundry-simple-storage/21-never-use-a-env-file/+page.md b/courses/foundry/1-foundry-simple-storage/21-never-use-a-env-file/+page.md new file mode 100644 index 000000000..266369548 --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/21-never-use-a-env-file/+page.md @@ -0,0 +1,44 @@ +--- +title: Never Use A Env File +--- + +_Follow along with this video:_ + +--- + +### Meanwhile, some things have changed + +In our previous lesson, we showed you how to configure and use a `.env` file to hold your private key and rpc url, some developments have taken place since that lesson was made so ... You should never use a `.env` again. + +### Encrypting your Keys Using ERC2335 + +For now, let's pretend our private key is this: + +`0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` (key 0 from Anvil) + +Type the following command in your terminal: +``` +cast wallet import nameOfAccountGoesHere --interactive +``` + +Ideally, you don't do this in your VS Code. + +You will be asked for your private key and a password to secure it. You will do this only once, which is amazing! + +If you remember, last lesson we deployed running the following command: +``` +forge script script/DeploySimpleStorage.s.sol --rpc-url $RPC_URL --broadcast --private-key $PRIVATE_KEY +``` + +Now that we configured our wallet we can deploy as following: + +``` +forge script script/DeploySimpleStorage.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --account nameOfAccountGoesHere --sender 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 +``` +You will be asked for your password. You won't be able to deploy without your password. + +To see all the configured wallets you can call the following: `cast wallet list`. + +Clear your history so your private key won't randomly remain there using the following command: `history -c`. + +***Stay safe! Stay froggy! Don't lose your keys. If you are seeing your private key in plain text, you are doing something wrong.*** diff --git a/courses/foundry/1-foundry-simple-storage/22-interact-with-a-smart-contract-using-the-cli/+page.md b/courses/foundry/1-foundry-simple-storage/22-interact-with-a-smart-contract-using-the-cli/+page.md new file mode 100644 index 000000000..308709c7c --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/22-interact-with-a-smart-contract-using-the-cli/+page.md @@ -0,0 +1,64 @@ +--- +title: Interact with a smart contract using the CLI +--- + +_Follow along with this video:_ + +--- + +### Interacting With Contract Addresses via Command Line & Foundry's Cast Tool + +This lesson builds on top of previous lessons where we deployed `SimpleStorage` via `forge script`. We have `Anvil` running and the smart contract is deployed. + +Copy the contract address. + +### Sending information to the blockchain + +Foundry has a built-in tool known as `Cast`. `Cast` comes loaded with numerous commands to interact with. Learn more about them by typing `cast --help`. One such useful command is `send` which is designed to sign and publish a transaction. To view help about `send`, type `cast send --help`. + +To use `send` we need a signature and some arguments. + +Please call the following in your terminal: + +**Note**: Down below use the address you copy-pasted from your terminal, there's a chance it will be different than the one mine was deployed. + +``` +cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3 "store(uint256)" 1337 --rpc-url $RPC_URL --private-key $PRIVATE_KEY +``` + +**What did we just do?** + +Let's break it down: +- `cast send` is the command we used to sign and publish our transaction; +- `0x5FbDB2315678afecb367f032d93F642f64180aa3` or any other address is the target of our `cast send`, the contract we are interacting with; +- `"store(uint256)"` is the [signature of the function](https://ethereum.stackexchange.com/questions/135205/what-is-a-function-signature-and-function-selector-in-solidity-and-evm-language) we are calling. +- `1337` is the number we pass to the `store` function. As we can see in the function signature, we are expected to provide an `uint256` input. You can obviously provide any number you want, as long as it fits `uint256`. +- you already know what `--rpc-url $RPC_URL --private-key $PRIVATE_KEY` are. The place where we send and the private key we use to sign. + +### Reading information from the blockchain + +`cast` conveniently provides a way to read information stored on the blockchain. Type `cast call --help` in your terminal to find out more. It works similarly to `send`, where you have to provide a signature and some arguments. The difference is you are only peering into the storage, not modifying it. + +Call the following command in your terminal: + +``` +cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "retrieve()" +``` + +We receive back the following: +``` +0x0000000000000000000000000000000000000000000000000000000000000539 +``` +This represents a hex value. In the previous lessons, we learned how to convert this to a normal number. + +Type the following command in your terminal: +``` +cast --to-base 0x0000000000000000000000000000000000000000000000000000000000000539 dec +``` +And surprise, surprise, `1337` came back. + +I recommend you play around and send multiple transactions with different numbers and then read them from the blockchain. + +Awesome! We've learned something very valuable. You are going to use this more times than you can count. + +**Up next:** Deploying a smart contract on Sepolia diff --git a/courses/foundry/1-foundry-simple-storage/23-deploying-to-a-testnet/+page.md b/courses/foundry/1-foundry-simple-storage/23-deploying-to-a-testnet/+page.md new file mode 100644 index 000000000..ef4e1332d --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/23-deploying-to-a-testnet/+page.md @@ -0,0 +1,56 @@ +--- +title: Deploying to a Testnet +--- + +_Follow along the course with this video._ + + + +--- + +## Deploying our Contract to Testnet or Live Network with Foundry and Alchemy + +Hi, everyone! Are you curious about what your contract would look like on a testnet or a live network? If so, buckle up because this blog post will cover exactly that! We'll walk through the process of updating our Environment Variable (.env) file for an actual testnet. + +Clearly, we need an actual testnet for a real network. But our trusty MetaMask has built-in Infura connections that are incompatible. Why? Because they're tailored specifically for MetaMask. Hence, we need our own Remote Procedure Call (RPC) URL. + +## Creating our Own RPC URL for a Testnet + +_To create one, we could run our own blockchain node, but let's be honest β€” many folks prefer avoiding that route. Instead, we utilize Node as a Service (NaaS) applications to expedite the process._ + +One promising option is using Alchemy - a free NaaS platform that we can send the transactions to. This procedure resides within the _Deploying to Testnet or Mainnnet_ section in the full course repo of the Foundry. + +::image{src='/foundry/19-testnet-deploy/testnet1.png' style='width: 100%; height: auto;'} + +To access the Alchemy platform, we simply click on the aforementioned function. On the platform, we sign up (I used Google sign-in for this demo). + +Our next step is creating a new app in the Alchemy user interface. I named mine _Sepolia Testing_ and kept the description the same, given that our chain will be an Ethereum one based on Ethiopia. + +We can bypass advanced features for now and finalize our app. Now we have the app details needed for our node, including frequency of calls and other details. We also have a new https endpoint by clicking view key, which functions exactly the same way as our ganache or MetaMask endpoint. + +## Altering our Private Key + +Next, let's do something about our private keys. Our ganache private key will no longer cut it β€” it has neither real money nor any testnet ETH in it. + +Our solution is to use one of our MetaMask private keys. To do this, we switch back to Sepolia in our MetaMask, choose an account with money in it, click on account details, and export the private key. _Remember, never share your real private key!_ + +Upon confirmation with your password, copy the private key and omit the line in the env file β€” hashtag or pound sign denoting comments. + +## Executing the Transaction + +With our Sepolia RPC URL and private key from MetaMask, executing a transaction now becomes tremendously easier. + +```bash +source .env +forge script script/deploySimpleStorage.s.sol --rpc-url=$Sepolia_RPC_URL --private-key=$PRIVATE_KEY --broadcast +``` + +This command deploys our contract to the testnet, and we can monitor the transaction on our Alchemy dashboard. + +We soon find that our contract, Simple Storage, has been deployed on the Sepolia chain. We can grab our transaction hash and input it into Sepolia etherscan IO to confirm the successful transaction. + +After we refresh our Alchemy dashboard, we'll verify the requests sent and track the ETH send raw transaction that transmitted our transaction to the blockchain. + +So, this is how we deploy our contract on a real testnet leveraging Foundry and Alchemy! + +Our next step will explore adding real-world components to the mix. Stay tuned! diff --git a/courses/foundry/1-foundry-simple-storage/24-verify-a-smart-contract-on-etherscan/+page.md b/courses/foundry/1-foundry-simple-storage/24-verify-a-smart-contract-on-etherscan/+page.md new file mode 100644 index 000000000..bd2ca42c5 --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/24-verify-a-smart-contract-on-etherscan/+page.md @@ -0,0 +1,43 @@ +--- +title: Verify a smart contract on Etherscan +--- + +_Follow along with this video:_ + +--- + +### Manually verify a smart contract on Etherscan + +Soooo ... we just deployed our smart contract on Sepolia, let's check it out! + +We go [here](https://sepolia.etherscan.io/address/0x1093560Fe9029c4fB9044AbF2fC94288970D98Db#code) click on `Contract` and find this: + +::image{src='/foundry-simply-storage/21-verify-a-smart-contract-on-etherscan/Image1.PNG' style='width: 75%; height: auto;'} + +This bytecode looks horrendous. We need to do something to improve the readability (which currently is non-existent). + +Etherscan is prompting us to do something about it via the message: `Are you the contract creator? Verify and Publish your contract source code today!` + +So, let's click on `Verify and Publish`. + +The address of the Contract comes prepopulated, if not please paste it from your terminal. + +Select `Solidity(Single file)` because we are using solidity and we have only one file. + +Select your Compiler Version. My contract used solidity 0.8.19. + +The license type we used is MIT. + +On the next page, paste your Solidity Contract. Select `Yes` in `Optimization`, and leave everything else as is. + +Finish up the `verify` process. If you get this message: + +``` +Successfully generated Bytecode and ABI for Contract Address [0x1093560Fe9029c4fB9044AbF2fC94288970D98Db] +``` + +you did it right! + +Now you have access to the `Read Contract` and `Write Contract`. This lets you interact directly with your contract through etherscan. + +Congratz! You just learned how to verify a smart contract on etherscan, the manual way. This is not the ideal way, in the future, we will teach you how to verify them programmatically. diff --git a/courses/foundry/1-foundry-simple-storage/25-cleaning-up-the-project/+page.md b/courses/foundry/1-foundry-simple-storage/25-cleaning-up-the-project/+page.md new file mode 100644 index 000000000..7a00e1b6a --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/25-cleaning-up-the-project/+page.md @@ -0,0 +1,62 @@ +## Cleaning Up the Project + +We've covered a lot of ground in this project. We learned how to write our first Solidity contract, how to deploy it to a test network, and how to interact with it using Forge. Before we move on to more complex projects, let's take a moment to clean things up and make sure our project is well-organized. + +### Formatting + +One important aspect of any project is consistency in formatting. We've been using VS Code's auto-formatter to help us with this, but it's important to make sure that anyone else working on the project is following the same formatting rules. + +Forge has a built-in format command that we can use to ensure consistent formatting across the project. We can run this command in our terminal with: + +```bash +forge fmt +``` + +This command will automatically format all our Solidity files according to Forge's default style. + +### README.md + +Another important file to include in any project is a `README.md` file. This file serves as a guide for anyone who wants to learn about your project, how to use it, or how to contribute to it. + +The `README.md` file is written in Markdown, which is a lightweight markup language that's easy to read and write. + +Here's an example of what you might include in your `README.md` file: + +````markdown +# SimpleStorage + +This is a simple Solidity contract that stores a single uint256 value. + +## Getting Started + +1. Clone this repository. +2. Install Forge using the instructions found at [https://github.com/foundry-rs/foundry](https://github.com/foundry-rs/foundry). +3. Run the following command to compile the contract: + +```bash +forge build +``` + +4. Run the following command to deploy the contract to a test network: + +```bash +forge create +``` + +5. Interact with the contract using Forge's interactive console. + +```bash +forge console +``` + +## Contributing + +We welcome contributions to this project. If you're interested in contributing, please open an issue or submit a pull request. +``` +We can preview our `README.md` file in VS Code by going to the `View` menu and selecting `Open Preview to the Side`. This will open a new window showing what the `README.md` file will look like when it's rendered on GitHub. + +### Using AI for Markdown Formatting + +If you find that formatting Markdown is a bit tedious, you can use an AI tool like ChatGPT to help you out. Just copy and paste the text you want to format into ChatGPT and ask it to format it in Markdown. It will do a pretty good job of converting your plain text into Markdown, and you can then review and edit it as needed. + +```` diff --git a/courses/foundry/1-foundry-simple-storage/26-foundry-zksync/+page.md b/courses/foundry/1-foundry-simple-storage/26-foundry-zksync/+page.md new file mode 100644 index 000000000..accf8bb0f --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/26-foundry-zksync/+page.md @@ -0,0 +1,34 @@ +--- +title: Foundry ZKsync +--- + +_Follow along with the video_ + +--- + +In this lesson, we'll explore Layer 2 deployment on ZKsync, which involves a different compilation method compared to Ethereum. This difference arises because ZKsync uses unique opcodes. While Solidity code behaves similarly on both platforms, the low-level outputs generated by Foundry in the `/out` folder will not be entirely compatible with the ZKsync VM. + +### Foundry ZKsync + +To get started with ZKsync, we will follow these three steps: + +1. πŸ› οΈ Install `foundry-zksync` +2. πŸ§‘β€πŸ’» Compile the Solidity contract with the `--zksync` flag +3. πŸ”„πŸ  Reinstall the original Vanilla Foundry + +> πŸ‘€β—**IMPORTANT**:br +> Installing `foundry-zksync` will override any existing Foundry binaries, such as `forge` and `cast`. + +The GitHub resources for this course contain a link to the [Foundry ZKsync repository](https://github.com/Cyfrin/foundry-full-course-cu?tab=readme-ov-file#compiling-to-zksync-in-foundry-zksync). `foundry-zksync` is a fork of Foundry tailored for the ZKsync environment. The [repository](https://github.com/matter-labs/foundry-zksync) includes quick install instructions to help you set up the tool. + +- First, clone the Foundry ZKsync repository in a different directory from your Foundry project. Use the `git clone` command to clone the repository locally on your computer. + +- Once cloned, navigate to the created Foundry ZKsync directory and run the installation command: + + ``` + ./install-foundry-zksync + ``` + + This command requires a Unix-like environment, such as WSL on Windows, or a Mac or Linux system. After running the command, verify the installation by checking the version with `forge --version`. A different version number will indicate the successful installation of Foundry ZKsync. + +- To keep your environment flexible, you can switch to Foundry ZKsync by running `foundryup-zksync`. After using it, it's recommended to switch back to Vanilla Foundry by running the `foundryup` command. This removes ZKsync-specific flags and settings, allowing you to easily toggle between Foundry ZKsync and Vanilla Foundry as needed. diff --git a/courses/foundry/1-foundry-simple-storage/27-compiling-foundry-zksync/+page.md b/courses/foundry/1-foundry-simple-storage/27-compiling-foundry-zksync/+page.md new file mode 100644 index 000000000..9d06c5c60 --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/27-compiling-foundry-zksync/+page.md @@ -0,0 +1,11 @@ +--- +title: Compiling foundry ZKsync +--- + +_Follow along with the video_ + +--- + +> Previously, when we ran the `forge build` command, it generated an `/out` folder in the root project directory. This folder contains all the compilation details related the Ethereum Virtual Machine (EVM) and Vanilla Foundry. To compile for the ZKsync chain instead, we use the command `forge build --zksync`. This command creates a new folder in our project root called `/zkout`, and contains all the compiled code compatible to the ZKsync Era VM. + +If we need to revert to vanilla Foundry for deployment on the EVM, we simply run the command `foundryup` and then use `forge build`, which builds a standard Foundry project. Unless otherwise specified, we should continue using this method. diff --git a/courses/foundry/1-foundry-simple-storage/28-zksync-local-node/+page.md b/courses/foundry/1-foundry-simple-storage/28-zksync-local-node/+page.md new file mode 100644 index 000000000..fbf8cbf1c --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/28-zksync-local-node/+page.md @@ -0,0 +1,37 @@ +--- +title: ZKsync Setting up Local Node +--- + +_Follow along with the video_ + +--- + +### Introduction + +> πŸ‘€β—**IMPORTANT**:br +> This lesson is optional. If you encounter difficulties installing or understanding the required tools, feel free to proceed to the next section and continue using Anvil to test your smart contract locally. + +In the previous lessons, we learned about deploying smart contracts with the `forge create` and `forge script` commands on our **local Anvil chain**. In this lesson, we will set up and run a **ZKsync local environment**. + +### Local Node Setup + +To deploy locally on a ZKsync local chain, you'll need additional tools: Docker, Node.js, and zksync-cli. + +1. **Docker**: Start the [Docker](https://www.docker.com/) daemon. On Mac OS, you can start it using the Docker application interface. On Linux, use commands like `sudo systemctl start docker` and `sudo systemctl stop docker` will manage Docker lifecycles. Verify the installation with `docker --version` and `docker ps` commands. + +2. **Node.js and npm**: Install [Node.js](https://nodejs.org/en) and [npm](https://www.npmjs.com/). Follow the Node.js documentation to install the right version for your operating system. Verify the installations with `npm --version` and `node --version` commands. + +3. **zksync-cli**: Once Docker and Node.js are installed, you can install the [zksync-cli](https://www.npmjs.com/package/zksync-cli) to manage your local ZKsync development environment. Run `npx zksync-cli dev config` to set up your configuration. Choose the `in-memory` node option for a quick startup without persistent state and avoid additional options like a portal or block explorer unless you want to explore them independently. + +To start your local ZKsync node, run `npx zksync-cli dev start`. This command spins up a ZKsync node in Docker and runs it in the background. Verify the process is running with `docker ps`. + +> πŸ—’οΈ **NOTE**:br +> If Docker isn’t running, the `npx zksync-cli dev start` command will fail. Ensure Docker is running before attempting to start the ZKsync node again. + +### Deployment + +The ZKsync deployment process is similar to previous deployments. We will use the same commands, but this time, we will append the `--zksync` and `--legacy` flags. Note that the `forge script` command is not well supported in ZKsync, so we will use `forge create` instead. + +### Conclusion + +Setting up a local ZKsync node involves a few additional tools, including Docker, Node.js, npm, and zksync-cli: they will help creating a robust ZKsync development environment and allowing test and deployment of smart contracts on a ZKsync local chain. diff --git a/courses/foundry/1-foundry-simple-storage/29-zksync-local-deploy/+page.md b/courses/foundry/1-foundry-simple-storage/29-zksync-local-deploy/+page.md new file mode 100644 index 000000000..2d2ea270a --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/29-zksync-local-deploy/+page.md @@ -0,0 +1,30 @@ +--- +title: ZKsync Local Deployment +--- + +_Follow along with the video_ + +--- + +In this lesson, we are going to deploy the contract `SimpleStorage.sol` on a **ZKsync local chain**. + +We start by verifying that the Forge version we are using is correct. By running the `forge --version` command it confirms that we are on version 0.2: this indicates we are using the right Foundry ZKsync edition. + +Next, we proceed with creating a `SimpleStorage` contract using the command: + +```bash +forge create src/SimpleStorage.sol:SimpleStorage --rpc_url --private_key --legacy --zksync +``` + +Here, `` represents ZKsync node address, such as `http://127.0.0.1:8011`. + +> πŸ‘€β—**IMPORTANT**:br +> Including private keys directly in commands is not a safe practice. + +This command instructs Foundry to locate the `SimpleStorage` contract in the `src/SimpleStorage.sol` file and deploy it. Upon execution, the contract compiles and deploys successfully. The output will display details such as the deployer, the deployed contract address, and the transaction hash. + +::image{src='/foundry-simply-storage/27-zksync-local-deploy/deployment-successful.png' style='width: 50%; height: auto;'} + +Using the `--legacy` flag is recommended for deploying simple contracts, while more complex codebases may require different approaches. Attempting to deploy without the `--legacy` flag might result in errors like `failed to serialize transaction, address to address is null`, which will be covered in future lessons. + +Once you are finished, you can close Docker Desktop and revert to the Vanilla Foundry environment using the `foundryup` command. diff --git a/courses/foundry/1-foundry-simple-storage/30-tx-types/+page.md b/courses/foundry/1-foundry-simple-storage/30-tx-types/+page.md new file mode 100644 index 000000000..e1565ff20 --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/30-tx-types/+page.md @@ -0,0 +1,36 @@ +--- +title: Transaction Types +--- + +_Follow along with the video_ + +--- + +### Introduction + +In this lesson, we will explore the different transaction types within the ZKsync VM and EVM ecosystems. + +### `/broadcast` Folder + +When deploying to a ZKsync local node, a `/broadcast` folder will be created and it will contain detailed information about the **deployment transactions**. Inside this folder, you will find subfolders named after specific deployment chain IDs, such as **`260`** for ZKsync and **`31337`** for Anvil. These subfolders store the data of the transactions executed during the deployment process. + +By examining both the `run-latest.json` file in these folders, we can observe different **transaction types** for each transaction within a chain. For instance, transactions on the Anvil chain might be labeled as type **`0x2`**, while those on the ZKsync chain will be of type **`0x0`**. Deploying a smart contract on the EVM without the `--legacy` flag results in a default transaction type of `0x2`. Adding the `--legacy` flag changes it to type `0x0`. + +The EVM and ZKsync ecosystems support multiple transaction types to accommodate various Ethereum Improvement Proposals (EIPs). Initially, Ethereum had only one transaction type (`0x0` legacy), but as the ecosystem evolved, multiple types were introduced through various EIPs. Subsequent types include type 1, which introduces an _access list_ of addresses and keys, and type 2, also known as [EIP 1559](https://eips.ethereum.org/EIPS/eip-1559) transactions. + +> πŸ‘€β—**IMPORTANT**:br +> This `0x2` type is the current default type for the EVM. + +Additionally, ZKsync introduces its [unique transaction type](https://docs.zksync.io/zk-stack/concepts/transaction-lifecycle#eip-712-0x71), the type `113` (`0x71` in hex), which can enable features like [account abstraction](https://docs.zksync.io/build/developer-reference/account-abstraction/). + +> πŸ’‘ **TIP**:br +> The `forge script` command will work in some scenarios, but it’s not entirely clear where it might fail. For the purpose of this course, we will assume scripting does not work while working with ZKsync. + +### Resources + +- [ZKsync documentation](https://docs.zksync.io/zk-stack/concepts/transaction-lifecycle#transaction-types) about transaction types +- [Cyfrin Blog on EIP-4844](https://www.cyfrin.io/blog/what-is-eip-4844-proto-danksharding-and-blob-transactions) + +### Conclusion + +The ZKsync VM and EVM ecosystems support various transaction types to meet different EIP requirements. By examining deployment folders and understanding the use of flags like `--legacy`, we can effectively distinguish between these transaction types. diff --git a/courses/foundry/1-foundry-simple-storage/31-why-l2/+page.md b/courses/foundry/1-foundry-simple-storage/31-why-l2/+page.md new file mode 100644 index 000000000..ceaa6b3fc --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/31-why-l2/+page.md @@ -0,0 +1,30 @@ +--- +title: L2s +--- + +_Follow along with the video_ + +--- + +### Introduction + +In previous lessons, we deployed to the Sepolia testnet and started working with the Layer 2 solution ZKsync. Deploying to Sepolia simulates deployment to the Ethereum mainnet, offering a comprehensive understanding of Layer 1 deployments. However, it's important to note that most projects today prefer deploying to Layer 2 solutions rather than directly to Ethereum due to the high costs associated with deployments. + +### Gas Usage + +When deploying to a ZKsync local node, a `/broadcast` folder is created, containing a lot of detailed deployment transaction information. For instance, in our `run-latest.json` file, we can see the `gasUsed` value and we can convert this hexadecimal number `0x5747A` to its decimal equivalent by typing `cast to base 0x5747A dec`. This conversion allows us to estimate the deployment cost on the Ethereum mainnet. By checking recent gas prices on Etherscan, we can calculate the total cost using the formula: + +``` +Total Cost = Gas Used * Gas Price +``` + +We can see this total cost in the deployment transaction on [Sepolia Etherscan](https://sepolia.etherscan.io/tx/0xc496b9d30df33aa9285ddd384c14ce2a58eef470898b5cda001d0f4a21b017f6), under the `Transaction Fee` section. In this case, `357,498` gas will costs `0.000279288255846978` ETH, which today is equivalent to $7. + +Deploying even a minimal contract like `SimpleStorage` on Ethereum can be expensive. Larger contracts, with thousands of lines of code, can cost thousands of dollars. This is why many developers prefer deploying to Layer 2 solutions like ZKsync, which offer the same security as Ethereum but at a fraction of the cost. + +### Deploying to ZKsync Sepolia + +Deploying to ZKsync Sepolia is similar to deploying to a ZKsync local node. You can retrieve a ZKsync Sepolia RPC URL from [Alchemy](https://www.alchemy.com/) by creating a new app based on the ZKsepolia network. Then, you can proceed to add the `ZKSYNC_RPC_URL` to your `.env` configuration. + +> πŸ—’οΈ **NOTE**:br +> To understand the cost benefits of Layer 2 solutions, visit [L2Fees.info](https://l2fees.info) and compare the significant cost differences between sending a transaction on Ethereum and ZKsync Era. diff --git a/courses/foundry/1-foundry-simple-storage/32-alchemy-mempool/+page.md b/courses/foundry/1-foundry-simple-storage/32-alchemy-mempool/+page.md new file mode 100644 index 000000000..03a1dcb3b --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/32-alchemy-mempool/+page.md @@ -0,0 +1,82 @@ +--- +title: Alchemy & The Mempool +--- + +_Follow along the course with this video._ + + + +--- + +## Alchemy: A Game Changer for Decentralized Application Development + +Innovation in the blockchain industry has come a long way, with powerful tools making their way into the ecosystem to support developers and bring efficiency to their workflows. Among these tools is Alchemy, and today we have Vito, the lead developer experience at Alchemy, to walk us through the platform, its features, and how you can leverage it to exponentially increase your productivity. + +## What is Alchemy? + +Alchemy is a platform equipped with APIs, SDKs, and libraries to enhance your developer experience while working on Web3 projects. Think of Alchemy as the AWS of Web3. It functions as a node provider and developer tooling platform predominantly used in thousands of Web3 and Web2 applications, including large Web2 corporations like Adobe, Shopify, and Stripe. + +The need for platforms such as Alchemy arises from the fact that, as a developer, you don't usually have to worry about running the servers your code operates on or developing the deployment and integration pipelines for your application. Instead, you use services such as AWS, Azure, and Google Cloud for thatβ€”Alchemy does the same but for Web3. + +## How Does Alchemy Work? + +Alchemy enhances your developer experience through a combination of features. The platform's primary component is the _Supernode_, a proprietary blockchain engine that works as a load balancer on top of your node. + +Like its name suggests, the Supernode ensures data from the blockchain is always up-to-date and readily available. Using the Supernode as a foundation, Alchemy has built the _Enhanced APIs_β€”a set of APIs that makes pulling data from the blockchain a breeze. + +To put it simply, the Alchemy Supernode sits at the core of its ecosystem, powering up functionalities like Enhanced APIs and monitoring tools while supporting multiple chains. + +What follows is a step-by-step guide on how to create a new account on Alchemy and leverage this platform to its full extent: + +## Creating a New Account on Alchemy + +Creating an account on Alchemy is not only easy but also completely free. You can also freely scale your applications up using the platform's generous premium plans. + +#### Step 1: Navigate to Alchemy.com + +Head over to [Alchemy.com](https://www.alchemy.com/) and create a new account. + +#### Step 2: Create a New Application + +Once you have signed in, create a new application. + +Next, give your application a name and a description. Then, select a chain and network. Alchemy currently supports the majority of EVM-compatible chains, including: + +- Ethereum +- Polygon PoS +- Polygon zkEVM +- Optimism +- Arbitrum +- Solana (non-EVM chain) + +## The Application-Specific Dashboard + +Once your application is up and running, you will have access to the application-specific dashboard. This dashboard provides crucial insights into your application and infrastructure health, such as latency, compute units, and transaction success rate, which can be valuable for debugging and identifying issues. + +If you observe a lower success rate for your transactions, go to the "Recent Invalid Request" tab. This will list all unsuccessful requests along with the reasons for their failure, making it easier for you to debug and fix issues. + +::image{src='/foundry/22-alchemy/alchemy1.png' style='width: 100%; height: auto;'} + +## Mempool Watcher + +Another powerful tool provided by Alchemy is the Mempool watcher. Picture it as Ethereum's mempool, where all pending transactions reside waiting for validation or mining. + +The Mempool watcher provides extensive details about your transactions, such as: + +- Transaction status (mined, pending, dropped, replaced) +- Gas used +- Time taken for validation +- Transaction value +- Sender's and receiver's address + +This detailed transaction tracking allows you to have a better understanding of each transaction and aids immensely in debugging specific issues related to individual transactions. + +## Wrapping Up + +To sum up, Alchemy is a revolutionary platform that brings a plethora of tools to aid your Web3 development experience. From Supernode to Enhanced APIs and crucial troubleshooting tools, Alchemy is undeniably a game changer in the world of decentralized applications. + +"Alchemy can be a powerful asset to any blockchain developer, offering a simplified experience in an inherently complicated Web3 environment." – Vito, Lead Developer Experience at Alchemy. + +Vito suggests that you check out Alchemy's [documentation](https://docs.alchemy.com/) to explore more about the platform, its APIs, SDKs, libraries, and tools. Also, don't forget to follow them on Twitter at [@AlchemyPlatform](https://twitter.com/alchemyplatform) and [@AlchemyLearn](https://twitter.com/alchemyLearn). And if you want to connect directly with Vito, feel free to reach out to him on Twitter at [@VitoStack](https://twitter.com/VittoStack). + +Alchemy is revolutionizing the landscape of blockchain development and making it more accessible and efficient for everyone involved. Happy building with Alchemy! diff --git a/courses/foundry/1-foundry-simple-storage/33-summary-congratulations/+page.md b/courses/foundry/1-foundry-simple-storage/33-summary-congratulations/+page.md new file mode 100644 index 000000000..fdffe8891 --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/33-summary-congratulations/+page.md @@ -0,0 +1,47 @@ +--- +title: Summary & Congratulations +--- + +_Follow along the course with this video._ + + + +--- + +## Celebrating Milestones in Foundry: A Complete Walkthrough of Our Recent Project + +You should feel a warm sense of accomplishment envelop you. Completing an entire project in Foundry is no mean feat. A hearty congratulation is in order for such an indomitable effort. This article serves as a quick, yet comprehensive, recap of everything we learnt in our project, proceeding into our next engagement. From the onset, rest assured, we are set to advance our Foundry skills, push upcoming projects on GitHub, and familiarize ourselves with advanced tooling. + +## A Quick Trip Down Memory Lane: Key Takeaways from the Project + +Firstly, we journeyed through the process of creating a new Foundry project using Forge and Knit. These essential tools afforded us a structured, professional environment complete with folders to keep our work organized. + +We not only learnt about Foundry’s basic commands but also their specific functionalities such as: + +- **Cast**: interacts with contracts that have been previously deployed. +- **Forge**: compiles and interacts with our contracts. +- **Anvil**: deploys a local blockchain, similar to another tool we used, Ganache. + +A pivotal part of our learning process was comprehending that sending a transaction via our MetaMask is tantamount to making an HTTP post request to a particular RPC URL. A similar RPC URL can be obtained from a node-as-a-service provider like [Alchemy](https://www.alchemyapi.io/) and used to send transactions directly from our Foundry projects. + +We obtained practical knowledge on how to compile code in Foundry and write a Solidity script for its subsequent deployment. We also find it critical to ensure the security of our private keys. Hence, throughout this course, we will be using an `.env` file. But be warned when dealing with real money, having your private key in plain text is not advisable. + +## Understanding Contract Deployment and Interaction on the Blockchain + +We delved into the automation of contract deployments to a blockchain. Post-deployment, we interacted with them using the `cast` keyword and `send` to make transactions, then `cast call` to read from those contracts. + +Moreover, the knowledge on how to auto format contracts with `Forge format` was acquired. We also learnt the painstaking yet rewarding manual method of verifying our contracts on the blockchain. + +```bash +forge format my_contract.sol +``` + +::image{src='/foundry/23-summary/summary1.png' style='width: 100%; height: auto;'} + +## Looking Ahead + +With these tools in your web development arsenal, you've performed exceptionally well – and yes, you should be incredibly proud. Remember, even something as small as installing tools like `Vs code` and `Foundry` can pose great difficulties, so, you're doing fantastic. + +Take a breather. Remember, breaks enhance productivity. Till next time, continue to strive for greatness in every line of code you write! + +::image{src='/foundry/23-summary/summary2.png' style='width: 100%; height: auto;'} diff --git a/courses/foundry/1-foundry-simple-storage/4-vscode-setup-windows/+page.md b/courses/foundry/1-foundry-simple-storage/4-vscode-setup-windows/+page.md new file mode 100644 index 000000000..e69de29bb diff --git a/courses/foundry/1-foundry-simple-storage/5-vscode-wsl-extension/+page.md b/courses/foundry/1-foundry-simple-storage/5-vscode-wsl-extension/+page.md new file mode 100644 index 000000000..e69de29bb diff --git a/courses/foundry/1-foundry-simple-storage/6-develop-in-cloud-using-gitpod/+page.md b/courses/foundry/1-foundry-simple-storage/6-develop-in-cloud-using-gitpod/+page.md new file mode 100644 index 000000000..11d4b0033 --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/6-develop-in-cloud-using-gitpod/+page.md @@ -0,0 +1,21 @@ +--- +title: Develop in the cloud using Gitpod +--- + +_Follow along with this video:_ + +--- + +### Develop in the cloud using Gitpod + +**BIG BOLDED DISCLAIMER: This is not the ideal way to develop smart contracts. While using Gitpod you should never share private information, like a key or a secret phrase. That's if you like to continue being the owner of the associated accounts.** + +Gitpod is an online platform that provides cloud-based, pre-configured development environments specifically designed for working with Git repositories. It's similar to Remix IDE, but it allows you to run VS Code in a browser, on a remote server. + +Go to [Gitpod's website](https://gitpod.io/login/) and click `Continue with Github`. After that, you'll be able to create a new workspace, starting from a Github repository, using a stable version of VS Code. + +You'll be amazed to find out it looks exactly like VS Code. There's also an option to open Gitpod into your VS Code desktop version. + +Everything works like in VS Code and thus you should be able to run anything using the same commands. + +Gitpod has some [fantastic resources](https://www.gitpod.io/docs/introduction/getting-started) to get you started. diff --git a/courses/foundry/1-foundry-simple-storage/7-local-setup/+page.md b/courses/foundry/1-foundry-simple-storage/7-local-setup/+page.md new file mode 100644 index 000000000..217dcbee6 --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/7-local-setup/+page.md @@ -0,0 +1,9 @@ +--- +title: Local Setup +--- +_Follow along with the video_ +--- + +To maintain a clean terminal, especially when it becomes cluttered, you can type the command "clear" and press Enter. Alternatively, use the shortcut `command + k` on a Mac or `control + k` on Linux or Windows. + +It's important to understand the difference between the **`trash can`** and the **`x`** icons in your VS code terminal: clicking the **`x`** icon it will _hide/show the terminal_, keeping all previous content visible when reopened. Clicking on the **`trash can`** icon instead, _terminates the current terminal session_. When you reopen it, you’ll see a fresh, empty session. \ No newline at end of file diff --git a/courses/foundry/1-foundry-simple-storage/8-foundry-install/+page.md b/courses/foundry/1-foundry-simple-storage/8-foundry-install/+page.md new file mode 100644 index 000000000..2868dbb5a --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/8-foundry-install/+page.md @@ -0,0 +1,87 @@ +--- +title: Foundry Install +--- + +_Follow along the course with this video._ + + + +--- + +Welcome to this handy guide on installing and operating Foundry, a versatile tool that will add a new level of command-line ease to your developer journey. Whether you're running Windows, Linux or MacOS, we've got you covered with instructions and tips. So sit back, grab a cup of coffee, and let's dive in. + +## Prepping your Terminal + +First things first. Before we dive into installing Foundry, make sure you have your terminal set up correctly. + +If you are using Windows, you should see something like `WSL` or `Ubuntu`. Once you have your terminal environment ready, it’s time for some quick tips to help streamline your workflow. + +### Keeping your Terminal Clutter-free + +When commands pile up in your terminal, things can get a little overwhelming. Clear it up by simply typing `clear` and hitting `Enter`. Alternatively, use `Command K` if you're on a Mac or `Control K` if you're on Linux or Windows. + +**Pro tip:** This is one of my favorite keyboard shortcuts that I use all the time. + +### Understanding the Trash Can and the X + +::image{src='/foundry/5-foundryinstall/foundryinstall1.png' style='width: 100%; height: auto;'} + +The trash can and the X buttons in your terminal perform distinct functions. Hitting `X` simply hides your terminal but retains all the previous lines of code. On the other hand, trashing it essentially deletes whatever is running in it. To open up a clean terminal, hit the trash can and then pull it back using `Toggle` or `Terminal > New Terminal`. + +## Installing Foundry + +With our terminal set and some tips up our sleeve, let's progress to installing Foundry. Navigate to the [Foundry website](https://book.getfoundry.sh/getting-started/installation) and from the installation tab, fetch the command to install Foundry. + +The command would look something like this: + +```bash +curl -L https://foundry.paradigm.xyz | bash +``` + +Hit `Enter` after pasting this in your terminal. + +**Note:** You must have Internet access for this to work as it's downloading Foundry from their official website. + +## Verifying Your Installation + +After running the `curl` command, an output will appear at the bottom of your terminal indicating the detected shell and the fact that Foundry has been added to your `Path`. + +For instance, the output can be something like this: + +```bash +Detected your preferred shell is bashrc and added Foundry to Path run:source /home/user/.bashrcStart +a new terminal session to use Foundry +``` + +Now, simply type `foundryup` and `Enter` to install and update Foundry to the latest version. Whenever you want to install an update for Foundry, simply run `foundryup` again. + +This will install four components: forge, cast, anvil, and chisel. To confirm the successful installation, run `forge --version`. You should get an output indicating the Forge version as shown below. + +```bash +Forge version x.x.x +``` + +Now, here's something to remember: when you hit the trash can in the top right, it literally 'removes' the terminal. The X button, in contrast, simply hides it. + +### Is Foundry Up Not Running? + +Don't panic if this command doesn't run. You might have an issue with your path, and you might need to add Foundry to your path. In case you run into this issue, check lesson 6 of the GitHub repo associated with this course. If no debugging tips are available there, feel free to start a discussion on the course's GitHub repo. Before doing so, make sure to check if a similar discussion already exists. + +Try typing `forge --version` into your terminal. Have you received an unwelcome output saying `Forge command found`? This implies that you have to rerun the `source` command that Foundry offered during installation. + +Note: Most of the time the `bashrc` file gets loaded automatically. However, if this doesn't apply to your setup, the following lines can add the required command to the end of your `Bash profile`. This will ensure that your `bashrc` file loads by default. + +```bash +cd ~ +echo 'source /home/user/.bashrc' >> ~/.bash_profile +``` + +> this depends on your operating system, please check foundry docs to see detailed instructions. + +## Wrapping Up + +And there we have it! Congratulations on installing Foundry and prepping your terminal to work seamlessly with it. Remember, hitting snags during installation is normal, especially if you're new to this. Don't hesitate to engage with the course community via GitHub if you run into issues. + +::image{src='/foundry/5-foundryinstall/foundryinstall2.png' style='width: 100%; height: auto;'} + +Here's to many hassle-free coding sessions with Foundry! diff --git a/courses/foundry/1-foundry-simple-storage/9-setup-your-vscode/+page.md b/courses/foundry/1-foundry-simple-storage/9-setup-your-vscode/+page.md new file mode 100644 index 000000000..48f14854c --- /dev/null +++ b/courses/foundry/1-foundry-simple-storage/9-setup-your-vscode/+page.md @@ -0,0 +1,67 @@ +--- +title: Setup your VSCode +--- + +_Follow along with this video:_ + +--- + +## VS Code setup + +### Installing VS Code extensions + +1. Open the Extensions view: + +There are two ways to do this: + +Click the Extensions icon in the Activity Bar on the left side of VS Code. + +Use the shortcut Ctrl+Shift+X (Windows/Linux) or Cmd+Shift+X (Mac). + +2. Browse or search for extensions: + +The Extensions view displays featured extensions by default. + +Use the search bar to find a specific extension by name. + +3. Install the extension: + +Once you've found the extension you want, click the "Install" button. +VS Code will handle the download and installation process. + +**That's it! The extension should be ready to use within VS Code.** + + +### Integrating AI into our work + +One of the best extensions that integrates AI in our development is GitHub Copilot + +Although it's a premium service, its intuitive AI-powered code autocomplete feature could be a game-changer for you. Of course, you can choose to go with other AI extensions based on your preferences. + +You can download GitHub Copilot [here](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot). More details and answers for your GitHub Copilot-related questions are available [here](https://github.com/features/copilot/?editor=vscode#faq). + + +### Other important VS Code Tips + +***Beware the white dot***, if you see it, your work is not saved, which means your project won't behave the way you want it to behave. + +```CTRL(CMD) + ` ```opens/closes your terminal. It's the equivalent of pressing the `X` button on the top right part of your terminal. + +The `trash can` button, on the left side of the `X` button destroys the terminal, make sure you always remember the difference between these two buttons. + +Hooray! This concludes the setup part of this course. Now we get to the fun part, actually developing a project using solidity and foundry. + + +### More setup ... + +Run the following commands in your terminal: + +``` +mkdir foundry-f23 +cd foundry-f23 +``` + +`mkdir` creates a directory or subdirectory. +`cd` changes the directory. + +Moving forward, it's advisable to keep all your repositories in this folder. Thus, you'll always have a place to reference all your code. diff --git a/courses/moccasin-101/1-python-in-updraft/1-intro/+page.md b/courses/moccasin-101/1-python-in-updraft/1-intro/+page.md new file mode 100644 index 000000000..4a548e331 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/1-intro/+page.md @@ -0,0 +1,13 @@ +## Moccasin Fundamentals + +Welcome back to the Moccasin and Python Fundamental Section, where you're going to learn how to build and deploy even more advanced Vyper smart contracts, using some really powerful Python tools. The main tool that we're going to be working with is this tool called Moccasin, which is a smart contract developer framework. We're going to be teaching you the same tools that the pros use. So, buckle up and let's get froggy! + +Remix is a phenomenal tool, but pretty soon we're going to be moving over to this tool called Moccasin, which is going to give us a more professional setup for deploying and testing and working with our smart contracts. + +However, before we do that, we're actually going to teach you how to do a lot of the basics with raw Python. With no frameworks, no nothing, just Python and Vyper, that's it. And this is incredibly helpful for you as a learner because you will learn what's going on under the hood of Moccasin, and then additionally, if ever you want to do something yourself, do something manually, you can come back and just do it right in Python. + +Additionally, you will also learn a lot of the basics of Python here as well, and how we work with Python in Updraft. I'm going to be teaching you a lot of modern best practices for working with Python that I think are going to help you in the long run, help you really understand what's going on that a lot of other Python tutorials kind of skip over sometimes. And similar to everything that we've been working with so far, all of the code for all of our examples are going to be in the GitHub repo associated with this course. + +Now, you might be asking a pretty astute question here. You might be saying, "Hey, uh Patrick, why are we even going to this other tool? Why are we even going to Moccasin? Remix is working great. Like, I'm able to deploy contracts, what are you talking about?" Here's what the issue is. We had to do a lot of manual work to do anything with Remix. If I wanted to deploy this contract, I would have to go okay, first, I got to go compile it. And, I got to wait for the compile. Okay, cool. And then, I go to the run tab. Well, let's say I didn't have this contract in here. I have to okay I got to switch to the correct network that I want to be at. Okay, cool, I got to Okay, let's grab the price feed. Let's okay, then I deployed it. Um let me look in here, uh hoops, I I made a mistake. Okay, I got to un- delete that. I got to come in here you know make my change whatever it is. I got to come back. I got to re- compile. I got to go back, hit the deploy, and then I got to paste this back in or uh I don't have the write address, paste it in. Okay, maybe I maybe I make a new function or something, okay, I want to see if it actually works. I go re- deploy, I go check it works. After some time, let's say you have a 1,000 different functions and you want to test that they're all still working, when you go back and you make some small change, you don't want to have to press every single button. Press deploy. Press get contract. Make sure all your new functions are working. It's going to take you forever to get anything done in Remix. So, we want a framework that automates all of this for us, and so that we can make sure we test every single part of our code base in a reproducible manner. Smart contracts are fickle. If you have a bug, it can be a disaster. It can ruin your entire smart contract. So, we want a way to interact with test, deploy, script, and all of that stuff in a more programmatic autonomous way, rather than us manually clicking buttons. In Moccasin, we can do all of that in one command, in one click. + +Like I said, we're going to be teaching you Python and Titanaboa first, and then we're going to be moving over to Moccasin. diff --git a/courses/moccasin-101/1-python-in-updraft/10-types/+page.md b/courses/moccasin-101/1-python-in-updraft/10-types/+page.md new file mode 100644 index 000000000..41683c5ac --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/10-types/+page.md @@ -0,0 +1,61 @@ +## Types + +We can convert types from one to another using something called casting. + +For example, let's say we had this: + +```python +year = "2025" +``` + +This is technically a string because it has quotes around it. If we got rid of the quotes, it would be a number, or an int. + +But, since it has quotes, it's a string. + +What if we tried to add 25 to this year number here? We can try this: + +```python +year + 25 +``` + +This will result in an error: + +```bash +TypeError: can only concatenate str (not "int") to str +``` + +Since 25 is an int, and year is a string, Python can't perform the addition. + +To fix this, we can typecast the year variable to an int: + +```python +year_as_int = int(year) +``` + +Now, we can add 25 to the year variable: + +```python +year_as_int + 25 +``` + +This will result in 2050. + +We can also convert the result back into a string using the `str()` function: + +```python +year_string = str(year_as_int + 25) +``` + +Now, we can print the string: + +```python +print(year_string + " is 25 years after 2025") +``` + +This will print the string: + +```bash +2050 is 25 years after 2025 +``` + +This is how we can convert types in Python using casting. diff --git a/courses/moccasin-101/1-python-in-updraft/11-math/+page.md b/courses/moccasin-101/1-python-in-updraft/11-math/+page.md new file mode 100644 index 000000000..56e0ad0f0 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/11-math/+page.md @@ -0,0 +1,37 @@ +## Math in Python + +We can perform simple mathematical calculations in Python. Let's look at addition, subtraction, multiplication, and division. + +### Addition + +```python +print("add") +my_number = 5 + 5 +print(my_number) +``` + +### Subtraction + +```python +print("subtract") +my_number = 5 - 5 +print(my_number) +``` + +### Multiplication + +```python +print("multiply") +my_number = 5 * 5 +print(my_number) +``` + +### Division + +```python +print("divide") +my_number = 7 / 2 +print(my_number) +``` + +We can run the script and view the results. diff --git a/courses/moccasin-101/1-python-in-updraft/12-more-math/+page.md b/courses/moccasin-101/1-python-in-updraft/12-more-math/+page.md new file mode 100644 index 000000000..b177ce95d --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/12-more-math/+page.md @@ -0,0 +1,30 @@ +## Advanced Math + +In this lesson, we're going to take a look at advanced math operations in Python. + +We can do something called integer division. + +```python +print("integer division") +my_number = 7 // 2 +print(my_number) +``` +Integer division will round down our division to the nearest whole number. + +We can also do something called modulo. This will return the remainder of our division. + +```python +print("modulo") +my_number = 7 % 2 +print(my_number) +``` +For example, 7 divided by 2 is 3 with a remainder of 1. + +Finally, we can perform exponent operations. + +```python +print("exponents") +my_number = 2 ** 3 +print(my_number) +``` +The double multiply symbol means raise to the power of. So, 2 raised to the power of 3 is 8. diff --git a/courses/moccasin-101/1-python-in-updraft/13-funcs/+page.md b/courses/moccasin-101/1-python-in-updraft/13-funcs/+page.md new file mode 100644 index 000000000..9aadf6664 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/13-funcs/+page.md @@ -0,0 +1,98 @@ +## Functions / Methods: The "def" Keyword + +We'll create a new section and talk about functions. Functions can be written by typing out the "def" keyword. A function is a self-contained, reusable chunk of logic. + +Let's create a function that will print "hi" two times. + +```python +def print_hi_two_times(): + print("hi") + print("hi") +``` + +We can create a function by typing the "def" keyword, then give our function a name, for example, "print_hi_two_times". Open and close parentheses, and then a colon. We'll explain what the parentheses are for in a moment. + +```python +def print_hi_two_times(): + print("hi") + print("hi") +``` + +Now, hit enter and Google Colab will indent the function for us. The line doesn't start at the beginning of the line, it's tabbed in. This is because Python is an indentation-based language. We need to indent to type the body of the function. + +```python +def print_hi_two_times(): + print("hi") + print("hi") +``` + +So, the function name is "print_hi_two_times", and anything that's indented is considered part of this function. + +```python +def print_hi_two_times(): + print("hi") + print("hi") +``` + +If we print "hi" and "hi" again, but the second "hi" isn't indented, Python will see that it's not part of the function. Indenting both lines will make them part of the function. We now have two "hi" print statements. + +```python +def print_hi_two_times(): + print("hi") + print("hi") +``` + +If we run the cell, nothing happens. We don't see any "hi"s printed out. To run the function, we can copy and paste it, adding parentheses, and then run the cell. + +```python +print_hi_two_times() +``` + +This will print "hi" twice. The line with "print_hi_two_times()" is considered an invocation, or a calling of the function. The definition of the function is "def print_hi_two_times():", where we tell Python what to do when somebody calls the function. The line with "print_hi_two_times()" is where we call the function. We're essentially saying, "Hey, do this thing." + +We can also create functions that take parameters. + +```python +def print_some_word(my_word): + print(my_word) +``` + +This function takes a "my_word" parameter and prints it out. We can call this function, passing in the word "miss you mom!" + +```python +print_some_word("miss you mom!") +``` + +This will print out "miss you mom!". The "miss you mom!" is passed in as a parameter and assigned to the "my_word" variable, which is then printed. + +We can also do more interesting things with math. + +```python +def add_six(my_number): + print(my_number + 6) +``` + +This function takes a "my_number" parameter, adds six to it, and then prints the result. + +```python +add_six(10) +``` + +We can call the function, passing in 10, which should print out 16. Running the cell confirms this. + +We can also create functions that take multiple parameters. + +```python +def add_together(my_first_number, my_second_number): + print(my_first_number + my_second_number) +``` + +This function takes two parameters, adds them together, and prints the result. + +```python +add_together(10, 20) +``` + +Calling the function with 10 and 20 as parameters should print out 30. Running the cell confirms this. + +This concludes the lesson on functions. diff --git a/courses/moccasin-101/1-python-in-updraft/14-conditionals/+page.md b/courses/moccasin-101/1-python-in-updraft/14-conditionals/+page.md new file mode 100644 index 000000000..37ab68d48 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/14-conditionals/+page.md @@ -0,0 +1,117 @@ +## Conditionals (if/else Statements) + +In Python, we can use `if` / `else` statements, also known as conditionals. + +LetοΏ½s create a new chunk of code. +```python +if 7 > 5: + print("7 is greater than 5") +``` + +We can run this code as it is. + +```bash +7 is greater than 5 +``` + +Now, letοΏ½s try seeing if the numbers are equal: + +```python +if 7 == 5: + print("7 is greater than 5") +``` + +If we run this code, nothing will print because seven is not equal to five. Notice we are using a double equals sign (`==`) here and not a single equals sign (`=`). A single equals sign is used to set a value. + +To check if they are actually equal, we will use a double equals: + +```python +if 7 == 5: + print("7 is greater than 5") +``` + +If we run this, nothing gets printed because seven is not equal to five. + +Just like with functions, we have some indentation here. If we remove the indentation, Python will give us an error. + +```python +if 7 == 5: +print("7 is greater than 5") +``` + +```bash +File "", line 3 + print("7 is greater than 5") + ^ +IndentationError: expected an indented block after 'if' statement on line 2 +``` + +Python is telling us that it expected an indentation block after the if statement. + +So we will go back, indent it and run it: + +```python +if 7 == 5: + print("7 is greater than 5") +``` + +It does not print out. If we change the code to check if seven is greater than five: + +```python +if 7 > 5: + print("7 is greater than 5") +``` + +When we run it, we see this printed to the console: + +```bash +7 is greater than 5 +``` + +We can also use an `else` statement. + +```python +if 7 > 5: + print("7 is greater than 5") +else: + print("5 is greater than 7") +``` + +If we run this line, it will still print out οΏ½7 is greater than 5οΏ½. This is because what we are saying is if 7 is greater than 5, print out οΏ½7 is greater than 5οΏ½. Otherwise, print out οΏ½5 is greater than 7οΏ½. + +If we were to flip the sign around to say if 7 is less than 5: + +```python +if 7 < 5: + print("7 is greater than 5") +else: + print("5 is greater than 7") +``` + +Running this code, we see the following output: + +```bash +5 is greater than 7 +``` + +This is because the `if` condition is false and so the `else` block is executed. + +This is kind of similar to saying if true do this: + +```python +if True: + print("7 is greater than 5") +else: + print("5 is greater than 7") +``` + +And if false, don't do this, do the else. + +```python +if False: + print("7 is greater than 5") +else: + print("5 is greater than 7") +``` + +If that is a little confusing, donοΏ½t worry about it for now. It will make sense to you the more you work with conditionals. diff --git a/courses/moccasin-101/1-python-in-updraft/15-while-loops/+page.md b/courses/moccasin-101/1-python-in-updraft/15-while-loops/+page.md new file mode 100644 index 000000000..7afd80392 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/15-while-loops/+page.md @@ -0,0 +1,61 @@ +## `while` Loops + +We'll learn about `while` loops. + +Let's say we wanted to print out the word "hi" a thousand times. It would be incredibly tedious to write "hi" a thousand times. + +There sure is! In Python, we can do repetitive tasks with something called loops. And, there are many different types of loops that we can use. + +The first one is going to be the `while` loop. + +Let's say we have a variable called `my_number` that starts at 0. So, `my_number` is set to 0. + +We can then do something like: + +```python +my_number = 0 +while my_number < 3: + print("My number is " + str(my_number)) + my_number = my_number + 1 +print("Done!") +``` + +And, as long as `my_number` stays less than 3, Python will just keep looping and running this. + +Now, don't hit the play button yet, because Python would technically run into an infinite loop because `my_number` would always be less than 3, since `my_number` is never incremented. + +So, first we'll say: + +```python +print("My number is " + str(my_number)) +``` + +Then, we'll say: + +```python +my_number = my_number + 1 +``` + +And, this is how every single time we run this loop, we add 1 to the number here. + +We will then print out "Done!". + +Notice again that this `print("Done!")` is outside of the `while` loop because it's not indented. + +So, if we hit play here, and scroll down, you'll see we get "My number is 0", "My number is 1", "My number is 2", and then, "Done!". + +You see it stops because once `my_number` is set to 3, `my_number` is no longer strictly less than 3, and it will execute. + +So, we can even kind of visualize this a little better by saying, okay, `my_number` starts off as 0. Then, we enter the loop. `My_number` is 0, so it says, "while 0 is less than 3", 0 is less than 3, so it goes into the loop. Print out "My number is " Again, it's 0, so we would print out 0. And, then `my_number` equals 0 + 1, so then, `my_number` would be set to 1. + +So, "while 1 is less than 3" Print "My number is 1". And, then `my_number` equals 1 + 1, which is now 2. So, then `my_number` is then set to 2. + +And, we would read it as "while 2 is less than 3" which it is. We would print out "My number is 2". `my_number` equals 2 + 1, so `my_number` now equals 3. + +Then, we would hit "while 3 is less than 3." Now, 3 is not less than 3, 3 is equal to 3, so it will skip executing the loop and just print out the "Done!". + +So, that's how you can think of walking through these `while` loops here. + +And, let's just revert everything back and we'll hit play. + +And, boom. diff --git a/courses/moccasin-101/1-python-in-updraft/16-loops-2/+page.md b/courses/moccasin-101/1-python-in-updraft/16-loops-2/+page.md new file mode 100644 index 000000000..19ffb90df --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/16-loops-2/+page.md @@ -0,0 +1,33 @@ +## Loops: For-Range + +We've learned about while loops, but there are other ways to loop, too. Let's look at a different kind of loop called a for-range loop. + +We previously used the code: +```python +my_number = 0 +while my_number < 3: + print("My number is " + str(my_number)) + my_number = my_number + 1 +print("Done!") +``` + +We can rewrite this using a for-range loop, which is more concise than the while loop: +```python +for number in range(3): + print("My number is " + str(number)) +print("Done!") +``` + +The `for` keyword lets Python know that we are going to use a for-range loop. The loop will iterate over the numbers in `range(3)` οΏ½ that is, the numbers 0, 1, and 2. For each number, it will execute the code in the loop block (which prints "My number is" plus the number). Let's run this code and see what happens. + +We can see the output: +``` +My number is 0 +My number is 1 +My number is 2 +Done! +``` + +So, `range(3)` says to loop from 0 to 2. We can see the result in the output: the code prints out "My number is" plus the number 0, then 1, then 2. + +This is a much more concise way to accomplish the same result as the while loop we used before. In the for-range loop, we don't need to worry about incrementing a counter variable because Python does that for us automatically. diff --git a/courses/moccasin-101/1-python-in-updraft/17-loops-3/+page.md b/courses/moccasin-101/1-python-in-updraft/17-loops-3/+page.md new file mode 100644 index 000000000..fb312eb92 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/17-loops-3/+page.md @@ -0,0 +1,51 @@ +## Loops: For-Each + +We've looked at `while` loops and `for` loops. Let's look at the `for-each` loop. + +Let's say we have a list: + +```python +my_list = [0, 1, 2] +``` + +We could loop through all the elements in this list by saying `for` each element `in` my list: + +```python +for element in my_list: + print("My number is " + str(element)) + print("Done!") +``` + +We can see this is going to loop through each element in the list. So if we hit play we see `My number is 0`, `My number is 1`, and `My number is 2`. + +Let's add some more numbers to our list: + +```python +my_list = [0, 1, 2, 3, 5, 7] +``` + +And then hit play. Now, we see our original numbers 0, 1, 2, and then our new numbers 3, 5, and 7. + +Now, let's look at how to use a loop for repeating a specific task a large number of times. + +Let's say we wanted to print "Hi" 1,000 times. It would be tedious to write: + +```python +print("Hi") +print("Hi") +print("Hi") +``` + +1,000 times. Is there a better way? + +There sure is! In Python, we can do repetitive tasks with something called "loops". + +We could do something as simple as `for` number `in` range 1,000 print: + +```python +for number in range(1000): + print("My number is " + str(number)) + print("Done!") +``` + +I recommend you don't do this, because you're going to get a massive cell that's outputted here. You know what, screw it. Let's try it. `For` number `in` range 1,000. Let's go ahead and hit play. If we scroll down we see just a ton of numbers. Some of this was even truncated and we'd have to scroll for a long time. But you can see it's printing out every single number between 0 and 999. So let's leave this as three so it's a lot more digestible. diff --git a/courses/moccasin-101/1-python-in-updraft/18-workshop/+page.md b/courses/moccasin-101/1-python-in-updraft/18-workshop/+page.md new file mode 100644 index 000000000..d32b555c1 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/18-workshop/+page.md @@ -0,0 +1,52 @@ +## What Year Function Workshop + +This workshop will walk you through creating a function in Python that will take two inputs: a year and a number of days. The function will then calculate and print the year after the given number of days has passed from the beginning of the year. + +### Defining the Function + +We'll start by defining our function, which we'll name `what_year`. This function will take two input parameters: `start_year` and `days`. + +```python +def what_year(start_year, days): + +``` + +### Calculating the Years Passed + +Now we'll write some code to calculate how many years have passed from the starting year based on the `days` passed. + +```python +def what_year(start_year, days): + years_passed = days // 365 + +``` + +### Calculating the Target Year + +Next we'll write code to calculate the `target_year`. This is the year after the `days` have passed from the `start_year`. + +```python +def what_year(start_year, days): + years_passed = days // 365 + target_year = start_year + years_passed + +``` + +### Printing the Result + +Now we need to write a conditional to determine whether to print the "still" version of the sentence, or to print out the year after the days have passed. + +```python +def what_year(start_year, days): + years_passed = days // 365 + target_year = start_year + years_passed + + if years_passed > 0: + print(f"{days} days after Jan 1st, {start_year}, it will be the year {target_year}") + else: + print(f"{days} days after Jan 1st, {start_year}, it will still be the year {target_year}") +``` + +### Trying Out Our Function + +We've successfully created our What Year function. Go ahead and try it out in the Updraft code editor. diff --git a/courses/moccasin-101/1-python-in-updraft/19-recap/+page.md b/courses/moccasin-101/1-python-in-updraft/19-recap/+page.md new file mode 100644 index 000000000..42c4eb442 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/19-recap/+page.md @@ -0,0 +1,28 @@ +In this lesson, we learned how to code using Python notebooks on Google Colab. This is a commonly used method for data scientists and portfolio managers, especially those who work at hedge funds or asset managers. + +In a previous lesson, we learned about the basics of Python. We were able to write a function to calculate how many years have passed after January 1st of a given year: + +```python +def what_year(start_year, days): + # Calculate how many years have passed (integer division) + years_passed = days // 365 + target_year = start_year + years_passed + + # Technically we might be off by a day, but we are going to ignore that + if years_passed > 0: + print(f"{days} days after Jan 1st, {start_year}, it will be the year {target_year}") + else: + print(f"{days} days after Jan 1st, {start_year}, it will still be the year {target_year}") + +what_year(1985, 376) +what_year(1985, 200) +what_year(1985, 1098) +``` + +We've learned a lot in this crash course, and if you were able to come up with the solution to our problem, give yourself a round of applause. It took me a long time to learn how to code Python and learning the basics here is absolutely fantastic. There's a lot more for us to learn. + +It's a great time to take a break, go get some coffee, go for a walk, go to the gym, and I'll see you soon. + +What we're going to do next is, in my opinion, the hardest part of any Python tutorial. I feel like this is the part of Python tutorials that most educators gloss over because setting up your Python environment on your local computer can be incredibly challenging. So we're going to spend the time to make sure we get you set up correctly so that you can be successful. + +I do actually want you to take at least a 30 minute break before going to this next section because I want some of the information here to settle in. And, guess what, your brain cannot process too much information in a day, it will do a worse job of learning that information. So, pause for at least a half an hour if you've already been coding for a while today. Pause for the whole day because installing Python and getting your environment set up correctly can take a long time. And if it does take a long time, don't be frustrated. It took me a long time the first time I got it set up. But, if you can get through installing Python and installing your environment the correct way, you can do anything, because it's honestly the hardest part of all of this. So, take a break and I'll see you soon. diff --git a/courses/moccasin-101/1-python-in-updraft/2-google-collab/+page.md b/courses/moccasin-101/1-python-in-updraft/2-google-collab/+page.md new file mode 100644 index 000000000..7e20340f7 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/2-google-collab/+page.md @@ -0,0 +1,45 @@ +## Google Colab Python Cells + +In this lesson, we will walk through a quick crash course in Python using the Google Colab tool. + +Google Colab is an excellent tool for running Python code online in your browser. You don't have to download or install anything to get started. Colab is similar to the way we were able to run everything online in the browser in previous lessons. + +Let's start with Google Colab: + +We will walk through setting up your development environment on MacOS and Linux, and Windows with WSL. Microsoft recommends installing Python from the Windows store, using PowerShell or using a different method, but we won't be doing that. Instead, we will teach you the Linux-based way, which is how many developers work, maybe even 99% of smart contract developers. + +We will first go over Google Colab and complete a Python crash course. Then, we will learn how to install and set up Python on your computers. + +We are going to skip all the installation and setup sections of the local development environment. If you are already comfortable with Python, feel free to do so as well. + +Let's start with Google Colab: +- We will be going over the completed code for the Google Colab. +- It's located here: [https://colab.research.google.com/drive/1OhOh0TtmJjlMKXwnbD-00ckXP3supNCwf7usps-sharing](https://colab.research.google.com/drive/1OhOh0TtmJjlMKXwnbD-00ckXP3supNCwf7usps-sharing) +- We'll do a brief walkthrough of everything you will see. + +**Python Crash Course:** + +We will walk through a Python crash course in the Google Colab. + +We will be teaching you Python, but we're also teaching you smart contract development. + +**Terminal Commands:** +```bash +code . +``` + +**Python in Updraft** + +We will cover how to install and set up Python locally. We will not be covering the Microsoft way of using PowerShell or the Windows store. We will be teaching you the Linux-based way which is how most developers work. + +In our opinion, setting up Python locally is the hardest part of this whole course. + +**Gitpod or Codespaces** + +If you are comfortable with Python and have a good foundation, you may choose to skip the setup sections. + +We will then go over a Python crash course, and then cover how to install and set up Python locally. Again, we will be skipping the Gitpod or Codespaces sections. Don't skip the Python in Updraft sections, because I promise you, the Python in Updraft section will be different from what you are used to. + +**Recap:** + +We have given you a heads up that these are the steps we will be taking. We will start by walking through a Python crash course with Google Colab. Then, we will learn how to install and set up Python locally. diff --git a/courses/moccasin-101/1-python-in-updraft/20-dev-env-setup/+page.md b/courses/moccasin-101/1-python-in-updraft/20-dev-env-setup/+page.md new file mode 100644 index 000000000..a7b9f99d7 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/20-dev-env-setup/+page.md @@ -0,0 +1,13 @@ +## Development Environment Setup Introduction + +In this lesson, we'll be looking at installing Visual Studio Code. We'll go through three different installation methods, for MacOS/Linux, Windows, and Gitpod/Codespaces. However, we recommend against using Gitpod or Codespaces because you can quickly run out of your free trial on those platforms. + +Visual Studio Code is often referred to as VS Code. It's a great tool for us to use in this course, as it'll make our development experience a lot easier. We'll also discuss the benefits of using Visual Studio Code and how to avoid common pitfalls. We'll be using an AI to help us debug any issues that we run into during this course. + +Let's start with Visual Studio Code. Visual Studio Code is a standalone source code editor that runs on Windows, MacOS, and Linux. The top pick for JavaScript and web developers, it works with extensions to support just about any programming language. + +For MacOS and Linux, we're going to download and install the Visual Studio Code application. The process is very straightforward. We'll use the download link provided on their website, [code.visualstudio.com](code.visualstudio.com). + +For Windows, we'll download and install the Visual Studio Code application using a different method. The process is similar to installing it on MacOS and Linux. We'll again use the download link provided on their website, [code.visualstudio.com](code.visualstudio.com). + +In the next few sections, we'll go through setting up Visual Studio Code the way we like it, but, of course, you can set it up however you like. diff --git a/courses/moccasin-101/1-python-in-updraft/21-install-vscode-macos-linux/+page.md b/courses/moccasin-101/1-python-in-updraft/21-install-vscode-macos-linux/+page.md new file mode 100644 index 000000000..d6e7f0b95 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/21-install-vscode-macos-linux/+page.md @@ -0,0 +1,7 @@ +## Installing VS Code for Linux and macOS Users + +We will now be setting up our development environment. This will be one of the hardest parts of the whole course. So, if it takes you a long time, that's okay! If you're able to solve everything quickly, great! Please use the discussions forums, AI, and search engines if you get stuck on any part. + +The first thing we're going to want to do is download VS Code. You can download the Mac installation of VS Code, or, if you're working with Linux, download the Linux installation. + +Once we have it installed, it will look a little something like this. And if it's a fresh installation, it'll even give you some tips and tools to actually get started. If you've never worked with Visual Studio Code before, we highly recommend going through any "Get Started" or "Getting Instructions" tips that come with opening Visual Studio Code. Additionally, we have a Visual Studio Code crash course in the GitHub repo associated with this course. diff --git a/courses/moccasin-101/1-python-in-updraft/22-install-wsl/+page.md b/courses/moccasin-101/1-python-in-updraft/22-install-wsl/+page.md new file mode 100644 index 000000000..feac123c1 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/22-install-wsl/+page.md @@ -0,0 +1,33 @@ +## Installing WSL + +This lesson covers how to install WSL (Windows Subsystem for Linux). WSL is a really important tool that helps us run Linux environments and utilities on our Windows machine. This will allow us to run any Linux tools and applications that we might need for our blockchain development. + +### What is WSL? + +Microsoft has significantly improved their support for developers in recent years, but when it comes to smart contract development, WSL is the better choice. It is the Windows Subsystem for Linux, and it is a game changer! WSL allows us to run a Linux distribution on our Windows machine. + +### Introduction + +Let's get started with the installation process! The first thing that we need to do is install WSL, which we can do through the Windows terminal. If we are using Windows 11, we can just hit the Windows key and type **terminal** and hit enter. + +### Installing WSL + +This will open the Windows Terminal for us which defaults to Powershell. However, if we are using Windows 10, we will need to install the Windows Terminal from the Microsoft Store. + +To install WSL, open the Windows Terminal and run the following command: + +```bash +wsl --install +``` + +This will trigger some admin level operations, as we are about to see on the screen now. We just need to select **yes**, hit **enter** and once the installation process is finished, this will require us to restart our computer. + +Once we reboot our computer, this is going to automatically trigger this window to continue the installation. + +The first thing we need to do here is to select our UNIX username. I am going to select **"cromewar"** and hit **enter**. Then we will be required to set up a new password. + +Something interesting about most Linux operating systems/distros is that when we are using the terminal, if we type anything over here, the password is going to remain hidden. That doesn't mean that we are not typing anything, but the terminal is not going to show anything over here, unless we use another Windows distro, for example, Linux Mint. Otherwise, most of the Linux operating systems are going to keep this always hidden. + +I am going to type my password though, and then hit **enter**. As we can see here, the terminal recognizes that password and asks me to type it again for confirmation. + +Once we finish this, that's it. We have WSL installed on our machine. As we can see here, the installation finished properly, and we can continue. I am going to actually close this for now. diff --git a/courses/moccasin-101/1-python-in-updraft/23-vscode-windows-install/+page.md b/courses/moccasin-101/1-python-in-updraft/23-vscode-windows-install/+page.md new file mode 100644 index 000000000..50d19cc43 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/23-vscode-windows-install/+page.md @@ -0,0 +1,23 @@ +## Windows Users Only: Installing VS Code + +We can install VS Code in a few different ways. + +The easiest is to use the **Windows Terminal**, using a package manager pre-installed on Windows 11 called **winget**. If we want to install VS Code using winget, we type the following command: + +```bash +winget search vscode +``` + +Then we copy the package name and use it in the following command: + +```bash +winget install Microsoft.VisualStudioCode +``` + +This will start the installation process. Once complete, we can access it from our Windows Menu. + +Another way is to use a web browser. We can go to the official website, **code.visualstudio.com**. We click on the "Download for Windows" button to trigger the download and install. + +If you want to ensure no data tracking from Microsoft, we can use a community-driven version of Visual Studio Code called **VsCodium**. It's basically the same code editor but without the Microsoft telemetry, gallery, logo, etc. The license is also changed to the MIT license. The installation process is similar to the previous method: We go to the VsCodium website, which is **vscodium.com**. We click on the "Install" button and download the latest release for Windows. + +We can now use VS Code in three different ways. The one we use for the tutorial, and for the rest of the course, is going to be the official Microsoft Visual Studio Code. diff --git a/courses/moccasin-101/1-python-in-updraft/24-vscode-windows-wsl-extension/+page.md b/courses/moccasin-101/1-python-in-updraft/24-vscode-windows-wsl-extension/+page.md new file mode 100644 index 000000000..ee7b8061c --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/24-vscode-windows-wsl-extension/+page.md @@ -0,0 +1,15 @@ +## Windows Users ONLY: WSL VSCode Extension + +We will be using a terminal built directly into Visual Studio Code. This is a Linux or Ubuntu terminal. We want to install the **WSL** extension for **Visual Studio Code** which is available from Microsoft. + +The WSL extension will pop up a terminal window similar to the PowerShell window, but we will choose Ubuntu to create a Linux terminal. This can be accessed by clicking on the three dots in the Visual Studio Code terminal dropdown and choosing **Ubuntu**. + +We will also be using the **Open a Remote Window** section at the bottom left of Visual Studio Code. We will select **Connect to WSL** in this dropdown to open up our VS Code directly in a WSL Ubuntu setup. + +To open a new terminal, we can either click on the three dots in the terminal dropdown and choosing **New Terminal**, or we can use the shortcut **Ctrl + Shift + `**. + +When we are in the Ubuntu terminal in VS Code, we will use Linux commands. We will not use Windows commands. If a website says, "Here is the Linux command, here is the Windows command", we will use the Linux command if we are in the Ubuntu terminal in VS Code. + +If we close out of Visual Studio Code and reopen it, we will be able to reconnect to the Ubuntu terminal by using the **Open a Remote Window** section at the bottom left of Visual Studio Code and selecting **Connect to WSL**. + +We will be working with Ubuntu or WSL or this Linux-like environment throughout this course. diff --git a/courses/moccasin-101/1-python-in-updraft/25-vscode-quickstart/+page.md b/courses/moccasin-101/1-python-in-updraft/25-vscode-quickstart/+page.md new file mode 100644 index 000000000..f8d96c439 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/25-vscode-quickstart/+page.md @@ -0,0 +1,36 @@ +## Visual Studio Code Quickstart 1: Terminals & Git + +Visual Studio Code (VS Code) is a powerful and popular text editor. One of the things that makes it so powerful is its built-in terminal feature. + +### Terminals + +VS Code's terminal lets us run scripts from within the text editor. It's essentially a command-line prompt. + +To open a terminal, we can: + +1. Click the "Terminal" menu and select "New Terminal." +2. Use the keyboard shortcut **ctrl backtick** on Mac or Linux, which will toggle the terminal on and off. + +### Git + +We will be using Git to manage our code projects. Git is a version control system that lets us track changes to our code and collaborate with others. + +We need to install Git before we can use it. To install Git on Linux, use one of these commands: + +```bash +sudo dnf install git-all +``` + +```bash +sudo apt install git-all +``` + +On macOS, we can run the following command in the terminal: + +```bash +git --version +``` + +This will prompt us to install Git if it's not already installed. We can also use a macOS Git installer by clicking the link to a Git installer on the [git-scm.com](https://git-scm.com/) website. + +We will go over how to use Git in detail in future lessons. For now, we can skip the next two sections of this course if we're not planning on using Windows or GitPod. diff --git a/courses/moccasin-101/1-python-in-updraft/26-install-python-macos/+page.md b/courses/moccasin-101/1-python-in-updraft/26-install-python-macos/+page.md new file mode 100644 index 000000000..cc344200e --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/26-install-python-macos/+page.md @@ -0,0 +1,34 @@ +## Installing Python on a Mac + +If you're on a Linux or a Windows machine, you can skip this lesson! + +To install Python on a macOS system, we'll go to the python.org website and download Python. + +On the python.org website, under Downloads, go to macOS and then scroll down to the latest Python 3.11 release. + +Click the link for the **Python 3.11.x macOS 64-bit universal2 installer**. The installation file will download to your Downloads folder. + +We'll then open the installer file, and then agree to the license agreement. + +We'll click **Continue**, and then **Install**. Python will install automatically. + +Once the installation is complete, a pop-up message will appear confirming that the installation was successful. We'll then close the installer. + +We'll open our VS Code terminal, either by clicking **Terminal > New Terminal** or by pressing **Control, Shift, Tilde**. + +In the VS Code terminal, we can confirm that the correct Python version is installed with: +```bash +python3.11 --version +``` + +We can also try: +```bash +python --version +``` + +Although, as of the recording, I don't have the `python` keyword. + +If you have multiple versions of Python installed, you can use this command to ensure that you're working with the correct version. + +We're going to switch to using tools called UV and Moccasin, which will make it easier to work with different versions of Python. + diff --git a/courses/moccasin-101/1-python-in-updraft/27-install-python-wsl-linux/+page.md b/courses/moccasin-101/1-python-in-updraft/27-install-python-wsl-linux/+page.md new file mode 100644 index 000000000..42fdfd340 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/27-install-python-wsl-linux/+page.md @@ -0,0 +1,49 @@ +## Installing Python (Windows WSL and Linux users) + +We're going to learn how to install Python in this WSL if you're on a Windows machine, or if you're on a Linux machine, just right on your Linux machine. Most sites are going to ask you to go directly to the Python website and select download. The issue with this for those of you on a Windows or a WSL environment is that downloading directly from the website will download this into your Windows operating system. We don't want to do that because we are going to be working in the Linux, the WSL, the Ubuntu operating system. So, we want to download into our Linux operating system. + +The same is actually true for any other software application that you want to run in your WSL environment; you're going to have to download it directly from the command line as opposed to downloading from some website with a click of a button. + +We'll need to make sure we're in a Linux environment. We can do this by typing `echo $SHELL` in the terminal. If we see a path that looks like `/bin/bash` or `/bin/zsh`, that's a good sign that we're in the right place. If we get an error, we can use an AI chatbot like Claude to help us troubleshoot. + +To install a specific version of Python, we'll use a few terminal commands. + +First, we'll run `sudo apt-get update`. This command updates our system's package index. Claude, a chatbot, can explain this in more detail. + +```bash +sudo apt-get update +``` + +Next, we'll install a common Linux package by running `sudo apt-get install software-properties-common`. + +```bash +sudo apt-get install software-properties-common +``` + +We'll then add the "deadsnakes" PPA by running `sudo apt-add-repository ppa:deadsnakes/ppa`. This PPA (Personal Package Archive) allows us to access different versions of Python. + +```bash +sudo apt-add-repository ppa:deadsnakes/ppa +``` + +Now, we need to update again to ensure that the PPA is correctly included. We'll run `sudo apt-get update`. + +```bash +sudo apt-get update +``` + +Finally, we'll install a specific version of Python. In this case, we're installing Python 3.11. We'll run `sudo apt-get install python3.11`. + +```bash +sudo apt-get install python3.11 +``` + +We can check that Python 3.11 is installed by running `python3.11 --version`. + +```bash +python3.11 --version +``` + +We're going to be moving away from using Python 3.11 in the terminal and instead use the "uv" tool. The "uv" tool will make it easier to switch between different versions of Python. + +If you do encounter any issues, remember that Googling, joining the discussions forum, and using AI can be incredibly helpful for troubleshooting. diff --git a/courses/moccasin-101/1-python-in-updraft/28-vscode-quickstart-2/+page.md b/courses/moccasin-101/1-python-in-updraft/28-vscode-quickstart-2/+page.md new file mode 100644 index 000000000..b48f29876 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/28-vscode-quickstart-2/+page.md @@ -0,0 +1,106 @@ +We are ready to get started with VS Code for this course! Let's open up VS Code. If you are on a Mac or Linux, you should see a shortcut to open a "remote window" in the bottom left of the screen. If you are on a Windows machine, it will likely display WSL or Ubuntu or some combination of the two. + +Let's open up a terminal using the command `Command ~` on Mac and `Control ~` on Windows or Linux. + +In our terminal, we can type the command +```bash +echo $SHELL +``` + +This will tell us the shell that we are using. It could be `zsh`, `bash`, or many other types of shells that all work the same way. The shell that we are using doesn't matter because the commands that we will be running work on any shell that you are working with. + +We'll often want to clear the terminal of old commands. We can do that by typing the command `clear`, and then hitting enter. + +We can also clear the terminal by using the keyboard shortcut `Command K` on Mac, or `Control K` on Windows or Linux. + +The trashcan icon on the terminal will delete the current terminal entirely. + +Let's open a new terminal and type a few lines. The X icon in the terminal is to hide the terminal. We can always pull the terminal back up. + +There is a keyboard shortcut for toggling the terminal, which is `Command ` `~` on Mac or `Control` `~` on Windows or Linux. This will hide and show the terminal window. + +We can also toggle the terminal by going to `View` and selecting `Terminal`. + +To delete a terminal window, we can click on the trashcan icon. + +Now, we'll create a new folder to store all of the files and folders that we will need for this curriculum. To create a new folder, we'll type the command: +```bash +mkdir mox-cu +``` +This command stands for `make directory`, and we'll use `mox-cu` as our folder name. We can call it anything we want. + +Now, we'll go into the folder that we just created by using the command: +```bash +cd mox-cu +``` + +The command `cd` stands for `change directory`. + +If we use the command `pwd`, which stands for `present working directory`, it will tell us the current location. + +To open the folder that we created, we can type the command `open .` on Mac. The dot (.) in this command refers to the current directory. + +There is a similar command on Windows to open a folder. For Windows users, we can type the command: +```bash +explorer . +``` +This will open the folder in Windows Explorer. + +For Linux, we can type the command: +```bash +xdg-open . +``` +This will open the current directory in the Linux file explorer. + +If you are a Linux user, you may not have this command installed by default. You may need to install this command. To install this, you can type: +```bash +sudo apt-get install xdg-utils +``` + +Now, we are going to install an AI extension. + +You can go to the extensions section. You'll find a search box where you can search for extensions. + +The AI that we will be using is GitHub Copilot. Feel free to search for other AI extensions, but GitHub Copilot is the one we will be using. + +After you install it, you'll see a little GitHub Copilot icon in the bottom right corner of your screen. This is a little reminder that you have installed it correctly. + +You might also want to install GitHub Copilot Chat. This will open a chat box for GitHub Copilot. + +Once you have installed it, you will see a little GitHub Copilot icon. You can right-click on the icon and select the chat option. This will open a chat box similar to ChatGPT or Cloud. + +We will be working in our `mox-cu` folder, which is the folder that we just created, and can see at the top of our VS Code screen. + +We can also view our file explorer. We'll be able to see all of our files here. + +You can toggle back to your terminal using the `Control ~` shortcut. + +We'll be working with files in this folder. We can create files by right-clicking and selecting `new file`. + +Let's create a file named `hi.txt`. + +You can see that `hi.txt` is listed in the left sidebar. + +Now, let's go to our terminal and type the command `ls`, which stands for `list`. This will list all of the files in our folder. + +We can see that our `hi.txt` file is in our folder. + +If we want to create a new file in VS Code, we can do so by right-clicking and selecting `new file`. Let's create a file named `hi2.txt`. + +We can see that both `hi.txt` and `hi2.txt` are listed. + +To edit a file, we can simply click on it. We can make changes and save the file. + +Let's delete this file. You can do this by right-clicking the file and selecting `delete`. You can also delete the file by using the trash can icon. + +You can also see a little dot next to the file name. This dot means the file is unsaved. To save, click `file`, `save`, and the dot will go away. + +That's a quick introduction to VS Code. + +Let's go ahead and try out GitHub Copilot. It's a little doodad that appears at the bottom of our screen, and helps us code. + +For example, we'll type `SPDX license identifier`, and GitHub Copilot will provide a suggestion. To accept the suggestion, we can hit `tab`. + +You can also click on the little GitHub Copilot icon. You can go to GitHub Copilot Chat to use GitHub Copilot in a chat-like format. + +In this lesson, we have learned the basics of navigating VS Code, creating files, editing files, and using GitHub Copilot. diff --git a/courses/moccasin-101/1-python-in-updraft/29-mental-prep/+page.md b/courses/moccasin-101/1-python-in-updraft/29-mental-prep/+page.md new file mode 100644 index 000000000..425d1ecb2 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/29-mental-prep/+page.md @@ -0,0 +1,38 @@ +## Mental Preparation + +Mental preparation is key when learning a new skill. Before we start coding, letοΏ½s think about why we want to learn, what we want to accomplish and what steps we will take to get there. + +The first question we should ask ourselves is: Why am I learning Vyper and Python? + +We should be honest with ourselves. Maybe we want to be part of the blockchain movement and make a lot of money, or maybe we want to learn a new skill and build our own projects. Whatever our rationale is, write it down and be honest with ourselves, don't placate ourselves. + +The next question is: What do I want to do/be when I finish this course? + +If we want to be a software engineer, security researcher, technical CEO or technical BD, write it down. What do we want to do? What do we want to feel like when we're finished with this course? + +Finally, what are the steps we are going to take to get there? + +This is a long course, so we need to figure out a learning cadence that works for us and stick to it. Maybe 30 minutes a day for 4 days a week, 1 hour a day, or 2 hours on Saturday and Sunday. Whatever our schedule is, write it down so that we can come back to it when things get tough. We should keep these goals private and be honest with ourselves. + +We're going to start by making a README.md file in our terminal. This will be our manifesto that we can come back to when things get hard. + +In our terminal, we're going to create a new file named README.md + +```bash +touch README.md +``` + +We can open this file in our VS code, by using a command in our terminal. + +```bash +code README.md +``` + +This should open up our new file in our VS Code. +Now, let's go back to our terminal and make sure we're in the correct directory. + +```bash +pwd +``` + +Now that our setup is complete, we are ready to start coding! diff --git a/courses/moccasin-101/1-python-in-updraft/3-setup/+page.md b/courses/moccasin-101/1-python-in-updraft/3-setup/+page.md new file mode 100644 index 000000000..fab7a007c --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/3-setup/+page.md @@ -0,0 +1,153 @@ +## Python Crash Course + +This section will be a Python Crash Course. We will be using the tool Google Colab, which is an online, browser-based IDE. It's great for working with Python. + +You can see an example of the final code from the tutorial in the repository, but we will be starting from a brand new notebook to ensure you can practice the code. + +To access Google Colab, you will need a Google account. + +To get started with Google Colab, we will need to go to the Google Colab site and create a new notebook. + +**Creating a New Notebook** + +You will need to select **File** and then **New Notebook in Drive**. This will create a new notebook for us. + +**Naming the Notebook** + +We can name our notebook whatever we want, but I will name my notebook "Updraft Python". + +**Jupyter Notebooks** + +Google Colab uses what are called Jupyter Notebooks. These are essentially a way to combine Markdown text, Python code, and other elements into a single document. + +The way this works is, you have these cells in a Jupyter Notebook. A Jupyter Notebook is made up of different types of cells. + +**Cells** + +You can run Python code in a Jupyter Notebook by creating a code cell, and running that code. You can also add text, or Markdown, to the cells as well. We will be using code cells to run all of our Python code. + +To execute a code cell, you simply click the play button. + +**Terminal Commands** + +We will also be using terminal commands. We will format these terminal commands in a code block, so you can copy and paste them. For example, the command to list the contents of a directory, which is "ls", will be formatted like this: + +```bash +ls +``` + +**Example Code** + +The code we're going to execute is a simple "Hello, World" example. + +```python +print("Hello, World") +``` + +This will print "Hello, World" to the console output. + +**Variables** + +We can also assign variables, like this: + +```python +my_variable = 7 +``` + +This will assign the value 7 to the variable my_variable. + +**Data Types** + +Python comes with many data types, including integers, strings, and booleans. + +```python +my_number = 10 +my_boolean = True +my_string = "hello!" +``` + +**Type Hints** + +We can also use type hints in Python. Type hints are a way to indicate the expected data type of a variable. + +```python +my_typed_number: int = 9 +``` + +This indicates that the variable my_typed_number is expected to be an integer. + +**Updating Variables** + +We can also update variables, like this: + +```python +my_number = 7 +``` + +This will assign the value 7 to the variable my_number. + +**Lists** + +Variables in Python can hold lists of elements. + +```python +my_list: list = [7, "cat", True] +``` + +**Accessing List Elements** + +In Python, lists are "0 indexed." This means that the first element in a list has an index of 0, the second element has an index of 1, and so on. + +For example: + +```python +my_list = [7, "cat", True] +my_list[1] = "dog" +print(my_list) +``` + +The output will be: + +```python +[7, "dog", True] +``` + +**Casting** + +You can convert data types from one to another in Python using casting. + +```python +year = "2025" +int_year = int(year) +added_year = int_year + 25 +print(added_year) +``` + +The output will be: + +```python +2050 +``` + +**Math Operations** + +You can also do math in Python, like this: + +```python +print("Add") +my_number = 5 + 5 +print(my_number) +print("Subtract") +my_number = 5 - 5 +print(my_number) +``` + +**Code Blocks** + +All of the code blocks in this tutorial will have code formatted on new lines, like this: + +```python +print("Hello, World") +``` + +Be sure to practice the code shown in this tutorial to get a good understanding of Python basics. diff --git a/courses/moccasin-101/1-python-in-updraft/30-local-py/+page.md b/courses/moccasin-101/1-python-in-updraft/30-local-py/+page.md new file mode 100644 index 000000000..ef2c947f9 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/30-local-py/+page.md @@ -0,0 +1,49 @@ +## Local Python Development: An Introduction + +We are going to create our first Python project and use a more professional environment. + +### Project Creation + +First, we need to create a new directory. + +We can do this using the command: + +```bash +mkdir python-in-updraft-cu +``` + +Or to keep things in order, we can use: + +```bash +mkdir 1-python-in-updraft-cu +``` + +Then hit enter. + +If we run the command `ls`, we can see we now have a folder, `python-in-updraft-cu`, and a file `README.md`. + +### Navigating to Our New Directory + +Next, we will navigate to the newly created folder. We can do this using the command: + +```bash +cd python-in-updraft-cu +``` + +You can also use the Tab key to auto-complete the directory name. This is a helpful tip. + +We can check our current location by using the command: + +```bash +pwd +``` + +### Opening Our Project in Visual Studio Code + +Now we need to open the project in Visual Studio Code. You can do this by typing the command: + +```bash +code . +``` + +Alternatively, you can open Visual Studio Code, go to the "File" menu, click "Open Folder" and then select the `python-in-updraft-cu` folder. diff --git a/courses/moccasin-101/1-python-in-updraft/31-python-shell/+page.md b/courses/moccasin-101/1-python-in-updraft/31-python-shell/+page.md new file mode 100644 index 000000000..f8d710e5f --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/31-python-shell/+page.md @@ -0,0 +1,61 @@ +## Python Shell / Console + +We can try out code in Python using the Python shell or console. To do this, we type: + +```bash +python3.11 +``` + +This will open the Python shell. The shell allows us to write any code we want and have it executed immediately. For example, to print the string "hi", we would type: + +```python +print("hi") +``` + +The shell will immediately respond: + +``` +hi +``` + +We can also assign values to variables. For example, to assign the value 7 to the variable my_var, we would type: + +```python +my_var = 7 +``` + +The shell will not respond to this, since we have only assigned the value. We can then print the variable value by typing: + +```python +print(my_var) +``` + +The shell will respond with the value: + +``` +7 +``` + +If we type quit and hit enter, we will exit the shell and return to the terminal. To reenter the Python shell, we would have to type: + +```bash +python3.11 +``` + +again. + +If we try to print the variable my_var without assigning it a value, we will get an error: + +```python +print(my_var) +``` + +The shell will respond with: + +``` +Traceback (most recent call last): + File "", line 1, in +NameError: name 'my_var' is not defined +``` + +This error occurs because variables assigned in the shell are not saved when we exit the shell. diff --git a/courses/moccasin-101/1-python-in-updraft/32-python-extensions-vscode/+page.md b/courses/moccasin-101/1-python-in-updraft/32-python-extensions-vscode/+page.md new file mode 100644 index 000000000..25abaa5ec --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/32-python-extensions-vscode/+page.md @@ -0,0 +1,34 @@ +## Jupyter and Python VS Code Extensions + +We can create an environment very similar to a Google Colab environment by creating a file in VS Code called `cells.ipynb`. + +To run the cells in this notebook, we need to install two extensions: + +1. **Jupyter Notebook**: This extension provides support for Jupyter notebooks. + ```bash + jupyter + ``` +2. **Python**: This extension provides language support for Python and helpful tools, including syntax highlighting and formatting. + ```bash + python + ``` + +After we install these extensions, we need to select a kernel. We can do this by clicking the three dots in the top right corner of the VS Code window and selecting "Extensions". Alternatively, we can click the "Extensions" button on the left side of the window. + +We can then select the desired Python environment. We'll choose `Python 3.11.9` in this example. + +Now we can run the cells in our Jupyter notebook by typing `shift enter`. We'll see that the output of the cells will appear below the cell itself. + +We can also run basic Python commands in a Jupyter notebook in VS Code. For example, we can create a variable called `my_var` and set it equal to 8. We can then run the cell to assign the value to the variable. + +```python +my_var = 8 +``` + +Then, we can print the value of the variable `my_var`. + +```python +print(my_var) +``` + +We can see that the output of this cell will be the number 8. diff --git a/courses/moccasin-101/1-python-in-updraft/33-our-first-python/+page.md b/courses/moccasin-101/1-python-in-updraft/33-our-first-python/+page.md new file mode 100644 index 000000000..55b35e978 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/33-our-first-python/+page.md @@ -0,0 +1,35 @@ +## Our first Python file + +We've been working with `cells.ipynb` files. Let's create our first basic Python file. This is how professional software engineers work with Python. + +Google Colab and these Jupyter Notebooks are great for testing, tinkering, learning, and working with stuff and the like, but they aren't great for professional environments. This is because, similar to Remix, if we wanted to test something and had to do shift-enter or execute all these cells at the same time, it could be very annoying. This isn't how professional projects work. + +What they do instead is they create Python files to run and execute. For example, we are going to create a new file called `basic_python.py`. + +The `.py` file extension is how VS Code and other Python tools know that this is a Python file. + +In here, we can type and run kind of the basic Python stuff. We can do + +```python +print("hi") +``` + +in here. Remember, this little white dot means it's not saved. Hit `command s` to save it. And then we can hit this little plus button that comes with this, and it will run my Python file with this `hi`. + +Now there is a whole bunch of other stuff that it will do, and so this is kind of like a cheat sheet shortcut. This is something that you can use to run your Python files. However, we are going to discourage that use for a lot of reasons. + +Instead, in your terminal, if you do + +```bash +ls +``` + +you can see we have this `basic_python.py`. We are going to write + +```bash +python3.11 basic_python.py +``` + +Hit enter and we will see it prints out `hi`. + +This is how we can execute an entire file of Python, like so. diff --git a/courses/moccasin-101/1-python-in-updraft/34-python-imports/+page.md b/courses/moccasin-101/1-python-in-updraft/34-python-imports/+page.md new file mode 100644 index 000000000..fa83c14a0 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/34-python-imports/+page.md @@ -0,0 +1,40 @@ +## Installing Libraries and using imports + +One of the things we can do in Python is install packages and libraries. + +To install the Python package called "Titanoboa" (a Python tool for interacting with Vyper), we can visit the Titanoboa Github repository: + +```bash +github.com/vyperlang/titanoboa +``` + +On the repository page, if we scroll down to the Installation section, we can see how to install the Python package directly into our environment. + +The command we will use is: + +```bash +pip install titanoboa +``` + +We can then run a command using the package in our Python. + +To install the Titanoboa package into our global Python environment, we will type: + +```bash +python3.11 -m pip install titanoboa +``` + +After installing Titanoboa, we can view a list of all the different packages that we have installed in our Python environment. + +To view this list, we can type: + +```bash +python3.11 -m pip freeze +``` + +We can use the Titanoboa package to evaluate Vyper. To evaluate some Vyper code, we can type "import boa". For example, if we want to evaluate the Vyper code to create an empty uint256 object (AKA 0), we can type: + +```python +import boa +print(boa.eval('"empty(uint256)"')) +``` diff --git a/courses/moccasin-101/1-python-in-updraft/35-installing-uv/+page.md b/courses/moccasin-101/1-python-in-updraft/35-installing-uv/+page.md new file mode 100644 index 000000000..875cfb285 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/35-installing-uv/+page.md @@ -0,0 +1,72 @@ +## UV Installation + +We are going to introduce you to the `uv` tool. + +`uv` is a very fast Python package and project manager. It was written in Rust. This tool is very fast and very modern. Let's install it so we can see what all the fuss is about. + +The `uv` tool can help us with the following: + +* Installing and managing Python versions +* Creating professional Python projects +* Maintaining Python versions + +First, we're going to go to the `uv` documentation website: + +```bash +docs.astral.sh/uv +``` + +Then, we're going to scroll down to the "Getting started" section. This is where we will find the instructions for installing the `uv` tool. + +We'll copy the command below and paste it into our terminal. + + +```bash +curl -Lssf https://astral.sh/uv/install.sh | sh +``` + +This will install both the `uv` and the `uvx` commands. + +Once we have it installed, we can go ahead and delete our terminal and type: + +```bash +which uv +``` +This will show us the location of our installed `uv` command. + +We can also type: + +```bash +uv --version +``` +This will display the current version of `uv` that we are using. + +Now that we have installed `uv`, we can use it to create a new Python project. + +```bash +uv init python-in-updraft-cu +``` + +This will create a new project folder called `python-in-updraft-cu`. + +We can now see a whole bunch of new files on the left side of the screen: + +* `.gitignore` +* `.python-version` +* `basic_python.py` +* `cells.ipynb` +* `hello.py` +* `pyproject.toml` +* `README.md` + +Let's go over what some of these files do: + +The `.gitignore` file is specifically for working with Git and GitHub. + +The `.python-version` file tells other developers and tools what Python version is recommended for this project. + +The `pyproject.toml` file tells the world and other Python projects how to interact with our Python project here. + +The `README.md` file is a file that every single one of your projects should come with. + +We'll explain these other files later. See you next time! diff --git a/courses/moccasin-101/1-python-in-updraft/36-uv-python-version/+page.md b/courses/moccasin-101/1-python-in-updraft/36-uv-python-version/+page.md new file mode 100644 index 000000000..f6e7c1d72 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/36-uv-python-version/+page.md @@ -0,0 +1,115 @@ +## Using uv to Automatically Change Python Versions + +We can use `uv` to automatically switch between different Python versions. This is useful when we need to run scripts that require a specific Python version. + +We start off looking at a `basic_python.py` file with the following code: +```python +import boa +print(boa.eval('"empty(uint256)"')) +print("Hello!") +``` + +If we run this script with Python 3.11: +```bash +python3.11 basic_python.py +``` + +We will get the following output: +```bash +0 +Hello! +``` + +Let's look at a different scenario. If we try to run this script with `uv` and Python 3.11: +```bash +uv run python3.11 basic_python.py +``` + +We will get the following error: +```bash +Traceback (most recent call last): + File "Users/patrick/mox-cu/python-in-updraft-cu/basic_python.py", line 1, in + import boa +ModuleNotFoundError: No module named 'boa' +``` + +The error occurs because the Boa package is installed into our global Python environment, not our `uv` environment. + +We can see our Python versions using `uv`. If we type: +```bash +uv run python --version +``` + +We will get the version we are currently working with. If we type: +```bash +python3.11 --version +``` + +We will get the version of our global Python environment. + +We are going to update this to use a specific Python version. We can add the following lines to our `basic_python.py` file: +```python +import sys +if sys.version_info < (3, 12): + raise RuntimeError("This script requires Python 3.12 or higher") +``` + +If we run this script with Python 3.11: +```bash +python3.11 basic_python.py +``` + +We will get the following error: +```bash +Traceback (most recent call last): + File "/Users/patrick/mox-cu/python-in-updraft-cu/basic_python.py", line 5, in + raise RuntimeError("This script requires Python 3.12 or higher") +RuntimeError: This script requires Python 3.12 or higher +``` + +So this is a way to specify that a script requires a certain Python version. + +We can go back to our terminal and type: +```bash +uv python install 3.12 +``` + +This will install Python 3.12 into our `uv` environment. + +If we clear our terminal and run our script: +```bash +uv run basic_python.py +``` + +We will now get an error about the `boa` module because we commented it out: +```bash +Traceback (most recent call last): + File "/Users/patrick/mox-cu/python-in-updraft-cu/basic_python.py", line 6, in + print(boa.eval('"empty(uint256)"')) +NameError: name 'boa' is not defined +``` + +We can use `uv` to see a list of the Python versions that we have installed. If we type: +```bash +uv python list +``` + +We will see a list of all the Python versions installed on our computer. + +We can also directly specify a version of Python to use: +```bash +uv run --python 3.12 python basic_python.py +``` + +This will run our `basic_python.py` script with Python 3.12.7. + +We can also pin the Python version for a project using a file named `.python-version` and store it in the project's root directory. Inside this file, we can specify the Python version that we want to use. We can then update the Python version in the `.python-version` file. + +This will update the Python version of the project and automatically download the new version and update the virtual environment. + +To see this in action, we can update our `.python-version` file to Python 3.13: +``` +3.13 +``` + +We can then use `uv run basic_python.py` to run our script and it will automatically download Python 3.13, update our virtual environment, and then run the script. diff --git a/courses/moccasin-101/1-python-in-updraft/37-adding-packages-uv/+page.md b/courses/moccasin-101/1-python-in-updraft/37-adding-packages-uv/+page.md new file mode 100644 index 000000000..b82fac182 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/37-adding-packages-uv/+page.md @@ -0,0 +1,74 @@ +## Adding Python packages/libraries with UV + +The UV tool helps us manage Python packages and dependencies across different projects and versions. + +We'll use the **Titanobo** package in this lesson to showcase how UV can be used to manage dependencies. + +**Titano Boa** is a package that allows us to evaluate code written in Python. + +First, let's create a simple Python script that will utilize **Titanobo**. +```python +import boa +import sys + +print(boa.eval('"empty(uint256())"')) +print("Hello!") +``` + +If we try to run this script without having **Titano Boa** installed, we'll encounter an error message as **Titanobo** is not available. + +**Let's install Titano Boa globally using pip** +```bash +python3.11 -m pip install titano Boa +``` + +This will install **Titano Boa** for the global Python 3.11 environment. + +Now we can run our script and we'll see the output: +```bash +python3.11 basic_python.py +``` + +We get the correct output because **Titano Boa** was installed globally in our Python 3.11 environment. + +**UV's strength lies in its ability to manage dependencies for specific project environments. Let's demonstrate this.** + +**We'll add Titano Boa to our project environment using UV.** + +**Add a dependency with UV** +```bash +uv add titano Boa +``` + +This will create a new folder called `venv` in our project directory and automatically add **Titano Boa** to the project environment. + +**UV will also generate a `uv.lock` file.** + +This file keeps track of all dependencies for our project. + +**Run our script with UV** +```bash +uv run python basic_python.py +``` + +This will run our script using the Python version and packages specified in our project environment. + +We can now switch to different Python versions or add different packages to our project, and UV will ensure that the correct versions are used when running our script. + +**Using pyproject.toml** +```toml +[project] +name = "Python-in-updraft-cu" +version = "0.1.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.11" + +dependencies = [ + "titano Boa>=0.2.4", +] +``` + +The `pyproject.toml` file acts as a configuration file for our project, defining dependencies and other project-related details. When we used `uv add titano Boa`, UV updated this file to include **Titano Boa** in our dependency section. + +UV's flexibility allows us to easily switch between different Python versions and environments without the hassle of managing global packages or manually installing dependencies for each project. diff --git a/courses/moccasin-101/1-python-in-updraft/38-virtual-envs/+page.md b/courses/moccasin-101/1-python-in-updraft/38-virtual-envs/+page.md new file mode 100644 index 000000000..14b967874 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/38-virtual-envs/+page.md @@ -0,0 +1,49 @@ +## Virtual Environments + +Virtual environments are an important concept to understand even if we don't use `uv`. + +Technically, a virtual environment is a setting that `uv` creates to isolate our Python version and packages. Back in our Cyfron Updraft terminal, we can type: + +```bash +uv venv +``` + +This will create a folder named `.venv` in our project directory where our virtual environment is stored. If we run the command again, it will do the same thing. To activate the virtual environment, we type: + +```bash +source .venv/bin/activate +``` + +We can think of virtual environments as "active Python environments" which are separate from the global Python environment. If we don't specify an active Python environment, we will be using the global one. + +We can set our active Python environment to be our virtual environment by activating it. + +```bash +source .venv/bin/activate +``` + +When we activate a virtual environment, our terminal will show parentheses indicating which virtual environment is active. + +We can run our script, `basic_python.py`, with `uv run` to automatically enter the virtual environment and execute it. + +However, when we run `basic_python.py` without using `uv`, we get a `ModuleNotFoundError`. This is because we need to install the `boa` package. + +We can install packages with `uv sync`. + +```bash +uv sync +``` + +The `uv sync` command looks at our `pyproject.toml` file, specifically the `dependencies` section. It installs all the Python packages we specified in this file. + +After we've installed the `boa` package, we can now run `basic_python.py` successfully. + +We can deactivate our virtual environment by typing: + +```bash +deactivate +``` + +We can also deactivate our virtual environment by trashing our terminal window and pulling it back up. + +`uv` comes with several other tools that we will learn throughout the course. diff --git a/courses/moccasin-101/1-python-in-updraft/39-recap-pt-2/+page.md b/courses/moccasin-101/1-python-in-updraft/39-recap-pt-2/+page.md new file mode 100644 index 000000000..1b5c0cbc6 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/39-recap-pt-2/+page.md @@ -0,0 +1,63 @@ +## Python in Updraft: Recap + +We've just finished our Python crash course. We've learned how to set up our VS Code environment, manage Python versions, and set up a virtual environment using UV. + +First, we installed Python into our Linux-like environments: + +```bash +python3.11 --version +``` + +We can manage our Python versions using UV: + +```bash +uv run python --version +``` + +To change to a different version of Python, we can use: + +```bash +uv python pin 3.12 +``` + +We can add packages to our virtual environment using UV: + +```bash +uv add Titanoboa +``` + +We can activate our virtual environment by running: + +```bash +uv venv +``` + +Then, we can activate the environment using: + +```bash +source .venv/bin/activate +``` + +Now, we can access the Python interpreter without having to use `python3.11` or `python3.11 --version`. We can simply use `python` or `python3`: + +```bash +python --version +``` + +```bash +python3 --version +``` + +To deactivate the virtual environment, we can run: + +```bash +deactivate +``` + +We can also check the path of our Python interpreter using: + +```bash +which python +``` + +Congratulations on completing this course! Now, you're ready to start developing smart contracts using Python in Updraft. diff --git a/courses/moccasin-101/1-python-in-updraft/4-notebook/+page.md b/courses/moccasin-101/1-python-in-updraft/4-notebook/+page.md new file mode 100644 index 000000000..75ab1922b --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/4-notebook/+page.md @@ -0,0 +1,30 @@ +## Working with IPython (Google Colab) Cells (Jupyter Notebook) + +In this lesson, we will learn how to work with cells in Google Colab or Jupyter Notebook. + +Each cell represents a unit of code that we can write and execute. + +To add code to a cell, we can simply type in the cell like this: +```python +print("hi") +``` +Then, we can go to the "Runtime" menu and choose "Run selection." + +Google Colab and Jupyter Notebook provide a variety of ways to run code. + +* To run the whole cell, we can click on the play button icon in the cell. +* To run only the line our cursor is on, we can use the "Run selection" option from the "Runtime" menu. +* To run all cells, we can use "Run all" from the "Runtime" menu. + +We can also run the whole cell using keyboard shortcuts: + +* **Shift + Enter:** Runs the whole cell and creates a new one below it. +* **Ctrl + Enter:** Runs the whole cell. + +In addition to code, cells can also contain comments. These are lines that are not executed by the program. Comments are often used to explain what the code does. + +Comments in Google Colab and Jupyter Notebook start with the hash symbol (#). For example, we could add a comment like this: +```python +# Hi, I'm a comment! +``` +Anything after the # symbol will not be executed. diff --git a/courses/moccasin-101/1-python-in-updraft/5-google-ai/+page.md b/courses/moccasin-101/1-python-in-updraft/5-google-ai/+page.md new file mode 100644 index 000000000..46bb1aec6 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/5-google-ai/+page.md @@ -0,0 +1,35 @@ +## Google's AI (Gemini) + +We're going to learn how to use Google Colab, and the AI features it provides, such as Google Gemini. + +What is really nice about Google Colab, is that if we don't understand something, we can use the AI features. We can hit "Gemini" here and be sure to read the privacy notice. Then, we can continue. We can enter different prompts here. We could even copy all the code here if we don't understand what's going on, go into Gemini, and write our little prompt. + +``` +What does this code do? +``` + +We'll put three backticks, a new line with shift enter, paste it in, shift enter again, three more backticks. This is typically how you notate that something is code. We put three backticks, put all the code, and three more backticks. So what does this code do? We'll zoom out a little bit. We'll hit enter, and Gemini will start thinking. Okay, let's break this down. This line is a comment. Comments in Python start with the hashtag symbol. They are ignored by the Python interpreter and are intended for humans to read. This comment is simply saying "Hi, I'm a comment!" + +```python +print("Hi!") +``` + +This line is a function call to the built-in print() function. This function takes one or more arguments and displays them to the console. In this case, it is printing the string "Hi!" to the console. And then another comment. + +It even gives you some sources for where it learned how to understand what this is doing. It did a little bit of Googling itself on the backend. So as you're coding, as you're learning, feel free to use different AI features to help expedite your learning process. + +We'll close this, and we'll delete everything in here, and start with the next section. + +If you want to practice using some generative code, you can click "Generate" here. And you could type something as simple as "Write a print statement" to print out the word "hi". Hit enter, and it'll give you exactly kind of the code that you want here. + +```python +print("Hi!") +``` + +It's important though, that you do understand what this code is actually doing so that when AI gets it wrong, you can actually correct it. + +We'll close this, and we'll delete everything in here, and start with the next section. + +If you want to save this locally for later, you can hit file, download. Then you can download this as a .pymb or .py file, free to have it locally if you want to study offline as well. + +We'll go ahead and create a new cell or a new block of code by scrolling down. We'll click the + Code button here. We'll get a new cell. diff --git a/courses/moccasin-101/1-python-in-updraft/6-python-vars/+page.md b/courses/moccasin-101/1-python-in-updraft/6-python-vars/+page.md new file mode 100644 index 000000000..3846e012f --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/6-python-vars/+page.md @@ -0,0 +1,38 @@ +## Variables in Python + +Let's talk about variables. In Python, we can assign different variables. + +Our generative AI is even giving us a suggestion here. + +And variables are essentially keywords that we can use to represent some other value. + +For example, we can say: +```python +my_variable = 7 +``` + +Now, anytime we use this keyword, my_variable, it's going to represent the number 7. + +So, if we were to print out my_variable, what do you think it will print out? Well, if we hit this little play button here to execute the cell, we see it prints out the number 7 because my_variable now represents 7. + +The keywords or the names of your variables can be almost anything that you want. We can assign almost anything to a variable, and Python comes with many types. + +For example, we can assign numbers: +```python +my_number = 10 +``` + +We can assign something called booleans which are true or false. We can say: +```python +my_boolean = True +``` + +We can also do false. + +We can assign whole strings or text: +```python +my_string = "hello!" +``` +Strings are denoted by being between these little quotes here. We can use single quotes or double quotes, both work great in Python. + +If the word string is a little confusing to you, just know that anytime someone says hey, this is a string, a string just means it's text. It means it's not a number, it's not a boolean, it's just text. diff --git a/courses/moccasin-101/1-python-in-updraft/7-type-hints/+page.md b/courses/moccasin-101/1-python-in-updraft/7-type-hints/+page.md new file mode 100644 index 000000000..d887984a5 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/7-type-hints/+page.md @@ -0,0 +1,28 @@ +## Type Hints + +Additionally, in Python we can use something called type hints. We can say: + +```python +my_typed_number: int = 9 +``` +This colon and then int is known as a type hint. It's more helpful for humans to know that anytime you see my typed number, that's going to be an integer or a number. We could also say: + +```python +my_typed_bool: bool = False +``` + +```python +my_typed_string: str = "hi" +``` + +So str is a type hint for a string, bool is a type hint for a boolean which is a true or false. Int is a type hint for an integer, a.k.a a number. You can also update variables. So my variable originally starts off as 7. + +```python +my_variable = 7 +``` + +We can copy this value, paste it here, and then we can update it to 77. + +```python +my_variable = 77 +``` \ No newline at end of file diff --git a/courses/moccasin-101/1-python-in-updraft/8-arrays/+page.md b/courses/moccasin-101/1-python-in-updraft/8-arrays/+page.md new file mode 100644 index 000000000..bc0c50efe --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/8-arrays/+page.md @@ -0,0 +1,64 @@ +## Arrays / Lists + +We can assign values to variables in Python, like so: + +```python +my_variable = 7 +print(my_variable) +``` + +We can assign many different types of values to variables: + +```python +my_number = 10 +my_boolean = True +my_string = 'hello!' +``` + +We can even type hint our variables: + +```python +my_typed_number: int = 9 +my_typed_bool: bool = False +my_typed_string: str = "hi!" +``` + +We can also update variables: + +```python +my_variable = 77 +``` + +### Lists + +We can store collections of values in a list, or an array: + +```python +my_list: list = [7, "cat", True] +print(my_list) +``` + +We can see that each item in the list has an index. It's important to remember that in Python, the first index is `0`. + +```python +# [7, "cat", True] +# [0, 1, 2] +``` + +We can use square bracket notation to update a specific item in the list: + +```python +my_list[1] = "dog" +print(my_list) +``` + +Python provides a convenient way to sort lists of numbers: + +```python +my_numbered_list = [0, 7, 8, 2, 3, 99] +print(my_numbered_list) +my_numbered_list.sort() +print(my_numbered_list) +``` + +We can use the `.sort()` method to sort our list. diff --git a/courses/moccasin-101/1-python-in-updraft/9-inputs/+page.md b/courses/moccasin-101/1-python-in-updraft/9-inputs/+page.md new file mode 100644 index 000000000..3454b05e2 --- /dev/null +++ b/courses/moccasin-101/1-python-in-updraft/9-inputs/+page.md @@ -0,0 +1,45 @@ +## Inputs and Strings + +In this lesson, we're going to learn about how to get user input and work with strings in Python. + +We can ask users for information by using the `input()` command. Let's create a new code section and write the following: + +```python +input("what is your name?") +``` + +We can then execute the cell, and see how Python is prompting us to enter our name. Let's type "Patrick" in the box and hit enter. It will display the input we provided. + +We can save the user's input into a variable. We can do that with this code: + +```python +name = input("what is your name?") +print("My name is " + name) +``` + +Python is again prompting us to enter a name. If we enter "Patrick" and execute the code, it will print out: + +```python +My name is Patrick +``` + +The plus sign in the code is a concatenation operator. This is a way of combining strings together. + +We can also use something called an "f-string" to combine strings. Let's add the following to the existing code section: + +```python +print(f"{name} is my name") +``` + +Now, when we execute the cell, it will prompt us for a name. If we enter "Patrick", the code will output: + +```python +My name is Patrick +patrick is my name +``` + +Let's break down this code. + +We're using `print()` to display a string. We're starting our quotes with the letter "f", which is used to denote an f-string. Inside our quotes, we have a set of curly brackets that contain the variable name. + +The f-string will take the value of the variable that we entered as input and insert it into the f-string. In this case, the value of the `name` variable is "Patrick", so it's inserted into the curly brackets. diff --git a/courses/moccasin-101/2-web3py/1-intro/+page.md b/courses/moccasin-101/2-web3py/1-intro/+page.md new file mode 100644 index 000000000..abc3b14cb --- /dev/null +++ b/courses/moccasin-101/2-web3py/1-intro/+page.md @@ -0,0 +1,167 @@ +## Web3py Favorites + +In this lesson, we will explore the world of Web3py by building a simple but powerful project. This will help us learn the fundamentals of blockchain development. We'll learn how to deploy a smart contract, interact with that smart contract, and work with more advanced Python tooling. + +To get started, we need to be familiar with the Git repository associated with this course. We can download all of the code from that repository at any time: + +```bash +https://github.com/Cyfrin/web3py-favorites-cu +``` + +We'll dive into the `README.md` file where we have a description of what we'll be building: + +``` +# web3py Favorites + +This is from the Cyfrin Updraft Vyper Course. + +- web3py Favorites +- Getting Started + - Prerequisites + - Optional prerequisites + - Optional Gitpod or CodeSpaces + - Installation + - uv + - pip/python +- Quickstart +- Deploying with Python + - 1. Setup Tenderly Virtual Network + - 2. Fund a wallet + - 3. Get your RPC URL + - The Unsafe Way + - 4. Run the unsafe version + - The Safer Way + - 4. Encrypt your private key + - 5. Run the safe version +- Maintainer notes + - Build a new requirements.txt +``` + +We have a file called `favorites.vy` which we'll interact with in this lesson: + +``` +# pragma version ^0.4.0 +# @license MIT + +my_favorite_number: uint256 + +struct Person: + favorite_number: uint256 + name: String[100] + +# Static Array/List +list_of_people: public(Person[5]) +list_of_people_index: uint256 + +name_to_favorite_number: HashMap(String[100], uint256) + +@external +def store(favorite_number: uint256): + self.my_favorite_number = favorite_number + +@external +def retrieve() -> uint256: + return self.my_favorite_number + +@external +def add_person(name: String[100], favorite_number: uint256): + new_person: Person = Person(favorite_number, name) + self.list_of_people[self.list_of_people_index] = new_person + self.list_of_people_index += 1 + self.name_to_favorite_number[name] = favorite_number + +``` + +We'll also learn how to write a Python key encryptor script, which will help us deploy a smart contract: + +``` +from eth_key import Account +import getpass +from pathlib import Path +import json + +KEYSTORE_PATH = Path("keystore.json") + +def main(): + # input for your private key + private_key = getpass.getpass("Enter your private key: ") + my_account = Account.from_private_key(private_key) + password = getpass.getpass("Enter a password: ") + encrypted_account = my_account.encrypt(password) + with KEYSTORE_PATH.open("w") as fp: + print(f"Saving to {KEYSTORE_PATH}...") + json.dump(encrypted_account, fp) + +if __name__ == "__main__": + main() +``` + +**Code Spaces** + +We can use Code Spaces for this project. We'll create a Code Space for the repository. Once the Code Space has loaded, we can access the terminal. Then, we can run the following command: + +```bash +pip install -r requirements.txt +``` + +**Deploying with Python** + +We'll deploy a smart contract with Python. This will involve the following steps: + +* **Setup Tenderly Virtual Network** + + To get started with Web3py, we need to make sure that we have a Web3 provider that is able to interact with our blockchain. We can do this through Tenderly's virtual networks. Tenderly has a nice feature that allows us to get up and running with a virtual network very easily. +* **Fund a wallet** + + We'll need a wallet to be able to interact with our blockchain. We can use Tenderly to get a test wallet that we can fund with some Ether. +* **Get your RPC URL** + + We need to know the RPC URL associated with our virtual network. We can get that from the Tenderly dashboard. +* **The Unsafe Way** + + The unsafe way involves hardcoding your private key directly into your Python script. This is generally not a good practice as this leaves your private key susceptible to security risks. + + We'll run the following command to create a file called `favorites_unsafe.py` + + ```bash + touch favorites_unsafe.py + ``` + + We'll add some code to this file: + + ```python + from web3 import Web3 + from web3.middleware import geth_poa_middleware + from web3.exceptions import BadFunctionCallOutput + from vyper import compile_lll + + # Input your RPC URL + # RPC_URL = "https://rpc-mumbai.maticvigil.com/" + RPC_URL = "https://virtual-mainnet.tenderly.co/fork/cd647361-44d3-43b5-8013-c75702600116/1" + + w3 = Web3(Web3.HTTPProvider(RPC_URL)) + w3.middleware_onion.inject(geth_poa_middleware, layer=0) + + print("Connecting...") + if w3.is_connected(): + print("Connected!") + else: + print("Not connected.") + + # Add code to favorites.vy - it should be a smart contract + + # Compile the Contract - to compile the smart contract + favorites_compiled_lll = compile_lll( + favorites_vy, + optimize=False + ) + + print("Favorites Compiled Successfully!") + # Create Contract Object + favorites_contract = w3.eth.contract( + abi=favorites_compiled_lll["abi"], + bytecode=favorites_compiled_lll["bytecode"] + ) + + # Build a Transaction + nonce = w3.eth.getTransactionCount("0x9f024882e119048302558080b91cfc1d61d1e39611556c6305736d1d16b1b800a4b1cfc1d61d1e39511556c630574000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/courses/moccasin-101/2-web3py/10-breakpoint/+page.md b/courses/moccasin-101/2-web3py/10-breakpoint/+page.md new file mode 100644 index 000000000..17d2a7666 --- /dev/null +++ b/courses/moccasin-101/2-web3py/10-breakpoint/+page.md @@ -0,0 +1,24 @@ +## Python Tip: breakpoint() + +We're going to run our code one more time, but we are going to use a Python debugging technique called breakpoints. + +Let's add the following line of code to our Python script: +```python +breakpoint() +``` + +Now, when we run our code, it will execute all of the lines up until the line where we've added `breakpoint()`. It will then drop us into a Python shell. This Python shell, also known as a PDB (Python Debugger) allows us to interactively execute code. + +To demonstrate this, let's run our code again, after deactivating our virtual environment: +```bash +uv run python deploy_favorites.py +``` + +After the code executes, we'll be dropped into a Python shell. Let's execute the following command: +```python +favorites_contract +``` + +This will output the result of `favorites_contract`, which is a placeholder for the bytecode. + +We can type `quit` to exit the Python debugger or `q` depending on the debugger, as well. We can then remove the `breakpoint()` line and continue coding. diff --git a/courses/moccasin-101/2-web3py/11-anvil/+page.md b/courses/moccasin-101/2-web3py/11-anvil/+page.md new file mode 100644 index 000000000..6ecfdf8c7 --- /dev/null +++ b/courses/moccasin-101/2-web3py/11-anvil/+page.md @@ -0,0 +1,49 @@ +## Installing Anvil + +In this lesson, we'll learn how to install Anvil, a tool that we'll use to create and deploy blockchain projects locally. + +**Why We Need Anvil** + +Anvil is a powerful smart contract development framework that allows us to test and deploy our projects without worrying about network fees or the limitations of using a public testnet. + +**Getting Started** + +To install Anvil, we'll use the command: + +```bash +curl -L https://foundry.paradigm.xyz | bash +``` + +This will install Anvil, Forge, Cast, and Chisel. + +**Testing Anvil** + +Once Anvil is installed, we can check that everything is working correctly by running: + +```bash +anvil --version +``` + +If Anvil is installed correctly, we should see the version number printed to our terminal. + +**Troubleshooting Anvil** + +If we can't run Anvil, there might be an issue with our bash or zsh profiles. To fix this, we can add the following lines to our profiles: + +For bash: + +```bash +echo "source $HOME/.bashrc >> $HOME/.bash_profile" +``` + +For zsh: + +```bash +echo "source $HOME/.zshrc >> $HOME/.zprofile" +``` + +These lines will help our terminal find Anvil more easily. + +**Conclusion** + +By installing Anvil, we've taken the first step towards building our own blockchain development environment. In the next lesson, we'll explore how to use Anvil to create and deploy our first smart contract! diff --git a/courses/moccasin-101/2-web3py/12-anvil2/+page.md b/courses/moccasin-101/2-web3py/12-anvil2/+page.md new file mode 100644 index 000000000..caa0da4e7 --- /dev/null +++ b/courses/moccasin-101/2-web3py/12-anvil2/+page.md @@ -0,0 +1,47 @@ +## Working with Anvil + +After installing Anvil, we can run it from the terminal. + +```bash +anvil --version +``` + +We will get an output similar to this: + +``` +anvil 0.2.0 (31c2400 2024-11-14T00:20:56.057504000Z) +``` + +Next, we can run the following command: + +```bash +anvil +``` + +And we will see the output of a running fake Ethereum blockchain node. + +We can leave the Anvil instance running in the terminal. LetοΏ½s create a new terminal and work inside of it instead of the first one. + +We will copy the link that Anvil gives us and replace our Tenderly endpoint. + +We will change this: + +```python +w3 = Web3(Web3.HTTPProvider('https://virtual.sepolia.rpc.tenderly.co/055b353c-fac-4506-8a8e-6496278f0c86')) +``` + +To this: + +```python +w3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545')) +``` + +Next, we will run the following command: + +```bash +python deploy_favorites.py +``` + +And we will see the output. + +To turn off the Anvil instance we can use **Control + C** to kill the terminal. We can also kill the terminal by closing the window. diff --git a/courses/moccasin-101/2-web3py/13-tx/+page.md b/courses/moccasin-101/2-web3py/13-tx/+page.md new file mode 100644 index 000000000..1abc5859a --- /dev/null +++ b/courses/moccasin-101/2-web3py/13-tx/+page.md @@ -0,0 +1,62 @@ +## Transactions + +So, we've been working with this `favorites_contract` thing which is on this contract object which is still a little hazy to us, but whatever. How do we actually deploy this thing? Well, if we think back to how we did it on the Remix VM, well, what do we do? We hit... well, we selected our contract, we went to the Vyper compiler, we hit "Compile", then we went to the "Deploy" tab, and we hit "Deploy" and it was that easy. When we were on injected provider with MetaMask, we would hit "Deploy", and MetaMask would pop up and say "Hey, do you want to send this transaction"? + +And that, my friends, is the key to our next step. To deploy this contract, we must build a transaction. This is how we are going to learn exactly what a transaction even is, and how we can create one. + +Now, this will be one of the few times we actually manually create the transaction ourselves. In the future, we will use tools that will essentially automatically create these transactions for us, but to really understand what's going on when we call a function or deploy a contract, we need to build our own custom, manually deployed, transaction. We have seen these transactions happen whenever we call a function or deploy a contract, but what are they, really? + +If we go to the ethereum.org website, there's a lot more information about what transactions really are. You can see down here, a submitted transaction includes the following information. You can see there's a `from` address, a `to` address, a signature (which is something we learned way, way, way back in blockchain basics when we learned about public and private keys and hashing and all that stuff). It has a nonce (which again we learned way back in blockchain basics). This is going to be a sequentially incrementing counter which indicates the transaction number - AKA the ID of the transaction. It has a `value`, which is that message.dot value, the amount of money sent with the transaction. You have `input data` - which, oh, we've seen that before right? If I call `deploy`.... we get this crazy hex data down here, and this is the input data for the transaction. You have `gasLimit`, which we have `gasLimit` and `maxPriorityFeePerGas` and `maxFeePerGas`, which is related to all the gas stuff. + +If you scroll down a little bit more, you'll see the transaction object will look something like this. And this is kind of a minimal example of a JSON or a JavaScript object in notation transaction. It has no real inputs or anything here. Having done this such a long time, I can very briefly tell you exactly what this is doing. All this is doing is sending this much Wei to this address from this address. That's pretty much it. This is an unsigned transaction here, there's no signatures, this is basically a regular transaction object that somebody would then need to sign. An Ethereum client, like `geth`, would usually handle the signing process or your MetaMask would handle the signing process or our Python would handle the signing process. + +So, in `geth` you would call this `account_signTransaction`, which would go ahead and sign it, and result in this signed transaction. We aren't going to use `geth`, we're going to use Python to sign our transactions here. Let me zoom out just a hair. Oh my god, that's also terrible! We're going to use Python to sign our transactions, but it will result in this signed transaction. + +You can see the transaction object has the following: + +```python +{ +"from": "0xEA674fd0e714f0979de3EdF0F56AA9716B889eC8", +"to": "0xac03bb73bca9e108536aaf14df5677c2b3d481e5a", +"gasLimit": "21000", +"maxFeePerGas": "300", +"maxPriorityFeePerGas": "10", +"nonce": "0", +"value": "1000000000000000" +} +``` + +You can see we have things like `gasLimit`, `maxPriorityFeePerGas`, `maxFeePerGas`, `nonce`, and `value`. The `input data` is a hex string, you can see, and the `from` and `to` addresses are all here. + +We will then sign it, which you can see we will get the VR and S components in the signed transaction: + +```python +{ +"jsonrpc": "2.0", +"id": 2, +"result": { +"raw": "0xf8838001820339407a565b7ed7a678686a4c162885bedbb695fe08044401", +"tx": { +"nonce": "0x0", +"maxFeePerGas": "0x1234", +"maxPriorityFeePerGas": "0x1234", +"gas": "0x555555", +"to": "0x07a565b7ed7a678686a4c162885bedbb695fe0", +"value": "0x1234", +"input": "0xabcd", +"v": "0x26", +"r": "0x222a79bc7cc5531c99bea708218316e80bcfcfeb0bbc32e977cc5c7f71a1abb20", +"s": "0x2aadee6b344b5bb15bc42c99c09d64a75a67009008da977d4a8cc72d48977f497149166", +"hash": "0xeba2df8f09e7a612a0d4444ccfa5cc839624bddc000ddd29e3346df46d9f3870f8" +} +} +} +``` + +The VR and S things are what we use to verify that it was indeed signed by whoever is signing it. I know this is a little bit confusing. If this goes over your head a little bit right now, don't worry too much about it. And the `hash` is obviously the hash of the transaction, and then the `raw` section right here is the recursive length prefix, or the RLP encoded form, that you can learn more about here. + +Now, this is kind of a lot of low-level signy stuff. There's kind of a lot here. Now there's a lot of kind of this low-level stuff here, and this VR and S thing can be a little bit overwhelming and confusing at first, but we are going to make it really simple. + +Basically, for us to construct a transaction, we want to create this object with a `from`, `gas`, all these gas parameters, `input data`, `to`, and `value`. So, we want to set up and create a transaction with all of this stuff in here. + +I hope you enjoyed this lesson. Be sure to watch the video for more information. diff --git a/courses/moccasin-101/2-web3py/14-tx-pt-2/+page.md b/courses/moccasin-101/2-web3py/14-tx-pt-2/+page.md new file mode 100644 index 000000000..1ff0b35e5 --- /dev/null +++ b/courses/moccasin-101/2-web3py/14-tx-pt-2/+page.md @@ -0,0 +1,60 @@ +## Crafting a Manually Created Transaction (tx) + +Transactions are often just noted with "tx". To get started creating our own transaction, we'll create an object called `transaction`. We'll make it blank for now. We'll also do a print line saying "Building the transaction...". + +```python +transaction = {} +print("Building the transaction...") +``` + +What are some of the things that we need in this transaction? Looking back here, we're probably going to need a `from` for the account that will deploy the contract. We'll skip this for now, you'll see why in a second. We also need a `to` which is the address that we're sending this to. Again, we'll come back to that as well. We'll also need `gas`. Since we have our `favorites_contract` setup in this `web3.eth.contract`, we can cheat a little bit. We can say `transaction = favorites_contract.constructor().build_transaction()`. This will help automatically populate a lot of those fields. + +```python +transaction = favorites_contract.constructor().build_transaction() +``` + +Let's do a little breakpoint here and run `python deploy_favorites.py`. + +```bash +python deploy_favorites.py +``` + +We ran into this issue: + +```bash +TypeError: 'NoneType' object is not iterable +``` + +Candidates equal ABI for ABI in contract type, if ABI equals constructor. We forgot to give our contract object the ABI, remember that Application Binary Interface. We can get that pretty easily by going back to our compilation details and just doing `compilation_details = compile_code(favorites_code, output_formats=['bytecode', 'abi'])`. + +```python +compilation_details = compile_code(favorites_code, output_formats=['bytecode', 'abi']) +``` + +Let's clear the terminal and rerun the command. + +```bash +python deploy_favorites.py +``` + +We now have this printing out, we have the ABI. We don't have any errors. + +```python +print(transaction) +``` + +We have `value` of zero because we don't send any value with this. We have `gas`, so it's estimating how expensive it is to send this transaction. How much gas is it going to cost to deploy our `favorites_contract`? It has `maxPriorityFeeGas`, `maxFeePerGas` which is more gas stuff. It has the `chainId` which it actually got from our anvil instance. + +So, if you look at anvil, you might even see this `eth_chainId`. If you see any lines or any word under that "Listening on 127.0.0.1:8545...", it means that somebody started making calls or interacting with this blockchain or interacting with our locally running anvil node. + +So, our Python code actually said, hey anvil, what is your chainId? Can I get that please? Oh, oh it's 31337. + +This data field that we have here, this is exactly the bytecode that we gave to our `favorites_contract` object. And then our `to` field is kind of confusing. + +If this breakpoint thing is confusing to you, by the way, you can just quit out and just type `print(transaction)` instead. + +```python +print(transaction) +``` + +Clear the terminal, and then rerun. You know just get an output like this, if that's if that's simpler for you than using the breakpoint here. diff --git a/courses/moccasin-101/2-web3py/15-tx-pt-3/+page.md b/courses/moccasin-101/2-web3py/15-tx-pt-3/+page.md new file mode 100644 index 000000000..b89a3e7de --- /dev/null +++ b/courses/moccasin-101/2-web3py/15-tx-pt-3/+page.md @@ -0,0 +1,36 @@ +## How to Deploy a Contract to the EVM + +In this lesson, we'll cover how to deploy a contract to the EVM. You'll see how to create a transaction object and build a contract. + +### The 'To' Field + +We are going to go over why the 'To' field in our transaction object is blank. When you deploy a contract to the EVM, you make the 'To' section of your transaction object blank. + +For example, if we were to take this transaction object and update the 'To' field, we could type: +```python +transaction["to"] = +``` +This will make the transaction no longer a contract deployment. Instead, it will send random data to the wallet address provided. + +For now, we will delete the 'To' field from the transaction object. We'll clear the terminal and rerun the code. + +### Building a Transaction Object +```python +def main(): + favorites_contract = w3.eth.contract(bytecode=compilation_details["bytecode"], + abi=compilation_details["abi"]) + # To deploy this, we must build a transaction + print("Building the transaction...") + transaction = favorites_contract.constructor().build_transaction() + print(transaction) +``` + +### Explanation + +We are building a transaction object that contains all the information we need to deploy a contract. The 'data' field inside the transaction object is where we will store the contract's bytecode. The 'To' field is left blank for deployment. + +The code shown above does not include the 'To' field. If we ran it in our terminal, the object would have a blank 'To' field as it is intended for a contract deployment. + +### Summary + +When deploying a contract to the EVM, you need to build a transaction object with a blank 'To' field. The data field will contain the bytecode of the contract, allowing you to successfully deploy it to the EVM. diff --git a/courses/moccasin-101/2-web3py/16-nonce/+page.md b/courses/moccasin-101/2-web3py/16-nonce/+page.md new file mode 100644 index 000000000..f4f266feb --- /dev/null +++ b/courses/moccasin-101/2-web3py/16-nonce/+page.md @@ -0,0 +1,67 @@ +## Setting the Transaction Nonce + +We'll start by customizing our transaction by adding a nonce. The nonce is a unique identifier associated with the transaction, and it is important to set it to ensure the transaction goes through successfully. + +We can add the nonce to our transaction by saying: + +```python +nonce = w3.eth.get_transaction_count(w3.eth.accounts[0]) +``` + +However, this line of code assumes we are using the first account in the list of accounts. To make this more specific, we can create a variable called `MY_ADDRESS` and store the address of the account we want to use. We can get this address from Anvil. + +```python +MY_ADDRESS = "0xf96e51aa8fd985ff46ce4a88827279cfffb92266" +``` + +Now, we can use this variable to get the nonce for our transaction. + +```python +nonce = w3.eth.get_transaction_count(MY_ADDRESS) +``` + +We can then add this nonce to our transaction dictionary. + +```python +transaction = favorites_contract.constructor().build_transaction( + { + "nonce": nonce, + "from": MY_ADDRESS, + "gasPrice": w3.eth.gas_price + } +) +``` + +We can also add the nonce directly to our transaction dictionary: + +```python +transaction = favorites_contract.constructor().build_transaction( + { + "nonce": nonce, + "from": MY_ADDRESS, + "gasPrice": w3.eth.gas_price + } +) +``` + +We can also manually set a gas price, if we want. We can do this by setting the `gasPrice` key in our transaction dictionary. + +```python +transaction = favorites_contract.constructor().build_transaction( + { + "nonce": nonce, + "from": MY_ADDRESS, + "gasPrice": w3.eth.gas_price + } +) +``` + +We can also manually set the chain ID. To do this, we can add a `chainId` key to our transaction dictionary. However, this will cause our transaction to fail if it is different from the chain ID of the network we are connected to. + +We can create our transaction object manually as well, but it is easier to use the `build_transaction` method. We can see that this method generates a lot of the data we need for our transaction, such as the `nonce`, `from`, `gasPrice`, and `data`. The `data` field represents the compiled bytecode of our Vyper contract. + +Finally, we can print out our transaction object to see the data that has been generated. + +```bash +python deploy_favorites.py +``` \ No newline at end of file diff --git a/courses/moccasin-101/2-web3py/17-sign-tx/+page.md b/courses/moccasin-101/2-web3py/17-sign-tx/+page.md new file mode 100644 index 000000000..7fbd9555e --- /dev/null +++ b/courses/moccasin-101/2-web3py/17-sign-tx/+page.md @@ -0,0 +1,35 @@ +## Signing a Transaction Object + +We've created our transaction object which is going to include the code for our Vyper contract. We're going to send it to the zero address because on the EVM world, that's how you deploy smart contracts. + +We're ready to go! Well, almost. What do we need to do? We need to sign it. Remember, that's how this whole blockchain thing works, you sign your transactions with your private key. So, we're going to use the web3 tooling to automatically sign this. We'll do: + +```python +w3.eth.account.sign_transaction(transaction, private_key='0x' * 64) +``` + +Now, as of right now, we don't have a private key, but I'm going to do something horrible that we're going to do just for now, and then I want you to never ever ever ever ever do this again. It's okay that we're doing it for now because we're using an Anvil private key, and these are very well known private keys. + +We're going to copy this private key and write it in our code. We're going to do: + +```python +PRIVATE_KEY = '0xac0974bec39a17e53ba78d40d28d4404b44facb478cbecde78ca4f704f7d74f7f2f80' +``` + +Then, down here, to sign our transaction, we're going to say: + +```python +signed_transaction = w3.eth.account.sign_transaction(transaction, private_key=PRIVATE_KEY) +``` + +Now, what we just did was something that, if you continue to do this, you will get destroyed in Web3. Just for stop. I don't make the rules. That is just what will happen, guaranteed, which was first off code our private key right into one of our scripts, and then number two, leave it in plain text. It's okay for right now because this is an Anvil private key, and these are well known publicly known private keys, so it's fine for now. But, we should never do this again, and I'll explain why in a minute. + +But, let's keep going. Let's do: + +```python +print(signed_transaction) +``` + +Let's even more cool about this is we just have a signed transaction, but we haven't actually sent it yet. We haven't sent it to the blockchain. It's literally just like sitting in our terminal here. Like, anybody could literally take this transaction, now that it's signed, and go execute it. + +So, additionally, you don't really want to have signed transactions lying around, so try not to do that, as well. But, p p p p p diff --git a/courses/moccasin-101/2-web3py/18-pkey-promise/+page.md b/courses/moccasin-101/2-web3py/18-pkey-promise/+page.md new file mode 100644 index 000000000..ffa83a9c1 --- /dev/null +++ b/courses/moccasin-101/2-web3py/18-pkey-promise/+page.md @@ -0,0 +1,12 @@ +We learned that we should never place our private keys directly in our scripts or in plaintext. This is a serious risk. We could lose all of our money if someone got their hands on our private keys. + +We need to protect our private keys. The best way to do this is to encrypt them and store them in a secure location. We should never share even our encrypted private keys. + +We can create a file called `promise.md` to remind ourselves of this important promise. Here's what we should write in the file: + +```text +I promise to never place my plaintext private key associated with real money anywhere. +I will always encrypt my private keys and never share even the encrypted keys. +``` + +This is a good practice to follow. It's always better to be safe than sorry! diff --git a/courses/moccasin-101/2-web3py/19-env-vars/+page.md b/courses/moccasin-101/2-web3py/19-env-vars/+page.md new file mode 100644 index 000000000..328a19e9d --- /dev/null +++ b/courses/moccasin-101/2-web3py/19-env-vars/+page.md @@ -0,0 +1,114 @@ +## Loading Environment Variables + +Let's learn about loading environment variables in a Python project. + +We'll use a file called `.env` to store sensitive information like our private key and address for the project. + +**1. Create a `.env` file** + +Create a `.env` file in your project directory, and we'll add our sensitive data here. + +``` +MY_ADDRESS="0xf39f0d6e51aad88f6f4c6eba88827279cfffb92266" +PRIVATE_KEY="0xac0974bec39a17e36ba4a6bd4d238ff1944bacb478cbded5efcae784d7f4f2ff80" +RPC_URL="http://127.0.0.1:8545" +``` + +**2. Install the `python-dotenv` package** + +We can use the `python-dotenv` package to load our environment variables. We'll use the terminal to install this package. + +```bash +uv add python-dotenv +``` + +**3. Import the necessary libraries and functions** + +We'll import the `load_dotenv` function from `python-dotenv`, and the `os` module. + +```python +from dotenv import load_dotenv +import os +``` + +**4. Load the environment variables** + +We can load the environment variables using the `load_dotenv` function. + +```python +load_dotenv() +``` + +**5. Access environment variables** + +Once the environment variables are loaded, we can access them using the `os.getenv()` function. + +```python +RPC_URL = os.getenv("RPC_URL") +MY_ADDRESS = os.getenv("MY_ADDRESS") +PRIVATE_KEY = os.getenv("PRIVATE_KEY") +``` + +**6. Add the `.env` file to your `.gitignore` file** + +It's very important to include the `.env` file in your `.gitignore` file. This will ensure that the file is not committed to your version control system, preventing accidental exposure of your sensitive information. + +**7. Rename your project file** + +Rename your project file to reflect that you're working with sensitive information, we'll add _unsafe_ to our file name. + +```python +# original file +deploy_favorites.py +# new file +deploy_favorites_unsafe.py +``` + +**8. Never use a private key associated with real funds in a `.env` file** + +Using a private key associated with real funds in a `.env` file is a major security risk and should never be done! Instead, use a separate file or configuration that is not part of your project's source control. + + **9. What to do instead of `.env`** + +Instead of using a `.env` file, a better approach is to utilize a separate configuration file or a dedicated environment variable management system that is designed for security. This is especially crucial when handling sensitive data like private keys. + +**10. Example Code** + +```python +from web3 import Web3 +from vyper import compile_code +from dotenv import load_dotenv +import os + +load_dotenv() + +RPC_URL = os.getenv("RPC_URL") +MY_ADDRESS = os.getenv("MY_ADDRESS") +PRIVATE_KEY = os.getenv("PRIVATE_KEY") + +def main(): + print("Let's read in the Vyper code and deploy it!") + with open("favorites.vy", "r") as favorites_file: + favorites_code = favorites_file.read() + compilation_details = compile_code(favorites_code, output_formats=["bytecode", "abi"]) + print(compilation_details) + + w3 = Web3(Web3.HTTPProvider(RPC_URL)) + favorites_contract = w3.eth.contract(bytecode=compilation_details["bytecode"], abi=compilation_details["abi"]) + + # To deploy, we must build a transaction + print("Building the transaction...") + nonce = w3.eth.get_transaction_count(MY_ADDRESS) + transaction = favorites_contract.constructor().build_transaction({ + "nonce": nonce, + "from": MY_ADDRESS, + "gasPrice": w3.eth.gas_price + }) + + signed_transaction = w3.eth.account.sign_transaction(transaction, PRIVATE_KEY) + + # We use the Web3 sendRawTransaction function to send the signed transaction to the blockchain + w3.eth.send_raw_transaction(signed_transaction.rawTransaction) + + print("Deployed! Transaction Hash:", signed_transaction.hash) +``` diff --git a/courses/moccasin-101/2-web3py/2-setup/+page.md b/courses/moccasin-101/2-web3py/2-setup/+page.md new file mode 100644 index 000000000..7d1829a8b --- /dev/null +++ b/courses/moccasin-101/2-web3py/2-setup/+page.md @@ -0,0 +1,50 @@ +## Project Setup + +We are going to learn about Web3 development and the tools we can use to interact with the blockchain. First, we need to get our development environment set up. We need to have Visual Studio Code installed along with the code command. + +We'll be working with a folder called `web3py-favorites-cu` and inside this folder, we'll be using the `uv` tool to manage our Python versioning. + +Let's create a new folder called `web3py-favorites-cu` using the `mkdir` command. + +```bash +mkdir web3py-favorites-cu +``` + +Once we've created the folder, let's navigate into it. We'll use the command `cd` and follow this with the name of the folder. + +```bash +cd web3py-favorites-cu +``` + +If you are not already using `uv` we need to initialize a new Python project using `uv`. To do this, we use the `uv init` command. + +```bash +uv init +``` + +We'll be using `uv` throughout the course. The `uv` tool allows us to manage Python versions, dependencies, and virtual environments. You can find more information on the `uv` tool at the following link: [https://docs.astral.sh/uv/](https://docs.astral.sh/uv/) + +The `uv init` command creates a number of files in the folder, including `pyproject.toml`, `hello.py`, `.gitignore`, and README.md`. + +We'll be able to see the files created in the Explorer section of VS Code. + +Our next step is to write a high level overview in the `README.md` file of what we'll be accomplishing in this project. + +```python +# Web3 Favorites CU + +What does this project do? +What are we going to learn? + +1. Deploy our Vyper contract from raw python +2. Encrypt our private keys locally, and use them to sign transactions +3. This is going to be reproducible by other engineers using uv with python + +1. How to deploy a contract from python +2. What is a transaction comprised of? +3. What is json keystore? +4. How can I safely store my private key? + +``` + +We will learn how to work with `uv`, along with all the other files we initialized in this project. We'll go through each of these items in a more detailed manner in future sections. diff --git a/courses/moccasin-101/2-web3py/20-send-tx/+page.md b/courses/moccasin-101/2-web3py/20-send-tx/+page.md new file mode 100644 index 000000000..272eee13d --- /dev/null +++ b/courses/moccasin-101/2-web3py/20-send-tx/+page.md @@ -0,0 +1,37 @@ +We are still in the process of learning how to build, sign, and deploy our first Solidity smart contract! In this lesson, we'll learn how to send our signed transaction, and then we'll tackle the subject of how to encrypt our private key, so that we never have to write it out like this again. + +Let's go ahead and finish up sending this transaction. We have our signed transaction, now we want to go ahead and send it. To send it, we could say, uh, we would do: + +```python +w3.eth.send_raw_transaction(signed_transaction.rawTransaction) +``` + +And this is going to send that signed transaction to our RPC URL that we've added right here. + +```python +RPC_URL = os.getenv("RPC_URL") +``` + +And we're going to get a response with a transaction hash. And we can say: + +```python +tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) +print(f"My TX hash is {tx_hash.hex()}") +``` + +And then, this is just when we send the transaction, we actually have we actually have to wait for the transaction to complete to be included in a block. So, we would say: + +```python +tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) +print(f"Done! Contract deployed to {tx_receipt.contractAddress}") +``` + +And boom! So, let's go ahead, let's pull up our terminal. Make sure our virtual environment is up. Make sure anvil is up and running. And we can run: + +```bash +python deploy_favorites_unsafe.py +``` + +We will now get this kind of crazy output. So, we get the signed transaction. Yeah, we've seen that. Now, we'll see the transaction hash is This is the byte code of the transaction hash. We could convert it from bytes to like a regular transaction hash that we're familiar with, but whatever. And then, we see Done! Contract deployed to here. And we have successfully deployed our contract. + +Now that it's actually deployed, we could then go ahead and interact with it and learn how to build transactions to interact with it. But, those are going to be pretty much very similar to what we just did here. So, you should be incredibly proud of yourself for deploying your first contract. But, don't log off yet, because I need to teach you how to never, ever, ever do this again. So, diff --git a/courses/moccasin-101/2-web3py/21-encrypt/+page.md b/courses/moccasin-101/2-web3py/21-encrypt/+page.md new file mode 100644 index 000000000..dd973423d --- /dev/null +++ b/courses/moccasin-101/2-web3py/21-encrypt/+page.md @@ -0,0 +1,97 @@ +## Encrypting Your Keys in Python + +We'll learn how to encrypt our private key so that we don't ever have to store it in plain text. We'll use tools that will automatically do this encryption for us later on, but let's learn how to do it manually. + +We'll start by creating a new file called `encrypt_key.py`: + +```python +if __name__ == "__main__": + main() +``` + +```python +def main(): + pass +``` + +We'll create a `main()` function. + +Now, this `encrypt_key.py` file is going to be how we encrypt our key. To encrypt it, we're going to use the Python built-in tool called `getpass`. + +We'll start by removing any references to our private key in a `.env` file. + +Next, we'll import the `getpass` library: + +```python +import getpass +``` + +In our `main()` function, we'll use `getpass.getpass()` to prompt the user for their private key: + +```python +def main(): + private_key = getpass.getpass("Enter your private key: ") # input() +``` + +This is similar to the `input()` command in Python. + +We'll then create an Eth account using the `eth_account` library that comes bundled with Web3.py: + +```python +from eth_account import Account +``` + +```python +def main(): + private_key = getpass.getpass("Enter your private key: ") # input() + my_account = Account.from_key(private_key) +``` + +We'll then ask the user for a password to encrypt the private key: + +```python +password = getpass.getpass("Enter a password:\n") # input() +``` + +We'll use the `\n` character to create a new line for the password prompt. + +We'll encrypt the private key using the password and the `Account` object we created: + +```python +encrypted_account = my_account.encrypt(password) +``` + +We'll then save the encrypted account to a file. To do this, we'll use the `pathlib` library that comes bundled with Python: + +```python +from pathlib import Path +``` + +We'll create a path object to specify the path to our encrypted keystore: + +```python +KEYSTORE_PATH = Path("keystore.json") +``` + +We can then use the path object to open the file and save the encrypted account using the `json` library that comes bundled with Python: + +```python +import json +``` + +```python +with KEYSTORE_PATH.open("w") as fp: + json.dump(encrypted_account, fp) +``` + +To ensure that our encrypted keystore is not accidentally committed to version control, we should add it to our `.gitignore` file. + +We can now run our script from the terminal: + +```bash +python encrypt_key.py +``` + +We'll be prompted to enter our private key and then a password. Once we enter both, the script will save our encrypted private key to the `keystore.json` file. + +**Tag:** JSON object, keystore file, encrypt private key, `getpass`, `pathlib`, `json` diff --git a/courses/moccasin-101/2-web3py/22-pledge/+page.md b/courses/moccasin-101/2-web3py/22-pledge/+page.md new file mode 100644 index 000000000..31ee6eb36 --- /dev/null +++ b/courses/moccasin-101/2-web3py/22-pledge/+page.md @@ -0,0 +1,43 @@ +## The .env Pledge + +We've seen that nearly half a billion dollars was lost in private key leaks in the past year. It's important to reiterate this. We need to be good at storing and securing private keys. + +There's a pledge called the .env pledge. It's a simple concept, but it's important to understand every single line. + +I'll read the pledge to you now. + +**The Pledge** + +I solemnly swear, that I will never place a private key or secret phrase or mnemonic in a .env file that is associated with real funds. + +I will only place private keys in a .env file that have ONLY testnet ETH, LINK, or other cryptocurrencies. + +When I'm testing and developing, I will use a different wallet than the one associated with my real funds. + +I am aware that if I forget a .gitignore and push my key/phrase up to GitHub even for a split-second, or show my key/phrase to the internet for a split-second, it should be considered compromised and I should move all my funds immediately. + +If I am unsure if my account has real funds in it, I will assume it has real funds in it. If I assume it has real funds in it, I will not use it for developing purposes. + +I am aware that even if I hit add account on my metamask (or other ETH wallet) I will get a new private key, but it will share the same secret phrase/mnemonic of the other accounts generated in that metamask (or other ETH wallet). + +**How to Take the Pledge** + +We'll go over how to take the pledge now. You'll need to open your browser, we'll go over to x.com, you can just type in x.com or Twitter. Then, you can go ahead and copy this link, and we'll go over to our browser and we'll paste the link in here. You can either write a reply or you can comment. Whatever works better for you. + +**Taking the Pledge** + +We'll go over how to take the pledge now. You'll need to open your browser, we'll go over to x.com, you can just type in x.com or Twitter. Then, you can go ahead and copy this link, and we'll go over to our browser and we'll paste the link in here. You can either write a reply or you can comment. Whatever works better for you. + +```bash +https://x.com/ +``` + +You can type in "I will be safe" either as a reply or a comment. + +```bash +I WILL BE SAFE +``` + +And then, we highly recommend if you've taken this fund, I would love it if you you know were to copy the link, go to x.com, or Twitter, or whatever, post a little tweet saying "I will be safe, I have decided to take the .env pledge" and then, you know, tag me at PatrickAlphaC, and then, you know, maybe tag Cyfrinauts or whatever you want to do here. + +So anyways, now you know once we move on to moccasin, we will probably come back to this again, very briefly, just to again reiterate this .gitignore stuff, and make sure we understand the new pieces. But anyways, thank you for taking this pledge, and thank you for taking yourself seriously. All right, I'm going to go ahead and delete my terminal. diff --git a/courses/moccasin-101/2-web3py/23-workshop/+page.md b/courses/moccasin-101/2-web3py/23-workshop/+page.md new file mode 100644 index 000000000..ebcaf62aa --- /dev/null +++ b/courses/moccasin-101/2-web3py/23-workshop/+page.md @@ -0,0 +1,83 @@ +## Deploying Smart Contracts to Testnets + +In this lesson, we will cover how to deploy a smart contract to a testnet. We will deploy our `favorites.vy` contract to Anvil locally, and Tenderly. + +Before we begin, make sure you have the following setup: + +* Anvil RPC URL +* Your private key +* Tenderly account + +We will first deploy to Anvil. We have already covered how to compile our `favorites.vy` contract, so we will use that compiled bytecode for this deployment. + +```python +from vyper import compile_code +from web3 import Web3 +from dotenv import load_dotenv +import os +from getpass import getpass +from eth_account import Account + +load_dotenv() + +RPC_URL = os.getenv("RPC_URL") +MY_ADDRESS = os.getenv("MY_ADDRESS") + +def main(): + print("Let's read in the Vyper code and deploy it!") + with open("favorites.vy", "r") as favorites_file: + favorites_code = favorites_file.read() + compilation_details = compile_code(favorites_code, output_formats=["bytecode", "abi"]) + print(compilation_details) + + w3 = Web3(Web3.HTTPProvider("https://127.0.0.1:8545")) + favorites_contract = w3.eth.contract(bytecode=compilation_details["bytecode"], abi=compilation_details["abi"]) + print("Building the transaction...") + + nonce = w3.eth.get_transaction_count(MY_ADDRESS) + transaction = favorites_contract.constructor().build_transaction( + { + "nonce": nonce, + "from": MY_ADDRESS, + "gasPrice": w3.eth.gas_price + } + ) + print(transaction) + + private_key = decrypt_key() + signed_transaction = w3.eth.account.sign_transaction(transaction, private_key=private_key) + print(signed_transaction) + + tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) + print(f"My TX Hash is {tx_hash.hex()}") + + tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + print(f"My TX Hash is {tx_receipt.transactionHash.hex()}") + print(f"Done! Contract deployed to {tx_receipt.contractAddress}") + +def decrypt_key() -> str: + with open(KEYSTORE_PATH, "r") as fp: + encrypted_account = fp.read() + password = getpass("Enter your password: ") + key = Account.decrypt(encrypted_account, password) + print(f"Decrypted key! {key}") + return key + +if __name__ == "__main__": + main() +``` +We can run this code to deploy the `favorites.vy` contract to our locally running Anvil instance. + +Now, we will deploy to Tenderly. + +To deploy to Tenderly, we will need to copy the RPC URL from the Tenderly dashboard and update the `w3` variable in the code. + +```python +w3 = Web3(Web3.HTTPProvider("https://your-tenderly-rpc-url")) +``` + +The next step in our workshop is to figure out how to deploy our `favorites.vy` contract to Anvil locally, using a different wallet address and a new encrypted key. + +We can modify the code to connect to our local Anvil RPC URL. We can then modify the `from` address in the `transaction` object to a different wallet address. Finally, we need to create a new encrypted key and decrypt it before signing and sending the transaction. + +This will give us some additional practice with these concepts and is a good example of how to deploy a smart contract to a testnet. diff --git a/courses/moccasin-101/2-web3py/24-recap/+page.md b/courses/moccasin-101/2-web3py/24-recap/+page.md new file mode 100644 index 000000000..5924493a5 --- /dev/null +++ b/courses/moccasin-101/2-web3py/24-recap/+page.md @@ -0,0 +1,134 @@ +## Recap + +We learned a lot of useful blockchain and Python fundamentals in this section. Let's take a look at what we covered. + +### Python + +We learned that when we call a Python file, we're secretly setting `__name__` to `__main__`. This is a common practice in more professional Python setups. + +```python +if __name__ == "__main__": + main() +``` + +We learned how to compile our Vyper contract from the command line. + +```bash +Vyper favorites.vy +``` + +We can also use the Vyper package within our Python code. + +```python +from Vyper import compile_code +``` + +We learned how to create a function called `compile_code`, which opens a Vyper file, reads its contents, compiles the code, and returns both the bytecode and the ABI. + +```python +def compile_code(favorites_code, output_format='["bytecode", "abi"]'): + compilation_details = compile_code(favorites_code, output_format='["bytecode", "abi"]') + print(compilation_details) +``` + +We also learned how to connect to an RPC URL. + +```python +w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) +``` + +### Blockchain + +We learned to use the `load_dotenv` function to load environment variables from our `.env` file. + +```python +from dotenv import load_dotenv + +load_dotenv() +``` + +We then learned to use `os.getenv` to grab our RPC URL from our `.env` file. + +```python +RPC_URL = os.getenv("RPC_URL") +``` + +We also used `os.getenv` to grab our address from our `.env` file. + +```python +MY_ADDRESS = os.getenv("MY_ADDRESS") +``` + +We learned how to build our transactions using the `build_transaction` function. + +```python +transaction = favorites_contract.constructor().build_transaction({ + "nonce": nonce, + "from": MY_ADDRESS, + "gasPrice": w3.eth.gas_price +}) +``` + +Our transaction includes a nonce, a `from` address, and a gas price. We can also customize transactions with additional parameters. + +We learned to decrypt our private key using the `decrypt_key` function. + +```python +def decrypt_key(KEYSTORE_PATH): + encrypted_account = fp.read() + password = getpass.getpass("Enter your password: ") + key = Account.decrypt(encrypted_account, password) + print("Decrypted key!") + return key +``` + +We then used `w3.eth.account.sign_transaction` to sign our transaction using our private key. + +```python +signed_transaction = w3.eth.account.sign_transaction(transaction, private_key=private_key) +print(signed_transaction) +``` + +We then sent the signed transaction to our blockchain node using the `send_raw_transaction` function. + +```python +tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) +print(f"My TX hash is {tx_hash}") +``` + +Finally, we waited for our transaction to finish and learned that our transaction hash can be used to track the transaction. + +```python +tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) +print(f"Done! Contract deployed to {tx_receipt.contractAddress}") +``` + +### The `uv` Tool + +We learned how to use the `uv` tool to add Python packages to our `pyproject.toml` file. + +```bash +uv add python-dotenv +``` + +We also learned how to use the `uv` tool to install Vyper into its own isolated virtual environment. + +```bash +uv tool install Vyper +``` + +We learned how to use the `uv` tool to sync all dependencies to our virtual environment. + +```bash +uv sync +``` + +We learned how to activate our virtual environment using the `source` command. + +```bash +source .venv/bin/activate +``` + +We also learned how to choose the Python interpreter in VS Code by clicking the button on the status bar. + +Congratulations on finishing this section! You should be proud of yourself for getting this far. Now is a great time to go for a walk, grab some ice cream, maybe grab a coffee, but if it's too late, don't grab a coffee, and you should be incredibly proud of yourself for getting this far. I'll see you in the next one! diff --git a/courses/moccasin-101/2-web3py/3-vyper-extension/+page.md b/courses/moccasin-101/2-web3py/3-vyper-extension/+page.md new file mode 100644 index 000000000..4308896c0 --- /dev/null +++ b/courses/moccasin-101/2-web3py/3-vyper-extension/+page.md @@ -0,0 +1,19 @@ +## Vyper VS Code Extension + +We'll go back to the GitHub repo for this course: [link to repo] and then we can either grab the `favorites.vy` file from the Web3py Favorite's Cyffin Updraft, or go back to the Moccasin full course. + +We'll scroll up and look for the Favorite's Cyffin Updraft repo. We'll click the `favorites.vy` file and then copy the entire file. + +Next, we'll go to our VS Code and right-click in the file explorer. We'll create a new file and name it `favorites.vy`, and then paste the copied code into it. + +We'll now have our smart contract in our code editor. It's likely to look pretty rough and lack syntax highlighting. + +Remember that Remix has really nice syntax highlighting. We can get the same functionality in VS Code by using an extension. We'll go to the Extensions tab in VS Code and search for "Vyper". We'll see the "Vyper" extension by TintinWeb. We'll install it. + +We'll now see that our Vyper file is highlighted very nicely. + +**Tip:** If you have Vyper installed, you can type `which Vyper` in your terminal. This should show you the version you have installed. + +You might also see some red squiggly lines, this could be an indication that you have the wrong version of Vyper installed. But, if you've been following along in the previous sections, then you should have the right version. + +We'll cover how to fix this in a later section, but for now, we're going to keep moving forward. diff --git a/courses/moccasin-101/2-web3py/4-compile-python/+page.md b/courses/moccasin-101/2-web3py/4-compile-python/+page.md new file mode 100644 index 000000000..0c8cde0b4 --- /dev/null +++ b/courses/moccasin-101/2-web3py/4-compile-python/+page.md @@ -0,0 +1,20 @@ +## Compiling Vyper in Python + +We'll compile our Vyper code using the Vyper compiler. We can do this in a couple of different ways, but regardless of how we choose to compile our Vyper contract, we first need to download the Vyper compiler. + +The Vyper compiler is available on GitHub at: [https://github.com/vyperlang/vyper](https://github.com/vyperlang/vyper). We can install the Vyper compiler using the following command in our terminal: + +```bash +uv tool install vyper +``` + +After we install the Vyper compiler, we can compile our "favorites" Vyper contract using the following command: + +```bash +vyper favorites.vy +``` + +After we run this command, a large string of hexadecimal characters will be displayed in our terminal. This string of characters is the raw bytecode that will be sent to the blockchain. + +This bytecode represents our Vyper contract in a machine-readable form that can be executed by the blockchain. + diff --git a/courses/moccasin-101/2-web3py/5-idk/+page.md b/courses/moccasin-101/2-web3py/5-idk/+page.md new file mode 100644 index 000000000..4ce35504d --- /dev/null +++ b/courses/moccasin-101/2-web3py/5-idk/+page.md @@ -0,0 +1,83 @@ +We've just installed Vyper using the `uv tool install` command. This might seem a little different if you're used to installing packages using the Python environment. + +Traditionally, you might be used to running commands like: + +```bash +python3 -m pip install vyper +``` + +or even just: + +```bash +pip install vyper +``` + +These commands would install Vyper directly into your active Python environment. However, the `uv tool install` command installs Vyper into its own isolated environment. + +We have a global Python environment, which typically holds the default Python version and any globally installed packages. + +We also have a virtual environment, which is essentially a self-contained Python environment. This is where we installed Vyper using `uv tool install`. + +If we were to install Vyper into the global Python environment, we might run into issues if we want to use different package versions. It would then be difficult to switch between the two environments without affecting the other. + +To show you how isolated environments work, we can run a few commands. We'll start by creating a virtual environment using Python: + +```bash +python3.11 -m venv .venv +``` + +Then, we activate this virtual environment using: + +```bash +source .venv/bin/activate +``` + +We can check our current environment using: + +```bash +which vyper +``` + +We see that it points to the global Python environment, even though we're in the virtual environment. + +Now, let's deactivate the virtual environment: + +```bash +deactivate +``` + +And check the `which` command again: + +```bash +which vyper +``` + +We see that Vyper is not found, because we deactivated the virtual environment. + +Let's run the `uv tool` command again: + +```bash +uv tool install vyper +``` + +We see the output, and that Vyper is installed into its isolated environment. + +Now, if we run the `which` command again: + +```bash +which vyper +``` + +We see that Vyper is now installed in the virtual environment. + +The main advantage of using the `uv tool` to install Vyper is that it creates a completely isolated environment. We can't affect the global Python environment, and vice versa. This is important because Vyper, being a compiler, is extremely sensitive to different package versions. + +If we were to use different package versions, we could run into issues. It's best practice to install Vyper using the `uv tool` to avoid any conflicts. + +We're also going to talk about VS Code and how it interacts with virtual environments. + +We will have an environment, which is typically the global environment, and another environment which is the isolated environment. We can switch between the two environments. + +We can use our AI to ask questions about the difference between a Python virtual environment and when we're not in a virtual environment. + +That's it for our lesson on installing Vyper into an isolated environment using the `uv tool`. Remember, using isolated environments is the best way to avoid conflicts when working with Vyper. Keep practicing and don't hesitate to ask your AI buddies or the community any questions you have. diff --git a/courses/moccasin-101/2-web3py/6-__nam__/+page.md b/courses/moccasin-101/2-web3py/6-__nam__/+page.md new file mode 100644 index 000000000..d45031042 --- /dev/null +++ b/courses/moccasin-101/2-web3py/6-__nam__/+page.md @@ -0,0 +1,66 @@ +## Deploying our contract to the blockchain + +We are going to learn how to compile Vyper in a script and deploy it to a blockchain. First, we are going to create a new file called `deploy_favorites.py`. This is going to be our Python file for actually deploying our smart contract to a blockchain. + +Here is a bit of a Python tip: in Python, whenever you call a file (for example, when we did `python3.11 basic_python.py` from the last course or from the last section, or if we ran `python3.11 hello.py` or even if we did `uv run python hello.py`), we might see a pop up. We will fix our Python environment in just a second; for now, feel free to just hit that X there. + +But you will see `Hello from web3py-favorites-cu` or whatever you have in your `hello.py`. You will see, actually, UV gave us this weird template: + +```python +if __name__ == "__main__": + main() +``` + +If `__name__ == "__main__":` this string main calls the main function, which prints this out. Whenever you run a file, you essentially say, "hey, make the name that you are looking to call `__main__`." + +So typically we like to be very explicit when we are coding in a more professional environment. So we are actually going to follow this same convention in our `deploy_favorites.py`. + +We are going to close everything here and I am going to paste this line in here: + +```python +if __name__ == "__main__": + main() +``` + +Now, these double underscores are also known as dunder names or double underscore names. This is called `__name__` and this is called `__main__`, which means double underscore, so, double underscore name and double underscore main. If `__name__ == "__main__":` run main. We will create a little `def main`: + +```python +def main(): + print("Hello from deploy_favorites.py") +``` + +And to make sure this is working, we can do a little `print`: + +```python +print("Hello from deploy_favorites.py") +``` + +And you can see right there, actually, this is my GitHub Copilot giving me a suggestion. And since I want to take it, I'm going to go ahead and hit tab to add it to my file. Then, of course, hit command s or control s to save. And we can get rid of the pass line now. + +To make sure this is working, we can run: + +```bash +uv run python deploy_favorites.py +``` + +and we should get `Hello from deploy_favorites.py`. + +But, if you get sick of always doing `uv run python`, we can also, once again, do `uv venv`: + +```bash +uv venv +``` + +We can then do this line (I'm just going to copy and paste it): + +```bash +source .venv/bin/activate +``` + +Now, we have our virtual environment setup. And now we can just run: + +```bash +python deploy_favorites.py +``` + +and I can just hit tab to auto complete. And we get a `Hello from deploy_favorites.py`. Great! diff --git a/courses/moccasin-101/2-web3py/7-vyper-package/+page.md b/courses/moccasin-101/2-web3py/7-vyper-package/+page.md new file mode 100644 index 000000000..ea4349b21 --- /dev/null +++ b/courses/moccasin-101/2-web3py/7-vyper-package/+page.md @@ -0,0 +1,53 @@ +## Using the Vyper compiler as a package + +In this lesson, we will learn how to use the Vyper compiler as a package. + +We begin by opening a file named `deploy_favorites.py` and then edit the `print` statement to read: + +```python +print("Let's read in the Vyper code and deploy it!") +``` + +We then proceed to open our `favorites.vy` file, which contains Vyper code, in read-only mode by using the following code: + +```python +with open("favorites.vy", "r") as favorites_file: + favorites_code = favorites_file.read() +``` + +We then need to compile the Vyper code. We've already installed Vyper and can use it in our project. + +To do so, we must first add Vyper as a dependency to our project by running the following command in our terminal: + +```bash +uv add vyper +``` + +We then need to synchronize our dependencies to our project. We can do this by typing the following command in our terminal: + +```bash +uv sync +``` + +To call the Vyper compiler and compile our Vyper code, we can run the following code: + +```python +from vyper import compile_code +compilation_details = compile_code(favorites_code, output_formats=["bytecode"]) +``` + +We have successfully compiled our Vyper contract and stored the compilation output in a variable called `compilation_details`. + +We can then print the compilation details by adding the following line of code: + +```python +print(compilation_details) +``` + +We can run our Python script in our terminal with the following command: + +```bash +python deploy_favorites.py +``` + +We'll see the output of the compilation details including the bytecode section. This bytecode section will include the same hex codes we saw when running Vyper from the command line. diff --git a/courses/moccasin-101/2-web3py/8-vscode-py/+page.md b/courses/moccasin-101/2-web3py/8-vscode-py/+page.md new file mode 100644 index 000000000..32f2e0c88 --- /dev/null +++ b/courses/moccasin-101/2-web3py/8-vscode-py/+page.md @@ -0,0 +1,32 @@ +## Telling VS Code what our python environment is + +We've been working with our project, and we've run a command: + +```bash +uv deploy_favorites.vy +``` + +VS Code is telling us something important. We get a "yellow squiggly" error that says: + +> Import "vyper" could not be resolved PyLance (reportMissingImports) + +VS Code is saying "Hey, I'm currently looking for this vyper thing you're talking about, but I don't see it. I don't see this vyper package". + +We have the right environment for running our commands because we were able to print out compiled vyper bytecode. So VS Code doesn't know which python environment we're working with. + +We need to tell PyLance which python environment we're using: + +1. We'll click the button to "Select Interpreter" +2. We'll select the "Recommended" option + +The recommended option will probably be our venv. For me, it's my global python environment, but yours will likely be different. + +We're going to switch the PyLance's interpreter path to our venv by clicking the venv that we're using. Now, the squiggly line will disappear, and we'll see a nice green color instead. + +What's going on? VS Code PyLance linter (or formatter) now knows which python environment we're working with. We're using the python in the venv folder. + +The reason we setup every project like this is to avoid clashes between projects. It allows each project to kind of run in its own isolated type of environment. + +If you don't like doing this, you can go ahead and install everything in your global environment. I don't recommend doing that because it'll make your life miserable. + +You do whatever you want to do. diff --git a/courses/moccasin-101/2-web3py/9-web3-py/+page.md b/courses/moccasin-101/2-web3py/9-web3-py/+page.md new file mode 100644 index 000000000..815e6b4f7 --- /dev/null +++ b/courses/moccasin-101/2-web3py/9-web3-py/+page.md @@ -0,0 +1,44 @@ +## Introduction to web3.py + +We're going to learn how to deploy a contract to the Ethereum blockchain by using the web3.py library. We're going to start by installing web3.py in our UV environment. + +```bash +uv add web3 +``` + +We can check to make sure it's installed by running `uv sync` and checking our `pyproject.toml` file for the web3.py dependency. + +Next, we'll import web3.py and the Ethereum Tester Provider: + +```python +from web3 import Web3 +from web3.providers.eth_tester import EthereumTesterProvider +``` + +We'll then initialize a Web3 object by using the Ethereum Tester Provider: + +```python +w3 = Web3(EthereumTesterProvider()) +``` + +The Ethereum Tester Provider simulates a local blockchain node, which is useful for development. + +Now, let's get our bytecode from the Vyper object. We'll use the `compile_code` function and specify the output format as `bytecode`: + +```python +compilation_details = compile_code(favorites_code, output_formats=['bytecode']) +``` + +We can print out the `compilation_details` to see the results of compilation, including the byte code. + +```python +print(compilation_details) +``` + +Finally, we can create a contract instance by using the bytecode from the `compilation_details` object. + +```python +favorites_contract = w3.eth.contract(bytecode=compilation_details['bytecode']) +``` + +We'll explore how to interact with the deployed contract in later lessons. diff --git a/courses/moccasin-101/3-boa-favs/1-intro/+page.md b/courses/moccasin-101/3-boa-favs/1-intro/+page.md new file mode 100644 index 000000000..f9a95008b --- /dev/null +++ b/courses/moccasin-101/3-boa-favs/1-intro/+page.md @@ -0,0 +1,80 @@ +## TitanoBoa Favorite's List + +In this section, we're going to learn how to use **TitanoBoa**, a tool that streamlines the process of deploying and interacting with smart contracts. It's similar to what we did in the previous section using web3.py, but with TitanoBoa, the code becomes much more concise. + +We'll start by navigating to the **`boa-favorites-cu`** repository on GitHub. This repository contains the finalized code for this project. + +Here's how the code looks: + +```python +from dotenv import load_dotenv +import boa +from boa.network import NetworkEnv, EthereumRPC +from eth_account import Account + +load_dotenv() + +ANVIL_URL = "http://127.0.0.1:8545" +# Never do this with a real key... +ANVIL_KEY = "0xac0974bec39a17e36bba46bda4d238ff944bacb478ced5efcae784d7bf4f72ff80" + +def main(): + print("Let's read in the Vyper code and deploy it to the blockchain!") + my_account = Account.from_key(ANVIL_KEY) + env = NetworkEnv(EthereumRPC(ANVIL_URL)) + boa.set_env(env) + boa.env.add_account(my_account, force_eoa=True) + + # Deploy the contract to a 'pyevm' network! + favorites_contract = boa.load("favorites.vy") + print(favorites_contract) + + print("Storing a number...") + favorites_contract.store(5) + print(f"Favorite Number: {favorites_contract.retrieve()}\n") + + print("Storing a person...") + favorites_contract.add_person("Alice", 25) + print(f"New Person: {favorites_contract.list_of_people(0)}\n") + + if __name__ == "__main__": + main() +``` + +We can see that **TitanoBoa** simplifies the deployment process using just a single line of code: + +```python +favorites_contract = boa.load("favorites.vy") +``` + +This line reads in our Vyper code and deploys the contract to our PyEVM network. + +The rest of the main function in this code shows us how to interact with the contract. We can store a number, retrieve it, and add a person to the contract. + +To run this code, we need to open our terminal, activate our virtual environment using these commands: + +```bash +uv venv +``` + +```bash +uv sync +``` + +```bash +source .venv/bin/activate +``` + +Then, we can run the main Python file using this command: + +```bash +python deploy_favorites.py +``` + +This will compile the contract, run our interactions, and print the results to the terminal. + +We won't be running Anvil in this section because TitanoBoa manages the deployment process for us. However, we will show you how to run Anvil in a separate section. + +Remember, AI tools like GitHub Copilot can be incredibly helpful for understanding code. If you're ever confused about a specific line of code, copy it, paste it into Copilot, and ask it to explain what it does. + +This section provides a solid foundation for working with TitanoBoa, and we'll continue to build on these skills in the next sections. diff --git a/courses/moccasin-101/3-boa-favs/10-add-person/+page.md b/courses/moccasin-101/3-boa-favs/10-add-person/+page.md new file mode 100644 index 000000000..3a2533aae --- /dev/null +++ b/courses/moccasin-101/3-boa-favs/10-add-person/+page.md @@ -0,0 +1,63 @@ +## Titanboa Add Person + +We know how to set the RPC URL, add an account, deploy a contract, interact with a contract, and then interact with the same contract later on. + +Assuming that address exists. Now, luckily blockchains don't just get deleted. So, when we interact with Ethereum or ZK Sync, or like a real network. Once an address is deployed, it's there forever. So, typically we won't have to worry about running into errors like that except for when we're testing and when we're loading stuff like this. + +Now let's come back over to deploy favorites. And, let's just do one more thing. So, we've been working with this store number a whole lot, right? This, if we go back to the favorites .vi, we've been working with this store function. Which is cool but, like, what about this add person? This is a little bit more complex. It takes a name and a favorite number, and the name is, like, a string array, like, what? How do How do we call this? How do we interact with this? Well let's go ahead, let's get rid of some of this store stuff here. Right? Let's not even worry about that. + +Now, instead, let's now do + +```bash +print "Storing a person." +``` + +It sounds a little weird, but, all right, sure. "Storing a person." And, we'll do + +```bash +favorites contract .add person +``` + +Let's call this add person function which takes a name and a favorite number. So let's give it a name as a string, we'll do + +```bash +"Alice" +``` + +and then a favorite number as + +```bash +25 +``` + +Now this is just a regular Python string, and Python strings are actually dynamic by default, whereas Vyper strings are not dynamic. So, we gave our string a maximum size of 100 characters. So if we tried to add, you know, 101 characters in here, this transaction would fail. Let's go ahead, let's test it out with just one person, and then we'll do we'll say + +```bash +person data equals favorites contract . get person "Alice". +``` + +We can do + +```bash +list of people at index 0. +``` + +Since Alice is the first person that we added, they should be at index 0. Then we can print it out, we can do + +```bash +print f"Person: {person data}." +``` + +So, now back on our terminal, we have to kick up anvil again because this script is working with anvil. Let's pop open a new terminal. And, let's go ahead, let's run this. We can either, you know, do + +```bash +source .env +``` + +we could kick up our .env, but I'm just going to do + +```bash +uv run Python deploy favorites .py +``` + +Right this, and we'll see transaction broadcasted. Contract deployed. Storing person that sends another transaction. And, then we have person 25 Alice returned from the blockchain, right? Because down here we have person person data, the person data is going to be 25 and Alice. Because we go back to the favorites .vi, we have a list of Person structs. Right? And a Person struct is made up of a favorite number, which is a number, and a name which is a string. So, we're given back the favorite number and the name exactly as Vyper have told us. Nice. diff --git a/courses/moccasin-101/3-boa-favs/11-workshop/+page.md b/courses/moccasin-101/3-boa-favs/11-workshop/+page.md new file mode 100644 index 000000000..ea106c9d3 --- /dev/null +++ b/courses/moccasin-101/3-boa-favs/11-workshop/+page.md @@ -0,0 +1,42 @@ +## Workshop: Your First Vyper Contract + +We'll practice what we've learned so far in this section of the course by building a contract that we can deploy to Anvil, call a function on, and then deploy it to tenderly. + +Let's first create our Vyper contract: + +```python +# I'm a comment! +pragma version 0.4.0 +@license MIT + +struct Person: + favorite_number: uint256 + name: String[100] + +my_favorite_number: uint256 + +# Static Array/List +list_of_numbers: public(uint256[5]) +list_of_people: public(Person[5]) +list_of_people_index: uint256 + +name_to_favorite_number: HashMap[String[100], uint256] + +@deploy +def __init__(): + self.my_favorite_number = 7 + +@external +def store(favorite_number: uint256): + # 0, 0, 0, 0, 0 + self.list_of_numbers[self.list_of_numbers_index] = favorite_number + self.list_of_numbers_index += 1 +``` + +This is what our contract currently looks like. We'll continue to build it out in this workshop. + +Before we get started, we want to make sure we have all the prerequisites and understand how to use the tools we are using. Don't just copy and paste the prompt into ChatGPT and have it give you an answer, we want to try to do this ourselves. Feel free to use AI to ask questions to help you understand the things we learned in this section. + +So pause the video, try to do this workshop yourself, and we'll see you in a little bit. + +Remember, repetition is the mother of skill, and we want to be skillful. So if you didn't do the workshop, go do it! If you did do the workshop, congratulations and welcome back. diff --git a/courses/moccasin-101/3-boa-favs/12-recap/+page.md b/courses/moccasin-101/3-boa-favs/12-recap/+page.md new file mode 100644 index 000000000..69d0af577 --- /dev/null +++ b/courses/moccasin-101/3-boa-favs/12-recap/+page.md @@ -0,0 +1,73 @@ +## Recap + +We learned a lot of things in this section. First, we learned how to deploy a smart contract using `Titanobo Boa`, and then how to interact with existing contracts. + +### Deploying with Titanobo Boa + +We can deploy a contract with a single line of code: + +```python +favorites_contract = boa.load("favorites.vy") +``` + +This will compile, deploy, and send a transaction to a fake PyEVM chain, which is a local chain that `Titanobo Boa` spins up. We will not be using local chains after this section. + +### Interacting with Existing Contracts + +To interact with an existing contract, we need the contract address and ABI. + +```python +MY_CONTRACT = "0xc7fE13acC3a54d7e9e704c793E8DB9F7634f80FC59" +# Address + +# ABI + +favorite_deployer = boa.load_partial("favorites.vy") +favorites_contract = favorite_deployer.at(MY_CONTRACT) +``` + +The `load_partial` method will return a deployer object, which has the ABI and `at` method. We pass the contract address to `at` to create a contract object. + +### Working with Environment Variables + +We also learned how to store environment variables in a `.env` file. + +```bash +cat .env +RPC_URL=http://127.0.0.1:8545 +ANVIL_KEY=0xacd974bec3917e35ba4d46f2384d7f944bac47b8ded5efcae784a07f1472f2ff +``` + +We used the `dotenv` library to load these variables into our script. + +### Code Snippets + +We also used some code snippets in the `deploy_favorites.py` file to set up the `RPC` and account. + +```python +import boa +from dotenv import load_dotenv +from boa.network import NetworkEnv, EthereumRPC +import os +from eth_account import Account + +load_dotenv() +def main(): + rpc = os.getenv("RPC_URL") + env = NetworkEnv(EthereumRPC(rpc)) + boa.set_env(env) + anvil_key = os.getenv("ANVIL_KEY") + my_account = Account.from_key(anvil_key) + boa.env.add_account(my_account, force_eoa=True) + favorites_contract = boa.load("favorites.vy") + print("Storing a person...") + favorites_contract.add_person("Alice", 25) + person_data = favorites_contract.list_of_people(0) + print(f"Person: {person_data}") + if __name__ == "__main__": + main() +``` + +### Moccasin + +This is the last section of the `Titanobo Boa` series, in the next section we'll move on to the `Moccasin` series. diff --git a/courses/moccasin-101/3-boa-favs/2-setup/+page.md b/courses/moccasin-101/3-boa-favs/2-setup/+page.md new file mode 100644 index 000000000..05ddcab5e --- /dev/null +++ b/courses/moccasin-101/3-boa-favs/2-setup/+page.md @@ -0,0 +1,77 @@ +## Setting Up a Boa Favorites Project + +We'll be starting with a Boa favorites project. + +First, we'll create a new folder using the terminal: + +```bash +mkdir boa-favorites-cu +``` + +Next, we will use the `cd` command to navigate to the folder: + +```bash +cd boa-favorites-cu +``` + +We'll create a new Python project using the `uv` command: + +```bash +uv init +``` + +We can see a new folder has been created with the following files: + +- `README.md` +- `pyproject.toml` +- `hello.py` +- `.python-version` +- `.gitignore` + +We'll then open our folder in VS Code using the following terminal command: + +```bash +code boa-favorites-cu +``` + +Next, we'll create a new `favorites.vy` file by right-clicking on the folder and selecting "New File." After creating it, we'll paste the following code: + +```python +# I'm a comment! +# pragma version ^0.4.0 +# @license MIT +struct Person: + favorite_number: uint256 + name: String[100] +my_favorite_number: uint256 +# Static Array/List +list_of_numbers: public(uint256[5]) +list_of_people: public(Person[5]) +list_of_people_index: uint256 +name_to_favorite_number: HashMap(String[100], uint256) +@deploy +def __init__(self): + self.my_favorite_number = 7 + +@external +def store(favorite_number: uint256): + self.my_favorite_number = favorite_number +@external +@view +def retrieve() -> uint256: + return self.my_favorite_number + +@external +def add_person(name: String[100], favorite_number: uint256): + new_person: Person = Person(favorite_number = favorite_number, name = name) + self.list_of_people[self.list_of_people_index] = new_person + self.list_of_numbers[self.list_of_people_index] = favorite_number + self.list_of_people_index += 1 + self.name_to_favorite_number[name] = favorite_number +``` + +Finally, we can compile our contract using the following command: + +```bash +Vyper favorites.vy +``` diff --git a/courses/moccasin-101/3-boa-favs/3-pyevm/+page.md b/courses/moccasin-101/3-boa-favs/3-pyevm/+page.md new file mode 100644 index 000000000..19a3fedf1 --- /dev/null +++ b/courses/moccasin-101/3-boa-favs/3-pyevm/+page.md @@ -0,0 +1,74 @@ +## Deploying to pyevm With TitanoBoa + +We've created a virtual environment and installed TitanoBoa. Now we can start to use TitanoBoa to work with our Vyper contracts! + +We'll start with creating a new Python file called `deploy_favorites_pyevm.py`. We can then start to write some code in the file. + +```python +if __name__ == "__main__": + main() +``` + +We'll also define a main function. We'll start out with a simple print statement for now. + +```python +def main(): + print("Let's read in the Vyper code and deploy it!") +``` + +Our next step is to actually load our contract from the `favorites.vy` file. We can do this by importing the `boa` library and using the `boa.load()` function. + +```python +def main(): + print("Let's read in the Vyper code and deploy it!") + favorites_contract = boa.load("favorites.vy") +``` + +We can see what type of object our `favorites_contract` is using the `type()` function. + +```python +def main(): + print("Let's read in the Vyper code and deploy it!") + favorites_contract = boa.load("favorites.vy") + print(type(favorites_contract)) +``` + +We can see from our output that our `favorites_contract` is of the type `boa.contracts.vyper.vyper_contract.VyperContract`. + +```bash +python deploy_favorites_pyevm.py +``` + +Let's read in the Vyper code and deploy it! + + +We can use type hints to help us understand what kind of objects our variables are. For example, we can type hint our `favorites_contract` variable to be a `VyperContract`. + +```python +def main(): + print("Let's read in the Vyper code and deploy it!") + favorites_contract: VyperContract = boa.load("favorites.vy") + print(type(favorites_contract)) +``` + +To use the `VyperContract` type hint, we'll need to import it from the `boa.contracts.vyper.vyper_contract` module. + +```python +from boa.contracts.vyper.vyper_contract import VyperContract + +def main(): + print("Let's read in the Vyper code and deploy it!") + favorites_contract: VyperContract = boa.load("favorites.vy") + print(type(favorites_contract)) +``` + +We can then run this code again. + +```bash +python deploy_favorites_pyevm.py +``` + +Let's read in the Vyper code and deploy it! + + +Type hints are not required in Python, but we will be using them more heavily later in the course. For now, we can ignore them. diff --git a/courses/moccasin-101/3-boa-favs/4-vypercontract/+page.md b/courses/moccasin-101/3-boa-favs/4-vypercontract/+page.md new file mode 100644 index 000000000..416f93700 --- /dev/null +++ b/courses/moccasin-101/3-boa-favs/4-vypercontract/+page.md @@ -0,0 +1,41 @@ +Let's learn how to interact with our smart contract. We have `store` in here which updates the favorite number. Our favorite number is going to be private. We can call `retrieve` to view it. + +So, what we could do is, we could do a little print line here. Let's do: + +```python +print("Starting favorite number = ") +``` + +We can then print to a little F string here: + +```python +print(f"The starting favorite number is: {starting_favorite_number}") +``` + +Now let me clear the terminal and run this again. The starting favorite number is 7. Does that make sense? Let's go to `favorites.py`. Ah, so I have currently `self.my_favorite_number = 7`. Whatever you have for yourself that favorite number in the deploy/the init/the constructor, that's where your favorite number is going to start off, so that looks correct. + +Now, we do: + +```python +favorites_contract.store(5) +``` + +So if we change our favorite number to 5 we can then copy this line: + +```python +starting_favorite_number = favorites_contract.retrieve() +``` + +Paste it here, and say this is now the: + +```python +ending_favorite_number = favorites_contract.retrieve() +``` + +So ending favorite number equals `favorites_contract.retrieve`. So, we're going to update it and then we're going to get that number that we updated it with. We can then run `print`: + +```python +print(f"The ending favorite number is: {ending_favorite_number}") +``` + +Now we run this script now. This sends a transaction, right? Because the `store` function is not a view function, right? `retrieve` is a view function, so Boa goes, "Oh okay, I don't need to send a transaction for this. I'm just going to call it. I'm just going to read it myself. I'm just reading data. I'm not changing any state of the blockchain." Whereas the `store` function is actually changing state, right? The `my_favorite_number` storage or state variable is going to be updated to whatever you told it to change it to. So now, if I pull up the terminal let's clear it out, I'm going to hit up twice to rerun this. We now see: "Let's read the Vyper code and deploy it! The starting favorite number is 7. The ending favorite number is 5." Now, we still didn't see any transaction information in here and that's because we're using the `PyEVM`. You'll understand what I mean in a minute. But, boom, we were able to update our starting favorite number from 7 to 5 in this tiny little script. We didn't have to manually create the transaction or anything like that. Boa under the hood, when it calls `load`, it does that whole build transaction thing, constructor thing, sign thing, all of that under the hood for us. Additionally, when we call `store` it does the same thing. It does all the sign transaction, build transaction, all that stuff under the hood for us. diff --git a/courses/moccasin-101/3-boa-favs/5-debug/+page.md b/courses/moccasin-101/3-boa-favs/5-debug/+page.md new file mode 100644 index 000000000..33611918b --- /dev/null +++ b/courses/moccasin-101/3-boa-favs/5-debug/+page.md @@ -0,0 +1,27 @@ +## Vyper Debug Statements + +We've been using the `boa` library and the `boa` library is great for us, but now let's talk about the `print` function. Let's say that we want to be able to see the `msg.sender` when we call the `store` function. So, we can actually put a print statement right in our Vyper code! + +Now, we need to be very careful. This is specifically for debugging, and we don't want to have these `print` statements in real smart contracts. We'll add this line to the `store` function, `print(msg.sender)`. + +```python +def store(favorite_number: uint256): + print(msg.sender) + self.my_favorite_number = favorite_number +``` + +So, what we'll do is we'll run our code. First, we'll clear out our terminal: + +```bash +clear +``` + +And then we'll run our Python script: + +```bash +python deploy_favorites_pyevm.py +``` + +We get a warning telling us `print` should only be used for debugging. This is because we should not deploy print statements to the blockchain. + +But, even though it's a waste of gas and it might result in some weird oddities, we can see that we get the sender address. diff --git a/courses/moccasin-101/3-boa-favs/6-pyevm_/+page.md b/courses/moccasin-101/3-boa-favs/6-pyevm_/+page.md new file mode 100644 index 000000000..cc7d5186b --- /dev/null +++ b/courses/moccasin-101/3-boa-favs/6-pyevm_/+page.md @@ -0,0 +1,91 @@ +We will learn about how a simple Python script interacts with a blockchain. + +We will use a library called boa to help us with this process. + +Let's create a Python script file called `deploy_favorites_pyevm.py`: + +```python +import boa + +def main(): + print("Let's read in the Vyper code and deploy it!") + favorites_contract = boa.load("favorites.vy") + starting_favorite_number = favorites_contract.retrieve() + print(f"The starting favorite number is: {starting_favorite_number}") + favorites_contract.store(5) # This sends a transaction! + ending_favorite_number = favorites_contract.retrieve() + print(f"The ending favorite number is: {ending_favorite_number}") + +``` + +Now, we can run this Python script: + +```bash +python deploy_favorites_pyevm.py +``` + +This will generate the output in the terminal. Let's break down what is happening here step by step: + +First, we will import the `boa` library: +```python +import boa +``` + +We will then define a `main` function: + +```python +def main(): +``` + +Next, we will print a message stating what we are doing: + +```python + print("Let's read in the Vyper code and deploy it!") +``` + +We'll then load a Vyper smart contract file called `favorites.vy` and assign it to a variable called `favorites_contract`: + +```python + favorites_contract = boa.load("favorites.vy") +``` + +We'll call the `retrieve()` function from the `favorites_contract` and store the output in a variable called `starting_favorite_number`: + +```python + starting_favorite_number = favorites_contract.retrieve() +``` + +We'll print the `starting_favorite_number` to the terminal: + +```python + print(f"The starting favorite number is: {starting_favorite_number}") +``` + +Then, we will store a value of 5 using the `store()` function from the `favorites_contract`: + +```python + favorites_contract.store(5) # This sends a transaction! +``` + +After the transaction, we will call the `retrieve()` function again and store the result in a variable called `ending_favorite_number`: + +```python + ending_favorite_number = favorites_contract.retrieve() +``` + +We'll then print the `ending_favorite_number` to the terminal: + +```python + print(f"The ending favorite number is: {ending_favorite_number}") +``` + +We will then close the `main` function: +```python +``` + +Finally, we will run this Python script: +```bash +python deploy_favorites_pyevm.py +``` + +This script will interact with the blockchain. Now if we run this again, we still see the starting favorite number is seven, the ending favorite number is five. So, uh but wait, what if I run it again? Well, the starting favorite number is still seven and the ending favorite why isn't the starting favorite number now five? Well, so here's what happens when we actually run this script. When we call Python deploy favorites pyevm.py The first thing Titano Boa does is it says, hey, let's spin up this fake pyevm chain. It's smart enough to know, oh, okay. We didn't give it an RPC, we don't have any accounts, let's just we'll make a fake chain like it's fine. Then, we do this favorites_contract equals boa.load favorites.vy. And in this pyevm chain in this kind of fake chain It'll deploy the contract with whatever the variables are in here. Right? So we have my favorite number equals seven, so it'll go great. My favorite number is seven in here. Or favorite number is seven. Then we go, hey, favorites_contract.store(5). And it goes great, no problem. And it updates my favorite number to five. And then the script ends. And Titano Boa goes, okay, great. Let's delete the chain. Everything's deleted. So that means the next time we run this script, we will do the exact same process. We'll spin up a fake chain. We'll deploy the contract, we'll store, and then we'll delete it. So this is why every time we run this script The favorite number starts with seven. Now this is because every single time we're running this boa.load In the future, you'll probably not want to deploy the same contract over and over and over again to a real chain. You'd probably just want to interact with whatever contract was deployed most recently. And we'll learn some tools to interact with and get the most recent deployed contract. But again, for now, since we're just testing, this is what the process looks like. Boa spins up a new chain. We deploy a contract to it. We update the storage, or the state value favorite number, and then we delete the whole chain. Okay. Great. diff --git a/courses/moccasin-101/3-boa-favs/7-anvil/+page.md b/courses/moccasin-101/3-boa-favs/7-anvil/+page.md new file mode 100644 index 000000000..2db14bb45 --- /dev/null +++ b/courses/moccasin-101/3-boa-favs/7-anvil/+page.md @@ -0,0 +1,124 @@ +## TitanoBoa with Anvil + +In this lesson, we are going to learn how to connect to a real network using TitanoBoa. We're going to use Anvil as our local blockchain to interact with. + +First, we will create a file called `deploy_favorites.py`. This file will contain our code to connect to a real network using TitanoBoa. + +```python +if __name__ == "__main__": + main() +def main(): + print("Hi from deploy favorites") +``` + +We will also create a new file called `.env` in the same directory as our `deploy_favorites.py` file. This file will store our RPC URL and our Anvil key. + +```bash +touch .env +``` + +We need to install `python-dotenv` to automatically pull in our `.env` file. + +```bash +pip install python-dotenv +``` + +Let's copy the `python-dotenv` package into our .env file. + +```python +from dotenv import load_dotenv +``` + +We will also need to import TitanoBoa, along with the `NetworkEnv` and `EthereumRPC` objects. + +```python +import boa +from boa.network import NetworkEnv, EthereumRPC +``` + +Next, let's add our RPC URL to our `.env` file. + +```bash +RPC_URL="http://127.0.0.1:8545" +``` + +We'll then need to import the `os` library. + +```python +import os +``` + +Now, let's pull in our .env file into our Python script. + +```python +load_dotenv() +``` + +Now, in our main function, we need to set up our environment. We do this by creating a new `NetworkEnv` object and using the `EthereumRPC` object to wrap our RPC URL. + +```python +def main(): + rpc = os.getenv("RPC_URL") + env = NetworkEnv(EthereumRPC(rpc)) +``` + +Next, we'll set up our environment. + +```python +boa.set_env(env) +``` + +We'll then need to create a new account, and we'll store it in a variable named `my_account`. + +```python +from eth_account import Account +my_account = Account.from_key() +``` + +We will need to obtain our Anvil key from the terminal, and we'll copy it into the `.env` file. + +```bash +ANVIL_KEY= +``` + +Once we have our Anvil key, we'll add it to our Python script. + +```python +anvil_key = os.getenv("ANVIL_KEY") +``` + +We can now use our Anvil key to create our account. + +```python +my_account = Account.from_key(anvil_key) +``` + +Now, we need to tell TitanoBoa to use our account. We do this by using the `boa.env.add_account` function. + +```python +boa.env.add_account(my_account, force_eoa=True) +``` + +Now that we have our environment and our account set up, we can deploy our contract. Let's copy our code from the previous lesson and add it to our `deploy_favorites.py` file. + +```python +favorites_contract = boa.load("favorites.vy") +``` + +We can now run our script to deploy our contract. + +```bash +python deploy_favorites.py +``` + +TitanoBoa will automatically use our account to deploy our contract to the Anvil network. + +If we go to Anvil, we should see that our contract has been deployed. + +```bash +Contract deployed at +``` + +Our contract will be able to interact with the Anvil network. If you'd like to do some additional interactions with this contract, you can modify the code in the `deploy_favorites.py` file. + +This is a basic introduction to connecting to a real network with TitanoBoa. In future lessons, we'll explore additional concepts and how we can improve our code. \ No newline at end of file diff --git a/courses/moccasin-101/3-boa-favs/8-interacting-anvil/+page.md b/courses/moccasin-101/3-boa-favs/8-interacting-anvil/+page.md new file mode 100644 index 000000000..2d1d658b0 --- /dev/null +++ b/courses/moccasin-101/3-boa-favs/8-interacting-anvil/+page.md @@ -0,0 +1,38 @@ +## Interacting with Anvil Contract with Titanoboa + +We will explore how to interact with an Anvil contract with Titanoboa. + +Let's take a look at our code. We've learned how to deploy this contract and now we will interact with it. To do so we will start with a print statement: + +```python +print("Storing number...") +``` + +We will call a function called "store" from our favorites contract. This will set our favorite number to 5. + +```python +favorites_contract.store(5) +``` + +Now we will print the ending favorite number. + +```python +ending_favorite_number = favorites_contract.retrieve() +print(f"Ending favorite number is: {ending_favorite_number}") +``` + +Finally, we will run the code in our terminal: + +```bash +python deploy_favorites.py +``` + +We will see the output: + +- Transaction created +- Contract deployed +- Starting favorite number is 7 +- Storing number... +- Ending favorite number is 5 + +We deployed a new contract and stored a number to it. Now we can see the effects of storing a number and retrieving it through the use of our smart contract and Titanoboa! diff --git a/courses/moccasin-101/3-boa-favs/9-interact-prevs/+page.md b/courses/moccasin-101/3-boa-favs/9-interact-prevs/+page.md new file mode 100644 index 000000000..186f5ac80 --- /dev/null +++ b/courses/moccasin-101/3-boa-favs/9-interact-prevs/+page.md @@ -0,0 +1,60 @@ +## Interacting with a Previously Deployed Contract + +We've deployed a contract, but what if we want to interact with it? We'll need to use TitanoBoa to connect to the anvil chain, and then we can use its `at` function. + +To start, we'll grab our contract address, and create a new Python file called `interact.py`. We'll set a variable to hold the address, and then we'll need to import TitanoBoa, and import `Account` from `eth_account`: + +```python +MY_CONTRACT = "0xC77E03AcC45A467e9e704c703eED087f6347B0FC9" + +``` + +```python +from boa.network import EthereumRPC, NetworkEnv +import boa +from eth_account import Account + +``` + +Next, we'll connect to the anvil chain. We'll use the same code as we did to deploy the contract. This includes importing the `os` module, setting up the RPC, ENV, and adding the private key to Boa: + +```python +import os +load_dotenv() + +def main(): + rpc = os.getenv("RPC_URL") + env = NetworkEnv(EthereumRPC(rpc)) + boa.set_env(env) + anvil_key = os.getenv("ANVIL_KEY") + my_account = Account.from_key(anvil_key) + boa.env.add_account(my_account, force_eoa=True) + +``` + +We can then use `boa.load_partial` to interact with our deployed contract. We'll use the address we stored in `MY_CONTRACT`, and use the `at` function from TitanoBoa to get a contract object: + +```python +favorite_deployer = boa.load_partial("favorites.vy") +favorites_contract = favorite_deployer.at(MY_CONTRACT) + +``` + +Now we can use our contract object to retrieve the current favorite number: + +```python +favorite_number = favorites_contract.retrieve() +print(f"Favorite number is {favorite_number}") + +``` + +If we want to update the favorite number, we can use the `store` function: + +```python +favorites_contract.store(22) +favorite_number_updated = favorites_contract.retrieve() +print(f"Favorite number is now {favorite_number_updated}") + +``` + +In the example above, we've changed the favorite number from 5 to 22. If we were to kill the anvil chain, and then try to run this code again, we'd receive an error. This is because anvil doesn't know about our locally running chain. We'd need to restart anvil and redeploy the contract in order to interact with it. diff --git a/courses/moccasin-101/4-mox-favs/1-intro/+page.md b/courses/moccasin-101/4-mox-favs/1-intro/+page.md new file mode 100644 index 000000000..c76386569 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/1-intro/+page.md @@ -0,0 +1,79 @@ +## Section: Moccasin Favorites + +We are now on what I think is going to be one of my favorite sections of the whole course because we will be introducing you to the **Moccasin** tool. + +Moccasin is a smart contract development framework that will help us: + +* Deploy +* Test +* Maintain +* Write professional code + +The **Moccasin** tool is going to be the main tool we work with throughout the rest of this course, as well as **Advanced Moccasin**. We will have a brief stop to learn about HTML/JavaScript. + +Let's take a look at what we will be learning in this section and why we are taking this final step to learn and grow as smart contract developers. + +We will be working with the **favorites.vy** contract. + +This contract is a simple contract, but it will help us learn how to use **Moccasin**. + +Now, in the last couple of sections, we ran into a few problems when we were trying to deploy and test our smart contracts: + +* We had to write a separate Python script for every network that we wanted to work with. +* We did not have any tests, so we had no way to verify that our contracts were working correctly. + +**Moccasin** makes our lives easier by providing us with a way to: + +* Deploy our contracts to any network with just a few lines of code +* Write tests directly in **Moccasin** +* Automatically verify our contracts on a blockchain explorer + +Let's see how **Moccasin** works in practice. + +We will first open our terminal and run the following command to deploy our contract to the **pyevm** network: + +```bash +mox run deploy +``` + +Now, let's look at the code for the **deploy.py** script: + +```python +from src import favorites +from moccasin.boa.tools import VyperContract +from moccasin.config import get_active_network + +def deploy_favorites() -> VyperContract: + active_network = get_active_network() + print("Currently on network:", active_network.name) + favorites_contract: VyperContract = favorites.deploy() + print("Starting favorite number:", favorites_contract.retrieve()) + favorites_contract.store(77) + print("Ending favorite number:", favorites_contract.retrieve()) + + if active_network.has_explorer(): + print("Verifying contract on explorer...") + result = active_network.mocassin.verify(favorites_contract) + result.wait_for_verification() + print(f"https://{active_network.explorer.explorer_uri}/address/{favorites_contract.address}") + + return favorites_contract + +def moccasin_main() -> VyperContract: + return deploy_favorites() +``` +We can also change networks by using the **--network** flag. For example, to deploy our contract to the **Anvil** network, we would run the following command: + +```bash +mox run deploy --network anvil +``` + +In the future, we can deploy to other networks like **zkSync** or **Ethereum**. + +**Moccasin** is a powerful tool that will help us to write professional smart contracts. + +We will also learn how to use the **pyproject.toml** file to configure our projects. + +We will also learn how to add different network information to the **mocassin.toml** file. + +Let's get started learning **Moccasin**. diff --git a/courses/moccasin-101/4-mox-favs/10-account-cli/+page.md b/courses/moccasin-101/4-mox-favs/10-account-cli/+page.md new file mode 100644 index 000000000..90d21aa80 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/10-account-cli/+page.md @@ -0,0 +1,39 @@ +## Account Command Line Options + +We are incredibly lazy and we like to work really hard to be really lazy. So, even just adding the `--account anvil` command is very tiring for us. We don't want to have to do that. + +So what we can do, let me get rid of this. We are using a fun little `Control+W` to quickly delete it here. This is in the `networks.anvil` section of our TOML file. We can also add a default account. + +So we can say `default_account_name = "anvil1"`. In this way, now all we have to do now in our terminal is hit enter and it's going to automatically use our anvil1 account. So if we hit enter now, it says "enter your password for keystone anvil1" and we can type our password in and boom, it sends it through. + +So this way, when we use `networks.anvil`, we automatically work with our `anvil1` key. Now, like I just said, we like to work incredibly hard to be really lazy. So there is one more thing that we can do to make this command even easier to run. Right? We did `mox run deploy --network anvil`, hit Enter. We are now prompted for our password for our `anvil1` command here. + +Well, we can do one more thing. And if we go to the, we go copy and paste this into our search bar and scroll down, in here we can see there is this `unsafe_password_file` where we can add a password file that has our password for a different account. Now, it's very specifically named `unsafe_password_file` because you really don't want people getting a hold of your password file. So, you want to put it in a location outside of your project. + +So we can actually just save a password in a file and then edit it again. Where is that? That keyword here? That `unsafe_password_file`. And then, save it as a parameter in our `mocassin.toml` file. + +Now, a lot of people are going to be tempted just to, "Oh, okay cool," you know, `password.txt` paste it in here. And then you know, print it in here and then set it here. I'm going to recommend not doing that because you might accidentally go to share your project with somebody and you are like, "Oh, what's a daisy? Your password is still in your project." You know how we do all these GitHub repos, right? If you go to share your project which you should share your project, but you forget to take the password file out. uh, well, that would be pretty bad. So what might be a nicer thing to do is if you type `cd` into your terminal here. And if you do, um, `cd .mocassin`, you'll notice that you have a new file, a new folder called `.mocassin` in your home directory. Mine is obviously `users/patrick`, yours will be `users/` wherever you are. If you type `ls` in this folder, you'll see a couple of different folders in here. You'll see you have this `keystores` file and you might have some other folders and files in here as well. + +But, what might be a better thing for you to do is in this `.mocassin`, make a new directory called `unsafe_passwords`. + +```bash +mkdir unsafe_passwords +``` + +`cd` into this `unsafe_passwords` file and add this password into this `unsafe_passwords` folder. So, to add some text in here, what you can type is you can enter some bash text editor like `vim`, `vi` or something like that. But, I'm just going to make your life a little bit easier. We are just going to use an `echo` command here. This is a bash, this is a Linux command. If you are not familiar with how it works, this is where AIs are very helpful. Uh, if you run this command, you should definitely ask an AI what it does and how it works. But, we can run `echo` password and this little caret command here means to like dump it in. + +```bash +echo "password" > anvil1 +``` + +Now, if we type `ls`, we have this anvil1 file in our `unsafe_passwords` folder. And if I type `cat anvil1`. `cat` is a command to help like print out whatever is in a file. I can see my password is in anvil1. So again, this, so again, this is okay for now because we are because anvil1 is a, is a dummy key right? Like, it's just comes default with anvil. But, ideally, you are not saving your passwords for actual private keys like so, but we are just doing this to make life a little bit easier for our dummy account. + +So, now, what I can do. Let me `cd` back to my directory. I'm going to `cd` to `moxcyfrindraft` and then `cd` to `moxfavoritescyfrindraft`. Okay, great. I'm now back in this directory. I'm going to hide my terminal here. Now, I can say `unsafe_password_file` = this little tilde stands for the home directory. You could also do `.` home or dollar sign home, whatever you want to do. + +```bash +unsafe_password_file = "~/.mocassin/unsafe_passwords/anvil1" +``` + +So now, I have a `default_account_name` and I have an `unsafe_password_file` which is going to point to this anvil1. So now let me pull up my terminal again. I'm back in my `moxfavoritescyfrindraft`. I can now do `mox run deploy --network anvil`. I will hit Enter and I don't get prompted. It automatically decrypts my anvil account using the password we gave it here. + +So very exciting here. Just by learning this you are learning much, much safer ways to work with your private keys. You just learned how to encrypt a private key using moccasin into a keystore file, then decrypt it by being prompted or using an unsafe password file. You should be incredibly excited and incredibly proud of yourself, because like I said, the year 2024, the number one attack vector was leaked private keys. So you are going to learn how to encrypt your keys and work with keys in a much safer way than the rest of the industry does. As of right now, and this is both very exciting and very sad to hear, as of right now, you are more knowledgeable than half a billion dollars worth of money in the blockchain industry because you know how to encrypt your private keys and not share them in plain text. You should be very exciting. diff --git a/courses/moccasin-101/4-mox-favs/11-mox-testing/+page.md b/courses/moccasin-101/4-mox-favs/11-mox-testing/+page.md new file mode 100644 index 000000000..df7e97914 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/11-mox-testing/+page.md @@ -0,0 +1,48 @@ +## Introduction to Mocassin Testing + +We have our contract. What we haven't done is test it. Now, testing is probably one of the most important things any smart contract developer needs to get good at. + +We're going to learn how to write a test for this. We'll be using pytest, which is a testing framework built into Mocassin. It's a very well-known and widely used framework, so if you're already familiar with pytest, you're already familiar with the basics of Mocassin testing. + +First, we need to create a new file called `test_favorites.py`. Inside the file, we can create our first test. + +```python +def test_starting_values(): + pass +``` + +We're just going to use the `pass` keyword for now to get this set up. + +In pytest, we look for a `test` keyword at the beginning of function names, and the functions need to be inside a folder called `tests`. So you must have a folder named `tests` and each one of your tests must start with `test` to be recognized. + +We'll create a test that checks to make sure our contract starts with the correct initial value, which is 7. + +To do this, we'll import our contract using the following code: + +```python +from src import favorites +``` + +Then, we'll deploy the contract: + +```python +favorites_contract = favorites.deploy() +``` + +Now, we'll assert that the retrieved value is 7: + +```python +assert favorites_contract.retrieve() == 7 +``` + +Now, we can test this code by opening our terminal and running the following command: + +```bash +mox test +``` + +This will run all of our tests. In this case, we have only one test, so you'll see a green dot in the terminal to indicate that the test has passed. + +Let's create another test. We'll copy and paste our first test and rename it to `test_starting_values_two`. + +Now, we can run the tests again. This time, we'll see two green dots in the terminal, indicating that both tests have passed. diff --git a/courses/moccasin-101/4-mox-favs/12-testing-deploy-script/+page.md b/courses/moccasin-101/4-mox-favs/12-testing-deploy-script/+page.md new file mode 100644 index 000000000..1a902d310 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/12-testing-deploy-script/+page.md @@ -0,0 +1,46 @@ +## Testing Deploy Scripts + +We're going to continue testing smart contracts. We'll focus on a common issue that arises: testing a deploy script. + +Currently, our tests only verify the starting values. However, we haven't included tests for the changes our deploy script makes. + +Let's fix this by adding tests for the changes our deploy script makes. + +We are going to make use of the import system in Python, which can be done by importing modules, which are essentially folders or files that contain code we can access. + +The script folder in our project contains an `__init__.py` file, which informs Python that the folder is a module and allows us to import functions from it. + +We'll modify the import statement in our test file: + +```python +from script.deploy import deploy_favorites +``` + +We can then use our function in our tests to get the contract: + +```python +def test_starting_values(): + favorites_contract = deploy_favorites() + assert favorites_contract.retrieve() == 77 +``` + +Now, our tests will verify the starting values of our contracts. + +Let's also add a test to verify that our deploy script can change values. + +```python +def test_can_change_values(): + favorites_contract = deploy_favorites() + favorites_contract.store(42) + assert favorites_contract.retrieve() == 42 +``` + +By using the import system and explicitly referencing our deploy script, we can effectively test the changes made to our contracts during deployment. + +Now, we'll run our tests: + +```bash +mox test +``` + +We can see that our tests pass. diff --git a/courses/moccasin-101/4-mox-favs/13-aaa/+page.md b/courses/moccasin-101/4-mox-favs/13-aaa/+page.md new file mode 100644 index 000000000..1ad32ccd5 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/13-aaa/+page.md @@ -0,0 +1,61 @@ +## Testing a Vyper Contract with Mocks + +In this lesson, we'll be working on a Vyper contract. To test this contract, we use Mocks, a Python library that lets us create and interact with dummy objects, allowing us to isolate and test specific parts of our code. + +We'll create tests using the Arrange, Act, Assert (AAA) framework. This approach helps structure our tests for readability and clarity. + +**Arrange** involves setting up everything necessary for the test. For example, we'll deploy a mock version of our contract. +**Act** will be the action we are testing - for example, calling a specific function in our contract. +**Assert** will be the final step where we make sure that the action performed in the Act section delivers the expected outcome. + +The following is an example of the code we will be working with: + +```python +def test_can_change_values(): + # Arrange + favorites_contract = deploy_favorites() + + # Act + favorites_contract.store(42) + + # Assert + assert favorites_contract.retrieve() == 42 +``` + +We are testing a function called 'store' in this example. + +First, we **arrange** by deploying our contract using 'deploy_favorites()'. +Then, we **act** by calling 'store(42)' on our contract. +Finally, we **assert** that 'retrieve()' on our contract returns the expected value (42). + +We'll now add another test, this time we'll make sure we can add people to our contract. We can use the existing contract structure, but this time we'll need to test the 'add_person' functionality: + +```python +def test_can_add_people(): + # Arrange + new_person = "Becca" + favorite_number = 16 + favorites_contract = deploy_favorites() + + # Act + favorites_contract.add_person(new_person, favorite_number) + + # Assert + assert favorites_contract.list_of_people(0) == (favorite_number, new_person) +``` + +Let's run the tests we've created. + +```bash +mox test +``` + +This command will run all of our tests, and it will output the results. + +We can also run just one test using the **-k** flag: + +```bash +mox test -k test_can_add_people +``` + +This command will run only the 'test_can_add_people' test, making it easier to troubleshoot individual tests if needed. diff --git a/courses/moccasin-101/4-mox-favs/14-fixtures/+page.md b/courses/moccasin-101/4-mox-favs/14-fixtures/+page.md new file mode 100644 index 000000000..0ad8d465e --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/14-fixtures/+page.md @@ -0,0 +1,104 @@ +## Pytest Fixtures + +We're going to cover the concept of pytest fixtures. + +In our `test_favorites.py`, we have repeated a line of code in every test: + +```python +favorites_contract = deploy_favorites() +``` + +This line deploys our favorites contract, which could take a long time if we had a thousand tests. + +To make our tests more efficient, we can use a concept called a **fixture**. + +Let's start by importing pytest: + +```python +import pytest +``` + +We can then create a fixture function like this: + +```python +@pytest.fixture +def favorites_contract(): + return deploy_favorites() +``` + +We can now pass the fixture as a parameter to our test functions: + +```python +def test_starting_values(favorites_contract): + assert favorites_contract.retrieve() == 77 +``` + +Note, we no longer need to call `deploy_favorites()` in the body of the test function. + +Let's repeat this process for the other test functions: + +```python +def test_can_change_values(favorites_contract): + # Act + favorites_contract.store(42) + + # Assert + assert favorites_contract.retrieve() == 42 + +def test_can_add_people(favorites_contract): + # Arrange + new_person = "Becca" + favorite_number = 16 + + # Act + favorites_contract.add_person(new_person, favorite_number) + + # Assert + assert favorites_contract.list_of_people()[0] == (favorite_number, new_person) +``` + +Now, when we run `mox test`, our tests will run much faster because `deploy_favorites()` is only called once. + +Pytest fixtures also have a `scope` parameter, which controls the number of times the fixture is executed. The default scope is `function`. + +The `scope` parameter can be set to: + +* `function` (default): The fixture is executed once per test function. +* `session`: The fixture is executed once per test session. + +Let's set our fixture to a session scope: + +```python +@pytest.fixture(scope="session") +def favorites_contract(): + favorites_contract = deploy_favorites() + return favorites_contract +``` + +This will deploy the contract only once for the entire test session. + +If our `deploy_favorites()` function took an hour to run, we would have to wait an hour between every test. But, now that we have a session scope, our tests run much more efficiently. + +We can also use a `time.sleep` function to slow down a test: +```python +import time + +def deploy_favorites(): + favorites_contract = favorites.deploy() + starting_number = favorites_contract.retrieve() + print(f"Starting number is: {starting_number}") + favorites_contract.store(77) + ending_number = favorites_contract.retrieve() + print(f"Ending number is: {ending_number}") + time.sleep(5) + return favorites_contract +``` + +If we run `mox test` now, we'll see that the tests are much slower: +```bash +mox test +``` + +We can see that it takes 15.05 seconds to run all three tests because we're waiting 5 seconds between each test. + +Fixtures are a powerful way to make our tests more efficient and easier to write. diff --git a/courses/moccasin-101/4-mox-favs/15-conftest/+page.md b/courses/moccasin-101/4-mox-favs/15-conftest/+page.md new file mode 100644 index 000000000..95c15c1ec --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/15-conftest/+page.md @@ -0,0 +1,32 @@ +In this lesson, we're going to take a look at a different way to write tests. Typically, we code our contract, like we did in the previous lesson, and then code our tests. We also have our fixtures at the top of the file. In this lesson, we're going to do things a little differently. + +We're going to create a new file, called `conftest.py`, in our `tests` folder. + +```python +import pytest +from script.deploy import deploy_favorites + +@pytest.fixture(scope="session") +def favorites_contract(): + favorites_contract = deploy_favorites() + return favorites_contract +``` + +Now, we need to import `pytest` in the `conftest.py` file, as well as import the `deploy_favorites` function. We'll also write our fixture, like we did in the previous lesson. + +Next, let's go into our `test_favorites.py` file. + +```python +from script.deploy import deploy_favorites +import pytest +``` + +We can now delete the fixture from `test_favorites.py`, since we now have it in the `conftest.py` file, and we can also delete the `import pytest` line. + +We can now run `mox test` to verify that everything passes. + +```bash +mox test +``` + +This method is just an alternative way to write and organize your tests. diff --git a/courses/moccasin-101/4-mox-favs/16-type-hints/+page.md b/courses/moccasin-101/4-mox-favs/16-type-hints/+page.md new file mode 100644 index 000000000..fac5684c3 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/16-type-hints/+page.md @@ -0,0 +1,29 @@ +## Type Hints for Python in Moccasin + +We are continuing to shape up our project. We have a few files that we're not entirely sure what they do yet, but we'll learn about them in later sections. We do have a `deploy.py` file that looks very good. We are able to deploy a contract. It doesn't matter what chain it's on because we can use the `--network` flag to easily switch. + +```bash +uv deploy +``` + +Our Vyper code is what's known as statically typed, or strongly typed, because every single variable in Vyper has to have a type associated with it. This is incredibly important for smart contracts, because smart contracts are very specific. If something goes wrong or unexpected in your smart contract, you could lose a lot of money. Our Python code, by contrast, doesn't have to have types. + +If you went through the Python Crash Course, you know that we did talk about type hinting in Python. Python does have a type hinting system. We're going to go ahead and say that we should do the same thing. We should add type hints to our Python. However, this is 100% optional. So, this one here is 100% valid Python, but we could also add type hints. The reason that I like type hints in Python is because this `favorites_contract` object, we might not know what functions are associated with it, or, you know, maybe we'd run like `favorites_contract` equals 7 and then I'd try to do `favorites_contract.store`, you know, 42 or 40 and this no longer works because `favorites_contract` is now a number and it's no longer a contract object. So, I like adding type hints. And additionally, type hints make kind of debugging and working through your code a lot easier sometimes. So, this will be optional, but I am going to go ahead and take this time to add some type hints to this. So let's go ahead and get started. So, this `favorites_contract` right here, `from src import favorites`, we know that this `favorites` object is of type Vyper deployer. And any time we import some contract like this, we know that this is going to be a Vyper deployer. When we call `VyperDeployer.deploy` it's going to return a type Vyper contract or, and this is how you do `or` to do different types, or ZK sync contract. But for us, we're just going to do a Vyper contract. Now you'll get this little yellow squiggly line because Vyper, because our Python is going, hey, uh what is this Vyper contract you're talking about, I don't know what this is. + +Now we can get this Vyper contract from a couple different places. We can get it directly from Titanoboa or Moccasin has actually some built-in ways to get this Vyper contract much quicker. So, we can say `from moccasin.boa.tools import VyperContract`. And, you might even get this little yellow squiggly line because our VENV, if we go back to our `pyproject.toml`, we don't have Moccasin in here. So, we could just simply do `uv add moccasin` if we want to be more specific. We could say `mocassin ==` and then, you know, whatever version what version do we have in here? And then scroll down this 0.3 point whatever enter, and then for this one in particular, I have to do this `--prerelease-allow`. You might not have to do that. Okay, we added that in here. Now in our `deploy.py` file, we see that those little squigglies go away because our Python project now knows that this Moccasin tool exists. So cool, so this is of type Vyper contract and in Vias code, if you hit Command Click or Control Click, like we said, you can actually see the code of the Vyper contract, or the class Vyper contract, which if you took the Python Crash Course, you haven't learned about classes yet. Don't worry about it. Okay, next, `starting_number`. This `favorites_contract.retrieve`, it's going to be what, it's going to be an `int`. Great. `favorites_contract.store`. There's no typing there. `ending_number` is going to be an `int`. Okay, great. And then, we return a `favorites_contract`. So, since we're returning something, we should add a type hint to our function definition. And actually, just like Vyper, how Vyper does this little arrow and then the type, we're going to do the same thing. We're going to do a little arrow and this is a Vyper contract, so we'll say arrow `VyperContract` like this. + +```python +from src import favorites +from moccasin.boa.tools import VyperContract + +def deploy_favorites() -> VyperContract: + favorites_contract: VyperContract = favorites.deploy() + starting_number: int = favorites_contract.retrieve() + print(f"Starting number is: {starting_number}") + favorites_contract.store(77) + ending_number: int = favorites_contract.retrieve() + print(f"Ending number is: {ending_number}") + return favorites_contract +``` + +And even if we wanted to we could have our `mocassin_main` return a Vyper contract and we could just say `return deploy_favorites` like this. Great. And now we have type hints in here. diff --git a/courses/moccasin-101/4-mox-favs/17-sepolia-deploy/+page.md b/courses/moccasin-101/4-mox-favs/17-sepolia-deploy/+page.md new file mode 100644 index 000000000..f8c9524dd --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/17-sepolia-deploy/+page.md @@ -0,0 +1,79 @@ +## Deploying to the Sepolia Test Network + +We are going to be deploying a contract to the Sepolia test network. This is an optional step as testnet tokens can be hard to get. + +Here's the code for the deploy script: +```python +from src import favorites +from moccasin.boa_tools import VyperContract + +def deploy_favorites() -> VyperContract: + favorites_contract: VyperContract = favorites.deploy() + starting_number: int = favorites_contract.retrieve() + print(f"Starting number is: {starting_number}") + + favorites_contract.store(77) + ending_number: int = favorites_contract.retrieve() + print(f"Ending number is: {ending_number}") + return favorites_contract + +def moccasin_main() -> VyperContract: + return deploy_favorites() +``` + +For the Sepolia network, we need to update the configuration. Here's the modified code: +```python +[project] +src = "src" + +[networks.anvil] +url = "http://127.0.0.1:8545" +save_to_db = false +prompt_live = false +default_account_name = "anvil" +unsafe_password_file = "~/.mocassin/unsafe-passwords/anvil" + +[networks.sepolia] +url = "https://eth-sepolia.g.alchemy.com/v2/...." +save_to_db = false +default_account_name = "default" +explorer_uri = "https://eth-sepolia.blockscout.com/" +explorer_type = "blockscout" +``` + +We are using Alchemy to get an RPC URL. + +```bash +mox wallet import default +``` + +We will save the private key to keystores. It's important to never store private keys in your dot env file. + +In our deploy script, we will add a few lines of code to verify our smart contract: +```python +from moccasin.config import get_active_network + +def deploy_favorites() -> VyperContract: + favorites_contract: VyperContract = favorites.deploy() + starting_number: int = favorites_contract.retrieve() + print(f"Starting number is: {starting_number}") + + favorites_contract.store(77) + ending_number: int = favorites_contract.retrieve() + print(f"Ending number is: {ending_number}") + + active_network = get_active_network() + if active_network.has_explorer(): + result = active_network.mocassin_verify(favorites_contract) + result.wait_for_verification() + + return favorites_contract +``` + +We can then run the deploy script to verify our smart contract. + +```bash +mox run deploy --network sepolia +``` + +This will deploy and verify our smart contract, providing a link to the verified contract. diff --git a/courses/moccasin-101/4-mox-favs/18-verify/+page.md b/courses/moccasin-101/4-mox-favs/18-verify/+page.md new file mode 100644 index 000000000..a25f1cb6f --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/18-verify/+page.md @@ -0,0 +1,41 @@ +## Verifying Already Deployed Contracts + +We can verify a smart contract that's already been deployed using the `Deployer` object. We will use the `at()` method. The `at()` method is a static method used to interact with a contract that has already been deployed. + +We can verify a deployed contract by grabbing the contract's address and using it in the `at()` method. + +### Verifying a Smart Contract + +We can verify the deployed *favorites* contract in the `deploy.py` file: + +```python +favorites_contract = favorites.at("0x4C7EE301AC56BDaE56B95FB4a6Dc2e26D0D9DC350") +result = active_network.has_explorer() +if result == active_network.mockchain: + result.wait_for_verification(favorites_contract) +``` + +The code above shows how to verify a previously deployed smart contract. We can copy the contract address from the Blockscout website and use it to interact with the contract. Then, we can use the `wait_for_verification()` method to verify that the contract has been successfully verified. + +We can add code that verifies a deployed smart contract in the `deploy.py` file: + +```python +def deploy_favorites() -> VyperContract: + favorites_contract: VyperContract = favorites.deploy() + starting_number: int = favorites_contract.retrieve() + print(f"Starting number is: {starting_number}") + favorites_contract.store(77) + ending_number: int = favorites_contract.retrieve() + print(f"Ending number is: {ending_number}") + active_network = get_active_network() + favorites_contract = favorites.at("0x4C7EE301AC56BDaE56B95FB4a6Dc2e26D0D9DC350") + if active_network.has_explorer(): + result = active_network.mockchain + result.wait_for_verification(favorites_contract) + return favorites_contract + +def moccasin_main() -> VyperContract: + return deploy_favorites() +``` + +This code verifies that a deployed smart contract exists in the `favorites.py` file. We can execute the script and see that the verification is successful. The code then prints the beginning and ending numbers from the verified contract. diff --git a/courses/moccasin-101/4-mox-favs/19-zksync-setup/+page.md b/courses/moccasin-101/4-mox-favs/19-zksync-setup/+page.md new file mode 100644 index 000000000..23c8d7db0 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/19-zksync-setup/+page.md @@ -0,0 +1,98 @@ +## Setting Up ZkSync + +In this video, we will learn how to set up ZkSync using a framework called Moccasin. + +ZkSync is an EVM equivalent, just like Arbitrum, Base, OP, etc. We'll be working with a ZK Rollup, which is going to be more gas efficient and more performant. + +ZkSync uses an Era VM. It is not completely EVM equivalent, but the co-founder of Ethereum, Vitalik Buterin, has tweeted that he thinks the future of blockchains is on these ZK Rollups. + +### Deploying to a Local ZkSync Instance + +We're going to deploy to a local ZkSync instance using Moccasin. This is very similar to PyEVM, as we can run + +```bash +mox deploy +``` + +or + +```bash +mox run deploy +``` + +which runs our deploy script. This runs on a PyEVM network but is still EVM-compatible. + +### Installing Additional Tools + +To start working with ZkSync, we need to install two additional tools: + +- Era Test Node +- Era Compiler Vyper + +### Era Test Node + +The Era Test Node is an in-memory node that supports forking the state of other networks. + +The first thing we need to do is make sure we have Rust installed on our machine. + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +This command will download the Rust installer. Depending on what operating system you're using, the installation instructions for Rust may vary. If you want a more detailed walkthrough, you can go through the getting started page on the Rust website. + +We can confirm that Rust is installed by running: + +```bash +rustup --help +``` + +It will output a list of available Rust commands. + +Now we need to install the Era Test Node. We can do this by running the installation script. In the Era Test Node repository, we'll need to run: + +```bash +sudo ./install.sh +``` + +This will download the Era Test Node and install it to our machine. We can then confirm the installation by running + +```bash +which era_test_node +``` + +which will output the Era Test Node's location. + +We can also run + +```bash +era_test_node --version +``` + +which will output the version of the Era Test Node that we have installed. + +Finally, if we run + +```bash +era_test_node run +``` + +it will start the Era Test Node, which will be the local ZkSync instance that we can deploy to. It will output a bunch of info, starting with the chain ID, which is L2ChainID (260). + +### Era Compiler Vyper + +The Era Compiler Vyper is a Vyper compiler for ZkSync. To download it, we will go to the releases tab of the Era Compiler Vyper repository and select the version that we want to download. + +We're going to download version 1.5.7. We can copy the link to the file from the Releases tab. + +We can check if we've successfully downloaded and installed the Era Compiler Vyper by running: + +```bash +./zkvyper --version +``` + +The output of this will be the version of the Era Compiler Vyper that we installed. + +Keep in mind that both of these tools will have been downloaded into your local repository. You can move them to another location if you want. + +By installing these tools, we've taken the first steps towards unlocking the power of ZkSync, which will be more gas efficient and performant than current EVM solutions. diff --git a/courses/moccasin-101/4-mox-favs/2-install-mox/+page.md b/courses/moccasin-101/4-mox-favs/2-install-mox/+page.md new file mode 100644 index 000000000..468b1e449 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/2-install-mox/+page.md @@ -0,0 +1,40 @@ +## Installing Moccasin + +We're going to install Moccasin by using the command `uv tool install 'mocassin==0.3.4b1' --prerelease=allow`. This command will install a specific pre-release version of Moccasin, which includes some extra features and quality of life improvements. + +We can check if the installation was successful by running the following commands: + +```bash +which mox +``` + +```bash +which moccasin +``` + +The output of these commands should show the location of the installed executables. If you see the locations, it means the installation was successful. + +We can also explore the available commands by running: + +```bash +mox --help +``` + +This will show you a list of all the commands available in Moccasin, including: + +* **init**: Initialize a new project. +* **compile**: Compiles the project. +* **test**: Runs all tests in the project. +* **run**: Runs a script with the project's context. +* **deploy**: Deploys a contract named in the config with a deploy script. +* **wallet**: Wallet management utilities. +* **console**: Interacts with the network in a python shell. +* **install**: Installs the project's dependencies. +* **purge**: Purge a given dependency. +* **config**: View the Moccasin configuration. +* **explorer**: Work with block explorers to get data. +* **inspect**: Inspect compiler data of a contract. +* **deployments**: View deployments of the project from your DB. +* **utils**: Helpful utilities - right now it's just the one. + +We'll be learning how to use these commands in the upcoming lessons. diff --git a/courses/moccasin-101/4-mox-favs/20-eravm/+page.md b/courses/moccasin-101/4-mox-favs/20-eravm/+page.md new file mode 100644 index 000000000..c1e267874 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/20-eravm/+page.md @@ -0,0 +1,43 @@ +## Getting Started with ZK Sync and Moccasin + +In this lesson, we will continue our journey with Moccasin by working with ZK Sync. We will be focusing on deploying and testing our contracts with ZK Sync. + +First, let's make sure we have installed the necessary tools. We need to install the `era_test_node` and `zkVyper` commands. We can verify their installations with the following commands in our terminal: + +```bash +era_test_node --version +``` + +```bash +zkVyper --version +``` + +We will be using the `mox` command to deploy and test our smart contracts. We can deploy to ZK Sync with the following command: + +```bash +mox run deploy --network eravm +``` + +Moccasin will automatically spin up a local ZK Sync network when we run the `mox run deploy` command. We can then run our tests using the following command: + +```bash +mox test --network eravm +``` + +We will see a lot of warnings during the testing phase. These warnings are due to ZK Sync VM being different from the EVM. However, we can ignore these warnings as our tests will still pass. + +We also saw how to compile our contracts with both the EVM and ZK Sync: + +```bash +mox compile +``` + +```bash +mox compile --network eravm +``` + +Moccasin uses different bytecode for both the EVM and ZK Sync. We can check the bytecode by looking at the `favorites.json` file in our `out` folder. We will see that the VM has changed from EVM to `eravm` and the bytecode has changed. However, the ABI (Application Binary Interface) will remain the same for both the EVM and ZK Sync. + +We have now learned how to deploy and test our contracts with ZK Sync using Moccasin. This allows us to take advantage of the power of ZK Rollups, making our applications faster and more efficient. + +We are now ready to deploy and test our smart contracts in a ZK Sync environment, and we can do so with the confidence that our code is working as expected. diff --git a/courses/moccasin-101/4-mox-favs/21-setting-up-zksync/+page.md b/courses/moccasin-101/4-mox-favs/21-setting-up-zksync/+page.md new file mode 100644 index 000000000..dc65fdcd2 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/21-setting-up-zksync/+page.md @@ -0,0 +1,47 @@ +## Setting up ZkSync network configuration + +In this lesson, we will learn how to set up the configuration necessary to deploy a smart contract to the ZkSync Sepolia Testnet. This can be confusing, but we'll walk through this together. + +We can start with the `mocassin.toml` file. We need to create a new network configuration below the existing network entries. The new network configuration should be named `sepolia-zkync`. + +Here is the basic structure of our network configuration: + +```toml +[networks.sepolia-zkync] +url = "$ZKSYNC_SEPOLIA_RPC_URL" +chain_id = 300 +save_to_db = false +default_account_name = "default" +explorer_uri = "https://explorer.sepolia.era.zkync.dev" +explorer_type = "zkyncxplorer" +is_zkync = true +``` + +We've already configured our RPC URL and chain ID, but we still need to add the `explorer_uri` and `explorer_type`. + +We can use the `mox explorer list` command to list all available explorers, but there are a lot of them. The one we are using is `sepolia-zkync-era`. + +```bash +mox explorer list +``` + +Here is our complete `mocassin.toml` file: + +```toml +[networks.sepolia-zkync] +url = "$ZKSYNC_SEPOLIA_RPC_URL" +chain_id = 300 +save_to_db = false +default_account_name = "default" +explorer_uri = "https://explorer.sepolia.era.zkync.dev" +explorer_type = "zkyncxplorer" +is_zkync = true +``` + +Now, we need to add our new network to MetaMask. We will do this by going to the network settings and hitting "Add a custom network". + +Give the network a name like `ZkSync Era - Sepolia` and then add the RPC URL, chain ID, and currency symbol. We will use the same block explorer URL as our `explorer_uri` in the `mocassin.toml` file. + +Finally, hit "save" in MetaMask and our new network is ready! + +We've now set up our ZkSync Sepolia Testnet and are ready to deploy. diff --git a/courses/moccasin-101/4-mox-favs/22-bridging/+page.md b/courses/moccasin-101/4-mox-favs/22-bridging/+page.md new file mode 100644 index 000000000..40bed46a9 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/22-bridging/+page.md @@ -0,0 +1,69 @@ +## Bridging to ZkSync + +In this lesson, we are going to learn how to bridge funds from the Sepolia Testnet to the ZkSync Sepolia Testnet. + +First, we need to connect our wallet. We will connect via Metamask. + +Then, in Metamask, go to "Edit Networks" and ensure the following are selected: + +* ZkSync Sepolia Testnet +* Sepolia + +Then, hit the "Update" button, and then "Connect". + +Now, we will see an error message. This is due to the fact that we are on the wrong chain in our wallet. If we open the "Menu" tab, we will see an option to change the network. + +We will select "ZkSync Sepolia Testnet" from the menu. + +Our interface now looks like a standard bridging app, and we will type in the amount we want to bridge. + +Then, we will hit "Continue", and we are asked to confirm our transaction. After confirming, we will have to wait approximately 15 minutes for our funds to appear on the ZkSync network. + +Let's look at our configuration file and see what we've added: + +```toml +[networks.sepolia-zkSync] +url = "$ZKSYNC_SEPOLIA_RPC_URL" +chain_id = 300 +save_to_db = false +default_account_name = "default" +explorer_uri = "https://explorer.sepolia.era.zkSync.dev" +explorer_type = "zkSyncExplorer" +is_zkSync = true +``` + +We've also added the explorer type, which is "zkSyncExplorer". + +Next, we'll go to our deploy script, and make sure the following are correct: + +```python +from src import favorites +from moccasin.boa.tools import VyperContract +from moccasin.config import get_active_network + +def deploy_favorites() -> VyperContract: + favorites_contract: VyperContract = favorites.deploy() + starting_number: int = favorites_contract.retrieve() + print(f"Starting number is: {starting_number}") + favorites_contract.store(77) + ending_number: int = favorites_contract.retrieve() + print(f"Ending number is: {ending_number}") + active_network = get_active_network() + if active_network.has_explorer(): + result = active_network.mocassin.verify(favorites_contract) + result.wait_for_verification() + return favorites_contract + +def moccasin_main() -> VyperContract: + return deploy_favorites() +``` + +Now, we are going to deploy our smart contract to the ZkSync Sepolia Testnet. We will run the following in the terminal: + +```bash +python deploy.py +``` + +This will deploy our contract, and then verify it on the ZkSync Sepolia Testnet. + +We will then see a new transaction in our Metamask wallet, and our funds will be available to us on ZkSync! \ No newline at end of file diff --git a/courses/moccasin-101/4-mox-favs/23-zksync-deploy/+page.md b/courses/moccasin-101/4-mox-favs/23-zksync-deploy/+page.md new file mode 100644 index 000000000..6e5ffb2c8 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/23-zksync-deploy/+page.md @@ -0,0 +1,44 @@ +## Deploying to ZKSync + +In this lesson, we'll deploy our smart contract to the ZKSync network. We'll cover the following: + +- Deploying to ZKSync Testnet +- Verifying Our Smart Contract +- Sharing Your Achievement +- Cleaning Up + +### Deploying to ZKSync Testnet + +We can deploy to ZKSync using this command: + +```bash +mox run deploy --network sepolia-zksync +``` + +We'll be prompted to confirm that we want to continue. Type "y" and hit enter. + +We'll then be asked to enter our password. + +Now we'll see our transactions being broadcasted and we can see our contract address has been printed in the terminal. + +### Verifying Our Smart Contract + +Let's verify our smart contract by copying the contract address from our terminal and going to the ZKSync explorer. + +We can find the ZKSync explorer by searching "zksync sepolia testnet explorer" on Google. + +Once we've pasted the contract address in the explorer search, we'll be able to see our contract information, including the source code and deployed bytecode. + +### Sharing Your Achievement + +If we successfully deployed our contract to ZKSync, we can go to the Github repo associated with this course and click on the "Tweet Me (add your contract in)!" link. + +We recommend sending a tweet at ZKSync letting them know we just deployed a smart contract using Mocassin. + +Sharing our success with the community is an important part of our developer journey. + +### Cleaning Up + +If we have an "install.sh" file in our project directory, we can delete it. + +We may also have a "era_test_node.log" file. We can delete this file as well. diff --git a/courses/moccasin-101/4-mox-favs/24-zksync-contract-deployment/+page.md b/courses/moccasin-101/4-mox-favs/24-zksync-contract-deployment/+page.md new file mode 100644 index 000000000..26eacf8c0 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/24-zksync-contract-deployment/+page.md @@ -0,0 +1,18 @@ +## ZK Sync Contract Deployment + +ZK Sync contract deployment is a little different than Ethereum deployments. Instead of having a blank to address, ZK Sync has a to address that's actually another contract, the Contract Deployer contract. This is the contract we used to send our contract to. + +We can find the Contract Deployer's address under the "to" field on the Block Explorer, which we can verify by searching for the address in our MetaMask Wallet. + +The address we found in our MetaMask Wallet is `0x0000000000000000000000000000000000000006`. + +The Contract Deployer contract is actually written in Solidity, and we can find the code using the block explorer. + +Here's an example of how to write the code to call the `ContractDeployer` contract: + +```python +contract_deployer_contract = ContractDeployer.at("0x0000000000000000000000000000000000000006") +contract_deployer_contract.create(bytes_salt, bytes_bytecodeHash, bytes_input) +``` + +The code above shows the necessary parameters to call the `ContractDeployer` contract's `create` function, and the `bytes_salt`, `bytes_bytecodeHash` and `bytes_input` parameters are ones that we would have to convert our Vyper code to. The `ContractDeployer` contract will handle this conversion for us, under the hood. diff --git a/courses/moccasin-101/4-mox-favs/25-tx-types/+page.md b/courses/moccasin-101/4-mox-favs/25-tx-types/+page.md new file mode 100644 index 000000000..6a4c55f1c --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/25-tx-types/+page.md @@ -0,0 +1,39 @@ +## Transaction Types + +We are going to talk about transaction types. We won't do a deep dive explainer, but we will provide a link to an article in the GitHub repo associated with this course. + +Let's dive into this. + +If we run the command: + +```bash +mox run deploy --network eravm +``` +we get a warning that keeps popping up: + +```bash +No EIP-1559 transaction available, falling back to legacy +``` +What's going on here? + +Well, in the blockchain world, there are several different types of transactions. And I kind of glossed over it when we went over the transaction object. + +We said: "Okay, here's what a transaction looks like. It has its from, to, gas limit, fee, blah, blah, blah." + +But, there's also a typed transaction envelope. We can also pass a transaction type in that RLP encoding. + +Now what makes it kind of confusing is that it's not actually in this little JSON blob here, right? It's actually outside of the JSON blob, but again, we're getting kind of a low-level; doesn't really matter where it is. All you need to know is that there are actually multiple different types of transactions. + +The Ethereum website does a decent job of kind of walking you through some of the different types of transactions, but we can also look at them in the Cyfrin Updraft. + +So what's happening is when we get this warning: "Hey, uh no EIP 1559 or 1559 transaction available, falling back to legacy," is that ZK Sync works with a few different types of transactions, like legacy transactions, and they have a couple of their own custom transactions themselves. The main transactions or the new, normal type of transactions on Ethereum are known as these Type 2 or EIP-1559 transactions. Again, if this is going over your head a little bit, don't worry about it too much, but this type of transaction doesn't exist on ZK Sync, right? This is essentially like an advanced gas type of transaction, and ZK Sync has them in by default. + +So, what happens here is that our tools try to send a Type 2 transaction. This super gas, this EIP 1559 transaction onto ZK Sync, and ZK Sync says, "Hey, we don't have those. We're already like pretty darn gas efficient. So, Mocassin and Titania will always try to send an EIP 1559 transaction first, but then, it'll revert back to a legacy transaction for ZK Sync. + +Now, when we're sending on Ethereum, it will work with a Type 2 transaction. It will work with a Type 2 transaction. You can also send Type 0 transactions on Ethereum, but you shouldn't, because they're kind of worse for gas and like gas is money, so you don't want to spend more money. So, pretty much nowadays, everyone sends Type 2 transactions on Ethereum on ZK Sync, they send legacy transactions because the gas on ZK Sync is just better. + +I'm kind of glossing over the details, and this is kind of it from a high level. That's why you're seeing this error here is because there's multiple different types of transactions. + +One of the coolest types of transactions is this EIP 712 or an OX 71 or a 113 transaction, which is known as a native account abstraction transaction. We won't be learning about that in this curriculum. + +**[VIDEO TAG]** **show Solidity examples of transaction types in Cyfrin Updraft** diff --git a/courses/moccasin-101/4-mox-favs/26-workshop/+page.md b/courses/moccasin-101/4-mox-favs/26-workshop/+page.md new file mode 100644 index 000000000..8668ded0f --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/26-workshop/+page.md @@ -0,0 +1,69 @@ +## Workshop + +We have a few workshops for you. + +The first is to deploy your contract to your Tenderly virtual network. We'll be using a `mocassin.toml` file for this. To add a new network, you'll need to create a new array within the networks key. For example, to deploy to Tenderly you can create a new network named `our-fake-chain` and then add the necessary keys and values. + +```toml +networks.our-fake-chain +``` + +Next, we'll write our own deploy script and deploy to the PyVM network. To do this, we can create a new file called `my_deploy.py` within the `script` folder. Here's some boilerplate code to get started: + +```python +def deploy_me(): + print("Hello!") + +def moccasin_main(): + return deploy_me() +``` + +We can test this with the following command: + +```bash +mox run my_deploy +``` + +The output should be: + +```bash +Hello! +``` + +We can also write a deploy script and deploy to the EraVM network. To do this, we'll use a `--network EraVM` flag. + +```bash +mox run my_deploy --network EraVM +``` + +The final workshop is to write a new test in your test file and run it. You can do this in the `test_favorites.py` file. + +For our final example, we'll be using the `buy_me_a_coffee.vy` contract. The first thing you need to do is get the price of a different asset using Chainlink price feeds on your fake chain. We have a function called `get_eth_to_usd_rate` that we can repurpose for this. + +We can rename the function to `get_rate`. + +```vyper +def get_rate(eth_amount: uint256) -> uint256: + return self.get_eth_to_usd_rate(eth_amount) +``` + +The second workshop is to write a function to get the total amount of funds in the contract. We can use a loop and the `funders` array to accomplish this. + +```vyper +def get_total() -> uint256: + return self.balance +``` + +Finally, we'll write a function that allows you to change the owner of the contract. Remember, our owner at the top is immutable, so we'll need to remove this to create our function. + +```vyper +OWNER: public(immutable(address)) +``` + +We need to make the `OWNER` variable mutable. We can do that by removing the `immutable` keyword. + +```vyper +OWNER: public(address) +``` + +You should spend no more than 30 minutes working on these workshops. If you get stuck, you can use an AI assistant to help you. If you still can't figure it out after 30 minutes, take a break and come back to it later. diff --git a/courses/moccasin-101/4-mox-favs/27-recap/+page.md b/courses/moccasin-101/4-mox-favs/27-recap/+page.md new file mode 100644 index 000000000..39489661d --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/27-recap/+page.md @@ -0,0 +1,94 @@ +## Mocassin Favorites Recap + +And with that, we have crammed so much information into your brain that you should be bursting at the seams with information. And we are going to take some time to wrap up so that you can go on an ice cream break or a gym break or a coffee break or whatever kind of break that you would like to go on. We've learned a ton in this section. So let's do a quick recap of all the things that you've learned so you can take a break. + +First off, we were introduced to this tool called Mocassin, which is this TitanoBoa based smart contract development framework, which allows us to test deploy and work with our smart contracts in a much simpler way. We spun up a brand-new Mocassin project and we learned a ton of stuff. First off, we learned how to even test our contracts, which is a crucial piece of our journey, and we know that no one's going to take our code seriously unless we have tests. Tests are absolutely crucial to your smart contract development journey. We learned how to write tests using Mocassin's pie test framework. + +```python +def test_add_favorite(): + favorites = Favorites.deploy() + favorites.add_favorite(1, "Ethereum") + assert favorites.favorites(1) == "Ethereum" +``` + +Additionally, we learned about how fixtures can help write our tests to be faster and even more proficient. + +```python +@pytest.fixture +def favorites(): + return Favorites.deploy() + +def test_add_favorite(favorites): + favorites.add_favorite(1, "Ethereum") + assert favorites.favorites(1) == "Ethereum" +``` + +We learned how to write a deploy script in Mocassin using this native import syntax like from SRC import favorites where we can actually just deploy our contract with the deploy keyword. + +```python +from src import favorites + +def main(): + favorites.deploy() +``` + +And then additionally, right inside Mocassin, we can verify our smart contracts with just one or two, or I guess three here, three lines of code. + +```bash +mox verify --network sepolia +``` + +We learned how to switch between networks in Mocassin. If we want to work with a different network, all we have to do is add the information about that network to our moccasin.toml under the networks area. And we can deploy to different networks just by running mox run deploy with --network, and then the name of the other network that we want to deploy to. + +```toml +[networks] +default = "pie-evm" +anvil = { + url = "http://127.0.0.1:8545" +} +sepolia = { + url = "https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID" +} +``` + +```bash +mox run deploy --network anvil +``` + +If we don't specify a network, it will use the default network. And Mocassin defaults to the pie EVM, which is this fake, locally running network. We also learned that Mocassin comes built in with the EraVM. + +```bash +mox run deploy --network EraVM +``` + +We can test and deploy to ZK Sync. + +We learned about encrypting and decrypting our wallets. So, if I do mox wallet list, I can see a list of accounts that I've encrypted locally so that I don't have to expose my private key. Oh my gosh, that feels good. That feels good. And I know that if I were to, you know, for example, let's spin up let's spin up anvil on another terminal. If I were to do mox run deploy --network anvil, and I were to get rid Where is it? And I were to get rid of the unsafe password file, it would prompt me for my password, meaning my keys can stay secure. I will not be sharing my private keys with you. Although, this one, the password is just "password" which is a terrible password. But you get the picture. We learned how to do that. We learned how to deploy to Sepolia and to ZK Sync Sepolia. And if you didn't actually deploy there, no worries, you got to see the process. You got to watch us automatically verify these contracts on different explorers, like BlockScout and ZK Sync Explorer. + +We learned we could compile our contracts. And we learned we could compile them both with EVM or with ZK Sync EVM. + +```bash +mox compile --network sepolia +``` + +We learned a little bit more Python, we added type hinting into our scripts here. We learned about environment variables and how our .env file can have environment variables. We know that it's not a great place to store private keys. It's It's more okay to store RPC URLs cuz they're not that sensitive. Private keys are very sensitive. + +```bash +mox run deploy --network sepolia +``` + +We learned a little bit more about Python here, where we added some dependencies for our project so that our linter, our little syntax highlighter, worked correctly. + +```toml +[project] +name = "mocassin" +version = "^0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.11" +[dependencies] +mocassin-0 = "0.3.4b2", +pytest-8 = "8.3.3", +``` + +You have learned an absolute ton in this section. And you should be incredibly proud of yourself. Now, if you haven't shared on Twitter or on some social media, I'm going to just tell you one more time, you absolutely should. Joining the developer community is going to a increase your chances of getting a job if that's what you're looking for. b you're going to get to meet a ton of really cool phenomenal people. And c give you a chance to Yeah, join like-minded individuals. You can join here or if you're on Cyffin Updraft, you can of course join the Discord and meet a ton of other like-minded developers as well. That being said, huge congratulations for getting this far. And huge congratulations for finishing Mocassin Favorites. diff --git a/courses/moccasin-101/4-mox-favs/3-create-mox/+page.md b/courses/moccasin-101/4-mox-favs/3-create-mox/+page.md new file mode 100644 index 000000000..313af66c5 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/3-create-mox/+page.md @@ -0,0 +1,71 @@ +## Creating a Moccasin project + +Now that we have Moccasin installed, we can start creating our project. We'll be working in our `mox-cu` folder. + +Let's start by creating a new folder called `mox-favorites-cu` with the following command: + +```bash +mkdir mox-favorites-cu +``` + +Next, we'll open this new folder in VS Code. + +```bash +code mox-favorites-cu/ +``` + +If you don't have the `code` command, you can also do `File > Open Folder`. + +Moccasin comes with a built-in tool to help us create new projects. We can use the `mox init` command to do so. + +```bash +mox init +``` + +Before running this command, we can use the `--help` flag to see what it will do. + +```bash +mox init --help +``` + +This will print out a list of options and the directory structure that the project will have. We'll be using VS Code and want to include a `pyproject.toml` file, so we can run the following command to initialize the project: + +```bash +mox init --vscode --pyproject +``` + +The terminal will output a message confirming the project was initialized. + +Let's take a look at the files and folders that have been created: + +- `.vscode` : This folder contains settings for VS Code, which we'll explore later. +- `lib` : This is where we'll install any packages or dependencies our project requires. +- `script` : This folder is where we'll store any deploy scripts. In this case, we have a script called `deploy.py` which deploys the `Counter` contract. +- `src` : This is where we'll store our Vyper contracts. Moccasin has included a simple `Counter.vy` contract for us to start with. +- `tests` : This folder will house any tests related to our contracts. Currently, it has a few simple dummy tests. +- `pytest_cache` : This folder stores information from the pytest testing framework to improve test run times. +- `coveragerc` : This file is used for code coverage reporting and we'll discuss it in detail later. +- `.gitattributes` and `.gitignore` : These files are used to configure Git behavior and specify files or folders that shouldn't be tracked by Git. +- `mocassin.toml` : This file configures Moccasin settings. It has information like network URLs and chain IDs. +- `pyproject.toml` : This file contains information about our Python project, including dependencies and project metadata. +- `README.md` : This file provides a brief overview of the project, including instructions for getting started. + +We can start by creating a new file called `favorites.vy` in the `src` folder. We can do this by right-clicking within the `src` folder, selecting `New File`, and entering the name `favorites.vy`. + +We'll then copy and paste the code for the `favorites.vy` contract from the GitHub repository for this course. + +Let's go through the folders and files in detail: + +- `.vscode` : This folder contains the `settings.json` file, which is only relevant to users of VS Code. It helps with syntax highlighting and other editor features. +- `lib` : We'll use this folder to install dependencies. +- `script` : This folder contains the deploy script `deploy.py` which deploys the `Counter` contract. It uses the `deploy` keyword to deploy the contract, followed by calling the `increment` function to check the starting and ending count. +- `src` : This folder will contain all our Vyper contracts. Currently, we have a `Counter.vy` contract and we've just added the `favorites.vy` contract, which we'll use throughout this course. +- `tests` : This folder is used to store tests. We can run the tests with the `mox test` command. +- `pytest_cache` : This folder is used by pytest to store information that makes tests run faster. +- `coveragerc` : This file is used to configure coverage reporting. +- `.gitattributes` and `.gitignore` : These files are used to manage Git behavior. +- `mocassin.toml` : This file stores Moccasin settings. +- `pyproject.toml` : This file contains project information. +- `README.md` : This file provides a brief overview of the project and instructions for getting started. + + diff --git a/courses/moccasin-101/4-mox-favs/4-mox-something/+page.md b/courses/moccasin-101/4-mox-favs/4-mox-something/+page.md new file mode 100644 index 000000000..b96a73fcd --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/4-mox-something/+page.md @@ -0,0 +1,26 @@ +## Compiling a project + +We have our `favorites.vy` file here with a pragma version 0.4.0. We also see a comment indicating the license is MIT. + +```python +# pragma version 0.4.0 +# @license MIT +``` + +We have the contract code in here, which is typical. Usually we would compile this with Vyper SRC, but in this case, we will do it with the MoccaSin command. We will run the Vyper compiler to actually compile the project. + +```bash +vyper src/favorites.vy +``` + +We can compile every file in our SRC folder by running the MoccaSin command. This will compile one project into an out directory. + +```bash +mox compile +``` + +We can see that this successfully creates a `favorites.json` file in the out directory. + +This has a ton of compilation details about the favorites contract. It has the bytecode that we're used to seeing now. It has the ABI. If I scroll over to the left here, I can even click this little drop-down to see what else we have and then we have VM which is the EVM or the Ethereum virtual machine. So that's great. And if we were to have Let me just copy-paste this in here a whole bunch If we were to have a ton of contracts in here Same thing, we could just run `mox compile` compile every single project to the out folder and be good. So, I'm going to go ahead and delete all of these now. + +Now what's cool about MoccaSin is that you don't even have to compile. When you run your scripts, if they don't compile, well the scripts will just break. So compiling is actually optional. It's just that if you do want to compile so that you have these different so that you can actually see, you can inspect the bytecode and the ABI, etc, you can see that in the out folder there. diff --git a/courses/moccasin-101/4-mox-favs/5-mox-script/+page.md b/courses/moccasin-101/4-mox-favs/5-mox-script/+page.md new file mode 100644 index 000000000..5b625d62f --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/5-mox-script/+page.md @@ -0,0 +1,75 @@ +## Moccasin Scripting + +In this section, we will learn how to deploy our smart contract using a deploy script. + +We will start with the typical way we would deploy a contract using Boa, as we did in the previous section. We would use code similar to this: + +```python +import boa +boa.load("favorites.vy") +``` + +However, in Moccasin, we can directly import the contract we want to deploy using the Python import system. For example: + +```python +from src import favorites +``` + +Moccasin is smart enough to know that it is dealing with a Vyper contract and will automatically import the necessary libraries and dependencies. + +To deploy the contract using a deploy script, we will need to write a Python script that utilizes the `deploy` command. In this script, we can also interact with the deployed contract to verify its state. + +Let's create a new file called `deploy.py` and add the following code: + +```python +from src import favorites + +def deploy(): + favorites_contract = favorites.deploy() + starting_number = favorites_contract.retrieve() + print(f"Starting number is: {starting_number}") + +def moccasin_main(): + deploy() +``` + +To run this script in Moccasin, we can use the `mox run` command. + +This command will execute the `mocassin_main()` function, which in turn will call the `deploy()` function. The `deploy()` function will deploy the `favorites` contract and then retrieve the starting number. We can then print the starting number to the terminal to verify that the contract has been deployed correctly. + +We can also use the `breakpoint()` function to step through the script and inspect the state of the variables. + +To use `breakpoint()`, we can add the following code to our `deploy.py` script: + +```python +from src import favorites + +def deploy(): + breakpoint() + favorites_contract = favorites.deploy() + starting_number = favorites_contract.retrieve() + print(f"Starting number is: {starting_number}") + +def moccasin_main(): + deploy() +``` + +When we run this script with `mox run deploy`, the script will pause at the `breakpoint()` function. We can then use the debugger to inspect the state of the variables and step through the code. + +```bash +mox run deploy +``` + +We can then use commands such as `type(favorites)` to view the type of the `favorites` object: + +```bash +type(favorites) +``` + +Finally, we can run `quit()` to exit the debugger and continue the execution of the script. + +```bash +quit() +``` + +This is a basic example of deploying a contract using a deploy script in Moccasin. There are many other ways to interact with the deployed contract, such as calling functions and viewing events. In the following sections, we will explore some of these advanced techniques in more detail. diff --git a/courses/moccasin-101/4-mox-favs/6-other-net/+page.md b/courses/moccasin-101/4-mox-favs/6-other-net/+page.md new file mode 100644 index 000000000..f48e84e4b --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/6-other-net/+page.md @@ -0,0 +1,83 @@ +We've learned how to deploy a simple script to our local PiVM. Now, let's learn how to deploy it to a different network. + +We can deploy this script to our anvil network. You know, our locally running anvil network. + +First, open a new terminal, clear it out, and run anvil: + +```bash +anvil +``` + +We can then run our script like this: + +```bash +mox run deploy +``` + +Our script will run, but nothing will happen on Anvil. This is because we're deploying to Anvil, and our current setup is running on PiVM. + +We're going to use a flag to tell our script which network we're deploying to. If we run: + +```bash +mox run --help +``` + +We'll see a number of commands to help us interact with our Anvil chain. There's a flag called `--network` which lets us define the network we want to deploy to, taken from our `mocassin.toml` file. + +To see what's in our `mocassin.toml` file, let's look at it! + +This `mocassin.toml` file is used to configure all of our networks and contains any kind of setting for Moccasin. + +You can see all the possible settings in the TOML documentation here: https://cyfrin.github.io/mocassin/all_mocassin_toml_parameters.html + +In our case, we can create a new network object named anvil. + +To do this, we'll make a new section in our `mocassin.toml` file: + +```toml +[networks.anvil] +``` + +We need to give our network a URL. We can find the Anvil URL running in the terminal: + +```toml +url = "http://127.0.0.1:8545" +``` + +We'll also add this flag: + +```toml +save_to_db = false +``` + +We've added our new network section to our `mocassin.toml` file. + +Let's try to deploy our contract again! + +```bash +mox run deploy --network anvil +``` + +Our script will run, but it'll ask us to hit yes. + +We'll see a value error here. + +```bash +ValueError: eoa not defined! +``` + +This is a common error. You can find more information about this in the Moccasin documentation: https://cyfrin.github.io/mocassin/common_errors.html + +It means that we're trying to deploy to Anvil, but we haven't added an account to our `mocassin.toml` file! + +We can do this by following the wallet guide in the documentation: https://cyfrin.github.io/mocassin/core/concepts/wallet.html + +Our Anvil network is running, so we can go there and grab a private key to deploy our contract. + +We can then run our deploy script like this: + +```bash +mox run deploy --network anvil --private-key 0x9740....2f764180aa3 +``` + +That was an example of how to deploy a script to a different network. In the next section, we'll learn how to use Moccasin's wallet commands! diff --git a/courses/moccasin-101/4-mox-favs/7-encrypt-key/+page.md b/courses/moccasin-101/4-mox-favs/7-encrypt-key/+page.md new file mode 100644 index 000000000..5eff336f3 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/7-encrypt-key/+page.md @@ -0,0 +1,62 @@ +## Encrypting a Private Key + +We've learned how to encrypt our keys back when we were using Python, but Mocassin has its own way to handle encryption. + +We can find it in the documentation under the **Wallet** section. + +We'll use the `wallet import ACCOUNT_NAME` command to create a keystore file in the default keystore directory. + +The command will then prompt us to enter our private key and password. + +The example command is as follows: + +```bash +mox wallet import my_account +``` + +Let's look at the different commands available to us in the `wallet` section: + +```bash +mox wallet --help +``` + +This shows a list of commands, including: + +* `list (ls)`: This will list all of our accounts. +* `generate (g, new)`: This will create a new account with a random private key. +* `import (i, add)`: This will import a private key into an encrypted keystore. +* `view (v)`: This will view the JSON of a keystore file. +* `decrypt (dk)`: This will decrypt a keystore file to get the private key. +* `delete (d)`: This will delete a keystore file. + +We'll use the `import` command to encrypt our key. To do this, we'll copy the private key for the Anvil account and run this command in our terminal: + +```bash +mox wallet import anvil1 +``` + +The terminal will then ask us to enter our private key. We'll paste the copied key into the terminal and press Enter. + +The terminal will then ask us to enter a password to encrypt our key. We'll enter a password and confirm it. The terminal will then show: + +```bash +Saved account anvil1 to keystores! +``` + +This will save the encrypted account to our keystore. + +Now, if we want to view the accounts we have, we can run: + +```bash +mox wallet list +``` + +The terminal will show us all accounts we have saved in our keystore. + +We can also use the `view` command to see the JSON of a specific keystore. For example: + +```bash +mox wallet view anvil1 +``` + +This will show the JSON for the `anvil1` keystore. We'll see the address of the account and the encrypted key. diff --git a/courses/moccasin-101/4-mox-favs/8-shell/+page.md b/courses/moccasin-101/4-mox-favs/8-shell/+page.md new file mode 100644 index 000000000..00df35f0d --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/8-shell/+page.md @@ -0,0 +1,3 @@ +--- + title: Shell + --- \ No newline at end of file diff --git a/courses/moccasin-101/4-mox-favs/9-using-key/+page.md b/courses/moccasin-101/4-mox-favs/9-using-key/+page.md new file mode 100644 index 000000000..4c59eed74 --- /dev/null +++ b/courses/moccasin-101/4-mox-favs/9-using-key/+page.md @@ -0,0 +1,27 @@ +## Using an encrypted key in your moccasain script + +We've already encrypted a private key in our Mocaasain project and have the following in our `mocassin.toml` file. + +```toml +[project] +src = "src" + +[networks.anvil] +url = "http://127.0.0.1:8545" +save_to_db = false +prompt_live = false +``` + +Now we can run this script. We can do + +```bash +mox run deploy --network anvil +``` + +We can also add the following to our command: + +```bash +mox run deploy --network anvil --account anvil1 +``` + +Moccasin knows that `anvil1` is one of the accounts that we have encrypted. When we hit enter we are prompted to enter our password for the keystore. We'll enter our password and you'll notice that it doesn't show our password. When we hit enter, it decrypts our key and sends the transactions. We can see that we have some transaction activity on the Anvil chain. This is very exciting! diff --git a/courses/moccasin-101/5-mox-five/1-intro/+page.md b/courses/moccasin-101/5-mox-five/1-intro/+page.md new file mode 100644 index 000000000..a5ee45916 --- /dev/null +++ b/courses/moccasin-101/5-mox-five/1-intro/+page.md @@ -0,0 +1,13 @@ +## Moccasin Five More + +We're going to keep moving right along in this course. We're going to be learning about modules today. The repo's coming along very well. We're going to keep moving on along here, and we're going to go into be the next section, the Moccasin Five More. This is going to be the final time we work with our favorites contract. We will see you no more. However, we're not just working with the favorites contract. We're going to be working with a few other contracts here. + +Let me show you what we're going to be learning in this section. We have a couple of new contracts, along with a `favorites_factory` contract, that we will interact with. We will also use the `ext call` to interact with our contracts and also learn about `static call`. + +We also have an interface, which is a new thing we've not encountered before. Our main Vyper code will be in our `five_more.vy` file. We'll use it to import favorites. + +We're going to use the `initialize` and `favorites.init` functions. + +We also have the `export` function at the bottom of our file. + +This section is all about modules. We will be playing a whole bunch with this contract. This should be a quick pit stop here, just to learn about modules, and then we're going to go on to the more advanced Buy Me a Coffee. \ No newline at end of file diff --git a/courses/moccasin-101/5-mox-five/10-vyper-modules/+page.md b/courses/moccasin-101/5-mox-five/10-vyper-modules/+page.md new file mode 100644 index 000000000..0d10a4fb4 --- /dev/null +++ b/courses/moccasin-101/5-mox-five/10-vyper-modules/+page.md @@ -0,0 +1,56 @@ +## Introduction to Vyper Modules + +We're going to be learning about Vyper modules. + +We'll create a new file called `five_more.vy`. It's going to be very similar to the `favorites.vy` contract, but we'll modify the `store()` function to add five to the number stored. + +```python +self.my_favorite_number = new_number + 5 +``` + +We want to reuse as much code as possible without copy-pasting everything, so we'll use Vyper's module system. + +A module is a way to encapsulate reusable code. + +Vyper favors **composition over inheritance** - we compose modules to create new contracts. + +Our `favorites_factory.vy` contract imports the `I_favorites` module: + +```python +from interfaces import I_favorites +``` + +The `I_favorites` module helps us type our list of `favorites` contracts. + +```python +list_of_favorite_contracts: public(DynArray[I_favorites, 100]) +``` + +When we import a module, it doesn't automatically become part of our contract. We need to explicitly tell Vyper how to use it. + +We can import the `favorites` contract into our `five_more.vy` file: + +```python +import favorites +``` + +Now, if we comment out the variables and functions that we imported, the contract still compiles because we only imported it, not copied it. + +```python +# my_favorite_number: uint256 +# @external +# def store(new_number: uint256): +# self.my_favorite_number = new_number +``` + +We can verify this by running the following commands: + +```bash +mox compile +``` + +The ABI of our `five_more.vy` contract will be empty since we're just importing it. + +```bash +# mox run deploy +``` diff --git a/courses/moccasin-101/5-mox-five/11-initializes-module-state/+page.md b/courses/moccasin-101/5-mox-five/11-initializes-module-state/+page.md new file mode 100644 index 000000000..fd5dd0314 --- /dev/null +++ b/courses/moccasin-101/5-mox-five/11-initializes-module-state/+page.md @@ -0,0 +1,75 @@ +## Initializing Module State + +We can initialize a module's state using the `initializes` keyword in Vyper. The `initializes` keyword declares where a module's storage is located in the contract's storage layout. If the module has an `_init()` function, the `initializes` keyword will create a requirement that the module's `_init()` function is invoked. We can use this keyword to make sure the module's state is set up before interacting with it. + +We can also reference a module's state using the `uses` keyword. This approach is more advanced and commonly used by library designers. The `uses` statement allows us to utilize another module's state without directly initializing it. This is a good approach when we are using libraries that have pre-defined states that we don't want to modify. + +Let's look at an example of initializing a module using the `initializes` keyword. + +We have a module called `favorites` that has a few state variables: + +```python +struct Person: + favorite_number: uint256 + name: String[100] + +my_name: public(String[100]) +my_favorite_number: public(uint256) # 7 +list_of_numbers: public(uint256[5]) # 0, 0, 0, 0, 0 +list_of_people: public(Person[5]) +index: public(uint256) +name_to_favorite_number: public(HashMap[String[100], uint256]) +``` + +The `favorites` module also has an `_init()` function: + +```python +@deploy +def _init_(): + self.my_favorite_number = 7 + self.index = 0 + self.my_name = "YOUR NAME!" +``` + +We can initialize the `favorites` module from our contract using the `initializes` keyword: + +```python +initializes: favorites +``` + +To access the state variables, we can use the following code: + +```python +@deploy +def _init_(): + favorites._init_() + print(favorites.my_favorite_number) +``` + +When we compile the code, the `favorites` module's state variables will be initialized, and we will be able to access them. We'll get a warning message: + +```bash + "print should only be used for debugging!", node) +``` + +Since the `favorites` module has no view functions, we cannot directly call any of them. However, if we wrap a variable with the `public` keyword, it will implicitly create an external view function. + +We also have a module called `five_more` that has no `_init()` function and we can access its state variables in the same way: + +```python +@deploy +def _init_(): + favorites._init_() + print(favorites.my_favorite_number) + print("HI FROM FIVE MORE!") +``` + +We can access a module's state variables by referencing them using the module name and the variable name: + +```python +print(favorites.my_favorite_number) +``` + +The `uses` statement provides an alternative approach to initializing a module, but we will not cover that here. + +Let's review the major topics discussed in this lesson. We learned how to initialize a module's state using the `initializes` keyword. We also reviewed how to access the module's state variables. Finally, we learned that we can use the `public` keyword to implicitly create an external view function. This approach provides a more secure way of accessing a module's state. diff --git a/courses/moccasin-101/5-mox-five/12-exports/+page.md b/courses/moccasin-101/5-mox-five/12-exports/+page.md new file mode 100644 index 000000000..b669c9d57 --- /dev/null +++ b/courses/moccasin-101/5-mox-five/12-exports/+page.md @@ -0,0 +1,171 @@ +## Exports + +The next question we'll address is how to access functions from another Vyper contract. We'll demonstrate this with our `five_more` contract. + +First we'll have a look at the `five_more.json` file, specifically the `abi` portion: + +```json +{ + "contract_name": "five_more", + "bytecode": "346180615761000c61001e565b100286100e565b396100266100f358600760055", + "abi": [ + { + "stateMutability": "nonpayable", + "type": "constructor", + "inputs": [], + "outputs": [] + }, + { + "stateMutability": "view", + "type": "function", + "name": "retrieve", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "store", + "inputs": [ + { + "name": "new_number", + "type": "uint256" + } + ], + "outputs": [] + }, + { + "stateMutability": "view", + "type": "function", + "name": "my_name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "my_favorite_number", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "list_of_numbers", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256[]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "list_of_people", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "tuple[]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "index", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "name_to_favorite_number", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "map(string,uint256)" + } + ] + } + ], + "vmf": "evm" +} +``` + +We can select which functions we want to expose in our contract. + +We'll use the `exports` keyword to achieve this: + +```vyper +exports: ( + favorites.retrieve, + favorites.store +) +``` + +We can access these functions from our `five_more` contract by using the following format: + +```vyper +favorites.retrieve() +``` + +```vyper +favorites.store(new_number) +``` + +Let's add another external function called `store` that adds a new number to the `my_favorite_number` variable: + +```vyper +@external +def store(new_number: uint256): + favorites.my_favorite_number = new_number + 5 +``` + +Let's compile our updated `five_more` contract: + +```bash +mox compile +``` + +We can now use the `store` function in our `deploy.py` script to store a new value in the `my_favorite_number` variable: + +```python +five_more_contract.store(90) +print(five_more_contract.retrieve()) +``` + +And now we'll run our deploy script: + +```bash +mox run deploy +``` + +We should see that `retrieve` gives us a new value of 95! + +We can think of the `exports` keyword like this, if we have a contract `favorites.vy` inside of `five_more.vy`. + +Using the `exports` keyword allows us to define which of the functions or public variables from `favorites.vy` we want to expose publicly in our `five_more.vy` contract. diff --git a/courses/moccasin-101/5-mox-five/13-workshop/+page.md b/courses/moccasin-101/5-mox-five/13-workshop/+page.md new file mode 100644 index 000000000..04db1277b --- /dev/null +++ b/courses/moccasin-101/5-mox-five/13-workshop/+page.md @@ -0,0 +1,57 @@ +## Modules and Uses Statement + +Let's talk about modules and the uses statement in Vyper. + +We've already gone over how to create a module. There's just one more thing to talk about. + +### Initializing a Module With Dependencies + +Sometimes, you'll encounter a module that uses other modules. + +Vyper's module system allows this. However, it requires us to explicitly declare access to the imported module's state. + +We will use the special "walrus" syntax to initialize the module that we're going to use. + +We'll import `ownable_2step` and then we'll say `initializes: ownable`. This is the walrus syntax. + +```python +import ownable_2step + +initializes: ownable +``` + +Next, we'll initialize the `ownable_2step` module, and we'll have to use the walrus operator for this. Remember that this syntax might seem weird. + +```python +initializes: ownable_2step[ownable] := ownable +``` + +This is just saying that the `ownable_2step` module is initialized by this module. This might be a little confusing, but it'll make sense once you see it in practice. + +### Workshop + +Here's a workshop for you: + +1. Add a second module to `five_more`. We've already imported `favorites`, and we've initialized `favorites`. + +```python +import favorites + +initializes: favorites +``` + +2. Try to export functions from two different modules. + +### The Uses Statement + +This is one area we'll use an AI. You can go ahead and ask AI questions to solve this problem. + +Remember, there is a `uses` statement in Vyper. You can read through the docs and understand how to use it to make a contract with a `uses` statement. + +```python +import ownable + +uses: ownable +``` + +We'll go over this more later in the course! diff --git a/courses/moccasin-101/5-mox-five/14-recap/+page.md b/courses/moccasin-101/5-mox-five/14-recap/+page.md new file mode 100644 index 000000000..3ea28052b --- /dev/null +++ b/courses/moccasin-101/5-mox-five/14-recap/+page.md @@ -0,0 +1,68 @@ +## Recap + +We've learned a lot in this section, so let's do a quick recap. + +### Creating Contracts from Other Contracts + +We've learned about creating contracts from other contracts, and we've learned about two keywords that Vyper has built in. + +One of these keywords is `create_copy_of`. This takes the address of another contract and literally copies the bytecode of it. + +```python +create_copy_of(self.original_favorite_contract) +``` + +### Creating Interfaces + +We've also learned about interfaces. We can have a .vyi file where we just have the function definitions, and we don't have any of the function body. + +```python +@external +def store(new_number: uint256): + ... +``` + +### Interacting with Other Contracts + +We've learned that our contracts can interact with other contracts using `staticcall` and `extcall`. + +`staticcall` is something we learned about earlier, but now we've finally learned about `extcall`, which is an external call where we're going to call a contract outside of our contract. + +```python +extcall favorites_contract.store(new_number) +``` + +### Initializing Variables + +We've learned that we can initialize state and storage variables with the `initializes` keyword. + +```python +initializes: favorites +``` + +### Importing Functions + +We've learned about the `exports` keyword, which we can use to import specific functions from any modules that we've initialized or imported. + +```python +exports: (favorites.retrieve, favorites.add_person) +``` + +### Function Name Collision + +We've learned that we can't have a function with the same name as one of the functions that we export. Otherwise, we'll get a collision. You can't have two functions of the same name. + +### Chaining Commands + +We've learned a bit more about chaining commands, which is something we've done a few times now, but we made it more explicit in this section. + +```python +favorites.init() +``` + +## Moccasin Buy Me A Coffee (GET HERE!) + +Now is a great time to take a break. Go get some coffee, go for a walk, or go to the gym because the next section is probably one of the most important sections you'll do in your entire coding curriculum. +We will be putting this on your GitHub. This will be the start of your Syphon profile, doing projects like this, showing other people how badass you are and how much of a badass coder you are, is how you're going to be able to get hired. + +And, if you don't care about getting hired, it's drilling these skills in that're going to make you very skillful. Repetition is the mother of skill. We want you to be skillful, so be sure to take that break before tackling this project. I'll see you very soon. diff --git a/courses/moccasin-101/5-mox-five/3-create-copy-of/+page.md b/courses/moccasin-101/5-mox-five/3-create-copy-of/+page.md new file mode 100644 index 000000000..44b1543b6 --- /dev/null +++ b/courses/moccasin-101/5-mox-five/3-create-copy-of/+page.md @@ -0,0 +1,46 @@ +## Creating Copies of Existing Contracts in Vyper + +We are going to implement the `create_copy_of` function in Vyper. This function will create an identical copy of an existing contract, with a new unique address. + +First, we need to save the address of our `favorites.vy` contract. We will store it as a state variable called `original_favorite_contract`. + +```python +original_favorite_contract: address +``` + +Next, we need to define an `_init` function. This function will be called when we deploy the `favorites_factory.vy` contract. We will pass in the address of the `favorites.vy` contract as an argument, and then store it as a state variable. + +```python +@deploy +def _init(original_contract: address): + self.original_favorite_contract = original_contract +``` + +We will now create a new function called `create_favorites_contract`. This function will be responsible for creating a copy of the `favorites.vy` contract. It will call the `create_copy_of` function, which will create a byte-for-byte copy of the runtime code stored at the `original_favorite_contract` address. The function will then return the address of the new contract. + +```python +@external +def create_favorites_contract(): + new_favorites_contract: address = create_copy_of(self.original_favorite_contract) + return new_favorites_contract +``` + +Let's also save the addresses of our new contracts to a list, so we can keep track of them. + +```python +list_of_favorite_contracts: public(DynArray[address], 100) +``` + +We are creating a dynamic array with a maximum size of 100. This is where we will store the address of every new contract. + +Now, we will update our `create_favorites_contract` function to append the address of our new contract to the `list_of_favorite_contracts` array. + +```python +@external +def create_favorites_contract(): + new_favorites_contract: address = create_copy_of(self.original_favorite_contract) + self.list_of_favorite_contracts.append(new_favorites_contract) + return new_favorites_contract +``` + +This is all we need to do to create copies of existing contracts. We have successfully implemented the `create_copy_of` function in Vyper. diff --git a/courses/moccasin-101/5-mox-five/4-static-ext-call/+page.md b/courses/moccasin-101/5-mox-five/4-static-ext-call/+page.md new file mode 100644 index 000000000..f30fda8d2 --- /dev/null +++ b/courses/moccasin-101/5-mox-five/4-static-ext-call/+page.md @@ -0,0 +1,28 @@ +## Staticcall and Extcall + +In this lesson, we'll be expanding on our previous work with the `chainlink` price feed and our `favorites` contract by creating a `favorites_factory` contract that can interact with deployed `favorites` contracts. We'll specifically be looking at the `staticcall` and `extcall` function calls, and how to use them to read and write data, respectively. + +First, let's create a new external function called `store_from_factory`. This function will take two inputs: `favorites_index` of type `uint256` and `new_number` of type `uint256`. These inputs represent the index of a deployed `favorites` contract and the new value that we want to store within it. We can then use the `extcall` method to execute the `store` function on the desired `favorites` contract. + +```python +def store_from_factory(favorites_index: uint256, new_number: uint256): + favorites_contract: address = self.list_of_favorite_contracts[favorites_index] + favorites_contract.store(new_number) +``` + +Let's break down the code above: + +- First, we access the address of the `favorites` contract corresponding to the provided `favorites_index` using the `list_of_favorite_contracts` array. +- Then, we call the `store` function on the `favorites_contract`, passing in the `new_number` value. + +Using the `extcall` method allows us to modify the state of the `favorites` contract. This differs from using `staticcall`, which only reads data and doesn't change the state of the contract. + +```python +favorites_contract.staticcall(store(new_number)) +``` + +The code above demonstrates how we can use `staticcall` to read the current value of `my_favorite_number` on our deployed `favorites` contract. + +We can then use `extcall` to call the `store` function on this contract, setting a new value for `my_favorite_number`. + +By using `extcall` and `staticcall`, we have greater control over how our `favorites_factory` contract interacts with deployed `favorites` contracts, enabling both reading and writing of data. diff --git a/courses/moccasin-101/5-mox-five/5-vyi/+page.md b/courses/moccasin-101/5-mox-five/5-vyi/+page.md new file mode 100644 index 000000000..ab0cdd272 --- /dev/null +++ b/courses/moccasin-101/5-mox-five/5-vyi/+page.md @@ -0,0 +1,55 @@ +## File Interfaces + +We will learn about file interfaces in Vyper, which are a more efficient way of dealing with interfaces. You may be asking "What's wrong with the inline interfaces?" And that's a fair question. The inline interfaces are perfectly valid and there is nothing wrong with using them. However, there are some reasons why using a file interface can be a better approach. + +One reason to use a file interface is that if we have a contract with a lot of functions, for example an interface with 50 functions, the inline interface would be very long and visually clunky. + +We can create a file interface in a separate file and import it into our main contract file. For example, we will make a file interface for our *favorites* contract. + +We can start by creating a new folder and then a new file in that folder. + +```bash +mkdir interfaces +touch interfaces/i_favorites.vyi +``` + +We like to use an 'i_' prefix for our interface files, but this is not a requirement of Vyper. + +Now, the interface that we wrote earlier was an inline interface. An inline interface is written directly within our contract file, for example: + +```vyper +interface i_favorites: + def store(new_number: uint256): nonpayable + def retrieve() -> uint256: view +``` + +And the file interface that we just created is written in a separate file. + +Now, in our factory file, we can go ahead and import our *i_favorites* file interface. We can do this by using a similar syntax to Python. + +```vyper +from interfaces import i_favorites +``` + +We don't need to include the '.vyi' extension after the *i_favorites* import, as Vyper knows that it is a file interface. + +So, this one line of code is equivalent to writing the interface directly inline. + +Now, we can actually call our interface in our factory file. Let's go ahead and do that. + +```vyper +favorites_contract: i_favorites = i_favorites(self.list_of_favorite_contracts[favorites_index]) +favorites_contract.store(new_number) +``` + +We can test our code by running: + +```bash +mox compile +``` + +It should compile successfully. + +We have now created our first file interface in Vyper. We are going to go ahead and tweak one more piece of our factory contract. + + courses\moccasin-101\5-mox-five\5-vyi\+page.md diff --git a/courses/moccasin-101/5-mox-five/6-idk/+page.md b/courses/moccasin-101/5-mox-five/6-idk/+page.md new file mode 100644 index 000000000..089750b9e --- /dev/null +++ b/courses/moccasin-101/5-mox-five/6-idk/+page.md @@ -0,0 +1,51 @@ +## Deploy Script + +We're going to write a deploy script and test this out. + +First, we'll create a new file in our `script` directory named `deploy.py`. + +We'll create a function called `deploy_favorites()`. This function will deploy our `favorites` contract. We can grab our contract with `from src import favorites`. Next, we'll import `VyperContract` so we can assign a type to our deployed contract: + +```python +from src import favorites +from moccasin.boa_tools import VyperContract +``` + +Let's finish the function: + +```python +def deploy_favorites() -> VyperContract: + favorites_contract: VyperContract = favorites.deploy() + return favorites_contract +``` + +Next, we'll create a function called `deploy_factory()` that will take a `favorites_contract` and deploy our `favorites_factory`: + +```python +def deploy_factory(favorites_contract: VyperContract): + favorites_factory = favorites_factory.deploy(favorites_contract.address) + favorites_factory.create_favorites_contract() +``` + +To make sure our `favorites_factory` is deployed with the address of our initial `favorites` contract, we'll use the `@` function in our deploy script to grab the address: + +```python +new_favorites_address: str = favorites_factory.list_of_favorite_contracts[0] +new_favorites_contract: VyperContract = favorites.at(new_favorites_address) +print(f"Stored value is: {new_favorites_contract.retrieve()}") +``` + +We can now create our `mocassin_main()` function and run it in our terminal: + +```python +def moccasin_main(): + favorites_contract = deploy_favorites() + deploy_factory(favorites_contract) + +``` + +```bash +mox run deploy +``` + +This will deploy our contracts, and we can see the results in the terminal. diff --git a/courses/moccasin-101/5-mox-five/7-extcall/+page.md b/courses/moccasin-101/5-mox-five/7-extcall/+page.md new file mode 100644 index 000000000..730264597 --- /dev/null +++ b/courses/moccasin-101/5-mox-five/7-extcall/+page.md @@ -0,0 +1,43 @@ +## Cross-Contract Interactions (Extcall) + +In this lesson, we are going to learn about interacting with other contracts. + +We begin with a Python script. + +```python +from src import favorites, favorites_factory +from moccasin.boa.tools import VyperContract + +def deploy_favorites() -> VyperContract: + favorites_contract: VyperContract = favorites.deploy() + return favorites_contract + +def deploy_factory(favorites_contract: VyperContract): + factory_contract: VyperContract = favorites_factory.deploy(favorites_contract_address) + factory_contract.create_favorites(1) + new_favorites_address: str = factory_contract.list_of_favorite_contracts(0) + new_favorites_contract: VyperContract = favorites.at(new_favorites_address) + print(f"Stored value is: {new_favorites_contract.retrieve()}") + factory_contract.store_from_factory(0, 88) + print(f"New contract stored value is: {new_favorites_contract.retrieve()}") + print(f"Factory stored value is: {factory_contract.retrieve()}") + +def moccasin_main(): + favorites_contract = deploy_favorites() + deploy_factory(favorites_contract) + +moccasin_main() +``` + +We will run the code in a terminal. + +```bash +patrick@cu:mox-five-more-cu $ & mox run deploy +Running run command... +Stored value is: 77 +New contract stored value is 88 +Original contract stored value is 7 +patrick@cu:mox-five-more-cu $ +``` + +We can use this to show how calling a function from a factory contract can affect the state of another contract. We can update the value of `favorites.vy` by calling the `store()` function from the `favorites_factory.vy` contract. We can also call `retrieve()` on the `favorites.vy` contract to see the stored value. diff --git a/courses/moccasin-101/5-mox-five/8-store-contract-by-interface/+page.md b/courses/moccasin-101/5-mox-five/8-store-contract-by-interface/+page.md new file mode 100644 index 000000000..fad37f067 --- /dev/null +++ b/courses/moccasin-101/5-mox-five/8-store-contract-by-interface/+page.md @@ -0,0 +1,40 @@ +## Storing Contracts By Interface Type + +We're going to do a little bit of refactoring here, just to show you something cool that you can do. + +This `i_favorites` interface, we can also store contracts not just by address, but we could store them as type `i_favorites`. + +So, what we can do is instead of storing this as an address, we can store these as this `i_favorites` type. + +If we do that, we're actually going to get to save an extra line of code. + +Down here, when we call `self.list_of_favorite_contracts` at the favorites index, it's not going to give me an address anymore, it's going to give me an `i_favorites`. + +So, what we can do is we can say, instead of `favorites_address`, we can do `favorites_contract` of type `i_favorites` and then just delete that next line. + +Additionally, when we do `self.list_of_favorite_contracts.append`, we would have to wrap our `new_favorites_contract` as type `i_favorites`. + +We don't have to do this. If what we just showed you is actually very confusing, feel free to just move it back to what it was. Which is this one right here. Feel free to just move it back to what it was, and go with that. Whatever is easier for you, this is your learning journey, and you can come back to this later and go, "Okay, what did Patrick mean by that? Let me ask some AIs, etc., etc." + +This is another way that we can actually store and work with these variables. + +```python +from interfaces import i_favorites + +list_of_favorite_contracts: publicDynArray[i_favorites, 100] +original_favorite_contract: address + +@deploy +def __init__(original_favorite_contract: address): + self.original_favorite_contract = original_favorite_contract + +@external +def create_favorites_contract(): + new_favorites_contract: address = create_copy_of(self.original_favorite_contract) + self.list_of_favorite_contracts.append(new_favorites_contract) + +@external +def store_from_factory(favorites_index: uint256, new_number: uint256): + favorites_contract: i_favorites = self.list_of_favorite_contracts[favorites_index] + favorites_contract.store(new_number) +``` \ No newline at end of file diff --git a/courses/moccasin-101/5-mox-five/9-method-chaining/+page.md b/courses/moccasin-101/5-mox-five/9-method-chaining/+page.md new file mode 100644 index 000000000..cd9b72c02 --- /dev/null +++ b/courses/moccasin-101/5-mox-five/9-method-chaining/+page.md @@ -0,0 +1,38 @@ +In this lesson, we will continue working with the `favorites` contract. + +We will introduce a `view` function and we'll look at some more advanced concepts, like chaining several lines of code together to perform a single action. + +First, let's add a `view` function. + +```python +@view +def view_from_factory(favorites_index: uint256) -> uint256: + favorites_contract: IFavorites = self.list_of_favorite_contracts[favorites_index] + return extcall(favorites_contract.retrieve()) +``` + +We can call this function using `extcall` but, because it's a `view` function, we'll use `staticcall`. + +```python +values: uint256 = staticcall(favorites_contract.retrieve()) +``` + +This line of code is equivalent to the following three lines: + +```python +favorites_contract: IFavorites = self.list_of_favorite_contracts[favorites_index] +values: uint256 = extcall(favorites_contract.retrieve()) +return values +``` + +Whether you prefer to use the single line or the three separate lines is up to you, but both achieve the same result. + +This is an example of method chaining. We're chaining several lines of code together to perform a single action. We can also chain the calls to `staticcall` and `retrieve`. + +```python +values: uint256 = staticcall(self.list_of_favorite_contracts[favorites_index].retrieve()) +``` + +This line of code will first access the `favorites_contract` from the list, then call the `retrieve` function on it, and finally return the value. + +The use of method chaining can help to make your code more concise and readable. It's a powerful technique that you can use in your own Vyper contracts. diff --git a/courses/moccasin-101/6-mox-coffee/1-intro/+page.md b/courses/moccasin-101/6-mox-coffee/1-intro/+page.md new file mode 100644 index 000000000..b7c5ed6de --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/1-intro/+page.md @@ -0,0 +1,45 @@ +## Section: Moccasin: Buy Me A Coffee + +In this section, we are going to make our Buy Me A Coffee project very professional. + +We are going to use the GitHub repo associated with the project to find all the code. + +We are going to make some changes to the code, including refactoring, modules and imports. The biggest thing that we are going to improve on is our deployment process and our tests. + +We will write unit tests, staging tests and integration tests. + +We are going to learn what deployments.db does and what storage does. + +We are going to learn about a powerful moccasin feature called manifest named, and how it makes testing easier. + +Finally, we are going to push our code up to GitHub, which will be the start of your portfolio. + +Now let's look at the tests. + +We can run our tests with the following command: + +```bash +mox test +``` + +We can also run coverage tests with: + +```bash +mox test --coverage +``` + +We can also run tests on a fork of the Sepolia testnet with: + +```bash +mox test --network sepolia --fork +``` + +We can also run staging tests. + +We are going to learn how to write very defensible code. One mistake can mean the end of millions of dollars. + +In the smart contract world, you need to be able to write very defensible code. + +We are going to learn about all the cool features of moccasin, and you are going to become a better Python and Vyper developer. + +We are going to push this code up to GitHub so you can start building your portfolio. diff --git a/courses/moccasin-101/6-mox-coffee/10-kinds/+page.md b/courses/moccasin-101/6-mox-coffee/10-kinds/+page.md new file mode 100644 index 000000000..6dc6f028b --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/10-kinds/+page.md @@ -0,0 +1,17 @@ +## Kinds of Tests + +We're back in the GitHub repo associated with this course in section with Moccasin Buy Me A Coffee. + +We have several, several, different types of tests. + +The test that we've gone over so far are something called unit tests. And these basically test a single function or part of your code. + +The next step up from that is usually integration tests. And this is testing different systems together or different contracts together or basically the definition can be a little bit loose depending on how you define it. + +Then, the step up from that would be a staging test. And these are testing your code in a production-like environment. + +Now, one of the tests kind of unique to blockchain is going to be this type of forked test. Although, you could probably categorize these as staging tests as well. And this is essentially where you deploy your contracts to a fake virtual network or a forked network and call the different functions on it. We've kind of done something like this with tenderly's virtual network, but we're also going to do it with alchemy here as well. And these fake networks will simulate what it's like to actually work with the real blockchain. + +Then, there's fuzz and formal verification. Now, fuzzing is incredibly important, and you should 100% learn how to fuzz. And then, formal verification is when you mathematically prove different functions. + +So, for us, we are going to do all of these types of tests for this project. And then, pretty soon once you get to the stablecoin portion, we are going to add fuzz testing as well. diff --git a/courses/moccasin-101/6-mox-coffee/11-unit/+page.md b/courses/moccasin-101/6-mox-coffee/11-unit/+page.md new file mode 100644 index 000000000..7d61be4cb --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/11-unit/+page.md @@ -0,0 +1,71 @@ +## Unit Tests + +We'll start with the simplest kind of test, unit tests. We'll test a function and see if it works. We can create a new folder called **unit** to hold these tests. + +We'll need to create a file called **conftest.py** (make sure this is in your **tests** folder, not in your **unit** folder) to hold our fixtures. + +```python +import pytest + +@pytest.fixture +def coffee(): + return deploy_coffee() +``` + +Here, we import the `pytest` library and create a function called `coffee` that returns the result of calling our deploy script. + +Next, we need to create a file called **test_unit_coffee.py** in our **unit** folder to write our actual tests. + +```python +from eth_utils import to_wei + +def test_price_feed_is_correct(coffee, eth_usd): + assert coffee.PRICE_FEED() == eth_usd.address + +def test_starting_values(coffee, account): + assert coffee.MINIMUM_USD() == to_wei(5, "ether") + assert coffee.OWNER() == account.address +``` + +Here, we import the `to_wei` function from `eth_utils`, then we create two tests. The first checks if the price feed address is correct. The second tests the starting values of the coffee contract. + +We'll run our tests using the command: + +```bash +mox test +``` + +We'll need to make sure to import the `get_active_network` function in **conftest.py**, as well as create a fixture for our account. + +```python +from moccasin.config import get_active_network + +@pytest.fixture(scope="session") +def account(): + return get_active_network().get_default_account() +``` + +Now, we've created a `get_active_network` fixture that will always return the same default account for each test, ensuring consistency. + +We've also added a `to_wei` function to test the minimum USD value in Wei. + +If we run our tests again using the command: + +```bash +mox test +``` + +We'll see that our tests are all passing. + +## TAG: add a code block to show the deployment script + +We've covered how to write unit tests for our coffee contract, ensuring the price feed and starting values are correct. Now, let's test the owner of the contract. + +Let's create a new test: + +```python +def test_owner_is_account(coffee, account): + assert coffee.OWNER() == account.address +``` + +We use the `account` fixture we created in our **conftest.py** to make sure the owner of the contract is the same as the account that deployed it. diff --git a/courses/moccasin-101/6-mox-coffee/12-test-revert/+page.md b/courses/moccasin-101/6-mox-coffee/12-test-revert/+page.md new file mode 100644 index 000000000..e8bd88d54 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/12-test-revert/+page.md @@ -0,0 +1,35 @@ +## Testing for Reverts + +We will learn how to test for reverts in our smart contracts. + +When we test our smart contracts, we also want to test that they revert correctly. + +For example, if somebody calls our fund function without enough money, we should expect this to actually revert. So, how do we test when a function reverts? + +We can use the `boa.reverts` method. We can add this code to our test file: + +```python +def test_fund_fails_without_enough_eth(coffee): + with boa.reverts(): + coffee.fund() +``` + +This will test that the `fund` function reverts when called without enough ETH. + +We can even check the exact revert message. We can add this code to our test file: + +```python +def test_fund_fails_without_enough_eth(coffee): + with boa.reverts("You must spend more ETH!"): + coffee.fund() +``` + +This will test that the `fund` function reverts with the message "You must spend more ETH!". + +To run a specific test, we can use the following command: + +```bash +mox test -k test_fund_fails_without_enough_eth +``` + +This will only run the test called `test_fund_fails_without_enough_eth`. diff --git a/courses/moccasin-101/6-mox-coffee/13-set-balance/+page.md b/courses/moccasin-101/6-mox-coffee/13-set-balance/+page.md new file mode 100644 index 000000000..bb4758fe5 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/13-set-balance/+page.md @@ -0,0 +1,34 @@ +## Set balance in MochaSin and TitanoBoa + +We are going to discuss some cheat codes in MochaSin and TitanoBoa. These codes will allow us to set the balance of a user and also pretend or prank a user. This is important if we want to test how other users with different amounts of money interact with our contract. + +We are going to call our `fund` function and actually send some money, but we need to make sure that whoever calls this `fund` function actually has money to do so. This is where we can start getting into the concept of cheat codes or functions that work with the PyEVM or local forked network in order to set up our tests better. + +For example, if we want to call `fund`, we'll need some money to actually have money. So, we can add this test: + +```python +def test_fund_with_money(coffee, account): + account.transfer(coffee.address, to_wei(5, "ether")) + assert coffee.balance() == to_wei(5, "ether") + boa.env.set_balance(account.address, SEND_VALUE) + coffee.fund(value=SEND_VALUE) + # Assert + amount_funded = coffee.funders(0) + assert amount_funded == SEND_VALUE +``` + +We are going to call the `fund` function with money. In order to do this, we'll need to send some money to the account we want to interact with. Then, we can set the balance of the account with the `SEND_VALUE` amount. We can then go ahead and run this test: + +```bash +mox test +``` + +Or, we can run a specific test with: + +```bash +mox test -k test_fund_with_money +``` + +We are testing some basic functionality here, and I'm going to leave a lot of time for you to actually write a whole bunch of tests because you should get incredibly good at writing tests. AI's are very helpful at writing tests, and you should use AI's to help you write your tests. But, sometimes, AI's screw it up, and what's worse is if you write a test or AI writes a test, and it doesn't test what you want it to test, and it breaks. That will be very bad. + +So, let's keep going, and I'll leave you some time to write tests afterwards. diff --git a/courses/moccasin-101/6-mox-coffee/14-prank/+page.md b/courses/moccasin-101/6-mox-coffee/14-prank/+page.md new file mode 100644 index 000000000..f8175b517 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/14-prank/+page.md @@ -0,0 +1,63 @@ +## Pranking and Impersonating in Solidity Tests + +Sometimes when we're writing tests, we want to pretend to be somebody else, or we want to prank. Our account, the default account, is going to be who deployed this address. But we should probably check that the non-owner cannot withdraw. + +We will need to get some other random user. We can assign a value to `RANDOM_USER`: + +```python +RANDOM_USER = boa.env.generate_address('non-owner') +``` + +Now we can write a test called `test_non_owner_cannot_withdraw`: + +```python +def test_non_owner_cannot_withdraw(coffee, account): + # Arrange + boa.env.set_balance(account.address, SEND_VALUE) + coffee.fund(value=SEND_VALUE) + # Act + with boa.prank(RANDOM_USER): + boa.reverts('You are not the owner!', + coffee.withdraw()) +``` + +We are arranging by funding the account. Then, we are pranking a random user, and we are reverting. Let's test it out with a terminal command: + +```bash +mox test +``` + +This is how you pretend to be some other user. This is how you set the current user to somebody else random. And this is how you revert. Let's keep going. + +We will check to see if it doesn't work. Let's also check to see how it does work. We can do a test called `test_owner_can_withdraw`. + +```python +def test_owner_can_withdraw(coffee, account): + # Arrange + boa.env.set_balance(account.address, SEND_VALUE) + coffee.fund(value=SEND_VALUE) + # Act + with boa.env.prank(coffee.OWNER): + coffee.withdraw() + # Assert + assert coffee.funders() == boa.env.get_balance(coffee.OWNER) == 0 +``` + +Usually, just to make sure, I like to also prank that I am actually the owner here. So, I might do something like `boa.env.set_balance` and then `coffee.OWNER`. + +We then do `coffee.fund(value=SEND_VALUE)`. Finally, we assert that `boa.env.get_balance(coffee.address)` is equal to zero. + +Let's test this out. + +```bash +mox test +``` + +We ran into an error "unsupported type". Let's scroll up and see what line it's mad at me about. It looks like it's on the `assert` line about getting the balance of `coffee.address`. + +Let's change the `assert` line, to instead be: +```python +assert coffee.funders() == 0 +``` + +Let's run `mox test` again and see if it passes. diff --git a/courses/moccasin-101/6-mox-coffee/15-mid-sec-work/+page.md b/courses/moccasin-101/6-mox-coffee/15-mid-sec-work/+page.md new file mode 100644 index 000000000..45a17f13f --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/15-mid-sec-work/+page.md @@ -0,0 +1,42 @@ +## Mid-Section Workshop + +We've done a little bit of refactoring to make our code more efficient. We're also starting to do some interesting things with our tests, and we want you to write one of the last tests we'll need before we move on to some more advanced topics. + +We're going to do Workshop 1. We want you to write a test that: + +* funds the contract with 10 different funders +* withdraws the funds using the owner +* asserts that: + * the ending balance of `buy_me_a_coffee` is 0 + * the ending balance of the owner is the addition of all the amounts the funders added + +This might sound simple, but there are some tricky parts to it. We've seen how to use `boa.env.generate_address()` to make a random address to use as a funder, so try making 10 of them. For bonus points, see if you can write some code that makes it a bit easier to make a bunch of these addresses at once, rather than just copy-pasting the code 10 times. + +To withdraw the funds, we'll have to use the `prank` function to pretend we are the owner. We've done this in earlier lessons, so this should be a review. + +Finally, we'll want to use the `boa.env.get_balance()` function to check the ending balance of both the contract and the owner. We've also seen this function in earlier lessons, so this part should be familiar. + +As always, spend at most 25 minutes on this workshop. If you're stuck, take a break and ask for help or look at the solution on GitHub. + +```python +RANDOM_USER = boa.env.generate_address("non-owner") +``` + +```python +def test_fund_with_money(coffee, account): + # Arrange + boa.env.set_balance(account.address, SEND_VALUE) + # Act + coffee.fund(value=SEND_VALUE) + # Asset + funder = coffee.funders(0) + assert funder == account.address + assert coffee.funder_to_amount(funder) == SEND_VALUE +``` + +```python +def test_owner_can_withdraw(coffee, funded): + with boa.env.prank(coffee.OWNER()): + coffee_funded.withdraw() + assert boa.env.get_balance(coffee_funded.address) == 0 +``` \ No newline at end of file diff --git a/courses/moccasin-101/6-mox-coffee/16-more-fixtures/+page.md b/courses/moccasin-101/6-mox-coffee/16-more-fixtures/+page.md new file mode 100644 index 000000000..726722802 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/16-more-fixtures/+page.md @@ -0,0 +1,87 @@ +## More Fixtures + +We can create a fixture to reduce the code duplication in our tests. + +We can use a `pytest.fixture` with a `scope="function"` to create a fixture that runs once per test function. Here's how we can write a new fixture called `coffee_funded`: + +```python +@pytest.fixture(scope="function") +def coffee_funded(coffee, account): + boa.env.set_balance(account.address, SEND_VALUE) + coffee.fund(value=SEND_VALUE) + return coffee +``` + +We can then use this fixture in our tests. For example, in the `test_non_owner_cannot_withdraw` test, instead of setting the balance of the account and funding the contract, we can now use the `coffee_funded` fixture: + +```python +def test_non_owner_cannot_withdraw(coffee_funded, account): + # Arrange + with boa.env.prank(RANDOM_USER): + with boa.reverts("Not the contract owner!"): + coffee_funded.withdraw() +``` + +We can also update the `test_owner_can_withdraw` test: + +```python +def test_owner_can_withdraw(coffee_funded, account): + with boa.env.prank(coffee_funded.OWNER): + coffee_funded.withdraw() + assert boa.env.get_balance(coffee_funded.address) == 0 +``` + +We'll also add a new test to make sure we can withdraw from the contract: + +```python +def test_fund_with_money(coffee_funded, account): + # Arrange + boa.env.set_balance(account.address, SEND_VALUE) + + # Act + coffee_funded.fund(value=SEND_VALUE) + + # Assert + funder = coffee_funded.funders(0) + assert funder == account.address + assert coffee_funded.to_amount_funded(funder) == SEND_VALUE +``` + +And lastly, we can update the `test_fund_with_money` test as well: + +```python +def test_fund_with_money(coffee_funded, account): + # Arrange + boa.env.set_balance(account.address, SEND_VALUE) + + # Act + coffee_funded.fund(value=SEND_VALUE) + + # Assert + funder = coffee_funded.funders(0) + assert funder == account.address + assert coffee_funded.to_amount_funded(funder) == SEND_VALUE +``` + +We'll make sure to import `boa` and `SEND_VALUE` in our test file. +```python +from eth_utils import to_wei +import boa +from tests.conftest import SEND_VALUE +``` + +Now we can run our tests: + +```bash +mox test +``` + +We'll have to update our `conftest.py` file to add our new `SEND_VALUE` variable: + +```python +SEND_VALUE = to_wei(1, "ether") +``` + +Running our tests again will pass. + +Our tests now reuse code by using the `coffee_funded` fixture, which helps us avoid duplicating code in our tests and makes them more readable. diff --git a/courses/moccasin-101/6-mox-coffee/17-test-coverage/+page.md b/courses/moccasin-101/6-mox-coffee/17-test-coverage/+page.md new file mode 100644 index 000000000..bd64019be --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/17-test-coverage/+page.md @@ -0,0 +1,34 @@ +## Test Coverage + +We've been writing tests for our smart contracts. You might be thinking, "Hey, I've done a pretty good job testing all of my contracts. I'm content with the amount I've tested." This is where the concept of **coverage** comes into play. + +Moccasin comes with support for a tool called **pytest-cov**. It's a way to test how much of our code has actually been tested. We can install **pytest-cov** using this command: + +```bash +pip install pytest-cov +``` + +Let's say we want to test how much of our current smart contract has been tested. We can run the following command: + +```bash +mox test --coverage +``` + +This command will run all of our tests and compare it to how much of our contracts we've actually tested. + +The output of the command will show us exactly which lines of code haven't been tested. For example, we might see that lines 64-69 of our smart contract have not been tested. + +To see the exact lines of code that we've missed, we can run this command: + +```bash +mox test --coverage --cov-report term-missing +``` + +Now, we can go into the lines of code that haven't been tested and add some test cases for those. For example, if lines 64-69 are our `get_eth_to_usd_rate` function, we might add a test case for that like so: + +```python +def test_get_rate(coffee): + assert coffee.get_eth_to_usd_rate(SEND_VALUE) > 0 +``` + +We should get a value that is greater than zero, because we are sending one whole ETH. Now if we run our test coverage command again, we should see that the lines 64-69 are now covered. diff --git a/courses/moccasin-101/6-mox-coffee/18-gas-profile/+page.md b/courses/moccasin-101/6-mox-coffee/18-gas-profile/+page.md new file mode 100644 index 000000000..dac0b48dd --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/18-gas-profile/+page.md @@ -0,0 +1,41 @@ +## Gas Profiling + +Let's talk about gas profiling. We'll start by looking at the differences in gas cost between storage variables and constant variables. + +So, to do some testing, let's go ahead and run a command in our terminal: + +```bash +mox test --help +``` + +We can see in the help output there is a `gas-profile` command here where we can get an output on gas use for all of our test functions. So, if I just run: + +```bash +mox test --gas-profile +``` + +This will give us a large output showing how much gas these functions cost. + +This output is kind of borderline impossible to read. So what we might do is run this again with a little zoom way the heck out. Now, I'm going to run this command, and zoom way back in. We can see things a little bit better now. + +So, we have count, mean, median, standard deviation, min, and max. If we scroll down into this section here and scroll over, we can see how much calling each one of these functions costs, including the functions up here, the different lines, and, of course, our different functions. + +Down here, we can see, first off the count, which is how many times in the test, or the test suite, it was called. Then, we have the average gas cost, the median gas cost, standard deviation, the minimum, and the maximum. And, it's literally for like every line of computation here, which is kind of crazy. + +Okay, raw call owner. Let's look at this one. This one was 19 gas on average. Okay, cool. + +So, now let's go back here. Let's do a little refactoring. You don't have to do this, by the way. + +Let's get rid of it being immutable. So, the owner is no longer immutable. So, we'll do `self.owner` here. Any place we see owner, where else is owner? Owner, `self.owner`, we'll do `self.owner` here. + +Okay, great. Remember, this was 19 gas when it was a constant or, excuse me, 19 gas when it was immutable. + +Let's rerun this again to `gas.txt`: + +```bash +mox test --gas-profile > gas.txt +``` + +Now, we're using `self.owner`. Oh, I got to Sorry, I got to zoom way out and run this. Zoom zoom zoom up up. Run it. Zoom way back in. Now, let's see. Now it costs more. Now it's 21 here. + +So, we can see that we actually ended up spending more gas by having it be a storage variable. So I'm actually going to move it back to immutable. So, if you want to check how much more or less gas your contract uses, you can use that `gas-profile` command. diff --git a/courses/moccasin-101/6-mox-coffee/19-fork-tests/+page.md b/courses/moccasin-101/6-mox-coffee/19-fork-tests/+page.md new file mode 100644 index 000000000..e8f72aa72 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/19-fork-tests/+page.md @@ -0,0 +1,29 @@ +## Fork Tests + +We've written some unit tests and learned a little more about gas. We could write an integration test, but we've been kind of doing that with our unit tests here, so I'm going to skip it for now. But, we haven't done the staging tests and we also haven't done the forked tests. So, let's go ahead and create the staging folder, but let's do the fork tests, because technically the fork tests are a type of staging tests. + +Now, what are fork tests? Now, fork tests are going to be very similar to us just working with the tenderly virtual network. So, our tenderly virtual network is an example of a forked network. So, what really is a forked chain, cuz I've kind of been dancing around it a little bit? Well, so, imagine there's a real blockchain, right? A real blockchain is happening and blocks are being added, and people are sending transactions, and everything. Well, imagine we, on the side, what we do is we spin up our little fake anvil chain. We spin up a fake much tinier little chain. Now, the idea behind a forked blockchain is that it will resemble everything that's on a real blockchain, but in an easier way. So, if in my python code, I'm connected to this forked blockchain, but I call `price_feed.latestAnswer`... Well, our forked blockchain is empty, it doesn't have this price feed on it. Well, the real blockchain does have the price feed, right? It does have this price feed contract. So, since we're connected to kind of this forked empty little thing, what happens is we say hey, we want the latest answer or fake chain. What it does is it makes an API call to the real chain. Says hey, I need this price feed. I need this contract. And what it does is kind of at request time, it'll grab that price feed and give it to itself. Give it to itself and it'll like, oh yeah, I had this the whole time. They'll go, oh, I had this the whole time. And then, we will be able to call it and interact with it as if we were interacting with the real blockchain. This allows us to quote-unquote have the whole blockchain, but only really when we request something about the real blockchain. This is also incredibly helpful for us to have this forked blockchain because then, we can do something like `buy_me_a_coffee.deploy` and we'll deploy it to this forked faked blockchain, which kind of has all the real contracts that are on the actual real blockchain. Because any time any of these contracts need the data from a real contract, our forked blockchain will just go ahead and grab it and copy it to itself essentially. So, forked blockchains are incredibly helpful because they allow us to simulate a real blockchain without actually having the whole blockchain, but for our from our perspectives, we have the whole blockchain at our disposal, which is really really nice. And forked tests and running forked tests therefore are one of the most important steps whenever you're going to try to deploy a smart contract because you want to see hey, what does my test look like compared up against a real blockchain with the real contracts, because my mocks could have been wrong. I could have screwed these up, right? So, we always want to run what's called forked tests. + +Now, these are actually really easy for us to run in Moccasin. All we have to do is in our `Moccasin.toml` we have that real network set up and then what we can do is we can just do: +```bash +mox test --network sepolia --fork +``` + +Let's say we wanted to test our smart contracts against the real sepolia test net. But, then all we have to do is `--fork`. This tells our test suite to use the sepolia testnet but run it as a fork. Don't run it, don't actually deploy any contracts, don't actually do anything. And, if we hit enter, we do have to enter our password for the default key, because we did set the default account name to default. So, we do have to do this. You could have, we could have not done that and it would have worked a little bit easier, but anyways, let's enter the password. You'll notice it didn't prompt us to say hey you're sending a real transaction cuz it's smart enough to know that we're not. + +Now, this will take a little bit longer to run these tests because we're actually making API calls to our sepolia RPC URL. And, I just realized that our active network does indeed have an explorer, but we need to do: +```python +if active_network.has_explorer() and active_network.is_local_or_forked_network() is False: +``` + +So, if we don't have this line, when we go to run our fork tests, this is going to try to kick off and we don't want to verify to nowhereville. So yeah. So, now that we've updated this, if `active_network.has_explorer()`, and `active_network.is_local_or_forked_network()` is false: +```python +result = active_network.moccasin_verify(coffee) +``` + +Now, we can run: +```bash +mox test --network sepolia --fork +``` + +And we see that everything passes. So, this is additionally why it's so important to do these conf tests as well. diff --git a/courses/moccasin-101/6-mox-coffee/2-setup/+page.md b/courses/moccasin-101/6-mox-coffee/2-setup/+page.md new file mode 100644 index 000000000..101d663d0 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/2-setup/+page.md @@ -0,0 +1,124 @@ +## Project Setup + +We're back in our `mox-cu` folder. Let's do a little `ls` to see what we've built so far: + +```bash +ls +``` + +We're building up a nice little folder here filled with demo projects that we're using to level up our career. Let's go ahead and make the next folder. We'll do `mkdir` and name this new folder `mox-buy-me-a-coffee-cu`. + +```bash +mkdir mox-buy-me-a-coffee-cu +``` + +Then, we'll `cd` into that folder. + +```bash +cd mox-buy-me-a-coffee-cu +``` + +Now, we can open this folder in our code editor. We'll do `code .`. + +```bash +code . +``` + +We already know that we have `mox --help` and `mox` installed. We're going to initialize our new project using `mox init`. We want to use VS Code, so we'll pass in the flag `--vscode`, and we'll use a `pyproject` file, so we'll pass in the `--pyproject` flag as well: + +```bash +mox init --vscode --pyproject +``` + +Now, we'll delete the files that were created in this directory so we can start from a blank project. We can just right-click, then click "delete" and "move to trash". + +To get our starting code for this project, we'll use the code from the "Buy Me A Coffee" section in the Remix Fundamentals video. We'll go over to our GitHub repository, open `remix-buy-me-a-coffee-cu` and then open the `buy-me-a-coffee.vy` file. We're going to bring this code into our `mox-buy-me-a-coffee-cu` directory. We can copy it and paste it into a new file. We can right-click, select "New File", and name it `buy-me-a-coffee.vy`: + +```python +# pragma version ^0.4.0 +""" +@license MIT +@title Buy Me A Coffee! +@author You! +@notice This contract is for creating a sample funding contract + +We'll learn a new way to do interfaces later... +""" +interface AggregatorV3Interface: + def decimals() -> uint256: view + def description() -> String[1000]: view + def version() -> uint256: view + def latestAnswer() -> int256: view + +# Constants & Immutables +MINIMUM_USD: public(constant(uint256)) = wei_value(5, "ether") +PRICE_FEED: public(immutable(AggregatorV3Interface)) = # 0x694AA1769357215DE4FAC081bf1f309c33Dc4536 sepolia +OWNER: public(immutable(address)) = +PRECISION: constant(uint256) = 1 * (10 ** 18) + +# Storage +funders: public(DynArray[address, 1000]) +funder_to_amount_funded: public(HashMap[address, uint256]) + +with constants: 262, 853 + +@deploy +def init(price_feed: address): + PRICE_FEED = AggregatorV3Interface(price_feed) + OWNER = msg.sender + +@external +@payable +def fund(): + """Allows users to send $ to this contract + Have a minimum amount to send + How do we convert the ETH amount to dollars amount? + """ + usd_value_of_eth: uint256 = self.get_eth_to_usd_rate(msg.value) + assert usd_value_of_eth >= MINIMUM_USD, "You must spend more ETH!" + self.funders.append(msg.sender) + self.funder_to_amount_funded[msg.sender] += msg.value + +@external +def withdraw(): + """Take the money out of the contract, that people sent via the fund function. + How do we make sure only we can pull the money out? + """ + assert msg.sender == OWNER, "Not the contract owner!" + raw_call(OWNER, b"", value = self.balance) + # send(OWNER, self.balance) + # resetting + for funder: address in self.funders: + self.funder_to_amount_funded[funder] = 0 + self.funders = [] + +@internal +@view +def _get_eth_to_usd_rate(eth_amount: uint256) -> uint256: + """Chris sent us 0.01 ETH for us to buy a coffee + Is that more or less than $5? + """ + price: int256 = staticcall(PRICE_FEED.latestAnswer()) + eth_price: uint256 = convert(price, price * eth_amount) * (10 ** 10) + eth_amount_in_usd: uint256 = (eth_price * eth_amount) // PRECISION + return eth_amount_in_usd # 18 '0's, 18 decimal places + +@external +@view +def get_eth_to_usd_rate(eth_amount: uint256) -> uint256: + return self._get_eth_to_usd_rate(eth_amount) + +@external +@payable +def default(): + self.fund() + +``` + +Now, we'll compile the code. We'll use `mox compile` to do that. + +```bash +mox compile +``` + +We have successfully compiled the contract. diff --git a/courses/moccasin-101/6-mox-coffee/20-staging/+page.md b/courses/moccasin-101/6-mox-coffee/20-staging/+page.md new file mode 100644 index 000000000..c16615268 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/20-staging/+page.md @@ -0,0 +1,51 @@ +## Staging Tests + +We're going to be working with staging tests. Fork tests are technically a type of staging test. But we're going to be working with a type of test where we can actually deploy the smart contracts to a testnet or even to a mainnet. + +A set of tests you may want to run on an actual network is to make sure that the network does what you want it to do. + +We can also signify staging tests to run on real networks. To learn more about this, we can scroll down to staging markers in the Moccasin documentation. + +A lot of developers should run some sanity checks on a live network. For example, if you're working with oracles or any setup tricks. But, a lot of people don't. + +In order to run a test on an actual testnet, we have to mark it with a staging `pytest` marker. + +We can tell Moccasin that it can run the test, deploy the code, and send transactions. If we don't have a staging test, the tests should skip it. + +By default, all tests are marked as `local`. We can make a test that's both a staging and a local test by using the following: + +```python +@pytest.mark.staging +@pytest.mark.local +``` + +Let's write a staging test. We will create a new file called `test_staging_coffee.py`. + +```python +def test_can_fund_and_withdraw_live(): + active_network = get_active_network() + price_feed = active_network.manifest_named("price_feed") + coffee = deploy_coffee(price_feed) + coffee.fund(value=SEND_VALUE) + amount_funded = coffee.address_to_amount_funded(boa.env.eoa) + assert amount_funded == SEND_VALUE + coffee.withdraw() + assert boa.env.get_balance(coffee.address) == 0 +``` + +We will then run the test in our terminal using the following command: + +```bash +mox test --network sepolia +``` + +This will take a lot longer because we're sending real transactions. We can modify our `deploy` file to only run verification in the main function. + +**Tag:** We can add a code block here for the updated deploy file. + +This test might take a long time to run, and we can expect our tests to fail because we don't have any ETH on our testnet. + +We can also see that the tests are trying to verify the contract. + +This is how we write a staging test and run it on a live network. + diff --git a/courses/moccasin-101/6-mox-coffee/21-deployments-db/+page.md b/courses/moccasin-101/6-mox-coffee/21-deployments-db/+page.md new file mode 100644 index 000000000..9e1e3e129 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/21-deployments-db/+page.md @@ -0,0 +1,118 @@ +## Deployments Database + +We are working with a Python project that interacts with smart contracts, including a `buy_me_a_coffee` contract. + +We've set up a few test files, and if we run them, we can see that they all pass. + +```bash +mox test +``` + +We can also see that we have a `mocassin.toml` file with the following code: + +```python +[networks.tenderly-sepolia] +url = "$TENDERLY_RPC_URL" +save_to_db = false +default_account_name = "default" +``` + +This file is key to working with a database. The important feature to note here is the `save_to_db` option. We want to set this to `true`, which means Mocassin will save information about deployments to a database. + +Let's add a new network to the `mocassin.toml` file for interacting with the Anvil blockchain. + +```python +[networks.anvil] +url = "http://127.0.0.1:8545" +default_account_name = "anvil1" +save_to_db = true +``` + +We also want to create a file to write our withdrawal function: + +```python +from src import buy_me_a_coffee +from moccasin.config import get_active_network + +def withdraw(): + active_network = get_active_network() + coffee = active_network.manifest_named("buy_me_a_coffee") + print(f"On network {active_network.name}, withdrawing from {coffee.address}") + coffee.withdraw() + +def moccasin_main(): + return withdraw() +``` + +If we run this, the function will use the `get_active_network` to get the latest deployment, and it'll print the network and address of the contract we're withdrawing from. We can also add more details to this script using the `get_latest_contract_checked` option. + +```python +from src import buy_me_a_coffee +from moccasin.config import get_active_network + +def withdraw(): + active_network = get_active_network() + coffee = active_network.manifest_named("buy_me_a_coffee") + coffee = active_network.get_latest_contract_unchecked("buy_me_a_coffee") + print(f"On network {active_network.name}, withdrawing from {coffee.address}") + coffee.withdraw() + +def moccasin_main(): + return withdraw() +``` + +This allows us to specifically target a contract based on a name that we've given it. + +We can also see that this is creating a database file called `deployments.db`. + +Let's go back to the terminal and run the Anvil blockchain locally: + +```bash +anvil +``` + +Now we can deploy to the Anvil chain. + +```bash +mox run deploy --network anvil +``` + +If we open our `deployments.db` file, we can see that the database is storing information about our deployments. The database will store many different pieces of information such as: +- session ID +- contract address +- contract name +- RPC deployer +- TX hash +- TX broadcast +- receipt dict +- source code +- ABI + +If we scroll over the database, we can see the contract address that was just deployed for the contract named "buy_me_a_coffee". + +Now that we have this, let's go back to our `withdraw.py` file and create a new function where we interact specifically with that address. + +```python +from src import buy_me_a_coffee +from moccasin.config import get_active_network + +def withdraw(): + active_network = get_active_network() + coffee = active_network.manifest_named("buy_me_a_coffee") + coffee = active_network.get_latest_contract_unchecked("buy_me_a_coffee") + print(f"On network {active_network.name}, withdrawing from {coffee.address}") + coffee.withdraw() + +def moccasin_main(): + return withdraw() +``` + +It's best practice to use the `manifest_named` function when dealing with smart contracts because it ensures you are working with the specific contract you intended. + +Let's clear the terminal and run the `withdraw.py` file. + +```bash +mox run withdraw --network anvil +``` + +We can now see the transaction being broadcasted and the withdrawal function being executed. This is how we can have even more powerful scripting and keep track of our deployments. Mocassin keeps track of all of that for us, which is incredibly exciting. diff --git a/courses/moccasin-101/6-mox-coffee/22-zksync-testing/+page.md b/courses/moccasin-101/6-mox-coffee/22-zksync-testing/+page.md new file mode 100644 index 000000000..df4c98714 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/22-zksync-testing/+page.md @@ -0,0 +1,41 @@ +## zkSync Testing + +We have learned about using a mock network to test our code. This is useful because it allows us to test our code without having to deploy it to a live network. + +We can also test our code against zkSync if we have the zkSync prerequisites installed. + +First, we need to check the version of the zkSync node and zkVyper: + +```bash +era test node --version +``` + +```bash +zkVyper --version +``` + +Once we have those installed, we can run the following command to test our code against the zkSync network: + +```bash +mox test --network eravm +``` + +This will spin up a fake zkSync network, allowing us to test our code against zkSync without deploying it to a live network. + +To further test our code, we also need to ensure we are correctly handling the `not enough balance` error. We can fix this by adding some code to our tests. For example, in the `conftest.py` file, we can add a line of code to set the balance of an account to 10 times the `SEND_VALUE`: + +```python +boa.env.set_balance(account.address, SEND_VALUE * 10) +``` + +Then, in the `test_unit_coffee.py` file, we can add a line of code to set the balance of an account to 3 times the `SEND_VALUE`: + +```python +boa.env.set_balance(account.address, SEND_VALUE * 3) +``` + +Running our tests again, we can see that all our tests pass. We have now tested our code against the zkSync network and ensured that it handles the `not enough balance` error correctly. + +We can also see that our tests run on zkSync as they now display the expected information in the terminal. + +We are now confident that our code is working correctly on zkSync! diff --git a/courses/moccasin-101/6-mox-coffee/23-github/+page.md b/courses/moccasin-101/6-mox-coffee/23-github/+page.md new file mode 100644 index 000000000..f20f647a0 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/23-github/+page.md @@ -0,0 +1,28 @@ +## Push to GitHub + +We have one thing left to do here, it's finally time for your GitHub repos, for your GitHub profiles to start to develop. And also your Cyfrn profiles as well. People always ask me, Patrick, how do I get hired? What do I do after I take your curriculum? And the answer is you do a lot of stuff, and you gotta show off all the cool stuff that you're doing. So pushing your projects to GitHub and then also signing up for Cyfrn profiles is going to be how you start showing off. + +So, I'm going to have a link to Cyfrn profiles, of course, in the GitHub repo associated with this course. But now we're going to do what I think is arguably one of the most important things for us to do in this entire course. And this is push our codebase up to GitHub. I've mentioned this a couple of times, but GitHub is how a lot of the entire blockchain industry works. For example, if I look up geth GitHub. That's, that's for the go implementation of Ethereum. It's got 20,000 forks. Almost 50,000 stars. 2000 people watching it. This is one of the most popular repositories in all of the EVM and the blockchain ecosystem. The go Ethereum is the code that runs one of the most popular ways to run an Ethereum node. If you were to look up Mocassin Cyfrn Mocassin, that we've shown you before. This is the GitHub repo of the tool we're working with. If we look up Vyper, we look up Vyper in here. Vyper lang/Vyper. 5000 stars. Almost 1000 forks. This is the language that we're learning in here. And if you go into issues, you can see countless issues. You can see countless pull requests. This is how developers interact with each other. This is how developers in the open source community contribute and work with each other. And, guess what I do sometimes? I might go to repo, I might go hmm, okay, who is working on this? Who are the cool people here? I might click someone's profile and go Whoa, this person is working on some really cool stuff. And especially, if you're a hiring manager, this is often how you're going to do a lot of your hiring is looking at people's profiles and saying what is this person about? What does this person do? What have they done? What have they shown success in? And the Cyfrn profiles, as well, they do the same thing, especially when it comes to competitive audits and security, which is something that you can get in much, much later after finishing the Updraft curriculum. And, the profiles are growing more and more with more and more interesting features coming out to show off to potential hiring managers. + +So, the first step, though, is for us to push our code up to GitHub and sign up for Cyfrn profiles. So, let's go ahead. Let's get started pushing up to GitHub. You have done a ton in this project, and this is the final step. This is a bad-ass project that you're going to push up. Now, the first thing that you should check finally is that in the .gitignore, the .env is in here. So way, way back when I was like, hey, the pledge, the .env pledge blah, blah, blah. If we go back to the GitHub repo associated with this course. Let's scroll way up to the top. Let's go to discussions here. The .env pledge. In here, we said, "I am aware". If I forget a .gitignore and push my key/phrase up to GitHub even for a split-second, or show my key/phrase to the internet for a split-second, it should be considered compromised and I should move all my funds immediately. If I am unsure if my account has real funds in it, I will assume it has real funds in it. I will assume it has real funds in it, I will not use it for developing purposes. I am aware that even if I hit add account on my Metamask (or other ETH wallet), I will get a new private key, but it will share the same secret phrase/mnemonic of the other accounts generated in that Metamask (or other ETH wallet). Pledge Additions. For this course, I will only use funds associated with a brand new never-before-used Metamask (or other ETH wallet). I am aware that my account associated with my private key is the same on testnets that it is on mainnets. If I must use a private key associated with real funds in the future, until I am 100% sure what I am doing, I will always either: 1. Encrypt the private key MYSELF in such a way as Patrick taught me. 2. Use the built-in functionality of Mocassin to encrypt my private keys. 3. Use the command line way to pass private keys, and delete my command line history right after ONLY AS A LAST OPTION. If I never actually deploy anything to mainnet myself or work with a private key with real funds, I do not need to be concerned. This .gitignore is a file that says hey, when we're pushing stuff up to GitHub ignore these files. So, we do not want to push these up to GitHub. If there's anything else sensitive that you don't want to push up to GitHub it's a good idea to put it in this file. We will, we will still be able to check what we're about to push up to GitHub before we push it up. That's how, that's why this line in the pledge is so important. And then, additionally down here, now this sentence makes sense, use the built-in functionality of Mocassin to encrypt my private keys. Now, you understand how that works. Now, you understand you never, ever, ever need to really deal with your private keys ever again. So, now this is the GitHub repo that I'm going to be deploying to and it's a newer GitHub repo because I wanted to show you what a blank GitHub repo looks like. It's got this sick neon sign as the avatar. So to get started here, we first need to make sure we have git installed. git --version per usual. This is one of the things we installed way, way, way at the beginning of this whole course. So, you should have git installed. If you don't have git installed there's a ton there are links in the GitHub repo associated with this course. As well as AIs are very helpful at this. Google is very helpful at this. There's a lot of places where you can install git. Now, GitHub is a centralized company and there are decentralized versions of GitHub being worked on but, as of today, GitHub is what we have. As of today, GitHub is still the dominant player to the point where we kind of have to use it. So with that being said, if we go to the GitHub docs, we should go to the get started. And, in here, it says get started with GitHub documentation. We'll go to the quickstart, as well. And, you should have already created an account on GitHub, right? So, you should already have an account here. So, you don't have to do that one. You don't have to set up your profile, downloading files. Now, we could follow along with this uploading a project to GitHub, but this is going to show you kind of the manual way to upload everything. And, that's cool, but like, I don't want to have to go onto a website every single time I want to upload my code. I want to work incredibly hard to be incredibly lazy. So, what I'm going to do instead is I am going to first come to repositories. I'm going to select new, and I am going to create a new repository, but I'm going to upload all the stuff from it from the command line. So, we're going to call this our Mox Buy Me a Coffee Cyfrn Updraft. And we are going to make sure this is public so other people can see how cool we are for finishing this project. We're going to go ahead and click Create repository. And, boom. Now, this is created. There are some notes down here. It says quick setup if you've done this kind of thing before or create a new repository on the command line. And, this will show you how to get set up here as well, but this assumes you're already signed in. We are not signed in. We want to do this the hardcore way, so we're going to look up load locally hosted files, adding locally hosted code to GitHub and this is what we're going to follow. And, since Windows users are using WSL you should be following the Mac or the Linux stuff here. I'm going to follow along with the Linux pieces. This link will be in the GitHub repo associated with this course. So, the first thing that we're going to need to do here is do git init. And, this will say Initialized an empty Git repository in. At this location, right? At your pwd wherever you're working with. You might even see some colors light up in your VS code as well. And, that's actually our first step in the docs as well. Initialize a Git repository. It says git init -b main. You can do that, as well. Or, just git init like what we did. Don't do this one. Then, what we want to do is we want to decide which one of these files we want to put into what's called version control or source control. You also might see something like this pop up. This tells us what branch we're on. You can learn about branches another day. But, we want to say which files we want to push up to GitHub. Which files do we want to share with the world? Now, if we do a little git status what we can see is a list of all the different potential files that we could push up to GitHub. All the different files that we've recently made changes on that we need to update. And, all these look pretty good. So, this is a coverage file for coverage, git attributes, gitignore. These all look fine. I don't see any I don't see the .env in here. This is great. If you want to test it. If you go into your .gitignore delete the line that says .env. Then run, you know clear, git status again. Now, in here, oh, looks like we still don't see it. Oh, it's cuz there's more .envs, there's a whole bunch of .envs. If we delete all the .envs and then we do git status we now see .env is in our list of untracked files. That's very bad. So, I'm going to undo all of that that we just did. I'm going to do git status again. And, now, it's no longer in there. Now, we can override the .gitignore, but having the .env and any sensitive stuff in the .gitignore is going to be helpful to not push it up to GitHub. So, this is the list of stuff that we're going to push up to GitHub. You might notice on the left hand side, if you're using VS code, depending on how your formatting is, some of these are green, and some of these are grayed out. So, Mocassin comes default with this .gitignore, and it automatically grays out a bunch of stuff, like build out, lib, DS store, etc. So, some of these are great out because you don't really need to push a lot of these to GitHub. You definitely don't need to push the deployments database. You don't need to push your .env, the era test node, the compiled out folder, lib, there's a bunch of stuff that's not really important that you share with the world. So, we don't push it up. So, once we have it initialized, we can choose which files we want to add by we can do git add uh SRC like this. Now, if we do git status. We see oh, everything in the SRC folder is in this other section called changes to be committed. And then down here is untracked files. So, this is like a staging ground for, it's like hey it's almost committed, it's almost pushed up to GitHub. These are the and these are the files that we've changed, but we haven't like we haven't staged them to be committed. So just stage everything, we can do git add . This period stands for everything in this current directory. Do clear, I do a git status again. Now, we see README.md is once again untracked, and these ones are staged. I want to add it back, so I'll just do get add . like this. Run another git status, and now I see once again, everything is in here. Now, to commit this to our local Git history, we do git commit -m which stands for message, and then some message like "our first commit!" little exclamation mark there. Enter. Oops, uh sorry, it's uh single quotes, not double quotes. Oh what a new mistake by me. Now, you'll see this create mode for all this stuff, and it's entering all these files into our git history. Now, if we do a little git log, we can actually see a log of all the commits that have ever been done and we can see we just have one, our first commit. Now, if I do git status it'll look like it's blank. There's nothing to commit. This has been committed to our locally running commit history. But, if we go back to GitHub, we do a little refresh on our repo here. It's still blank. Uh, what the heck? Patrick, your GitHub repos have like so much code. What's going on? Well, we we need to push our git changes to GitHub. So, this is, this git commit is only on our machine. We gotta push it over to GitHub. Then, we have to do what I think is arguably the hardest part. Importing a Git repository with the command line. After you've initialized a Git repository, you can push the repository to GitHub using either GitHub's CLI or git. You can download the GitHub application, as well, if that's going to make your life easier. You can 100% download the GitHub CLI and, to be honest, that'll probably make your life a little bit easier if you're struggling. But, I want to do it raw with the git command because it's going to allow you to use more than just GitHub as your remote provider. And if that doesn't make sense, don't worry, the more you do this, eventually that will make sense. So we're going to follow this adding a local repository to GitHub using git. It's important to note that GitHub and git are different, right? When I type git --version this is a tool for doing version control management, and git hub is a place to push code. It's like like a cloud provider or like a Google Drive for code if you will. So git is a tool. GitHub is a company, they are different. So, for us to do this we're going to need to create a new repository on GitHub. We've actually already done this. Well done, us. To avoid errors, do not initialize the new repository with README, license, or gitignore files. You can add these files after your project has been pushed to GitHub. For more information, see "Creating a new repository." At the top of your repository on GitHub's Quick Setup page, click the copy button to copy the remote repository URL. Okay? So that's going to be this button, right here. Okay, clicked it. And then what we're going to do is we're going to open our terminal and run a couple of commands here. So, we're going to run this command here. We're going to run git remote add origin. And then paste that in there. And, what this is going to do is it's going to when we type git remote -v, it's going to say whenever we push or pull code from a remote Git repository, it's This one that we just set up. It's the one that we're going to use. And then, finally you can just run get push -u origin, origin, main like this. And, I'm getting an error because I'm logged in as a different user. Oops. So to make my life easier, I'm just going to give access to Patrick. All right. So, I've gone and added my other account to this one. I'm just going to rerun that command and what's going to happen now, Now, if I do a little refresh here. Now I see all the code that we just created in here. Now, we have a terrible readme, and if you've been following along with the GitHub repo associated with this course, Of course, you know that in most of our readmes, you know, let's look at uh yeah, let's look at this one, for example. If you go to the code base associated with this one it has a really nice readme, where it says here's how you get started, here's how you can install, here's how you can work with all the code in this code base. But now, I will tell you right now there's a good chance that you'll run into an error here and working with AIs, AIs are phenomenal at triaging GitHub issues. So, if you run into an issue please work with an AI, be sure to Google around because getting this up here getting this project up here, like I said, is incredibly important for your career, because guess what, if you go build a bunch of amazing awesome, really cool projects, and really cool things, people want to see that. So, you kind of have to show it off. You have to have a place to show it off. + +```bash +git config --global user.name "Your Name" +``` + +```bash +git config --global user.email "youremail@example.com" +``` + +To verify your settings: + +```bash +git config --list +``` + +If you need to update credentials for authentication: + +- For HTTPS: Use a Personal Access Token (PAT) as your password when prompted. +- For SSH: You'll need to create and add an SSH key to your Git account. + +Would you like me to explain more about any of these methods? diff --git a/courses/moccasin-101/6-mox-coffee/24-clone/+page.md b/courses/moccasin-101/6-mox-coffee/24-clone/+page.md new file mode 100644 index 000000000..b6ed80f53 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/24-clone/+page.md @@ -0,0 +1,131 @@ +## Cloning a Repo + +We are going to learn how to clone a repo. + +On every GitHub repo we create, we usually include an "Installation" section. This section typically includes commands like: + +```bash +git clone +``` + +and + +```bash +cd +``` + +Let's see what this looks like. + +We will run a command in our terminal. + +Here we are in our `mox-cu` folder. + +We can run the following command: + +```bash +git clone https://github.com/cyfrin/mox-buy-me-a-coffee-cu cyfrin-buy-me-a-coffee-mox +``` + +This command will clone a repo, and we can name it something like `cyfrin-buy-me-a-coffee-mox`. + +The terminal will then output a list of files that have been cloned. + +We can then run the following command: + +```bash +cd cyfrin-buy-me-a-coffee-mox +``` + +to change into the folder we just created. + +We can then run the following command: + +```bash +code +``` + +to open VS Code. + +This gives us access to all of the code that is in the repo. + +We can see all of the files in our explorer pane. + +Now, let's go back to our terminal. We will run a few more commands here. + +```bash +cd +``` + +This changes us back to the parent folder. + +We can now change directories into the `mox-buy-me-a-coffee-cu` folder that we cloned from GitHub. + +```bash +cd mox-buy-me-a-coffee-cu +``` + +Another command we can run in the terminal is: + +```bash +git log +``` + +This command will display a log of all the commits to the repo. + +We can see the first commit made in the terminal. + +We can then make a small change in the README file. + +Let's add the following line: + +``` +Hello! This is a test. +``` + +We then save the file. + +Now, if we run the following command: + +```bash +git status +``` + +we can see that the README file has been changed. + +We can then run the following commands: + +```bash +git add . +``` + +```bash +git status +``` + +This will stage the file for commit. + +Next, we can commit the changes with the following command: + +```bash +git commit -m 'update!' +``` + +We can then push the changes to the GitHub repository. + +```bash +git push +``` + +Now, when we go back to our GitHub repo, we can see there are two commits: our first commit and the update commit. + +Sometimes, you might encounter errors when following these steps. We encourage you to work with your AI to help you solve these errors so that you can continue working with your GitHub repo. + +Let's also see what this looks like on the GitHub repo. + +If we go to the repo and refresh the page, we can see the two commits we made. + +If we click on the update commit, we can see the small change we made to the README file. + +A great way to engage with the developer community is by tweeting about your work on GitHub. + +You can do this by clicking on the "Tweet Me" button, which will then send a tweet about your project to your Twitter account. diff --git a/courses/moccasin-101/6-mox-coffee/25-workshop/+page.md b/courses/moccasin-101/6-mox-coffee/25-workshop/+page.md new file mode 100644 index 000000000..e35133e52 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/25-workshop/+page.md @@ -0,0 +1,31 @@ +## Introduction to Testing in Solidity + +In this lesson, we will cover some best practices for testing your Solidity code, in this case, we will focus on our `buy_me_a_coffee` contract. + +We've already learned a lot, now we will get hands-on with our testing methods. We'll begin by writing enough tests to get our code to 100% coverage. + +Here is how we'll do it: + +1. Write enough tests to get your code to 100% code coverage for `buy_me_a_coffee.vy`. + + We'll be using our testing framework: + + ```bash + mox test --coverage + ``` + + We'll use this command to run our tests, which will output the coverage we have achieved. + +2. Sign up for [Cyfrn Profiles](https://profiles.cyfrn.io/). + + This is very important for building your web3 career. Cyfrn Profiles provides a platform for you to showcase your skills, find opportunities, and build a network of fellow developers. + +3. Push your code to GitHub + + By pushing your code to GitHub, you'll be able to share your progress with others, document your work, and track changes. + +4. Add a new commit to your GitHub repo + + Adding a commit helps to keep your project organized and track the changes you make. + +We encourage you to spend at most 25 minutes on this workshop, without the aid of AI. If you're unable to solve it, take a break, and then work with AI or look to the discussions for help. Good luck! diff --git a/courses/moccasin-101/6-mox-coffee/26-recap/+page.md b/courses/moccasin-101/6-mox-coffee/26-recap/+page.md new file mode 100644 index 000000000..bb1221949 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/26-recap/+page.md @@ -0,0 +1,219 @@ +## Moccasin: A Smart Contract Development Course + +This course will walk you through building a foundational understanding of smart contract development. We will be using Moccasin, a free and open source development framework, to learn how to write, test, and deploy smart contracts. + +The course is designed to be interactive, with quizzes, code samples, and workshops to help you learn. We'll go through a real-world example: a "Buy Me a Coffee" smart contract, to help solidify the concepts learned. + +**This section will be covering:** + +* Formatting code +* Using Constants and Immutables +* Understanding storage vs constants vs immutables +* Writing unit tests +* Using mocks to test our contracts +* Using mox's manifest named +* Keeping track of deployments in a database +* Running fork tests + +### Formatting Code + +We will start by learning how to format our codebase in a more readable and maintainable way. We will split our codebase into separate files. + +For our example, we'll have three files: + +* buy_me_a_coffee.vy +* get_price_module.vy +* AggregatorV3Interface.vyi + +#### buy_me_a_coffee.vy + +```python +#pragma version 0.4.0 + +@license MIT +@title Buy Me A Coffee! +@author You! + +#notice This contract is for creating a sample funding contract +#....... + +from interfaces import AggregatorV3Interface +import get_price_module + +# Constants & Immutables +MINIMUM_USD: public(constant(uint256)) = wei_value(5, "ether") +PRICE_FEED: public(immutable(AggregatorV3Interface)) = 0x694AA1769357215DE4FAC081b1f +OWNER: public(immutable(address)) = + +# Storage +funders: public(DynArray(address, 1000)) +funder_to_amount_funded: public(HashMap(address, uint256)) + +# With constants: 262,853 +@deploy +def __init__(price_feed: AggregatorV3Interface, address: address): + self.price_feed = price_feed + self.address = address +``` + +#### get_price_module.vy + +```python +from interfaces import AggregatorV3Interface + +PRECISION: constant(uint256) = 1 * (10 ** 18) + +@internal +@view +def get_eth_to_usd_rate(price_feed: AggregatorV3Interface, eth_amount: uint256) -> uint256: + # Chris sent us 0.01 ETH for us to buy a coffee + # Is that more or less than $5? + + price: int256 = staticcall(price_feed.latestAnswer()) + eth_price: uint256 = convert(price, uint256) * (10 ** 10) / (10 ** 18) + eth_amount_in_usd: uint256 = (eth_price * eth_amount) // (10 ** 18) + + return eth_amount_in_usd # 18 '0's, decimal places +``` + +#### AggregatorV3Interface.vyi + +```python +@external +@view +def decimals() -> uint8: + #....... + +@external +@view +def description() -> String[100]: + #....... + +@external +@view +def version() -> uint256: + #....... + +@external +@view +def latestAnswer() -> int256: + #....... +``` + +### Understanding Constants and Immutables + +We'll then go over the concepts of constants and immutables, key elements in smart contract development. + +**Constants:** +* Values defined during contract deployment. +* Cannot be changed once deployed. +* Their values are inlined at compile time. + +**Immutables:** +* Values defined during contract deployment, like constants. +* Their values are stored in a specific location in storage, not inlined. +* Can be read by anyone. + +### Testing Smart Contracts + +We will then learn how to test our smart contracts using the Python testing framework PyTest. + +**Types of Tests:** + +* Unit Tests: Test a single function or part of your code. +* Integration Tests: Test how different parts of your code work together. +* Fork Tests (Staging): Test your code in a production-like environment, but with a forked version of the blockchain. + +We will be primarily writing unit tests in this section. To run our tests, we will use the following terminal command: + +```bash +mox test +``` + +This will run all of our unit tests. We can also run a specific test file: + +```bash +mox test tests/unit/test_unit_coffee.py +``` + +#### Writing Unit Tests + +We will write unit tests to verify the following: + +* The `PRICE_FEED` is correctly set. +* The initial values of `MINIMUM_USD` and `OWNER` are correct. +* The `fund` function fails if the sender doesn't provide enough ETH. +* The `fund` function correctly sets the `funders` array and `funder_to_amount_funded` mapping. +* The `withdraw` function can only be called by the `OWNER`. +* The `withdraw` function correctly updates the balance of the contract and the `OWNER`. + +**test_unit_coffee.py** + +```python +from eth_utils import to_wei +import boa +from tests.conftest import SEND_VALUE + +RANDOM_USER = boa.env.generate_address("non-owner") + +def test_price_feed_is_correct(coffee, eth_usd): + assert coffee.PRICE_FEED() == eth_usd.address + +def test_starting_values(coffee, account): + assert coffee.MINIMUM_USD() == to_wei(5, "ether") + assert coffee.OWNER() == account.address + +def test_fund_fails_without_enough_eth(coffee): + with boa.reverts("You must spend more ETH!"): + coffee.fund() + +def test_fund_with_money(coffee, account): + # Arrange + boa.env.set_balance(account.address, SEND_VALUE * 3) + + # Act + coffee.fund(value=SEND_VALUE) + + # Asset + funder = coffee.funders(8) + assert funder == account.address + assert coffee.funder_to_amount_funded(funder) == SEND_VALUE + +def test_non_owner_cannot_withdraw(coffee_funded, account): + with boa.env.prank(RANDOM_USER): + with boa.reverts("Not the contract owner!"): + coffee_funded.withdraw() + +def test_owner_can_withdraw(coffee_funded): + with boa.env.prank(coffee_funded.OWNER()): + coffee_funded.withdraw() + + assert boa.env.get_balance(coffee_funded.address) == 0 + +def test_get_rate(coffee): + assert coffee.get_eth_to_usd_rate(SEND_VALUE) > 0 +``` + +### Fork Tests + +Fork tests allow us to test our smart contracts in a production-like environment, but without sending any real money. This is useful for testing complex scenarios or for verifying that our contracts work as expected. + +To run a fork test, we use the following terminal command: + +```bash +mox test --network sepolia --fork +``` + +This will run our tests on a forked version of the Sepolia testnet. + +### Working with the Database + +Moccasin provides a database where we can store information about our deployments. This database is used to keep track of the addresses of our contracts, the RPCs used to deploy them, and the transactions that were sent. + +### Wrapping Up + +Congratulations on completing this section! You have learned how to format code, understand constants and immutables, write unit tests, and run fork tests. You've also learned how to use mox's manifest named and how to use a database to keep track of your deployments. + +Now is a great time to take a break, go for a walk, or lift some iron at the gym! You have earned it! We've covered a lot in this section, so make sure to review the material and reach out if you have any questions. + +Get ready for the next section, where we will learn about deploying our smart contracts to the Ethereum blockchain! diff --git a/courses/moccasin-101/6-mox-coffee/3-refactor/+page.md b/courses/moccasin-101/6-mox-coffee/3-refactor/+page.md new file mode 100644 index 000000000..5646ad853 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/3-refactor/+page.md @@ -0,0 +1,334 @@ +## Refactoring + +We've learned a couple of things since we originally wrote this code. We first learned about interfaces and how they can actually be in their own file. So, to make this code a little bit nicer, we're going to go ahead create a new folder called "interfaces". And we're going to create an interface called AggregatorV3interface. + +```python +interface AggregatorV3Interface: + def decimals() -> uint8: view + def description() -> String[1000]: view + def version() -> uint256: view + def latestAnswer() -> int256: view +``` + +Now, the reason I'm using camel case here instead of snake case, right? I'm having these capital letters and no underscores, is because this is the name of the interface in its Solidity edition, and I'm looking to keep the same name of the interface to match what Chainlink Labs wrote for the Solidity edition of this. + +But if this is confusing, you can obviously do that, I underscore, or the I methodology that we did previously. What we can't do is we can't just copy-paste this in here. We have to actually convert it to its interface edition. But, oh, man, that's quite annoying, uh, that's quite a lot of work, and I am incredibly lazy. + +So let's see what our friend ChatGPT has to say. Let's give this a little prompt. I'll say, here is my inline Vyper interface: + +```python +interface AggregatorV3Interface: + def decimals() -> uint8: view + def description() -> String[1000]: view + def version() -> uint256: view + def latestAnswer() -> int256: view +``` + +Can you convert it to a Vyper file interface? Here is an example. + +```python +@external +@view +def retrieve() -> uint256: + ... +``` + +You actually already know how to convert this from its edition here to its file edition, right? You you know how to do that transition. You could 100% do it manually yourself, if you want to, too. But, it's just kind of busy work, right? We're not, it's not super important, so it'd be great if AI could just do it. + +So we're going to say, hey here is my inline Vyper interface: + +```python +interface AggregatorV3Interface: + def decimals() -> uint8: view + def description() -> String[1000]: view + def version() -> uint256: view + def latestAnswer() -> int256: view +``` + +Can you convert it to a Vyper file interface? Here is an example: + +```python +@external +def store(favorites_number: uint256): + ... +@external +@view +def retrieve() -> uint256: + ... +``` + +Let's see what ChatGPT gives us. To convert your inline Vyper interface into a standard Vyper file interface, you need to define the functions with the appropriate decorators for visibility (@external, @view) and specify the function signatures clearly. Here's how you can structure the AggregatorV3Interface in a Vyper file: + +```python +# AggregatorV3Interface.vy +interface AggregatorV3Interface: + @view + @constant + def decimals() -> uint8: + ... + @view + @constant + def description() -> String[1000]: + ... + @view + @constant + def version() -> uint256: + ... + @view + @constant + def latestAnswer() -> int256: + ... +``` + +Okay. Clearly it's a little bit less up-to-date. + +Oh, ChatGPT, though, is a little confused. It's come up with a new fake decorator called "constant" and it did this at the top for some reason. + +Okay. Well, uh, I guess this one's closer. Let's let's use this one. + +Okay, so maybe, uh AI didn't do such a great job here. But, that's okay. We're going to copy this, we're going to grab this um, and we're just going to do a find and replace. + +So, I did, I gr I got this find and replace by doing command F or control F, depending on if you're Mac or Windows or Linux. And I'm going to say "pass", and we're just going to replace everything with three dots. Hit enter, save it. And that looks pretty good to me. + +```python +@external +@view +def description() -> String[1000]: + ... +``` + +Decimals, description, version, latest answer. + +Decimals, description, latest version, answer, version, latest answer. + +Okay. That looks pretty good to me. Okay, great. So let's go ahead delete this now. And now we can do, from interfaces import I'll hit tab here. + +```python +from interfaces import AggregatorV3Interface +``` + +Yeah, I got this one, right? AggregatorV3Interface. + +And, let's just make sure this works. We'll do a little "mox compile". + +```bash +patrick@cu mox-buy-me-a-coffee-cu % mox compile +``` + +Nice. Okay, so that works. + +Thanks AI. Now, let's actually get a little bit more practice working with these modules. Let's take out, let's look at this eth to USD rate. Let's take this out as an internal function. + +```python +@external +@view +def get_eth_to_usd_rate(eth_amount: uint256) -> uint256: + ... + return self._get_eth_to_usd_rate(eth_amount) +``` + +And let's turn this into a module. So, let's get some practice working with this. So, I'm actually going to delete this from the buy me a coffee. Going to go back to the SRC, new file. + +We're going to do, get price module.vy. And we're going to paste this in here: + +```python +@internal +@view +def _get_eth_to_usd_rate(eth_amount: uint256) -> uint256: + """ + Chris sent us 0.01 ETH for us to buy a coffee + Is that more or less than $5? + """ + + price: int256 = staticcall PRICE_FEED.latestAnswer() + eth_price: uint256 = (convert(price, price * uint256)) * (10 ** 18) + eth_amount_in_usd: uint256 = (eth_price * eth_amount) // PRECISION + return eth_amount_in_usd # $ 8.0's, 18 decimal places +``` + +What are the variables we need? We need a price feed in here. So, let's actually just pass it in as a variable. The price feed is of type AggregatorV3Interface. So we need that, so we'll do from interfaces import AggregatorV3Interface like this. + +```python +from interfaces import AggregatorV3Interface +``` + +We'll pass this in as a variable. We'll say, price feed of type AggregatorV3Interface. + +```python +@internal +@view +def _get_eth_to_usd_rate(price_feed: AggregatorV3Interface, eth_amount: uint256) -> uint256: + """ + Chris sent us 0.01 ETH for us to buy a coffee + Is that more or less than $5? + """ + + price: int256 = staticcall price_feed.latestAnswer() + eth_price: uint256 = (convert(price, price * uint256)) * (10 ** 18) + eth_amount_in_usd: uint256 = (eth_price * eth_amount) // PRECISION + return eth_amount_in_usd # $ 8.0's, 18 decimal places +``` + +And then we'll do staticcall price feed.latest answer. + +```python +@internal +@view +def _get_eth_to_usd_rate(price_feed: AggregatorV3Interface, eth_amount: uint256) -> uint256: + """ + Chris sent us 0.01 ETH for us to buy a coffee + Is that more or less than $5? + """ + + price: int256 = staticcall price_feed.latestAnswer() + eth_price: uint256 = (convert(price, price * uint256)) * (10 ** 18) + eth_amount_in_usd: uint256 = (eth_price * eth_amount) // PRECISION + return eth_amount_in_usd # $ 8.0's, 18 decimal places +``` + +Buy me a coffee. We also have this precision variable. Let me look for this in any other place. It looks like this is the only line. It looks like it was only used in the function, so we're going to take that out. + +```python +PRECISION: constant(uint256) = 1 * (10 ** 18) +``` + +We're going to paste that in here as well. + +```python +PRECISION: constant(uint256) = 1 * (10 ** 18) +``` + +And this is looking pretty good. Now, what we can do back in here is, we can import this now. We can say, from, or excuse me, we can just say import get price module. + +```python +from interfaces import AggregatorV3Interface +import get_price_module +``` + +Right? Because these are in the same folder. Right. So we're just going to say go ahead, just import get price module. + +```python +from interfaces import AggregatorV3Interface +import get_price_module +``` + +And then down here, we're going to have this error because we're saying self.get eth, but instead, we want to do get price module.get eth to USD rate. + +```python +@external +@view +def get_eth_to_usd_rate(eth_amount: uint256) -> uint256: + ... + return get_price_module.get_eth_to_usd_rate(PRICE_FEED, eth_amount) +``` + +Now we know that this function also takes two parameters now. So we're also going to want to pass in the price feed, like this. + +```python +@external +@view +def get_eth_to_usd_rate(price_feed: AggregatorV3Interface, eth_amount: uint256) -> uint256: + ... + return get_price_module.get_eth_to_usd_rate(PRICE_FEED, eth_amount) +``` + +And, oh, we're going to want to do the same thing up here. Price feed, pass it in. + +```python +@internal +@view +def _get_eth_to_usd_rate(eth_amount: uint256) -> uint256: + """ + How do we convert the ETH amount to dollars amount? + """ + + usd_value_of_eth: uint256 = get_price_module.get_eth_to_usd_rate(PRICE_FEED, msg.value) + assert usd_value_of_eth >= MINIMUM_USD, "You must spend more ETH!" + self.funders.append(msg.sender) + self.funder_to_amount_funded[msg.sender] = msg.value + +``` + +And now we're looking pretty good here. So let's do a little mox compile. + +```bash +patrick@cu mox-buy-me-a-coffee-cu % mox compile +``` + +And it compiles successfully. + +Awesome. + +**Code Block** + +```python +@external +@payable +def fund(): + """ + Allows users to send $ to this contract + Have a minimum amount to send + """ + _get_eth_to_usd_rate(msg.value) + +``` + +**Code Block** + +```python +from interfaces import AggregatorV3Interface +import get_price_module + +PRECISION: constant(uint256) = 1 * (10 ** 18) + +@external +@view +def get_eth_to_usd_rate(eth_amount: uint256) -> uint256: + """ + Chris sent us 0.01 ETH for us to buy a coffee + Is that more or less than $5? + """ + + price: int256 = staticcall PRICE_FEED.latestAnswer() + eth_price: uint256 = (convert(price, price * uint256)) * (10 ** 18) + eth_amount_in_usd: uint256 = (eth_price * eth_amount) // PRECISION + return eth_amount_in_usd # $ 8.0's, 18 decimal places + +``` + +**Code Block** + +```python +@internal +@view +def _get_eth_to_usd_rate(eth_amount: uint256) -> uint256: + """ + How do we convert the ETH amount to dollars amount? + """ + + usd_value_of_eth: uint256 = get_price_module.get_eth_to_usd_rate(PRICE_FEED, msg.value) + assert usd_value_of_eth >= MINIMUM_USD, "You must spend more ETH!" + self.funders.append(msg.sender) + self.funder_to_amount_funded[msg.sender] = msg.value + +``` + +**Code Block** + +```python +@external +@payable +def fund(): + """ + Allows users to send $ to this contract + Have a minimum amount to send + """ + _get_eth_to_usd_rate(msg.value) + +``` + +**Code Block** + +```bash +patrick@cu mox-buy-me-a-coffee-cu % mox compile +``` diff --git a/courses/moccasin-101/6-mox-coffee/4-storage/+page.md b/courses/moccasin-101/6-mox-coffee/4-storage/+page.md new file mode 100644 index 000000000..47ba4ea62 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/4-storage/+page.md @@ -0,0 +1,129 @@ +## Storage in Smart Contracts + +In our last lesson, we learned about state variables, which are variables that exist on the blockchain and can be accessed by other smart contracts. These variables are stored in a location called _storage_, but we didn't go into detail about how storage works. + +We'll be doing that in this lesson. In this lesson, we'll learn the differences between _constants_, _immutables_, and _storage_. We'll also cover how variables are stored in the EVM and look at a script that allows us to read storage directly. + +### Storage Layout + +The EVM has a giant list of storage locations where we can store variables. This is how we can update values and retrieve them later. + +Let's start by looking at a minimal smart contract that has one state variable. + +```python +favorite_number: uint256; +``` + +This variable is of type _uint256_. When this contract is deployed, the EVM needs to store the value of this variable. + +Our contract will also have a state variable of type _bool_. + +```python +some_bool: bool; +``` + +The EVM handles booleans by representing them as a hex value. A _true_ boolean will have a hex value of 0x00000001, while a _false_ boolean will have a hex value of 0x00000000. + +The EVM assigns storage slots to variables in order. + +- If we have a variable called _favorite_number_, it's going to be stored in slot zero. +- _some_bool_, being the second variable, will be stored in slot one. + +### Fixed-Sized Arrays + +Let's say we have an array of 1000 slots. + +```python +my_fixed_array: uint256[1000]; +``` + +Fixed-sized arrays take up all the storage slots associated with their size. The EVM will start with slot two for the array and allocate a slot for each element. + +### Dynamic Arrays + +Dynamic arrays differ from fixed-sized arrays by having a special "holder" slot to keep track of the array's length. + +```python +my_dyn_array: DynArray(uint256, 100] +``` + +- This dynamic array can hold up to 100 elements. +- The array starts at slot 1002. +- The length is stored at slot 1003. + +### Mappings + +Mappings use a hashing function to determine the storage location for elements. + +```python +my_map: HashMap[(uint256, bool)]; +``` + +- The mapping will have a blank placeholder slot. +- Each element is stored at a slot based on its key. + +### Constants and Immutables + +Constants and immutables are not stored in storage. They are part of the contract's bytecode. + +```python +NOT_IN_STORAGE: constant(uint256) = 123 +IMMUTABLE_NOT_IN_STORAGE: immutable(uint256) +``` + +This is why we don't need to initialize them like the other state variables. + +### Accessing Storage with a Script + +We can use a script to read values directly from storage. Let's have a look at the storage script from the course repository. + +```python +from src.example_contracts import fun_with_storage +from moccasin.config import get_active_network +import boa + +def deploy_storage(): + fws = fun_with_storage.deploy() + active_network = get_active_network() + if active_network.has_explorer(): + print("Verifying contract on explorer...") + result = active_network.moccasin.verify(fws) + result.wait_for_verification() + print(f"favorite_number is {fws.storage.favorite_number.get()}") + print(f"Immutables: {fws.immutables.IMMUTABLE_NOT_IN_STORAGE}") + print("You can call directly from storage slots!") + print(f"Value at storage slot 0: {boa.env.get_storage(fws.address, 0)}") + print(f"First element of the fixed array: {boa.env.get_storage(fws.address, 2)}") + print(f"Length of the dyn array: {boa.env.get_storage(fws.address, 1002)}") + print(f"First element in dyn array: {boa.env.get_storage(fws.address, 1003)}") + print("Mapping placeholder") + print(f"First element of the mapping: {boa.env.get_storage(fws.address, 1103)}") + slot = 1103 + k = 0 + location = boa.eval(f"convert(keccak256(concat(convert({slot}, bytes32), convert({k}, bytes32))), uint256)") + print(f"Storage of element: {boa.env.get_storage(fws.address, location)}") + +def moccasin_main(): + deploy_storage() +``` + +We can run this script to see the values stored in storage. For example: + +```bash +mox run storage.py +``` + +You can see that we can read the values stored in different slots using `boa.env.get_storage()`. + +We can use `mox inspect` to see the storage layout. This is a useful tool that can be used to debug and understand how storage is being used. + +```bash +mox inspect fun_with_storage storage-layout +``` + +This command will output a dictionary with the following information for each variable: + +- _slot:_ The storage slot where the variable is stored +- _type:_ The type of the variable + +We hope this lesson has helped you understand how storage works in Solidity smart contracts. As we progress through the course, we'll be exploring more about storage and how to use it effectively. diff --git a/courses/moccasin-101/6-mox-coffee/5-refactor-2/+page.md b/courses/moccasin-101/6-mox-coffee/5-refactor-2/+page.md new file mode 100644 index 000000000..f113c60d3 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/5-refactor-2/+page.md @@ -0,0 +1,21 @@ +We now have a decent understanding of storage. We also understand why we don't need to initialize the `get_price_module`. All we want from this module is the `get_eth_to_usd_rate` function. We also get this `PRECISION` variable, but we get it by default because this is a constant variable and it's going to be embedded directly into the bytecode of this contract. We don't have to initialize storage at all. + +So with that, that's pretty much all the refactoring we're going to do. Now, I'm going to show you later how we can do a little bit of gas profiling to see how much more expensive this would be if these were storage variables instead of constants and immutables. But for the most part, this is looking pretty good. + +```python +from interfaces import AggregatorV3Interface +import get_price_module + +PRECISION = constant(int256(1 * (10 ** 18))) + +@internal +def get_eth_to_usd_rate(price_feed: AggregatorV3Interface, eth_amount: uint256) -> uint256: + """ + Chris sent us 0.01 ETH for us to buy a coffee + Is that more or less than $5? + """ + price: int256 = staticcall(price_feed.latestAnswer(), []) + eth_price: uint256 = convert(price, int256) + (10**18) + eth_amount_in_usd: uint256 = (eth_price * eth_amount) // PRECISION + return eth_amount_in_usd # $ 3 & 0's decimal places +``` diff --git a/courses/moccasin-101/6-mox-coffee/6-script/+page.md b/courses/moccasin-101/6-mox-coffee/6-script/+page.md new file mode 100644 index 000000000..2e859f839 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/6-script/+page.md @@ -0,0 +1,90 @@ +## Deploying our Smart Contract + +We have now written our smart contract code. Now, we need to give it some scaffolding. We will write some scripts so that we can deploy this and some tests so that we can make sure our code is working correctly. + +First, we will write our deploy script. + +**Code:** + +```python +def moccasin_main(): + deploy_coffee() + +``` + +To ensure that everything is working, we will run our deploy script. First, we will run this command in the terminal. +```bash +& mox run deploy +``` + +Now, we need to figure out what our script needs to deploy the contract. In this example, we are working with a contract called `buy_me_a_coffee`. + +First, we need to figure out which network we are working with. We can import the `get_active_network` function from the `mocassin.config` module. +**Code:** +```python +from moccasin.config import get_active_network + +def deploy_coffee(): + print("Hi from deploy") + +def moccasin_main(): + active_network = get_active_network() + deploy_coffee() +``` + +We can use the `get_active_network` function to get information about our network. + +**Code:** +```python +from moccasin.config import get_active_network +from src import buy_me_a_coffee + +def deploy_coffee(price_feed: str): + buy_me_a_coffee.deploy(price_feed) + +def moccasin_main(): + active_network = get_active_network() + deploy_coffee() + +``` + +Next, we need to get our price feed. In our case, the price feed needs to change based on the network. We will use conditional statements to decide which address to use. + +**Code:** + +```python +from moccasin.config import get_active_network +from src import buy_me_a_coffee + +def deploy_coffee(price_feed: str): + buy_me_a_coffee.deploy(price_feed) + +def moccasin_main(): + active_network = get_active_network() + price_feed = "" + if active_network.name == "sepolia": + price_feed = "0x69444A1769357215DE4FAC081bf1f1309aDc325306" + # if active_network.name == "mainnet": + # price_feed = "" + deploy_coffee(price_feed) + +``` + +In the code above, we have set up the `price_feed` address based on the network. This is the code we would have to write for every network. We could also just set it up like this: +**Code:** + +```python +from moccasin.config import get_active_network +from src import buy_me_a_coffee + +def deploy_coffee(price_feed: str): + buy_me_a_coffee.deploy(price_feed) + +def moccasin_main(): + active_network = get_active_network() + price_feed = "" + if active_network.name == "sepolia": + price_feed = "0x69444A1769357215DE4FAC081bf1f1309aDc325306" + deploy_coffee(price_feed) +``` +This is a pretty inefficient way to manage the price feed address. This is where we will learn about forking and other techniques for testing our code. diff --git a/courses/moccasin-101/6-mox-coffee/7-mocks/+page.md b/courses/moccasin-101/6-mox-coffee/7-mocks/+page.md new file mode 100644 index 000000000..e6347fb19 --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/7-mocks/+page.md @@ -0,0 +1,202 @@ +## Mock Contracts + +To test our smart contracts, we are going to deploy mock versions of them to our own local PyEVM network. + +First, we need to make a script to deploy our own fake price feed contract. To deploy our own fake price feed contract, we will need to grab the code from the Chainlink GitHub repo. + +We can find the code in the `src/mocks/mock_v3_aggregator.vy` file. + +We will copy and paste the code into our project, and will need to convert it from Solidity to Vyper. + +We have a minimal Chainlink price feed contract here in Vyper. We are going to use this for mocking, not for actually deploying price feeds. + +We have created a new script called `deploy_mocks.py`. This script will deploy our mock price feed contract. + +```python +from src.mocks import mock_v3_aggregator +from moccasin.boa.tools import VyperContract + +STARTING_DECIMALS = 8 +STARTING_PRICE = int(2000e8) + +def deploy_feed() -> VyperContract: + return mock_v3_aggregator.deploy(STARTING_DECIMALS, STARTING_PRICE) + +def moccasin_main(): + active_network = get_active_network() + price_feed: VyperContract = deploy_feed() + coffee = buy_me_a_coffee.deploy(price_feed.address) + print(f"Coffee deployed at: {coffee.address}") + print(coffee.get_eth_to_usd_rate(1000)) + +``` + +We also have a `deploy.py` script to run these contracts. + +```python +from moccasin.config import get_active_network +from script.deploy_mocks import deploy_feed +from moccasin.boa.tools import VyperContract +from src import buy_me_a_coffee + +def deploy_coffee(price_feed: str): + buy_me_a_coffee.deploy(price_feed) + +def moccasin_main(): + active_network = get_active_network() + price_feed: VyperContract = deploy_feed() + coffee = buy_me_a_coffee.deploy(price_feed.address) + print(f"Coffee deployed at: {coffee.address}") + coffee.get_eth_to_usd_rate(1000) + +``` + +This method of mocking is common in testing. We might need to rewrite a contract to make it easier to work with. + +**Code Block for `mock_v3_aggregator.vy`:** + +```python +# pragma version 0.4.0 +#license MIT + +DECIMALS: immutable(uint8) + +latestAnswer: public(int256) +latestTimestamp: public(uint256) +latestRound: public(uint256) + +getAnswer: public(HashMap[uint256, int256]) +getTimestamp: public(HashMap[uint256, uint256]) +getStartedAt: public(HashMap[uint256, uint256]) + +supply: uint256 +decimals: uint256 +version: public(constant(uint256)) = 4 + +@deploy +def __init__(decimals: uint8, initialAnswer: int256): + DECIMALS = decimals + self.updateAnswer(initialAnswer) + print(f"HELLO FROM MOCK AGGREGATOR") + +@internal +def updateAnswer(answer: int256): + self.latestAnswer = answer + self.latestTimestamp = block.timestamp + self.latestRound = self.latestRound + 1 + self.getAnswer[self.latestRound] = answer + self.getTimestamp[self.latestRound] = block.timestamp + self.getStartedAt[self.latestRound] = block.timestamp + +@external +def updateRoundData( + _roundId: uint256, answer: int256, timestamp: uint256, startedAt: uint256 +): + self.latestRound = _roundId + self.latestAnswer = answer + self.latestTimestamp = timestamp + self.getAnswer[self.latestRound] = answer + self.getTimestamp[self.latestRound] = timestamp + self.getStartedAt[self.latestRound] = startedAt + +@external +@view +def getRoundData(roundId: uint256) -> (uint256, int256, uint256, uint256, uint256): + return ( + roundId, + self.getAnswer[roundId], + self.getStartedAt[roundId], + self.getTimestamp[roundId], + roundId, + ) + +@external +@view +def latestRoundData() -> (uint256, int256, uint256, uint256, uint256): + return ( + self.latestRound, + self.getAnswer[self.latestRound], + self.getStartedAt[self.latestRound], + self.getTimestamp[self.latestRound], + self.latestRound, + ) + +``` + +**Terminal Command to run the script** + +```bash +mox run deploy +``` + +**Code block for `buy_me_a_coffee.vy`** + +```python +# Storage +funders: public(DynArray[address, 1000]) +funder_to_amount_funded: public(HashMap[address, uint256]) + +# With constants: 262,853 +@deploy +def __init__(price_feed: address): + PRICE_FEED = AggregatorV3Interface(price_feed) + OWNER = msg.sender + +@external +@payable +def fund(): + self._fund() + +@internal +@payable +def _fund(): + """ Allows users to send $ to this contract + Have a minimum $ amount to send + How do we convert the ETH amount to dollars amount? + ... + """ + usd_value_of_eth: uint256 = get_price_module.get_eth_to_usd_rate(PRICE_FEED, msg.value) + assert usd_value_of_eth >= MINIMUM_USD, "You must spend more ETH!" + self.funders.append(msg.sender) + self.funder_to_amount_funded[msg.sender] += msg.value + +@external +def withdraw(): + """Take the money out of the contract, that people sent via the fund function. + How do we make sure only we can pull the money out? + ... + """ + assert msg.sender == OWNER, "Not the contract owner!" + raw_call(OWNER, b"", value=self.balance) + +@external +@view +def get_eth_to_usd_rate(eth_amount: uint256) -> uint256: + return get_price_module.get_eth_to_usd_rate(PRICE_FEED, eth_amount) + +@external +@payable +def default(): + self._fund() +``` + +**Code Block for `get_price_module.vy`:** + +```python +from interfaces import AggregatorV3Interface + +PRECISION: constant(uint256) = 1 * (10 ** 18) + +@internal +@view +def _get_eth_to_usd_rate(price_feed: AggregatorV3Interface, eth_amount: uint256) -> uint256: + """Chris sent us 0.01 ETH for us to buy a coffee + Is that more or less than $5? + ... + """ + price: int256 = staticcall(price_feed.latestAnswer()) + eth_price_in_usd: uint256 = convert(price, uint256) * (10 ** 10) // (10 ** 18) + eth_amount_in_usd: uint256 = (eth_price * eth_amount) // PRECISION + return eth_amount_in_usd # 18 '0's, 18 decimal places + +``` diff --git a/courses/moccasin-101/6-mox-coffee/8-manifest/+page.md b/courses/moccasin-101/6-mox-coffee/8-manifest/+page.md new file mode 100644 index 000000000..87fa527da --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/8-manifest/+page.md @@ -0,0 +1,97 @@ +## Manifest Named + +This lesson expands on our understanding of deploying contracts to real networks. We will use a feature called "Named Contracts" from the Mocassin framework. + +This feature will allow us to streamline the process of deploying our price feed contract to different networks. + +In the previous lesson, we were able to deploy our price feed contract to the PyVM. However, this was not very helpful for deploying to real networks. + +Let's explore the example from the Mocassin documentation: + +``` +[project] +src = "contracts" + +[networks.mainnet-fork] +url = "https://ethereum-rpc.publicnode.com" +chain_id = 1 +fork = true + +# Look here! We have a named contract named "usdc" +[networks.mainnet-fork.contracts] +usdc = { address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"} + +``` + +This code introduces a new section called "contracts" within each network. This section defines a named contract, "usdc" in this case, and provides a specific address associated with that contract on the mainnet fork. + +Mocassin's named contracts allow us to define a variety of parameters. We saw the following: + +* **address:** The address of the contract on the network. +* **abi:** The ABI source of the contract. This can be a JSON file, a Vyper contract file, a Vyper interface file, or a raw ABI string. +* **abi_from_explorer:** This flag is set to true if you want to get the ABI from a public source. You may need to set an `explorer_api_key` in your `mocassin.toml` file or environment variable. +* **deployment_script:** This is the path to the deployment script for the contract. +* **force_deploy:** Set this to true if you want to force deploy the contract even when a contract exists at that address. + +In the previous lesson, we deployed our price feed contract to the PyVM using the `deploy_mocks.py` script. + +Let's revisit this script and incorporate named contracts: + +``` +from src.mocks import mock_v3_aggregator_v2 +from moccasin.boa_tools import VyperContract + +STARTING_DECIMALS = 8 +STARTING_PRICE = int(200e8) + +def deploy_feed() -> VyperContract: + return mock_v3_aggregator_v2.deploy(STARTING_DECIMALS, STARTING_PRICE) + +def moccasin_main() -> VyperContract: + return deploy_feed() +``` + +Now, we can update the `deploy.py` script to use named contracts: + +``` +from moccasin.config import get_active_network +from script.deploy_mocks import deploy_feed +from moccasin.boa_tools import VyperContract +from src import buy_me_a_coffee + +def moccasin_main(): + active_network = get_active_network() + price_feed = active_network.manifest_named("price_feed") + print(f"On network {active_network.name}, using price feed at {price_feed.address}") +``` + +Let's now open our `mocassin.toml` file to configure our deployment. + +First, we'll add a section for the Sepolia network: + +``` +[networks.sepolia] +url = "$SEPOLIA_RPC_URL" +save_to_db = false +default_account_name = "default" +explorer_uri = "https://eth-sepolia.blockscout.com/" +explorer_type = "blockscout" +``` + +We will create an `.env` file and set the `SEPOLIA_RPC_URL` environment variable to the Sepolia RPC URL from our Alchemy dashboard. + +We'll also add the price feed address and deploy script for Sepolia: + +``` +[networks.sepolia.contracts.price_feed] +address = "0x6944A41769357215DE4FAC081bf1f309aDC325306" +deployer_script = "script/deploy_mocks.py" +``` + +We're ready to test our deployment by running the command: + +```bash +mox run deploy --network sepolia +``` + +Mocassin will now be smart enough to know that there isn't a price feed contract on the PyVM. Therefore, it will use the address and deploy script we defined in our `mocassin.toml` file. This will ensure our deployment is efficient across various networks. diff --git a/courses/moccasin-101/6-mox-coffee/9-top-level/+page.md b/courses/moccasin-101/6-mox-coffee/9-top-level/+page.md new file mode 100644 index 000000000..db971419f --- /dev/null +++ b/courses/moccasin-101/6-mox-coffee/9-top-level/+page.md @@ -0,0 +1,68 @@ +## Top-level Named Contracts + +In this lesson, we're going to be learning about a new concept called "top-level named networks". So far, we've been deploying our contracts using separate configurations for different networks. But, what if we could write one configuration for each of our networks and then have a top-level file to act as the main configuration file? This way, we can avoid repeating ourselves when deploying to different networks. We can use a technique called "top-level named networks" to do this. + +Here's how we can implement this technique: + +1. First, let's create a new section for each network: + +```toml +[networks.contracts] +deployer_script = "script/deploy_mocks.py" +abi = "Mock/v3_aggregator.json" + +[networks.contracts.price_feed] +address = "0x6944A41769357215DE4FAC4081f1f1309aDc323586" +deployer_script = "script/deploy_mocks.py" + +[networks.pyevm.contracts.price_feed] +deployer_script = "script/deploy_mocks.py" +``` + +2. Next, create a section for a "top-level named contract": + +```toml +[networks.contracts.coffee] +deployer_script = "script/deploy.py" +``` + +Now, for every network that doesn't have an address, it will automatically deploy a mock for us. This makes our life a little easier, but we can also specify an address for the network if we want. + +We can see that we do not need a deployer script for the `sepolia` network because we already have a default deployer script that can be used for all networks. + +Let's go ahead and run the following command to deploy to our network: + +```bash +mox run deploy --network sepolia +``` + +We are warned that our contract won't be compiled, but we are able to deploy. + +Now, we're going to refactor our code to include the power of top-level named networks. Let's add a line of code to our `deploy_coffee` function: + +```python +def deploy_coffee(price_feed: VyperContract) -> VyperContract: + coffee: VyperContract = buy_me_a_coffee.deploy(price_feed) + print(f"Coffee deployed at {coffee.address}") +``` + +We're going to go ahead and pass in the price feed to the `deploy_coffee` function. We can access our price feed using a `manifest named` function. The `manifest named` function will allow us to access any named contract that has been specified in our `mocassin.toml` configuration file. We can use this function in our `mocassin_main` function like so: + +```python +def moccasin_main() -> VyperContract: + active_network = get_active_network() + price_feed: VyperContract = active_network.manifest_named('price_feed') + print(f"On network {active_network.name}, using price feed at {price_feed.address}") + return deploy_coffee(price_feed) +``` + +Here's the breakdown of the code: + +1. We're first going to get the active network using the `get_active_network` function. +2. Next, we're going to use the `manifest_named` function to get the price feed. +3. We're going to print a message about the network and the price feed. +4. Finally, we're going to return the deployed `coffee` contract. + +This allows us to deploy our contract in a way that is easy to maintain and test. + +We can now deploy to our local network, an actual network, or a fork network. diff --git a/courses/moccasin-101/7-mox-html/1-mox-intro/+page.md b/courses/moccasin-101/7-mox-html/1-mox-intro/+page.md new file mode 100644 index 000000000..c45b3c1ba --- /dev/null +++ b/courses/moccasin-101/7-mox-html/1-mox-intro/+page.md @@ -0,0 +1,116 @@ +## HTML/JS Introduction + +This lesson is a little bit different, because we are going to be teaching you how your wallets and how your Metamask interacts with websites. + +If you scroll down in this repo, the front-end code we have written here is actually going to work great for both Foundry and for Moccasin running anvil in the background. + +So, if you want to go ahead and follow along with me here, you can Whenever I say, hey, run this with Foundry, to deploy this with Foundry, just know that I mean with Moccasin. + +And we have some instructions in here on how to get started here with Moccasin instead of Foundry. + +Now, you do not have to follow along with me here. You can just watch and learn because it is really important that you understand what is actually happening when your Metamask pops up and says, hey, confirm this transaction. + +Because, if you confirm the wrong transaction, that's actually really scary. + +So, we want to learn how interacting with websites work, what it looks like. And if you have no intention of ever being a front-end developer, which is totally fine if you're like, hey, screw this I just want to be a Python dev, then great. You don't need to follow along here at all. + +But, if you would like to, the instructions in here for Moccasin are here as well. + +Additionally, the Foundry version of this project is just called fund me for the Moccasin edition. I thought it would be way nicer and more advanced if we called it buy me a coffee. + +So, whenever whenever I refer to fund me, just know that I am referring to the same buy me a coffee contract. Okay? + +So, we are going to go ahead We are going to go through the We are going to go through with this section I'm going to, you know, this is a slightly older video I'm going to be showing you how to do a lot of stuff using Foundry, but again, you can just as easily use Moccasin for this as well. + +So, let's go ahead, let's learn how websites actually work and how our wallets interact with these websites. So, here we go. + +**[Insert Metamask image here]** + +We are going to clone this repository: + +```bash +git clone https://github.com/Cyfrin/html-fund-me-cu +``` + +```bash +cd html-fund-me-cu +``` + +We are going to run the following commands to deploy to Moccasin: + +```bash +git clone https://github.com/Cyfrin/buy-me-a-coffee-cu +cd buy-me-a-coffee-cu +anvil +``` + +We are going to open up another terminal and run the following commands: + +```bash +mox run deploy --network anvil +``` + +**[Insert connect button image here]** + +When we click the **connect** button in the HTML code, Metamask will pop up and ask us to confirm a transaction. + +**[Insert Metamask transaction confirmation image here]** + +This transaction is actually just a call to the blockchain that is running in the background and will allow the connected wallet to interact with our website. + +**[Insert constants.js image here]** + +Here in our **constants.js** file we can see the address of our deployed contract. + +**[Insert Metamask add network image here]** + +We are going to add this network to Metamask. + +```bash +mox run deploy --network anvil +``` + +Let's take a look at the HTML code: + +```python + + + + + + Fund Me + + +

Fund Me

+ + + + + +``` + +This is the simple front-end for our project that we are going to connect with our contract. + +We will connect to our contract with the **index.js** file. + +```python +const connectButton = document.getElementById("connect"); + +connectButton.addEventListener('click', async () => { + if(typeof window.ethereum !== 'undefined'){ + const accounts = await window.ethereum.request({ + method: "eth_requestAccounts", + }); + console.log("Connected", accounts); + } else { + alert("Please install Metamask!"); + } +}) + +``` + +Here in this code, we are selecting the button and assigning it to a variable. We are then adding an event listener, which will fire when the button is clicked. + +The function fires when the button is clicked. It is then going to check if the user has Metamask installed, which we will check with the command `window.ethereum`. If it does, it will request the accounts from the user. And if it doesn't, it will alert the user. + +Hopefully this gives you a good overview of how to interact with your contracts from a web application! diff --git a/courses/moccasin-101/7-mox-html/2-introduction/+page.md b/courses/moccasin-101/7-mox-html/2-introduction/+page.md new file mode 100644 index 000000000..d3234817e --- /dev/null +++ b/courses/moccasin-101/7-mox-html/2-introduction/+page.md @@ -0,0 +1,19 @@ +## HTML/JS Fund Me (Quick Fullstack/Front End Tutorial) + +In this lesson, we will take a look at HTML/JS Fund Me (Quick Fullstack/Front End Tutorial). + +We can find the code for this lesson on our GitHub repo: + +```python +Cyfriin/foundry-full-course-f23 +``` + +In this lesson we will be learning the basics of how our MetaMask or wallet interacts with a website. This is important knowledge. We will also be teaching you how to verify that your wallet is sending the transaction that you intended to send. + +We will not be teaching you how to build a full-stack application in this lesson. However, we are planning to launch a full-stack course on Cyfriin Updraft in the future. + +This HTML/JS Fund Me is a very basic, raw JavaScript full-website application. If you want to try to replicate it, you are free to do so. However, it is important that you understand what is going on under the hood when you interact with these websites. The knowledge that we are going to teach you here will work for every single website you interact with. This will allow you to know exactly what's going on when you interact with a website and send a transaction to the blockchain. + +Normally, we walk you through what we're going to do, but in this lesson we will jump right in. + +Assuming that you have downloaded Git, and have been working with Git and GitHub, we can actually start working with this codebase as if we had just come across it. diff --git a/courses/moccasin-101/7-mox-html/3-setup/+page.md b/courses/moccasin-101/7-mox-html/3-setup/+page.md new file mode 100644 index 000000000..cd9372e83 --- /dev/null +++ b/courses/moccasin-101/7-mox-html/3-setup/+page.md @@ -0,0 +1,18 @@ +## HTML Fund Me: Setup + +In this lesson, we'll learn how to set up a basic application to demonstrate how MetaMask interacts with a website. + +First, we need to clone the repository from Github. We can do this by opening our terminal and using the following commands: + +```bash +git clone https://github.com/CyfrinAccel0/html-fund-me-f23 +cd html-fund-me-f23 +``` + +Once we've cloned the repository, we can open the `index.html` file in our browser. This will display the basic webpage that we'll be working with. + +We'll be using the Live Server extension to run the website right from VS Code. This extension allows us to quickly preview changes and test our code. + +**[Image of a basic website with buttons and an input field]** + +This website is the foundation for our HTML Fund Me application. As we progress, we'll add functionality to allow users to interact with a smart contract through their MetaMask wallets. diff --git a/courses/moccasin-101/7-mox-html/4-metamask/+page.md b/courses/moccasin-101/7-mox-html/4-metamask/+page.md new file mode 100644 index 000000000..83857b083 --- /dev/null +++ b/courses/moccasin-101/7-mox-html/4-metamask/+page.md @@ -0,0 +1,65 @@ +## HTML Fund Me: How MetaMask Works with your Browser + +The first thing to understand when working with a website is this MetaMask bit. + +We can inspect the website by right-clicking and hitting "inspect". This opens a window that includes a console, which is a live JavaScript shell. This shell contains information about the browser. + +If we type `window` into the console, we get a list of functions that we can call on this window object. One of the objects included in the window is the `window.ethereum` object. MetaMask injects this object into the browser. This is how websites interact with MetaMask to send transactions. + +If we open the same website in a browser that doesn't have MetaMask installed, the `window.ethereum` object will be undefined. This is because the browser doesn't have an API to connect to MetaMask. + +The MetaMask documentation includes information on how to send and work with the `window.ethereum` object. + +Now in our HTML Fund Me F23 project, we can scroll over to the `index.js` file, where we can see the code the website uses to interact with a wallet. One of the first things most websites do is check to see if MetaMask exists. This is done with the following line: + +```python +if (typeof window.ethereum !== "undefined") +``` + +If MetaMask does exist, they'll call the following function: + +```python +await ethereum.request({ method: "eth_requestAccounts" }) +``` + +This function allows the website to see which accounts the user has available to send transactions from. It doesn't expose the private key, but it lets the website send transactions for the user to sign. + +In the HTML code for this project, there is a button called `connectButton`. In the JavaScript code, the following line finds the `connectButton` in the HTML: + +```python +const connectButton = document.getElementById("connectButton") +``` + +The `connectButton`'s `onclick` event calls the `connect` function. This function checks if MetaMask is present, then attempts to connect to an account using the following line: + +```python +await ethereum.request({ method: "eth_requestAccounts" }) +``` + +If we go to the MetaMask extension, we can see that we're not connected to the site. However, if we click the `connectButton` in the website, a MetaMask popup appears. This popup asks which account we want to connect. We can then select an account to connect. + +We can then call other functions, like `getBalance`, `withdraw`, or `fund`. These functions interact with the Foundry Fund Me contract that we deployed. + +We've hard-coded the contract address and ABI into a `constants.js` file: + +```python +export const contractAddress = "0xe7f1725e7734ce288f8367e1bb043e90db35f0512" +``` + +The contract address and ABI are needed to interact with the deployed contract. + +To connect to a local anvil chain that we deployed the contract to, we open a terminal and run the following command: + +```bash +make anvil +``` + +This will start a local anvil chain. We can then run the following command to deploy the contract to this chain: + +```bash +make deploy +``` + +Now, we can go back to our website and hit the "connect" button. We can then interact with the contract using the "getBalance", "withdraw", and "fund" functions. When we hit the "fund" button, a MetaMask popup appears. This popup lets us sign the transaction that the website sends to the MetaMask. + +The website never accesses the private key. The private key remains secure in the MetaMask extension. diff --git a/courses/moccasin-101/7-mox-html/5-function-selectors/+page.md b/courses/moccasin-101/7-mox-html/5-function-selectors/+page.md new file mode 100644 index 000000000..f698adbc9 --- /dev/null +++ b/courses/moccasin-101/7-mox-html/5-function-selectors/+page.md @@ -0,0 +1,59 @@ +## HTML Fund Me: Introduction to Function Selectors + +We'll look at function selectors and how they work within a smart contract. We can confirm or reject a transaction, and there is a lot of data visible within the MetaMask transaction window. + +If we go to the **DATA** tab, we see the function type is **Fund**. + +We can also see the raw transaction data in the **HEX** section of the transaction. We see the value, which is 0.1 GO or etherium. + +We are going to explore function selectors in greater detail in a later lesson, but for now, we can see how they operate. + +We have a **Fund** function within our Solidity code. + +```python +function fund() public payable { + require(msg.value.getConversionRate(priceFeed) >= MINIMUM_USD, + "You need to spend more ETH!!" + ); + // addressToAmountFunded[msg.sender] += msg.value; + s_funders.push(msg.sender); + s_addressToAmountFunded[msg.sender] += msg.value; +} +``` + +This function needs to be converted into a low-level byte code for ethereum to understand. This is where function selectors come in. + +We can see that the MetaMask transaction window shows us the function type is **Fund**. + +We can find the function selector ourselves by using a cast command. We run a cast command with a signature such as: + +```bash +cast sig "fund()" +``` + +The output for this command would be a function selector in Hex format, which is 0xb6e0d4288. + +The function signature **fund** returns the function selector. + +If we were to change the name of the function within our smart contract from **fund** to **stealMoney**, we can see that the function selector would be different. + +```python +function stealMoney() public payable { + require(msg.value.getConversionRate(priceFeed) >= MINIMUM_USD, + "You need to spend more ETH!!" + ); + // addressToAmountFunded[msg.sender] += msg.value; + s_funders.push(msg.sender); + s_addressToAmountFunded[msg.sender] += msg.value; +} +``` + +We can check this in the terminal with the command: + +```bash +cast sig "stealMoney()" +``` + +The function selector for stealMoney() is 0xa7ea5e4e, different from the fund() selector. + +We'll explore function selectors and this methodology in greater depth later. diff --git a/courses/moccasin-101/7-mox-html/6-summary/+page.md b/courses/moccasin-101/7-mox-html/6-summary/+page.md new file mode 100644 index 000000000..452555444 --- /dev/null +++ b/courses/moccasin-101/7-mox-html/6-summary/+page.md @@ -0,0 +1,29 @@ +## HTML Fund Me Completed! + +We've just finished a quick lesson on how websites interact with wallets. While it's short, this lesson is important for understanding how websites work when interacting with smart contracts. + +We learned a very basic way of checking something called the "function selector" or "function signature" to make sure that a website isn't malicious and doesn't send a bad transaction. + +We can connect to a wallet by injecting a browser extension into the browser. This can be achieved by checking the existence of `window.ethereum` in the browser. + +If we hit the "connect" button, we can see in the wallet that we've successfully connected. + +When a website sends a transaction, it first needs to obtain the provider or the RPC URL from MetaMask. This is often achieved by using the following code: + +```python +const provider = new ethers.providers.Web3Provider(window.ethereum) +``` + +The above code essentially tells MetaMask that the website needs access to the network's RPC URL. + +After connecting, a website can send transactions to our wallet. The code for a simple transaction might look like this: + +```python +const transactionResponse = await contract.fund({ + value: ethers.utils.parseEther(ethAmount), +}) +``` + +The above code sends a transaction to the wallet, which will then prompt us to sign the transaction by confirming. + +This lesson was a brief overview of the basics of front-end and wallet interactions. As we go further in the course, we'll learn more about function selectors and function signatures and how we can ensure that we protect ourselves against malicious transactions. diff --git a/courses/moccasin-101/8-mox-erc20/1-intro/+page.md b/courses/moccasin-101/8-mox-erc20/1-intro/+page.md new file mode 100644 index 000000000..094de6a65 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/1-intro/+page.md @@ -0,0 +1,178 @@ +## Moccasin ERC20 Introduction + +We are about to learn a ton about ERC20 tokens, and how to deploy them. We'll also see a bug that we'll intentionally create in our smart contract. + +### Project Walkthrough + +Let's first take a look at the project. We're working with a GitHub repo that has an ERC20 project. We'll see that this project has a few things, including a contracts folder, a tests folder, and a script folder. + +We will also be creating our own custom token. + +We'll start with a base ERC20 token and then customize it to our liking. + +### Snekmate + +We're going to use a library called Snekmate to help us build our custom token. Snekmate is a Vyper library that provides a ton of boilerplate code for common smart contract functionality. + +### Custom ERC20 + +We will create a custom token that includes: + +- `name` +- `symbol` +- `decimals` +- `eip712 version` + +### Deploying the Token + +To deploy our token, we will use the `moxc` tool. + +```bash +moxc run deploy +``` + +### Events + +We will also learn about events. Events are a way for smart contracts to communicate with the outside world. + +### Testing + +We will use a combination of unit tests and fuzz tests to ensure our ERC20 token is secure and functional. + +- **Fuzz Testing** - Fuzz tests are a type of testing that involves generating random inputs and then running them against your smart contract. Fuzz testing is particularly useful for finding security vulnerabilities. +- **Stateless and Stateful Fuzzing** - We will learn how to create both stateless and stateful fuzzers. + +### Formatting + +We will also learn how to format our smart contract code in a way that automatically ensures our code is consistent and professional. + +### Code Blocks + +```python +pragma version ^0.4.0 +``` + +```python +license MIT +``` + +```python +title snek_token +``` + +```python +author You! +``` + +```python +notice This is my ERC20 token! +``` + +```python +from ethereum.ercs import ERC20 +``` + +```python +implements: ERC20 +``` + +```python +from ethereum.ercs import ERC20Detailed +``` + +```python +implements: ERC20Detailed +``` + +```python +from snekmate.auth import ownable as ow +``` + +```python +initializes: ow +``` + +```python +from snekmate.tokens import erc20 +``` + +```python +initializes: erc20(ownable = ow) +``` + +```python +exports: erc20._interface_ +``` + +```python +NAME: constant(String[25]) = "snek token" +``` + +```python +SYMBOL: constant(String[5]) = "SNEK" +``` + +```python +DECIMALS: constant(uint8) = 18 +``` + +```python +EIP712_VERSION: constant(String[20]) = "1" +``` + +```python +initialSupply: uint256 +``` + +```python +@deploy +def __init__(initial_supply: uint256): + ow.__init__() + erc20.__init__(NAME, SYMBOL, DECIMALS, NAME, EIP712_VERSION) + erc20.mint(msg.sender, initial_supply) + self.initialSupply = erc20.totalSupply +``` + +```python +@external +def super_mint(): + # We forget to update the total supply! + amount: uint256 = as_wei_value(100, "ether") + self.totalSupply += amount + erc20.balanceOf(msg.sender) = erc20.balanceOf(msg.sender) + amount + log ERC20.Transfer(empty_address, msg.sender, amount) +``` + +```python +from script.deploy import deploy +``` + +```python +from eth_utils import to_wei +``` + +```python +from moccasine.boa_tools import VyperContract +``` + +```python +from contracts import snek_token +``` + +```python +INITIAL_SUPPLY = to_wei(1000, "ether") +``` + +```python +def deploy() -> VyperContract: + snek_contract = snek_token.deploy(INITIAL_SUPPLY) + print(f"Deployed SnekToken at {snek_contract.address}") + return snek_contract +``` + +```python +def moccasine_main() -> VyperContract: + return deploy() +``` + +This is just a brief introduction to ERC20 tokens and how to deploy them. This lesson will help you get started with this kind of smart contract. diff --git a/courses/moccasin-101/8-mox-erc20/10-built-in-interfaces/+page.md b/courses/moccasin-101/8-mox-erc20/10-built-in-interfaces/+page.md new file mode 100644 index 000000000..1811bc13c --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/10-built-in-interfaces/+page.md @@ -0,0 +1,33 @@ +## Vyper Built-in Interfaces + +This lesson will explore the built-in interfaces provided by the Vyper compiler. + +The first interface we will look at is the `IERC20` interface. This interface comes built-in with the Vyper compiler. We can import and implement this interface in our contract. + +```python +from ethereum.erc20 import IERC20 + +implements: IERC20 +``` + +In the code above, we import the `IERC20` interface from the `ethereum.erc20` library and then implement the interface. + +The `implements` keyword in Vyper makes sure our contract will not compile unless it adds all the functions of the `IERC20` interface. The interface ensures our contract will be compatible with the ERC20 standard. + +Vyper comes with a large number of interfaces that are commonly used on the Ethereum blockchain. We will be exploring these in more detail as the course progresses. + +We can also make our own interfaces and implement them in our contracts. This can be beneficial when we want to make our own standards. + +For example, we could make an interface called `my_interface.vyi`. + +```python +# my_interface.vyi +``` + +We can then import and implement this interface in our smart contract to ensure our contract follows our custom standard. + +We are going to create a deployment script so that we can deploy our first token. We will come back to this code later and look at the interfaces in more detail. + +```python +# deploy.py +``` \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/11-deploy-scripts/+page.md b/courses/moccasin-101/8-mox-erc20/11-deploy-scripts/+page.md new file mode 100644 index 000000000..d7579eaa1 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/11-deploy-scripts/+page.md @@ -0,0 +1,71 @@ +## Deploy Scripts + +Let's write a deployment script. We can create a function called: + +```python +def moccasin_main(): +``` + +We can then call another function within this one: + +```python +return deploy() +``` + +Let's create that function: + +```python +def deploy(): +``` + +We'll add a pass statement to the function for now. + +```python +pass +``` + +We'll also add an import statement at the top of the script. + +```python +from contracts import snek_token +``` + +We will import the *to_wei* function from *eth_utils* as well. + +```python +from eth_utils import to_wei +``` + +Let's assign an initial supply to our token in a variable called *INITIAL_SUPPLY* + +```python +INITIAL_SUPPLY = to_wei(1000, 'ether') +``` + +Now, let's get our contract: + +```python +snek_contract = snek_token.deploy(INITIAL_SUPPLY) +``` + +Lastly, let's add a print statement to see the address of our deployed contract. + +```python +print(f"Deployed SnekToken at: {snek_contract.address}") +``` + +Now, if we run our script, we should see our deployed contract address printed in the terminal. + +We can run our script from the terminal. + +```bash +mox run deploy +``` + +We can also run the script using the file path. + +```bash +mox run ./script/deploy.py +``` + +Both of these will output our contract address. diff --git a/courses/moccasin-101/8-mox-erc20/12-tests/+page.md b/courses/moccasin-101/8-mox-erc20/12-tests/+page.md new file mode 100644 index 000000000..484d23d6c --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/12-tests/+page.md @@ -0,0 +1,31 @@ +## Tests + +We've deployed our contract, and now we're ready to write some tests. We're not going to go in-depth with writing tests in this video, but we'll show a simple test case to get you started. + +In our project, we've created a `tests` folder. We can create a new file here called `test_token.py` and start to write our test. + +First, we'll import the `deploy` function from our `deploy.py` file: + +```python +from script.deploy import deploy, INITIAL_SUPPLY +``` + +Next, we'll create a test function: + +```python +def test_token_supply(): + snek_token = deploy() + assert snek_token.total_supply() == INITIAL_SUPPLY +``` + +The `test_token_supply` function will test the total supply of our token. We first deploy our token using the `deploy` function and store the contract in the `snek_token` variable. Then, we assert that the total supply is equal to `INITIAL_SUPPLY`, which we also imported from our `deploy.py` file. + +To run our test, we can open a terminal and run the following command: + +```bash +mox test +``` + +The test will run and pass, since we've deployed our token with an initial supply of 1000. We can write many more tests to test different functionality of our token. We can also create separate tests for our other functions like `mint`, `transfer`, and `burn`. + +The most important takeaway from this video is that we should be writing tests for our smart contracts to ensure that they function correctly. diff --git a/courses/moccasin-101/8-mox-erc20/13-events/+page.md b/courses/moccasin-101/8-mox-erc20/13-events/+page.md new file mode 100644 index 000000000..9486c2a99 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/13-events/+page.md @@ -0,0 +1,152 @@ +## Lesson 9: Events in our Raffle.sol + +In this lesson, we learn about events. You might think you're familiar with events, but you're really not. Let's dive into that. + +First, we've got our `Raffle.sol` contract. It's going to be a pretty simple raffle contract. It will have a `winner` variable and a `raffleEntries` variable. + +```python +pragma solidity ^0.8.0; + +contract Raffle { + + uint256 public raffleEntries; + address public winner; + + constructor() public { + raffleEntries = 0; + winner = address(0); + } + +``` + +Then we will add an event called `Entered`, it takes a single parameter, which is an address, and it will be used to record everyone who enters the raffle. + +```python + event Entered(address entrant); + +``` + +We'll also add an event called `WinnerSelected` with a single parameter which is an address. This is going to record who has won the raffle. + +```python + event WinnerSelected(address winner); + +``` + +Now let's add a function called `enter`. This function is going to be called by a user when they want to enter the raffle. It's going to use the `msg.sender` which is the address of the current user. + +```python + function enter() public payable { + require(msg.value >= entryFees, "You need to pay at least the entry fee to enter the raffle."); + raffleEntries++; + winner = msg.sender; + emit Entered(msg.sender); + } + +``` + +Let's add another function called `selectWinner`. This function is going to be called to randomly select the winner. + +```python + function selectWinner() public { + require(raffleEntries > 0, "There needs to be at least one entry in the raffle."); + uint256 randomIndex = uint256(keccak256(abi.encodePacked(lastBlockHash, msg.sender, raffleEntries))) % raffleEntries; + emit WinnerSelected(randomIndex); + } +} +``` + +We need to be careful, we can't just use the `block.timestamp` or `block.difficulty` for random numbers, because this is not a true random number. For now, let's assume this is a secure way to generate a random number. Of course, it's not truly random, because we're using the block, but it's fine for this example. + +So, we are now ready to test this. + +Now this is what it looks like, if we now go to Remix and copy this code in and compile it, and we can open up the console here and we can type in `raffleEntries` and we can see that it's currently zero, because we haven't had anyone enter the raffle yet. + +```bash +raffleEntries +``` + +We can type in `enter` and then `raffleEntries` again, so we can see that now there is at least one person in the raffle. + +```bash +enter +raffleEntries +``` + +If we now go to the `WinnerSelected` event, we are going to try to call it, and we can try to see the result of it. If we try to run this as it is, it's going to give us a pretty generic error here, because there is no `keccak256` and the `msg.sender` is not really allowed to be used inside an event. If we copy over the whole code, I'll show you what happens when we call it. Let me compile this. + +```bash +selectWinner +``` + +So, it says error, revert. `Invalid opcode`. This is because you can't run `keccak256` inside an event, it's not allowed. Now, how do we solve this issue? It's going to cost some gas, but we're going to need to generate the random number outside of the event. Now, the safest way to do this is to use the `blockhash`. + +Let's add another variable `lastBlockHash`. + +```python + uint256 public lastBlockHash; + +``` + +Now, we're going to make this variable `lastBlockHash` be `blockhash(block.number - 1)`. What does this mean? + +```python + constructor() public { + raffleEntries = 0; + winner = address(0); + lastBlockHash = blockhash(block.number - 1); + } + +``` + +What does this mean? This means that we are storing the hash of the block before this one. For example, let's say we're now on block number 200, so the `lastBlockHash` will be the hash of block number 199. + +Now let's modify the `selectWinner` function to use this `lastBlockHash`: + +```python + function selectWinner() public { + require(raffleEntries > 0, "There needs to be at least one entry in the raffle."); + uint256 randomIndex = uint256(keccak256(abi.encodePacked(lastBlockHash, msg.sender, raffleEntries))) % raffleEntries; + emit WinnerSelected(randomIndex); + } +} +``` + +We are now ready to test this! + +Now this is what it looks like, if we now go to Remix and copy this code in and compile it, and we can open up the console here and we can type in `raffleEntries` and we can see that it's currently zero, because we haven't had anyone enter the raffle yet. + +```bash +raffleEntries +``` + +We can type in `enter` and then `raffleEntries` again, so we can see that now there is at least one person in the raffle. + +```bash +enter +raffleEntries +``` + +If we now go to the `WinnerSelected` event, we are going to try to call it, and we can try to see the result of it. + +```bash +selectWinner +``` + +And you'll see that it successfully executes, it does not give us a revert error. Now it's going to emit an event in the `Logs` tab in Remix. + +So, this is how we've used events to record what's happening in our raffle contract. We can read and log data for our contracts. + +**Now we can write some code to read the events.** Let's go back to Solidity. We can add another function `getWinner`, and let's make this function a view function. + +```python + function getWinner() public view returns(address){ + return winner; + } + +} +``` + +This function is going to simply return the `winner`. + +Now if we go back to Remix and we compile this, we can call `getWinner` and we can see that it's returning the address `0x...` We can go back to the `Events` tab and we can see that this event is now being emitted. So this is how we can emit events and then read events in our raffle contract. If you have any questions about this, please ask! /home/equious/Nevermore/courses/moccasin-101/8-mox-erc20\13-events\+page.md diff --git a/courses/moccasin-101/8-mox-erc20/14-testing-events/+page.md b/courses/moccasin-101/8-mox-erc20/14-testing-events/+page.md new file mode 100644 index 000000000..7cb678d98 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/14-testing-events/+page.md @@ -0,0 +1,51 @@ +## Testing for Events + +We've learned what events are and what they're used for. Let's look at our `snek_token` and see if we need to add it anywhere. Okay, maybe for this mint function. Okay, maybe for our initial deploy, but actually, does this mint function already emit logs? Let's take a look. And, yep, it sure does. Okay, so we don't need to do any logging on our `snek_token`. Our `snek_token` is good. + +Let's go to our test token though and let's learn how to read these logs to make sure that we're actually using them. + +So we'll do a little test or a def test `token_emits_event` like this. + +```python +def test_token_emits_event(): + snek_token = deploy() + with boa.env.prank(snek_token.owner): + snek_token.transfer(RANDOM_USER, INITIAL_SUPPLY) + logs = snek_token.get_logs() + log_owner = logs[0].topics[0] + assert log_owner == snek_token.owner() +``` + +We'll run this line again, which again is not good. We should be using this as a fixture. But then, we'll do `with boa.env.prank(snek_token.owner):` Let's just be the `snek_token.owner`. + +We need to import boa. + +```python +import boa +``` + +Let's do `snek_token.transfer(boa.accounts[1], 100)` to some random users. So I'm going to say `RANDOM_USER = boa.env.generate_address('random_user')` like this. + +```python +RANDOM_USER = boa.env.generate_address('random_user') +``` + +We'll do `snek_token.transfer(RANDOM_USER, INITIAL_SUPPLY)`. So this transfer function, if we go to def `transfer`, this is on the `erc20.vy` and it calls this `self.transfer`. This internal transfer function, which is a whole bunch of code here, but basically, this is what updates the balance, right? So the balance of a certain address is really just a mapping. So when people are like, "Oh, I have X number of tokens," what they're really saying is "Oh, I have seven in my mapping on that contract," or however many tokens they have, right? That's really all having tokens is, is having an entry in this mapping in this smart contract. Kind of crazy, right? Well, when we do `snek_token.transfer`, this transfer emits this `IERC20.Transfer(owner, to, amount)`. So this transfer event is actually specified in the `IERC20` interface. + +And events, by the way, they look just like structs. To make an event or a log, you just have event, the name of the event, and any of the parameters in it. And then, you emit it like this or you log it like this. So in the `IERC20` interface, it has those event objects. Specified, one such is the transfer event, but it takes the owner, the to, and the amount and it emits them. + +So we can actually get this log back in our test here after we do the transfer function, but still inside of this this with context, we'll say we can say logs equals `snek_token.get_logs` like this. And this will return a little array of logs. So I can even do a little breakpoint in here and let's run this test. So we'll do `mox test -k` paste that in. We'll pop us into this little terminal here. So, let's see what the the logs variable looks like. So the logs has a single event in it. So this is an array of different logs or events that have happened. And there's a single transfer event in it with a sender, receiver, and a value. So we have We see sender, receiver, value. We'll go back to the `erc20`. We go down to this. We have sender, receiver, and value exactly as the transfer log is doing. + +So to do a little assertion here we could say, let's even look at the `snek_token.owner`. `snek_token.owner` is this address here. We can see that it is also the sender. So what we could do is we could say the log owner is going to be equal to the logs, zero. So the first log or the first event.topics, which we learned about from the video we just watched, zero, which is going to be this sender. And we could do `assert log_owner == snek_token.owner`. We can even run this. We can even run this in our little terminal down here. Let's paste that line in here. + +```bash +mox test -k test_token_emits_event +``` + +We get a little error cuz we're editing our test in a breakpoint, which is annoying. And then, we can run this. + +```bash +mox test -k test_token_emits_event +``` + +And we get true. So, I'm going to type `q`. We're going to quit that. Got rid of the breakpoint. We could also add another assert in here if we want it. We can say assert `snek_token.balanceOf(RANDOM_USER) == INITIAL_SUPPLY`. diff --git a/courses/moccasin-101/8-mox-erc20/15-formatting/+page.md b/courses/moccasin-101/8-mox-erc20/15-formatting/+page.md new file mode 100644 index 000000000..0083f04e4 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/15-formatting/+page.md @@ -0,0 +1,45 @@ +## Formatting: Vyper & Python + +We're going to go into fuzz testing in just a minute, but first we'll take a look at some ways to format our Vyper and Python code. + +Our code looks like this: + +```python +script +from moccasin.boa.tools import VyperContract +from eth_utils import to_wei +INITIAL_SUPPLY = to_wei(1000, "ether") +from contracts import snek_token +def deploy() -> VyperContract: + snek_contract = snek_token.deploy(INITIAL_SUPPLY) + print(f"Deployed to {snek_contract.address}") + return snek_contract +def moccasin_main() -> VyperContract: + return deploy() +``` + +This code is not formatted well, with imports in a strange order. We can either manually change this code, or we can use a tool. + +Let's go ahead and install the **ruff** formatter. We can do this through UV's command palette by typing "format document" and hitting enter. + +We can also format our code in the terminal using the following command: + +```bash +uv run ruff check --select I--fix +``` + +This will automatically fix a lot of our imports. + +What about Vyper? We can also format our Vyper code using a tool called **mamushi**. This tool can be installed using the following command: + +```bash +uv add mamushi +``` + +After installing mamushi, we can run the following command to format our contracts folder: + +```bash +uv run mamushi contracts/ +``` + +This will reformat our entire `snek_token.vy` file to be much nicer looking. diff --git a/courses/moccasin-101/8-mox-erc20/16-justfile/+page.md b/courses/moccasin-101/8-mox-erc20/16-justfile/+page.md new file mode 100644 index 000000000..e0547d1da --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/16-justfile/+page.md @@ -0,0 +1,29 @@ +## Optional: Justfile + +We can use something called a Justfile to run a bunch of commands easily. This is 100% optional but, it's a handy way to save and run project-specific commands. If we're very specific about how we like to have formatters, we can create a Justfile. + +We can create a command that's a combination of a bunch of commands, but are really easy for us to run. For example, if we want to run the formatter, we can add the following to our Justfile: + +```python +# Run the formatter +format: + @just fmt +``` + +We can also add a command to run our formatter: + +```python +# Run the formatter +format: + uv run ruff check --select I --fix +``` + +We're going to use the VS Code Just extension. We can install this from the VS Code marketplace. We can change the language mode of our Justfile from Python to Just. + +Now, we can run the Just format command: + +```bash +just format +``` + +This will run both of the formatter commands for us. We can add a lot of commands to our Justfile and use this as a way to run multiple commands quickly. diff --git a/courses/moccasin-101/8-mox-erc20/17-vheaders/+page.md b/courses/moccasin-101/8-mox-erc20/17-vheaders/+page.md new file mode 100644 index 000000000..4bf83d5f0 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/17-vheaders/+page.md @@ -0,0 +1,74 @@ +## Section Headers in Vyper + +We've learned about how to format our Vyper code with tools such as `black` and `ruff`. However, when our code grows, it can become difficult to navigate and understand. Section headers help with this. + +Section headers help us organize our code and separate it into different sections. For instance, we can separate the **Imports**, **State Variables**, and **Functions** sections to make our code easier to read and understand. + +There are two main ways to create section headers: + +- Using `vheader` Python package +- Using `headers-vy` Rust package + +## `vheader` Python Package + +We can use the `vheader` Python package to generate section headers in our Vyper code. Let's look at how to install this package. + +First, we go to the repository's `README` file. + +Then, we copy the following command to install `vheader`: + +```bash +uv tool install vheader +``` + +Once installed, we can run the following command in our terminal: + +```bash +vheader imports +``` + +This command will generate a section header for the **Imports** section in our Vyper code. + +We can do the same for other sections: + +```bash +vheader state variables +``` + +```bash +vheader functions +``` + +This will add section headers to the **State Variables** and **Functions** sections. + +## `headers-vy` Rust Package + +There is also the `headers-vy` Rust package that we can install. + +To install `headers-vy`, we need to make sure Rust and Cargo are installed on our machine. The `README` file has instructions for installing those if needed. + +Once we have Rust and Cargo installed, we can clone the `headers-vy` repository and run the following command in our terminal to install the package: + +```bash +cargo install --path . +``` + +Once the package is installed, we can run the following command in our terminal: + +```bash +./headers-vy "testing 123" +``` + +This command will generate the following header: + +```bash +# ################################################## +# # +# TESTING 123 # +# # +# ################################################## +``` + +## Conclusion + +Adding section headers to our Vyper code can significantly improve readability and organization. By separating our code into distinct sections, we can make it easier to navigate and understand, ultimately leading to more maintainable and efficient code. diff --git a/courses/moccasin-101/8-mox-erc20/18-fuzzing/+page.md b/courses/moccasin-101/8-mox-erc20/18-fuzzing/+page.md new file mode 100644 index 000000000..ee6871015 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/18-fuzzing/+page.md @@ -0,0 +1,130 @@ +## Fuzz Testing: Introduction + +Fuzz Testing is a technique for finding vulnerabilities in software by feeding it random data. We can use this technique to find bugs and vulnerabilities in smart contracts. + +Let's start by looking at a simple example: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract MyContract { + uint256 public shouldAlwaysBeZero = 0; + uint256 private hiddenValue = 0; + function doStuff(uint256 data) public { + if (data == 2) { + shouldAlwaysBeZero = 1; + } + if (hiddenValue == 7) { + shouldAlwaysBeZero = 1; + } + hiddenValue = data; + } +} +``` + +We have a function called `doStuff` which takes an integer as an input parameter. We know that the variable `shouldAlwaysBeZero` should always be zero. This is known as an invariant. + +The invariant is a property of the system that must always hold. + +Let's look at a unit test for this code: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {MyContract} from "./MyContract.sol"; + +contract MyContractTest { + MyContract exampleContract; + + function testAlwaysGetZeroUnit() public { + uint256 data = 0; + exampleContract.doStuff(data); + assert(exampleContract.shouldAlwaysBeZero() == 0); + } +} +``` + +We pass in a single data point, call the function, and then check if our invariant is still true. We might think that our code is covered, but if we look closer at the `doStuff` function we see that if our data input is 2, `shouldAlwaysBeZero` will be set to 1. This will break our invariant, since `shouldAlwaysBeZero` will no longer be zero. + +In order to find this bug, we can write a fuzz test in Foundry. This will allow us to automatically generate random input data and run it through our code to try to find edge cases. + +Let's write a fuzz test for this example: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {MyContract} from "./MyContract.sol"; +import {Test} from "forge-std/Test.sol"; + +contract MyContractTest is Test { + MyContract exampleContract; + + function testAlwaysGetZeroFuzz(uint256 data) public { + // uint256 data = 0; + exampleContract.doStuff(data); + assert(exampleContract.shouldAlwaysBeZero() == 0); + } +} +``` + +In the fuzz test, we will remove the manually selected data and let Foundry automatically randomize the data. + +We can run this fuzz test from the terminal using this command: + +```bash +forge test -m testAlwaysGetZeroFuzz +``` + +Foundry will then automatically run through our code with a ton of different random inputs to try to find a scenario where the invariant is broken. + +We can see that the fuzz test has successfully identified a counter-example. This allows us to go back to our code and fix the bug. + +Foundry fuzz tests are known as stateless fuzz tests, which means that the state of the previous run is discarded for the next run. + +There's another type of fuzz test that we can write in Foundry called a stateful fuzz test. This is where we use the ending state of our previous fuzz run as the starting state of the next fuzz run. + +To write a stateful fuzz test, we use the `invariant` keyword. + +Let's write a stateful fuzz test for our example: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {MyContract} from "./MyContract.sol"; +import {Test} from "forge-std/Test.sol"; +import {StdInvariant} from "forge-std/StdInvariant.sol"; + +contract MyContractTest is StdInvariant, Test { + MyContract exampleContract; + + function setUp() public { + exampleContract = new MyContract(); + targetContract(address(exampleContract)); + } + + function invariant_testAlwaysReturnsZero() public { + assert(exampleContract.shouldAlwaysBeZero() == 0); + } +} +``` + +We first need to import the `StdInvariant` contract and inherit it in our test contract. + +We then need to tell Foundry which contract to run the fuzz test on. Since we only have one contract, we'll tell Foundry that `exampleContract` should be used as the target contract. + +Lastly, we will write a function called `invariant_testAlwaysReturnsZero` that asserts that `shouldAlwaysBeZero` is equal to zero. + +Now when we run this test, Foundry will randomly run through our code, calling our functions in a random order with random data. + +Since we only have one function `doStuff`, it will call `doStuff` with random data, then call `doStuff` again with random data, then call `doStuff` again with random data, and so on. If it happens to call `doStuff` with an input of 7, it will set `hiddenValue` to 7. The next time it calls `doStuff`, it will set `shouldAlwaysBeZero` to 1. When it runs our invariant test, it will find that our invariant is broken. + +We can see in the terminal that the invariant test found a failing test with this sequence: + +```bash +[FAIL. Reason: Assertion violated] +[Sequence] +sender=0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/19-stateless-fuzz/+page.md b/courses/moccasin-101/8-mox-erc20/19-stateless-fuzz/+page.md new file mode 100644 index 000000000..5ef3d096d --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/19-stateless-fuzz/+page.md @@ -0,0 +1,87 @@ +## Stateless Fuzzing in Python + +We're going to start this lesson by learning how to write stateless fuzz tests. First, we need to understand what fuzz tests are and the difference between stateful and stateless fuzz tests. + +Stateful fuzz tests need to save state information, allowing them to be more complex, and stateless fuzz tests do not need to save state information. This means that stateless fuzz tests are typically simpler and easier to write, so that's what we're going to focus on first. + +Let's get started by creating a new folder called sub lesson. Inside of that folder, let's copy two files from our GitHub repository: + +* `stateful_fuzz_solvable.vy` +* `stateless_fuzz_solvable.vy` + +These files contain code that we will use to write our fuzz tests. If we open the `stateless_fuzz_solvable.vy` file we'll see this code: + +```python +# pragma version 0.4.0 + +# title always_return_input +@license MIT +# notice INVARIANT: always_returns_input_number should always return the input number + +some_number: public(uint256) + +@external +@pure +def always_returns_input_number(input_number: uint256) -> uint256: + @param input_number The input number to check + if input_number == 2: + return 0 + return input_number +``` + +This is a very simple smart contract that has one function called `always_returns_input_number`. This function has an invariant that it should always return the same value that is passed into the function. + +We also see that there is an exception to this invariant, where the function returns 0 if it is passed the value of 2. + +Now, we're going to create a new file called `test_stateless.py`, which will contain our stateless fuzz tests. We need to start by importing the `pytest` and `hypothesis` libraries. Hypothesis comes included with Mocassin, which means that we can just import it directly. Let's also import the `StatelessFuzzSolvable` contract. We do this with these commands: + +```python +import pytest +from contracts.sub_lesson import stateless_fuzz_solvable + +``` + +Now, we need to create a fixture to deploy the smart contract: + +```python +@pytest.fixture(scope="function") +def contract(): + return stateless_fuzz_solvable.deploy() +``` + +This fixture will be used to deploy the contract before running each of our tests. + +To write our actual fuzz test we need to import the `given` function from hypothesis. + +```python +from hypothesis import given +``` + +Now, we can start writing our test! + +```python +@given(input=strategies.uint256) +def test_always_returns_input(contract, input): + print(input) + assert contract.always_returns_input_number(input) == input +``` + +This test uses the `given` function from hypothesis to generate a random `uint256` value. Then we pass that value into the `always_returns_input_number` function and assert that it returns the same value. + +One important thing to note is that Hypothesis doesn't work well with fixtures. We're going to add a setting to our test that will allow it to function properly. + +```python +@settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) +``` + +This setting tells Hypothesis to ignore the warning about using fixtures. + +Now, let's run this test and see the results. We will use this command to run the test: + +```bash +mox test -k test_always_returns_input -s +``` + +The `-s` flag allows us to see the output of the `print` function in the test. We'll see that Hypothesis has generated and tested a bunch of random `uint256` values and has found one example where the invariant is broken. This is because the function returns 0 when given the value of 2. + +This concludes our look at stateless fuzzing. We've successfully written a fuzz test that found a bug in our simple smart contract. diff --git a/courses/moccasin-101/8-mox-erc20/2-setup/+page.md b/courses/moccasin-101/8-mox-erc20/2-setup/+page.md new file mode 100644 index 000000000..fcc7a149d --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/2-setup/+page.md @@ -0,0 +1,3 @@ +--- + title: Setup + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/20-stateful-fuzz/+page.md b/courses/moccasin-101/8-mox-erc20/20-stateful-fuzz/+page.md new file mode 100644 index 000000000..44c25f29a --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/20-stateful-fuzz/+page.md @@ -0,0 +1,3 @@ +--- + title: Stateful Fuzz + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/21-workshop/+page.md b/courses/moccasin-101/8-mox-erc20/21-workshop/+page.md new file mode 100644 index 000000000..789149836 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/21-workshop/+page.md @@ -0,0 +1,3 @@ +--- + title: Workshop + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/22-rng/+page.md b/courses/moccasin-101/8-mox-erc20/22-rng/+page.md new file mode 100644 index 000000000..353327f11 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/22-rng/+page.md @@ -0,0 +1,3 @@ +--- + title: Rng + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/23-cei/+page.md b/courses/moccasin-101/8-mox-erc20/23-cei/+page.md new file mode 100644 index 000000000..fa728fee9 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/23-cei/+page.md @@ -0,0 +1,3 @@ +--- + title: Cei + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/24-mox-console/+page.md b/courses/moccasin-101/8-mox-erc20/24-mox-console/+page.md new file mode 100644 index 000000000..14036c356 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/24-mox-console/+page.md @@ -0,0 +1,3 @@ +--- + title: Mox Console + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/25-recap/+page.md b/courses/moccasin-101/8-mox-erc20/25-recap/+page.md new file mode 100644 index 000000000..385e6edfe --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/25-recap/+page.md @@ -0,0 +1,3 @@ +--- + title: Recap + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/3-custom-pyproject/+page.md b/courses/moccasin-101/8-mox-erc20/3-custom-pyproject/+page.md new file mode 100644 index 000000000..d9e46800a --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/3-custom-pyproject/+page.md @@ -0,0 +1,3 @@ +--- + title: Custom Pyproject + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/4-what-is-erc20/+page.md b/courses/moccasin-101/8-mox-erc20/4-what-is-erc20/+page.md new file mode 100644 index 000000000..b099a949d --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/4-what-is-erc20/+page.md @@ -0,0 +1,3 @@ +--- + title: What Is Erc20 + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/5-build-it/+page.md b/courses/moccasin-101/8-mox-erc20/5-build-it/+page.md new file mode 100644 index 000000000..dcfcc37bb --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/5-build-it/+page.md @@ -0,0 +1,3 @@ +--- + title: Build It + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/6-mox-libs-git/+page.md b/courses/moccasin-101/8-mox-erc20/6-mox-libs-git/+page.md new file mode 100644 index 000000000..98e5b17b8 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/6-mox-libs-git/+page.md @@ -0,0 +1,3 @@ +--- + title: Mox Libs Git + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/7-mox-libs-pypi/+page.md b/courses/moccasin-101/8-mox-erc20/7-mox-libs-pypi/+page.md new file mode 100644 index 000000000..ce3cbc423 --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/7-mox-libs-pypi/+page.md @@ -0,0 +1,3 @@ +--- + title: Mox Libs Pypi + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/8-mox-idk/+page.md b/courses/moccasin-101/8-mox-erc20/8-mox-idk/+page.md new file mode 100644 index 000000000..9ee318b3d --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/8-mox-idk/+page.md @@ -0,0 +1,3 @@ +--- + title: Mox Idk + --- \ No newline at end of file diff --git a/courses/moccasin-101/8-mox-erc20/9-init-supply/+page.md b/courses/moccasin-101/8-mox-erc20/9-init-supply/+page.md new file mode 100644 index 000000000..9121611ae --- /dev/null +++ b/courses/moccasin-101/8-mox-erc20/9-init-supply/+page.md @@ -0,0 +1,3 @@ +--- + title: Init Supply + --- \ No newline at end of file diff --git a/courses/moccasin-101/9-how-to-get-hired/1-intro/+page.md b/courses/moccasin-101/9-how-to-get-hired/1-intro/+page.md new file mode 100644 index 000000000..b7e64e913 --- /dev/null +++ b/courses/moccasin-101/9-how-to-get-hired/1-intro/+page.md @@ -0,0 +1,3 @@ +--- + title: Intro + --- \ No newline at end of file diff --git a/courses/moccasin-101/9-how-to-get-hired/2-workshop/+page.md b/courses/moccasin-101/9-how-to-get-hired/2-workshop/+page.md new file mode 100644 index 000000000..789149836 --- /dev/null +++ b/courses/moccasin-101/9-how-to-get-hired/2-workshop/+page.md @@ -0,0 +1,3 @@ +--- + title: Workshop + --- \ No newline at end of file diff --git a/courses/vyper-101/1-favorites/1-welcome/+page.md b/courses/vyper-101/1-favorites/1-welcome/+page.md new file mode 100644 index 000000000..6960b3f5a --- /dev/null +++ b/courses/vyper-101/1-favorites/1-welcome/+page.md @@ -0,0 +1,27 @@ +## Welcome to Vyper Fundamentals + +This course is especially exciting to us because this is the Python and Vyper edition. Something we haven't done before in the past. In the past 5 years that we've been doing this, we've done Python, but never Vyper. Now Python is positioning itself to be one of the most important languages to learn in the coming years. In the age of AI. + +When it comes to financial technology companies like hedge funds or asset managers, all the portfolio managers or traders, they're using Python. Why? Because Python is very easy to read, easy to script, and it's a production language to build very powerful applications with. And additionally, looking at the AI landscape, Python is dominant there as well with tools like PyTorch and TensorFlow, and all these libraries and packages built specifically around the AI landscape. + +We're also looking forward to advancements in languages like Mojo, which are going to be languages literally built to run in the AI environment. + +So, you, who is watching this video right now, you are setting yourself up to have an edge over all these waves of people coming in, because you are learning this technology of the future with Python, and specifically with Vyper. + +Now we'll learn more about Vyper in the coming videos, but basically, Vyper is a smart contract development language. So, we're actually going to learn Vyper, and learning Vyper, you will learn Python along the way, which is very nice. + +So, if you have zero Python experience, us teaching you Vyper here will also teach you some Python. Now there are some differences between the languages, between Vyper and Python. However, a lot of the syntax is going to be very similar. So, whether you're brand new to Python, whether you're brand new to Vyper, this is the course for you. Now, it would be beneficial if you had a little bit of Python experience before jumping into this. That's going to make your life a lot easier, you're going to move a lot quicker. There are plenty of courses on Free Code Camp that will teach you these things along the way. + +However, we're going to assume that you know nothing about Python. We're going to assume that you're a beginner Python developer. That way, you can follow along, and you can learn production professional Python code as well in this curriculum, too. Now, that being said, if you do already know Python, which that's great as well, you can skip over some of the sections where we teach you Python, and at the beginning of those videos I will label them, saying hey, we're going to teach some Python skills in this lesson, feel free to skip. + +Now a question that you're probably asking is, "Why Vyper? Why would I learn Vyper over Solidity?" + +Looking at this chart here as of recording, Vyper has around 3% of the total TVL of smart contracts. Basically, what this means is not a whole lot of money is in Vyper smart contracts. However, the reason you would want to do this is, because there are a couple applications that have a ton of money in Vyper smart contracts. Curve, one of the largest DEXs out there is built exclusively on Vyper. Lido, one of the largest staking platforms, is built with a lot of Vyper. Yearn Finance as well has a lot of Vyper. Vyper, by its nature, is meant to be easy to learn, easy to read, easy to write, and very easy for AIs to understand. And importantly, very easy for security researchers to understand, so that your smart contracts have fewer bugs in them. Which, in the smart contracts space, is incredibly important, because a single bug can mean you lose millions of dollars. + +And additionally, because Vyper is in this Python tooling world, you are going to step into the AI, to the finance, to the fintech world, and have all these powerful Pythonic tools and libraries at your disposal already. + +So, jumping into the Python and Vyper landscape, will set you up for this future of AI and finance. So I am incredibly excited for you to be on this journey with us here, learning Vyper, learning smart contract development because we've got a lot in store for you. + +At the end of Vyper Fundamentals, you will understand the basics of Vyper, you'll even learn a little bit of Python without even knowing it. You'll learn some tools and techniques for interacting with AI to help you write your smart contracts, help you write your code. You will have deployed your first three smart contracts yourself, and you'll be ready to move into a more professional environment, and just by you being here, just by you writing your first line of code, you are a smart contract developer and welcome to Web3. + +And if you missed this section during Blockchain Basics, let's go ahead and get started with some best practices so that we can set you up for success for learning here. diff --git a/courses/vyper-101/1-favorites/10-contract-design/+page.md b/courses/vyper-101/1-favorites/10-contract-design/+page.md new file mode 100644 index 000000000..ae4696ec7 --- /dev/null +++ b/courses/vyper-101/1-favorites/10-contract-design/+page.md @@ -0,0 +1,96 @@ +We're going to make a very minimal favorite things list. We want to store favorite numbers and favorite people with their favorite numbers. + +Here is some basic code to get started: + +```python +# favorite things list: +# favorite numbers +# favorite people with their favorite number +``` + +To store these items, we're going to use mappings. A mapping is like a dictionary or hashmap. We'll use the key to be the person and the value to be their favorite number. + +```python +# favorite things list: +# favorite numbers +# favorite people with their favorite number + +favorite_numbers: dict[uint256, uint256] +favorite_people: dict[address, uint256] +``` + +Mappings have a default value. If we don't assign a value, the default value is 0. + +```python +# favorite things list: +# favorite numbers +# favorite people with their favorite number + +favorite_numbers: dict[uint256, uint256] +favorite_people: dict[address, uint256] + +def set_favorite_number(number: uint256): + favorite_numbers[number] = number +``` + +Now we have a function to set a favorite number. This is a pretty simple and straightforward function. We take a number and set it to the same number, but we could change this later. + +```python +# favorite things list: +# favorite numbers +# favorite people with their favorite number + +favorite_numbers: dict[uint256, uint256] +favorite_people: dict[address, uint256] + +def set_favorite_number(number: uint256): + favorite_numbers[number] = number + +def set_favorite_person(person: address, number: uint256): + favorite_people[person] = number +``` + +We have another function to set the favorite person. It takes a person, their address, and their favorite number. We set it to the dictionary where we store the favorite person. + +```python +# favorite things list: +# favorite numbers +# favorite people with their favorite number + +favorite_numbers: dict[uint256, uint256] +favorite_people: dict[address, uint256] + +def set_favorite_number(number: uint256): + favorite_numbers[number] = number + +def set_favorite_person(person: address, number: uint256): + favorite_people[person] = number + +def get_favorite_number(number: uint256) -> uint256: + return favorite_numbers[number] +``` + +Lastly, we have a function to get the favorite number. It takes a number and then it returns the favorite number. It simply returns the value of the number from the dictionary. + +```python +# favorite things list: +# favorite numbers +# favorite people with their favorite number + +favorite_numbers: dict[uint256, uint256] +favorite_people: dict[address, uint256] + +def set_favorite_number(number: uint256): + favorite_numbers[number] = number + +def set_favorite_person(person: address, number: uint256): + favorite_people[person] = number + +def get_favorite_number(number: uint256) -> uint256: + return favorite_numbers[number] + +def get_favorite_person(person: address) -> uint256: + return favorite_people[person] +``` + +We now have a function to get the favorite person, which takes a person's address and returns their favorite number. diff --git a/courses/vyper-101/1-favorites/11-vyper-types/+page.md b/courses/vyper-101/1-favorites/11-vyper-types/+page.md new file mode 100644 index 000000000..40c991ed6 --- /dev/null +++ b/courses/vyper-101/1-favorites/11-vyper-types/+page.md @@ -0,0 +1,39 @@ +## Vyper Types + +We're going to learn about types in Vyper, and to get started we're going to need to know how to store different things in our smart contract. We're going to go to the Vyper documentation, which we can find at docs.vyperlang.org. + +If we scroll down the left side here, we can see all the different types that we can create in a smart contract. And to store different values like our favorite number or different people, we'll have to assign each value to a type and then assign it to a variable. + +So, we have a lot of different types in Vyper, and some of the main ones are Booleans, signed integers, unsigned integers, decimals, addresses, byte arrays and then a number of other ones as well. + +For example, if we were to type in here: + +```python +has_favorite_number: bool +``` + +This would mean this `has_favorite_number` could either be `True` or `False` since it's a Boolean. + +Now, if you were to try to compile this right now, by hitting command S, you'd get a little error saying "storage variables cannot have an initial value". So for now, we can't actually give it a value here, but this line would say that `has_favorite_number` is going to be of type Boolean, which means it'll either be `True` or `False`. + +We could also say: + +```python +my_favorite_number: uint256 +``` + +This `uint256` is saying that `my_favorite_number` is an unsigned integer. This means `my_favorite_number` is going to be an unsigned integer of 256 bits. Don't worry too much about the bits part for now, but unsigned just means that this can only be a positive number. + +We can also have an address: + +```python +my_address: address +``` + +An address is going to be an address in the blockchain space. For example, in our Metamask, we can click this little copy button, go back to Remix, and type `my_address: address` and then set it equal to the value that we copied. + +So we have Booleans, signed integers, unsigned integers and we can also do decimals. Which decimals are a more advanced value that we won't be using for quite some time, but you could say `my_decimal: decimal` and you also have to add `enable decimals` to the compiler. + +You can also have an M byte wide fixed size byte array, which we can do something like `my_bytes: byte32` . + +And there are several other more advanced types that we can use to store different things in our smart contract, but those are some of the basics. diff --git a/courses/vyper-101/1-favorites/12-visibilitiy/+page.md b/courses/vyper-101/1-favorites/12-visibilitiy/+page.md new file mode 100644 index 000000000..60193962a --- /dev/null +++ b/courses/vyper-101/1-favorites/12-visibilitiy/+page.md @@ -0,0 +1,43 @@ +## Variable Visibility & Deploying to the Remix VM + +We've learned about the different types of values that Vyper variables can hold. Now, we'll see how to use these variables in practice by deploying a smart contract. + +We'll start with a simple smart contract that has a single state variable called `my_favorite_number`. This variable will be a `uint256`, which is an unsigned integer. We'll initialize it to 0. + +```python +#pragma version 0.4.0 +@license MIT +my_favorite_number: uint256 = 0 +``` + +The syntax for declaring variables in Vyper is similar to Python. We use a colon to separate the variable name from its type. + +In Vyper, variables declared at the top level of a contract are called _state variables_ or _storage variables_. These variables are stored on the blockchain and can be accessed by anyone. + +To test our smart contract, we'll deploy it to the Remix VM. The Remix VM is a local blockchain simulator that lets us run and test our contracts without deploying them to a real blockchain. + +We'll start by compiling our contract. + +```bash +# In Remix, go to the "Compiler" tab and compile your contract. +``` + +Once the contract is compiled, we can deploy it by going to the "Deploy & Run Transactions" tab. The Remix VM will provide us with a dummy account and gas limit that we can use to deploy the contract. + +We can also delete a deployed contract by clicking the "X" button next to the contract address. + +Since we haven't added any functionality to our contract yet, we only have low-level interactions available. This means we can't see the value of our `my_favorite_number` variable yet. To make it visible, we need to change its _visibility_. + +We can make a variable visible to everyone by adding the keyword `public` before its type. + +```python +#pragma version 0.4.0 +@license MIT +my_favorite_number: public(uint256) = 0 +``` + +By default, variables in Vyper are _internal_, which means they can only be accessed from within the contract. By making our variable public, we allow anyone to access its value. + +Now, if we deploy our contract again and click on the "my_favorite_number" button, we'll be able to see its value. This will confirm that it was initialized to 0. + +The `public` keyword is important for making data on the blockchain accessible to everyone. It's a key part of building decentralized applications, as it allows users to interact with your contract in a secure and transparent way. diff --git a/courses/vyper-101/1-favorites/13-functions/+page.md b/courses/vyper-101/1-favorites/13-functions/+page.md new file mode 100644 index 000000000..75a681a1b --- /dev/null +++ b/courses/vyper-101/1-favorites/13-functions/+page.md @@ -0,0 +1,84 @@ +## Functions: The `def` Keyword + +We can create a function so we can actually save a value to our `my_favorite_number`. The way we declare a function is to use the `def` keyword. + +The `def` keyword can be thought of as standing for definition. + +```python +def store +``` + +We will create a function called `store`. + +```python +def store() +``` + +Next we need to add the parentheses. We put whatever parameters this `store` function is going to take inside the parentheses. For example, we need to tell our `store` function what number we want to update `my_favorite_number` with. + +```python +def store(new_number) +``` + +We will tell our function to update `my_favorite_number` with `new_number`, which will be of type `uint256` as well. + +```python +def store(new_number: uint256) +``` + +Next, we need to add a colon to indicate that the next lines will be the subset of code for our function. + +```python +def store(new_number: uint256): +``` + +We will hit enter and you will notice that we immediately get a tab. Vyper and Python are tab-based languages, and the tab tells our compiler that this line of code is associated with our function. If we were to type over here, the compiler would have a hard time knowing that this code is part of our function. + +```python +def store(new_number: uint256): + adsasdf +``` + +We need to add this tab which tells our compiler that this line of code is part of the function. + +```python +def store(new_number: uint256): + self.my_favorite_number +``` + +We will say: "Okay, self dot my favorite number equals or is set to new number". + +```python +def store(new_number: uint256): + self.my_favorite_number = new_number +``` + +Now, `self` is a very specific keyword in Vyper. It refers to the contract that we are writing. So, when we say `self.my_favorite_number`, we are saying "point to the storage variable or the state variable `my_favorite_number`." + +If we didn't have `self` the compiler would get a little confused. It would say, "Okay, well what are you referring to? Is it my favorite number a new variable, or is it a state variable?" We put `self` in here, and the compiler says, "Ah, that's a state variable you're dealing with, got it. Makes sense." + +So, what this function is going to do is: we are going to pass in a number, like seven, and we're going to save it to `my_favorite_number`. + +```python +@external +def store(new_number: uint256): + self.my_favorite_number = new_number +``` + +Now, similar to how we need the `public` keyword up here so we can actually get that blue button to read `my_favorite_number`, functions have the same concept. If we don't give them some type of visibility, it will automatically be considered internal. + +We need to add `@external` right above our function definition. This is known as a decorator. Once again, these exist in Python as well. The `external` keyword means that this function can be called by us, by people outside of this smart contract. This doesn't quite make sense yet, but just ignore it for now. We will explain it deeper as we go on. + +But, now that we have this kind of boilerplate code, we can make sure to compile this. And we can deploy this. Let's go ahead and remove that other contract and re-hit deploy. Then let's hit the little drop down here. Now, you'll see we have two buttons: we have `my_favorite_number` and we have `store`. + +The reason `store` is greyed out is because we need to give it an input. If we hit `my_favorite_number`, remember this is a `public` variable, so we get this blue button. We add the number `7` in here, and then I hit `store`. + +It will actually send a transaction to store the number seven at `my_favorite_number`. If we look in our little terminal section here, we can actually see that when I click this store button, these little statements show up. These are transactions. + +So, storing seven or their simulated transactions on this little fake Remix environment. So storing the number seven actually is updating the state of this smart contract. And to update state, you actually have to send a transaction. So `my_favorite_number` is being populated with, well, if it's getting started as zero, but then it's getting set to seven. And when we call `my_favorite_number` now, we now get the number seven here. It's really really tiny, but we get a seven back. If I were to add some crazy number, like this, hit the `store` button and then hit `my_favorite_number`, we see it's been updated here. + +```bash +store(1235) +``` + +We see it's been updated here. diff --git a/courses/vyper-101/1-favorites/14-func-visibility/+page.md b/courses/vyper-101/1-favorites/14-func-visibility/+page.md new file mode 100644 index 000000000..0a24251a8 --- /dev/null +++ b/courses/vyper-101/1-favorites/14-func-visibility/+page.md @@ -0,0 +1,67 @@ +## Visibility in Solidity + +In this lesson, we will explore the concept of visibility within Solidity contracts. Visibility is a fundamental aspect of how we control access to different components within a contract. We will focus on the keywords `public` and `internal`. + +The concept of visibility allows us to define the scope of accessibility to different elements in our smart contracts. Let's dive in by examining the code for our `favorites.vy` contract: + +```python +#pragma version 0.4.0 +@license MIT +my_favorite_number: public(uint256) = 0 +@external +def store(new_number: uint256): + self.my_favorite_number = new_number +``` + +We have a state variable `my_favorite_number` that is declared as `public`. This means it is visible and accessible from anywhere. For example, an external user could interact with the `my_favorite_number` variable directly, without having to use any functions. + +We also have a function `store` that is declared as `@external`. This means that this function can only be accessed from outside the contract, via a transaction. No other functions within the contract can call `store` directly. + +Now let's look at how `internal` visibility functions work. We can remove the `public` keyword from our `my_favorite_number` state variable. We can then change the `@external` decorator to `@internal`: + +```python +#pragma version 0.4.0 +@license MIT +my_favorite_number: uint256 = 0 +@internal +def store(new_number: uint256): + self.my_favorite_number = new_number +``` + +We can then compile the code: + +```bash +Compile favorites.vy +``` + +The `internal` keyword means that the function `store` can only be called by other functions within the same contract. Let's add an example: + +```python +#pragma version 0.4.0 +@license MIT +my_favorite_number: uint256 = 0 +@internal +def store(new_number: uint256): + self.my_favorite_number = new_number +def store_other(): + self.store(7) +``` + +When we try to compile this code, we get an error because the `store_other` function is trying to call the `store` function, which is declared as `internal`. + +We can fix this by making the `store` function `external`, which allows it to be called from outside the contract. We can also make `my_favorite_number` `public` again so that it's visible from the outside. + +```python +#pragma version 0.4.0 +@license MIT +my_favorite_number: public(uint256) = 0 +@external +def store(new_number: uint256): + self.my_favorite_number = new_number +def store_other(): + self.store(7) +``` + +We can compile this code and we will see it functions as expected. + +This is a basic introduction to the concept of visibility in Solidity contracts. By mastering the use of keywords like `public` and `internal`, we can create more secure and modular smart contracts. diff --git a/courses/vyper-101/1-favorites/15-view-and-pure/+page.md b/courses/vyper-101/1-favorites/15-view-and-pure/+page.md new file mode 100644 index 000000000..7c89c6f9f --- /dev/null +++ b/courses/vyper-101/1-favorites/15-view-and-pure/+page.md @@ -0,0 +1,114 @@ +In this lesson we will learn how to build a view function. + +Firstly, let's go ahead and make a function that returns a value. + +```python +@external +def retrieve() -> uint256: + return self.my_favorite_number +``` + +Then we save and redeploy this: + +```bash +favorites --favorites.vy +``` + +Now we will comment out the store function for now. + +```python +# @external +# def store(new_number: uint256): +# self.my_favorite_number = new_number +``` + +We can now see the value returned in the UI: + +```bash +my_favorite_number --call +``` + +We will now change the retrieve function to return a value: + +```python +@external +def retrieve() -> uint256: + return self.my_favorite_number +``` + +This time we will redeploy and check the UI: + +```bash +favorites --favorites.vy +``` + +Now, if we want to explicitly tell the compiler that this is not a transaction we can simply add the @view decorator to the function: + +```python +@external +@view +def retrieve() -> uint256: + return self.my_favorite_number +``` + +Let's redeploy and see what happens in the UI. + +```bash +favorites --favorites.vy +``` + +The UI will now display a blue button, indicating that the function is a view function. We will now create a second view function which we will call 'retrieve', and we will also include a note within our code explaining how view functions work. + +```python +@external +def store(new_number: uint256): + self.my_favorite_number = new_number + +@external +@view +def retrieve() -> uint256: + return self.my_favorite_number + +# a view function can be called +# by a human for free -- no gas + +# but when a transaction calls +# a view function, it costs gas + +@external +def store(new_number: uint256): + self.my_favorite_number = new_number + +@internal +@view +def retrieve() -> uint256: + return self.my_favorite_number +``` + +Next, we will make the retrieve function an external function, so that we can call it. We will also add a few extra retrieve functions, just to highlight the gas cost differences. + +```python +@external +def store(new_number: uint256): + self.my_favorite_number = new_number + +self.retrieve() +self.retrieve() +self.retrieve() +self.retrieve() + +@internal +@view +def retrieve() -> uint256: + return self.my_favorite_number +``` + +Let's redeploy and see the difference in gas cost between the view function and external retrieve function. + +```bash +favorites --favorites.vy +``` + +If you recall, calling a view function from the UI requires no gas. However, if another transaction calls a view function, then it will incur a gas cost. + +Calling a view function from the UI is essentially the same as calling the 'public' function. diff --git a/courses/vyper-101/1-favorites/16-constructor/+page.md b/courses/vyper-101/1-favorites/16-constructor/+page.md new file mode 100644 index 000000000..676f775d4 --- /dev/null +++ b/courses/vyper-101/1-favorites/16-constructor/+page.md @@ -0,0 +1,115 @@ +## Constructors in Vyper + +In this lesson, we'll discuss what a constructor is in Vyper. We'll focus on the mechanics of how the constructor function works and create a constructor to initialize our `favorites` smart contract. + +### Constructors Explained + +A constructor is a special function that's called only once when a smart contract is deployed. The most common use for a constructor is to initialize state variables or to call other functions needed to set up the contract. + +### Defining Constructors + +Let's set up a constructor in a Vyper contract. First, we'll create a state variable named `owner` which will be a public address: + +```python +owner: public(address) +``` + +To define a constructor, we start with the `@deploy` decorator, followed by the definition of a function named `__init__`. Inside the parentheses, we can add any arguments we want to pass to the constructor. For now, we'll leave it empty: + +```python +@deploy +def __init__(): + pass +``` + +### Setting State Variables in Constructors + +One of the most common things we do inside a constructor is to set the `owner` of the contract. We can do this by setting the `owner` state variable to `msg.sender`. The `msg.sender` variable refers to the account that deployed the contract. + +```python +@deploy +def __init__(): + self.owner = msg.sender +``` + +### Passing Parameters to Constructors + +Let's look at some examples of passing parameters to a constructor. + +First, we'll add a state variable named `name` that is a public string with a maximum length of 100 characters: + +```python +name: public(string[100]) +``` + +Now we will pass the parameter `name` to our constructor function: + +```python +@deploy +def __init__(name: string[100]): + self.owner = msg.sender +``` + +We will then use the `name` parameter to set the `name` state variable: + +```python +@deploy +def __init__(name: string[100]): + self.owner = msg.sender + self.name = name +``` + +Now, let's create another state variable named `expiresAt` that will be a public `uint256`: + +```python +expiresAt: public(uint256) +``` + +We will now add a second parameter, `duration`, to our constructor function: + +```python +@deploy +def __init__(name: string[100], duration: uint256): + self.owner = msg.sender + self.name = name +``` + +Finally, we will set the `expiresAt` variable equal to the current block timestamp plus the `duration`: + +```python +@deploy +def __init__(name: string[100], duration: uint256): + self.owner = msg.sender + self.name = name + self.expiresAt = block.timestamp + duration +``` + +Now, let's compile and deploy our contract. We'll go to the `Compile` tab. +We will click the `Compile` button. +We will go to the `Deploy & Run Transactions` tab. +We will click on the `Deploy` button. We will then enter the `name`, which is `Vyper`, and the `duration`, which will be `10`. We'll click the `Transact` button. +We can then check the state variables. +Our `owner` address will be set to the current account. +Our `name` will be set to `Vyper`. +Our `expiresAt` will be set to the current block timestamp plus 10. + +### Another Example + +Let's create a constructor for our `favorites` smart contract to initialize a `my_favorite_number` state variable. Our state variable `my_favorite_number` will be a public `uint256`. + +```python +my_favorite_number: public(uint256) +``` + +Now let's create a constructor: + +```python +@deploy +def __init__(): + self.my_favorite_number = 7 +``` + +To compile and deploy this contract, we will click the `Compile` button. We will then go to the `Deploy & Run Transactions` tab. We will click the `Deploy` button. +We can then interact with our contract by clicking on the `my_favorite_number - call` button, which will return the value 7. + +Great work! diff --git a/courses/vyper-101/1-favorites/17-advanced-funcs/+page.md b/courses/vyper-101/1-favorites/17-advanced-funcs/+page.md new file mode 100644 index 000000000..2f57587ee --- /dev/null +++ b/courses/vyper-101/1-favorites/17-advanced-funcs/+page.md @@ -0,0 +1,130 @@ +## Advanced Vyper Functions + +We will show you some examples of functions in Vyper. All of the examples that I will show you here are called external functions. This means that after we deploy the contract, we will be able to call these functions. + +So, we'll declare as external: + +```python +@external +``` + +Now, there's another keyword called internal, which I will explain in another video, and then I'll put another keyword called pure: + +```python +@pure +``` + +Also, explain this in another video. Here, I just want to show you some syntax for how to write functions in Vyper: + +```python +def; +``` + +Let's call this function multiply: + +```python +def multiply +``` + +And as the name implies it will multiply two uint256, X uint256 and Y uint256. + +```python +def multiply(x: uint256, y: uint256) +``` + +And we want to return the product of X and Y. + +```python +def multiply(x: uint256, y: uint256) -> uint256: +``` + +To do this, we'll say put an arrow, then say we're going to return uint256 uint256. + +```python +def multiply(x: uint256, y: uint256) -> uint256: +return +``` + +And then, call it. Okay, and to multiply two numbers, we will say return. + +```python +def multiply(x: uint256, y: uint256) -> uint256: +return x * y +``` + +So, that's an example of a simple function that takes in two inputs, and then returns the product of the two numbers. + +Let me show you another example. I'm going to copy this. Again, we'll start with external, pure: + +```python +@external +@pure +``` + +Then I'll call this function divide: + +```python +def divide(x: uint256, y: uint256) -> uint256: +``` + +We're going to divide X by Y and it's going to return uint256. + +```python +def divide(x: uint256, y: uint256) -> uint256: +return +``` + +Now, you would think that to divide two numbers, you just have to put a slash. However, in Vyper when you're dividing two integers, then you need to do double slash. + +```python +def divide(x: uint256, y: uint256) -> uint256: +return x // y +``` + +For the next example, sometimes you want to declare a function, but don't want to implement the code yet, you just want to make sure that the contract compiles. In this case, you can use the keyword pass. So, for example, let's say that we have a function external: + +```python +@external +def todo(): +``` + +def, and I'll call this, let's say, todo. And we're not going to return any outputs. It's also not going to take any inputs. What we want to say is we're going to implement the code inside here later. What you can do is type pass: + +```python +@external +def todo(): +pass +``` + +Basically, this function will do nothing. + +Okay, and the last example I'll show you is how do you return multiple outputs? So, let's say external again, and I'll use pure again for this example, and say def return many. + +```python +@external +@pure +def return_many() +``` + +For the input, we'll keep it simple, it's not going to take any inputs. And for the output, how would we return multiple outputs? To do this, you'll need to put the type of the output inside the parentheses. For example, let's say we wanted to return uint256 and some boolean: + +```python +@external +@pure +def return_many() -> (uint256, bool): +``` + +Bool, we'll say return. For the first output, we need to return a uint256. So, let's return 123. And for the second output, we'll need to return a boolean, that's say true. + +```python +@external +@pure +def return_many() -> (uint256, bool): +return (123, True) +``` + +Okay, so those are some examples of how to write functions in Vyper. Let's try compiling and deploying the contract. + +```bash +vyper compile advanced_functions.vy +``` diff --git a/courses/vyper-101/1-favorites/18-ref-types/+page.md b/courses/vyper-101/1-favorites/18-ref-types/+page.md new file mode 100644 index 000000000..6c7c7fa1d --- /dev/null +++ b/courses/vyper-101/1-favorites/18-ref-types/+page.md @@ -0,0 +1,127 @@ +We will be going over reference types in Vyper. These are used to store data, but are not copied when assigned as a value. + +## Reference Data Types + +We will be looking at several reference types in Vyper: fixed-sized list, mappings, and structs. + +### Fixed-Sized List + +We can use a fixed-sized list to store data of a given type. We first declare the type, then put the fixed size in brackets, as follows: + +```vyper +nums: public(uint256[10]) +``` + +We can read and write to this list. To get the value in index 0, we use: + +```vyper +self.nums[0] +``` + +And to set the value in index 0 to 123: + +```vyper +self.nums[0] = 123 +``` + +We can also set another value in index 1 to 456: + +```vyper +self.nums[1] = 456 +``` + +### Mappings + +Mappings store data based on a key value. The first value type will be the key, and the second value type will be the data stored. Let's create a mapping called "myMap": + +```vyper +myMap: public(HashMap[address, uint256]) +``` + +We can access and store data in a mapping. To get the value stored at the address of "msg.sender": + +```vyper +self.myMap[msg.sender] +``` + +Let's store the value 1 in this mapping: + +```vyper +self.myMap[msg.sender] = 1 +``` + +We can also change the value stored at "msg.sender" to 11: + +```vyper +self.myMap[msg.sender] = 11 +``` + +### Structs + +Structs are custom data types. Let's declare a struct called "Person". This struct has a name and an age: + +```vyper +struct Person: + name: String[10] + age: uint256 +``` + +We can declare a state variable of type struct: + +```vyper +person: public(Person) +``` + +Let's store data in this state variable: + +```vyper +self.person.name = "vyper" +self.person.age = 33 +``` + +Then, we can copy this state variable into memory by declaring a variable of type struct and assigning the state variable: + +```vyper +p: Person = self.person +``` + +We can then change the name to "solidity" and the age to 22, but this will only update the value stored in memory, not the state variable. + +```vyper +p.name = "solidity" +p.age = 22 +``` + +We can compile this code using the Vyper compiler and then deploy the contract. + +To compile the code, we can click on the Vyper icon. Then click "Compile Ref.vy". The contract will be compiled. Now, we can deploy the contract. + +We can click on the "Deploy & run transactions" tab. Then we can click on the name of the contract file, which is Ref.vy. Next, we can click on the "Deploy" button to deploy the contract. + +We can see the state variables under the "Deployed Contracts" tab. We have "myMap", "nums", and "person". + +We can call the state variables to check what values are stored. We can call the state variable "nums" with index 0: + +```bash +nums(0) +``` + +We see it returns 0. This is because we did not set any values for index 0. Let's now call the state variable "nums" with index 1: + +```bash +nums(1) +``` + +We get 456, because we set the value for index 1 to 456. + +Next, we can call the state variable "person": + +```bash +person +``` + +We see it returns "name": "vyper" and "age": 33, because we set those values in the state variable. + +## Summary + +This is how we can store data in different reference types in Vyper. Remember that when using structs or mappings, we need to understand how these are copied. We can copy the value in a state variable into memory using a variable, but this only copies the value at that moment in time, not a reference to the state variable. So, if the state variable is updated, the value stored in the memory variable won't be updated. diff --git a/courses/vyper-101/1-favorites/19-claude/+page.md b/courses/vyper-101/1-favorites/19-claude/+page.md new file mode 100644 index 000000000..b9ea4869c --- /dev/null +++ b/courses/vyper-101/1-favorites/19-claude/+page.md @@ -0,0 +1,69 @@ +## AI Tutor: Claude 3.5 + +We are going to start working with AI agents. Claude 3.5 is a great AI agent to get started with, but so is Chat GPT 4.0. + +Claude is especially helpful for beginners, but when things get more advanced it might break down. Let's start with a simple question for Claude: + +```python +What is this line representing in Vyper? +``` + +To tell Claude we are writing a code block, we enter three backticks, then hit Shift-Enter. Then, we paste our code: + +```python +nums: public(uint256[10]) +``` + +and press Enter. + +Claude tells us this line declares a public state variable called `nums`, and that it is an array of 10 unsigned integers. It also tells us that `public` means the array can be read externally and a getter function is automatically generated. + +Next, we ask Claude: + +```python +What even is an array? +``` + +Claude responds: + +An array is a data structure that stores multiple elements of the same type in a contiguous memory location. It allows you to: + +- Store multiple values under a single variable name +- Access elements by their index (position) +- Have a fixed or dynamic size depending on the programming language +- Organize and efficiently manage collections of data + +Claude also gives us an example: + +```python +[10, 20, 30, 40, 50] +``` + +and explains that the first element is 10 and the third element is 30. + +We are going to ask Claude one more question: + +```python +Ok, what's an example of an array of 10 like in the Vyper above? +``` + +Claude gives us this example: + +```python +nums: public(uint256[10]) = [ + 100, # index 0 + 200, # index 1 + 300, # index 2 + 400, # index 3 + 500, # index 4 + 600, # index 5 + 700, # index 6 + 800, # index 7 + 900, # index 8 + 1000 # index 9 +] +``` + +Claude explains that if we wanted to access the element in position 2 we would use `nums[2]` which would return 300. + +There are a lot of different AI agents out there, with different strengths and weaknesses. You can explore the ones that are free and see what you prefer. GitHub Copilot is another great one. diff --git a/courses/vyper-101/1-favorites/2-best-practices/+page.md b/courses/vyper-101/1-favorites/2-best-practices/+page.md new file mode 100644 index 000000000..66a9e0c34 --- /dev/null +++ b/courses/vyper-101/1-favorites/2-best-practices/+page.md @@ -0,0 +1,51 @@ +## Best Practices (Don't Skip!!) + +We're going to start off by talking about some best practices so that you can get the absolute most out of this course and be as effective as possible. + +You're either watching this on Cyfr.in Updraft or on YouTube. We encourage everyone to watch this on Cyfr.in Updraft, because we've got a ton of features to make the learning experience that much easier for you. + +If you are watching this on Cyfr.in Updraft, though, there's a couple links I need you to be aware of. The first one, in the top right, is going to be the GitHub Resources page. This will bring you over to what's called a GitHub repo, or a GitHub repository, or basically a site that has all the code and all the information and basically all the materials that you're going to need to learn everything in our curriculum. You could basically think of this as your bible for the duration of your blockchain developer journey. + +Additionally, in this GitHub, there's a Discussions tab right here that you can click on, and in here is where you can ask questions, discuss with other people taking the course, interact with members helping out, and it's where you can discuss anything that you're having trouble with. + +Then, on the other side, you'll see this tab called Written Lessons. If you cannot stand the voice coming out of my mouth, you can just flip over to that, and just read the course curriculum as well, if you prefer the written content over the video content. It's good to go over to the written lessons anyway to maybe copy paste some stuff. + +If you scroll down here, right now it's blank, but if anytime there is an update to a video, something's changed in the video, and we haven't swapped out the video yet, you'll see a little Updates section with information saying, "Hey, the video says this, but you should do this instead." + +Now, like I said, this is a very fast-moving industry, and sometimes things change, and sometimes things need to be updated. So, when you're watching one of these videos, be sure to look for the Updates tab at the bottom, and then additionally, whenever you're working with the code that we're working with, I will give you a link to the finalized edition that you can use as a reference as well to make sure that the code that you're working with actually is going to match what we're going to build. + +Additionally, if you think you've found something that is different, or doesn't quite work, be sure to make a discussion for it in that GitHub repo. Like I said, it's going to be your bible. + +Additionally, there's a link to join the GitHub discussions. This is your platform to ask questions, engage with the community, and learn with both other people taking the course, and also our TAs who are going to be helping you out along the way. + +Additionally, there is a link to the Discord for more real-time communication. I urge you to ask questions in the GitHub discussions as well, because those are going to be indexed, going to make them much easier to Google search later and have them show up, as opposed to Discord. Discord is still phenomenal for you to join them. + +For those of you watching on YouTube, hello, you should scroll down to the description, and in the description are going to be links to these resources as well, and additionally, a link to Cyfr.in Updraft. If you've been watching, you've already seen some of the advantages that Cyfr.in Updraft has, including Written Lessons, single videos, and there's also ways to track your progress instead of having to scrub on a giant YouTube video. So, for all of you who are watching this on YouTube, definitely be sure to go over to Cyfr.in Updraft, sign up there and watch the curriculum there because your learning experience will be much better. But, leave this video playing in the background on YouTube, so we get the bump from the YouTube algorithm. Thank you. + +That being said, as we go through this course, we're also going to teach you some best practices on working with artificial intelligence, how to best prompt these AIs, so that they can give you the best results. Just keep in mind, they sometimes get things wrong. And, it's a good idea if you are going to use an AI to fact-check it with a human, or another resource. + +So, be sure to say hi in the discussions and maybe meet some like-minded peers. + +And, additionally, once we do get to the coding portion of this course, it's a good idea to code along with me, as I'm explaining things. So, having the video up, as well as your coding screen, is a good idea, so you can follow along with me, as I'm explaining it. If you're watching this on Cyfr.in Updraft, you can just click the little video pop-out button and have the video pop out as such, and code next to it. + +All of this is to say, if you run into an issue, jump to that GitHub repo, and make a discussion. We will also be giving you some tips very soon about how to best make a discussion. Yes, asking well-formatted questions is not only the secret to being a fantastic AI prompt engineer, but also becoming an incredibly successful developer. We're going to learn how to ask well-formatted questions, and whenever we post on discussions or forums or whatever, we're going to work on formatting them as best as possible. + +Take breaks. I cannot tell you how many people have tried to rush through these courses and be like, "Oh, I'm going to finish in a single weekend!" Your brain doesn't work like that. Your brain needs time to absorb the information, so take breaks. Maybe every 25 minutes to a half hour, take a 5 minute break, or maybe you like working in longer chunks, maybe take a whole hour, and then take a 15-20 minute break. Don't try to rush through the whole video in a day, you're not going to retain the information. Go outside, go for a walk, grab some ice cream, get some coffee, go to the gym. Your brain needs time to have the information settle. Maybe every 2 hours, just step away. Maybe be done for the day. Work at whatever pace makes sense for you. Everyone's going to have a different learning pace. There is no right speed for this course. I've had people take my courses in 2 weeks, in 3 months, in 6 months. It doesn't matter. Pick a pace that you can do, and stick to it. Not only work at your pace, make sure that I'm talking at a pace that makes sense for you. There's a little gear icon in the YouTube video here, where you can change the speed of how I'm talking and how fast the video's going. So, if I'm talking way too fast for you, then you can slow me down. But, if I'm talking too slow, then you can speed me up. + +And, if you're watching this on Cyfr.in Updraft, you have the same dials as well in the bottom right-hand corner. Additionally, if English isn't your native language, we have several different subtitles on the Cyfr.in Updraft video player as well. So, make the adjustments you need to make me go the speed you want me to go. And, of course, this course is modular, so you can bounce around topic to topic, and go to where you want to go. + +You don't want to do any full stack stuff, then skip that section. If you want to go right to the advanced stuff, do that. Like I said, go the pace and take the learnings that you want to do. + +And, after every lesson, it might be a good idea to go back and reflect on each lesson to really make sure the knowledge gets ingrained. Repetition is the mother of skill, and we're going to be repeating a lot of smart contract development. + +Now, the last bit here is in the Cyfr.in Updraft platform. We're going to have quizzes that you can take to help see if you learn the knowledge that you were supposed to learn. + +If you're watching this on YouTube, you don't have that, so go sign up for Cyfr.in Updraft and then play the YouTube video in the background, so the YouTube algorithm bumps this stuff. But, additionally, at the end of every section, if you go to the GitHub repo associated with that section, and you scroll down, there's going to be a bonus NFTs section with a link. This will bring you to a coding challenge on chain that you can actually solve to mint yourself an NFT, a badge of honor proving that you gained the knowledge that you were supposed to. + +These are optional challenges that you can do to try to make sure that you actually learned what was meant to be learned here. + +And, if you do solve them, you get a very cool NFT along with it. Don't know what an NFT is? Don't worry, we'll teach you later. + +Blockchain development and open source development world is incredibly collaborative. So, be sure to use tools like, of course, the GitHub Discussions tab, Ethereum Stack Exchange, the decentralized Q&A forum, Pyra, issues on different GitHubs, artificial intelligence and more. + +And, like I said, we'll give you more tips on how to most effectively use these sites in the future, and the reason I'm putting so much emphasis on this and that I will continue to put so much emphasis on this, is being a successful smart contract developer is more than just knowing how Solidity works. Knowing where to go for information and how to collaborate with people is often more important than your smart contract knowledge, because oftentimes you're going to run into issues you don't know how to solve, so we're going to teach you to unblock yourself on this and really anything in life, plus syncing with other people in this space makes it way more fun. \ No newline at end of file diff --git a/courses/vyper-101/1-favorites/20-arrays-lists/+page.md b/courses/vyper-101/1-favorites/20-arrays-lists/+page.md new file mode 100644 index 000000000..fb4536ca6 --- /dev/null +++ b/courses/vyper-101/1-favorites/20-arrays-lists/+page.md @@ -0,0 +1,40 @@ +## Arrays / Lists + +We've learned about different reference types, let's add them to our smart contract. We'll start with adding a minimal list of favorite numbers. + +We'll create a new variable called `list_of_numbers`. + +```python +list_of_numbers: public(uint256[5]) = [0, 0, 0, 0, 0] +``` + +We've now created a public array called `list_of_numbers` that is five elements long. + +We can read from this array by interacting with the contract via the terminal, for example: + +```bash +list_of_numbers +``` + +To update this array, we'll create a new function called `add_number`. + +```python +@external +def add_number(favorite_number: uint256): + self.list_of_numbers[self.index] = favorite_number + self.index = self.index + 1 +``` + +This function takes a `favorite_number` as input and updates the `list_of_numbers` at the current index. + +To ensure that our `list_of_numbers` doesn't exceed the length of five, we'll create a new variable `index` at the top of our contract: + +```python +index: public(uint256) = 0 +``` + +We've set this index to zero in our constructor (`init`) function. We can then use this `index` variable to keep track of the next available element in the array. + +**Video Tag:** Show this process in the deployed contract in the terminal, make sure to show it with the updated array. + +We've now found a way to incrementally add numbers to this array. If we try to exceed the length of the array, the transaction will revert. diff --git a/courses/vyper-101/1-favorites/21-idk/+page.md b/courses/vyper-101/1-favorites/21-idk/+page.md new file mode 100644 index 000000000..7ce9c8146 --- /dev/null +++ b/courses/vyper-101/1-favorites/21-idk/+page.md @@ -0,0 +1,58 @@ +We're learning about structs and strings in Vyper. + +Structs are a way for us to create new custom types. They're like blueprints for variables, grouping together related data under a single name. This is how we might create a `Person` struct: + +```vyper +struct Person: + favorite_number: uint256 + name: String[100] +``` + +Strings are a typing we haven't learned about yet. A string is a fixed-size array of characters. For example, here's how we might create a string variable: + +```vyper +example_str: String[100] = "Test String" +``` + +We'll use structs to store data about multiple people in a list, and to accomplish this, we'll use an array of structs. + +To begin, let's create a variable in our `deploy` function: + +```vyper +@deploy +def __init__(): + self.my_favorite_number = 7 + self.index = 0 + self.my_name = "Patrick!" +``` + +We need to make our new `Person` type publicly accessible, so we'll add the keyword `public` before our `String` type: + +```vyper +my_name: public(String[100]) +``` + +We can now create an array of `Person` structs in our contract: + +```vyper +list_of_people: public(Person[5]) +``` + +This is an array of five `Person` structs, which is where we'll store our new `Person` information. + +Next, we'll create an external function to add a new `Person` to our `list_of_people` array: + +```vyper +@external +def add_person(name: String[100], favorite_number: uint256): + # Add favorite number to the numbers list + self.list_of_numbers[self.index] = favorite_number + # Add the person to the person's list + new_person: Person = Person(favorite_number = favorite_number, name = name) + self.list_of_people[self.index] = new_person + self.index = self.index + 1 +``` + +This function takes the `name` and `favorite_number` of a new `Person` and stores that data in our `list_of_people` array. + +We can now compile and deploy this contract. diff --git a/courses/vyper-101/1-favorites/22-workshop/+page.md b/courses/vyper-101/1-favorites/22-workshop/+page.md new file mode 100644 index 000000000..f3d233c0b --- /dev/null +++ b/courses/vyper-101/1-favorites/22-workshop/+page.md @@ -0,0 +1,21 @@ +## Workshop Challenge + +We've gotten pretty far into our code. Now is a great time to pause and try a challenge. + +Can you get the `list_of_numbers` to return 8, and the `list_of_people` to return Wong instead of 0? + +**Hint:** You might have to add something to the contract, and then interact with the contract. + +**Pause the video and try to figure this out!** + +## Solution + +If your strategy was to just call `add_person` with Wang and 8 a bunch of times, that would work. We'll try that. + +```bash +add_person Wang 8 +``` + +We'll click `add_person` a few times here, and it looks like we are just spamming it! Oh, we've filled up the array! Now the index is too much. + +Well, now we're stuck with a list of people that just says `8 Wong` for every single person. This is definitely one way to do it, but it's not very elegant. We'll look at other, more effective ways to do this in future lessons. diff --git a/courses/vyper-101/1-favorites/23-hash-map/+page.md b/courses/vyper-101/1-favorites/23-hash-map/+page.md new file mode 100644 index 000000000..35964fb38 --- /dev/null +++ b/courses/vyper-101/1-favorites/23-hash-map/+page.md @@ -0,0 +1,46 @@ +## Retrieving Data From a Hashmap + +In the previous lessons, we created a smart contract that allowed us to add people to a list. However, we need to know the index of a person on the list in order to retrieve their data. + +Hashmaps, also known as mappings, are incredibly powerful because they allow us to retrieve data without knowing the index. We can think of a hashmap like a dictionary, with a string key and a uint256 value. + +In our smart contract, we are going to create one more storage variable called `name_to_favorite_number`, which will be a hashmap of type `string` and `uint256`. + +```python +name_to_favorite_number: public(HashMap[string, uint256]) +``` + +For example, we can add the name `Mariah` and associate her with the number 30. + +```python +# name_to_favorite_number: public(HashMap[string, uint256]) +``` + +In the `add_person` function, we are going to add the person to the hashmap. + +```python +@external +def add_person(name: String[100], favorite_number: uint256): + # Add favorite number to the numbers list + self.list_of_numbers[self.index] = favorite_number + # Add the person to the person's list + new_person: Person = Person(favorite_number = favorite_number, name = name) + self.list_of_people[self.index] = new_person + # Add the person to the hashmap + self.name_to_favorite_number[name] = favorite_number + self.index = self.index + 1 +``` + +Now, we can use the `name_to_favorite_number` function to retrieve Mariah's favorite number, without needing to know her index. + +```bash +name_to_fav... mariah +``` + +We can see that Mariah's favorite number is 30. + +```bash +0: uint256:30 +``` + +This is the power of hashmaps, or mappings, in smart contracts. They allow us to quickly retrieve data without knowing the index. As we learn more about smart contract programming, we will understand the trade-offs of each data structure and when to use each one. diff --git a/courses/vyper-101/1-favorites/24-recap/+page.md b/courses/vyper-101/1-favorites/24-recap/+page.md new file mode 100644 index 000000000..37455df9a --- /dev/null +++ b/courses/vyper-101/1-favorites/24-recap/+page.md @@ -0,0 +1,54 @@ +In this lesson, we will continue to develop our smart contract which stores favorite numbers. Let's take a look at what we have done so far. + +We have created a new type called a **struct**, called **Person**, which has two variables: + +- **favorite_number**: A public variable to store our favorite number +- **name**: A public string variable to store a name + +We have defined several state variables: + +- **my_name**: A string which stores our name. +- **my_favorite_number**: A public variable to store our favorite number. +- **list_of_numbers**: A public list that will store a list of favorite numbers. +- **list_of_people**: A public list that will store a list of people. +- **index**: A public integer that will track the size of our lists. +- **name_to_favorite_number**: A public hashmap that will store a name and a favorite number as a key value pair. + +The **constructor** function is used to initialize the state variables. In our example, we will set the **my_favorite_number** to 7 and the **index** to 0. We will also set **my_name** to "Patrick". + +We also have two functions, a **store** function to set our favorite number and a **retrieve** function to retrieve our favorite number. + +Here is the code we have written so far: + +```python +# pragma version ^0.4.0 +# @license MIT + +struct Person: + favorite_number: uint256 + name: String[100] + +my_name: public(String[100]) +my_favorite_number: public(uint256) = 7 +list_of_numbers: public(uint256[5]) = [0, 0, 0, 0, 0] +list_of_people: public(Person[5]) +index: public(uint256) +name_to_favorite_number: public(HashMap[String[100], uint256]) + +@deploy +def __init__(): + self.my_favorite_number = 7 + self.index = 0 + self.my_name = "Patrick!" + +@external +def store(new_number: uint256): + self.my_favorite_number = new_number + +@external +@view +def retrieve() -> uint256: + return self.my_favorite_number +``` + +In the next lesson, we will deploy our smart contract and test it. diff --git a/courses/vyper-101/1-favorites/25-tenderly/+page.md b/courses/vyper-101/1-favorites/25-tenderly/+page.md new file mode 100644 index 000000000..a2afe1d48 --- /dev/null +++ b/courses/vyper-101/1-favorites/25-tenderly/+page.md @@ -0,0 +1,34 @@ +We're going to learn how to deploy smart contracts with metamask, let's get started. Here's a simple smart contract that we're going to deploy: + +```python +pragma solidity ^0.8.4; + +contract Person { + string name; + uint256 favorite_number; +} +``` + +Let's go ahead and deploy this contract. First, we're going to need to create a new metamask wallet, and then we're going to need to select the network. We're going to select the Sepolia test network. + +Now, let's go through the steps of deploying the smart contract: + +- Click on the Remix **Deploy & Run Transactions** tab. +- Click on the **Deploy** button. +- Next we can hit the **My Favorite Number** +- Let's put Patrick. +- Let's put 7. +- Then we can hit the **Call** button. +- Now let's go ahead and deploy this contract. +- Select our **fake chain**. +- Click the **Deploy** button. We will see a message "Contract ... **compiled by Vyper**". Next, we will see a **green tick** above the **Contract** area. +- Click on the **green tick** to reveal the **deployed contract** details. +- We're going to hit the **index** +- We're going to click the **index** to reveal the **index** tab, +- We're going to hit the **call** button. +- Then, **MetaMask popped up** +- Now, we're going to go ahead and hit **Confirm**. +- We're going to click on **Confirm**. +- It says "Here I'm to help you!" + +That's how you deploy smart contracts with metamask. diff --git a/courses/vyper-101/1-favorites/26-zksync/+page.md b/courses/vyper-101/1-favorites/26-zksync/+page.md new file mode 100644 index 000000000..5470f68dd --- /dev/null +++ b/courses/vyper-101/1-favorites/26-zksync/+page.md @@ -0,0 +1,141 @@ +## Deploying ZkSync Contracts + +We will be exploring deploying contracts with ZkSync to a test net. + +First, we'll go to the plugin manager in Remix. + +If we look up ZkSync, we'll see there's an inactive module. We can go ahead and activate that. + +As of recording, the ZkSync plugin only works with Solidity. So, it doesn't work with Vyper at the moment. + +We are not going to be deploying this to ZkSync through Remix. + +However, when we switch over to Python, we will be deploying to ZkSync. + +First, we'll install `zksync-python`: + +```bash +pip install zksync-python +``` + +Now, we can open a Python script and import the relevant libraries: + +```python +import zksync +from zksync import Account +from zksync.transaction import DeployTransaction +from zksync.signer import PrivateKeySigner +from zksync.utils import to_hex + +from zksync.contracts import ( + zkSyncContract, + zkSyncInterface +) +from zksync.provider import ZkSyncProvider +from zksync.wallet import Wallet +``` + +We'll then initialize an account, which we'll use to deploy the contract. + +```python +PRIVATE_KEY = "0x..." +signer = PrivateKeySigner(PRIVATE_KEY) +wallet = Wallet(signer, ZkSyncProvider(url='https://zksync2-testnet.zksync.io/')) +address = wallet.get_address() +``` + +We'll define the address of the contract. + +```python +contract_address = "0x..." +``` + +Now, we'll set the `bytecode` of the contract: + +```python +bytecode = "0x..." +``` + +And, we'll specify the contract's constructor arguments. + +```python +constructor_args = [] +``` + +We'll instantiate a deploy transaction. + +```python +deploy_tx = DeployTransaction( + address=address, + contract_address=contract_address, + bytecode=bytecode, + constructor_args=constructor_args +) +``` + +We'll then call the `sign_deploy_transaction` method to sign the deploy transaction. + +```python +signed_deploy_tx = wallet.sign_deploy_transaction(deploy_tx) +``` + +Finally, we'll call the `send_deploy_transaction` method to send the transaction to the network. + +```python +wallet.send_deploy_transaction(signed_deploy_tx) +``` + +Now, we can call the deployed contract's functions. We'll define the name of the function and its arguments. + +```python +function_name = "add" +arguments = [10, 20] +``` + +Now, we can use the `zksyncInterface` class to call the `function_name` method. + +```python +zkSyncInterface(wallet.get_address(), wallet.get_signer(), ZkSyncProvider(url='https://zksync2-testnet.zksync.io/')) +``` + +We'll then call the `call_function` method to call the contract's function. + +```python +zksyncInterface.call_function(function_name, arguments) +``` + +We'll then call the `zksyncContract` class to call the `get_balance` method. + +```python +zksyncContract(wallet.get_address(), wallet.get_signer(), ZkSyncProvider(url='https://zksync2-testnet.zksync.io/')) +``` + +We'll then call the `get_balance` method to get the contract's balance. + +```python +zksyncContract.get_balance() +``` + +Now, we'll call the `get_code` method to get the contract's code. + +```python +zksyncContract.get_code() +``` + +Finally, we'll call the `get_transactions` method to get the contract's transactions. + +```python +zksyncContract.get_transactions() +``` + +We'll then print the results. + +```python +print(zksyncContract.get_balance()) +print(zksyncContract.get_code()) +print(zksyncContract.get_transactions()) +``` + +This will print the contract's balance, code, and transactions. + +We've now deployed a ZkSync contract and called its functions. diff --git a/courses/vyper-101/1-favorites/27-share/+page.md b/courses/vyper-101/1-favorites/27-share/+page.md new file mode 100644 index 000000000..7a9c4b0ea --- /dev/null +++ b/courses/vyper-101/1-favorites/27-share/+page.md @@ -0,0 +1,20 @@ +## Sharing your progress + +We're going to talk about the tweet me button on the Moccasin Vyper full course repo. + +Go to the repo on GitHub and scroll down to Vyper 101. + +We'll be talking about the section titled "Welcome to Remix - Favorite's List". + +Click the tweet me button. If you're signed in, this will open a tweet intent. You should see the following text: + +``` +I just deployed a Smart Contract! +Thanks @PatrickAlphaC, @zkSyncdevs, and @CyfrinUpdraft! +``` + +Go ahead and send that tweet. Be sure to say hi, we're so excited you're deploying your first smart contract! + +You can even thank Tenderly for helping make getting Testnet ETH a lot easier. + +We love hearing from all of you, so make sure you tweet out when you deploy your first contract. diff --git a/courses/vyper-101/1-favorites/28-evm/+page.md b/courses/vyper-101/1-favorites/28-evm/+page.md new file mode 100644 index 000000000..cdb563605 --- /dev/null +++ b/courses/vyper-101/1-favorites/28-evm/+page.md @@ -0,0 +1,50 @@ +## Smart Contracts, EVM, and EVM-Compatible Chains + +We will start to look at how smart contracts are actually run. When we compile our smart contract, we are converting it into a format that the EVM can understand. + +The EVM, or Ethereum Virtual Machine, is a set of rules or standards that dictate how compiled smart contract code should look. + +Any blockchain that follows these rules is considered EVM-compatible. Examples of these include: + +- Ethereum +- Arbitrum +- Optimism +- Polygon +- zkSync + +We should be mindful of the chain that we are deploying to. A common mistake is to deploy to a chain that does not support the smart contract language we are using. We will discuss this in more detail later. + +We will be using zkSync in this lesson. Here is the code we will be working with: + +```python +# EVM: Ethereum Virtual Machine +# Ethereum, Arbitrum, Optimism, Polygon, zkSync + +# pragma version 0.4.0 +# @license MIT + +struct Person: + favorite_number: uint256 + name: String[100] + +my_name: public(String[100]) +my_favorite_number: public(uint256) = 7 + +list_of_numbers: public(uint256[5]) = [0, 0, 0, 0, 0] +list_of_people: public(Person[5]) +index: public(uint256) + +name_to_favorite_number: public(HashMap[String[100], uint256]) + +@deploy +def __init__(): + self.my_favorite_number = 7 + self.index = 0 + self.my_name = "Patrick!!" + +@external +def store(new_number: uint256): + self.my_favorite_number = new_number +``` + +This code works correctly with the zkSync compiler, but as we progress through this lesson, we will start to see how certain keywords, for example zkSync, do not work correctly. diff --git a/courses/vyper-101/1-favorites/29-pure-and-view/+page.md b/courses/vyper-101/1-favorites/29-pure-and-view/+page.md new file mode 100644 index 000000000..e5ad08222 --- /dev/null +++ b/courses/vyper-101/1-favorites/29-pure-and-view/+page.md @@ -0,0 +1,78 @@ +## Pure vs. View Functions + +We're going to look at a couple of key concepts in Vyper: `pure` and `view` functions. These are important to know to write better smart contracts. + +We'll use the `@pure` and `@view` decorators in our code. Let's start with the `@pure` decorator. + +**Pure Functions** + +A `pure` function is a function that: + +- **Is read-only:** It doesn't modify the state of the blockchain. +- **Does not read state or global variables:** This means it can't access any data that's stored on the blockchain or in the global scope. + +Let's look at an example: + +```python +@external +@pure +def add(x: uint256, y: uint256) -> uint256: + return x + y +``` + +This function takes two inputs, `x` and `y`, both of which are `uint256` (unsigned integers up to 256 bits). The function returns their sum. Since it's `@pure`, we know it's read-only and doesn't use any state or global variables. + +**View Functions** + +A `view` function is also read-only, but it can read data from the blockchain. This includes state variables and global variables. Global variables are those defined by the EVM like `block` and `msg`. + +For example, we can read from a state variable called `count`: + +```python +@external +@view +def add_to_count(x: uint256) -> uint256: + return x + self.count + block.timestamp +``` + +This `view` function returns the sum of the input `x`, the current value of the state variable `count`, and the current block timestamp. + +Let's recap: + +- **Pure functions** are read-only and don't read any state or global variables. +- **View functions** are read-only but can read state and global variables. + +**Code Example** + +```python +@external +@pure +def add(x: uint256, y: uint256) -> uint256: + return x + y + +count: public(uint256) + +@external +@view +def add_to_count(x: uint256) -> uint256: + return x + self.count + block.timestamp +``` + +In this example, `add` is a `pure` function, and `add_to_count` is a `view` function. + +**Deploying and Interacting with the Contract** + +Let's deploy the contract and try using these functions: + +1. **Compile the contract:** We can compile the code and get the ABI (Application Binary Interface) and bytecode, which are needed to deploy the contract. +2. **Deploy the contract:** We can deploy the contract to the blockchain using a tool like Remix. +3. **Call the `add` function:** This function is `@pure`, so it won't modify any state. We can call it with two inputs, say 2 and 3, and it will return 5. +4. **Call the `add_to_count` function:** This function is `@view`, so it won't modify any state. We can call it with an input, say 2, and it will return a value that's the sum of 2, the current value of the `count` state variable, and the current block timestamp. + +**Important Considerations** + +- Using `@pure` and `@view` functions can make your smart contracts more gas-efficient. This is because they don't need to write data to the blockchain. +- `Pure` functions are deterministic: They always return the same output for the same input. This is useful for functions that are used in calculations or for verifying data. +- `View` functions are useful for reading data from the blockchain and displaying it to users. + +We've just scratched the surface of pure and view functions. There's much more to learn, like their relationship to the EVM and their differences from other function types. But this overview should give you a good starting point. diff --git a/courses/vyper-101/1-favorites/3-mox-best-practices/+page.md b/courses/vyper-101/1-favorites/3-mox-best-practices/+page.md new file mode 100644 index 000000000..34fa8fcd4 --- /dev/null +++ b/courses/vyper-101/1-favorites/3-mox-best-practices/+page.md @@ -0,0 +1,36 @@ +## Welcome to Solidity Fundamentals: Best Practices + +Let's start by talking about best practices. We will be looking at some best practices that we cover in our Moccasin course. + +If you are viewing this lesson on Cyfrin Updraft, you can head to the **Course Resources** section and you will be taken to the **Cyfrin/mocassin-full-course-cu** GitHub repository. This repository has all the codebase associated with the course. For instance, if we click on **Section 1** of **Vyper 101**, which is titled **Welcome to Remix - Favorite's List**, you will be taken to a repository called **Cyfrin/remix-favorites-cu**. This repository has the code associated with that section in the **Favorites.vy** file. + +If you are watching this on YouTube, the links are provided in the description. + +We can also head back to the **Cyfrin/mocassin-full-course-cu** repository. In here, we can go to the **Discussions** tab. This is a good place to come if you have any questions. We will teach you how to ask questions in a manner that is helpful to both humans and AI. + +We will be using AI throughout the course. As of recording, our favorites include **Claude**, **Cursor**, and **ChatGPT**. + +We recommend that you pause the video and create a GitHub account. GitHub is crucial to your success as a smart contract developer, or even just as a technical person in Web3. + +We also recommend that you check out the **Twitter** and **LinkedIn** links that are provided in the repository. + +One other important tool is **Stack Exchange Ethereum**. This is a good place to go to ask questions about Ethereum in general. + +We will now move on to **Section 1** of **Moccasin Fundamentals**: **Welcome to Python!** + +## Welcome to Python! + +We're going to do a code block here. We're going to make a little Python script. I'm going to start with a **__main__** function, which is a special function in Python that allows code to be run in a top level. + +```python +if __name__ == "__main__": + print("Hello world") +``` + +So, we can copy this code block, and paste it into our terminal. This will print out **Hello world**. + +```bash +python code.py +``` + +And there you have it. Now, we're going to go to the next lesson. diff --git a/courses/vyper-101/1-favorites/30-conditionals/+page.md b/courses/vyper-101/1-favorites/30-conditionals/+page.md new file mode 100644 index 000000000..891580126 --- /dev/null +++ b/courses/vyper-101/1-favorites/30-conditionals/+page.md @@ -0,0 +1,71 @@ +## Conditionals in Vyper + +We'll now cover the conditional statement in Vyper. This statement allows us to control the flow of our code based on different conditions. It's similar to the `if else` statement you might be familiar with in other programming languages. + +Let's create an example of an `if else` conditional statement in Vyper. We'll create an external function, we'll call this function `if_else`, which takes a single input of `uint256`. It will return a `uint256` value as well. + +```python +@external +def if_else(x: uint256) -> uint256: +``` + +Now, we'll write the `if else` conditional statement. Let's say `if` the input `x` is less than or equal to 10, then we'll return 1. + +```python +@external +def if_else(x: uint256) -> uint256: + if x <= 10: + return 1 +``` + +Then we'll add the `else` statement, we'll return 0 for this example. + +```python +@external +def if_else(x: uint256) -> uint256: + if x <= 10: + return 1 + else: + return 0 +``` + +If we wanted another conditional statement in between, we would say `else if`, but in Vyper, we would type `elif`. + +```python +@external +def if_else(x: uint256) -> uint256: + if x <= 10: + return 1 + elif x <= 20: + return 2 + else: + return 0 +``` + +For this example, we forgot to make this a pure function, so let's do that. + +```python +@external +pure +def if_else(x: uint256) -> uint256: + if x <= 10: + return 1 + elif x <= 20: + return 2 + else: + return 0 +``` + +We'll now compile the contract. Click on the Vyper icon, Compile `ifElse.vy`. + +```bash +commands +``` + +We'll now call this function. We'll put 1 for `x` and call the function. We get a 1 back. + +We'll now put 20 for `x` and call the function. We get a 2 back. + +Lastly, we'll put 100 for `x` and call the function. We get a 0 back. + +This has been an introduction to `if else` conditional statements in Vyper. diff --git a/courses/vyper-101/1-favorites/31-workshop-pt-2/+page.md b/courses/vyper-101/1-favorites/31-workshop-pt-2/+page.md new file mode 100644 index 000000000..3ad66fc1d --- /dev/null +++ b/courses/vyper-101/1-favorites/31-workshop-pt-2/+page.md @@ -0,0 +1,46 @@ +## Welcome to Remix - Favorite's List + +Congratulations on finishing the "Welcome to Remix - Favorite's List" section! Before we jump off and wrap up this section, we have a workshop for you. On every single section moving forward, at the end, we're going to have a little workshop for you to practice and drill in some of the things that we taught you. + +Here's where AI is incredibly helpful because AI will very often be able to give you the right answer. You can just work with AI and figure out a solution that you like because oftentimes there actually might be multiple solutions. + +For all these workshops, there will be like a little prompt, and what we want you to do is to spend at most 25 minutes on the prompts. If you spend more than 25 minutes, stop, take a break, and then come back and work with an AI that you like to help solve these. Or if an AI is giving you trouble, you can always jump into the discussions, jump to the Discord, etc. + +Here are the three prompts that we want you to do: + +1. Create a function called `add` that adds one to whatever is in `self.favorite_number`. +2. Have the starting favorite number be different from seven and check to make sure it's the number you set. +3. Create a new type using the `struct` keyword and create a function that will save a new variable of that type to a state/storage variable. + +Here are some code blocks to get you started. + +```python +struct Person: + favorite_number: uint256 + name: String[100] + +my_favorite_number: uint256 + +# Static Array/List +list_of_numbers: public(uint256[5]) +list_of_people: public(Person[5]) +list_of_people_index: uint256 + +name_to_favorite_number: HashMap(String[100], uint256) + +@deploy +def __init__(): + self.my_favorite_number = 7 + +@external +def store(favorite_number: uint256): + self.my_favorite_number = favorite_number + +@view +def retrieve() -> uint256: + return self.my_favorite_number +``` + +We have a `struct Person` and a `my_favorite_number` that is a `uint256`. You would have your name as part of your struct, and we have our `deploy` function that is initialized with seven as the favorite number. We also have a `store` function that updates the number and a `retrieve` function to get the current number. + +Repetition is the mother of skill, and we want to make you incredibly skillful. So, pause the video, get cracking on some of these workshop problems. Spend at most 25 minutes on them, and then when you're done, if you still run into issues, work with an AI. diff --git a/courses/vyper-101/1-favorites/32-recap-pt-2/+page.md b/courses/vyper-101/1-favorites/32-recap-pt-2/+page.md new file mode 100644 index 000000000..98519135f --- /dev/null +++ b/courses/vyper-101/1-favorites/32-recap-pt-2/+page.md @@ -0,0 +1,97 @@ +We've learned about pragma version or `@version` where we can specify the version of our Vyper code directly on our contract. +We've deployed, and we've compiled and we've worked with the Remix Ethereum IDE, the online integrated development environment which is incredibly powerful for learning and seeing and deploying smart contracts. +We've created this struct Person which is our way of creating a new type. + +```vyper +struct Person: + favorite_number: uint256 + name: String[100] +``` + +We've got a whole bunch of state variables and we've set them all to public visibility so we can have that blue button for them so we can actually call them and actually see what these are when we actually deploy this contract. + +```vyper +my_favorite_number: uint256 +my_name: String[100] +people: Person[5] +index: uint256 +my_mapping: mapping(String[100] => uint256) +``` + +We've got several different types in here. We've got a string, a fixed sized string. We've got a uint256, aka a number. We've got a uint256 array which, similar to this string, this array or this list is saying hey there can be a total of five numbers in this list of numbers. Our string up here, my name, is a string where there can be a total of 100 characters in this string. We've got a list of people. We can even have a list or an array of our custom struct, our custom type Person, and we can have up to five people in this list of people. We also have an index which is just a number to keep track of hey, what number are we on here? And then finally we have this very advanced data structure called a hash map, or a mapping where it takes a key of a string and a value of a number. + +```vyper +my_favorite_number: uint256 +my_name: String[100] +people: Person[5] +index: uint256 +my_mapping: mapping(String[100] => uint256) +``` + +We have a constructor, where we have this deploy section where we set my favorite number to seven. So that once the instance of this contract is deployed, my favorite number gets initialized to seven. We set self. index to zero. If we didn't set it, it would still be zero, but we're explicitly setting it to zero. We have self.my name we're setting it to Patrick with an exclamation mark in here, but you should do your name instead of Patrick. + +```vyper +@version ^0.2.9 +def __init__(): + self.my_favorite_number = 7 + self.index = 0 + self.my_name = "Patrick!" +``` + +We've got a, and we've got a function that we learned with the def keyword, and the external decorator saying anyone outside of this contract can call this store function. If we didn't have this external keyword, it would be internal meaning only other functions inside this contract can actually call it. We're setting my favorite number to the new number that we're passing into store. We know that this is a state variable because we can see the self keyword here. Whenever we see the self keyword, we know we're referring to properties of the contract itself. Whereas this new number is just this temporary number that exists just for the duration of this function call. This self.my favorite number will persist outside of this function call. + +```vyper +@version ^0.2.9 +def __init__(): + self.my_favorite_number = 7 + self.index = 0 + self.my_name = "Patrick!" + +@external +def store(new_number: uint256): + self.my_favorite_number = new_number +``` + +We have a view function which allows us to just view some number instead of + +```vyper +@version ^0.2.9 +def __init__(): + self.my_favorite_number = 7 + self.index = 0 + self.my_name = "Patrick!" + +@external +def store(new_number: uint256): + self.my_favorite_number = new_number + +@external +def retrieve() -> uint256: + return self.my_favorite_number +``` + +And then we have our monstrosity function called add person where it takes a name of type string 100, a favorite number which is a uint256, and it adds this person to our list of numbers using the index, creates a new person using this wonderful struct syntax, it adds that person to our list of people, and then it adds that person to our hash map as well and increments the index. + +```vyper +@version ^0.2.9 +def __init__(): + self.my_favorite_number = 7 + self.index = 0 + self.my_name = "Patrick!" + +@external +def store(new_number: uint256): + self.my_favorite_number = new_number + +@external +def retrieve() -> uint256: + return self.my_favorite_number + +@external +def add_person(name: String[100], favorite_number: uint256): + self.people[self.index] = Person(favorite_number, name) + self.my_mapping[name] = favorite_number + self.index += 1 +``` + +And last but not least, congratulations on your first contract here. Now's the time to go grab a coffee, go grab an ice cream, go to the gym, go for a walk, and take a break because your mind needs it to digest all this wonderful information you just learned. Take that break, and we'll see you soon. diff --git a/courses/vyper-101/1-favorites/4-walkthrough/+page.md b/courses/vyper-101/1-favorites/4-walkthrough/+page.md new file mode 100644 index 000000000..3df46e366 --- /dev/null +++ b/courses/vyper-101/1-favorites/4-walkthrough/+page.md @@ -0,0 +1,49 @@ +## Deployment of First Vyper Smart Contract + +In this lesson, we'll be deploying our first Vyper smart contract. We'll be using `remix.ethereum.org` (aka the Remix IDE) to build our code base here. You should follow along coding with us, and even try to do some coding yourself as we go on here. Remember, both on Cyfrin Updraft and on YouTube, you can change the speed of the video so that it matches your pace. + +### Smart Contract Code + +The smart contract code we will be deploying is: + +```python +# I'm a comment! +# SPDX-License-Identifier: MIT +# @version ^0.4.0 + +my_favorite_number: uint256 + +struct Person: + favorite_number: uint256 + name: String[100] + +# Static Array/List +list_of_people: public Person[5] +list_of_people_index: uint256 + +name_to_favorite_number: HashMap[String[100], uint256] + +@external +def store(favorite_number: uint256): + self.my_favorite_number = favorite_number + +@external +@view +def retrieve() -> uint256: + return self.my_favorite_number + +@external +def add_person(name: String[100], favorite_number: uint256): + new_person: Person = Person(favorite_number, name) + self.list_of_people[self.list_of_people_index] = new_person + self.list_of_people_index += 1 + self.name_to_favorite_number[name] = favorite_number +``` + +### Deploying the Smart Contract + +1. **Paste the code:** Open Remix IDE and create a new file. Paste the code from the code block above into the new file. +2. **Compile:** Click the "Compile" button to compile the code. +3. **Deploy:** Click the "Deploy" button to deploy the contract to the blockchain. + +We're excited for you to complete your first smart contract deployment! Remember to check the [Remix Favorites CU GitHub repository](https://github.com/Cyfrin/remix-favorites-cu) for the code and additional resources. diff --git a/courses/vyper-101/1-favorites/5-welcome-to-remix/+page.md b/courses/vyper-101/1-favorites/5-welcome-to-remix/+page.md new file mode 100644 index 000000000..d1ee5b4fc --- /dev/null +++ b/courses/vyper-101/1-favorites/5-welcome-to-remix/+page.md @@ -0,0 +1,34 @@ +## Welcome to Remix! + +We're going to begin by getting familiar with Remix, an Integrated Development Environment, or IDE, that's phenomenal for helping us visualize our smart contracts. We'll be using Remix throughout the course to build smart contracts using Vyper, a popular smart contract language. + +Remix has many features, including a built-in AI assistant that helps us understand concepts. While AI can be very useful, we need to understand the fundamentals of smart contract programming. AI can't always understand or provide accurate information about advanced concepts. + +### Getting Started with Remix + +1. Open the Remix IDE. You can find it at `remix.ethereum.org`. +2. Click on "New File" under the "Solidity/Vyper/EVM" section. +3. Choose "Vyper" as the file type and name your contract, e.g., `MinimalContract.vy`. +4. In this new file, add a basic Vyper smart contract template: + +```python +# Minimal Contract +pragma solidity ^0.6.12; + +contract Minimal: + def hello(self) -> str: + return "Hello World" +``` + +5. Save the file. +6. Go to your IDE's left panel and click on "Deploy" under the "Solidity/Vyper/EVM" section. + +### Working with the Vyper Compiler + +1. Click on the "Plugin Manager" button at the bottom of the Remix IDE. +2. In the search bar, type "vyper." +3. Activate the "Vyper Compiler" module. +4. You will now see the "Vyper Compiler" section on the left-hand side of the Remix IDE. +5. Click the "Compile favorites.vy" button to compile our smart contract. + +You can also access more advanced compiler settings if you click on the "Advanced Compiler Settings" button. This will allow you to set up your own local Vyper Compiler, but we will be using the remote compiler for now. diff --git a/courses/vyper-101/1-favorites/6-pragma-version/+page.md b/courses/vyper-101/1-favorites/6-pragma-version/+page.md new file mode 100644 index 000000000..5054cdb86 --- /dev/null +++ b/courses/vyper-101/1-favorites/6-pragma-version/+page.md @@ -0,0 +1,25 @@ +## Pragma Version + +We will start by setting a version for our Vyper smart contract. This is an important step to ensure that our code is compatible with the appropriate compiler version. + +The version is set using the `#pragma version` directive. + +We will use `0.4.0`, a stable and widely supported version. + +```python +#pragma version 0.4.0 +``` + +We can also use other versions, such as 0.4.1 and above, by using the `^` symbol to indicate a minimum version. + +```python +#pragma version ^0.4.0 +``` + +We will stick with `0.4.0` in this lesson. + +Using this version is recommended because it's widely supported and allows for greater compatibility across different compilers. + +For production code, it's best to use a specific version for the reasons mentioned above. + +Let's move on to the next step in creating our smart contract. diff --git a/courses/vyper-101/1-favorites/7-comments/+page.md b/courses/vyper-101/1-favorites/7-comments/+page.md new file mode 100644 index 000000000..955769634 --- /dev/null +++ b/courses/vyper-101/1-favorites/7-comments/+page.md @@ -0,0 +1,27 @@ +## Comments in Vyper + +We're going to talk about comments in Vyper. + +This little hashtag here is, or pound symbol, makes this whole line here, what's known as a comment. + +```python +# pragma version 0.4.0 +``` + +And typically, a comment is a line in your code where you can write pretty much whatever you want, and the compiler will ignore it. The compiler doesn't ignore these lines if it sees a comment. + +```python +# asdfagolsa asdfasrtafdsdf +``` + +We can also use comments to leave ourselves notes, like this: + +```python +# This is how we tell the Vyper compiler +# what version to use +# pragma version 0.4.0 +``` + +We can also copy and paste these comments into a text editor or a notes app so that we can refer to them later. + +It is important to save our work frequently, because if we lose our tab, we'll lose all of our text. diff --git a/courses/vyper-101/1-favorites/8-license/+page.md b/courses/vyper-101/1-favorites/8-license/+page.md new file mode 100644 index 000000000..e1d01d36c --- /dev/null +++ b/courses/vyper-101/1-favorites/8-license/+page.md @@ -0,0 +1,27 @@ +We are building a simple smart contract for a favorites list. + +The first line of code we will write in our Solidity smart contract file should be: + +```python +pragma version 0.4.0 +``` + +This line tells the compiler what version of Solidity we will be using. In this case, we will be using Solidity version 0.4.0. + +Next, we will add another line that defines the license of our code: + +```python +@license MIT +``` + +This line tells the world that this code is licensed under the MIT license. + +Let's break down why we use a license: + +- There are many types of code licenses out there. +- Most of the code in the blockchain world is open source. +- This line tells the compiler what license our code is under. + +The MIT license is one of the most permissive licenses. This means that anyone can use, redistribute, or modify our code as they see fit. + +We've now established a basic code structure and license. diff --git a/courses/vyper-101/1-favorites/9-compiling/+page.md b/courses/vyper-101/1-favorites/9-compiling/+page.md new file mode 100644 index 000000000..a71396dc8 --- /dev/null +++ b/courses/vyper-101/1-favorites/9-compiling/+page.md @@ -0,0 +1,35 @@ +## Compiling + +We're going to look at the compilation process for a smart contract written using Vyper. + +Even with just this small contract, we have no code. All we have is a version and a license. Let's go ahead and compile this contract. + +We'll go to the Vyper compiler and click the Compile button. + +Clicking the "Compile favorites.vy" button gives us a compilation details box. + +We'll click the "Compilation Details" button and see bytecode, runtime bytecode, and ABI. + +The bytecode and runtime bytecode are sent to the blockchain because the blockchain only understands raw bytecode. The blockchain only understands zeros and ones and doesn't understand what "pragma version" means. So when we compile our code we're essentially transforming our human-readable code into machine readable code. + +If our compilation fails, it means we have a mistake in our code somewhere. + +Let's go back to our "favorites.vy" file and try adding a line with gibberish code: + +```python +asdfhasjh a:dhfpuiowht92y92 +``` + +Trying to compile this gives us an error: + +```bash +favorites.vy +SyntaxException:Semi-colon statements not allowed +line 4:11 +``` + +We'll remove the line and compile again. This time the compiler will successfully compile our code, which is indicated by a green checkmark. + +While we're not actually deploying a contract here, this is still important to help us understand the compilation process. + +As you get used to coding, you'll get used to hitting Cmd+S or Ctrl+S. diff --git a/courses/vyper-101/2-remix-coffee/1-introduction/+page.md b/courses/vyper-101/2-remix-coffee/1-introduction/+page.md new file mode 100644 index 000000000..96c8cbb27 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/1-introduction/+page.md @@ -0,0 +1,84 @@ +## Introduction to Remix Buy Me A Coffee Smart Contract + +This lesson is part of the Cyfrin Updraft Vyper course. We will be exploring the Buy Me a Coffee smart contract. + +The code for this lesson is available on the following GitHub repo: + +```bash +https://github.com/Cyfrin/remix-buy-me-a-coffee-cu +``` + +We will be building a simple smart contract that allows people to send you money based on a price that we set. In this case, we'll be using a price of $50. We will also explore a withdrawal function that allows the owner of the contract to withdraw the funds. We will be using Remix and the Tenderly virtual network for this lesson. + +We will explore the following code: + +```python +pragma version 0.4.0 +pragma enable-decimals + +license: MIT +title: A sample buy-me-a-coffee contract + +@author You! +@notice This contract is for creating a sample funding contract + +# We'll learn a new way to do interfaces later... +interface AggregatorV3Interface: + def decimals() -> uint8: view + def description() -> String[1000]: view + def version() -> uint256: view + def getRoundData(roundId: uint80) -> (uint80, int256, uint256, uint256, uint80): view + def latestRoundData() -> (uint80, int256, uint256, uint256, uint80): view + +# minimum USD decimals: public(constant(decimal)) = 50.0 +MINIMUM_USD: public(constant(uint256)) = 50 * (10**18) +PRECISION: constant(uint256) = 1 * (10**18) +OWNER: public(immutable(address)) +funders: public(DynArray[address], 100) +address_to_amount_funded: public(HashMap[address, uint256]) +price_feed: public(AggregatorV3Interface) + +@deploy +def __init__(price_feed: address): + self.price_feed = AggregatorV3Interface(price_feed) + OWNER = msg.sender + +@internal +def _only_owner(): + assert msg.sender == OWNER, "Not the contract owner!" + +@external +@payable +def fund(): + usd_value_of_eth: uint256 = self.get_eth_to_usd_rate(self.price_feed, msg.value) + assert usd_value_of_eth >= MINIMUM_USD, "You need to spend more ETH!!" + self.address_to_amount_funded[msg.sender] += msg.value + self.funders.append(msg.sender) + +@external +def withdraw(): + self._only_owner() + for funder: address in self.funders: + self.address_to_amount_funded[funder] = 0 + self.funders = [] + send(OWNER, self.balance) + +@internal +@view +def get_eth_to_usd_rate(price_feed: AggregatorV3Interface, eth_amount: uint256) -> uint256: + # Check the conversion rate + a: uint80 = 0 + b: int256 = 0 + c: uint256 = 0 + d: uint256 = 0 + e: uint80 = 0 + (a, b, c, d, e) = staticcall(price_feed.latestRoundData()) + # We know the price has 8 decimals, so we need to add 1 + eth_amount: uint256 = convert(int256, (10**18) * (b * (10**-8))) + # return eth_amount: uint256 = convert(int256, price * eth_amount) + price: int256 = 0 + return price: int256 +``` + +We can compile and deploy this smart contract in Remix. Remix allows us to interact with our smart contract to fund it with our test ETH and then withdraw those funds as well. + +We can use Tenderly to see the balance of our deployed smart contract. diff --git a/courses/vyper-101/2-remix-coffee/10-chainlink-in-vyper/+page.md b/courses/vyper-101/2-remix-coffee/10-chainlink-in-vyper/+page.md new file mode 100644 index 000000000..b00141c8c --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/10-chainlink-in-vyper/+page.md @@ -0,0 +1,78 @@ +We are going to create a simple smart contract to teach you how to use Chainlink price feeds. + +We are going to use Remix to create this smart contract, with the following code. We will explain this code in detail later. + +```python +# @license: MIT +# @author: You! + +minimum_usd: uint256 + +@deploy +def __init__(): + self.minimum_usd = 5 + +@external +@payable +def fund(): + """Allows users to send $ to this contract. + Have a minimum $ amount send + """ + # How do we convert the ETH amount to dollars amount? + assert msg.value >= self.minimum_usd, "You must spend more ETH!" + +@external +def withdraw(): + pass +``` + +We want users to send us the minimum USD value. In this example, we have set this value to 5. So how do we convert this ETH amount to dollars? + +First, let's create a new function for this conversion. + +```python +@internal +def get_eth_to_usd_rate(): + # Address: 0x9aA77716935D5E4AC08b1f17f309aC232d236858 + # ABI + pass +``` + +We will need to obtain the price of ETH in dollars. + +Chainlink provides price feeds, which we can use for this conversion. We will need to get the Chainlink price feed address for the network we are working with. We can find this information in the Chainlink documentation. We are going to be working with the Sepolia testnet, and we can find the address for the ETH / USD price feed there. + +We have the address for the Chainlink price feed. Now, we need the ABI or the Application Binary Interface. This interface defines the functions of the Chainlink price feed contract. + +The ABI is also available in the Chainlink documentation. + +```python +@internal +def get_eth_to_usd_rate(): + # Address: 0x9aA77716935D5E4AC08b1f17f309aC232d236858 + # ABI + eth_usd_price_feed = Contract("0x9aA77716935D5E4AC08b1f17f309aC232d236858", + get_eth_to_usd_rate_abi) + # Now, we can call the latestRoundData function + # of the price feed and store it as a tuple. + # latestRoundData is a function that returns the latest round data for the price feed. + # It returns a tuple of four values: + # - current round + # - latest answer + # - latest timestamp + # - latest updated timestamp + # The second element of this tuple, + # latest answer, is the latest price + # of ETH in dollars. + ( + # current round + , + # latest answer + latest_answer + # latest timestamp + , + # latest updated timestamp + , + ) = eth_usd_price_feed.latestRoundData() + return latest_answer +``` diff --git a/courses/vyper-101/2-remix-coffee/11-abi/+page.md b/courses/vyper-101/2-remix-coffee/11-abi/+page.md new file mode 100644 index 000000000..3520b15ae --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/11-abi/+page.md @@ -0,0 +1,48 @@ +## ABI + +The ABI (Application Binary Interface) is an important part of interacting with smart contracts. It defines the functions that a contract exposes and how different applications can interact with them. + +We'll look at a simple Vyper contract in Remix and explore the ABI generated by the compiler. + +### Generating the ABI + +Let's start by compiling our contract. + +```bash +Compile buy_me_a_coffee.vy +``` + +Once compiled, we can see the compilation details, including the ABI. The ABI is a JSON-formatted document that specifies the contract's functions, inputs, outputs, and other metadata. + +### Understanding the ABI + +The ABI is a list of different types of functions that the smart contract can accept. For example, we can see a `fund` function and a `withdraw` function in our contract. + +```python +"root": { + "0": { + "type": "string \"function\"", + "name": "string \"fund\"", + "stateMutability": "string \"payable\"", + "inputs": [], + "outputs": [] + } +}, +"1": { + "type": "string \"function\"", + "name": "string \"withdraw\"", + "stateMutability": "string \"nonpayable\"", + "inputs": [], + "outputs": [] +} +``` + +The ABI also includes information about the contract's version, EVM version, and other metadata. + +### Using the ABI + +The ABI is used by applications to interact with the smart contract. By providing the ABI to a library or tool, the application can understand the contract's functions and interact with them accordingly. + +The ABI doesn't include the contract's logic. It only defines the interface for interacting with the contract. + +We will see how to integrate the ABI into our smart contract in the next lesson. diff --git a/courses/vyper-101/2-remix-coffee/12-in-line-inter/+page.md b/courses/vyper-101/2-remix-coffee/12-in-line-inter/+page.md new file mode 100644 index 000000000..9801f2031 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/12-in-line-inter/+page.md @@ -0,0 +1,71 @@ +## In-Line Vyper Interfaces + +In this lesson, we'll learn about in-line interfaces in Vyper. + +In-line interfaces are a way to define the functions and return types of a contract without needing to know the logic inside the contract itself. This is useful for interacting with contracts that have been deployed to a blockchain. + +We can define an in-line interface by using the `interface` keyword followed by the name of the interface and a colon. Inside the interface, we can define functions using the `def` keyword, followed by the function name and a colon. Then, we use a right arrow (`->`) to specify the return type for the function. An example of defining an interface is as follows: + +```vyper +interface AggregatorV3Interface: + def decimals(self) -> uint256: view + def description(self) -> String[1000]: view + def version(self) -> uint256: view + def latestAnswer(self) -> int256: view +``` + +We can use this interface to call the functions of the contract without needing to know the logic inside of the contract itself. For example, we could call the `latestAnswer` function using the following code: + +```vyper +price_feed: AggregatorV3Interface = AggregatorV3Interface(0x694A4A17699357215D6F4C081f1f309dDC325306) +return price_feed.latestAnswer() +``` + +This code snippet creates a variable called `price_feed` that holds an interface to the AggregatorV3Interface contract at the address `0x694A4A17699357215D6F4C081f1f309dDC325306`. Then, we can call the `latestAnswer()` function on this variable to obtain the price feed. + +Now we'll deploy a custom function, just to get and see this price feed here. + +```vyper +@external +def get_price() -> int256: + price_feed: AggregatorV3Interface = AggregatorV3Interface(0x694A4A17699357215D6F4C081f1f309dDC325306) + return price_feed.latestAnswer() +``` + +Now, we'll go ahead and deploy this. This custom function gets the latest answer from the price feed. It's an integer, but the price feed is using 8 decimals. We'll want to adjust that. + +```vyper +@external +def get_price() -> int256: + price_feed: AggregatorV3Interface = AggregatorV3Interface(0x694A4A17699357215D6F4C081f1f309dDC325306) + return price_feed.latestAnswer() / 10 ** 8 +``` + +This is what's going to return that price feed. We'll go ahead and deploy this to our blockchain. + +## Deploying to Blockchain + +To deploy this function to the blockchain, we can use Remix. Remix is an online IDE that allows us to write and deploy Solidity code to the blockchain. We'll also want to adjust the `decimals` value in the `AggregatorV3Interface` contract to 8 for this function to work properly. + +```vyper +interface AggregatorV3Interface: + def decimals(self) -> uint256: view + def description(self) -> String[1000]: view + def version(self) -> uint256: view + def latestAnswer(self) -> int256: view +``` + +This code snippet is defining the interface for the `AggregatorV3Interface` contract. The `decimals` function specifies that the returned value for this function is an `int256` (integer with 256 bits). We need to change this so it reflects the 8 decimals used by the price feed. + +Now, we'll paste this code into the `AggregatorV3Interface` contract, just so the `decimals` function now returns the correct value. + +```vyper +interface AggregatorV3Interface: + def decimals(self) -> uint256: view + def description(self) -> String[1000]: view + def version(self) -> uint256: view + def latestAnswer(self) -> int256: view + def latestRoundData(self) -> (int256,int256,int256,uint256,uint256): view +``` + +We can see that the `decimals` function is now correctly reflecting the 8 decimals that are being used in the price feed. We can deploy our `get_price()` function and test it. When we deploy, the `get_price()` function can be tested and it will return a price feed that is represented by 8 decimals. diff --git a/courses/vyper-101/2-remix-coffee/13-staticcall/+page.md b/courses/vyper-101/2-remix-coffee/13-staticcall/+page.md new file mode 100644 index 000000000..a9bf3ba9d --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/13-staticcall/+page.md @@ -0,0 +1,12 @@ +## Calling External Contracts in Vyper + +We are going to make sure that we use the proper keyword when we call external contracts. In Solidity, itοΏ½s pretty straightforward. We just call an external contract. However, iVyperer, we need to use a specific keyword. If we are calling a view function or donοΏ½t want to change any state, we use `staticcall`. Otherwise, we use `extcall`, which stands for external call. + +We will use the following code block to demonstrate how to call an external contract using `staticcall` in Vyper. + +```python +price_feed: AggregatorV3Interface = AggregatorV3Interface(0x6944AA1769357215DE4FAC081bf1f309aDC325306) +return staticcall price_feed.latestAnswer() +``` + +So, in this code block, we're first defining a variable called `price_feed` and setting it to an object of type `AggregatorV3Interface`. Next, we are saying that we want to return the value returned by the `latestAnswer` function, which is a view function in this case. We use `staticcall` since we are calling a view function. This means that we are not going to change any state in our contract. diff --git a/courses/vyper-101/2-remix-coffee/14-tenderly/+page.md b/courses/vyper-101/2-remix-coffee/14-tenderly/+page.md new file mode 100644 index 000000000..86c28316c --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/14-tenderly/+page.md @@ -0,0 +1,29 @@ +## Deploying to Tenderly Virtual Network + +We're going to deploy our contract to our fake Tenderly chain. Remember, if we have your Tenderly application setup, it will fetch data for a second. + +We'll switch to injected Metamask in our Remix IDE and select our fake chain. We'll then deploy the contract. Before deploying, we need to ensure the Solidity contract is compiled. This is a requirement of the Remix IDE as of this recording. + +After we deploy our contract, we'll switch back to Remix VM. Our fake chain is based on the Sepolia network, which is a test network. We've turned off state sync for this chain. That means we won't update the chain to the latest Sepolia block. This will keep costs down. + +We can then call the `get_price` function, which will return the ETH price on Sepolia when we first created this chain. + +Let's go over the major concepts we just used: + +1. **ABI** +2. **Address** +3. **Static Call** + +We need to understand what an ABI and address is when we interact with contracts. We got the address for our chainlink price feed from the chainlink documentation. We also got the ABI for the contract from the `AggregatorV3Interface` interface. This interface essentially defines the ABI and tells us which functions are available. + +When we interact with a contract, we have to specify whether it is a `static call` or `ext call`. A `static call` does not modify the state of a contract. In this case, we were performing a `static call` to our `get_price` function because we were simply calling the view function of the contract. + +If you're still getting up to speed on this, don't worry. We'll go over these concepts in more detail throughout Updraft. + +```python +def get_price() -> int256: + price_feed: AggregatorV3Interface = AggregatorV3Interface(0x6944A41769357215DE4FAC081bf1f309aDb) + return staticcall(price_feed.latestAnswer()) +``` + +This code shows how we are getting the latest ETH price from the chainlink price feed. We call the `latestAnswer` function of the price feed, which returns an integer. diff --git a/courses/vyper-101/2-remix-coffee/15-ai/+page.md b/courses/vyper-101/2-remix-coffee/15-ai/+page.md new file mode 100644 index 000000000..8211f1c30 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/15-ai/+page.md @@ -0,0 +1,74 @@ +## AI Prompting for Smart Contracts + +AI can be extremely helpful when dealing with smart contract code, and we'll be looking at how it can be used in this lesson. + +The first thing we can do is use an AI to help explain what we are looking at, particularly if we're confused. Let's look at an example: + +```python +@external +@view +def get_price() -> int256: + price_feed: AggregatorV3Interface = AggregatorV3Interface(0x69444A1769357215DEA6AC081bf1f130939a0dC25306) + # ABI + # Address + return staticcall(price_feed.latestAnswer(), []) +``` + +If we're unsure about what the `staticcall` keyword is doing, we can copy that line of code and paste it into the AI of our choice. For this example we'll be using Claude. We'll add some context around the code and ask our AI to explain it to us. + +Here's an example of what we might ask Claude: + +```` +Hi, I have this function: + +```python +@external +@view +def get_price() -> int256: + price_feed: AggregatorV3Interface = AggregatorV3Interface(0x69444A1769357215DEA6AC081bf1f130939a0dC25306) + # ABI + # Address + return staticcall(price_feed.latestAnswer(), []) +```` + +This is in my Vyper smart contract. I don't understand what the `staticcall` keyword is doing. Can you help explain it to me? + +```` + +Claude will then give us an explanation of what the keyword does, which can help us to better understand this part of our smart contract. + +We can also ask Claude to write some code for us, so we can avoid having to type it all ourselves, and this can be especially helpful when we are working with more complex code. If we have a smart contract that is already written and we're looking at how it's currently being used, we can ask Claude to write a new function that takes that code, restructures it, and makes it safe for our use. + +For example, if we have the following function: + +```python +@external +def set_price(price: int256): + price_feed: AggregatorV3Interface = AggregatorV3Interface(0x69444A1769357215DEA6AC081bf1f130939a0dC25306) + # ABI + # Address + price_feed.setLatestAnswer(price) +```` + +We can copy this function, paste it into Claude and ask: + +```` +Hi Claude, this function: + +```python +@external +def set_price(price: int256): + price_feed: AggregatorV3Interface = AggregatorV3Interface(0x69444A1769357215DEA6AC081bf1f130939a0dC25306) + # ABI + # Address + price_feed.setLatestAnswer(price) +```` + +This is in my Vyper smart contract. I would like to make it so that this function can be called by a `staticcall`. Can you help me write a new function that does that? + +``` + +Claude will then write a new function that takes the existing code, restructures it, and uses the `staticcall` keyword so that it is safe for our use. + +As we progress through this course, we will be learning a lot about smart contracts, but often the hardest part is knowing when to ask for help. We'll be doing everything in our power to avoid that! +``` diff --git a/courses/vyper-101/2-remix-coffee/16-deploy-parameter/+page.md b/courses/vyper-101/2-remix-coffee/16-deploy-parameter/+page.md new file mode 100644 index 000000000..1b995a7c9 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/16-deploy-parameter/+page.md @@ -0,0 +1,40 @@ +## Deployment Parameterization + +We're back to our code base. We now have this `get_price` function which we are actually going to comment out. Again, I'm using command forward slash as a keyboard shortcut for this because we are not actually going to use this. + +But what we are going to do is we're going to create this `get_eth_to_usd_rate` function. + +We just learned how to get the price from a chain price feed. This is really exciting. We're only going to use this one price feed in this entire contract. + +We are going to make the price feed a state variable, up at the top. + +Since our structs are the same as in the last lesson, the `AggregatorV3Interface` is a new type. We can create a variable, a state variable, or a storage variable, called `price_feed` of type `AggregatorV3Interface`. + +In our deploy function we can say `self.price_feed = AggregatorV3Interface()` at address. We are hardcoding this address into the contract. + +We can add a comment saying that we are using the Sepolia testnet: + +```python +# Sepolia +``` + +We should instead add the address as a parameter. We can do this with the following code: + +```python +@external +def deploy(price_feed_address: address): + self.minimum_usd = 5 + self.price_feed = AggregatorV3Interface(price_feed_address) +``` + +The deploy function now has an input for the price feed address. + +Now that we have our `price_feed` object, we can scroll down to our `get_eth_to_usd_rate` function. We will make the price variable equal to `staticcall self.price_feed.latestAnswer()`. + +```python +@internal +def get_eth_to_usd_rate(): + price: int256 = staticcall(self.price_feed.latestAnswer()) +``` + +This way, right when we deploy our contract, we can parameterize the address that we want to use. This is a much cleverer way than hard coding it in. diff --git a/courses/vyper-101/2-remix-coffee/17-integer-precision/+page.md b/courses/vyper-101/2-remix-coffee/17-integer-precision/+page.md new file mode 100644 index 000000000..4b9129ca8 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/17-integer-precision/+page.md @@ -0,0 +1,107 @@ +## Integer Precision + +We're going to work on integer precision. We'll be getting the price of Ethereum using a Chainlink price feed, which returns a price with eight decimals. + +Here we're creating a function called `get_eth_to_usd_rate` which returns the price. + +```python +def get_eth_to_usd_rate(): + price: int256 = staticcall(self.price_feed.latestAnswer(), []) +``` + +We'll use the `decimals` function to get the number of decimal places in the price. + +```python + # 8 decimals +``` + +This function returns a price in USD, which is 3,021. + +```python + # $3,021 +``` + +We need to calculate the ETH amount in USD. + +```python + eth_amount_in_usd +``` + +The price that we're going to get from Chainlink is going to be a number like this: + +```python + price: int256 = staticcall(self.price_feed.latestAnswer(), []) # 33655100000 +``` + +We need to convert this to the ETH amount in USD. + +```python + eth_amount_in_usd +``` + +The function that we'll use is called `get_eth_to_usd_rate`, and we'll pass in the ETH amount. + +```python +def get_eth_to_usd_rate(eth_amount: uint256): +``` + +Let's say Chris sent us 0.01 ETH to buy a coffee. + +```python + Chris sent us 0.01 ETH for us to buy a coffee. +``` + +Is that more or less than $5? + +```python + Chris sent us 0.01 ETH for us to buy a coffee. + Is that more or less than $5? +``` + +Our minimum USD is $5. + +```python +minimum_usd: uint256 +``` + +So how do we convert this 0.01 to its dollar amount? + +```python + price: int256 = staticcall(self.price_feed.latestAnswer(), []) # 33655100000 +``` + +We get the price, which will be something like this. + +```python + price: int256 = staticcall(self.price_feed.latestAnswer(), []) # 33655100000 +``` + +Now, our ETH amount is going to have how many decimal places? + +```python + price: int256 = staticcall(self.price_feed.latestAnswer(), []) # 33655100000 +``` + +This is where the math in the blockchain gets a little bit tricky. + +Remember before, in Ethereum, there's ether, there's Gwei, and there's Wei. One ether has this many Wei. + +So it's got 18 zeros, or 18 decimal places. However, we found from looking at this, and then actually there's also a decimals function, which has eight here. This answer only has eight decimal places. It has eight additional zeros, although some of the zeros are actual numbers. So it has eight decimal places. + +So we first going to need to update this price from having eight decimals to 18. That's actually really easy. All we have to do is say ETH price, + +```python + eth_price: uint256 = +``` + +of type uint256 equals price times 10 raised to the 10th. + +```python + eth_price: uint256 = price * 10**10 +``` + +This is how we add those decimal places. + +```python + eth_price: uint256 = price * 10**10 +``` diff --git a/courses/vyper-101/2-remix-coffee/18-converting-types/+page.md b/courses/vyper-101/2-remix-coffee/18-converting-types/+page.md new file mode 100644 index 000000000..0536f7b88 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/18-converting-types/+page.md @@ -0,0 +1,30 @@ +## Converting Types in Vyper + +We will learn about type conversions in Vyper. We'll start with an example of a scenario where we might need to perform a type conversion: + +```python +@external +def get_eth_to_usd_rate(eth_amount: uint256): + # Chris sent us 0.01 ETH for us to buy a coffee. + # Is that more or less than $5? + price: int256 = staticcall(self.price_feed, "latestAnswer()", [336551000000]) + # 8 decimals + # $3,021 + eth_amount_in_usd = price * (10 ** 10) +``` + +This code shows an error: + +```bash +TypeError: TypeMismatchGiven reference has type int256, expected uint256 +``` + +This error indicates that the variable `eth_amount` has a type `int256`, but the variable `price` is expecting `uint256`. + +We can address this by performing a type conversion on the `price` variable. Vyper provides a built-in function called `convert`, which we can use to convert a variable or literal from one type to another. + +```python +eth_amount_in_usd = convert(price, uint256) * (10 ** 10) +``` + +This line converts the `price` variable, which is `int256`, to `uint256`, and then multiplies by `10**10`. We can compile this code and it should execute as expected. diff --git a/courses/vyper-101/2-remix-coffee/19-integer-division/+page.md b/courses/vyper-101/2-remix-coffee/19-integer-division/+page.md new file mode 100644 index 000000000..7f0505d68 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/19-integer-division/+page.md @@ -0,0 +1,100 @@ +## Integer Division in Vyper + +We're going to make a smart contract to buy us a coffee. Our price is going to be a uint256. + +```python +price: int256 = staticcall self.price_feed.latestAnswer() +``` + +Let's say the price is $3.021. + +```python +# 8 decimals +# $3,021 +``` + +We can then multiply this price times 10 ^ 10 to get it into units of ETH. + +```python +eth_price: uint256 = convert(price, uint256) * (10 ** 10) +``` + +Now we've multiplied this price by 10 ^ 10, so we need to add zeros. + +```python +# ETH : 110000000000000000000 +``` + +We can then get a USD value of ETH. + +```python +eth_amount_in_usd: uint256 = (eth_amount * eth_price) // 1 * (10 ** 18) +``` + +Let's break down this equation to understand what's going on. First, we have the eth_amount, which is the amount of ETH. This is multiplied by eth_price, which is in dollars per ETH. We are going to add zeros to this. + +```python +# $ / ETH : 336551000000000000000 +``` + +To do this, we divide by 10 ^ 18. + +```python +// 1 * (10 ** 18) +``` + +We're going to create a new function called divide_me which will take a uint256 number as input and return a uint256. + +```python +def divide_me(number: uint256) -> uint256: +``` + +We'll return this value. + +```python +return number // 3 +``` + +We can see that this works by trying a few examples. + +```python +# 4 / 2 = 2 +# 6 / 3 = 2 +# 7 / 3 +``` + +Let's compile this and deploy it. + +```bash +Compile buy_me_a_cofee.vy +``` + +We'll grab any address, like our price feed address. We'll scroll down and put in one for divide_me. What do you think we'll get if we do 1 / 3? + +```bash +Deploy +``` + +We get 0. What about 2? Still 0. How about 3? We get 1. Four? One. Five? One. Six? Two. So, integer division is basically cutting off any decimal places. Let's try 7 / 3. + +```bash +Transact +``` + +It's still 2. What if we do 7 / 3 on our calculator? It's 2.33333333. This is because we can't have decimals in our smart contracts. We need to be very precise when working with smart contracts. That's a crucial piece of knowledge to keep in mind when designing mathematically complex applications. We're also going to comment out the math examples we used, but we'll leave them in the Git repo associated with this course. We also have a function called get_eth_to_usd_rate. + +```python +def get_eth_to_usd_rate(eth_amount: uint256) -> uint256: +``` + +We can now go back up and convert msg.value, the amount of ETH that they sent, to its dollar equivalent. + +```python +usd_value_of_eth: uint256 = self.get_eth_to_usd_rate(msg.value) +``` + +So, instead of saying that msg.value must be greater than 1 ether, which would mean coffee cost $3,000, we'll say that the USD value of ETH must be greater than or equal to self.minimum_usd. + +```python +assert usd_value_of_eth >= self.minimum_usd, "You must spend more ETH!" +``` diff --git a/courses/vyper-101/2-remix-coffee/2-setup/+page.md b/courses/vyper-101/2-remix-coffee/2-setup/+page.md new file mode 100644 index 000000000..53044a9ad --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/2-setup/+page.md @@ -0,0 +1,100 @@ +We're going to start by deleting everything in our Remix here. You're probably a little bit familiar with Remix at this point, which is great. So, we're going to go ahead and delete everything in my Remix here. And, I'm going to create again this hi.sol contract here. Oh, it came back. Sorry about that. And, I'm going to create this hi.sol contract because, again, Remix assumes you're going to be working with Solidity and is always looking for Solidity. So, we have to at least make a Solidity file. + +```python +pragma solidity ^0.8.18; + +contract hi { + +} +``` + +So, we're going to go ahead and we'll go to the Solidity compiler. We'll compile hi.sol so that we don't have to worry about this in the future. And then, we'll also do SPDX license identifier MIT. + +```python +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract hi { + +} +``` + +Again, don't need to worry about the Solidity stuff for now, just know that as of recording, Remix has this bug where it needs to see a Solidity file in order for it to deploy anything. But, so let's go back to our File Explorer here. And let's create a new file called buy me a coffee. + +Now, right on the naming convention here, you see we're using underscores for spaces between the names here. This is known as snake case. And, there's a bit of a disagreement in the Vyper community as of recording, if this is better, or something called camel case is better, which looks like this. Where it's like every start of a new word just has a capitalized letter at the beginning. Python by nature is typically what's called snake case with these underscores like this. But, I want to point out that for the names of files, you might see it both ways. In the Python, in the Vyper code itself, we'll definitely be using snake case with the underscores. But, for the names of file it might go either way. Now, before we actually begin coding anything, let's write down what we want to do here. It's usually a good idea to write down what you want your code base to do before you even get started coding so that you have a design spec of what it should be doing. And, this is a really good step for anybody who is building smart contracts or building any type of code. So, what do we want this to do? Well, we want this application to do three things: allow us to get funds from users so that people can buy us coffee. We want to be able to withdraw those funds so that we can actually go off and buy the coffee. And then finally, set a minimum funding value in USD. + +Now, originally, I said $50, we might make it five. We might make it two because now that I'm thinking about it, $50 coffee is ridiculous. But, you know, you can set your coffee price to whatever you want it to be. So, let's go ahead and let's get started here, of course, with pragma version 0.4.0. And, we'll say @license MIT. + +```python +# Get funds from users +# Withdraw funds +# Set a minimum funding value in USD + +pragma version 0.4.0 +@license: MIT +``` + +And then if you want, you can also do @author and put your name here. + +```python +# Get funds from users +# Withdraw funds +# Set a minimum funding value in USD + +pragma version 0.4.0 +@license: MIT +@author: You! +``` + +There's a couple other different types of tags that you'll see people use pretty often. And, pretty soon we'll actually learn a nicer way to format this than with these hashtags here, but we'll get to that in a bit. + +Now, let's actually build a little bit of a skeleton for our smart contract here. Let's just build the names of the functions that we want this to have. So, we probably want a def fund some type of fund function here. + +```python +# Get funds from users +# Withdraw funds +# Set a minimum funding value in USD + +pragma version 0.4.0 +@license: MIT +@author: You! + +def fund(): +``` + +And what you can do that's quite nice in Vyper is that if you want to just name a function but not have it do anything quite yet, you can just type this pass in the function definition. + +```python +# Get funds from users +# Withdraw funds +# Set a minimum funding value in USD + +pragma version 0.4.0 +@license: MIT +@author: You! + +def fund(): + pass +``` + +So, this contract right here, even though it doesn't really do anything, is actually valid Vyper. So, I can even go ahead and compile this. And, you'll see that it compiles successfully. + +So, this pass keyword is valid Vyper here. So, we want def fund. What else? We probably want def withdraw. We want to be able to withdraw the money that is funded to us. And, that's pretty much it. Now, these two functions are going to be the main functionality of the contract. However, we are obviously going to be adding other functions in here as well because, well, in order to fund and withdraw, we probably need some other functionality, including getting price and etc. So, this fund function, we want anybody outside of this contract to be able to call. So, let's give it a visibility of @external. So now, anybody can call this. Same thing, we want humans to actually be able to call withdraw. So, we'll do @external. We probably only want us to call this, but we'll fix that in a bit. + +```python +# Get funds from users +# Withdraw funds +# Set a minimum funding value in USD + +pragma version 0.4.0 +@license: MIT +@author: You! + +@external +def fund(): + pass + +@external +def withdraw(): + pass +``` diff --git a/courses/vyper-101/2-remix-coffee/20-as-wei-value/+page.md b/courses/vyper-101/2-remix-coffee/20-as-wei-value/+page.md new file mode 100644 index 000000000..3455a8ef8 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/20-as-wei-value/+page.md @@ -0,0 +1,43 @@ +## As Wei Value + +In this lesson, we're going to look at the `as_wei_value()` function within solidity. + +Let's say we have a smart contract that requires a minimum USD value to be sent to it. In this case, that minimum amount is 5 USD. + +```python +self.minimum_usd = 5 +``` + +However, this can be an issue because we're working in Ethereum, and the units of value are in Ether. + +```python +usd_value_of_eth: uint256 = self.get_eth_to_usd_rate(msg.value) +assert usd_value_of_eth >= self.minimum_usd, "You must spend more ETH!" +``` + +Remember, ETH has 18 decimal places. So, a value of 5 USD will not be represented as 5 in this context. Instead, we'll need to convert the value of 5 USD into Ether (WEI). + +```python +self.minimum_usd = 5 * (10 ** 18) +``` + +We can also do this: + +```python +self.minimum_usd = 5000000000000000000 +``` + +However, this is not a readable way to present the code. Instead, we can use the function `as_wei_value()` to make the code much more legible. + +```python +self.minimum_usd = as_wei_value(5, "ether") +``` + +We can now use this new minimum USD value (in WEI) for our assertions: + +```python +assert usd_value_of_eth >= self.minimum_usd, "You must spend more ETH!" +``` + +This `as_wei_value()` function is a great shortcut for adding those 18 decimal places. +courses\vyper-101\2-remix-coffee\20-as-wei-value\+page.md diff --git a/courses/vyper-101/2-remix-coffee/21-send-eth-in-tx/+page.md b/courses/vyper-101/2-remix-coffee/21-send-eth-in-tx/+page.md new file mode 100644 index 000000000..061899398 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/21-send-eth-in-tx/+page.md @@ -0,0 +1,54 @@ +In this lesson, we'll learn how to send a transaction using ETH. We can then use a deployed contract to show how to call another function. + +Let's begin by going over how to compile, deploy and run transactions. + +We'll need to compile our contract. We can then deploy it to our fake chain. + +```python +# Get funds from users +# Withdraw funds +# Set a minimum funding value in USD +# pragma version 0.4.8 +# license: MIT +# author: You! +# We'll learn a new way to do interfaces later... +interface AggregatorV3Interface: + def decimals() -> uint8: view + def description() -> String: view + def version() -> uint256: view + def latestAnswer() -> int256: view +minimum_usd: uint256 +price_feed: AggregatorV3Interface = AggregatorV3Interface(0x69444179935776935DE41F4AC081f1FD3C909Dc256) # sepolia +# ... +def __init__(price_feed: address): + self.minimum_usd = as_wei_value(5, "ether") + self.price_feed = AggregatorV3Interface(price_feed) +# ... +# Allows users to send $ to this contract +def fund(): + # Have a minimum $ amount to send + # How do we convert the ETH amount to get dollars amount? + usd_value_of_eth: uint256 = self.get_eth_to_usd_rate(msg.value) + assert usd_value_of_eth >= self.minimum_usd, "You must spend more ETH!" +# ... +def get_eth_to_usd_rate(amount: uint256) -> uint256: + # ... + eth_price: uint256 = staticcall(self.price_feed, latestAnswer()) + # ... + eth_amount_in_usd: uint256 = (eth_price * eth_amount / 10 ** 18 + 18 ** 18 * 1) / 10 ** 18 + return eth_amount_in_usd # in $ 's, 18 decimal places +# ... +# remove all decimals +def divide_number(me: uint256) -> uint256: + # ... +``` + +After deploying our contract we can use a calculator to determine the amount of ETH that should be sent to the `fund` function. The `fund` function requires at least 5 dollars worth of ETH. + +We will need to use a website like `eth-converter.com` to calculate the amount of ETH required. + +We'll then paste this amount in Remix and send the transaction to our contract. + +We can now call the `fund` function from Remix to test our logic. This will send our calculated amount of ETH to the contract and trigger the `fund` function. + +The contract balance will then be updated to show the new balance after the transaction has been processed. diff --git a/courses/vyper-101/2-remix-coffee/22-contract-to-contract/+page.md b/courses/vyper-101/2-remix-coffee/22-contract-to-contract/+page.md new file mode 100644 index 000000000..cdfca2c8f --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/22-contract-to-contract/+page.md @@ -0,0 +1,34 @@ +## Making a function call another function + +In this lesson, we'll learn about making a function call another function in Vyper. + +We'll use an example of a function that takes an amount of ETH in Wei and converts it to USD. + +First, we create a function to do the calculation. + +```python +def get_eth_to_usd_rate(eth_amount: uint256) -> uint256: + """ + Chris sent us 0.01 ETH for us to buy a coffee: + Is that more or less than $5? + """ + eth_price: uint256 = staticcall(self.price_feed, latestAnswer()) * (10 ** 10) + price_in_usd: uint256 = (convert(uint256, price) * eth_amount) // (1 * 10 ** 18) + eth_amount_in_usd_uint256: uint256 = price_in_usd * eth_amount // (1 * 10 ** 18) + return eth_amount_in_usd_uint256 # 18 0's, 18 decimal places +``` + +We can create an external function that runs our internal function. + +```python +@external +@view +def get_eth_to_usd_rate(eth_amount: uint256) -> uint256: + return self._get_eth_to_usd_rate(eth_amount) +``` + +We'll say ETH amount uint256 returns a uint256. All we got to do is say return self.\_get_eth_to_usd_rate(eth_amount). + +This creates an external function `get_eth_to_usd_rate` that calls the internal function `_get_eth_to_usd_rate` passing the `eth_amount` as an argument. + +This is a useful technique when you need to expose an internal function. It allows you to keep internal functions private while still providing an external interface to access their functionality. diff --git a/courses/vyper-101/2-remix-coffee/23-getting-revert/+page.md b/courses/vyper-101/2-remix-coffee/23-getting-revert/+page.md new file mode 100644 index 000000000..eb63086a1 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/23-getting-revert/+page.md @@ -0,0 +1,35 @@ +## Getting a Revert + +In this lesson, we are going to learn how to get a revert. We'll test out this revert to ensure that our code is working as intended. We'll use a fake blockchain network, Remix, and a smart contract for this example. + +### Testing our Revert + +We are going to use the smart contract we created in previous lessons to test our revert. We'll first deploy the contract to our fake blockchain network. + +Let's test our smart contract! We need to check if the revert function is working correctly. We need to see what happens when people don't send enough money to the contract. + +#### Making a Transaction + +We can use a website like eth-converter.com to determine how much Wei we need to send to the contract in order to meet the required minimum value. Let's use 2000000000000000 Wei, which is $6.53. + +Now let's copy this Wei value and paste it into the `get_eth_to_usd_rate` function in Remix. + +We'll now go ahead and click on the `fund` button on the Remix interface. + +#### The Revert + +You will notice that we are getting an error: `Gas estimation failed`, `Transaction execution will likely fail`. Let's go ahead and click on the `Send Transaction` button. + +Now that the transaction has been sent, we can go ahead and use the drop-down menu to view the transaction details. We can see that the transaction has been `mined` but that the `execution failed`. + +This error happens because we are not sending at least 5 USD to the contract. The `assert` function was designed to stop the execution of the transaction if the user is not meeting the minimum value. This is called a `revert`. + +Our `assert` function is working exactly as it should! + +```python +assert usd_value_of_eth >= self.minimum_usd, "You must spend more ETH!" +``` + +As we can see in the code, the `assert` function is used to ensure that the user is sending at least 5 USD. If the user doesn't meet this requirement, then the `revert` message is triggered. + +We've now demonstrated what a `revert` looks like in practice. We'll learn more about reverts and how to handle them in future lessons. diff --git a/courses/vyper-101/2-remix-coffee/24-msg-sender/+page.md b/courses/vyper-101/2-remix-coffee/24-msg-sender/+page.md new file mode 100644 index 000000000..2323a0727 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/24-msg-sender/+page.md @@ -0,0 +1,51 @@ +## msg.sender, owner and access control + +We're building a smart contract where we want to make sure only we can pull the money out of the contract. We do this by using the msg.sender, which is the account that's sending the message. We can use this to make sure that only the owner of the contract can withdraw the money. + +We first set the owner in the deploy function: + +```python +owner: address +``` + +```python +def __init__(price_feed: address): + self.minimum_usd = as_wei_value(5, "ether") + self.price_feed = AggregatorV3Interface(price_feed) + self.owner = msg.sender +``` + +We use the `msg.sender` built-in variable to set the owner of the contract to the account that's deploying it. + +Then, in the withdraw function, we use the `assert` statement to make sure that the msg.sender is equal to the owner. + +```python +external +def withdraw(): + """Take the money out of the contract, that people sent via the fund function. + How do we make sure only we can pull the money out? + """ + assert msg.sender == self.owner, "Not the contract owner!" +``` + +If the msg.sender is not the owner, the transaction will revert. This makes sure that only the owner of the contract can withdraw the money. + +To make sure the owner is public we add `public` in front of the declaration. + +```python +owner: public(address) +``` + +We can see this in action if we redeploy the contract and call the withdraw function. + +If we call the withdraw function from the owner account, it will be successful. However, if we call the withdraw function from a different account, it will revert with the error message "Not the contract owner!". + +This is an example of how we can use the msg.sender to control access to our smart contract functions. It's a simple but powerful technique that can be used to secure our code. + +```bash +truffle compile +``` + +```bash +truffle migrate +``` diff --git a/courses/vyper-101/2-remix-coffee/25-withdraw-eth/+page.md b/courses/vyper-101/2-remix-coffee/25-withdraw-eth/+page.md new file mode 100644 index 000000000..175e1b1bb --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/25-withdraw-eth/+page.md @@ -0,0 +1,72 @@ +## Sending ETH From a Contract + +In this lesson, we'll learn how to send ETH from a smart contract to a designated address, using the `send` function. Later, we'll look at another built-in function called `raw_call` and how to implement that function. + +We'll start by creating a `withdraw` function that will send the ETH to the owner of the smart contract. + +```python +@external +def withdraw(): + """Take the money out of the contract, that people sent via the fund function. + How do we make sure only we can pull the money out? + """ + assert msg.sender == self.owner, "Not the contract owner!" + + send(self.owner, self.balance) +``` + +In our function, we can see that we are using the `send` function and giving it two arguments: the recipient's address (`self.owner`) and the value that we want to send (`self.balance`). + +Our smart contract has a balance of ETH that we'll use to send to the owner. We'll start by funding our contract, using the `fund` function. + +```python +@external +@payable +def fund(): + """Allows users to send $ to this contract + Have a minimum $ amount to send + """ + usd_value_of_eth: uint256 = self.get_eth_to_usd_rate(msg.value) + assert usd_value_of_eth >= self.minimum_usd, "You must spend more ETH!" +``` + +We can see that our `fund` function takes an amount in ETH and converts it to USD for comparison. + +Now, we'll deploy the contract. We'll use the injected provider and set the value in wei. Then, we'll confirm the deployment. + +```bash +vyper --version +vyper --optimize-for-size --optimize-for-speed --deploy +``` + +Now, we can call the `fund` function and provide a value in wei to fund our contract. + +```bash +fund(1000000000000000) +``` + +We'll confirm the transaction. It will take a few seconds for the balance to update, but we'll confirm that it worked. + +Now, let's go ahead and call the `withdraw` function. We'll confirm the transaction, and then we'll check our balance to make sure that the ETH was successfully transferred. + +```bash +withdraw() +``` + +Now, we'll check that the balance has gone back to zero in our deployed contract. + +We can see that our balance has gone back to zero. + +Now, if we want to fund with more than just wei, we can use the `ether` option in the value field and fund our contract with one ETH. + +```bash +fund(1 ether) +``` + +We'll confirm the transaction, and we'll check that the balance has gone up to 998. + +Let's go back to our contract and call the `withdraw` function. Confirm the transaction, and then check the balance in Metamask to make sure it went up to 998. + +We can see that we successfully funded our contract with 1 ETH. + +That's how we can send ETH from a contract! In the next lesson, we'll learn how to use the `raw_call` function. \ No newline at end of file diff --git a/courses/vyper-101/2-remix-coffee/26-dynamic-vs-fixed-array/+page.md b/courses/vyper-101/2-remix-coffee/26-dynamic-vs-fixed-array/+page.md new file mode 100644 index 000000000..f3e3153ea --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/26-dynamic-vs-fixed-array/+page.md @@ -0,0 +1,119 @@ +## Dynamic vs. Fixed Arrays in Vyper + +We're going to create a new Vyper contract to demonstrate the differences between a **dynamic array** and a **fixed-size array.** + +### Dynamic Array + +First, let's declare a dynamic array: + +```vyper +dynamic_array: public DynArray[uint256, 100] +``` + +The array type is `DynArray` and has a maximum size of 100. We set this array to be `public` so that it's viewable from outside the contract. + +### Fixed-Size Array + +Now, let's create a fixed-size array: + +```vyper +fixed_sized_array: public uint256[100] +``` + +The fixed-size array is simply a list of 100 `uint256` values. Note that we can't set the size of the array to be dynamic like we did with the `DynArray.` + +### `len` Keyword + +We can use the `len` keyword to determine the length of a dynamic array. Let's create a function called `dyn_array_size` that returns the length of our dynamic array: + +```vyper +@external +@view +def dyn_array_size() -> uint256: + return len(self.dynamic_array) +``` + +### Adding Elements to Arrays + +We can add elements to our arrays with the `append` function. Let's create a function called `add_to_array` that adds an element to both arrays: + +```vyper +@external +def add_to_array(): + self.fixed_sized_array[self.index] = 1 + self.dynamic_array.append(1) + self.index = self.index + 1 +``` + +We also need to declare an index variable: + +```vyper +index: uint256 +``` + +### Dynamic Array Length + +We can see that the dynamic array length is initially 0 because it's initialized as an empty array: + +```vyper +#[ ] +``` + +However, after we append an element, the length increases to 1: + +```vyper +#[1] +``` + +We can append as many elements to the dynamic array as long as the maximum size of 100 is not exceeded. + +### Fixed Array Length + +Now, let's try to append an element to our fixed array. We'll create a function called `fixed_array_size` that returns the size of the fixed array: + +```vyper +@external +@view +def fixed_array_size() -> uint256: + return len(self.fixed_sized_array) +``` + +We can compile this code: + +```bash +vyper arrays_compared.vy +``` + +We can now deploy the contract. Let's run the `add_to_array` function to add an element to our dynamic array: + +The dynamic array length now reflects the change to 1. + +```vyper +#[1] +``` + +Now, let's try to run the `add_to_array` function again. + +We can see that the dynamic array length is now 2. + +```vyper +#[1,1] +``` + +However, the fixed array length is still 1. + +### Fixed Array Limitations + +We can try to append to the fixed array: + +```vyper +self.fixed_sized_array.append(1) +``` + +But we'll get an error! + +We cannot append to fixed-size arrays because the length is determined at the time the array is declared. However, we can modify existing elements. + +### Conclusion + +The difference between dynamic and fixed-size arrays is whether they can change in size during runtime. Fixed-size arrays are more efficient than dynamic arrays, but we lose flexibility. Dynamic arrays allow for flexibility but can be less efficient. diff --git a/courses/vyper-101/2-remix-coffee/27-resetting-array/+page.md b/courses/vyper-101/2-remix-coffee/27-resetting-array/+page.md new file mode 100644 index 000000000..72dc92cda --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/27-resetting-array/+page.md @@ -0,0 +1,21 @@ +## Resetting a Dynamic Array + +We are looking to keep track of all the funders in our contract. We can set this up as a fixed-size array, but it is probably going to be better for us to use a dynamic array. Let's make this a public dynamic array of addresses with a max size of, let's say, 1,000. This means there will be a maximum of 1,000 addresses we can have as funders. If we have more funders, our contract will kind of break, but let's just hope that we don't get too many people who want to buy us coffee. + +We'll scroll down to our fund function and we'll keep track of this funder here, whoever funds us. We'll say: + +```python +self.funders.append(msg.sender) +``` + +So, we're going to say, hey, in our funders dynamic array, we'll just append, we'll just add to the list, the message.sender, whoever calls this fund function. + +Great, and then when we call withdraw, we probably want to reset the funders, maybe we don't, but I want to reset the funders every time I call withdraw. We can do this really easily with dynamic arrays which is part of the reason why I'm using a dynamic array. We can just say: + +```python +self.funders = [] +``` + +Boom! That's it. + +This is how you identify an empty array. So self.funders equals this, means it's an empty array, and it is essentially reset. diff --git a/courses/vyper-101/2-remix-coffee/28-plus-equals/+page.md b/courses/vyper-101/2-remix-coffee/28-plus-equals/+page.md new file mode 100644 index 000000000..acdec5825 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/28-plus-equals/+page.md @@ -0,0 +1,49 @@ +## Mappings to track funders + +In this lesson, we'll learn how to keep track of who sent funds to our contract, and how much they sent. + +We'll use a mapping. Mappings are a fundamental concept in smart contracts. Think of them as dictionaries in Python or JavaScript. They pair a key with a value. We can make mappings public to allow anyone to read them. + +We'll create a new mapping to keep track of funders and how much they sent. + +```python +# # Keep track of who sent us +# # How much they sent us +``` + +We'll call this new mapping `_funders_to_amount_funded`. + +```python +# funder -> how much they funded +_funders_to_amount_funded: public(HashMap[address, uint256]) +``` + +This mapping will store the address of the funder as a key and the amount they sent as a value. + +In our `fund` function, we'll track the amount each person sent us. + +```python +@payable +def fund(): + """ + Allows users to send $ to this contract + Have a minimum $ amount to send + """ + # ... previous code + self._funders_to_amount_funded[msg.sender] += msg.value + # ... +``` + +Here, we access the hash map with the `msg.sender` address as the key and add the amount sent by the `msg.sender` (`msg.value`) to the value associated with the `msg.sender` in the hash map. We can also use the `+=` operator as a shorthand for adding to a value in a mapping. + +```python +self._funders_to_amount_funded[msg.sender] = self._funders_to_amount_funded[msg.sender] + msg.value +``` + +We can also use the `+=` operator as a shorthand for adding to a value in a mapping. + +```python +self._funders_to_amount_funded[msg.sender] += msg.value +``` + +This is the same as writing the line above. diff --git a/courses/vyper-101/2-remix-coffee/29-for-loops/+page.md b/courses/vyper-101/2-remix-coffee/29-for-loops/+page.md new file mode 100644 index 000000000..c481f628d --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/29-for-loops/+page.md @@ -0,0 +1,94 @@ +## For Loops + +In this lesson we will explore the `for` loop in Vyper. + +We will first use a for loop to iterate over a range of integers. We can use the `range` function to specify the starting and ending index of the loop. + +```python +@external +@pure +def for_loop() -> DynArray[uint256]: + arr: DynArray[uint256, 10] = [] + + for i: uint256 in range(5): + arr.append(i) + + return arr +``` + +In the above code, we are using a for loop to iterate over the first 5 integers. For each iteration, we append the current integer to the `arr`. Finally, we return the `arr`. + +We can also iterate over a fixed-size list. + +```python +@external +@pure +def for_loop_list() -> DynArray[uint256]: + arr: DynArray[uint256, 10] = [] + nums: uint256[4] = [11, 22, 33, 44] + + for i: uint256 in nums: + arr.append(i) + + return arr +``` + +In this code, we are iterating over the fixed-size list `nums` and appending each element to the `arr`. + +You can also skip iterations using the `continue` keyword. For example: + +```python +@external +@pure +def for_loop_skip() -> DynArray[uint256]: + arr: DynArray[uint256, 10] = [] + nums: uint256[4] = [11, 22, 33, 44] + + for i: uint256 in nums: + if i == 2: + continue + arr.append(i) + + return arr +``` + +In the code above, we are iterating over the list `nums`. If the current element is equal to 2, the `continue` keyword will skip the rest of the code for that iteration and move on to the next iteration. + +You can also break out of a for loop early using the `break` keyword. + +```python +@external +@pure +def for_loop_break() -> DynArray[uint256]: + arr: DynArray[uint256, 10] = [] + nums: uint256[4] = [11, 22, 33, 44] + + for i: uint256 in nums: + if i == 2: + continue + if i == 4: + break + arr.append(i) + + return arr +``` + +In this code, we are iterating over the list `nums`. If the current element is equal to 2, the `continue` keyword will skip the rest of the code for that iteration and move on to the next iteration. If the current element is equal to 4, the `break` keyword will exit the for loop. + +To reset the elements of a hashmap to zero, we can use a for loop. + +```python +for funder: address in self.funders: + self.funder_to_amount_funded[funder] = 0 +``` + +The code above iterates through each address in the `self.funders` list and sets the corresponding value in the `self.funder_to_amount_funded` hashmap to 0. + +We can also iterate through the `self.funders` list using a for each loop. + +```python +for funder: address in self.funders: + self.funder_to_amount_funded[funder] = 0 +``` + +Using a for loop to reset the values in a hashmap will use a lot of gas. You should carefully consider the gas costs and trade-offs of different data structures when designing your smart contracts. diff --git a/courses/vyper-101/2-remix-coffee/3-doc-strings/+page.md b/courses/vyper-101/2-remix-coffee/3-doc-strings/+page.md new file mode 100644 index 000000000..eb414ef73 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/3-doc-strings/+page.md @@ -0,0 +1,31 @@ +We're going to look at what's called a docstring in Vyper. It's a method of adding comments to our code. + +```python +@external +def fund(): + pass +``` + +To create a docstring, we use triple quotes. + +```python +@external +def fund(): + """ + """ + pass +``` + +Similar to how the hashtag works in a regular comment, we can use triple quotes for multi-line comments. The docstring is then ignored by the compiler. + +```python +@external +def fund(): + """ + Allows users to send $ to this contract. + Have a minimum $ amount sent + """ + pass +``` + +Docstrings are a commonly used method of documenting functions in Vyper. diff --git a/courses/vyper-101/2-remix-coffee/30-tenderly_/+page.md b/courses/vyper-101/2-remix-coffee/30-tenderly_/+page.md new file mode 100644 index 000000000..b8c69ab05 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/30-tenderly_/+page.md @@ -0,0 +1,105 @@ +We are learning to build a smart contract that will allow users to send funds, with the goal of buying a coffee. We will also explore the concept of interfaces, how we can use interfaces to retrieve data and perform actions from our contract, and how to use a price feed. + +We will be building our contract in the Remix IDE. Remix IDE is a great tool for developing smart contracts and interacting with them on the blockchain. It provides a user-friendly interface for writing, compiling, deploying, and testing smart contracts. + +First, we will create a new Remix project and write the following code: + +```python +# Get funds from users +# Withdraw funds +# Set a minimum funding value in USD +pragma version ^0.4.0 +@license: MIT +@author: You! + +# We'll learn a new way to do interfaces later... +interface AggregatorV3Interface: + def decimals() -> uint8: view + def description() -> String[1000]: view + def version() -> uint256: view + def latestAnswer() -> int256: view + +minimum_usd: public uint256 +price_feed: public AggregatorV3Interface # 0x6944A41769357215DE4F4C0881f1f309a0DC325306 sepolia +owner: public address +funders: public DynArray[address, 1000] + +# funder -> how much they funded +funder_to_amount_funded: public HashMap[address, uint256] + +# Keep track of who sent us +# How much they sent us + +@deploy +def __init__(price_feed: address): + self.minimum_usd = as_wei_value(5, "ether") + self.price_feed = AggregatorV3Interface(price_feed) + self.owner = msg.sender + +@external +@payable +def fund(): + """Allows users to send $ to this contract + Have a minimum $ amount to send + How do we convert the ETH amount to dollars amount? + """ + + usd_value_of_eth: uint256 = self.get_eth_to_usd_rate(msg.value) + assert usd_value_of_eth >= self.minimum_usd, "You must spend more ETH!" + self.funders.append(msg.sender) + self.funder_to_amount_funded[msg.sender] += msg.value + +@external +def withdraw(): + """Take the money out of the contract, that people sent via the fund function. + How do we make sure only we can pull the money out? + """ + + assert msg.sender == self.owner, "Not the contract owner!" + send(self.owner, self.balance) + + # resetting + for funder: address in self.funders: + self.funder_to_amount_funded[funder] = 0 + self.funders = [] + +@internal +@view +def get_eth_to_usd_rate(eth_amount: uint256) -> uint256: + """Chris sent us 0.01 ETH for us to buy a coffee + Is that more or less than $5? + """ + + price: int256 = staticcall(self.price_feed, latestAnswer()) + eth_price: uint256 = (convert(price, uint256) * 10**18) / (10**10) + eth_amount_in_usd: uint256 = (eth_price * eth_amount) / (1 * 10**18) + return eth_amount_in_usd * 10**18 # 0's, 18 decimal places + +# 5 000000000000000000 +``` + +After we have written our code, we are ready to compile it. We can do so in Remix by clicking the "Compile" button and selecting the desired compiler version. + +Once we have compiled our code successfully, we can deploy it to the blockchain. We will first need to connect to the blockchain network. In this case, we are connecting to a fake virtual testnet that we have created on Tenderly. You can create a virtual testnet on Tenderly for free. We will be deploying our contract to this virtual testnet. Once you are connected to the network, you can deploy the contract by clicking the "Deploy" button in Remix. You will also need to provide the price feed address. The price feed address is the address of the Chainlink price feed. + +Once you have provided the price feed address and deployed your contract, you can now start testing. + +Let's switch to our account 2 and fund it with 1,000 ETH using the Tenderly faucet. + +```bash +1000 +``` + +Next, we will confirm the fund function, and we should see that account 2 has been funded. + +Now, let's switch back to our owner account (account 1). We can see that account 1 is connected. We can now call withdraw on the contract. + +Now we'll verify that only the contract owner can withdraw the funds. To do this, we will switch back to account 2 and try to withdraw the funds. We will confirm this action in MetaMask, and we expect it to fail. + +And it does! We can now switch back to account 1, the owner, and attempt to withdraw our funds. + +We will confirm this action in MetaMask, and we expect it to succeed. + +As we can see, it succeeds! So, our smart contract is doing everything that we wanted it to do. We now have a way to deploy our contract and update some starter variables in our contract constructor (aka the init function). We can fund the contract and make sure there is a minimum dollar amount that must be hit in order to even fund this contract. We're keeping track of how much and who is sending us money for us to go buy a coffee, and then only we can withdraw the money out, that is withdrawn with this send function from the contract. Then, we're going to reset those data structures that are keeping track of the funders. We have a way to actually get the price of this data, and we have a little external function as well, so that we can make sure everything is working. This is fantastic! Great job. + +Now, if you want to go the extra mile and actually test this on a true testnet, like Sepolia, you can, but again, like I said, getting testnet Sepolia can be very tricky, very difficult. So, we're going to, So, I don't recommend you do this, because you're going to run into a lot of issues. However, So I highly recommend we just stick with the Tenderly virtual testnet here, which again, shows us all these transactions very similar to what an explorer would do anyways. diff --git a/courses/vyper-101/2-remix-coffee/31-idk/+page.md b/courses/vyper-101/2-remix-coffee/31-idk/+page.md new file mode 100644 index 000000000..32769983b --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/31-idk/+page.md @@ -0,0 +1,27 @@ +## Immutables & Constants + +We'll learn about immutables and constants. Immutables are a way to declare values that we know won't change once a contract is deployed. They help improve the readability and gas efficiency of our code. + +Let's look at an example. Imagine we have an owner for our contract, and we know this owner will never change within the contract. We can use a constant to represent this value. + +First, let's declare the `owner` variable as a public address: + +```python +owner: public(address); +``` + +Now, we want to set this value in the constructor of the contract. Using the keyword `constant`, we need to initialize it with a value: + +```python +owner: public(constant(address)) = msg.sender +``` + +Notice that we can't assign a value to a constant within the constructor. Instead, we need to assign it at compile time. So, we can change the code to: + +```python +OWNER: public(constant(address)) = msg.sender +``` + +Here, we are assigning a value to a constant, which is the `msg.sender` address at compile time. + +We will cover constants in more detail in the following lessons. diff --git a/courses/vyper-101/2-remix-coffee/32-natspec/+page.md b/courses/vyper-101/2-remix-coffee/32-natspec/+page.md new file mode 100644 index 000000000..48f6baf7e --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/32-natspec/+page.md @@ -0,0 +1,44 @@ +## Natspec docstring + +Now that we've learned more about immutables and constants, we can improve our code to be more professional and gas-efficient. + +Before we even get to immutables and constants, we can improve our code by adding documentation at the top of the contract. + +This is a special type of comment called a docstring. We can do this by adding three quotes: + +```python +""" +""" +``` + +This is the same as adding a hashtag (#) to every single line, but this means that we don't have to do that. Everything within these three quotes will be a comment. + +We can use a special docstring format called NatSpec Metadata to add documentation to our code. It's a way to provide rich documentation for functions, return values, and more. + +To use NatSpec, we add an "at" symbol (@) followed by a tag. We'll add the following NatSpec tags at the top of our contract: + +```python +pragma version 0.4.0 +@license MIT +@title Buy Me A Coffee! +@author You! +@notice This contract is for creating a sample funding contract. +``` + +- **`pragma version`**: specifies the version of Vyper we are using. It isn't part of NatSpec, but it is convention to put it at the top of our contract. +- **`@license`**: describes the license for our code (in this case, MIT). +- **`@title`**: provides a short title for our contract. +- **`@author`**: identifies the author of the code. +- **`@notice`**: explains to an end user what the contract does. + +The compiler doesn't parse docstrings for internal functions. While we can add NatSpec tags in comments for internal functions, the compiler won't process or include them. + +We can add other NatSpec tags to document different parts of our contract, like functions and parameters. For example: + +```python +@dev Explains to a developer any extra details. +@param Documents a single parameter. +@return Documents one or all return value(s). +``` + +Let's add these NatSpec tags to our code so it looks a little more professional. diff --git a/courses/vyper-101/2-remix-coffee/33-update-immute/+page.md b/courses/vyper-101/2-remix-coffee/33-update-immute/+page.md new file mode 100644 index 000000000..efd279d26 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/33-update-immute/+page.md @@ -0,0 +1,43 @@ +## Updating our Immutables and Constants + +We'll finally now move on to the immutables and the constant variables here. So, down here we have these different values: minimum USD, price feed, owner, funders, etc. Now, as of today, these are all what's known as storage variables. + +Storage variables can be very gas-intensive to work with. Every time we read or write to a storage variable, it costs a lot of gas. We're going to learn about storage much later in the course. So, for now, just think of any time a variable isn't ever going to change, and you want to think about how making it a constant or immutable. + +Constant and immutable variables mean that they can never be changed. However, they are way more gas efficient for a whole bunch of reasons. And, we can even test this out. So, let's take this minimum USD. Now, minimum USD, we set to $5, and then we never change. So, what we could do is we could set this minimum USD to be a constant. So, we could say: + +```python +public constant uint256 +``` + +And, the convention for constant variables is to have constant variables be all upper case. So, we would do MINIMUM_USD, and then since it's constant, we don't set it in the constructor down here anymore. We would have to set it right up here. So, we would say MINIMUM_USD, public constant uint256 equals as wei value (5, "ether"), and then we would delete this line here. + +Now, constant variables are not considered storage variables, which is another reason why we have them all updated. Both constants and immutables are not storage variables. Again, we'll learn about storage much later. And, since it's not a storage variable, we scroll down. We no longer have to reference it with self.minimum USD. This self when we do this self.minimum USD, we're usually referring to storage variables or functions. Since MINIMUM_USD is a constant, we can actually just refer to it like this in all caps here. + +Is there any other place where we have minimum USD? Nope, that's it. Okay, great, we can just leave it like that. Okay, the next, the price feed. So, price feed we set one time, and we never update it again. + +So, can we set the price feed to be constant? Well, we could if we were always using the same price feed. So, I could do: + +```python +constant aggregator V interface +``` + +equals aggregator V interface, wrapped around this address here. However, this would assume that we're always going to use this address. Now, the price feed for the Ethereum USD price is going to be different on different chains. So, we don't actually want this to be constant. What we want it to be is immutable. So, let me undo everything I just did with command Z or control Z. So, instead of constant here, we could make this immutable. So, to set this as immutable, we would go: + +```python +immutable aggregator V interface +``` + +And then, now, same thing, price feed, we would do from lower case to all upper case. PRICE_FEED. + +Because, this is also not stored in storage. Now, what we can do though is right in our constructor, we can set price feed equals aggregator V interface, the price feed that we pass in. This way, whenever we deploy this, we will update the price feed with whatever address that gets passed in here. This way, when we deploy this contract on different chains, we can use different price feed addresses on those different chains and leave the contract as such. So, now I can scroll down. Let me look for any spots that we refer to the price feed. Same thing here, static call self.price feed . latest answer. We would just do the immutable PRICE_FEED instead. + +Let me remove these comments as well. Okay, great. So okay, so this one's a constant because it's always going to be five, no matter what. This one's immutable because it's going to be different depending on the chain that we're on. But, once we figure out what it's going to be, it's always going to be the same. + +The owner, hm, what should the owner be? Well, if we change accounts, the owner is probably going to be different depending on when we change it. So, the owner we probably want to be immutable as well. So, we say immutable, and then same thing. We're going to make this upper case, OWNER. We scroll down here. Now instead of self.owner we're just going to say OWNER. + +Now we can scroll down some more, let's look for any spot we refer to OWNER. Okay, great, right here. OWNER, self.owner is now just OWNER. Let's look for some more. Nope, looks pretty good. Okay, great. + +Do a little clean up here, these comments. Okay, so these are going to be constants and immutables. Okay, great. What about the funders dynamic array? Is this ever going to change? Well, yes. Every single time a funder funds, this is going to get updated. So, this we have to keep as a storage variable. Storage. + +What about funder to amount funded? Is this ever going to change? Yes. Same thing. Every single time a funder funds or we withdraw money, this is going to change. So, we're going to leave these as not constants and not immutables. diff --git a/courses/vyper-101/2-remix-coffee/34-magic/+page.md b/courses/vyper-101/2-remix-coffee/34-magic/+page.md new file mode 100644 index 000000000..100bba111 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/34-magic/+page.md @@ -0,0 +1,23 @@ +## Antipattern: Magic Numbers + +We're going to learn about a programming antipattern called "magic numbers." An antipattern is essentially a "bad coding habit" or an unrecommended way to write code. + +We can see a magic number in the following code block: + +```python +eth_amount_in_usd: uint256 = (eth_price * eth_amount) // (10 ** 18) +``` + +The magic number here is 18. + +We'll improve readability by using a variable to represent this value: + +```python +PRECISION: constant(uint256) = 1 * (10 ** 18) +``` + +Now, we can replace the magic number with the `PRECISION` variable, which makes the code clearer: + +```python +eth_amount_in_usd: uint256 = (eth_price * eth_amount) // PRECISION +``` diff --git a/courses/vyper-101/2-remix-coffee/35-gas/+page.md b/courses/vyper-101/2-remix-coffee/35-gas/+page.md new file mode 100644 index 000000000..79394875a --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/35-gas/+page.md @@ -0,0 +1,74 @@ +## Gas Comparison: Constants and Immutables vs Storage + +We are going to explore how expensive it is to deploy a contract, and call a fund function with constants and immutables. + +Let's make sure that our contract has compiled and is ready to be deployed. + +Next, we will navigate to the deploy tab in Remix. We are going to be using our injected Metamask. However, we're going to go back to our fake chain. + +Now, we're going to use this price feed address. We'll copy this, paste it in, and deploy our contract. + +After a few moments, we'll see that our contract has deployed and is available to interact with. + +Let's take a look at how much gas it cost to deploy our contract. In the transaction details, scroll down and you'll find the transaction cost. This is the entire cost of the whole transaction. Let's copy this number. + +Now we'll return to our contract code, and above our deploy function, we'll add this line: + +```python +# With constants: 262853 +``` + +We'll add a second line: + +```python +# With constants: 105332 +``` + +Next, we'll make some changes to our contract code to remove the `constant` keyword. This will require us to change our deploy function to the following code: + +```python +@deploy +def __init__(price_feed: address): + self.minimum_usd = as_wei_value(5, "ether") + PRICE_FEED = AggregatorV3Interface(price_feed) + OWNER = msg.sender +``` + +We've also changed our fund function to the following: + +```python +@external +@payable +def fund(): + """Allows users to send $ to this contract + Have a minimum $ amount to send + How do we convert the ETH amount to dollars amount?""" + + usd_value_of_eth: uint256 = self.get_eth_to_usd_rate(msg.value) + assert usd_value_of_eth >= self.minimum_usd, "You must spend more ETH!" + self.funders.append(msg.sender) + self.funder_to_amount_funded[msg.sender] += msg.value + +``` + +We'll compile these changes and deploy our contract again. + +Now, let's take a look at the gas costs and compare them. + +With constants, deploying our contract cost 262,853 gas. Let's put those numbers side by side to really see the difference. + +Without constants, it cost 282,553 gas to deploy our contract. + +That's 20,000 gas difference, or in real world money, it's 7 cents cheaper. + +Let's do the same for the fund function. We'll copy 0.002, and call the fund function. + +After confirmation, let's take a look at the gas cost. + +With constants, the transaction cost was 105,332 gas. We'll go back to our contract and put these costs side by side. + +Without constants, the transaction cost is 107,432 gas. + +Again, by making the minimum a constant we saved 2,000 gas. + +So we've seen that making small changes to our code can lead to significantly lower gas usage, which will save money for anyone interacting with your contracts. diff --git a/courses/vyper-101/2-remix-coffee/36-fallback/+page.md b/courses/vyper-101/2-remix-coffee/36-fallback/+page.md new file mode 100644 index 000000000..b8b226158 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/36-fallback/+page.md @@ -0,0 +1,64 @@ +We're learning about the default function in Vyper. + +Vyper is a smart contract language similar to Solidity, but it has some key differences. + +We're building a smart contract that allows people to send money to this contract, which we'll call "Buy Me A Coffee". + +We'll use Vyper to make sure that when somebody sends money to this contract, we can keep track of them as a funder. We're going to do this through a function called "fund". + +The fund function is a key part of our "Buy Me A Coffee" smart contract. It helps us keep track of who's donated to our cause. + +The fund function allows people to send us money in the form of Ether. This contract uses a price feed to convert the Ether amount into US dollars. + +We've already made this contract work, and we've even added functionality so we can withdraw money from this contract. + +However, right now, this contract is dependent on the fund function to keep track of the funders. + +We can make our smart contract more robust by allowing somebody to send us money without even calling the fund function. We can do this using the default function. + +Here is an example of the code we've written so far. + +```python +@external +@payable +def fund(): + """Allows users to send $ to this contract + Have a minimum $ amount to send + """ + usd_value_of_eth: uint256 = self.get_eth_to_usd_rate(msg.value) + assert usd_value_of_eth >= MINIMUM_USD, "You must spend more ETH!!" + self.funders.append(msg.sender) + self.funder_to_amount_funded[msg.sender] += msg.value +``` + +We're going to add a new function above this one called "def _default_". The default function is executed when somebody sends money to our contract without calling a specific function. + +Here is the code: + +```python +@external +@payable +def _default_(): + pass +``` + +The default function should accept no parameters. We use the "pass" keyword to tell Vyper to do nothing. We then recompile the code and deploy this contract again. + +In our Remix VM, we'll now try to send 1 Ether to this contract, without calling the fund function, but the transaction will revert. Our "funders" array still shows zero funders. + +Now, we'll try to call the fund function from our default function. + +Here is the updated code: + +```python +@external +@payable +def _default_(): + self.fund() +``` + +We'll recompile the code and deploy this contract. + +Now when we send 1 Ether to our "Buy Me A Coffee" contract, it should still update the balance of the contract, but this time, it should also add the funder to the "funders" array. + +This default function is a very powerful tool that can be used to add a layer of functionality to our Vyper smart contracts. diff --git a/courses/vyper-101/2-remix-coffee/37-better-way-to-send/+page.md b/courses/vyper-101/2-remix-coffee/37-better-way-to-send/+page.md new file mode 100644 index 000000000..23535b887 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/37-better-way-to-send/+page.md @@ -0,0 +1,37 @@ +## Better way to send ETH + +We've learned how to send ETH to another account using the `send` function. However, there's a better way to do it that's considered more secure. + +The `send` function is a lower-level function that can be unreliable. There are potential issues with how much gas the EVM uses for different opcodes (for example, logging), and this could cause transactions to fail. + +Instead, we're going to use the `raw_call` function. This is a built-in function in Vyper that allows us to call any function in the world, even if the target contract doesn't have that function. + +Let's look at how to use `raw_call` to send ETH. We'll use the `withdraw` function as an example. + +```vyper +@external +def withdraw(): + """Take the money out of the contract, that people sent via the fund function. + + How do we make sure we can pull the money out? + ..... + """ + assert msg.sender == OWNER, "Not the contract owner!" + #send(OWNER, self.balance) + raw_call(OWNER, b"", value=self.balance) + revert_on_failure=True + #resetting + for funder, address in self.funders: + self.funder_to_amount_funded[funder] = 0 + self.funders = [] +``` + +**Explanation:** + +- `raw_call(OWNER, b"", value=self.balance)` calls the raw call function. +- The first argument `OWNER` is the address we're sending ETH to. +- The second argument `b""` is an empty byte string that represents the data we're sending. +- The third argument `value=self.balance` is the amount of ETH we're sending, which is the contract's current balance. +- `revert_on_failure=True` tells Vyper to revert the transaction if `raw_call` fails. + +We can learn more about `raw_call` and its advanced features in later lessons. For now, understand that `raw_call` is considered a safer way to send ETH than using the `send` function. diff --git a/courses/vyper-101/2-remix-coffee/38-recap/+page.md b/courses/vyper-101/2-remix-coffee/38-recap/+page.md new file mode 100644 index 000000000..3980dc237 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/38-recap/+page.md @@ -0,0 +1,70 @@ +In this lesson we will continue working with our _buy-me-a-coffee_ smart contract. We will introduce a few more key fundamentals to help us understand the basics of smart contract development in Vyper. We'll then move on to more complex aspects later in the course. + +Here is the code we are working with so far: + +```python +#pragma version 0.4.0 +@license MIT +@title Buy Me A Coffee! +@author You! +@notice This contract is for creating a sample funding contract + +# We'll learn a new way to do interfaces later... +interface AggregatorV3Interface: + def decimals() -> uint8: view + def description() -> String[1000]: view + def version() -> uint256: view + def latestAnswer() -> int256: view + +# Constants & Immutables +MINIMUM_USD: public(constant(uint256)) = as_wei(value=5, "ether") +PRICE_FEED: public(immutable(AggregatorV3Interface)) = 0x6944AA1769357215DE4FAC081f113094dC325306 sep0lia # ABI Address +OWNER: public(immutable(address)) +PRECISION: constant(uint256) = 1 * (10 ** 18) + +# Storage +funders: public(DynArray[address, 1000]) +funder_to_amount_funded: public(HashMap[address, uint256]) + +# With constants: 262, 853 +@deploy +def __init__(price_feed: address): + PRICE_FEED = AggregatorV3Interface(price_feed) + OWNER = msg.sender + +@external +@payable +def fund(): + """Allows users to send $ to this contract + Have a minimum $ amount to send + """ + usd_value_of_eth: uint256 = self.get_eth_to_usd_rate(msg.value) + assert usd_value_of_eth >= MINIMUM_USD, "You must spend more ETH!!" + self.funders.append(msg.sender) + self.funder_to_amount_funded[msg.sender] += msg.value +``` + +In this code, we have: + +- A _decimals_ function which returns a uint8 value. +- A _description_ function which returns a String value. +- A _version_ function which returns a uint256 value. +- A _latestAnswer_ function which returns an int256 value. +- We have declared the constants _MINIMUM_USD_, _PRICE_FEED_, _OWNER_, and _PRECISION_. +- We have declared the storage variables _funders_ and _funder_to_amount_funded_. +- We have declared the initializer function **_init_**. +- We have declared the _fund_ function which is payable and allows users to send ETH to the contract. + +The topics we've covered so far are: + +- Interfaces +- Constants +- Immutables +- Storage +- Initializers +- Payable Decorators +- Function Documentation + +We have also seen how to use `msg.sender`, `msg.value`, and `self` in our code. + +We will cover more advanced topics later in the course. diff --git a/courses/vyper-101/2-remix-coffee/4-sending-eth/+page.md b/courses/vyper-101/2-remix-coffee/4-sending-eth/+page.md new file mode 100644 index 000000000..2bc1a107d --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/4-sending-eth/+page.md @@ -0,0 +1,9 @@ +The first question that will come up is how do we send ETH to this contract? The answer to this is that in every transaction you send, there's a value field that allows you to send a certain amount of ETH. You can even see this in Metamask, where you can send a specific amount of ETH to an address. This populates the value field in the transactions. + +Not all functions can read from this value field. To tell a function that it can read from the value field, we use the `@payable` keyword. This tells Vyper that the function can work with the `msg.value` variable. Similar to wallets, smart contracts can hold funds. To ensure someone calling the `fund` function sends at least one ETH, we can use the `assert` function and say: + +```python +assert msg.value == 1000000000000000000 +``` + +We'll understand what `wei` is later on. We also have a variable called `msg.sender`, which is the sender of the current message. We will be using this variable in our code. diff --git a/courses/vyper-101/2-remix-coffee/5-gwei-wei-eth/+page.md b/courses/vyper-101/2-remix-coffee/5-gwei-wei-eth/+page.md new file mode 100644 index 000000000..064b9caec --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/5-gwei-wei-eth/+page.md @@ -0,0 +1,69 @@ +## Wei, Gwei, and ETH in Vyper + +We're going to learn about how to convert between Wei and ETH. + +Here's a code snippet: + +```python +# pragma version 0.4.0 +# @license: MIT +# @author: You! + +@external +@payable +def fund(): + """Allows users to send $ to this contract. + Have a minimum $ amount send + + 1. How do we send ETH to this contract? + """ + assert msg.value == 1000000000000000000 +``` + +This is a pretty confusing concept, but it will make more sense as we explore it. + +First, if you go to the site eth-converter.com, you can see there are some different units for Ethereum. + +If you type in one ETH, you'll see how much one ETH is in Wei. Wei is the smallest divisible unit of account in the Ethereum ecosystem. So, one ETH, as of recording, is worth approximately $3241.63, and has 18 zeros in its Wei equivalent. + +Let's look at how we can make our code more readable. + +Instead of typing in that long Wei number every time, we can use a built-in Vyper function called as_wei_value. Here's an example: + +```python +assert msg.value == as_wei_value(1, "ether") +``` + +We're saying, hey, let's get the wei value of one ETH and return it as Wei. + +Another way to see this is with a calculation: + +```python +assert msg.value == (1 * 10 ** 18) +``` + +This double times, or power of 18, is the same as the Wei value of one ETH. + +We can add a message in our code in case someone doesn't send enough ETH, like this: + +```python +assert msg.value == as_wei_value(1, "ether"), "You must spend more ETH!" +``` + +This helpful message will let users know if they don't send enough ETH. Without this message, if someone sends less than one ETH, the transaction will break. + +Let's talk about the different ways we can use the 'equals' sign in Vyper. The `==` symbol is what we use to check if two things are equivalent. + +Here's an example: + +```python +my_num: uint256 = 7 +``` + +This is setting the value of `my_num` to 7. It's not checking if the value of `my_num` is equal to 7. So, this line is not equivalent to: + +```python +assert msg.value == as_wei_value(1, "ether"), "You must spend more ETH!" +``` + +This is a subtle but important distinction for new Vyper developers. Don't worry too much about this right now, as you practice more it'll start to make more sense. diff --git a/courses/vyper-101/2-remix-coffee/6-send-tx/+page.md b/courses/vyper-101/2-remix-coffee/6-send-tx/+page.md new file mode 100644 index 000000000..6ac1dc604 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/6-send-tx/+page.md @@ -0,0 +1,63 @@ +## Sending a Transaction with ETH in Remix + +We're going to compile and deploy a contract. We will then call a function on this contract which requires the transaction to pay ETH. We're going to do this using the Remix VM. + +First, we'll compile our contract. + +```python +pragma version 0.4.0 +# @license: MIT +# @author: You! + +external +@payable +def fund(): + """Allows users to send $ to this contract. + Have a minimum $ amount send + + 1. How do we send ETH to this contract? + """ + assert msg.value == as_wei(1, "ether") "You must spend more ETH!" +external +def withdraw(): + pass +``` + +Now, we'll deploy this contract. + +```python +pragma version 0.4.0 +# @license: MIT +# @author: You! + +external +@payable +def fund(): + """Allows users to send $ to this contract. + Have a minimum $ amount send + + 1. How do we send ETH to this contract? + """ + assert msg.value == as_wei(1, "ether") "You must spend more ETH!" +external +def withdraw(): + pass +``` + +We'll see a red button and an orange button. + +The orange button `withdraw` is not payable and doesn't do anything. But `fund` is red and is payable because it has the `@payable` keyword in the code. + +We can send a transaction to `fund` and the Remix VM will know to include some ETH. The default account in Remix has 99.9999999999999999 ETH in it. + +We can send a transaction to `fund` by placing a value in the `VALUE` section and calling the function. + +If we call the function with a value of zero, the transaction will be reverted, because our contract requires a value of at least one ETH. We'll see a message: "You must spend more ETH!". + +Transactions can be reverted because the contract has specific requirements that were not met, such as the requirement to send at least one ETH in this case. + +If we call the function with a value of one ETH, the transaction will go through. + +We can set the value to two ETH and try to call the function again, but the transaction will be reverted, showing the same error message: "You must spend more ETH!". + +We'll go into reverts in more depth in a later lesson. diff --git a/courses/vyper-101/2-remix-coffee/7-reverts/+page.md b/courses/vyper-101/2-remix-coffee/7-reverts/+page.md new file mode 100644 index 000000000..695c83be9 --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/7-reverts/+page.md @@ -0,0 +1,50 @@ +## Reverts in Solidity + +Reverts are an incredibly important part of the Solidity language. They allow us to undo any actions that have been done and send the remaining gas back. Let's explore how they work! + +We can see a revert in action with this code: + +```python +// SPDX-License-Identifier: MIT +pragma version 0.4.0 + +contract buy_me_a_coffee { + + my_num: public uint256; + + @external + @payable + def fund(): + """ + Allows users to send $ to this contract. + Have a minimum $ amount send + 1. How do we send ETH to this contract? + """ + self.my_num = self.my_num + 2; + assert msg.value == as_wei_value(1, "ether"), "You must spend more ETH!" + + @external + def withdraw(): + pass +} +``` + +Let's set our state variable to `2` and then call our `fund` function. We'll deploy the contract and then call `my_num`. We'll see that our `my_num` has been set to `2` as expected. This is because the `assert` condition was met. + +Now, let's change the `value` to `0` and then call `fund`. Our terminal output will show us that the transaction failed and has been reverted. When we call `my_num` again, we'll see that it is still set to `2` and the value has not changed. + +This is because the `assert` condition was not met. The transaction was reversed and the state was rolled back to how it was before. + +Reverts are a great way to ensure that transactions are only executed when certain conditions are met. They are particularly useful when working with real networks, where transactions can be expensive. + +We will also want to check that our transaction did not revert. We can do this by running the following terminal command: + +```bash +truffle test +``` + +If the transaction reverted, the terminal output will tell us why. This can be useful for debugging and understanding why our transaction is failing. + +In the blockchain world, if you send a transaction and it reverts, you've updated nothing. You will have spent money but the transaction will not have updated the state. We want to do everything in our power to not send transactions that are going to revert. + +Metamask will also give us a popup saying that the transaction will likely revert. If this happens, we should confirm that we want to send the transaction. diff --git a/courses/vyper-101/2-remix-coffee/8-chainlink/+page.md b/courses/vyper-101/2-remix-coffee/8-chainlink/+page.md new file mode 100644 index 000000000..6d0c4d95f --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/8-chainlink/+page.md @@ -0,0 +1,126 @@ +## Oracles and Chainlink + +We are going to learn about oracles and Chainlink and how to bring real-world data into our smart contracts. + +Blockchains are deterministic systems, meaning they can't interact with real-world data and events on their own. They can't know what the value of ETH is, they can't know if it's sunny outside, or what the temperature is. They also can't do any external computation. + +This is known as the smart contract connectivity problem or the oracle problem. This is bad news because we want our smart contracts to replace traditional agreements. And traditional agreements need data, and they need to interact with the real world. + +This is where Chainlink and blockchain oracles come into play. A blockchain oracle is any device that interacts with the off-chain world to provide external data or computation to smart contracts. + +However, the whole story doesn't end there. If we use a centralized oracle, we are reintroducing a point of failure. We've done all this work to make our logic layer decentralized, but if we get our data through a centralized node or through a centralized API, or we decide we want to make the API call ourselves, we are reintroducing these trust assumptions that we've worked so hard to get rid of. We're essentially ruining the entire purpose of building a smart contract. So we don't want to get our data, or do external computation, through centralized nodes. Those are bad news. + +Chainlink is the solution here. Chainlink is a decentralized oracle network for bringing data and external computation into our smart contracts. + +As we mentioned before, this gives rise to these hybrid smart contracts, which combine on-chain and off-chain to make incredibly feature-rich, powerful applications. + +Chainlink is a modular decentralized oracle network that can be customized to deliver any data or do any external computation that you like. + +So, for example, a lot of people might just say, "Oh, well I can just make an API call to, you know, a data provider to get the pricing information on my smart contract." However, blockchain nodes can't actually make these API calls. You can't make like an HTTP GET, if you're familiar with that, HTTP GET. You can't make an HTTP GET call because the different Ethereum nodes, the different blockchain nodes, would not be able to reach consensus about what the result of that API call even is. Additionally, this is a huge centralization vector and kind of defeats the purpose of smart contracts in the first place. + +So instead, we need a decentralized network of nodes to get us this data for us. + +Now, Chainlink networks can be completely customized to bring any data or any external computation that you want. However, doing the customization can be a little bit extra work. There are a ton of Chainlink features that come out of the box, completely decentralized, ready to plug-and-play into your smart contract applications. + +What are those features? The first one is going to be Chainlink Data Feeds. And, that's the one we're actually going to be using for our application here. Chainlink Data Feeds currently, at the time of recording, are powering over $50 billion in the DeFi world. + +The way they work is a network of Chainlink nodes gets data from different exchanges and data providers and brings that data through a network of decentralized Chainlink nodes. The Chainlink nodes use a median to figure out what the actual price of the asset is, and then deliver that in a single transaction to what's called a reference contract, a price feed contract, or a data contract, on-chain, that other smart contracts can use. And then, those smart contracts use that pricing information to power their DeFi application. + +You can see an example at data.chain.link. And you can change networks, you can change price feeds, you can change a whole bunch of information to see some of those popular price feeds. + +Let's look at ETHUSD, for example. + +On ETHUSD, we can see this whole network of independent Chainlink node operators that are each getting different answers for the price of ETHUSD. They're getting aggregated by the network and then delivered on-chain. We can see how often they're updated. These ones are updated for a 0.5 deviation threshold, or a few hour heartbeat, whichever one hits first. We can see when the last update was. We can see the number of oracle responses, etc. + +We can see the contract address directly on-chain. + +We can even look at the contract on Etherscan. + +We can see some of the history. We can see all the responses of the different oracles. + +And then, at the bottom, we can see the different users and sponsors keeping this network up. + +Similar to transaction gas, whenever a node operator delivers data to a smart contract, the Chainlink node operators are paid a little bit of oracle gas in the Chainlink token. Right now, these users of the protocol are sponsoring keeping these feeds up, and are paying the oracle gas associated with delivering this data on-chain. + +We can take a look at an example over at docs.chain.link. The solidity docs have the solidity example directly in them, but if you scroll down you can also see some Vyper examples or, if you click this little README, it will bring you to the Vyper edition of the Chainlink starter kit, which will show you how to make a price feeds using Vyper. + +We are going to walk you through a little demo. This is in solidity, so you can ignore the exact code, but the process will be essentially the same. + +Now, the docs are probably going to look very different by the time you actually start looking at them because they change the docs pretty frequently. So an easy way to get started here is maybe go to docs.chain.link, and then over either on developer hub, or overview, go to data feeds. And this is where you can see most of what you need in the getting started section of the documentation. + +We can see an example of an art contract that uses and reads from one of these Chainlink price feeds. + +**Code:** + +```python +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; + +contract PriceConsumerV3 { + + AggregatorV3Interface internal priceFeed; + + /** + * Network: Kovan + * Aggregator: ETH/USD + * Address: 0x9326BFA02AD2366d30baCb125266AF641031331 + */ + constructor() { + priceFeed = AggregatorV3Interface(0x9326BFA02AD2366d30baCb125266AF641031331); + } + + /** + * Returns the latest price + */ + function getLatestPrice() public view returns (int) { + ( + uint80 roundID, + int price, + uint startedAt, + uint timeStamp, + uint80 answeredInRound + ) = priceFeed.latestRoundData(); + return price; + } +} +``` + +We can even open up this up in Remix and work with it in Remix. It looks like this example is running from a price feed on Kovan. The reason we are actually going to use a testnet to see this work is that there's a set of Chainlink nodes monitoring the testnet to show you exactly how this works out. + +Once we get deeper into the course, we'll show you how to actually run tests and work with Chainlink nodes without actually being on a testnet, which will make your development much faster. But I highly recommend walking through this section along with me so that you can see first-hand how this actually works. + +So let's go ahead to faucets.chain.link /kovan We're going to switch to the Kovan network. + +We're going to get some Kovan ETH, but remember, look at the network flag, and use whatever network is in the documentation. + +**Terminal:** + +```bash +faucet.chain.link/kovan +``` + +We're going to turn off test LINK; we'll just stay with ETH. I'm not a robot. + +And then send request. Once our Kovan ETH has reached our wallet, we can go ahead and close. And, we can take a look in our wallet and see that we do indeed have 0.1 ETH on Kovan. + +Now, let's go back to our Remix. We'll compile this contract. + +We'll go and deploy this on injected Web3. + +And again, the reason we are going to use injected Web3, instead of Javascript VM, is that there's no network of Chainlink nodes watching our little fake Javascript VM. There are a network of Chainlink nodes watching the testnet. + +So we'll scroll down. We'll switch contract to the PriceConsumerV3. + +We'll hit deploy. MetaMask will pop up, and after a brief delay, we can see our PriceFeedConsumer down here, and we can hit get the latest price, which shows us the latest price of Ethereum in terms of USD. You may be wondering why the number looks so weird. That seems like a really large number for the price of Ethereum in terms of USD, and this is because decimals don't actually work so well in Solidity. And we'll get to that in a little bit. + +There's a decimals flag associated with this PriceFeed address that tells us how many decimals to include with this price. It's also in the documentation. However, I know that this one has eight decimals. So, this is saying the value of Ethereum right now is 3,262. + +It may, of course, be different when you go ahead and try this. + +Now there's a number of things that happen in this contract that I'll explain in our fund me example. But if you want to take a look now and see if you can figure out what's going on, I recommend you do so. Price feeds are one of the most powerful out-of-the-box decentralized features you can use in your smart contract to level them up, especially for decentralized finance. + +If you're looking for different addresses of different price feeds, you can check the contract addresses section of the documentation, choose the network that you want, and then scroll down and then look some of the different addresses of the different price feeds. For example, this address will give you the price of 1inch token in terms of ETH. + +This address will give you the price of the Apple stock in terms of USD, and so on and so forth. You can see a lot of the different Chainlink functionalities and tools, and services in the DevHub, or, aka, the docs of Chainlink. One of the newest and coolest ones that we have a section on in this little course is Chainlink CCIP or cross-chain interoperability protocol, which is a way to do token transfers across different blockchains, which, as of today, is a big issue, but you'll learn about that much later. diff --git a/courses/vyper-101/2-remix-coffee/9-mid-recap/+page.md b/courses/vyper-101/2-remix-coffee/9-mid-recap/+page.md new file mode 100644 index 000000000..69fdf462c --- /dev/null +++ b/courses/vyper-101/2-remix-coffee/9-mid-recap/+page.md @@ -0,0 +1,20 @@ +## Recap + +We haven't written much code yet, but we've covered some important topics. + +The `payable` decorator is used to mark a function that can receive Ethereum. We've seen this in the `fund()` function: + +```python +@payable +def fund(): +``` + +We've also looked at the `assert` keyword. This allows us to check conditions and if the condition is false, the transaction will revert. In our code, we've checked that the `msg.value` sent to the contract is at least 1 ether. + +```python +assert msg.value >= wei_value(1, "ether"), "You must spend more ETH!" +``` + +If the transaction reverts, all changes made during the transaction are undone, and the user receives a refund of gas. + +The next topic we'll discuss is how to get data from the real world into the blockchain. We learned that this is not as simple as making an API call, because we need to avoid relying on centralized entities. We'll explore decentralized oracle networks like Chainlink Price Feeds to address this challenge. diff --git a/courses/vyper-101/3-ai/1-intro/+page.md b/courses/vyper-101/3-ai/1-intro/+page.md new file mode 100644 index 000000000..2383ac530 --- /dev/null +++ b/courses/vyper-101/3-ai/1-intro/+page.md @@ -0,0 +1,5 @@ +## AI Prompting + +Why learn this when there is AI? + +Now a lot of people get discouraged by AI thinking ah, like why bother learning anything if AI already knows a lot of the stuff. Well, in order to think critically about problems and how to solve them, you always need the prerequisite knowledge before you can even start coming up with solutions to fix a problem. So yes, AI knows a lot of things. But, in order for you to solve problems effectively, you need that knowledge as well. So, you need to take the knowledge from AI. Thank you AI for giving us all this knowledge as well. So that you can think critically and come up with better solutions than AI could ever do. Because as of today, that's where humans shine. We are phenomenally better at coming up with solutions. And AI is a tool to help us have even better solutions. But in order for us to come up with the best solutions, we need the knowledge first. \ No newline at end of file diff --git a/courses/vyper-101/3-ai/2-vid/+page.md b/courses/vyper-101/3-ai/2-vid/+page.md new file mode 100644 index 000000000..13eefe9c3 --- /dev/null +++ b/courses/vyper-101/3-ai/2-vid/+page.md @@ -0,0 +1,33 @@ +## AI Prompting Introduction + +In the age of AI, debugging and getting into software engineering has never been easier. Here are the exact six steps we need to take to unblock ourselves from any software engineering error we'll ever get: + +1. **Tinker** - Try to pinpoint your error. We can usually use an AI buddy to help us out here. Pinpointing your error will potentially solve your problem before we even go into an AI and allow us to craft a better question to an AI. + * **Example:** Try adding a debug print to see where the issue is. + * **Example:** Comment out sections of code to see if it fixes the issue. + +2. **Ask your AI** - We can use ChatGPT, Phind, Bing's AI, or if we want to get wrong answers, Google's Bard. There are six principles to prompt engineering so that we can get the best out of our AI. + * **Example:** Ask "I'm getting this error [Error message]: [Code]. What's the issue?" + * **Principle 1: Write clear and specific instructions** - Give as much context as possible. + * **Principle 2: Give as much context as possible** - Use delimiters to clearly indicate distinct parts of the input. + * **Principle 3: Use delimiters to clearly indicate distinct parts of the input** - Especially look out for something called hallucinations. + * **Principle 4: Look out for hallucinations** - Hallucinations are when your AI gives you an output that it thinks is right, but it's completely wrong. For example, if we write about writing Solidity invariant tests in Foundry, ChatGPT disgraces us by saying we have to npm install it from OpenZeppelin. These can be tough to spot, but once we try it out we'll see it doesn't work. + * **Principle 5: Understand limitations** - Finally, we want to understand the limitations of the AI we're working with. + * **Principle 6: Iterate constantly** - Large language models are trained on human conversations, so we can interact with them as if we're having a conversation, but it's important to know the limitations of these AIs. As most AIs have a limit on how many tokens or words they can keep in context at one time. AI is trained off of human language, so if we're good at asking other humans questions, we'll probably be good at asking robots questions too. Asking questions is a skill, so keep practicing. I've got a link in the description to learn.deeplearning.ai, which is a free course to help software engineers be better prompt engineers. + +3. **Read docs** - When the AIs can't help us, we'll have to go back to the old standbys, actually doing work ourselves. And one of the first pieces of work is reading the documentation. We should have done this already. However, we can still use ChatGPT because a strategy that we constantly use is, I'll copy paste sections of the documentation, add to ChatGPT's context and say, "Something like the above are the docs for tool X, based on those docs, how do I do Y?" + * **Example:** "This is a section of Solidity docs: [Copy and paste relevant docs here] This is my solidity code: [Copy and paste solidity code here] Based on the docs, how do I do [Specific goal in code]?" + +4. **Web Search** - Google might be crying because ChatGPT is eating its lunch, but Google still has what AI doesn't have - the entire internet. Previously, anytime I ran into an issue, I prayed someone else had run into it before I made a post on it so I could Google search that exact issue. There's a new tool called Phind that combines web search with AI as it does a web search and it crawls through all the data of the sites, reads them all, and then gives us an answer based off of what it reads. + * **Example:** "How do I debug Solidity code?" + +5. **Ask in a forum** - Sometimes, the information just isn't out there and we need to ask human beings. We always want to ask our questions on a web-indexed forum like Stack Exchange, Stack Overflow, Quora, or Reddit. This way web crawlers and more likely AIs can scrape the data from these sites and learn from us. That way the next time we have this question, we can get our answers quickly. Asking on Discord and Twitter is a shit because your knowledge will get lost to the unsearchable hole conversations that Discord is and web crawlers don't index them. + * **Example:** "I'm getting this error in Solidity: [Code]. How do I fix it?" + * **Example:** "I'm trying to deploy my smart contract, but I'm getting this error: [Code]. How do I fix it?" + * **Example:** "I'm trying to compile my Solidity code, but I'm getting this error: [Code]. How do I fix it?" + +6. **Ask support or GitHub** - The super secret alpha is to ask a question on Stack Exchange, and then post your Stack Exchange link to Discord. We should 100% always ask our questions and format them with markdown. And if we're not sure how to do markdown, we can ask ChatGPT to help us format our questions in markdown. + * **Example:** "I'm getting this error in Solidity: [Code]. How do I fix it?" + +7. **Iterate** - Do all these same steps over again. And, as always, keep hopping through the code and until next time, stay ribbeting, my fellow blockchainers. + diff --git a/courses/vyper-101/3-ai/3-triage/+page.md b/courses/vyper-101/3-ai/3-triage/+page.md new file mode 100644 index 000000000..0395f3aa6 --- /dev/null +++ b/courses/vyper-101/3-ai/3-triage/+page.md @@ -0,0 +1,39 @@ +## 7 Triage Steps for This Course (and Real Life!) + +We're going to learn a bit more about different tools and techniques we can use when we get stuck. We'll cover tools like ChatGPT and forums to ask questions, as well as Google Search for finding information, and Stack Exchange or Peeranha for posting questions. + +### 7 Triage Steps + +1. **Limit self-triage to 15-20 minutes.** For instance, if we're trying to compile our `PriceConverter.sol` file and we've forgotten to add an address, we'll go ahead and compile, but then we'll get this error: + +```python +TypeError: Exactly one argument expected for explicit type conversion. + --> PriceConverter.sol:15:43: +15 | AggregatorV3Interface priceFeed = AggregatorV3Interface( + | ^ (Relevant source part starts here and spans across multiple lines.) +``` + +This error should be pretty obvious: "exactly one argument expected", and we just need to remove that argument. But maybe you're having a really hard time and can't seem to figure it out. Limit yourself to 15-20 minutes, so you don't get stuck for so long that you can't figure anything out. Something like this would be very easy for us to compile, copy the error, go to ChatGPT, and say something like, "Hey, I'm getting an error compiling my solidity. Here is the error: + +```python +TypeError: Exactly one argument expected for explicit type conversion. + --> PriceConverter.sol:15:43: +15 | AggregatorV3Interface priceFeed = AggregatorV3Interface( + | ^ (Relevant source part starts here and spans across multiple lines.) +``` + +" and then it'll probably give us the answer. And, sure enough, the error message suggests that you need to provide an address when using explicit type conversion to create an `AggregatorV3Interface` instance. You're missing the address of the deployed Chainlink Price Feed contract. To fix the error, provide the contract address as an argument when creating the instance. + +2. **Don't be afraid to ask AI, but don't skip learning.** The purpose of this course is for you to be able to learn everything here. AI is going to get things wrong, and it's only the people who actually have learned what's going on under the hood who are going to be able to call out when AI gets something wrong. When AI gets something wrong, it does something called "hallucinate", and we talked a bit about that in that AI video we just watched. So, hallucinations are when the AI makes something up that it thinks is right, but it's completely wrong, and that's something we absolutely need to watch out for when we're coding and when we're learning. + +3. **Use the forums!** Web3Education.dev will have a place for you to ask questions, but in the meantime, definitely be sure to use the Foundry full course discussions. Use this to your advantage. And, use this to help other people out who have questions. This is a community project, we're all going to be helping each other out learning. + +4. **Google the exact error.** If AI doesn't know, and the forums aren't know, Google the exact error. Maybe somebody else has come across this error previously, asked a question online, and you can just Google it. + +5. **Post in Stack Exchange or Peeranha.** And, like we said in that AI video, you want to use Markdown and format your questions as best as possible. So, if you're unfamiliar with how to use Markdown, if you're unfamiliar with how to format your questions, again, please be sure to ask ChatGPT or some other AI how to best format your questions. You can even simply say, "Ask ChatGPT, could you give me an example of a well-formatted Stack Overflow question with code?" And it'll actually give us a really good formatted question. So it gives us the question, it formats the code for us, it's using this with the three backticks that I showed you in order to get this code block here. It's using three backticks and it's being very verbose in giving all the details of the question. + +6. **Posting an issue on github/git.** Obviously when you're out in the wild, and you're actually building things yourself, obviously the education site and the discussions channel of the GitHub repo, the people in these aren't going to have context for the new creative thing that you're working on. So I want you to get used to doing that methodology that we're teaching here. Right? After asking your AI buddy, if your AI buddy doesn't know, Googling, asking questions on Stack Exchange, Ethereum, asking questions on Peeranha, Peeranha is arguably better because you're asking your questions in a decentralized location as opposed to a centralized server. But I definitely want you to practice asking questions, and go to Peeranha, go to Stack Exchange. Make accounts for these so that when you do run into an issue, because you will, you'll know where to post them. I highly encourage you to pause and post a question on one of these forums right now, just so that you get used to it. Just so that you get out of your shell, and you're going to suck at asking questions in the beginning, and that's okay. You will get better at asking questions as you continue with this course. Asking good questions is a skill in itself, and if you learn how to ask very good questions, that's the secret sauce to being amazing at AI prompting as well. + +7. **Post in stack exchange or github/git.** Now, the final step in all of this is actually going to be posting an issue on GitHub/git. A surprisingly incredibly important part of being a software engineer and being a developer in Web3 is interacting with the community. The vast majority of these tools that we're working with are going to be something called open source, meaning that the code associated with them is actually available for anybody to view. This tool, Foundry that we're going to be working with is one such example. And, if you see, it has this issues tab where there's just a ton of issues people are posting while using this tool. This is how code is able to be improved and move forward. All this code is public for people to use and work with. And, if there's a question that isn't answered, you can post here and tell the developers of this tool, "Hey, I'm running into an issue, maybe we should make a project improvement." I would say be very respectful with the way that you make issues. You definitely want to do some searching on issues to make sure that nobody else has asked the question that you're working with, and be sure to use these other resources first, like Peeranha, like Stack Exchange. But in the future, once you leave this course, it's going to be incredibly important for you to participate in the ecosystem by making issues, by making pull requests, and actually improving some of the tools that we're going to be working with yourself. Even Ethereum has a GitHub, and, if there's any improvements that you want to make, you can add an EIP to actually improve Ethereum. We'll talk about EIPs some more later. You can improve some of the clients that Ethereum runs, so interacting with each other is incredibly important. And, in fact, if you don't have a GitHub, we are going to get you set up with GitHub right now, because GitHub is a platform that most developers use to share code and write code and interact with code in between each other. So, if you don't have a GitHub, let's sign up right now. And, we're going to use the GitHub to create our own portfolios of all the different cool coding products that we've created. This way, when you go to apply for a job, you can say, "Hey, go look at my GitHub, I've got all these really cool projects." + +We're going to walk through formatting a question on GitHub, as well. I know I'm belaboring the point here, but I really really want to stress the importance of formatting these questions really well. So, we're actually going to show a clip from my past video, so we're going to be using the full blockchain facility course JS. But, again, for this video, just be sure to use the GitHub repo associated with this course, and not that other old GitHub repo. diff --git a/courses/vyper-101/3-ai/4-formatting-qs/+page.md b/courses/vyper-101/3-ai/4-formatting-qs/+page.md new file mode 100644 index 000000000..486d5ae9e --- /dev/null +++ b/courses/vyper-101/3-ai/4-formatting-qs/+page.md @@ -0,0 +1,54 @@ +## Formatting a Question + +In this lesson, we'll learn how to properly format a question on a forum or when using an AI. + +We'll start by creating a new discussion on GitHub. + +Click the "Discussions" tab, then click the "New discussion" button. Choose "General" as the category, and you'll be presented with a form. + +For the discussion title, let's use "Test discussion". + +For the body, we can type something simple, like: "Feel free to say hello here!". After typing, click "Start discussion". + +We can then add a comment, which will be our poorly-formatted question. This comment will read: + +``` +Hey why my code not be good? AggregatorV3Interface priceFeed = AggregatorV3Interface { } +``` + +We'll then use a different tool, Remix, to copy the actual error message. We'll be copying the error: + +``` +TypeError: Exactly one argument expected for explicit type conversion, +-- > PriceConverter.sol:21:43: +AggregatorV3Interface priceFeed = AggregatorV3Interface +(Relevant source part starts here and spans across multiple lines). +``` + +Copy the entire error message and then navigate back to our GitHub discussion. + +Let's add a new comment and write: + +``` +I am receiving this error while compiling: ``` +TypeError: Exactly one argument expected for explicit type conversion, +-- > PriceConverter.sol:21:43: +AggregatorV3Interface priceFeed = AggregatorV3Interface +(Relevant source part starts here and spans across multiple lines). +``` +Here is my code: ```solidity +AggregatorV3Interface priceFeed = AggregatorV3Interface {} +``` +Can someone help me figure out what the issue is? + +``` + +You'll notice that our question is now much better formatted and more readable. It also includes the exact error we are receiving and only the relevant code to the error. + +We can then copy the entire question and paste it into a new chat in ChatGPT. The AI will be able to read and comprehend the code blocks, which will then provide a good, helpful response. + +The AI's response will be something like: "You're receiving the error because you're trying to create an instance of 'AggregatorV3Interface' without providing the necessary address argument for explicit type conversion. You need to supply the address of the deployed ChainLink Price Feed contract when creating the instance. To resolve this error, provide the contract address as an argument when creating the 'AggregatorV3Interface' instance." + +By following these steps, you can see how much better your questions will be received both by humans and AIs. This lesson will help you grow and move through your learning journey much faster. Make sure to spend time answering other people's questions in the forum. Think of it as question and answer debt. Every time you answer a question, make a little mental note to yourself: in the future, I need to at least try to answer one of somebody else's questions. This is how we grow and move so much faster. + +We also learned a key formatting technique: code blocks. Code blocks make your code more readable, and they help AIs to understand the content more easily. We'll cover more formatting techniques in future lessons. diff --git a/courses/vyper-101/3-ai/5-workshop/+page.md b/courses/vyper-101/3-ai/5-workshop/+page.md new file mode 100644 index 000000000..a3621938d --- /dev/null +++ b/courses/vyper-101/3-ai/5-workshop/+page.md @@ -0,0 +1,61 @@ +## Section 3: Prompting, Asking Questions, and Researching + +We've learned a lot about Vyper in the previous sections and now we are ready to put that knowledge to the test. We are going to do some work with AI. While AI can be a powerful tool, it is important to understand its limitations and use it wisely. It's crucial to be aware of situations where AI might be mistaken or give us incorrect information. + +For this workshop, we'll be testing out a few different AI tools and comparing their abilities. We'll be asking them some softball questions to gauge their understanding of smart contract development. + +**Our First Test:** + +Our first test will be to ask the AI to make a minimal Vyper contract. + +**Next, we'll ask the AI to explain the difference between a dynamic array and a fixed-sized array in Vyper.** + +**We can also test the AI's ability to understand Vyper code by asking it to explain a code snippet.** + +For example, we can ask: + +```python +@external +def add_person(name: String[100], favorite_number: uint256): + new_person = Person(self.list_of_people_index, favorite_number, name=name) + self.list_of_people[self.list_of_people_index] = new_person + self.list_of_numbers[self.list_of_people_index] = favorite_number + self.list_of_people_index += 1 + self.name_to_favorite_number[name] = favorite_number +``` + +**What does this code do?** + +**Finally, we'll test the AI's knowledge of Vyper security by asking it to analyze a code snippet and identify any potential vulnerabilities.** + +For example, we can ask: + +```python +send(OWNER, self.balance) +``` + +**Is this Vyper code safe?** + +We've learned that we need to use **raw call** in Vyper instead of **send** for security reasons. Let's ask the AI to tell us if our code is safe and suggest a better way to send funds: + +```python +send(OWNER, self.balance) +``` + +**What's a better way?** + +**Let's test the AI's understanding of how to use the `raw_call` method.** + +We can ask: + +```python +success = raw_call(self.owner, amount, gas=3000, value=amount) +``` + +**How can we use the `raw_call` method for sending Ether to prevent reentrancy issues?** + +**In the previous sections, we learned about the `raw_call` method.** It is a powerful tool in Vyper that allows us to interact with other contracts directly. But it is important to use the `raw_call` method cautiously and to be aware of the risks associated with it, specifically the risk of reentrancy. + +We are also going to test the AI to make sure it can provide secure solutions. + +**By testing these AI tools, we can gain a better understanding of how they work and how they can be used to help us write better Vyper contracts.**