├── LICENSE ├── README.md ├── hardhat-tutorial ├── .gitignore ├── README.md ├── constants │ └── index.js ├── contracts │ ├── CryptoDevToken.sol │ ├── Greeter.sol │ └── ICryptoDevs.sol ├── hardhat.config.js ├── package-lock.json ├── package.json ├── scripts │ ├── deploy.js │ └── sample-script.js └── test │ └── sample-test.js └── my-app ├── .eslintrc.json ├── .gitignore ├── README.md ├── constants └── index.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── api │ └── hello.js └── index.js ├── public ├── 0.svg ├── favicon.ico └── vercel.svg └── styles ├── Home.module.css └── globals.css /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 LearnWeb3 DAO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Launch your own Initial Coin Offering 2 | 3 | Now it's time for you to launch a token for `Crypto Devs`. Let's call the token Crypto Dev Token. 4 | 5 | ![](https://i.imgur.com/78uY3Mm.png) 6 | 7 | --- 8 | ## Prefer a Video? 9 | If you would rather learn from a video, we have a recording available of this tutorial on our YouTube. Watch the video by clicking on the screenshot below, or go ahead and read the tutorial! 10 | [![ICO dApp Part-1](https://i.imgur.com/RbQWNQ1.png)](https://www.youtube.com/watch?v=qywahxzavkw "ICO dApp Tutorial") 11 | [![ICO dApp Part-2](https://i.imgur.com/frmWOa2.png)](https://www.youtube.com/watch?v=faIuZW5zCi8 "ICO dApp Tutorial") 12 | 13 | Note: The video tutorial is based on Rinkeby testnet, but the text tutorial is based on Goerli for future support. Please use the Goerli network. 14 | 15 | ## Build 16 | 17 | ## Requirements 18 | 19 | - There should be a max of `10,000 CD` tokens. 20 | - Every `Crypto Dev` NFT holder should get 10 tokens for free but they would have to pay the gas fees. 21 | - The price of one CD at the time of ICO should be `0.001 ether`. 22 | - There should be a website that users can visit for the ICO. 23 | 24 | Let's start building 🚀 25 | 26 | 27 | ## Prerequisites 28 | 29 | - You must have completed the [NFT-Collection tutorial](https://github.com/LearnWeb3DAO/NFT-Collection). 30 | 31 | ## Theory 32 | 33 | - What is an ERC20? 34 | - ERC-20 is a technical standard; it is used for all smart contracts on the Ethereum blockchain for token implementation and provides a list of rules that all Ethereum-based tokens must follow. 35 | - Please look at all the ERC20 [functions](https://docs.openzeppelin.com/contracts/2.x/api/token/erc20) before moving ahead. 36 | 37 | ## Build 38 | 39 | ### Smart Contract 40 | 41 | To build the smart contract we will be using [Hardhat](https://hardhat.org/). Hardhat is an Ethereum development environment and framework designed for full stack development in Solidity. In simple words you can write your smart contracts, deploy them, run tests, and debug your code. 42 | 43 | - To setup a Hardhat project, open up a terminal and execute these commands: 44 | 45 | ```bash 46 | mkdir ICO 47 | cd ICO 48 | mkdir hardhat-tutorial 49 | cd hardhat-tutorial 50 | npm init --yes 51 | npm install --save-dev hardhat 52 | ``` 53 | - In the same directory where you installed Hardhat run: 54 | 55 | ```bash 56 | npx hardhat 57 | ``` 58 | 59 | - Select `Create a Javascript project` 60 | - Press enter for the already specified `Hardhat Project root` 61 | - Press enter for the question on if you want to add a `.gitignore` 62 | - Press enter for `Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)?` 63 | 64 | Now you have a hardhat project ready to go! 65 | 66 | If you are on Windows, please do this extra step and install these libraries as well :) 67 | 68 | ```bash 69 | npm install --save-dev @nomicfoundation/hardhat-toolbox 70 | ``` 71 | 72 | - In the same terminal, install `@openzeppelin/contracts` as we will be importing [Openzeppelin's ERC20 Contract](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol) and [Openzeppelin's Ownable Contract](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol) in our `CryptoDevToken` contract. 73 | 74 | ```bash 75 | npm install @openzeppelin/contracts 76 | ``` 77 | 78 | - We need to call the `CryptoDevs Contract` that you deployed for the previous level to check for owners of CryptoDev NFT's. As we only need to call `tokenOfOwnerByIndex` and `balanceOf` methods, we can create an interface for `CryptoDevs contract` with only these two functions. This way we save `gas` as we do not need to inherit and deploy the entire `CryptoDevs Contract`, but only a part of it. 79 | 80 | - Create a new file inside the `contracts` directory and call it `ICryptoDevs.sol`. Add the following lines: 81 | 82 | ```go 83 | // SPDX-License-Identifier: MIT 84 | pragma solidity ^0.8.0; 85 | 86 | interface ICryptoDevs { 87 | /** 88 | * @dev Returns a token ID owned by `owner` at a given `index` of its token list. 89 | * Use along with {balanceOf} to enumerate all of ``owner``'s tokens. 90 | */ 91 | function tokenOfOwnerByIndex(address owner, uint256 index) 92 | external 93 | view 94 | returns (uint256 tokenId); 95 | 96 | /** 97 | * @dev Returns the number of tokens in ``owner``'s account. 98 | */ 99 | function balanceOf(address owner) external view returns (uint256 balance); 100 | } 101 | 102 | ``` 103 | 104 | - Create another file inside the `contracts` directory and call it `CryptoDevToken.sol`. Add the following lines: 105 | 106 | ```go 107 | // SPDX-License-Identifier: MIT 108 | pragma solidity ^0.8.0; 109 | 110 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 111 | import "@openzeppelin/contracts/access/Ownable.sol"; 112 | import "./ICryptoDevs.sol"; 113 | 114 | contract CryptoDevToken is ERC20, Ownable { 115 | // Price of one Crypto Dev token 116 | uint256 public constant tokenPrice = 0.001 ether; 117 | // Each NFT would give the user 10 tokens 118 | // It needs to be represented as 10 * (10 ** 18) as ERC20 tokens are represented by the smallest denomination possible for the token 119 | // By default, ERC20 tokens have the smallest denomination of 10^(-18). This means, having a balance of (1) 120 | // is actually equal to (10 ^ -18) tokens. 121 | // Owning 1 full token is equivalent to owning (10^18) tokens when you account for the decimal places. 122 | // More information on this can be found in the Freshman Track Cryptocurrency tutorial. 123 | uint256 public constant tokensPerNFT = 10 * 10**18; 124 | // the max total supply is 10000 for Crypto Dev Tokens 125 | uint256 public constant maxTotalSupply = 10000 * 10**18; 126 | // CryptoDevsNFT contract instance 127 | ICryptoDevs CryptoDevsNFT; 128 | // Mapping to keep track of which tokenIds have been claimed 129 | mapping(uint256 => bool) public tokenIdsClaimed; 130 | 131 | constructor(address _cryptoDevsContract) ERC20("Crypto Dev Token", "CD") { 132 | CryptoDevsNFT = ICryptoDevs(_cryptoDevsContract); 133 | } 134 | 135 | /** 136 | * @dev Mints `amount` number of CryptoDevTokens 137 | * Requirements: 138 | * - `msg.value` should be equal or greater than the tokenPrice * amount 139 | */ 140 | function mint(uint256 amount) public payable { 141 | // the value of ether that should be equal or greater than tokenPrice * amount; 142 | uint256 _requiredAmount = tokenPrice * amount; 143 | require(msg.value >= _requiredAmount, "Ether sent is incorrect"); 144 | // total tokens + amount <= 10000, otherwise revert the transaction 145 | uint256 amountWithDecimals = amount * 10**18; 146 | require( 147 | (totalSupply() + amountWithDecimals) <= maxTotalSupply, 148 | "Exceeds the max total supply available." 149 | ); 150 | // call the internal function from Openzeppelin's ERC20 contract 151 | _mint(msg.sender, amountWithDecimals); 152 | } 153 | 154 | /** 155 | * @dev Mints tokens based on the number of NFT's held by the sender 156 | * Requirements: 157 | * balance of Crypto Dev NFT's owned by the sender should be greater than 0 158 | * Tokens should have not been claimed for all the NFTs owned by the sender 159 | */ 160 | function claim() public { 161 | address sender = msg.sender; 162 | // Get the number of CryptoDev NFT's held by a given sender address 163 | uint256 balance = CryptoDevsNFT.balanceOf(sender); 164 | // If the balance is zero, revert the transaction 165 | require(balance > 0, "You dont own any Crypto Dev NFT's"); 166 | // amount keeps track of number of unclaimed tokenIds 167 | uint256 amount = 0; 168 | // loop over the balance and get the token ID owned by `sender` at a given `index` of its token list. 169 | for (uint256 i = 0; i < balance; i++) { 170 | uint256 tokenId = CryptoDevsNFT.tokenOfOwnerByIndex(sender, i); 171 | // if the tokenId has not been claimed, increase the amount 172 | if (!tokenIdsClaimed[tokenId]) { 173 | amount += 1; 174 | tokenIdsClaimed[tokenId] = true; 175 | } 176 | } 177 | // If all the token Ids have been claimed, revert the transaction; 178 | require(amount > 0, "You have already claimed all the tokens"); 179 | // call the internal function from Openzeppelin's ERC20 contract 180 | // Mint (amount * 10) tokens for each NFT 181 | _mint(msg.sender, amount * tokensPerNFT); 182 | } 183 | 184 | /** 185 | * @dev withdraws all ETH and tokens sent to the contract 186 | * Requirements: 187 | * wallet connected must be owner's address 188 | */ 189 | function withdraw() public onlyOwner { 190 | address _owner = owner(); 191 | uint256 amount = address(this).balance; 192 | (bool sent, ) = _owner.call{value: amount}(""); 193 | require(sent, "Failed to send Ether"); 194 | } 195 | 196 | // Function to receive Ether. msg.data must be empty 197 | receive() external payable {} 198 | 199 | // Fallback function is called when msg.data is not empty 200 | fallback() external payable {} 201 | } 202 | 203 | ``` 204 | 205 | - Now we will install the `dotenv` package to be able to import the env file and use it in our config. Open up a terminal pointing to the `hardhat-tutorial` directory and execute this command: 206 | 207 | ```bash 208 | npm install dotenv 209 | ``` 210 | 211 | - Create a `.env` file in the `hardhat-tutorial` folder and add the following lines. Use the instructions in the comments to get your Alchemy API Key URL and GOERLI Private Key. Make sure that the account from which you get your Goerli private key is funded with Goerli Ether. 212 | 213 | ```bash 214 | 215 | // Go to https://www.alchemyapi.io, sign up, create 216 | // a new App in its dashboard and select the network as Goerli, and replace "add-the-alchemy-key-url-here" with its key url 217 | ALCHEMY_API_KEY_URL="add-the-alchemy-key-url-here" 218 | 219 | // Replace this private key with your GOERLI account private key 220 | // To export your private key from Metamask, open Metamask and 221 | // go to Account Details > Export Private Key 222 | // Be aware of NEVER putting real Ether into testing accounts 223 | GOERLI_PRIVATE_KEY="add-the-goerli-private-key-here" 224 | ``` 225 | 226 | - Let's deploy the contract to the `goerli` network. Create a new file (or replace the existing default file) named `deploy.js` under the `scripts` folder. 227 | 228 | - Now we will write some code to deploy the contract in `deploy.js` file. 229 | 230 | ```js 231 | const { ethers } = require("hardhat"); 232 | require("dotenv").config({ path: ".env" }); 233 | const { CRYPTO_DEVS_NFT_CONTRACT_ADDRESS } = require("../constants"); 234 | 235 | async function main() { 236 | // Address of the Crypto Devs NFT contract that you deployed in the previous module 237 | const cryptoDevsNFTContract = CRYPTO_DEVS_NFT_CONTRACT_ADDRESS; 238 | 239 | /* 240 | A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts, 241 | so cryptoDevsTokenContract here is a factory for instances of our CryptoDevToken contract. 242 | */ 243 | const cryptoDevsTokenContract = await ethers.getContractFactory( 244 | "CryptoDevToken" 245 | ); 246 | 247 | // deploy the contract 248 | const deployedCryptoDevsTokenContract = await cryptoDevsTokenContract.deploy( 249 | cryptoDevsNFTContract 250 | ); 251 | 252 | // print the address of the deployed contract 253 | console.log( 254 | "Crypto Devs Token Contract Address:", 255 | deployedCryptoDevsTokenContract.address 256 | ); 257 | } 258 | 259 | // Call the main function and catch if there is any error 260 | main() 261 | .then(() => process.exit(0)) 262 | .catch((error) => { 263 | console.error(error); 264 | process.exit(1); 265 | }); 266 | ``` 267 | 268 | - You can see that the `deploy.js` file requires a constant. Let's create a `constants` folder under `hardhat-tutorial` folder. 269 | - Inside the `constants` folder create a new file named `index.js` and add the following lines to it. 270 | 271 | - Replace "address-of-the-nft-contract" with the address of the `CryptoDevs.sol` that you deployed in the previous module(`NFT-Collection`): 272 | 273 | ```js 274 | // Address of the NFT Contract that you deployed 275 | const CRYPTO_DEVS_NFT_CONTRACT_ADDRESS = "address-of-the-nft-contract"; 276 | 277 | module.exports = { CRYPTO_DEVS_NFT_CONTRACT_ADDRESS }; 278 | ``` 279 | 280 | - Now open the `hardhat.config.js` file, we will add the `goerli` network here so that we can deploy our contract to Goerli. Replace all the lines in the `hardhat.config.js` file with the given below lines: 281 | 282 | ```js 283 | require("@nomicfoundation/hardhat-toolbox"); 284 | require("dotenv").config({ path: ".env" }); 285 | 286 | const ALCHEMY_API_KEY_URL = process.env.ALCHEMY_API_KEY_URL; 287 | 288 | const GOERLI_PRIVATE_KEY = process.env.GOERLI_PRIVATE_KEY; 289 | 290 | module.exports = { 291 | solidity: "0.8.9", 292 | networks: { 293 | goerli: { 294 | url: ALCHEMY_API_KEY_URL, 295 | accounts: [GOERLI_PRIVATE_KEY], 296 | }, 297 | }, 298 | }; 299 | ``` 300 | 301 | - Compile the contract, open up a terminal pointing to the `hardhat-tutorial` directory and execute this command: 302 | 303 | ```bash 304 | npx hardhat compile 305 | ``` 306 | 307 | - Execute this command in the same directory to deploy the contract: 308 | ```bash 309 | npx hardhat run scripts/deploy.js --network goerli 310 | ``` 311 | - Save the CryptoDevToken Contract Address that was printed to your terminal in your notepad. You will need it later in the tutorial. 312 | 313 | ### Website 314 | 315 | - To develop the website we will be using [React](https://reactjs.org/) and [Next Js](https://nextjs.org/). React is a javascript framework which is used to make websites and Next Js is built on top of React. 316 | - You first need to create a new `next` app. Your folder structure should look something like this: 317 | 318 | ``` 319 | - ICO 320 | - hardhat-tutorial 321 | - my-app 322 | ``` 323 | 324 | - To create this `my-app`, in the terminal point to the `ICO` folder and type: 325 | 326 | ```bash 327 | npx create-next-app@latest 328 | ``` 329 | 330 | and press `enter` for all the questions. 331 | 332 | - Now to run the app, execute these commands in the terminal: 333 | 334 | ``` 335 | cd my-app 336 | npm run dev 337 | ``` 338 | 339 | - Now go to `http://localhost:3000`, your app should be running 🤘 340 | 341 | - Now let's install the Web3Modal library(https://github.com/Web3Modal/web3modal). Web3Modal is an easy-to-use library to help developers add support for multiple providers in their apps with a simple customizable configuration. By default Web3Modal Library supports injected providers like (Metamask, Dapper, Gnosis Safe, Frame, Web3 Browsers, etc). You can also easily configure the library to support Portis, Fortmatic, Squarelink, Torus, Authereum, D'CENT Wallet, and Arkane. 342 | Open up a terminal pointing to `my-app` directory and execute this command: 343 | 344 | ```bash 345 | npm install web3modal 346 | ``` 347 | 348 | - In the same terminal also install `ethers.js`: 349 | 350 | ```bash 351 | npm install ethers 352 | ``` 353 | 354 | - In the `public` folder, download the following image (https://github.com/LearnWeb3DAO/NFT-Collection/tree/main/my-app/public/cryptodevs/0.svg). Make sure that the name of the downloaded image is `0.svg`. 355 | 356 | - Now go to styles folder and replace all the contents of `Home.modules.css` file with the following code. This will add some styling to your dapp: 357 | 358 | ```css 359 | .main { 360 | min-height: 90vh; 361 | display: flex; 362 | flex-direction: row; 363 | justify-content: center; 364 | align-items: center; 365 | font-family: "Courier New", Courier, monospace; 366 | } 367 | 368 | .footer { 369 | display: flex; 370 | padding: 2rem 0; 371 | border-top: 1px solid #eaeaea; 372 | justify-content: center; 373 | align-items: center; 374 | } 375 | 376 | .image { 377 | width: 70%; 378 | height: 50%; 379 | margin-left: 20%; 380 | } 381 | 382 | .input { 383 | width: 200px; 384 | height: 100%; 385 | padding: 1%; 386 | margin-bottom: 2%; 387 | box-shadow: 0 0 15px 4px rgba(0, 0, 0, 0.06); 388 | border-radius: 10px; 389 | } 390 | 391 | .title { 392 | font-size: 2rem; 393 | margin: 2rem 0; 394 | } 395 | 396 | .description { 397 | line-height: 1; 398 | margin: 2rem 0; 399 | font-size: 1.2rem; 400 | } 401 | 402 | .button { 403 | border-radius: 4px; 404 | background-color: blue; 405 | border: none; 406 | color: #ffffff; 407 | font-size: 15px; 408 | padding: 5px; 409 | width: 100px; 410 | cursor: pointer; 411 | margin-bottom: 2%; 412 | } 413 | @media (max-width: 1000px) { 414 | .main { 415 | width: 100%; 416 | flex-direction: column; 417 | justify-content: center; 418 | align-items: center; 419 | } 420 | } 421 | ``` 422 | 423 | - Open the `index.js` file under the `pages` folder and paste the following code. An explanation of the code can be found in the comments. 424 | 425 | ```javascript 426 | import { BigNumber, Contract, providers, utils } from "ethers"; 427 | import Head from "next/head"; 428 | import React, { useEffect, useRef, useState } from "react"; 429 | import Web3Modal from "web3modal"; 430 | import { 431 | NFT_CONTRACT_ABI, 432 | NFT_CONTRACT_ADDRESS, 433 | TOKEN_CONTRACT_ABI, 434 | TOKEN_CONTRACT_ADDRESS, 435 | } from "../constants"; 436 | import styles from "../styles/Home.module.css"; 437 | 438 | export default function Home() { 439 | // Create a BigNumber `0` 440 | const zero = BigNumber.from(0); 441 | // walletConnected keeps track of whether the user's wallet is connected or not 442 | const [walletConnected, setWalletConnected] = useState(false); 443 | // loading is set to true when we are waiting for a transaction to get mined 444 | const [loading, setLoading] = useState(false); 445 | // tokensToBeClaimed keeps track of the number of tokens that can be claimed 446 | // based on the Crypto Dev NFT's held by the user for which they havent claimed the tokens 447 | const [tokensToBeClaimed, setTokensToBeClaimed] = useState(zero); 448 | // balanceOfCryptoDevTokens keeps track of number of Crypto Dev tokens owned by an address 449 | const [balanceOfCryptoDevTokens, setBalanceOfCryptoDevTokens] = useState( 450 | zero 451 | ); 452 | // amount of the tokens that the user wants to mint 453 | const [tokenAmount, setTokenAmount] = useState(zero); 454 | // tokensMinted is the total number of tokens that have been minted till now out of 10000(max total supply) 455 | const [tokensMinted, setTokensMinted] = useState(zero); 456 | // isOwner gets the owner of the contract through the signed address 457 | const [isOwner, setIsOwner] = useState(false); 458 | // Create a reference to the Web3 Modal (used for connecting to Metamask) which persists as long as the page is open 459 | const web3ModalRef = useRef(); 460 | 461 | /** 462 | * getTokensToBeClaimed: checks the balance of tokens that can be claimed by the user 463 | */ 464 | const getTokensToBeClaimed = async () => { 465 | try { 466 | // Get the provider from web3Modal, which in our case is MetaMask 467 | // No need for the Signer here, as we are only reading state from the blockchain 468 | const provider = await getProviderOrSigner(); 469 | // Create an instance of NFT Contract 470 | const nftContract = new Contract( 471 | NFT_CONTRACT_ADDRESS, 472 | NFT_CONTRACT_ABI, 473 | provider 474 | ); 475 | // Create an instance of tokenContract 476 | const tokenContract = new Contract( 477 | TOKEN_CONTRACT_ADDRESS, 478 | TOKEN_CONTRACT_ABI, 479 | provider 480 | ); 481 | // We will get the signer now to extract the address of the currently connected MetaMask account 482 | const signer = await getProviderOrSigner(true); 483 | // Get the address associated to the signer which is connected to MetaMask 484 | const address = await signer.getAddress(); 485 | // call the balanceOf from the NFT contract to get the number of NFT's held by the user 486 | const balance = await nftContract.balanceOf(address); 487 | // balance is a Big number and thus we would compare it with Big number `zero` 488 | if (balance === zero) { 489 | setTokensToBeClaimed(zero); 490 | } else { 491 | // amount keeps track of the number of unclaimed tokens 492 | var amount = 0; 493 | // For all the NFT's, check if the tokens have already been claimed 494 | // Only increase the amount if the tokens have not been claimed 495 | // for a an NFT(for a given tokenId) 496 | for (var i = 0; i < balance; i++) { 497 | const tokenId = await nftContract.tokenOfOwnerByIndex(address, i); 498 | const claimed = await tokenContract.tokenIdsClaimed(tokenId); 499 | if (!claimed) { 500 | amount++; 501 | } 502 | } 503 | //tokensToBeClaimed has been initialized to a Big Number, thus we would convert amount 504 | // to a big number and then set its value 505 | setTokensToBeClaimed(BigNumber.from(amount)); 506 | } 507 | } catch (err) { 508 | console.error(err); 509 | setTokensToBeClaimed(zero); 510 | } 511 | }; 512 | 513 | /** 514 | * getBalanceOfCryptoDevTokens: checks the balance of Crypto Dev Tokens's held by an address 515 | */ 516 | const getBalanceOfCryptoDevTokens = async () => { 517 | try { 518 | // Get the provider from web3Modal, which in our case is MetaMask 519 | // No need for the Signer here, as we are only reading state from the blockchain 520 | const provider = await getProviderOrSigner(); 521 | // Create an instace of token contract 522 | const tokenContract = new Contract( 523 | TOKEN_CONTRACT_ADDRESS, 524 | TOKEN_CONTRACT_ABI, 525 | provider 526 | ); 527 | // We will get the signer now to extract the address of the currently connected MetaMask account 528 | const signer = await getProviderOrSigner(true); 529 | // Get the address associated to the signer which is connected to MetaMask 530 | const address = await signer.getAddress(); 531 | // call the balanceOf from the token contract to get the number of tokens held by the user 532 | const balance = await tokenContract.balanceOf(address); 533 | // balance is already a big number, so we dont need to convert it before setting it 534 | setBalanceOfCryptoDevTokens(balance); 535 | } catch (err) { 536 | console.error(err); 537 | setBalanceOfCryptoDevTokens(zero); 538 | } 539 | }; 540 | 541 | /** 542 | * mintCryptoDevToken: mints `amount` number of tokens to a given address 543 | */ 544 | const mintCryptoDevToken = async (amount) => { 545 | try { 546 | // We need a Signer here since this is a 'write' transaction. 547 | // Create an instance of tokenContract 548 | const signer = await getProviderOrSigner(true); 549 | // Create an instance of tokenContract 550 | const tokenContract = new Contract( 551 | TOKEN_CONTRACT_ADDRESS, 552 | TOKEN_CONTRACT_ABI, 553 | signer 554 | ); 555 | // Each token is of `0.001 ether`. The value we need to send is `0.001 * amount` 556 | const value = 0.001 * amount; 557 | const tx = await tokenContract.mint(amount, { 558 | // value signifies the cost of one crypto dev token which is "0.001" eth. 559 | // We are parsing `0.001` string to ether using the utils library from ethers.js 560 | value: utils.parseEther(value.toString()), 561 | }); 562 | setLoading(true); 563 | // wait for the transaction to get mined 564 | await tx.wait(); 565 | setLoading(false); 566 | window.alert("Sucessfully minted Crypto Dev Tokens"); 567 | await getBalanceOfCryptoDevTokens(); 568 | await getTotalTokensMinted(); 569 | await getTokensToBeClaimed(); 570 | } catch (err) { 571 | console.error(err); 572 | } 573 | }; 574 | 575 | /** 576 | * claimCryptoDevTokens: Helps the user claim Crypto Dev Tokens 577 | */ 578 | const claimCryptoDevTokens = async () => { 579 | try { 580 | // We need a Signer here since this is a 'write' transaction. 581 | // Create an instance of tokenContract 582 | const signer = await getProviderOrSigner(true); 583 | // Create an instance of tokenContract 584 | const tokenContract = new Contract( 585 | TOKEN_CONTRACT_ADDRESS, 586 | TOKEN_CONTRACT_ABI, 587 | signer 588 | ); 589 | const tx = await tokenContract.claim(); 590 | setLoading(true); 591 | // wait for the transaction to get mined 592 | await tx.wait(); 593 | setLoading(false); 594 | window.alert("Sucessfully claimed Crypto Dev Tokens"); 595 | await getBalanceOfCryptoDevTokens(); 596 | await getTotalTokensMinted(); 597 | await getTokensToBeClaimed(); 598 | } catch (err) { 599 | console.error(err); 600 | } 601 | }; 602 | 603 | /** 604 | * getTotalTokensMinted: Retrieves how many tokens have been minted till now 605 | * out of the total supply 606 | */ 607 | const getTotalTokensMinted = async () => { 608 | try { 609 | // Get the provider from web3Modal, which in our case is MetaMask 610 | // No need for the Signer here, as we are only reading state from the blockchain 611 | const provider = await getProviderOrSigner(); 612 | // Create an instance of token contract 613 | const tokenContract = new Contract( 614 | TOKEN_CONTRACT_ADDRESS, 615 | TOKEN_CONTRACT_ABI, 616 | provider 617 | ); 618 | // Get all the tokens that have been minted 619 | const _tokensMinted = await tokenContract.totalSupply(); 620 | setTokensMinted(_tokensMinted); 621 | } catch (err) { 622 | console.error(err); 623 | } 624 | }; 625 | 626 | /** 627 | * getOwner: gets the contract owner by connected address 628 | */ 629 | const getOwner = async () => { 630 | try { 631 | const provider = await getProviderOrSigner(); 632 | const tokenContract = new Contract(TOKEN_CONTRACT_ADDRESS, TOKEN_CONTRACT_ABI, provider); 633 | // call the owner function from the contract 634 | const _owner = await tokenContract.owner(); 635 | // we get signer to extract address of currently connected Metamask account 636 | const signer = await getProviderOrSigner(true); 637 | // Get the address associated to signer which is connected to Metamask 638 | const address = await signer.getAddress(); 639 | if (address.toLowerCase() === _owner.toLowerCase()) { 640 | setIsOwner(true); 641 | } 642 | } catch (err) { 643 | console.error(err.message); 644 | } 645 | }; 646 | 647 | /** 648 | * withdrawCoins: withdraws ether and tokens by calling 649 | * the withdraw function in the contract 650 | */ 651 | const withdrawCoins = async () => { 652 | try { 653 | const signer = await getProviderOrSigner(true); 654 | const tokenContract = new Contract( 655 | TOKEN_CONTRACT_ADDRESS, 656 | TOKEN_CONTRACT_ABI, 657 | signer 658 | ); 659 | 660 | const tx = await tokenContract.withdraw(); 661 | setLoading(true); 662 | await tx.wait(); 663 | setLoading(false); 664 | await getOwner(); 665 | } catch (err) { 666 | console.error(err); 667 | } 668 | } 669 | 670 | /** 671 | * Returns a Provider or Signer object representing the Ethereum RPC with or without the 672 | * signing capabilities of metamask attached 673 | * 674 | * A `Provider` is needed to interact with the blockchain - reading transactions, reading balances, reading state, etc. 675 | * 676 | * A `Signer` is a special type of Provider used in case a `write` transaction needs to be made to the blockchain, which involves the connected account 677 | * needing to make a digital signature to authorize the transaction being sent. Metamask exposes a Signer API to allow your website to 678 | * request signatures from the user using Signer functions. 679 | * 680 | * @param {*} needSigner - True if you need the signer, default false otherwise 681 | */ 682 | const getProviderOrSigner = async (needSigner = false) => { 683 | // Connect to Metamask 684 | // Since we store `web3Modal` as a reference, we need to access the `current` value to get access to the underlying object 685 | const provider = await web3ModalRef.current.connect(); 686 | const web3Provider = new providers.Web3Provider(provider); 687 | 688 | // If user is not connected to the Goerli network, let them know and throw an error 689 | const { chainId } = await web3Provider.getNetwork(); 690 | if (chainId !== 5) { 691 | window.alert("Change the network to Goerli"); 692 | throw new Error("Change network to Goerli"); 693 | } 694 | 695 | if (needSigner) { 696 | const signer = web3Provider.getSigner(); 697 | return signer; 698 | } 699 | return web3Provider; 700 | }; 701 | 702 | /* 703 | connectWallet: Connects the MetaMask wallet 704 | */ 705 | const connectWallet = async () => { 706 | try { 707 | // Get the provider from web3Modal, which in our case is MetaMask 708 | // When used for the first time, it prompts the user to connect their wallet 709 | await getProviderOrSigner(); 710 | setWalletConnected(true); 711 | } catch (err) { 712 | console.error(err); 713 | } 714 | }; 715 | 716 | // useEffects are used to react to changes in state of the website 717 | // The array at the end of function call represents what state changes will trigger this effect 718 | // In this case, whenever the value of `walletConnected` changes - this effect will be called 719 | useEffect(() => { 720 | // if wallet is not connected, create a new instance of Web3Modal and connect the MetaMask wallet 721 | if (!walletConnected) { 722 | // Assign the Web3Modal class to the reference object by setting it's `current` value 723 | // The `current` value is persisted throughout as long as this page is open 724 | web3ModalRef.current = new Web3Modal({ 725 | network: "goerli", 726 | providerOptions: {}, 727 | disableInjectedProvider: false, 728 | }); 729 | connectWallet(); 730 | getTotalTokensMinted(); 731 | getBalanceOfCryptoDevTokens(); 732 | getTokensToBeClaimed(); 733 | withdrawCoins(); 734 | } 735 | }, [walletConnected]); 736 | 737 | /* 738 | renderButton: Returns a button based on the state of the dapp 739 | */ 740 | const renderButton = () => { 741 | // If we are currently waiting for something, return a loading button 742 | if (loading) { 743 | return ( 744 |
745 | 746 |
747 | ); 748 | } 749 | // if owner is connected, withdrawCoins() is called 750 | if (walletConnected && isOwner) { 751 | return ( 752 |
753 | 756 |
757 | ); 758 | } 759 | // If tokens to be claimed are greater than 0, Return a claim button 760 | if (tokensToBeClaimed > 0) { 761 | return ( 762 |
763 |
764 | {tokensToBeClaimed * 10} Tokens can be claimed! 765 |
766 | 769 |
770 | ); 771 | } 772 | // If user doesn't have any tokens to claim, show the mint button 773 | return ( 774 |
775 |
776 | setTokenAmount(BigNumber.from(e.target.value))} 781 | className={styles.input} 782 | /> 783 |
784 | 785 | 792 |
793 | ); 794 | }; 795 | 796 | return ( 797 |
798 | 799 | Crypto Devs 800 | 801 | 802 | 803 |
804 |
805 |

Welcome to Crypto Devs ICO!

806 |
807 | You can claim or mint Crypto Dev tokens here 808 |
809 | {walletConnected ? ( 810 |
811 |
812 | {/* Format Ether helps us in converting a BigNumber to string */} 813 | You have minted {utils.formatEther(balanceOfCryptoDevTokens)} Crypto 814 | Dev Tokens 815 |
816 |
817 | {/* Format Ether helps us in converting a BigNumber to string */} 818 | Overall {utils.formatEther(tokensMinted)}/10000 have been minted!!! 819 |
820 | {renderButton()} 821 |
822 | ) : ( 823 | 826 | )} 827 |
828 |
829 | 830 |
831 |
832 | 833 | 836 |
837 | ); 838 | } 839 | ``` 840 | 841 | - Now create a new folder under the `my-app` folder and name it `constants`. 842 | - In the `constants` folder create a file called `index.js` and paste the following code: 843 | 844 | ```js 845 | export const NFT_CONTRACT_ABI = "abi-of-your-nft-contract"; 846 | export const NFT_CONTRACT_ADDRESS = "address-of-your-nft-contract"; 847 | export const TOKEN_CONTRACT_ABI = "abi-of-your-token-contract"; 848 | export const TOKEN_CONTRACT_ADDRESS = "address-of-your-token-contract"; 849 | ``` 850 | 851 | - Replace `"abi-of-your-nft-contract"` with the abi of the NFT contract that you deployed in the last tutorial. 852 | - Replace `"address-of-your-nft-contract"` with the address of the NFT contract that you deployed in your previous tutorial. 853 | - Replace `"abi-of-your-token-contract"` by the abi of the token contract. To get the abi of the Token contract, go to `hardhat-tutorial/artifacts/contracts/CryptoDevToken.sol` and then from`CryptoDevToken.json` file get the array marked under the `"abi"` key. 854 | - Replace `"address-of-your-token-contract"` with the address of the token contract that you saved to your notepad earlier in the tutorial. 855 | 856 | - Now in your terminal which is pointing to `my-app` folder, execute the following: 857 | 858 | ```bash 859 | npm run dev 860 | ``` 861 | 862 | Your ICO dapp should now work without errors 🚀 863 | 864 | --- 865 | 866 | ## Push to Github 867 | 868 | Make sure to push all your [code to Github before proceeding to the next step](https://medium.com/hackernoon/a-gentle-introduction-to-git-and-github-the-eli5-way-43f0aa64f2e4). 869 | 870 | --- 871 | 872 | ## Deploying your dApp 873 | 874 | We will now deploy your dApp, so that everyone can see your website and you can share it with all of your LearnWeb3 DAO friends. 875 | 876 | - Go to https://vercel.com/ and sign in with your GitHub. 877 | - Then click on `New Project` button and then select your ICO dApp repo. 878 | - When configuring your new project, Vercel will allow you to customize your `Root Directory`. 879 | - Click `Edit` next to `Root Directory` and set it to `my-app`. 880 | - Select the `Framework Preset` as `Next.js`. 881 | ![](https://i.imgur.com/2oJRKgO.png) 882 | 883 | - Click `Deploy` 884 | - Now you can see your deployed website by going to your dashboard, selecting your project, and copying the URL from there! 885 | 886 | ## CONGRATULATIONS! You're all done! 887 | 888 | Hopefully you enjoyed this tutorial. Don't forget to share your ICO website in the `#showcase` channel on Discord :D 889 | -------------------------------------------------------------------------------- /hardhat-tutorial/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | 7 | #Hardhat files 8 | cache 9 | artifacts 10 | -------------------------------------------------------------------------------- /hardhat-tutorial/README.md: -------------------------------------------------------------------------------- 1 | # Basic Sample Hardhat Project 2 | 3 | This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, a sample script that deploys that contract, and an example of a task implementation, which simply lists the available accounts. 4 | 5 | Try running some of the following tasks: 6 | 7 | ```shell 8 | npx hardhat accounts 9 | npx hardhat compile 10 | npx hardhat clean 11 | npx hardhat test 12 | npx hardhat node 13 | node scripts/sample-script.js 14 | npx hardhat help 15 | ``` 16 | -------------------------------------------------------------------------------- /hardhat-tutorial/constants/index.js: -------------------------------------------------------------------------------- 1 | // Address of the NFT Contract that you deployed 2 | const CRYPTO_DEVS_NFT_CONTRACT_ADDRESS = 3 | "0x6805a66179283Fa1FcEc8D89f4e162bebe5C0617"; 4 | 5 | module.exports = { CRYPTO_DEVS_NFT_CONTRACT_ADDRESS }; 6 | -------------------------------------------------------------------------------- /hardhat-tutorial/contracts/CryptoDevToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "./ICryptoDevs.sol"; 7 | 8 | contract CryptoDevToken is ERC20, Ownable { 9 | // Price of one Crypto Dev token 10 | uint256 public constant tokenPrice = 0.001 ether; 11 | // Each NFT would give the user 10 tokens 12 | // It needs to be represented as 10 * (10 ** 18) as ERC20 tokens are represented by the smallest denomination possible for the token 13 | // By default, ERC20 tokens have the smallest denomination of 10^(-18). This means, having a balance of (1) 14 | // is actually equal to (10 ^ -18) tokens. 15 | // Owning 1 full token is equivalent to owning (10^18) tokens when you account for the decimal places. 16 | // More information on this can be found in the Freshman Track Cryptocurrency tutorial. 17 | uint256 public constant tokensPerNFT = 10 * 10**18; 18 | // the max total supply is 10000 for Crypto Dev Tokens 19 | uint256 public constant maxTotalSupply = 10000 * 10**18; 20 | // CryptoDevsNFT contract instance 21 | ICryptoDevs CryptoDevsNFT; 22 | // Mapping to keep track of which tokenIds have been claimed 23 | mapping(uint256 => bool) public tokenIdsClaimed; 24 | 25 | constructor(address _cryptoDevsContract) ERC20("Crypto Dev Token", "CD") { 26 | CryptoDevsNFT = ICryptoDevs(_cryptoDevsContract); 27 | } 28 | 29 | /** 30 | * @dev Mints `amount` number of CryptoDevTokens 31 | * Requirements: 32 | * - `msg.value` should be equal or greater than the tokenPrice * amount 33 | */ 34 | function mint(uint256 amount) public payable { 35 | // the value of ether that should be equal or greater than tokenPrice * amount; 36 | uint256 _requiredAmount = tokenPrice * amount; 37 | require(msg.value >= _requiredAmount, "Ether sent is incorrect"); 38 | // total tokens + amount <= 10000, otherwise revert the transaction 39 | uint256 amountWithDecimals = amount * 10**18; 40 | require( 41 | (totalSupply() + amountWithDecimals) <= maxTotalSupply, 42 | "Exceeds the max total supply available." 43 | ); 44 | // call the internal function from Openzeppelin's ERC20 contract 45 | _mint(msg.sender, amountWithDecimals); 46 | } 47 | 48 | /** 49 | * @dev Mints tokens based on the number of NFT's held by the sender 50 | * Requirements: 51 | * balance of Crypto Dev NFT's owned by the sender should be greater than 0 52 | * Tokens should have not been claimed for all the NFTs owned by the sender 53 | */ 54 | function claim() public { 55 | address sender = msg.sender; 56 | // Get the number of CryptoDev NFT's held by a given sender address 57 | uint256 balance = CryptoDevsNFT.balanceOf(sender); 58 | // If the balance is zero, revert the transaction 59 | require(balance > 0, "You dont own any Crypto Dev NFT's"); 60 | // amount keeps track of number of unclaimed tokenIds 61 | uint256 amount = 0; 62 | // loop over the balance and get the token ID owned by `sender` at a given `index` of its token list. 63 | for (uint256 i = 0; i < balance; i++) { 64 | uint256 tokenId = CryptoDevsNFT.tokenOfOwnerByIndex(sender, i); 65 | // if the tokenId has not been claimed, increase the amount 66 | if (!tokenIdsClaimed[tokenId]) { 67 | amount += 1; 68 | tokenIdsClaimed[tokenId] = true; 69 | } 70 | } 71 | // If all the token Ids have been claimed, revert the transaction; 72 | require(amount > 0, "You have already claimed all the tokens"); 73 | // call the internal function from Openzeppelin's ERC20 contract 74 | // Mint (amount * 10) tokens for each NFT 75 | _mint(msg.sender, amount * tokensPerNFT); 76 | } 77 | 78 | // Enables withdrawal of ether sent to contract for ICO 79 | function withdraw() public onlyOwner { 80 | address _owner = owner(); 81 | uint256 amount = address(this).balance; 82 | (bool sent, ) = _owner.call{value: amount}(""); 83 | require(sent, "Failed to send Ether"); 84 | } 85 | 86 | // Function to receive Ether. msg.data must be empty 87 | receive() external payable {} 88 | 89 | // Fallback function is called when msg.data is not empty 90 | fallback() external payable {} 91 | } 92 | -------------------------------------------------------------------------------- /hardhat-tutorial/contracts/Greeter.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "hardhat/console.sol"; 5 | 6 | contract Greeter { 7 | string private greeting; 8 | 9 | constructor(string memory _greeting) { 10 | console.log("Deploying a Greeter with greeting:", _greeting); 11 | greeting = _greeting; 12 | } 13 | 14 | function greet() public view returns (string memory) { 15 | return greeting; 16 | } 17 | 18 | function setGreeting(string memory _greeting) public { 19 | console.log("Changing greeting from '%s' to '%s'", greeting, _greeting); 20 | greeting = _greeting; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /hardhat-tutorial/contracts/ICryptoDevs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | 4 | interface ICryptoDevs { 5 | /** 6 | * @dev Returns a token ID owned by `owner` at a given `index` of its token list. 7 | * Use along with {balanceOf} to enumerate all of ``owner``'s tokens. 8 | */ 9 | function tokenOfOwnerByIndex(address owner, uint256 index) 10 | external 11 | view 12 | returns (uint256 tokenId); 13 | 14 | /** 15 | * @dev Returns the number of tokens in ``owner``'s account. 16 | */ 17 | function balanceOf(address owner) external view returns (uint256 balance); 18 | } 19 | -------------------------------------------------------------------------------- /hardhat-tutorial/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomiclabs/hardhat-waffle"); 2 | require("dotenv").config({ path: ".env" }); 3 | 4 | const ALCHEMY_API_KEY_URL = process.env.ALCHEMY_API_KEY_URL; 5 | 6 | const GOERLI_PRIVATE_KEY = process.env.GOERLI_PRIVATE_KEY; 7 | 8 | module.exports = { 9 | solidity: "0.8.10", 10 | networks: { 11 | goerli: { 12 | url: ALCHEMY_API_KEY_URL, 13 | accounts: [GOERLI_PRIVATE_KEY], 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /hardhat-tutorial/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@nomiclabs/hardhat-ethers": "^2.0.3", 14 | "@nomiclabs/hardhat-waffle": "^2.0.1", 15 | "chai": "^4.3.4", 16 | "ethereum-waffle": "^3.4.0", 17 | "ethers": "^5.5.2", 18 | "hardhat": "^2.8.0", 19 | "prettier": "^2.5.1", 20 | "prettier-plugin-solidity": "^1.0.0-beta.19" 21 | }, 22 | "dependencies": { 23 | "@openzeppelin/contracts": "^4.4.1", 24 | "dotenv": "^10.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /hardhat-tutorial/scripts/deploy.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("hardhat"); 2 | require("dotenv").config({ path: ".env" }); 3 | const { CRYPTO_DEVS_NFT_CONTRACT_ADDRESS } = require("../constants"); 4 | 5 | async function main() { 6 | // Address of the Crypto Devs NFT contract that you deployed in the previous module 7 | const cryptoDevsNFTContract = CRYPTO_DEVS_NFT_CONTRACT_ADDRESS; 8 | 9 | /* 10 | A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts, 11 | so cryptoDevsTokenContract here is a factory for instances of our CryptoDevToken contract. 12 | */ 13 | const cryptoDevsTokenContract = await ethers.getContractFactory( 14 | "CryptoDevToken" 15 | ); 16 | 17 | // deploy the contract 18 | const deployedCryptoDevsTokenContract = await cryptoDevsTokenContract.deploy( 19 | cryptoDevsNFTContract 20 | ); 21 | 22 | // print the address of the deployed contract 23 | console.log( 24 | "Crypto Devs Token Contract Address:", 25 | deployedCryptoDevsTokenContract.address 26 | ); 27 | } 28 | 29 | // Call the main function and catch if there is any error 30 | main() 31 | .then(() => process.exit(0)) 32 | .catch((error) => { 33 | console.error(error); 34 | process.exit(1); 35 | }); 36 | -------------------------------------------------------------------------------- /hardhat-tutorial/scripts/sample-script.js: -------------------------------------------------------------------------------- 1 | // We require the Hardhat Runtime Environment explicitly here. This is optional 2 | // but useful for running the script in a standalone fashion through `node