├── .gitignore ├── .prettierrc.js ├── README.md ├── inbox ├── .env.example ├── .gitignore ├── README.md ├── compile.js ├── contracts │ └── Inbox.sol ├── deploy.js ├── package-lock.json ├── package.json └── test │ └── Inbox.test.js ├── kickstart ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── README.md ├── ethereum │ ├── .env │ ├── .env.example │ ├── build │ │ ├── Campaign.json │ │ └── CampaignFactory.json │ ├── compile.js │ ├── contracts │ │ └── Campaign.sol │ └── deploy.js ├── next.config.js ├── package.json ├── public │ └── favicon.ico ├── src │ └── pages │ │ ├── _app.js │ │ └── index.js ├── styles │ └── globals.css ├── test │ └── Campaign.test.js └── yarn.lock ├── lottery-react ├── .gitignore ├── README.md ├── config-overrides.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ ├── setupTests.js │ └── utils │ ├── lottery.js │ └── web3.js └── lottery ├── .env.example ├── .gitignore ├── README.md ├── compile.js ├── contracts └── Lottery.sol ├── deploy.js ├── package-lock.json ├── package.json ├── test └── Lottery.test.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules 2 | 3 | # Logs 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # IDEs and editors (shamelessly copied from @angular/cli's .gitignore) 9 | /.idea 10 | .project 11 | .classpath 12 | .c9/ 13 | *.launch 14 | .settings/ 15 | *.sublime-workspace 16 | 17 | # IDE - VSCode 18 | .vscode/* 19 | 20 | ### Linux ### 21 | *~ 22 | 23 | # temporary files which can be created if a process still has a handle open of a deleted file 24 | .fuse_hidden* 25 | 26 | # KDE directory preferences 27 | .directory 28 | 29 | # Linux trash folder which might appear on any partition or disk 30 | .Trash-* 31 | 32 | # .nfs files are created when an open file is removed but is still being accessed 33 | .nfs* 34 | 35 | ### OSX ### 36 | *.DS_Store 37 | .AppleDouble 38 | .LSOverride 39 | 40 | # Icon must end with two \r 41 | Icon 42 | 43 | # Thumbnails 44 | ._* 45 | 46 | # Files that might appear in the root of a volume 47 | .DocumentRevisions-V100 48 | .fseventsd 49 | .Spotlight-V100 50 | .TemporaryItems 51 | .Trashes 52 | .VolumeIcon.icns 53 | .com.apple.timemachine.donotpresent 54 | 55 | # Directories potentially created on remote AFP share 56 | .AppleDB 57 | .AppleDesktop 58 | Network Trash Folder 59 | Temporary Items 60 | .apdisk 61 | 62 | ### Windows ### 63 | # Windows thumbnail cache files 64 | Thumbs.db 65 | ehthumbs.db 66 | ehthumbs_vista.db 67 | 68 | # Folder config file 69 | Desktop.ini 70 | 71 | # Recycle Bin used on file shares 72 | $RECYCLE.BIN/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: false, 7 | trailingComma: "none", 8 | bracketSpacing: true, 9 | arrowParens: "avoid", 10 | endOfLine: "auto" 11 | }; 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ethereum and Solidity: The Complete Developer's Guide (Community Contributed Code Updates) 2 | 3 | ## Note: This repo is no longer maintained 4 | 5 | Hi, for anyone who has stumbled upon this repo in hope of finding up-to-date Solidity/web3.js/Node.js/React/Next.js code for the udemy.com course [Ethereum and Solidity: The Complete Developer's Guide](https://www.udemy.com/course/ethereum-and-solidity-the-complete-developers-guide/), a course that I was a student of and NOT the lecturer/creator, unfortunately while some parts of the repo do provide up-to-date code and explanations, I have not been able to afford the time to keep maintaining this repo as I would have liked and so I have decided to archive it. 6 | 7 | ## Purpose of this Repo 8 | 9 | Up-to-date Solidity/web3.js/Node.js/React/Next.js code for the udemy.com course [Ethereum and Solidity: The Complete Developer's Guide](https://www.udemy.com/course/ethereum-and-solidity-the-complete-developers-guide/). 10 | 11 | ## The Reason 12 | 13 | Toward the end of 2019 I became very interested in entering the blockchain development space and so I embarked on a journey to learn as much as I can, as quickly as I can, within this ever-evolving tech space, and to be more specific, the **Ethereum ecosystem**. Of course, I quickly realised that the development tools and packages being used to build, develop and deploy dApps and tech within this ecosystem all share a common trend: **rapid change and evolution, sometimes introducing breaking changes through iterations of their releases**. 14 | 15 | I make heavy use of the online learning website [udemy.com](https://www.udemy.com/) and find it to be a great supplementary learning tool. So naturally I bought a few courses on Ethereum and Solidity. The problem is, many of these courses target outdated versions of [Solidity](https://docs.soliditylang.org), [web3.js](https://web3js.readthedocs.io/) and [Truffle](https://www.trufflesuite.com/) in their course lessons and code examples. In the course creators' defense, remember, this is rapidly evolving tech we're dealing with here and the respective effort required to keep their video course content up-to-date with current software releases can be rather challenging. 16 | 17 | _And so, that's where I decided to lend a bit of a helping hand_. 18 | 19 | ## Let the Code speak 20 | 21 | I figured that if I wanted the online courses I enrolled in to provide up-to-date code then **other developers also had to want this**. So, I decided to take action and just write the updated code myself, starting with the Udemy course _Ethereum and Solidity: The Complete Developer's Guide_, the one I found most enjoyable and acceptable. 22 | 23 | ## Repository structure 24 | 25 | This repository was setup as a monolithic repository (without the full monorepo structure so as not to introduce unnecessary extra complexity beyond the scope of the udemy.com course), allowing me to keep the updated versions of the isolated bits of the course's code and tests well organized all within a single repository. 26 | 27 | ### Smart Contracts 28 | 29 | The smart contracts created in the course are: 30 | 31 | - [The Inbox Contract](/inbox/contracts/Inbox.sol) 32 | - [The Lottery Contract](/lottery/contracts/Lottery.sol) 33 | - [The CampaignFactory and Campaign contracts](/kickstart/ethereum/contracts/Campaign.sol) 34 | 35 | ### Working with the latest React tooling 36 | 37 | The course sections that cover building out a front-end application using React make use of outdated versions of [_Create React App_](https://create-react-app.dev) and [_Next.js_](https://nextjs.org). 38 | 39 | For Create React App, the previous approach of installing globally via `npm install -g create-react-app` is no longer the recommended approach. As such if you have already used this command and installed create-react-app globally then you should uninstall the package using `npm uninstall -g create-react-app` or `yarn global remove create-react-app`. To create a new React app you may now use one of the following methods to ensure that you always use the latest React version: 40 | 41 | - **npx**: `npx create-react-app my-app` 42 | - **npm**: `npm init react-app my-app` 43 | - **Yarn**: `yarn create react-app my-app` 44 | 45 | For more details on the above methods, see [https://create-react-app.dev/docs/getting-started](https://create-react-app.dev/docs/getting-started). 46 | 47 | **The Kickstart/CrowdCoin app implemented in this repo is itself currently being updated to the latest version of Next.js (v13).** 48 | 49 | ### The lottery-react App 50 | 51 | To create the `lottery-react` app I chose to use the npx command option, as follows: 52 | 53 | ```bash 54 | npx create-react-app lottery-react 55 | ``` 56 | 57 | - [Browse the lottery-react App code files](/lottery-react) 58 | - [lottery-react App README](/lottery-react/README.md) 59 | - [Live Demo of the app](https://lottery-react.onrender.com) 60 | 61 | ### The Kickstart/CrowdCoin App 62 | 63 | - [Browse the app code files](/kickstart) 64 | - Live Demo of the app (_update in progress_) 65 | 66 | ## Acknowledgement 67 | 68 | I would like to give credit to [Stephen Grider](https://www.udemy.com/user/sgslo/) for creating the [excellent course](https://www.udemy.com/course/ethereum-and-solidity-the-complete-developers-guide/) for which I created this repository as my own personal add-on. If any mistakes or errors are found within any of this repository's content they should be attributed to an oversight on my part, and in no part should be deemed any fault of the Udemy course author, Stephen Grider. 69 | -------------------------------------------------------------------------------- /inbox/.env.example: -------------------------------------------------------------------------------- 1 | ACCOUNT_MNEMONIC="" 2 | GOERLI_ENDPOINT="" -------------------------------------------------------------------------------- /inbox/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Logs 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # IDEs and editors (shamelessly copied from @angular/cli's .gitignore) 9 | /.idea 10 | .project 11 | .classpath 12 | .c9/ 13 | *.launch 14 | .settings/ 15 | *.sublime-workspace 16 | 17 | # IDE - VSCode 18 | .vscode/* 19 | !.vscode/settings.json 20 | !.vscode/tasks.json 21 | !.vscode/launch.json 22 | !.vscode/extensions.json 23 | 24 | ### Linux ### 25 | *~ 26 | 27 | # temporary files which can be created if a process still has a handle open of a deleted file 28 | .fuse_hidden* 29 | 30 | # KDE directory preferences 31 | .directory 32 | 33 | # Linux trash folder which might appear on any partition or disk 34 | .Trash-* 35 | 36 | # .nfs files are created when an open file is removed but is still being accessed 37 | .nfs* 38 | 39 | ### OSX ### 40 | *.DS_Store 41 | .AppleDouble 42 | .LSOverride 43 | 44 | # Icon must end with two \r 45 | Icon 46 | 47 | # Thumbnails 48 | ._* 49 | 50 | # Files that might appear in the root of a volume 51 | .DocumentRevisions-V100 52 | .fseventsd 53 | .Spotlight-V100 54 | .TemporaryItems 55 | .Trashes 56 | .VolumeIcon.icns 57 | .com.apple.timemachine.donotpresent 58 | 59 | # Directories potentially created on remote AFP share 60 | .AppleDB 61 | .AppleDesktop 62 | Network Trash Folder 63 | Temporary Items 64 | .apdisk 65 | 66 | ### Windows ### 67 | # Windows thumbnail cache files 68 | Thumbs.db 69 | ehthumbs.db 70 | ehthumbs_vista.db 71 | 72 | # Folder config file 73 | Desktop.ini 74 | 75 | # Recycle Bin used on file shares 76 | $RECYCLE.BIN/ 77 | 78 | # Environment file 79 | .env -------------------------------------------------------------------------------- /inbox/README.md: -------------------------------------------------------------------------------- 1 | # Up-to-date Inbox Smart Contract, Node.js Scripts & Unit Tests 2 | 3 | > Section 1 of the udemy.com course [Ethereum and Solidity: The Complete Developer's Guide](https://www.udemy.com/course/ethereum-and-solidity-the-complete-developers-guide/) by [Stephen Grider](https://www.udemy.com/user/sgslo/) implements an Inbox smart contract, along with a Node.js compile script, deploy script and unit tests for that contract. In this repo I provide up-to-date equivalents (along with detailed explanations) for each of these, for the benefit of students who have enrolled in Stephen's course. 4 | 5 | ## Contents 6 | 7 | - [Inbox smart contract](#inbox-smart-contract) 8 | - [Compile Script](#compile-script) 9 | - [Update your package.json](#update-your-packagejson) 10 | - [Unit Tests](#unit-tests) 11 | - [Deploy Script](#deploy-script) 12 | 13 |


14 | 15 | ## Inbox Smart Contract 16 | 17 | An up-to-date equivalent to the course's Inbox smart contract can be found [here](./contracts/Inbox.sol) and is shown immediately below: 18 | 19 | ```solidity 20 | // SPDX-License-Identifier: GPL-3.0-or-later 21 | pragma solidity >=0.5.0 <0.9.0; 22 | 23 | contract Inbox { 24 | string public message; 25 | 26 | constructor(string memory initialMessage) { 27 | message = initialMessage; 28 | } 29 | 30 | function setMessage(string memory newMessage) public { 31 | message = newMessage; 32 | } 33 | 34 | // Because we declared the `message` state variable above with 35 | // keyword `public`, the compiler automatically generates a getter 36 | // function for us equivalent to: 37 | // 38 | // function message() external view returns (string memory) { return message; } 39 | // 40 | // ...so we do **NOT** need to define a getter function ourselves. 41 | } 42 | ``` 43 | 44 | This smart contract is extremely simple but a good one for showing the changes that need to be made from the course version to bring it up-to-date with the latest Solidity version. 45 | 46 | The first line: 47 | 48 | ```solidity 49 | // SPDX-License-Identifier: GPL-3.0-or-later 50 | ``` 51 | 52 | is an [SPDX license identifier](https://docs.soliditylang.org/en/latest/layout-of-source-files.html?highlight=spdx#spdx-license-identifier), _introduced from Solidity 0.6.8_, which allows developers to specify the [license](https://spdx.org/licenses) the smart contract uses. **Every Solidity source file should start with a comment indicating its license** and it should be one of the identifiers listed at https://spdx.org/licenses. In this case I've specified that the smart contract uses the [GNU General Public License v3.0 or later](https://spdx.org/licenses/GPL-3.0-or-later.html) 53 | 54 | The next line: 55 | 56 | ```solidity 57 | pragma solidity >=0.5.0 <0.9.0; 58 | ``` 59 | 60 | specifies that this smart contract's source code is written for Solidity version 0.5.0 up to, but not including version 0.9.0. In the udemy.com course, the author uses a version pragma of ^0.4.17, so _the course's version of Inbox will not compile on a Solidity compiler earlier than version 0.4.17 nor will it compile on a compiler starting from version 0.5.0_. 61 | 62 | Within the contract body definition, we will keep the line that declares the `message` state variable as is: 63 | 64 | ```solidity 65 | string public message; 66 | ``` 67 | 68 | Now since we're declaring this variable using the `public` visibility keyword, the compiler will automatically generate a getter function that allows the current value of the `message` to be read from outside of the contract. The code of this function is equivalent to the following: 69 | 70 | ```solidity 71 | function message() external view returns (string memory) { return message; } 72 | ``` 73 | 74 | This makes the `getMessage` function that the course author adds to his Inbox contract definition redundant, so there is no need for your Inbox contract to have this function. 75 | 76 | ### Change in syntax for definining the Contructor 77 | 78 | In the course example, a constructor for Inbox is defined as follows: 79 | 80 | ```solidity 81 | function Inbox(string initialMessage) public { 82 | message = initialMessage; 83 | } 84 | ``` 85 | 86 | **This style of constructor definition is no longer valid**. As of Solidity 0.5.0, constructors [must be defined using the `constructor` keyword](https://docs.soliditylang.org/en/latest/050-breaking-changes.html#constructors). As of Solidity 0.7.0, [visibility (public / internal) is not needed for constructors anymore](https://docs.soliditylang.org/en/latest/050-breaking-changes.html#constructors). To prevent a contract from being created, it can be marked `abstract`, thereby making the visibility concept for constructors obsolete. 87 | 88 | The constructor function for the Inbox contract therefore needs to be changed to: 89 | 90 | ```solidity 91 | constructor(string memory initialMessage) { 92 | message = initialMessage; 93 | } 94 | ``` 95 | 96 | Note that with this change, we also use the `memory` keyword when declaring the constructor's `initialMessage` string parameter. This is because, as of Solidity 0.5.0, explicit data location for all variables of struct, array or mapping types is now mandatory (see https://docs.soliditylang.org/en/latest/050-breaking-changes.html#explicitness-requirements), and this applies to function parameters and return variables as well. So just for the sake of a bit more clarity: 97 | 98 | - Variables of type `string` are special arrays in Solidity. You can check out the official documentation on arrays [here](https://docs.soliditylang.org/en/latest/types.html#arrays). 99 | - Since `string`s are arrays we have to specifiy an explicit data location, so we specify the `memory` location. The Ethereum Virtual Machine (EVM) has three areas where it can store data: **storage**, **memory** and the **stack**. See https://docs.soliditylang.org/en/latest/introduction-to-smart-contracts.html#storage-memory-and-the-stack if you want to learn more about these data locations. 100 | 101 | The final change to the contract was to also add the `memory` data location to the `newMessage` parameter of the `setMessage` function. 102 | 103 |


104 | 105 | ## Compile Script 106 | 107 | An up-to-date equivalent to the course's compile script for the Inbox contract can be found [here](./compile.js) and is shown immediately below: 108 | 109 | ```js 110 | const path = require("path"); 111 | const fs = require("fs"); 112 | const solc = require("solc"); 113 | 114 | const inboxPath = path.resolve(__dirname, "contracts", "Inbox.sol"); 115 | const source = fs.readFileSync(inboxPath, "utf8"); 116 | 117 | const input = { 118 | language: "Solidity", 119 | sources: { 120 | "Inbox.sol": { 121 | content: source 122 | } 123 | }, 124 | settings: { 125 | metadata: { 126 | useLiteralContent: true 127 | }, 128 | outputSelection: { 129 | "*": { 130 | "*": ["*"] 131 | } 132 | } 133 | } 134 | }; 135 | 136 | const output = JSON.parse(solc.compile(JSON.stringify(input))); 137 | 138 | module.exports = output.contracts["Inbox.sol"].Inbox; 139 | ``` 140 | 141 | There's alot to digest with respect to the changes made to this script to bring it up-to-date. So let's dive in and explain what's going on: 142 | 143 | ### Compiler Input and Output JSON Description 144 | 145 | The recommended way to interface with the Solidity compiler, especially when developing more complex and automated setups is the so-called JSON-input-output interface. In summary, the compiler API expects a JSON formatted input and outputs the compilation result in a JSON formatted output. For details on this approach, including thorough descriptions of the input and output formats, check out the Solidity docs [here](https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-input-and-output-json-description). 146 | 147 | In our `compile.js` above, we declare a variable named `input` to hold a JavaScript object representation of the input that will be passed to the compiler after it's JSON stringified. In this object we define the single Solidity source file that has to be compiled, `Inbox.sol`, passing the fully loaded source code of the contract as the content source of the contract. 148 | 149 | The lines 150 | 151 | ```js 152 | const output = JSON.parse(solc.compile(JSON.stringify(input))); 153 | 154 | module.exports = output.contracts["Inbox.sol"].Inbox; 155 | ``` 156 | 157 | parse the output returned by the call to `solc.compile(...)` and store it in the `output` variable. We then extract the `Inbox` contract object only and set that as the only export from `compile.js`. 158 | 159 |


160 | 161 | ## Update your package.json 162 | 163 | Before I get into the unit tests and deploy script, I think it's important to first explain the updates that need to be made to this project's [package.json](./package.json), specificially the dependencies being used. **All** of the dependencies should be updated to their latest versions and the `ganache-cli` dependency replaced with `ganache`, since the `ganache-cli` package has been deprecated and is now just `ganache` (as explained [here](https://github.com/trufflesuite/ganache/blob/develop/UPGRADE-GUIDE.md)). 164 | 165 | Here is what my package.json looks like: 166 | 167 | ```json 168 | { 169 | "name": "inbox", 170 | "version": "2.0.2", 171 | "description": "Inbox smart contract Node.js project.", 172 | "main": "compile.js", 173 | "scripts": { 174 | "test": "mocha" 175 | }, 176 | "author": "Owan Hunte", 177 | "license": "ISC", 178 | "dependencies": { 179 | "@truffle/hdwallet-provider": "^2.1.6", 180 | "dotenv": "^16.0.3", 181 | "solc": "^0.8.18", 182 | "web3": "^1.8.2" 183 | }, 184 | "devDependencies": { 185 | "ganache": "^7.7.4", 186 | "mocha": "^10.2.0" 187 | } 188 | } 189 | ``` 190 | 191 | Note that I installed the `ganache` and `mocha` packages as development dependencies since both are only used in the [unit tests](./test/Inbox.test.js). 192 | 193 |


194 | 195 | ## Unit Tests 196 | 197 | An up-to-date equivalent to the course's unit tests (`Inbox.test.js`) can be found [here](./test/Inbox.test.js) and is shown immediately below: 198 | 199 | ```js 200 | const assert = require("assert"); 201 | const ganache = require("ganache"); 202 | const Web3 = require("web3"); 203 | const provider = ganache.provider(); 204 | const web3 = new Web3(provider); 205 | const { abi, evm } = require("../compile"); 206 | 207 | const message = "Hi there!"; 208 | let accounts; 209 | let inbox; 210 | 211 | beforeEach(async () => { 212 | // Get a list of all accounts. 213 | accounts = await web3.eth.getAccounts(); 214 | 215 | // Use one of those accounts to deploy the contract. 216 | inbox = await new web3.eth.Contract(abi) 217 | .deploy({ data: "0x" + evm.bytecode.object, arguments: [message] }) 218 | .send({ from: accounts[0], gas: "1000000" }); 219 | }); 220 | 221 | describe("Inbox", () => { 222 | it("deploys a contract", () => { 223 | assert.ok(inbox.options.address); 224 | }); 225 | 226 | it("has a default message", async () => { 227 | const msg = await inbox.methods.message().call(); 228 | assert.strictEqual(msg, message); 229 | }); 230 | 231 | it("can change the message", async () => { 232 | const newMsg = "bye"; 233 | await inbox.methods.setMessage(newMsg).send({ from: accounts[0] }); 234 | 235 | const msg = await inbox.methods.message().call(); 236 | assert.strictEqual(msg, newMsg); 237 | }); 238 | }); 239 | ``` 240 | 241 | There are 2 main changes happening with the above tests file. First we have the line: 242 | 243 | ```js 244 | const ganache = require("ganache"); 245 | ``` 246 | 247 | which replaces the line from the course's version that uses the now deprecated `ganache-cli`. 248 | 249 | Second, the line 250 | 251 | ```js 252 | const { abi, evm } = require("../compile");` 253 | ``` 254 | 255 | imports the compiled `Inbox` contract object that the compile script exports and stores the `abi` and `evm` object values as variables. The import line which the course has, `const { interface, bytecode } = require("../compile");`, will not work with the latest Solidity compiler versions. The `abi` object replaces the `interface` object, and we can access the contract's bytecode object via `evm.bytecode.object`, as shown above. 256 | 257 |


258 | 259 | ## Deploy Script 260 | 261 | An up-to-date equivalent to the course's deploy script for the Inbox contract can be found [here](./deploy.js) and is shown immediately below: 262 | 263 | ```js 264 | require("dotenv").config(); 265 | 266 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 267 | const Web3 = require("web3"); 268 | const { abi, evm } = require("./compile"); 269 | const mnemonicPhrase = process.env.ACCOUNT_MNEMONIC; 270 | const network = process.env.GOERLI_ENDPOINT; 271 | 272 | const provider = new HDWalletProvider({ 273 | mnemonic: { 274 | phrase: mnemonicPhrase 275 | }, 276 | providerOrUrl: network 277 | }); 278 | 279 | const web3 = new Web3(provider); 280 | const message = "Hi there!"; 281 | 282 | const deploy = async () => { 283 | const accounts = await web3.eth.getAccounts(); 284 | console.log("Attempting to deploy from account", accounts[0]); 285 | 286 | const result = await new web3.eth.Contract(abi) 287 | .deploy({ data: "0x" + evm.bytecode.object, arguments: [message] }) 288 | .send({ from: accounts[0] }); 289 | 290 | console.log("Contract deployed to", result.options.address); 291 | provider.engine.stop(); 292 | }; 293 | 294 | deploy(); 295 | ``` 296 | 297 | The changes in this script from the course's version are as follows: 298 | 299 | - The line `const { abi, evm } = require("./compile");` replaces the `const { interface, bytecode } = require("./compile");` line that's found in the course example, and we access the bytecode object via `evm.bytecode.object`. 300 | - Instead of hard-coding the account mnemonic and Infura endpoint as is done in the course's deploy script, I'm storing and referencing these via environment variables. 301 | - A [Goerli Infura](https://app.infura.io) endpoint (stored in the `process.env.GOERLI_ENDPOINT` environment variable) is passed to `HDWalletProvider` instead of a Rinkeby endpoint since the Rinkeby network no longer exists. So when copying your endpoint from the Infura dashboard, remember to grab the Goerli Ethereum endpoint. 302 | - The `dotenv` package is used to read these environment variables from a `.env` file. Create that file locally in the root of your inbox folder and copy the contents of [`.env.example`](./.env.example) into your `.env` file. Set `ACCOUNT_MNEMONIC` and `GOERLI_ENDPOINT` in your `.env` file appropriately. **DO NOT use a mnemonic for an account/wallet with real money or Ether associated with it!** 303 | - To prevent the deployment from hanging, the statement `provider.engine.stop();` is added at the end of the `deploy` function definition. 304 | 305 | ## That's all for now 306 | 307 | That about covers things where the updates to the Inbox smart contract and Node.js project are concerned. As always, I sincerely hope my contributions prove useful to all students of the course who find their way to this repository. 308 | -------------------------------------------------------------------------------- /inbox/compile.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const solc = require("solc"); 4 | 5 | const inboxPath = path.resolve(__dirname, "contracts", "Inbox.sol"); 6 | const source = fs.readFileSync(inboxPath, "utf8"); 7 | 8 | /*** 9 | * The recommended way to interface with the Solidity compiler, especially for more 10 | * complex and automated setups is the so-called JSON-input-output interface. 11 | * 12 | * See https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-input-and-output-json-description 13 | * for more details. 14 | */ 15 | const input = { 16 | language: "Solidity", 17 | sources: { 18 | // Each Solidity source file to be compiled must be specified by defining either 19 | // a URL to the file or the literal file content. 20 | // See https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description 21 | "Inbox.sol": { 22 | content: source 23 | } 24 | }, 25 | settings: { 26 | metadata: { 27 | useLiteralContent: true 28 | }, 29 | outputSelection: { 30 | "*": { 31 | "*": ["*"] 32 | } 33 | } 34 | } 35 | }; 36 | 37 | const output = JSON.parse(solc.compile(JSON.stringify(input))); 38 | 39 | module.exports = output.contracts["Inbox.sol"].Inbox; 40 | -------------------------------------------------------------------------------- /inbox/contracts/Inbox.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.5.0 <0.9.0; 3 | 4 | contract Inbox { 5 | // The keyword `public` automatically generates a function that 6 | // allows you to access the current value of the state variable 7 | // from outside of the contract (see lines 22 - 28). 8 | string public message; 9 | 10 | // Note: Removing `public` visibility specifier. Visibility (public / internal) 11 | // is not needed for constructors anymore: To prevent a contract from being 12 | // created, it can be marked abstract. This makes the visibility concept 13 | // for constructors obsolete. 14 | constructor(string memory initialMessage) { 15 | message = initialMessage; 16 | } 17 | 18 | function setMessage(string memory newMessage) public { 19 | message = newMessage; 20 | } 21 | 22 | // Because we declared the `message` state variable above with 23 | // keyword `public`, the compiler automatically generates a getter 24 | // function for us equivalent to: 25 | // 26 | // function message() external view returns (string memory) { return message; } 27 | // 28 | // ...so we do **NOT** need to define a getter function ourselves. 29 | } 30 | -------------------------------------------------------------------------------- /inbox/deploy.js: -------------------------------------------------------------------------------- 1 | // Load environment variables. 2 | require("dotenv").config(); 3 | 4 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 5 | const Web3 = require("web3"); 6 | const { abi, evm } = require("./compile"); 7 | const mnemonicPhrase = process.env.ACCOUNT_MNEMONIC; 8 | const network = process.env.GOERLI_ENDPOINT; 9 | 10 | const provider = new HDWalletProvider({ 11 | mnemonic: { 12 | phrase: mnemonicPhrase 13 | }, 14 | providerOrUrl: network 15 | }); 16 | 17 | const web3 = new Web3(provider); 18 | const message = "Hi there!"; 19 | 20 | const deploy = async () => { 21 | const accounts = await web3.eth.getAccounts(); 22 | console.log("Attempting to deploy from account", accounts[0]); 23 | 24 | const result = await new web3.eth.Contract(abi) 25 | .deploy({ data: "0x" + evm.bytecode.object, arguments: [message] }) 26 | .send({ from: accounts[0] }); 27 | 28 | console.log("Contract deployed to", result.options.address); 29 | provider.engine.stop(); 30 | }; 31 | 32 | deploy(); 33 | -------------------------------------------------------------------------------- /inbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inbox", 3 | "version": "2.0.2", 4 | "description": "Inbox smart contract Node.js project.", 5 | "main": "compile.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "author": "Owan Hunte", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@truffle/hdwallet-provider": "^2.1.6", 13 | "dotenv": "^16.0.3", 14 | "solc": "^0.8.18", 15 | "web3": "^1.8.2" 16 | }, 17 | "devDependencies": { 18 | "ganache": "^7.7.4", 19 | "mocha": "^10.2.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /inbox/test/Inbox.test.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const ganache = require("ganache"); 3 | const Web3 = require("web3"); 4 | const provider = ganache.provider(); 5 | const web3 = new Web3(provider); 6 | const { abi, evm } = require("../compile"); 7 | 8 | const message = "Hi there!"; 9 | let accounts; 10 | let inbox; 11 | 12 | beforeEach(async () => { 13 | // Get a list of all accounts. 14 | accounts = await web3.eth.getAccounts(); 15 | 16 | // Use one of those accounts to deploy the contract. 17 | inbox = await new web3.eth.Contract(abi) 18 | .deploy({ data: "0x" + evm.bytecode.object, arguments: [message] }) 19 | .send({ from: accounts[0], gas: "1000000" }); 20 | }); 21 | 22 | describe("Inbox", () => { 23 | it("deploys a contract", () => { 24 | assert.ok(inbox.options.address); 25 | }); 26 | 27 | it("has a default message", async () => { 28 | const msg = await inbox.methods.message().call(); 29 | assert.strictEqual(msg, message); 30 | }); 31 | 32 | it("can change the message", async () => { 33 | const newMsg = "bye"; 34 | await inbox.methods.setMessage(newMsg).send({ from: accounts[0] }); 35 | 36 | const msg = await inbox.methods.message().call(); 37 | assert.strictEqual(msg, newMsg); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /kickstart/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /kickstart/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /kickstart/.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies folder/files: 2 | node_modules 3 | .pnp 4 | .pnp.js 5 | 6 | # artifacts: 7 | build 8 | coverage 9 | 10 | # next.js and vercel 11 | .next 12 | .vercel 13 | out 14 | 15 | # debug files: 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # misc: 21 | .DS_Store 22 | *.pem 23 | 24 | # env files: 25 | .env 26 | .env.* -------------------------------------------------------------------------------- /kickstart/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "es5", 4 | singleQuote: true, 5 | printWidth: 100, 6 | tabWidth: 2, 7 | useTabs: false, 8 | }; 9 | -------------------------------------------------------------------------------- /kickstart/README.md: -------------------------------------------------------------------------------- 1 | # The Kickstart/CrowdCoin App 2 | 3 | This README is for my version of the Kickstart/CrowdCoin app developed in the udemy.com course [Ethereum and Solidity: The Complete Developer's Guide](https://www.udemy.com/course/ethereum-and-solidity-the-complete-developers-guide/). For this app I used the latest version of [Next.js](https://nextjs.org) when I created this repo, and this is now being updated to the currently latest version of Next.js (**v12**) as part of my current efforts to bring this repo up-to-date. So bear with me as I work towards completing this upgrade over the next week. 4 | -------------------------------------------------------------------------------- /kickstart/ethereum/.env: -------------------------------------------------------------------------------- 1 | ACCOUNT_MNEMONIC="question cream ensure tackle lyrics filter chat blue weasel whale dinner current" 2 | RINKEBY_ENDPOINT="https://rinkeby.infura.io/v3/103b800ab3a64b9f94500919bbaeb94a" -------------------------------------------------------------------------------- /kickstart/ethereum/.env.example: -------------------------------------------------------------------------------- 1 | ACCOUNT_MNEMONIC="" 2 | RINKEBY_ENDPOINT="" -------------------------------------------------------------------------------- /kickstart/ethereum/compile.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const solc = require("solc"); 3 | const fs = require("fs-extra"); 4 | 5 | const buildPath = path.resolve(__dirname, "build"); 6 | const contractFileName = "Campaign.sol"; 7 | 8 | // Delete the current build folder. 9 | fs.removeSync(buildPath); 10 | 11 | const campaignPath = path.resolve(__dirname, "contracts", contractFileName); 12 | const source = fs.readFileSync(campaignPath, "utf8"); 13 | 14 | /*** 15 | * The recommended way to interface with the Solidity compiler, especially for more 16 | * complex and automated setups is the so-called JSON-input-output interface. 17 | * 18 | * See https://docs.soliditylang.org/en/v0.8.6/using-the-compiler.html#compiler-input-and-output-json-description 19 | * for more details. 20 | */ 21 | const input = { 22 | language: "Solidity", 23 | sources: {}, 24 | settings: { 25 | metadata: { 26 | useLiteralContent: true, 27 | }, 28 | outputSelection: { 29 | "*": { 30 | "*": ["*"], 31 | }, 32 | }, 33 | }, 34 | }; 35 | 36 | input.sources[contractFileName] = { 37 | content: source, 38 | }; 39 | 40 | const output = JSON.parse(solc.compile(JSON.stringify(input))); 41 | const contracts = output.contracts[contractFileName]; 42 | 43 | // Create the build folder. 44 | fs.ensureDirSync(buildPath); 45 | 46 | // Extract and write the JSON representations of the contracts to the build folder. 47 | for (let contract in contracts) { 48 | if (contracts.hasOwnProperty(contract)) { 49 | fs.outputJsonSync(path.resolve(buildPath, `${contract}.json`), contracts[contract]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /kickstart/ethereum/contracts/Campaign.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.5.0 <0.9.0; 3 | 4 | contract CampaignFactory { 5 | Campaign[] public deployedCampaigns; 6 | 7 | function createCampaign(uint256 minimum) public { 8 | Campaign newCampaign = new Campaign(minimum, msg.sender); 9 | deployedCampaigns.push(newCampaign); 10 | } 11 | 12 | function getDeployedCampaigns() public view returns (Campaign[] memory) { 13 | return deployedCampaigns; 14 | } 15 | } 16 | 17 | contract Campaign { 18 | struct Request { 19 | string description; 20 | uint256 value; 21 | address payable recipient; 22 | bool complete; 23 | uint256 approvalCount; 24 | mapping(address => bool) approvals; 25 | } 26 | 27 | address public manager; 28 | uint256 public minimumContribution; 29 | mapping(address => bool) public approvers; 30 | uint256 public approversCount; 31 | 32 | // As of Solidity 0.7.0, an unsafe feature related to mappings in Structs was removed. 33 | // If a struct or array contains a mapping, it can only be used in storage. Prior to 0.7.0, 34 | // mapping members were silently skipped in memory, which is confusing and error-prone. 35 | // 36 | // So the line which is given in the course code: 37 | // 38 | // Request[] public requests; 39 | // 40 | // now becomes the following 2 lines of code. Also in the createRequest 41 | // function, instead of creating a memory Struct, which the code was proviously 42 | // doing, we now have to create a storage Struct. 43 | // 44 | // And finally, wherever the course code has the line `requests.length` gets replaced 45 | // by `numRequests`. 46 | // 47 | // https://docs.soliditylang.org/en/v0.7.0/070-breaking-changes.html#mappings-outside-storage 48 | // https://docs.soliditylang.org/en/v0.8.6/types.html?highlight=struct#structs 49 | // 50 | uint256 numRequests; 51 | mapping(uint256 => Request) public requests; 52 | 53 | constructor(uint256 minimum, address creator) { 54 | manager = creator; 55 | minimumContribution = minimum; 56 | } 57 | 58 | function contribute() public payable { 59 | require( 60 | msg.value >= minimumContribution, 61 | "A minumum contribution is required." 62 | ); 63 | approvers[msg.sender] = true; 64 | approversCount++; 65 | } 66 | 67 | function createRequest( 68 | string memory description, 69 | uint256 value, 70 | address payable recipient 71 | ) public onlyManager { 72 | Request storage r = requests[numRequests++]; 73 | r.description = description; 74 | r.value = value; 75 | r.recipient = recipient; 76 | r.complete = false; 77 | r.approvalCount = 0; 78 | } 79 | 80 | function approveRequest(uint256 index) public { 81 | Request storage request = requests[index]; 82 | require( 83 | approvers[msg.sender], 84 | "Only contributors can approve a specific payment request" 85 | ); 86 | require( 87 | !request.approvals[msg.sender], 88 | "You have already voted to approve this request" 89 | ); 90 | 91 | request.approvals[msg.sender] = true; 92 | request.approvalCount++; 93 | } 94 | 95 | function finalizeRequest(uint256 index) public onlyManager { 96 | Request storage request = requests[index]; 97 | require( 98 | request.approvalCount > (approversCount / 2), 99 | "This request needs more approvals before it can be finalized" 100 | ); 101 | require(!(request.complete), "This request has already been finalized"); 102 | 103 | request.recipient.transfer(request.value); 104 | request.complete = true; 105 | } 106 | 107 | function getSummary() 108 | public 109 | view 110 | returns ( 111 | uint256, 112 | uint256, 113 | uint256, 114 | uint256, 115 | address 116 | ) 117 | { 118 | return ( 119 | minimumContribution, 120 | address(this).balance, 121 | numRequests, 122 | approversCount, 123 | manager 124 | ); 125 | } 126 | 127 | function getRequestsCount() public view returns (uint256) { 128 | return numRequests; 129 | } 130 | 131 | modifier onlyManager() { 132 | require( 133 | msg.sender == manager, 134 | "Only the campaign manager can call this function." 135 | ); 136 | _; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /kickstart/ethereum/deploy.js: -------------------------------------------------------------------------------- 1 | // Load environment variables. 2 | require("dotenv").config(); 3 | 4 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 5 | const Web3 = require("web3"); 6 | const compiledFactory = require("./build/CampaignFactory.json"); 7 | const mnemonicPhrase = process.env.ACCOUNT_MNEMONIC; 8 | const network = process.env.RINKEBY_ENDPOINT; 9 | 10 | const provider = new HDWalletProvider({ 11 | mnemonic: { 12 | phrase: mnemonicPhrase 13 | }, 14 | providerOrUrl: network 15 | }); 16 | 17 | const web3 = new Web3(provider); 18 | 19 | const deploy = async () => { 20 | const accounts = await web3.eth.getAccounts(); 21 | console.log("Attempting to deploy from account", accounts[0]); 22 | 23 | const result = await new web3.eth.Contract(compiledFactory.abi) 24 | .deploy({ data: "0x" + compiledFactory.evm.bytecode.object }) 25 | .send({ from: accounts[0] }); 26 | 27 | console.log("Contract deployed to", result.options.address); 28 | provider.engine.stop(); 29 | }; 30 | 31 | deploy(); 32 | -------------------------------------------------------------------------------- /kickstart/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | eslint: { 3 | dirs: ["src"], // Run ESLint on the 'src' directory during production builds (next build) 4 | }, 5 | reactStrictMode: true, 6 | }; 7 | -------------------------------------------------------------------------------- /kickstart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kickstart-js", 3 | "version": "3.0.0", 4 | "description": "Kickstart/CrowdCoin App based on the udemy.com course Ethereum and Solidity: The Complete Developer's Guide", 5 | "private": true, 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "test": "mocha" 12 | }, 13 | "browserslist": [ 14 | ">0.3%", 15 | "not ie 11", 16 | "not dead", 17 | "not op_mini all" 18 | ], 19 | "dependencies": { 20 | "@truffle/hdwallet-provider": "^1.5.1", 21 | "dotenv": "^10.0.0", 22 | "fs-extra": "^10.0.0", 23 | "ganache-cli": "^6.12.2", 24 | "mocha": "^9.1.3", 25 | "next": "12.0.2", 26 | "react": "17.0.2", 27 | "react-dom": "17.0.2", 28 | "semantic-ui-css": "^2.4.1", 29 | "semantic-ui-react": "^2.0.4", 30 | "solc": "^0.8.9", 31 | "web3": "^1.6.0" 32 | }, 33 | "devDependencies": { 34 | "eslint": "7.32.0", 35 | "eslint-config-next": "12.0.2", 36 | "eslint-config-prettier": "^8.3.0", 37 | "prettier": "^2.4.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /kickstart/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owanhunte/ethereum-solidity-course-updated-code/ca00b11721ccd0074f3aa5f4c508d4607f139d5f/kickstart/public/favicon.ico -------------------------------------------------------------------------------- /kickstart/src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import 'semantic-ui-css/semantic.min.css'; 2 | import '../../styles/globals.css'; 3 | 4 | function MyApp({ Component, pageProps }) { 5 | return ; 6 | } 7 | 8 | export default MyApp; 9 | -------------------------------------------------------------------------------- /kickstart/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | export default function CampaignIndex() { 4 | return ( 5 |
6 | 7 | Kickstart/CrowdCoin App - by Owan Hunte 8 | 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /kickstart/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /kickstart/test/Campaign.test.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const ganache = require("ganache-cli"); 3 | const Web3 = require("web3"); 4 | const provider = ganache.provider(); 5 | const web3 = new Web3(provider); 6 | 7 | const compiledFactory = require("../ethereum/build/CampaignFactory.json"); 8 | const compiledCampaign = require("../ethereum/build/Campaign.json"); 9 | 10 | let accounts; 11 | let factory; 12 | let campaignAddress; 13 | let campaign; 14 | 15 | beforeEach(async () => { 16 | accounts = await web3.eth.getAccounts(); 17 | 18 | /*** 19 | * NOTE: In the Udemy course code for this test file, the instructor sets 20 | * gas to a value of 1000000 (1 million). That does not work here. My tests 21 | * result in a 'VM Exception while processing transaction: out of gas' 22 | * being generated when using that value. 23 | * 24 | * However, when I set gas to 1500000 (1.5 million) the tests passed. 25 | */ 26 | factory = await new web3.eth.Contract(compiledFactory.abi) 27 | .deploy({ data: "0x" + compiledFactory.evm.bytecode.object }) 28 | .send({ from: accounts[0], gas: "1500000" }); 29 | 30 | await factory.methods.createCampaign("100").send({ 31 | from: accounts[0], 32 | gas: "1500000" 33 | }); 34 | 35 | [campaignAddress] = await factory.methods.getDeployedCampaigns().call(); 36 | campaign = await new web3.eth.Contract(compiledCampaign.abi, campaignAddress); 37 | }); 38 | 39 | describe("Campaigns", () => { 40 | it("deploys a factory and a campaign", () => { 41 | assert.ok(factory.options.address); 42 | assert.ok(campaign.options.address); 43 | }); 44 | 45 | it("marks caller as the campaign manager", async () => { 46 | const manager = await campaign.methods.manager().call(); 47 | assert.strictEqual(manager, accounts[0]); 48 | }); 49 | 50 | it("allows people to contribute money and marks them as approvers", async () => { 51 | await campaign.methods.contribute().send({ 52 | value: "200", 53 | from: accounts[1] 54 | }); 55 | 56 | const isContributor = await campaign.methods.approvers(accounts[1]).call(); 57 | assert(isContributor); 58 | }); 59 | 60 | it("requires a minimum contribution", async () => { 61 | try { 62 | await campaign.methods.contribute().send({ 63 | value: "5", 64 | from: accounts[1] 65 | }); 66 | assert(false); 67 | } catch (error) { 68 | assert(error); 69 | } 70 | }); 71 | 72 | it("allows a manager to make a payment request", async () => { 73 | await campaign.methods.createRequest("Buy batteries", "100", accounts[1]).send({ 74 | from: accounts[0], 75 | gas: "1500000" 76 | }); 77 | 78 | const request = await campaign.methods.requests(0).call(); 79 | assert("Buy batteries", request.description); 80 | }); 81 | 82 | it("processes requests", async () => { 83 | // Let accounts[1] contribute 10 ether to the campaign. 84 | await campaign.methods.contribute().send({ 85 | value: web3.utils.toWei("10", "ether"), 86 | from: accounts[1] 87 | }); 88 | 89 | // Create a spend request for 5 ether to go to accounts[2]. 90 | await campaign.methods 91 | .createRequest("A cool spend request", web3.utils.toWei("5", "ether"), accounts[2]) 92 | .send({ 93 | from: accounts[0], 94 | gas: "1500000" 95 | }); 96 | 97 | // Approve the spend request. 98 | await campaign.methods.approveRequest(0).send({ 99 | from: accounts[1], 100 | gas: "1500000" 101 | }); 102 | 103 | // Finalize the request. 104 | await campaign.methods.finalizeRequest(0).send({ 105 | from: accounts[0], 106 | gas: "1500000" 107 | }); 108 | 109 | let balance = await web3.eth.getBalance(accounts[2]); 110 | balance = web3.utils.fromWei(balance, "ether"); 111 | balance = parseFloat(balance); 112 | 113 | assert(balance > 104); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /lottery-react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /lottery-react/README.md: -------------------------------------------------------------------------------- 1 | # Lottery React App 2 | 3 | This README is for my version of the lottery-react app developed in the [udemy.com](https://www.udemy.com) course [Ethereum and Solidity: The Complete Developer's Guide](https://www.udemy.com/course/ethereum-and-solidity-the-complete-developers-guide/). In the course the instructor makes use of the previous approach of installing `create-react-app` globally via `npm install -g create-react-app`. This is no longer the recommended approach. As such, I bootstrapped my setup of the project using one of the new recommended methods, specifically, I used the `npx` command: 4 | 5 | ```bash 6 | npx create-react-app lottery-react 7 | ``` 8 | 9 | ## Update `lottery-react/src/utils/lottery.js` 10 | 11 | Before running this app you should deploy the [Lottery smart contract](/lottery/contracts/Lottery.sol) to the Goerli Test Network and update the `contractAddress` and `abi` variables in your **lottery-react/src/utils/lottery.js** file with the blockchain address and ABI of your deployed contract on the Goerli Test Network. 12 | 13 | ## MetaMask browser plugin required 14 | 15 | As covered in the course, the app expects that you have the MetaMask web browser plugin installed since it makes use of the Ethereum Provider API that MetaMask injects into the web page. 16 | 17 | ## Final Note 18 | 19 | My implementation of the Lottery React app is somewhat different from what is given in the course code. Students attempting to learn from my solution should take note of the following: 20 | 21 | - I'm making use of the [@metamask/detect-provider](https://github.com/MetaMask/detect-provider) utility for detecting the MetaMask Ethereum provider and enforcing that MetaMask be used, as opposed to some other ethereum-compatible browser (via options.mustBeMetaMask). See https://docs.metamask.io/guide/ethereum-provider.html#using-the-provider for more details on using the Ethereum Provider API. 22 | - Even if MetaMask is installed and the Ethereum provider detected, [App.js enforces a requirement](./src/App.js#L35) that the user MUST be connected the Goerli test network and not any other Ethereum network. 23 | - Unlike the approach the course takes where it automatically initiates a connection request to access the user's Ethereum account(s) when the app is loaded, I take the recommended approach to only initiate a connection request in response to direct user action, such as clicking a button. See https://docs.metamask.io/guide/getting-started.html#connecting-to-metamask 24 | - The [src/utils/web3.js](./src/utils/web3.js) file exports an async function instead of an actual web3 reference to allow the web3 instance to be created asynchronously and stored in React state. 25 | - I defined [App.js](./src/App.js) as a functional component instead of a class component. 26 | - I gave the app a bit of styling. 27 | 28 | ## Live Demo 29 | 30 | I have deployed a live version of this app to [Render](https://render.com) at https://lottery-react.onrender.com. 31 | -------------------------------------------------------------------------------- /lottery-react/config-overrides.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | module.exports = function override(config) { 4 | const fallback = config.resolve.fallback || {}; 5 | Object.assign(fallback, { 6 | crypto: require.resolve("crypto-browserify"), 7 | stream: require.resolve("stream-browserify"), 8 | assert: require.resolve("assert"), 9 | http: require.resolve("stream-http"), 10 | https: require.resolve("https-browserify"), 11 | os: require.resolve("os-browserify"), 12 | url: require.resolve("url") 13 | }); 14 | config.resolve.fallback = fallback; 15 | config.ignoreWarnings = [/Failed to parse source map/]; 16 | config.plugins = (config.plugins || []).concat([ 17 | new webpack.ProvidePlugin({ 18 | process: "process/browser", 19 | Buffer: ["buffer", "Buffer"] 20 | }) 21 | ]); 22 | return config; 23 | }; 24 | -------------------------------------------------------------------------------- /lottery-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lottery-react", 3 | "version": "2.0.2", 4 | "description": "Lottery React App", 5 | "private": true, 6 | "dependencies": { 7 | "@metamask/detect-provider": "^2.0.0", 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-scripts": "5.0.1", 14 | "web-vitals": "^2.1.4", 15 | "web3": "^1.8.2" 16 | }, 17 | "scripts": { 18 | "start": "react-app-rewired start", 19 | "build": "react-app-rewired build", 20 | "test": "react-app-rewired test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "assert": "^2.0.0", 43 | "buffer": "^6.0.3", 44 | "crypto-browserify": "^3.12.0", 45 | "https-browserify": "^1.0.0", 46 | "os-browserify": "^0.3.0", 47 | "process": "^0.11.10", 48 | "react-app-rewired": "^2.2.1", 49 | "stream-browserify": "^3.0.0", 50 | "stream-http": "^3.2.0", 51 | "url": "^0.11.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lottery-react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owanhunte/ethereum-solidity-course-updated-code/ca00b11721ccd0074f3aa5f4c508d4607f139d5f/lottery-react/public/favicon.ico -------------------------------------------------------------------------------- /lottery-react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Play the Lottery! 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /lottery-react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owanhunte/ethereum-solidity-course-updated-code/ca00b11721ccd0074f3aa5f4c508d4607f139d5f/lottery-react/public/logo192.png -------------------------------------------------------------------------------- /lottery-react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owanhunte/ethereum-solidity-course-updated-code/ca00b11721ccd0074f3aa5f4c508d4607f139d5f/lottery-react/public/logo512.png -------------------------------------------------------------------------------- /lottery-react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Lottery App", 3 | "name": "Lottery React App", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /lottery-react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /lottery-react/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | text-align: center; 6 | width: 100%; 7 | } 8 | 9 | .test-site-msg { 10 | background-color: rgb(245, 245, 220); 11 | border-bottom: 1px solid rgb(221, 221, 221); 12 | font-size: 13px; 13 | line-height: 1.5; 14 | padding: 10px 18px; 15 | text-align: left; 16 | } 17 | 18 | .page-center { 19 | display: flex; 20 | flex: 1 1 0%; 21 | align-items: center; 22 | justify-content: center; 23 | padding: 20px 0; 24 | } 25 | 26 | .alert { 27 | border: 1px solid transparent; 28 | border-radius: 2px; 29 | color: rgb(47, 47, 47); 30 | padding: 23px 30px 25px; 31 | font-size: 1rem; 32 | max-width: 500px; 33 | } 34 | 35 | .info { 36 | border-color: rgb(206, 206, 206); 37 | background-color: rgb(190, 227, 248); 38 | } 39 | 40 | .error { 41 | background-color: rgb(254, 215, 215); 42 | border-color: rgb(165, 42, 42); 43 | } 44 | 45 | .no-margin { 46 | margin: 0; 47 | } 48 | 49 | .no-margin-top { 50 | margin-top: 0; 51 | } 52 | 53 | .center { 54 | text-align: center; 55 | } 56 | 57 | hr.spacey { 58 | margin: 22px 0 15px; 59 | } 60 | 61 | .btn { 62 | border: none; 63 | border-radius: 4px; 64 | color: rgb(255, 255, 255); 65 | padding: 13px 32px; 66 | text-align: center; 67 | text-decoration: none; 68 | display: inline-block; 69 | font-size: 16px; 70 | box-shadow: 0 2px 4px -1px rgb(0 0 0 / 20%), 0 4px 5px 0 rgb(0 0 0 / 14%), 0 1px 10px 0 rgb(0 0 0 / 12%); 71 | cursor: pointer; 72 | } 73 | 74 | .btn[disabled] { 75 | cursor: default; 76 | opacity: 0.5; 77 | } 78 | 79 | .primaryBtn { 80 | background-color: rgb(3, 125, 214); 81 | } 82 | 83 | .card { 84 | background-color: rgb(247, 247, 247); 85 | border: 1px solid #cecece; 86 | border-radius: 4px; 87 | padding: 25px 30px 30px; 88 | max-width: 768px; 89 | text-align: left; 90 | } 91 | 92 | h1 { 93 | text-align: center; 94 | } 95 | 96 | form { 97 | padding-bottom: 16px; 98 | } 99 | 100 | form div { 101 | display: flex; 102 | flex-direction: row; 103 | align-items: center; 104 | } 105 | 106 | form div * { 107 | display: inline-block; 108 | margin-right: 15px; 109 | } 110 | 111 | form div *:last-child { 112 | margin-right: 0; 113 | } 114 | 115 | form input { 116 | border: 1px solid #cecece; 117 | border-radius: 2px; 118 | height: 40px; 119 | width: 300px; 120 | } 121 | -------------------------------------------------------------------------------- /lottery-react/src/App.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import initWeb3 from "./utils/web3"; 3 | import { abi, contractAddress } from "./utils/lottery"; 4 | import "./App.css"; 5 | 6 | const { ethereum } = window; 7 | 8 | function App() { 9 | const lotteryContract = useRef(null); 10 | const [web3, setWeb3] = useState(null); 11 | const [doneCheckingForMetaMask, setDoneCheckingForMetaMask] = useState(false); 12 | const [connected, setConnected] = useState(false); 13 | const [connecting, setConnecting] = useState(false); 14 | const [isGoerliTestnet, setIsGoerliTestnet] = useState(false); 15 | 16 | const [manager, setManager] = useState(""); 17 | const [players, setPlayers] = useState([]); 18 | const [balance, setBalance] = useState(""); 19 | const [value, setValue] = useState(""); 20 | const [message, setMessage] = useState(""); 21 | 22 | const [enteringLottery, setEnteringLottery] = useState(false); 23 | const [pickingWinner, setPickingWinner] = useState(false); 24 | 25 | useEffect(() => { 26 | let cancelled = false; 27 | 28 | async function initWeb3WithProvider() { 29 | if (web3 === null) { 30 | if (!cancelled) { 31 | setDoneCheckingForMetaMask(false); 32 | const web3Instance = await initWeb3(); 33 | setWeb3(web3Instance); 34 | 35 | // Transactions done in this app must be done on the Goerli test network. 36 | const chainId = await ethereum.request({ method: "eth_chainId" }); 37 | if (chainId === "0x5") { 38 | setIsGoerliTestnet(true); 39 | } 40 | 41 | setDoneCheckingForMetaMask(true); 42 | 43 | if (web3Instance !== null) { 44 | // Create Contract JS object. 45 | lotteryContract.current = new web3Instance.eth.Contract(abi, contractAddress); 46 | 47 | // Check to see if user is already connected. 48 | try { 49 | const accounts = await ethereum.request({ method: "eth_accounts" }); 50 | if (accounts.length > 0 && ethereum.isConnected()) { 51 | setConnected(true); 52 | } 53 | } catch (error) { 54 | console.error(error); 55 | } 56 | 57 | // Implement `accountsChanged` event handler. 58 | ethereum.on("accountsChanged", handleAccountsChanged); 59 | } 60 | } 61 | } 62 | } 63 | 64 | initWeb3WithProvider(); 65 | 66 | return () => { 67 | cancelled = true; 68 | }; 69 | // eslint-disable-next-line react-hooks/exhaustive-deps 70 | }, []); 71 | 72 | useEffect(() => { 73 | let cancelled = false; 74 | 75 | if (connected) { 76 | async function handler() { 77 | const manager = await lotteryContract.current.methods.manager().call(); 78 | if (!cancelled) { 79 | setManager(manager); 80 | await updatePlayersListAndBalance(); 81 | } 82 | } 83 | handler(); 84 | } 85 | 86 | return () => { 87 | cancelled = true; 88 | }; 89 | // eslint-disable-next-line react-hooks/exhaustive-deps 90 | }, [connected]); 91 | 92 | const getAccount = async _event => { 93 | setConnecting(true); 94 | try { 95 | await ethereum.request({ method: "eth_requestAccounts" }); 96 | } catch (error) {} 97 | setConnecting(false); 98 | }; 99 | 100 | const handleAccountsChanged = _accounts => { 101 | window.location.reload(); 102 | }; 103 | 104 | /** 105 | * Define a function to update players list and balance in the page view 106 | * without the user having to perform a manual page reload. 107 | */ 108 | const updatePlayersListAndBalance = async () => { 109 | const players = await lotteryContract.current.methods.getPlayers().call(); 110 | const balance = await web3.eth.getBalance(lotteryContract.current.options.address); 111 | setPlayers(players); 112 | setBalance(balance); 113 | }; 114 | 115 | const onSubmit = async event => { 116 | event.preventDefault(); 117 | setEnteringLottery(true); 118 | 119 | if (value < "0.01") { 120 | showMessage("The minimum entry fee for the lottery is 0.01 ether."); 121 | } else { 122 | const accounts = await web3.eth.getAccounts(); 123 | showMessage("Attempting to enter you into the lottery..."); 124 | 125 | try { 126 | await lotteryContract.current.methods.enter().send({ 127 | from: accounts[0], 128 | value: web3.utils.toWei(value, "ether") 129 | }); 130 | 131 | showMessage("You have been entered!"); 132 | updatePlayersListAndBalance(); 133 | } catch (error) { 134 | switch (error.code) { 135 | case 4001: 136 | showMessage("You declined to enter the lottery."); 137 | break; 138 | default: 139 | showMessage( 140 | "An error occurred while attempting to enter you in the lottery. Don't worry, no funds were sent from your Goerli wallet." 141 | ); 142 | break; 143 | } 144 | } 145 | } 146 | setEnteringLottery(false); 147 | }; 148 | 149 | const pickWinner = async event => { 150 | event.preventDefault(); 151 | setPickingWinner(true); 152 | const accounts = await web3.eth.getAccounts(); 153 | showMessage("Attempting to pick a winner..."); 154 | try { 155 | await lotteryContract.current.methods.pickWinner().send({ 156 | from: accounts[0] 157 | }); 158 | 159 | showMessage("A winner has been picked!"); 160 | updatePlayersListAndBalance(); 161 | } catch (error) { 162 | switch (error.code) { 163 | case 4001: 164 | showMessage("You did not pick a winner."); 165 | break; 166 | default: 167 | showMessage("An error occurred while attempting to pick a winner."); 168 | break; 169 | } 170 | } 171 | setPickingWinner(false); 172 | }; 173 | 174 | const showMessage = msg => { 175 | setMessage(msg); 176 | }; 177 | 178 | return ( 179 |
180 | {/* This alert can be removed if you're not planning on doing a deployment of this app. */} 181 |
182 | This is a test-only deployment of the{" "} 183 | 184 | Lottery React app 185 | 186 | , based on the udemy.com course{" "} 187 | 188 | Ethereum and Solidity: The Complete Developer's Guide 189 | 190 | . Ether transactions from this app run on the Goerli test network only. 191 |
192 | 193 | {web3 === null && !doneCheckingForMetaMask && ( 194 |
195 |
196 |

Lottery Contract

197 |

Checking for MetaMask Ethereum Provider...

198 |
199 |
200 | )} 201 | 202 | {web3 === null && doneCheckingForMetaMask && ( 203 |
204 |
205 |

Lottery Contract

206 |

207 | MetaMask is required to run this app! Please install MetaMask and then refresh this page. 208 |

209 |
210 |
211 | )} 212 | 213 | {web3 !== null && doneCheckingForMetaMask && !isGoerliTestnet && ( 214 |
215 |
216 |

Lottery Contract

217 |

218 | You must be connected to the Goerli test network for Ether transactions made via this 219 | app. 220 |

221 |
222 |
223 | )} 224 | 225 | {web3 !== null && !connected && isGoerliTestnet && ( 226 |
227 |
228 |

Lottery Contract

229 |

Want to try your luck in the lottery? Connect with MetaMask and start competing right away!

230 |
231 | 234 |
235 |
236 |
237 | )} 238 | 239 | {web3 !== null && connected && isGoerliTestnet && ( 240 |
241 |
242 |

Lottery Contract

243 |

244 | This contract is managed by {manager}. 245 | {players.length === 1 246 | ? ` There is currently ${players.length} person entered, ` 247 | : ` There are currently ${players.length} people entered, `} 248 | competing to win {web3.utils.fromWei(balance, "ether")} ether! 249 |

250 | 251 |
252 | 253 |
254 |

Want to try your luck?

255 |
256 | {" "} 257 | setValue(event.target.value)} />{" "} 258 | 261 |
262 |
263 | 264 | {manager.toLowerCase() === ethereum.selectedAddress && ( 265 | <> 266 |
267 | 268 |

Ready to pick a winner?

269 | 272 | 273 | )} 274 | 275 |
276 | 277 |

{message}

278 |
279 |
280 | )} 281 |
282 | ); 283 | } 284 | 285 | export default App; 286 | -------------------------------------------------------------------------------- /lottery-react/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import App from "./App"; 3 | 4 | test("renders app heading", () => { 5 | render(); 6 | const headingElement = screen.getByText(/Lottery Contract/i); 7 | expect(headingElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /lottery-react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: rgb(255, 255, 255); 3 | color: rgb(47, 47, 47); 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", 6 | "Droid Sans", "Helvetica Neue", sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 13 | } 14 | 15 | p { 16 | margin-bottom: 30px; 17 | margin-top: 25px; 18 | } 19 | 20 | * { 21 | box-sizing: border-box; 22 | } 23 | 24 | hr { 25 | border-color: rgb(217, 217, 217); 26 | border-style: solid; 27 | } 28 | -------------------------------------------------------------------------------- /lottery-react/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /lottery-react/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lottery-react/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /lottery-react/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /lottery-react/src/utils/lottery.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * The following contract address and ABI are for a deployment of the Lottery contract made to the 3 | * Goerli Ethereum Test Network. The contract itself was compiled using version 0.8.18 of the Solidity 4 | * compiler. 5 | * 6 | * NOTE: Update the contractAddress and abi variables to those for your own deployed contract. 7 | */ 8 | 9 | export const contractAddress = "0x900ff505fF412551463Ae92E625f8fA546E3f32e"; 10 | 11 | export const abi = [ 12 | { 13 | inputs: [], 14 | stateMutability: "nonpayable", 15 | type: "constructor" 16 | }, 17 | { 18 | inputs: [], 19 | name: "enter", 20 | outputs: [], 21 | stateMutability: "payable", 22 | type: "function" 23 | }, 24 | { 25 | inputs: [], 26 | name: "getPlayers", 27 | outputs: [ 28 | { 29 | internalType: "address payable[]", 30 | name: "", 31 | type: "address[]" 32 | } 33 | ], 34 | stateMutability: "view", 35 | type: "function" 36 | }, 37 | { 38 | inputs: [], 39 | name: "manager", 40 | outputs: [ 41 | { 42 | internalType: "address", 43 | name: "", 44 | type: "address" 45 | } 46 | ], 47 | stateMutability: "view", 48 | type: "function" 49 | }, 50 | { 51 | inputs: [], 52 | name: "pickWinner", 53 | outputs: [], 54 | stateMutability: "nonpayable", 55 | type: "function" 56 | }, 57 | { 58 | inputs: [ 59 | { 60 | internalType: "uint256", 61 | name: "", 62 | type: "uint256" 63 | } 64 | ], 65 | name: "players", 66 | outputs: [ 67 | { 68 | internalType: "address payable", 69 | name: "", 70 | type: "address" 71 | } 72 | ], 73 | stateMutability: "view", 74 | type: "function" 75 | } 76 | ]; 77 | -------------------------------------------------------------------------------- /lottery-react/src/utils/web3.js: -------------------------------------------------------------------------------- 1 | import detectEthereumProvider from "@metamask/detect-provider"; 2 | import Web3 from "web3"; 3 | 4 | /** 5 | * IMPORTANT!! My implementation here is somewhat different from what is given 6 | * in the course code. Students attempting to learn from my solution should 7 | * take note of the following: 8 | * 9 | * 1) I'm making use of the @metamask/detect-provider utility for detecting 10 | * the MetaMask Ethereum provider and enforcing that MetaMask be used, as 11 | * opposed to some other ethereum-compatible browser (via options.mustBeMetaMask). 12 | * 13 | * See https://docs.metamask.io/guide/ethereum-provider.html#using-the-provider 14 | * for more details on using the Ethereum Provider API. 15 | * 16 | * 2) Even if MetaMask is installed and the Ethereum provider detected, App.js 17 | * enforces a requirement that the user MUST be on the Goerli testnet network. 18 | * 19 | * 3) Unlike the approach the course takes where it automatically initiates a 20 | * connection request to access the user's Ethereum account(s) when the app 21 | * is loaded, I take the recommended approach to only initiate a connection 22 | * request in response to direct user action, such as clicking a button. 23 | * 24 | * See https://docs.metamask.io/guide/getting-started.html#connecting-to-metamask 25 | * 26 | * 4) MetaMask provides the Ethereum Provider API (window.ethereum) for developers 27 | * to work with. Note that in January 2021 the former window.web3 API was 28 | * removed in favor of the window.ethereum API. 29 | * 30 | * 5) This file exports an async function instead of an actual web3 reference to 31 | * allow the web3 instance to be created asynchronously and stored in React state. 32 | */ 33 | const initWeb3 = async () => { 34 | let web3 = null; 35 | 36 | // Get the provider, or null if it couldn't be detected. 37 | const provider = await detectEthereumProvider({ 38 | mustBeMetaMask: true 39 | }); 40 | 41 | if (provider) { 42 | console.log("MetaMask Ethereum provider successfully detected!"); 43 | 44 | const { ethereum } = window; 45 | web3 = new Web3(provider); 46 | 47 | // Reload the page when the currently connected chain changes. 48 | ethereum.on("chainChanged", _chainId => { 49 | window.location.reload(); 50 | }); 51 | 52 | ethereum.on("disconnect", _error => { 53 | window.location.reload(); 54 | }); 55 | 56 | // Code to initiate connection request to user's Ethereum account(s) moved 57 | // to `src/App.js`, and only run in response to direct user action. 58 | } else { 59 | console.log("Please install MetaMask!"); 60 | } 61 | 62 | return web3; 63 | }; 64 | 65 | export default initWeb3; 66 | -------------------------------------------------------------------------------- /lottery/.env.example: -------------------------------------------------------------------------------- 1 | ACCOUNT_MNEMONIC="" 2 | GOERLI_ENDPOINT="" -------------------------------------------------------------------------------- /lottery/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Logs 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # IDEs and editors (shamelessly copied from @angular/cli's .gitignore) 9 | /.idea 10 | .project 11 | .classpath 12 | .c9/ 13 | *.launch 14 | .settings/ 15 | *.sublime-workspace 16 | 17 | # IDE - VSCode 18 | .vscode/* 19 | !.vscode/settings.json 20 | !.vscode/tasks.json 21 | !.vscode/launch.json 22 | !.vscode/extensions.json 23 | 24 | ### Linux ### 25 | *~ 26 | 27 | # temporary files which can be created if a process still has a handle open of a deleted file 28 | .fuse_hidden* 29 | 30 | # KDE directory preferences 31 | .directory 32 | 33 | # Linux trash folder which might appear on any partition or disk 34 | .Trash-* 35 | 36 | # .nfs files are created when an open file is removed but is still being accessed 37 | .nfs* 38 | 39 | ### OSX ### 40 | *.DS_Store 41 | .AppleDouble 42 | .LSOverride 43 | 44 | # Icon must end with two \r 45 | Icon 46 | 47 | # Thumbnails 48 | ._* 49 | 50 | # Files that might appear in the root of a volume 51 | .DocumentRevisions-V100 52 | .fseventsd 53 | .Spotlight-V100 54 | .TemporaryItems 55 | .Trashes 56 | .VolumeIcon.icns 57 | .com.apple.timemachine.donotpresent 58 | 59 | # Directories potentially created on remote AFP share 60 | .AppleDB 61 | .AppleDesktop 62 | Network Trash Folder 63 | Temporary Items 64 | .apdisk 65 | 66 | ### Windows ### 67 | # Windows thumbnail cache files 68 | Thumbs.db 69 | ehthumbs.db 70 | ehthumbs_vista.db 71 | 72 | # Folder config file 73 | Desktop.ini 74 | 75 | # Recycle Bin used on file shares 76 | $RECYCLE.BIN/ 77 | 78 | # Environment file 79 | .env -------------------------------------------------------------------------------- /lottery/README.md: -------------------------------------------------------------------------------- 1 | # Up-to-date Lottery Smart Contract, Node.js Scripts & Unit Tests 2 | 3 | > Section 3 of the udemy.com course [Ethereum and Solidity: The Complete Developer's Guide](https://www.udemy.com/course/ethereum-and-solidity-the-complete-developers-guide/) by [Stephen Grider](https://www.udemy.com/user/sgslo/) implements a Lottery smart contract, along with a Node.js compile script, deploy script and unit tests for that contract. In this repo I provide up-to-date equivalents (along with detailed explanations) for each of these, for the benefit of students who have enrolled in Stephen's course. 4 | 5 | ## Contents 6 | 7 | - [Lottery smart contract](#lottery-smart-contract) 8 | - [Compile Script](#compile-script) 9 | - [Update your package.json](#update-your-packagejson) 10 | - [Unit Tests](#unit-tests) 11 | - [Deploy Script](#deploy-script) 12 | 13 |


14 | 15 | ## Lottery Smart Contract 16 | 17 | This is the second smart contract implemented in the udemy.com course and whereas the first contract (`Inbox`) was extremely simple, the `Lottery` contract is more complex. It introduces some of Solidity's more advanced concepts which you should ensure you understand thoroughly if you want to be on your way to mastering smart contract development. Also note that in order to bring the Lottery contract's code up-to-date, the changes I made from what is implemented in the course are significant. 18 | 19 | You can find my version of the Lottery smart contract [here](./contracts/Lottery.sol) and it is shown immediately below. For frictionless readability, I've kept the comments alongside the source code, since they provide clear explanations for each part of the code in a seamless way. 20 | 21 | ```solidity 22 | // SPDX-License-Identifier: GPL-3.0-or-later 23 | pragma solidity >=0.5.0 <0.9.0; 24 | 25 | contract Lottery { 26 | // As of Solidity 0.5.0 the `address` type was split into `address` and 27 | // `address payable`, where only `address payable` provides the transfer 28 | // function. We therefore need to explicity use the `address payable[]` 29 | // array type for the players array. 30 | address public manager; 31 | address payable[] public players; 32 | 33 | // As of Solidity 0.5.0 constructors must be defined using the `constructor` 34 | // keyword. See https://docs.soliditylang.org/en/latest/050-breaking-changes.html#constructors 35 | // 36 | // As of Solidity 0.7.0 visibility (public / internal) is not needed for 37 | // constructors anymore. To prevent a contract from being created, 38 | // it can be marked abstract. 39 | constructor() { 40 | manager = msg.sender; 41 | } 42 | 43 | function enter() public payable { 44 | // Note: Although optional, it's a good practice to include error messages 45 | // in `require` calls. 46 | require(msg.value > .01 ether, "A minimum payment of .01 ether must be sent to enter the lottery"); 47 | 48 | // As of Solidity 0.8.0 the global variable `msg.sender` has the type 49 | // `address` instead of `address payable`. So we must convert msg.sender 50 | // into `address payable` before we can add it to the players array. 51 | players.push(payable(msg.sender)); 52 | } 53 | 54 | function random() private view returns (uint256) { 55 | // For an explanation of why `abi.encodePacked` is used here, see 56 | // https://github.com/owanhunte/ethereum-solidity-course-updated-code/issues/1 57 | // 58 | // VM Paris Update: Note that in September 2022, Ethereum transitioned to proof-of-stake consensus, 59 | // also known as The Merge. One of the changes this resulted in was the introduction of a new 60 | // opcode called 'prevrandao', which replaces the 'difficulty' opcode. As such, since Solidity 0.8.18 61 | // block.difficulty has been deprecated and instead, block.prevrandao should be used. 62 | return uint256(keccak256(abi.encodePacked(block.prevrandao, block.number, players))); 63 | } 64 | 65 | function pickWinner() public onlyOwner { 66 | uint256 index = random() % players.length; 67 | 68 | // As of Solidity 0.4.24 at least, `this` is a deprecated way to get the address of the 69 | // contract. `address(this)` must be used instead. 70 | address contractAddress = address(this); 71 | 72 | players[index].transfer(contractAddress.balance); 73 | players = new address payable[](0); 74 | } 75 | 76 | function getPlayers() public view returns (address payable[] memory) { 77 | return players; 78 | } 79 | 80 | modifier onlyOwner() { 81 | require(msg.sender == manager, "Only owner can call this function."); 82 | _; 83 | } 84 | } 85 | ``` 86 | 87 | If you haven't yet seen my explanations of the SPDX license identifier and pragma lines I'll explain them briefly now: 88 | 89 | ```solidity 90 | // SPDX-License-Identifier: GPL-3.0-or-later 91 | ``` 92 | 93 | is an [SPDX license identifier](https://docs.soliditylang.org/en/latest/layout-of-source-files.html?highlight=spdx#spdx-license-identifier), _introduced from Solidity 0.6.8_, which allows developers to specify the [license](https://spdx.org/licenses) the smart contract uses. **Every Solidity source file should start with a comment indicating its license** and it should be one of the identifiers listed at https://spdx.org/licenses. In this case I've specified that the smart contract uses the [GNU General Public License v3.0 or later](https://spdx.org/licenses/GPL-3.0-or-later.html) 94 | 95 | The line: 96 | 97 | ```solidity 98 | pragma solidity >=0.5.0 <0.9.0; 99 | ``` 100 | 101 | specifies that this smart contract's source code is written for Solidity version 0.5.0 up to, but not including version 0.9.0. In the udemy.com course, the author uses a version pragma of ^0.4.17, so _the course's version of Lottery will not compile on a Solidity compiler earlier than version 0.4.17 nor will it compile on a compiler starting from version 0.5.0_. 102 | 103 |


104 | 105 | ## Compile Script 106 | 107 | An up-to-date equivalent to the course's compile script for the Lottery contract can be found [here](./compile.js) and is shown immediately below: 108 | 109 | ```js 110 | const path = require("path"); 111 | const fs = require("fs"); 112 | const solc = require("solc"); 113 | 114 | const lotteryPath = path.resolve(__dirname, "contracts", "Lottery.sol"); 115 | const source = fs.readFileSync(lotteryPath, "utf8"); 116 | 117 | /*** 118 | * The recommended way to interface with the Solidity compiler, especially for more 119 | * complex and automated setups is the so-called JSON-input-output interface. 120 | * 121 | * See https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-input-and-output-json-description 122 | * for more details. 123 | */ 124 | const input = { 125 | language: "Solidity", 126 | sources: { 127 | // Each Solidity source file to be compiled must be specified by defining either 128 | // a URL to the file or the literal file content. 129 | // See https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description 130 | "Lottery.sol": { 131 | content: source 132 | } 133 | }, 134 | settings: { 135 | metadata: { 136 | useLiteralContent: true 137 | }, 138 | outputSelection: { 139 | "*": { 140 | "*": ["*"] 141 | } 142 | } 143 | } 144 | }; 145 | 146 | const output = JSON.parse(solc.compile(JSON.stringify(input))); 147 | 148 | module.exports = output.contracts["Lottery.sol"].Lottery; 149 | ``` 150 | 151 | ### Compiler Input and Output JSON Description 152 | 153 | The recommended way to interface with the Solidity compiler, especially when developing more complex and automated setups is the so-called JSON-input-output interface. In summary, the compiler API expects a JSON formatted input and outputs the compilation result in a JSON formatted output. For details on this approach, including thorough descriptions of the input and output formats, check out the Solidity docs [here](https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-input-and-output-json-description). 154 | 155 | In our `compile.js` above, we create a JavaScript object representation of the input that will be passed to the compiler after it's JSON stringified. In this object we define the single Solidity source file that has to be compiled, `Lottery.sol`, passing the fully loaded source code of the contract as the content source of the contract. 156 | 157 | The lines 158 | 159 | ```js 160 | const output = JSON.parse(solc.compile(JSON.stringify(input))); 161 | 162 | module.exports = output.contracts["Lottery.sol"].Lottery; 163 | ``` 164 | 165 | parse the output returned by the call to `solc.compile(...)` and store it in the `output` variable. We then extract the `Lottery` contract object only and set that as the only export from `compile.js`. 166 | 167 |


168 | 169 | ## Update your package.json 170 | 171 | Before I get into the unit tests and deploy script, I think it's important to first explain the updates that need to be made to this project's [package.json](./package.json), specificially the dependencies being used. **All** of the dependencies should be updated to their latest versions and the `ganache-cli` dependency replaced with `ganache`, since the `ganache-cli` package has been deprecated and is now just `ganache` (as explained [here](https://github.com/trufflesuite/ganache/blob/develop/UPGRADE-GUIDE.md)). 172 | 173 | Here is what my package.json looks like: 174 | 175 | ```json 176 | { 177 | "name": "lottery", 178 | "version": "2.0.2", 179 | "description": "Lottery smart contract Node.js project.", 180 | "main": "compile.js", 181 | "scripts": { 182 | "test": "mocha" 183 | }, 184 | "author": "Owan Hunte", 185 | "license": "ISC", 186 | "dependencies": { 187 | "@truffle/hdwallet-provider": "^2.1.6", 188 | "dotenv": "^16.0.3", 189 | "solc": "^0.8.18", 190 | "web3": "^1.8.2" 191 | }, 192 | "devDependencies": { 193 | "ganache": "^7.7.4", 194 | "mocha": "^10.2.0" 195 | } 196 | } 197 | ``` 198 | 199 | Note that I installed the `ganache` and `mocha` packages as development dependencies since both are only used in the [unit tests](./test/Lottery.test.js). 200 | 201 |


202 | 203 | ## Unit Tests 204 | 205 | An up-to-date equivalent to the course's unit tests (`Lottery.test.js`) can be found [here](./test/Lottery.test.js) and is shown immediately below: 206 | 207 | ```js 208 | const assert = require("assert"); 209 | const ganache = require("ganache"); 210 | const Web3 = require("web3"); 211 | const provider = ganache.provider(); 212 | const web3 = new Web3(provider); 213 | const { abi, evm } = require("../compile"); 214 | const { enterPlayerInLottery } = require("../util"); 215 | 216 | let lottery; 217 | let accounts; 218 | 219 | beforeEach(async () => { 220 | // Get a list of all accounts. 221 | accounts = await web3.eth.getAccounts(); 222 | 223 | // Use one of those accounts to deploy the contract. 224 | lottery = await new web3.eth.Contract(abi) 225 | .deploy({ data: "0x" + evm.bytecode.object }) 226 | .send({ from: accounts[0], gas: "3000000" }); 227 | }); 228 | 229 | describe("Lottery Contract", () => { 230 | it("deploys a contract", () => { 231 | assert.ok(lottery.options.address); 232 | }); 233 | 234 | it("allows one account to enter", async () => { 235 | await enterPlayerInLottery(lottery, accounts[1], web3, "0.02"); 236 | 237 | const players = await lottery.methods.getPlayers().call({ 238 | from: accounts[0] 239 | }); 240 | 241 | assert.strictEqual(players[0], accounts[1]); 242 | assert.strictEqual(players.length, 1); 243 | }); 244 | 245 | it("allows multiple accounts to enter", async () => { 246 | await enterPlayerInLottery(lottery, accounts[1], web3, "0.02"); 247 | await enterPlayerInLottery(lottery, accounts[2], web3, "0.02"); 248 | await enterPlayerInLottery(lottery, accounts[3], web3, "0.02"); 249 | 250 | const players = await lottery.methods.getPlayers().call({ 251 | from: accounts[0] 252 | }); 253 | 254 | assert.strictEqual(players[0], accounts[1]); 255 | assert.strictEqual(players[1], accounts[2]); 256 | assert.strictEqual(players[2], accounts[3]); 257 | assert.strictEqual(players.length, 3); 258 | }); 259 | 260 | it("requires a minimum amount of ether to enter", async () => { 261 | try { 262 | await enterPlayerInLottery(lottery, accounts[4], web3, 0); 263 | assert(false); 264 | } catch (error) { 265 | assert(error); 266 | } 267 | }); 268 | 269 | it("only manager can call pickWinner", async () => { 270 | try { 271 | await lottery.methods.pickWinner().send({ 272 | from: accounts[1] 273 | }); 274 | assert(false); 275 | } catch (error) { 276 | assert(error); 277 | } 278 | }); 279 | 280 | it("sends money to the winner and resets the players array", async () => { 281 | await enterPlayerInLottery(lottery, accounts[1], web3, "2"); 282 | 283 | const initialBalance = await web3.eth.getBalance(accounts[1]); 284 | await lottery.methods.pickWinner().send({ 285 | from: accounts[0] 286 | }); 287 | const finalBalance = await web3.eth.getBalance(accounts[1]); 288 | const difference = finalBalance - initialBalance; 289 | 290 | assert(difference > web3.utils.toWei("1.8", "ether")); 291 | }); 292 | }); 293 | ``` 294 | 295 | There are 2 main changes happening with the above tests file. First we have the line: 296 | 297 | ```js 298 | const ganache = require("ganache"); 299 | ``` 300 | 301 | which replaces the line from the course's version that uses the now deprecated `ganache-cli`. 302 | 303 | Second, the line 304 | 305 | ```js 306 | const { abi, evm } = require("../compile");` 307 | ``` 308 | 309 | imports the compiled `Lottery` contract object that the compile script exports and stores the `abi` and `evm` object values as variables. The import line which the course has, `const { interface, bytecode } = require("../compile");`, will not work with the latest Solidity compiler versions. The `abi` object replaces the `interface` object, and we can access the contract's bytecode object via `evm.bytecode.object`, as shown above. 310 | 311 |


312 | 313 | ## Deploy Script 314 | 315 | An up-to-date equivalent to the course's deploy script for the Lottery contract can be found [here](./deploy.js) and is shown immediately below: 316 | 317 | ```js 318 | // Load environment variables. 319 | require("dotenv").config(); 320 | 321 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 322 | const Web3 = require("web3"); 323 | const { abi, evm } = require("./compile"); 324 | const mnemonicPhrase = process.env.ACCOUNT_MNEMONIC; 325 | const network = process.env.GOERLI_ENDPOINT; 326 | 327 | const provider = new HDWalletProvider({ 328 | mnemonic: { 329 | phrase: mnemonicPhrase 330 | }, 331 | providerOrUrl: network 332 | }); 333 | 334 | const web3 = new Web3(provider); 335 | 336 | const deploy = async () => { 337 | const accounts = await web3.eth.getAccounts(); 338 | console.log("Attempting to deploy from account", accounts[0]); 339 | 340 | const result = await new web3.eth.Contract(abi) 341 | .deploy({ data: "0x" + evm.bytecode.object }) 342 | .send({ from: accounts[0] }); 343 | 344 | console.log("Contract deployed to", result.options.address); 345 | provider.engine.stop(); 346 | }; 347 | 348 | deploy(); 349 | ``` 350 | 351 | The changes in this script from the course's version are as follows: 352 | 353 | - The line `const { abi, evm } = require("./compile");` replaces the `const { interface, bytecode } = require("./compile");` line that's found in the course example, and we access the bytecode object via `evm.bytecode.object`. 354 | - Instead of hard-coding the account mnemonic and Infura endpoint as is done in the course's deploy script, I'm storing and referencing these via environment variables. 355 | - A [Goerli Infura](https://app.infura.io) endpoint (stored in the `process.env.GOERLI_ENDPOINT` environment variable) is passed to `HDWalletProvider` instead of a Rinkeby endpoint since the Rinkeby network no longer exists. So when copying your endpoint from the Infura dashboard, remember to grab the Goerli Ethereum endpoint. 356 | - The `dotenv` package is used to read these environment variables from a `.env` file. Create that file locally in the root of your lottery folder and copy the contents of [`.env.example`](./.env.example) into your `.env` file. Set `ACCOUNT_MNEMONIC` and `GOERLI_ENDPOINT` in your `.env` file appropriately. **DO NOT use a mnemonic for an account/wallet with real money or Ether associated with it!** 357 | - To prevent the deployment from hanging, the statement `provider.engine.stop();` is added at the end of the `deploy` function definition. 358 | 359 | ## That's all for now 360 | 361 | That about covers things where the updates to the Lottery smart contract and Node.js project are concerned. As always, I sincerely hope my contributions prove useful to all students of the course who find their way to this repository. 362 | -------------------------------------------------------------------------------- /lottery/compile.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const solc = require("solc"); 4 | 5 | const lotteryPath = path.resolve(__dirname, "contracts", "Lottery.sol"); 6 | const source = fs.readFileSync(lotteryPath, "utf8"); 7 | 8 | /*** 9 | * The recommended way to interface with the Solidity compiler, especially for more 10 | * complex and automated setups is the so-called JSON-input-output interface. 11 | * 12 | * See https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-input-and-output-json-description 13 | * for more details. 14 | */ 15 | const input = { 16 | language: "Solidity", 17 | sources: { 18 | // Each Solidity source file to be compiled must be specified by defining either 19 | // a URL to the file or the literal file content. 20 | // See https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description 21 | "Lottery.sol": { 22 | content: source 23 | } 24 | }, 25 | settings: { 26 | metadata: { 27 | useLiteralContent: true 28 | }, 29 | outputSelection: { 30 | "*": { 31 | "*": ["*"] 32 | } 33 | } 34 | } 35 | }; 36 | 37 | const output = JSON.parse(solc.compile(JSON.stringify(input))); 38 | 39 | module.exports = output.contracts["Lottery.sol"].Lottery; 40 | -------------------------------------------------------------------------------- /lottery/contracts/Lottery.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity >=0.5.0 <0.9.0; 3 | 4 | contract Lottery { 5 | // As of Solidity 0.5.0 the `address` type was split into `address` and 6 | // `address payable`, where only `address payable` provides the transfer 7 | // function. We therefore need to explicity use the `address payable[]` 8 | // array type for the players array. 9 | address public manager; 10 | address payable[] public players; 11 | 12 | // As of Solidity 0.5.0 constructors must be defined using the `constructor` 13 | // keyword. See https://docs.soliditylang.org/en/latest/050-breaking-changes.html#constructors 14 | // 15 | // As of Solidity 0.7.0 visibility (public / internal) is not needed for 16 | // constructors anymore. To prevent a contract from being created, 17 | // it can be marked abstract. 18 | constructor() { 19 | manager = msg.sender; 20 | } 21 | 22 | function enter() public payable { 23 | // Note: Although optional, it's a good practice to include error messages 24 | // in `require` calls. 25 | require(msg.value >= .01 ether, "A minimum payment of .01 ether must be sent to enter the lottery"); 26 | 27 | // As of Solidity 0.8.0 the global variable `msg.sender` has the type 28 | // `address` instead of `address payable`. So we must convert msg.sender 29 | // into `address payable` before we can add it to the players array. 30 | players.push(payable(msg.sender)); 31 | } 32 | 33 | function random() private view returns (uint256) { 34 | // For an explanation of why `abi.encodePacked` is used here, see 35 | // https://github.com/owanhunte/ethereum-solidity-course-updated-code/issues/1 36 | // 37 | // VM Paris Update: Note that in September 2022, Ethereum transitioned to proof-of-stake consensus, 38 | // also known as The Merge. One of the changes this resulted in was the introduction of a new 39 | // opcode called 'prevrandao', which replaces the 'difficulty' opcode. As such, since Solidity 0.8.18 40 | // block.difficulty has been deprecated and instead, block.prevrandao should be used. 41 | return uint256(keccak256(abi.encodePacked(block.prevrandao, block.number, players))); 42 | } 43 | 44 | function pickWinner() public onlyOwner { 45 | uint256 index = random() % players.length; 46 | 47 | // As of Solidity 0.4.24 at least, `this` is a deprecated way to get the address of the 48 | // contract. `address(this)` must be used instead. 49 | address contractAddress = address(this); 50 | 51 | players[index].transfer(contractAddress.balance); 52 | players = new address payable[](0); 53 | } 54 | 55 | function getPlayers() public view returns (address payable[] memory) { 56 | return players; 57 | } 58 | 59 | modifier onlyOwner() { 60 | require(msg.sender == manager, "Only owner can call this function."); 61 | _; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lottery/deploy.js: -------------------------------------------------------------------------------- 1 | // Load environment variables. 2 | require("dotenv").config(); 3 | 4 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 5 | const Web3 = require("web3"); 6 | const { abi, evm } = require("./compile"); 7 | const mnemonicPhrase = process.env.ACCOUNT_MNEMONIC; 8 | const network = process.env.GOERLI_ENDPOINT; 9 | 10 | const provider = new HDWalletProvider({ 11 | mnemonic: { 12 | phrase: mnemonicPhrase 13 | }, 14 | providerOrUrl: network 15 | }); 16 | 17 | const web3 = new Web3(provider); 18 | 19 | const deploy = async () => { 20 | const accounts = await web3.eth.getAccounts(); 21 | console.log("Attempting to deploy from account", accounts[0]); 22 | 23 | const result = await new web3.eth.Contract(abi) 24 | .deploy({ data: "0x" + evm.bytecode.object }) 25 | .send({ from: accounts[0] }); 26 | 27 | console.log("Contract deployed to", result.options.address); 28 | provider.engine.stop(); 29 | }; 30 | 31 | deploy(); 32 | -------------------------------------------------------------------------------- /lottery/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lottery", 3 | "version": "2.0.2", 4 | "description": "Lottery smart contract Node.js project.", 5 | "main": "compile.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "author": "Owan Hunte", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@truffle/hdwallet-provider": "^2.1.6", 13 | "dotenv": "^16.0.3", 14 | "solc": "^0.8.18", 15 | "web3": "^1.8.2" 16 | }, 17 | "devDependencies": { 18 | "ganache": "^7.7.4", 19 | "mocha": "^10.2.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lottery/test/Lottery.test.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const ganache = require("ganache"); 3 | const Web3 = require("web3"); 4 | const provider = ganache.provider(); 5 | const web3 = new Web3(provider); 6 | const { abi, evm } = require("../compile"); 7 | const { enterPlayerInLottery } = require("../util"); 8 | 9 | let lottery; 10 | let accounts; 11 | 12 | beforeEach(async () => { 13 | // Get a list of all accounts. 14 | accounts = await web3.eth.getAccounts(); 15 | 16 | // Use one of those accounts to deploy the contract. 17 | lottery = await new web3.eth.Contract(abi) 18 | .deploy({ data: "0x" + evm.bytecode.object }) 19 | .send({ from: accounts[0], gas: "3000000" }); 20 | }); 21 | 22 | describe("Lottery Contract", () => { 23 | it("deploys a contract", () => { 24 | assert.ok(lottery.options.address); 25 | }); 26 | 27 | it("allows one account to enter", async () => { 28 | await enterPlayerInLottery(lottery, accounts[1], web3, "0.02"); 29 | 30 | const players = await lottery.methods.getPlayers().call({ 31 | from: accounts[0] 32 | }); 33 | 34 | assert.strictEqual(players[0], accounts[1]); 35 | assert.strictEqual(players.length, 1); 36 | }); 37 | 38 | it("allows multiple accounts to enter", async () => { 39 | await enterPlayerInLottery(lottery, accounts[1], web3, "0.02"); 40 | await enterPlayerInLottery(lottery, accounts[2], web3, "0.02"); 41 | await enterPlayerInLottery(lottery, accounts[3], web3, "0.02"); 42 | 43 | const players = await lottery.methods.getPlayers().call({ 44 | from: accounts[0] 45 | }); 46 | 47 | assert.strictEqual(players[0], accounts[1]); 48 | assert.strictEqual(players[1], accounts[2]); 49 | assert.strictEqual(players[2], accounts[3]); 50 | assert.strictEqual(players.length, 3); 51 | }); 52 | 53 | it("requires a minimum amount of ether to enter", async () => { 54 | try { 55 | await enterPlayerInLottery(lottery, accounts[4], web3, 0); 56 | assert(false); 57 | } catch (error) { 58 | assert(error); 59 | } 60 | }); 61 | 62 | it("only manager can call pickWinner", async () => { 63 | try { 64 | await lottery.methods.pickWinner().send({ 65 | from: accounts[1] 66 | }); 67 | assert(false); 68 | } catch (error) { 69 | assert(error); 70 | } 71 | }); 72 | 73 | it("sends money to the winner and resets the players array", async () => { 74 | await enterPlayerInLottery(lottery, accounts[1], web3, "2"); 75 | 76 | const initialBalance = await web3.eth.getBalance(accounts[1]); 77 | await lottery.methods.pickWinner().send({ 78 | from: accounts[0] 79 | }); 80 | const finalBalance = await web3.eth.getBalance(accounts[1]); 81 | const difference = finalBalance - initialBalance; 82 | 83 | assert(difference > web3.utils.toWei("1.8", "ether")); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /lottery/util.js: -------------------------------------------------------------------------------- 1 | async function enterPlayerInLottery( 2 | lotteryContract, 3 | playerAddress, 4 | web3Instance, 5 | etherAmount 6 | ) { 7 | await lotteryContract.methods.enter().send({ 8 | from: playerAddress, 9 | value: etherAmount ? web3Instance.utils.toWei(etherAmount, "ether") : 0 10 | }); 11 | } 12 | 13 | module.exports = { 14 | enterPlayerInLottery 15 | }; 16 | --------------------------------------------------------------------------------