Buildspace: Solidity smart contract course notes (student-made) === ###### tags: `buildspace` >Project A 2-week project where you'll learn some Solidity, write + deploy a smart contract to the blockchain, and build a Web3 client app to interact with your contract. Perfect for hackers curious about crypto. [toc] # Pre-course ## Read - [INTRODUCTION TO THE ETHEREUM STACK](https://ethereum.org/en/developers/docs/ethereum-stack/) - [What's a testnet](https://ethereum.org/en/developers/docs/networks/#testnets) - [Hardhat](https://hardhat.org/getting-started/#overview) - [Crypto Glossary](https://cryptocurrencyalerting.com/glossary.html) - [Video sneak peek](https://www.loom.com/share/8746b43760c74c6791ba17af9940ea8e) - #random [What is Axie Infinity](https://www.fool.com/the-ascent/cryptocurrency/articles/what-is-axie-infinity-axs-and-should-you-buy-it/) - [Eth Brownies](https://eth-brownie.readthedocs.io/en/stable/) ## Do - Make a [Twitch account](https://www.twitch.tv/buildspace) - (optional) Install Hardhat ![](https://i.imgur.com/MEG5vpB.png) `1. npm install --save-dev hardhat` `2. Create a new directory, mkdir buildspace-test. cd into it.` `3. npx hardhat and then choose the option to create a project, say y to everything.` `4. npx hardhat compile` `5. npx hardhat test` **If you're on a Mac** 1. Install XCode first 2. Install [NPM](https://ourcodeworld.com/articles/read/1429/how-to-install-nodejs-in-macos-bigsur) should be >=12. I install v16 3. Then install [hardhat](https://hardhat.org/getting-started/#overview) 4. ![](https://i.imgur.com/OnWClnO.png) 5. Say yes to everything ![](https://i.imgur.com/7XXNG0C.png) 6. Done! ![](https://i.imgur.com/3tMwaEW.png) - Prep testnet Ether with [Ropsten](https://www.rinkeby.io/#faucet) - First two chapters of [cryptozombies](https://cryptozombies.io/en/course/) ``` pragma solidity >=0.5.0 <0.6.0; contract ZombieFactory { // Declare an event called NewZombie. It should pass zombieId (a uint), name (a string), and dna (a uint). event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; // Zombie structs, and name it zombies struct Zombie { string name; uint dna; } // Create a public array of Zombie structs, and name it zombies. Zombie[] public zombies; //Create a mapping called zombieToOwner. The key will be a uint (we'll store and look up the zombie based on its id) and the value an address. Let's make this mapping public. //Create a mapping called ownerZombieCount, where the key is an address and the value a uint. mapping (uint => address) public zombieToOwner; mapping (address => uint) ownerZombieCount; // Create a internal function named createZombie. It should take two parameters: _name (a string), and _dna (a uint). Don't forget to pass the first argument by value by using the memory keyword function _createZombie(string memory _name, uint _dna) internal { // You're going to need the zombie's id. array.push() returns a uint of the new length of the array - and since the first item in an array has index 0, array.push() - 1 will be the index of the zombie we just added. Store the result of zombies.push() - 1 in a uint called id, so you can use this in the NewZombie event in the next line. uint id = zombies.push(Zombie(_name, _dna))-1; // First, after we get back the new zombie's id, let's update our zombieToOwner mapping to store msg.sender under that id. // Second, let's increase ownerZombieCount for this msg.sender. zombieToOwner[id] = msg.sender; ownerZombieCount[msg.sender]++; // Modify the _createZombie function to fire the NewZombie event after adding the new Zombie to our zombies array. emit NewZombie(id, _name, _dna); } // Private functions start with _. Create a private function called _generateRandomDna. It will take one parameter named _str (a string), and return a uint. Don't forget to set the data location of the _str parameter to memory. function _generateRandomDna(string memory _str) private view returns (uint) { uint rand = uint(keccak256(abi.encodePacked(_str))); // We want our DNA to only be 16 digits long (remember our dnaModulus?). So the second line of code should return the above value modulus (%) dnaModulus. return rand % dnaModulus; } function createRandomZombie(string memory _name) public { // Put a require statement at the beginning of createRandomZombie. The function should check to make sure ownerZombieCount[msg.sender] is equal to 0, and throw an error otherwise. require(ownerZombieCount[msg.sender] == 0); // The first line of code should take the keccak256 hash of abi.encodePacked(_str) to generate a pseudo-random hexadecimal, typecast it as a uint, and finally store the result in a uint called rand. uint randDna = _generateRandomDna(_name); _createZombie(_name, randDna); } } pragma solidity >=0.5.0 <0.6.0; import "./zombiefactory.sol"; contract KittyInterface { function getKitty(uint256 _id) external view returns ( bool isGestating, bool isReady, uint256 cooldownIndex, uint256 nextActionAt, uint256 siringWithId, uint256 birthTime, uint256 matronId, uint256 sireId, uint256 generation, uint256 genes ); } // Make a contract called ZombieFeeding below ZombieFactory. This contract should inherit from our ZombieFactory contract. contract ZombieFeeding is ZombieFactory { //I've saved the address of the CryptoKitties contract in the code for you, under a variable named ckAddress. In the next line, create a KittyInterface named kittyContract, and initialize it with ckAddress — just like we did with numberContract above. address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d; KittyInterface kittyContract = KittyInterface(ckAddress); // Modify function definition here: function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) public { // We don't want to let someone else feed our zombie! So first, let's make sure we own this zombie. Add a require statement to verify that msg.sender is equal to this zombie's owner (similar to how we did in the createRandomZombie function). require(msg.sender == zombieToOwner[_zombieId]); //We're going to need to get this zombie's DNA. So the next thing our function should do is declare a local Zombie named myZombie (which will be a storage pointer). Set this variable to be equal to index _zombieId in our zombies array. Zombie storage myZombie = zombies[_zombieId]; //First we need to make sure that _targetDna isn't longer than 16 digits. To do this, we can set _targetDna equal to _targetDna % dnaModulus to only take the last 16 digits. _targetDna = _targetDna % dnaModulus; //Next our function should declare a uint named newDna, and set it equal to the average of myZombie's DNA and _targetDna (as in the example above). //Note: You can access the properties of myZombie using myZombie.name and myZombie.dna uint newDna = (myZombie.dna + _targetDna) / 2; // Next, after we calculate the new zombie's DNA, let's add an if statement comparing the keccak256 hashes of _species and the string "kitty". We can't directly pass strings to keccak256. Instead, we will pass abi.encodePacked(_species) as an argument on the left side and abi.encodePacked("kitty") as an argument on the right side. // Inside the if statement, we want to replace the last 2 digits of DNA with 99. One way to do this is using the logic: newDna = newDna - newDna % 100 + 99;. // Explanation: Assume newDna is 334455. Then newDna % 100 is 55, so newDna - newDna % 100 is 334400. Finally add 99 to get 334499. if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("kitty"))) { newDna = newDna - newDna % 100 + 99; } // Once we have the new DNA, let's call _createZombie. You can look at the zombiefactory.sol tab if you forget which parameters this function needs to call it. Note that it requires a name, so let's set our new zombie's name to "NoName" for now — we can write a function to change zombies' names later. _createZombie("NoName", newDna); } // Lastly, we need to change the function call inside feedOnKitty. When it calls feedAndMultiply, add the parameter "kitty" to the end. function feedOnKitty(uint _zombieId, uint _kittyId) public { uint kittyDna; (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId); feedAndMultiply(_zombieId, kittyDna, "kitty"); } } ``` # Course ## Day One ![](https://i.imgur.com/Wh3VahO.png) [fix the get.Signers() is not found bug](https://ethereum.stackexchange.com/questions/90832/following-the-hardhat-tutorial-i-get-this-error-typeerror-ethers-getsigners-i) when you run `npx hardhat node` you HAVE to be in the folder else you'll get the default page ![](https://i.imgur.com/t02feao.png) oomph error, `token` vs `Token` is kinda weird ![](https://i.imgur.com/mNEHQbQ.png) Bonus qn: Why do we do a waveTxn.wait(); though? What are we waiting for? Why didn’t we do a .wait() after we read the total number of waves? Post your thoughts in the course-chat on Discord. Whoever gets the right answer I will give $20 in Ethereum :). ![](https://i.imgur.com/fCZvw6Y.png) [Your wait](https://docs.ethers.io/v5/api/providers/types/#providers-TransactionResponse) ### Personalise? ``` function fistbump() public { totalFistbumps +=1; console.log("%s has fistbumped!",msg.sender); } function getTotalFistbumps() view public returns (uint) { console.log("we have %d fistbumps in total", totalFistbumps); return totalFistbumps; } ``` added a fistbump function just for fun, so you can either wave or give a fistbump ## Day Two **Set up** 1. [Forking replit from Farza's own IDE](https://replit.com/@Farza/waveportal-baseline-student?v=1) 2. [Alchemy](https://dashboard.alchemyapi.io/) to get testnet API key (https://eth-rinkeby.alchemyapi.io/v2/dMC4e2Rl8LhHDGmoqzhLs6X4W2ScGJ1W). Note that because it's my first time signing up, I straight away created the app already and I chose Rinkeby 3. Deploy using the URL you're given...you need the private key of your testnet account but that's the same account as my real account... so made a new one for Learning. 4. ![](https://i.imgur.com/F064OSG.png) Address is: 0x1ACb87d511Fb6d38d252509dD505ef8cb77d7ce6 5. Next you need to collect your metamask wallet to the app. To do so, you check for Metamask, and add a connect wallet button 6. Challenge was to actually get the count that you initialise. So what you have to do is to set a global variable for counting. Then have a function that checks the blockchain the minute you are logged in. ``` import * as React from "react"; import { ethers } from "ethers"; import './App.css'; import abi from "./utils/WavePortal.json"; export default function App() { const [currAccount, setCurrentAccount] = React.useState("") const contractAddress = "0x1ACb87d511Fb6d38d252509dD505ef8cb77d7ce6" const contractABI = abi.abi const [currCount, setCurrCount] = React.useState(""); const checkIfWalletIsConnected = () => { const { ethereum } = window; if (!ethereum) { console.log("Check if you have installed Metamask") return } else { console.log("We have the Ethereum object", ethereum) } ethereum.request({method: 'eth_accounts'}) .then(accounts => { if (accounts.length!==0){ const account = accounts[0]; console.log("Found an authorized account: ", account) setCurrentAccount(account); } else { console.log("No authorized account found") } }) } const connectWallet = async () => { const { ethereum } = window; if (!ethereum) { alert("Get Metamask") } ethereum.request({method: 'eth_requestAccounts' }) .then(accounts => { console.log("Connected", accounts[0]) setCurrentAccount(accounts[0]) }) .catch(err => console.log(err)); window.location.reload(); } const wave = async () => { const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner() const waveportalContract = new ethers.Contract(contractAddress, contractABI, signer); let count = await waveportalContract.getTotalWaves() console.log("Retrieved total waves", count.toNumber()) const waveTxn = await waveportalContract.wave() console.log("Mining...", waveTxn.hash) await waveTxn.wait() console.log("Mined -- ", waveTxn.hash) count = await waveportalContract.getTotalWaves() console.log("Retrieved total wave count...", count.toNumber()) setCurrCount(count.toNumber()) console.log(currCount) } const initCount = async () => { const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner() const waveportalContract = new ethers.Contract(contractAddress, contractABI, signer); let count = await waveportalContract.getTotalWaves() console.log("Retrieved total waves", count.toNumber()) setCurrCount(count.toNumber()) } React.useEffect(()=> { checkIfWalletIsConnected(); initCount(); }, []) return ( <div className="mainContainer"> <div className="dataContainer"> <div className="header"> 👋 Hello there! </div> <div className="bio"> QZ, @f_shbiscuit with {currCount} waves </div> <button className="waveButton" onClick={wave}> Wave at Me </button> {currAccount ? null: ( <button className="wavebutton" onClick={connectWallet}>Connect Wallet then refresh page to view count </button> )} </div> </div> ); } ``` ## Day Three Allow your waves to include a message ``` // SPDX-License-Identifier: UNLICENSED // Software Package Data Exchange (SPDX) is a file format used to document information on the software licenses under which a given piece of computer software is distributed. pragma solidity ^0.8.0; import "hardhat/console.sol"; contract WavePortal { uint totalWaves; uint totalFistbumps; event NewWave(address indexed from, uint timestamp, string message); struct Wave { address waver; string message; uint timestamp; } Wave[] waves; constructor() { console.log("we have been constructed"); } function wave(string memory _message) public { totalWaves +=1; console.log("%s has waved w/ message %s",msg.sender, _message); waves.push(Wave(msg.sender, _message, block.timestamp)); emit NewWave(msg.sender, block.timestamp, _message); } function getAllWaves() view public returns (Wave[] memory) { return waves; } function getTotalWaves() view public returns (uint) { console.log("we have %d waves in total", totalWaves); return totalWaves; } function fistbump() public { totalFistbumps +=1; console.log("%s has fistbumped!",msg.sender); } function getTotalFistbumps() view public returns (uint) { console.log("we have %d fistbumps in total", totalFistbumps); return totalFistbumps; } } ``` Update run.js ``` const { ethers } = require("ethers"); async function main() { //this was missing! const { ethers } = require("hardhat"); const [owner, randoPerson] = await ethers.getSigners(); //compile the contract const waveContractFactory = await hre.ethers.getContractFactory("WavePortal"); //deploy a local Eth network for this contract, like a devnet const waveContract = await waveContractFactory.deploy(); await waveContract.deployed(); console.log("Contract deployed to:", waveContract.address); console.log("Contract deployed by:", owner.address); let count = await waveContract.getTotalWaves() console.log(count.toNumber()) let waveCount; waveCount = await waveContract.getTotalWaves(); waveCount = await waveContract.getTotalWaves(); let waveTxn = await waveContract.wave("A message!"); await waveTxn.wait(); waveTxn = await waveContract.wave("Another message!") await waveTxn.wait() let allWaves = await waveContract.getAllWaves() console.log(allWaves) let fistbumpCount; fistbumpCount = await waveContract.getTotalFistbumps(); let fistbumpTxn = await waveContract.fistbump(); await fistbumpTxn.wait(); fistbumpCount = await waveContract.getTotalFistbumps(); } main() .then(() => process.exit(0)) .catch((error)=>{ console.error(error); process.exit(1); }); ``` ![](https://i.imgur.com/tRAaEaH.png) [24:45 min Twitch Sesh3](https://www.twitch.tv/videos/1123590748) [45:03 min Twitch Sesh3](https://www.twitch.tv/videos/1123590748) - to do the `getAllWaves()` portion after `setCurrentAccount` in the `checkIfWalletIsConnected()` Note that what you've done is already implemented `getallWaves()` further down don't forget to set the new smart contract address. the format is 1. compile with `npx hardhat run scripts/run.js` 2. Deploy with `npx hardhat run scripts/deploy.js --network rinkeby` ![](https://i.imgur.com/qXWdcf3.png) Completed react app ``` import * as React from "react"; import { ethers } from "ethers"; import './App.css'; import abi from "./utils/WavePortal.json"; export default function App() { const [currAccount, setCurrentAccount] = React.useState("") const contractAddress = "0xDD7CF75AF12881eC39e643c9D0cbf46bD7AcCa44" const contractABI = abi.abi const [currCount, setCurrCount] = React.useState(""); const[message, setMessage] = React.useState(""); const checkIfWalletIsConnected = () => { const { ethereum } = window; if (!ethereum) { console.log("Check if you have installed Metamask") return } else { console.log("We have the Ethereum object", ethereum) } ethereum.request({method: 'eth_accounts'}) .then(accounts => { if (accounts.length!==0){ const account = accounts[0]; console.log("Found an authorized account: ", account) setCurrentAccount(account); getAllWaves(); } else { console.log("No authorized account found") } }) } const connectWallet = async () => { const { ethereum } = window; if (!ethereum) { alert("Get Metamask") } ethereum.request({method: 'eth_requestAccounts' }) .then(accounts => { console.log("Connected", accounts[0]) setCurrentAccount(accounts[0]) }) .catch(err => console.log(err)); window.location.reload(); } const wave = async () => { const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner() const waveportalContract = new ethers.Contract(contractAddress, contractABI, signer); let count = await waveportalContract.getTotalWaves() console.log("Retrieved total waves", count.toNumber()) const waveTxn = await waveportalContract.wave(message) console.log("Mining...", waveTxn.hash) await waveTxn.wait() console.log("Mined -- ", waveTxn.hash) count = await waveportalContract.getTotalWaves() console.log("Retrieved total wave count...", count.toNumber()) setCurrCount(count.toNumber()) console.log(currCount) } const initCount = async () => { const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner() const waveportalContract = new ethers.Contract(contractAddress, contractABI, signer); let count = await waveportalContract.getTotalWaves() console.log("Retrieved total waves", count.toNumber()) setCurrCount(count.toNumber()) } const [allWaves, setAllWaves] = React.useState([]) async function getAllWaves() { const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner() const waveportalContract = new ethers.Contract(contractAddress, contractABI, signer); let waves = await waveportalContract.getAllWaves() let wavesCleaned = [] waves.forEach(wave => { wavesCleaned.push({ address: wave.address, timestamp: new Date(wave.timestamp*1000), message: wave.message }) }) setAllWaves(wavesCleaned) } React.useEffect(()=> { checkIfWalletIsConnected(); initCount(); }, []) return ( <div className="mainContainer"> <div className="dataContainer"> <div className="header"> 👋 Hello there! </div> <div className="bio"> QZ, @f_shbiscuit with {currCount} waves </div> <button className="waveButton" onClick={wave}> Wave at Me </button> {currAccount ? null: ( <button className="wavebutton" onClick={connectWallet}>Connect Wallet then refresh page to view count </button> )} <textarea value={message} onChange={(event) => setMessage(event.target.value)}/> {allWaves.map((wave, index)=> { return ( <div style={{backgroundColour: "OldLace", marginTop: "16px", padding: "8px"}}> <div>Address: {wave.address}</div> <div>Time: {wave.timestamp.toString()}</div> <div>Message: {wave.message}</div> </div> ) })} </div> </div> ) } ``` Adding payments ``` function wave(string memory _message) public { totalWaves +=1; console.log("%s has waved w/ message %s", msg.sender, _message); waves.push(Wave(msg.sender, _message, block.timestamp)); emit NewWave(msg.sender, block.timestamp, _message); uint prizeAmount = 0.00001 ether; require(prizeAmount <= address(this).balance, "Attempt to withdraw more than contract balance"); (bool success,) = (msg.sender).call{value: prizeAmount}(""); require(success, "Failed to withdraw money from contract"); } ``` Change your deploy.js ``` const { ethers } = require("ethers"); async function main() { const { ethers } = require("hardhat"); const [deployer] = await ethers.getSigners(); console.log("Deploying contracts with the account:", deployer.address); console.log("Account balance:", (await deployer.getBalance()).toString()); const waveContractFactory = await ethers.getContractFactory("WavePortal"); const waveContract = await waveContractFactory.deploy({value: ethers.utils.parseEther("0.1")}); await waveContract.deployed() console.log("WavePortal address:", waveContract.address); } main() .then(() => process.exit(0)) .catch((error) =>{ console.error(error); process.exit(1); }); ``` Finally after many debugs it is [deployed](https://rinkeby.etherscan.io/address/0xB624239e3619BB25229Cecc30df1981cfE64710a)