├── 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 |
--------------------------------------------------------------------------------