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

`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. 
5. Say yes to everything 
6. Done! 
- 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

[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

oomph error, `token` vs `Token` is kinda weird

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 :).

[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.  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);
});
```

[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`

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)