├── ocean-token ├── .env.example ├── README.md ├── .gitignore ├── scripts │ ├── deploy.js │ └── deployFaucet.js ├── hardhat.config.js ├── package.json ├── contracts │ ├── OceanToken.sol │ └── Faucet.sol └── test │ └── OceanToken.js └── faucet-ui ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── images │ ├── bg-1.png │ └── bg.png ├── setupTests.js ├── App.test.js ├── index.css ├── reportWebVitals.js ├── App.css ├── index.js ├── logo.svg ├── ethereum │ └── faucet.js └── App.js ├── README.md ├── .gitignore └── package.json /ocean-token/.env.example: -------------------------------------------------------------------------------- 1 | PRIVATE_KEY= 2 | INFURA_GOERLI_ENDPOINT= 3 | INFURA_RINKEBY_ENDPOINT= -------------------------------------------------------------------------------- /faucet-ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /faucet-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspruance/erc20-tutorial-block-explorer/main/faucet-ui/public/favicon.ico -------------------------------------------------------------------------------- /faucet-ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspruance/erc20-tutorial-block-explorer/main/faucet-ui/public/logo192.png -------------------------------------------------------------------------------- /faucet-ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspruance/erc20-tutorial-block-explorer/main/faucet-ui/public/logo512.png -------------------------------------------------------------------------------- /faucet-ui/src/images/bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspruance/erc20-tutorial-block-explorer/main/faucet-ui/src/images/bg-1.png -------------------------------------------------------------------------------- /faucet-ui/src/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspruance/erc20-tutorial-block-explorer/main/faucet-ui/src/images/bg.png -------------------------------------------------------------------------------- /faucet-ui/README.md: -------------------------------------------------------------------------------- 1 | # Starter code for Faucet dApp tutorial 2 | 3 | Boilerplate code consisting of a new create-react-app project and some basic HTML and CSS. 4 | -------------------------------------------------------------------------------- /ocean-token/README.md: -------------------------------------------------------------------------------- 1 | # ERC20 Token Tutorial 2 | 3 | This is the source code for the Block Explorer YouTube video: 4 | https://www.youtube.com/watch?v=gc7e90MHvl8 5 | -------------------------------------------------------------------------------- /ocean-token/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | typechain-types 7 | 8 | #Hardhat files 9 | cache 10 | artifacts 11 | 12 | -------------------------------------------------------------------------------- /faucet-ui/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 | -------------------------------------------------------------------------------- /faucet-ui/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /faucet-ui/.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 | -------------------------------------------------------------------------------- /faucet-ui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /faucet-ui/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 | -------------------------------------------------------------------------------- /ocean-token/scripts/deploy.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | 3 | async function main() { 4 | const OceanToken = await hre.ethers.getContractFactory("OceanToken"); 5 | const oceanToken = await OceanToken.deploy(100000000, 50); 6 | 7 | await oceanToken.deployed(); 8 | 9 | console.log("Ocean Token deployed: ", oceanToken.address); 10 | } 11 | 12 | main().catch((error) => { 13 | console.error(error); 14 | process.exitCode = 1; 15 | }); 16 | -------------------------------------------------------------------------------- /ocean-token/scripts/deployFaucet.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | 3 | async function main() { 4 | const Faucet = await hre.ethers.getContractFactory("Faucet"); 5 | const faucet = await Faucet.deploy("0x2225d9117e37329713884942992EE040B742D906"); 6 | 7 | await faucet.deployed(); 8 | 9 | console.log("Facuet contract deployed: ", faucet.address); 10 | } 11 | 12 | main().catch((error) => { 13 | console.error(error); 14 | process.exitCode = 1; 15 | }); 16 | -------------------------------------------------------------------------------- /ocean-token/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomicfoundation/hardhat-toolbox"); 2 | require("dotenv").config(); 3 | 4 | /** @type import('hardhat/config').HardhatUserConfig */ 5 | module.exports = { 6 | solidity: "0.8.17", 7 | networks: { 8 | rinkeby: { 9 | url: process.env.INFURA_RINKEBY_ENDPOINT, 10 | accounts: [process.env.PRIVATE_KEY] 11 | }, 12 | goerli: { 13 | url: process.env.INFURA_GOERLI_ENDPOINT, 14 | accounts: [process.env.PRIVATE_KEY] 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /ocean-token/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ocean-token", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@nomicfoundation/hardhat-toolbox": "^1.0.1", 13 | "chai": "^4.3.6", 14 | "hardhat": "^2.11.1" 15 | }, 16 | "dependencies": { 17 | "@openzeppelin/contracts": "^4.7.3", 18 | "dotenv": "^16.0.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /faucet-ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 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 | -------------------------------------------------------------------------------- /faucet-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "faucet-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "bulma": "^0.9.4", 10 | "ethers": "^5.7.1", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-scripts": "5.0.1", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /faucet-ui/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: url("./images/bg-1.png") no-repeat; 3 | background-color: #070709; 4 | color: white; 5 | } 6 | 7 | .title { 8 | color: white; 9 | margin-bottom: 1 rem; 10 | } 11 | 12 | .box { 13 | margin-top: 50px; 14 | } 15 | 16 | .panel { 17 | margin-top: 2em; 18 | } 19 | 20 | .panel-heading { 21 | xbackground-image: linear-gradient(to left,#38052F, #070709); 22 | background-color: black; 23 | color: white; 24 | } 25 | 26 | .address-box { 27 | padding: 3em; 28 | } 29 | 30 | .box .title { 31 | color: #333; 32 | } 33 | 34 | .navbar { 35 | background-color: black; 36 | } 37 | 38 | .navbar .navbar-item { 39 | color: white; 40 | } 41 | 42 | .connect-wallet span { 43 | color: #E10859; 44 | } 45 | 46 | .connect-wallet:hover { 47 | background-color: #e1e1e1 !important; 48 | } 49 | 50 | .container.main-content { 51 | max-width: 1000px !important; 52 | } 53 | 54 | .faucet-hero-body { 55 | margin-top: 100px; 56 | } 57 | 58 | .withdraw-error { 59 | color: red; 60 | } 61 | 62 | .withdraw-success { 63 | color: green; 64 | } -------------------------------------------------------------------------------- /faucet-ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "bulma/css/bulma.min.css"; 4 | import "./index.css"; 5 | import App from "./App"; 6 | import reportWebVitals from "./reportWebVitals"; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById("root")); 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | 15 | //****** ** ** ******** ** 16 | ///*////** /** /** /**///// ****** /** 17 | ///* /** /** ****** ***** /** ** /** ** **/**///** /** ****** ****** ***** ****** 18 | ///****** /** **////** **///**/** ** /******* //** ** /** /** /** **////**//**//* **///**//**//* 19 | ///*//// ** /**/** /**/** // /**** /**//// //*** /****** /**/** /** /** / /******* /** / 20 | ///* /** /**/** /**/** **/**/** /** **/** /**/// /**/** /** /** /**//// /** 21 | ///******* ***//****** //***** /**//** /******** ** //**/** ***//****** /*** //******/*** 22 | ///////// /// ////// ///// // // //////// // // // /// ////// /// ////// /// 23 | reportWebVitals(); 24 | -------------------------------------------------------------------------------- /ocean-token/contracts/OceanToken.sol: -------------------------------------------------------------------------------- 1 | // contracts/OceanToken.sol 2 | // SPDX-License-Identifier: MIT 3 | 4 | pragma solidity ^0.8.17; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; 9 | 10 | contract OceanToken is ERC20Capped, ERC20Burnable { 11 | address payable public owner; 12 | uint256 public blockReward; 13 | 14 | constructor(uint256 cap, uint256 reward) ERC20("OceanToken", "OCT") ERC20Capped(cap * (10 ** decimals())) { 15 | owner = payable(msg.sender); 16 | _mint(owner, 70000000 * (10 ** decimals())); 17 | blockReward = reward * (10 ** decimals()); 18 | } 19 | 20 | function _mint(address account, uint256 amount) internal virtual override(ERC20Capped, ERC20) { 21 | require(ERC20.totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded"); 22 | super._mint(account, amount); 23 | } 24 | 25 | function _mintMinerReward() internal { 26 | _mint(block.coinbase, blockReward); 27 | } 28 | 29 | function _beforeTokenTransfer(address from, address to, uint256 value) internal virtual override { 30 | if(from != address(0) && to != block.coinbase && block.coinbase != address(0) && ERC20.totalSupply() + blockReward <= cap()) { 31 | _mintMinerReward(); 32 | } 33 | super._beforeTokenTransfer(from, to, value); 34 | } 35 | 36 | function setBlockReward(uint256 reward) public onlyOwner { 37 | blockReward = reward * (10 ** decimals()); 38 | } 39 | 40 | function destroy() public onlyOwner { 41 | selfdestruct(owner); 42 | } 43 | 44 | modifier onlyOwner { 45 | require(msg.sender == owner, "Only the owner can call this function"); 46 | _; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /faucet-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ocean-token/contracts/Faucet.sol: -------------------------------------------------------------------------------- 1 | // contracts/Faucet.sol 2 | // SPDX-License-Identifier: MIT 3 | 4 | pragma solidity ^0.8.17; 5 | 6 | interface IERC20 { 7 | function transfer(address to, uint256 amount) external returns (bool); 8 | 9 | function balanceOf(address account) external view returns (uint256); 10 | 11 | event Transfer(address indexed from, address indexed to, uint256 value); 12 | } 13 | 14 | contract Faucet { 15 | address payable owner; 16 | IERC20 public token; 17 | 18 | uint256 public withdrawalAmount = 50 * (10**18); 19 | uint256 public lockTime = 1 minutes; 20 | 21 | event Withdrawal(address indexed to, uint256 indexed amount); 22 | event Deposit(address indexed from, uint256 indexed amount); 23 | 24 | mapping(address => uint256) nextAccessTime; 25 | 26 | constructor(address tokenAddress) payable { 27 | token = IERC20(tokenAddress); 28 | owner = payable(msg.sender); 29 | } 30 | 31 | function requestTokens() public { 32 | require( 33 | msg.sender != address(0), 34 | "Request must not originate from a zero account" 35 | ); 36 | require( 37 | token.balanceOf(address(this)) >= withdrawalAmount, 38 | "Insufficient balance in faucet for withdrawal request" 39 | ); 40 | require( 41 | block.timestamp >= nextAccessTime[msg.sender], 42 | "Insufficient time elapsed since last withdrawal - try again later." 43 | ); 44 | 45 | nextAccessTime[msg.sender] = block.timestamp + lockTime; 46 | 47 | token.transfer(msg.sender, withdrawalAmount); 48 | } 49 | 50 | receive() external payable { 51 | emit Deposit(msg.sender, msg.value); 52 | } 53 | 54 | function getBalance() external view returns (uint256) { 55 | return token.balanceOf(address(this)); 56 | } 57 | 58 | function setWithdrawalAmount(uint256 amount) public onlyOwner { 59 | withdrawalAmount = amount * (10**18); 60 | } 61 | 62 | function setLockTime(uint256 amount) public onlyOwner { 63 | lockTime = amount * 1 minutes; 64 | } 65 | 66 | function withdraw() external onlyOwner { 67 | emit Withdrawal(msg.sender, token.balanceOf(address(this))); 68 | token.transfer(msg.sender, token.balanceOf(address(this))); 69 | } 70 | 71 | modifier onlyOwner() { 72 | require( 73 | msg.sender == owner, 74 | "Only the contract owner can call this function" 75 | ); 76 | _; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /faucet-ui/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /faucet-ui/src/ethereum/faucet.js: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | 3 | const faucetAbi = [ 4 | { 5 | inputs: [ 6 | { 7 | internalType: "address", 8 | name: "tokenAddress", 9 | type: "address", 10 | }, 11 | ], 12 | stateMutability: "payable", 13 | type: "constructor", 14 | }, 15 | { 16 | anonymous: false, 17 | inputs: [ 18 | { 19 | indexed: true, 20 | internalType: "address", 21 | name: "from", 22 | type: "address", 23 | }, 24 | { 25 | indexed: true, 26 | internalType: "uint256", 27 | name: "amount", 28 | type: "uint256", 29 | }, 30 | ], 31 | name: "Deposit", 32 | type: "event", 33 | }, 34 | { 35 | anonymous: false, 36 | inputs: [ 37 | { 38 | indexed: true, 39 | internalType: "address", 40 | name: "to", 41 | type: "address", 42 | }, 43 | { 44 | indexed: true, 45 | internalType: "uint256", 46 | name: "amount", 47 | type: "uint256", 48 | }, 49 | ], 50 | name: "Withdrawal", 51 | type: "event", 52 | }, 53 | { 54 | inputs: [], 55 | name: "getBalance", 56 | outputs: [ 57 | { 58 | internalType: "uint256", 59 | name: "", 60 | type: "uint256", 61 | }, 62 | ], 63 | stateMutability: "view", 64 | type: "function", 65 | }, 66 | { 67 | inputs: [], 68 | name: "lockTime", 69 | outputs: [ 70 | { 71 | internalType: "uint256", 72 | name: "", 73 | type: "uint256", 74 | }, 75 | ], 76 | stateMutability: "view", 77 | type: "function", 78 | }, 79 | { 80 | inputs: [], 81 | name: "requestTokens", 82 | outputs: [], 83 | stateMutability: "nonpayable", 84 | type: "function", 85 | }, 86 | { 87 | inputs: [ 88 | { 89 | internalType: "uint256", 90 | name: "amount", 91 | type: "uint256", 92 | }, 93 | ], 94 | name: "setLockTime", 95 | outputs: [], 96 | stateMutability: "nonpayable", 97 | type: "function", 98 | }, 99 | { 100 | inputs: [ 101 | { 102 | internalType: "uint256", 103 | name: "amount", 104 | type: "uint256", 105 | }, 106 | ], 107 | name: "setWithdrawalAmount", 108 | outputs: [], 109 | stateMutability: "nonpayable", 110 | type: "function", 111 | }, 112 | { 113 | inputs: [], 114 | name: "token", 115 | outputs: [ 116 | { 117 | internalType: "contract IERC20", 118 | name: "", 119 | type: "address", 120 | }, 121 | ], 122 | stateMutability: "view", 123 | type: "function", 124 | }, 125 | { 126 | inputs: [], 127 | name: "withdraw", 128 | outputs: [], 129 | stateMutability: "nonpayable", 130 | type: "function", 131 | }, 132 | { 133 | inputs: [], 134 | name: "withdrawalAmount", 135 | outputs: [ 136 | { 137 | internalType: "uint256", 138 | name: "", 139 | type: "uint256", 140 | }, 141 | ], 142 | stateMutability: "view", 143 | type: "function", 144 | }, 145 | { 146 | stateMutability: "payable", 147 | type: "receive", 148 | }, 149 | ]; 150 | 151 | const faucetContract = (provider) => { 152 | return new ethers.Contract( 153 | "0xE16738Fb636c83b198A71368dd0D580FBc3B993B", 154 | faucetAbi, 155 | provider 156 | ); 157 | }; 158 | 159 | export default faucetContract; 160 | -------------------------------------------------------------------------------- /ocean-token/test/OceanToken.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const hre = require("hardhat"); 3 | 4 | describe("OceanToken contract", function() { 5 | // global vars 6 | let Token; 7 | let oceanToken; 8 | let owner; 9 | let addr1; 10 | let addr2; 11 | let tokenCap = 100000000; 12 | let tokenBlockReward = 50; 13 | 14 | beforeEach(async function () { 15 | // Get the ContractFactory and Signers here. 16 | Token = await ethers.getContractFactory("OceanToken"); 17 | [owner, addr1, addr2] = await hre.ethers.getSigners(); 18 | 19 | oceanToken = await Token.deploy(tokenCap, tokenBlockReward); 20 | }); 21 | 22 | describe("Deployment", function () { 23 | it("Should set the right owner", async function () { 24 | expect(await oceanToken.owner()).to.equal(owner.address); 25 | }); 26 | 27 | it("Should assign the total supply of tokens to the owner", async function () { 28 | const ownerBalance = await oceanToken.balanceOf(owner.address); 29 | expect(await oceanToken.totalSupply()).to.equal(ownerBalance); 30 | }); 31 | 32 | it("Should set the max capped supply to the argument provided during deployment", async function () { 33 | const cap = await oceanToken.cap(); 34 | expect(Number(hre.ethers.utils.formatEther(cap))).to.equal(tokenCap); 35 | }); 36 | 37 | it("Should set the blockReward to the argument provided during deployment", async function () { 38 | const blockReward = await oceanToken.blockReward(); 39 | expect(Number(hre.ethers.utils.formatEther(blockReward))).to.equal(tokenBlockReward); 40 | }); 41 | }); 42 | 43 | describe("Transactions", function () { 44 | it("Should transfer tokens between accounts", async function () { 45 | // Transfer 50 tokens from owner to addr1 46 | await oceanToken.transfer(addr1.address, 50); 47 | const addr1Balance = await oceanToken.balanceOf(addr1.address); 48 | expect(addr1Balance).to.equal(50); 49 | 50 | // Transfer 50 tokens from addr1 to addr2 51 | // We use .connect(signer) to send a transaction from another account 52 | await oceanToken.connect(addr1).transfer(addr2.address, 50); 53 | const addr2Balance = await oceanToken.balanceOf(addr2.address); 54 | expect(addr2Balance).to.equal(50); 55 | }); 56 | 57 | it("Should fail if sender doesn't have enough tokens", async function () { 58 | const initialOwnerBalance = await oceanToken.balanceOf(owner.address); 59 | // Try to send 1 token from addr1 (0 tokens) to owner (1000000 tokens). 60 | // `require` will evaluate false and revert the transaction. 61 | await expect( 62 | oceanToken.connect(addr1).transfer(owner.address, 1) 63 | ).to.be.revertedWith("ERC20: transfer amount exceeds balance"); 64 | 65 | // Owner balance shouldn't have changed. 66 | expect(await oceanToken.balanceOf(owner.address)).to.equal( 67 | initialOwnerBalance 68 | ); 69 | }); 70 | 71 | it("Should update balances after transfers", async function () { 72 | const initialOwnerBalance = await oceanToken.balanceOf(owner.address); 73 | 74 | // Transfer 100 tokens from owner to addr1. 75 | await oceanToken.transfer(addr1.address, 100); 76 | 77 | // Transfer another 50 tokens from owner to addr2. 78 | await oceanToken.transfer(addr2.address, 50); 79 | 80 | // Check balances. 81 | const finalOwnerBalance = await oceanToken.balanceOf(owner.address); 82 | expect(finalOwnerBalance).to.equal(initialOwnerBalance.sub(150)); 83 | 84 | const addr1Balance = await oceanToken.balanceOf(addr1.address); 85 | expect(addr1Balance).to.equal(100); 86 | 87 | const addr2Balance = await oceanToken.balanceOf(addr2.address); 88 | expect(addr2Balance).to.equal(50); 89 | }); 90 | }); 91 | 92 | }); -------------------------------------------------------------------------------- /faucet-ui/src/App.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import "./App.css"; 3 | import { ethers } from "ethers"; 4 | import faucetContract from "./ethereum/faucet"; 5 | 6 | function App() { 7 | const [walletAddress, setWalletAddress] = useState(""); 8 | const [signer, setSigner] = useState(); 9 | const [fcContract, setFcContract] = useState(); 10 | const [withdrawError, setWithdrawError] = useState(""); 11 | const [withdrawSuccess, setWithdrawSuccess] = useState(""); 12 | const [transactionData, setTransactionData] = useState(""); 13 | 14 | useEffect(() => { 15 | getCurrentWalletConnected(); 16 | addWalletListener(); 17 | }, [walletAddress]); 18 | 19 | const connectWallet = async () => { 20 | if (typeof window != "undefined" && typeof window.ethereum != "undefined") { 21 | try { 22 | /* get provider */ 23 | const provider = new ethers.providers.Web3Provider(window.ethereum); 24 | /* get accounts */ 25 | const accounts = await provider.send("eth_requestAccounts", []); 26 | /* get signer */ 27 | setSigner(provider.getSigner()); 28 | /* local contract instance */ 29 | setFcContract(faucetContract(provider)); 30 | /* set active wallet address */ 31 | setWalletAddress(accounts[0]); 32 | } catch (err) { 33 | console.error(err.message); 34 | } 35 | } else { 36 | /* MetaMask is not installed */ 37 | console.log("Please install MetaMask"); 38 | } 39 | }; 40 | 41 | const getCurrentWalletConnected = async () => { 42 | if (typeof window != "undefined" && typeof window.ethereum != "undefined") { 43 | try { 44 | /* get provider */ 45 | const provider = new ethers.providers.Web3Provider(window.ethereum); 46 | /* get accounts */ 47 | const accounts = await provider.send("eth_accounts", []); 48 | if (accounts.length > 0) { 49 | /* get signer */ 50 | setSigner(provider.getSigner()); 51 | /* local contract instance */ 52 | setFcContract(faucetContract(provider)); 53 | /* set active wallet address */ 54 | setWalletAddress(accounts[0]); 55 | } else { 56 | console.log("Connect to MetaMask using the Connect Wallet button"); 57 | } 58 | } catch (err) { 59 | console.error(err.message); 60 | } 61 | } else { 62 | /* MetaMask is not installed */ 63 | console.log("Please install MetaMask"); 64 | } 65 | }; 66 | 67 | const addWalletListener = async () => { 68 | if (typeof window != "undefined" && typeof window.ethereum != "undefined") { 69 | window.ethereum.on("accountsChanged", (accounts) => { 70 | setWalletAddress(accounts[0]); 71 | }); 72 | } else { 73 | /* MetaMask is not installed */ 74 | setWalletAddress(""); 75 | console.log("Please install MetaMask"); 76 | } 77 | }; 78 | 79 | const getOCTHandler = async () => { 80 | setWithdrawError(""); 81 | setWithdrawSuccess(""); 82 | try { 83 | const fcContractWithSigner = fcContract.connect(signer); 84 | const resp = await fcContractWithSigner.requestTokens(); 85 | setWithdrawSuccess("Operation succeeded - enjoy your tokens!"); 86 | setTransactionData(resp.hash); 87 | } catch (err) { 88 | setWithdrawError(err.message); 89 | } 90 | }; 91 | 92 | return ( 93 |
94 | 118 |
119 |
120 |
121 |

Faucet

122 |

Fast and reliable. 50 OCT/day.

123 |
124 | {withdrawError && ( 125 |
{withdrawError}
126 | )} 127 | {withdrawSuccess && ( 128 |
{withdrawSuccess}
129 | )}{" "} 130 |
131 |
132 |
133 |
134 | 140 |
141 |
142 | 149 |
150 |
151 |
152 |

Transaction Data

153 |
154 |

155 | {transactionData 156 | ? `Transaction hash: ${transactionData}` 157 | : "--"} 158 |

159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | ); 167 | } 168 | 169 | export default App; 170 | --------------------------------------------------------------------------------