├── .env
├── .env.local.example
├── .eslintrc.json
├── .gitignore
├── .prettierrc.json
├── LICENSE.md
├── README.md
├── abi
├── dummyToken.abi.json
└── stakingVault.abi.json
├── contracts
├── DummyToken.sol
└── StakingVault.sol
├── hardhat.config.js
├── index.html
├── justfile
├── netlify.toml
├── package-lock.json
├── package.json
├── public
├── favicon.svg
└── opn.png
├── scripts
├── deployStaking.js
└── deployToken.js
├── src
├── App.jsx
├── components
│ ├── dummyToken.jsx
│ └── staking.jsx
├── main.jsx
└── web3.js
└── vite.config.js
/.env:
--------------------------------------------------------------------------------
1 | VITE_DUMMY_TOKEN_ADDRESS="0xAf400297EaBb34dA11bBE3AD51DD1f3a6C4BbcCD"
2 | VITE_STAKING_ADDRESS="0x34f66A45f5773A8aF8251f418ddD1272BDa831BD"
3 |
--------------------------------------------------------------------------------
/.env.local.example:
--------------------------------------------------------------------------------
1 | BSC_SCAN_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2 | GETBLOCKIO_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3 | PRIVATE_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": ["eslint:recommended", "plugin:react/recommended", "prettier"],
7 | "overrides": [],
8 | "parserOptions": {
9 | "ecmaFeatures": {
10 | "jsx": true
11 | },
12 | "ecmaVersion": "latest",
13 | "sourceType": "module"
14 | },
15 | "settings": {
16 | "react": {
17 | "version": "detect"
18 | }
19 | },
20 | "plugins": ["react", "prettier"],
21 | "rules": { "prettier/prettier": ["error"], "react/prop-types": 0 }
22 | }
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | # Hardhat files
27 | cache
28 | artifacts
29 |
30 | # Local Netlify folder
31 | .netlify
32 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 4,
4 | "arrowParens": "avoid",
5 | "semi": true
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | ## Copyright (c) 2022 Atahan Yorgancı
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 | # dApp Development with React Workshop
2 |
3 | 
4 | 
5 | 
6 | [](https://ata-token.netlify.app)
7 |
8 | In this workshop we will be building a DeFi application with a custom ERC20 token and staking vault using [vite][vite] to bundle a [React.js][react] application and [ethers][ethers] library to connect to the blockchain. You can checkout the finished project [here][production]. The project is deployed on [Avalanche Fuji Testnet][fuji], you can receive funds from testnet's faucet [here][faucet].
9 |
10 | ## Initialize React Application with [`vite`][vite]
11 |
12 | [`vite`][vite] next generation tooling for building frontend applications. Get started with [`vite`][vite] by running following command.
13 |
14 | ```bash
15 | npm create vite@latest
16 | ```
17 |
18 | We can run the development server using `npm run dev` and local server will start will start with automatic reloads!
19 |
20 | > You can remove unnecessary markup and CSS that `vite` creates
21 |
22 | ## Connecting to MetaMask
23 |
24 | MetaMask adds a global object `ethereum` that can be used to interact with MetaMask. This object can be accessed by `window.ethereum`.
25 |
26 | We can send requests to MetaMask using `window.ethereum.request()` method, we ask the user to connect their account by sending the `eth_requestAccounts` request. Details of this request can be found in [MetaMask's documentation](https://docs.metamask.io/guide/rpc-api.html#eth-requestaccounts).
27 |
28 | ```js
29 | const [account] = await window.ethereum.request({
30 | method: "eth_requestAccounts",
31 | });
32 | ```
33 |
34 | We can use `useEffect` and `useState` hooks to initialize `account` state when the component mounts.
35 |
36 | > Providing `[]` (empty array) to dependency section of `useEffect(func, [])`
37 | > runs `func` only once when the component mounts!
38 |
39 | ```js
40 | const requestAccounts = async () => {
41 | if (!window.ethereum) {
42 | return null;
43 | }
44 | const [account] = await window.ethereum.request({
45 | method: "eth_requestAccounts",
46 | });
47 | return account;
48 | };
49 |
50 | function App() {
51 | const [account, setAccount] = useState(null);
52 |
53 | useEffect(() => {
54 | requestAccounts().then(setAccount).catch(console.error);
55 | }, []);
56 |
57 | //...
58 | }
59 | ```
60 |
61 | How ever there are two problems here! First the user gets prompted before they can see the app and if they change their account the app doesn't update!
62 |
63 | ### Listening to Account Changes
64 |
65 | `ethereum` object is an `EventEmitter` so we can listen to `accountsChanged` event when we initialize our application.
66 |
67 | ```js
68 | window.ethereum.on("accountsChanged", accounts => {
69 | setAccount(accounts[0]); // set new account state
70 | });
71 | ```
72 |
73 | ### Accessing Accounts
74 |
75 | There is an alternative method called `eth_accounts` that query MetaMask if the user has already connected their account to our application. If user has already connected their account MetaMask will simply return the account without prompting.
76 |
77 | ```js
78 | const [account] = await window.ethereum.request({
79 | method: "eth_accounts",
80 | });
81 | ```
82 |
83 | ## Interacting with EVM using [`ethers`][ethers]
84 |
85 | We will be using [`ethers`][ethers] to interact with the blockchain. `ethers` can be installed with `npm` simply by running following command in the terminal.
86 |
87 | ```bash
88 | npm i ethers
89 | ```
90 |
91 | `ethers` library includes multiple types of providers for accessing onchain data. These include popular providers like [`InfuraProvider`](https://docs.ethers.io/v5/api/providers/api-providers/#InfuraProvider) (a popular JSON-RPC endpoint provider, [website](https://infura.io/)), generic providers such as [`JsonRpcProvider`](https://docs.ethers.io/v5/api/providers/api-providers/#InfuraProvider) and [`Web3Provider`](https://docs.ethers.io/v5/api/providers/other/#Web3Provider) which connects using MetaMask.
92 |
93 | We can initialize our provider with global `ethereum` object as follows.
94 |
95 | ```js
96 | import { ethers } from "ethers";
97 |
98 | // ...
99 |
100 | const provider = new ethers.providers.Web3Provider(window.ethereum);
101 | ```
102 |
103 | Having initialized our provider we can now access chain data! Let's start by building a `Balance` React component that display's chain's default coin in this case AVAX. `Balance` component receives `account` and `provider` as props and computes the balance and displays it.
104 |
105 | ```jsx
106 | const Balance = ({ provider, account }) => {
107 | const [balance, setBalance] = useState("");
108 |
109 | useEffect(() => {
110 | const getBalance = async () => {
111 | const balance = await provider.getBalance(account);
112 | return ethers.utils.formatEther(balance);
113 | };
114 | getBalance().then(setBalance).catch(console.error);
115 | }, [account, provider]);
116 |
117 | if (!balance) {
118 | return
Loading...
;
119 | }
120 | return
Balance: {balance} AVAX
;
121 | };
122 | ```
123 |
124 | We derive our balance state from `account` and `provider` using `useEffect` hook. If the user changes their account their balance is recalculated. We use `provider.getBalance(account)` function to access user's AVAX balance and convert it to string using `formatEther` function.
125 |
126 | > In EVM balance of a ERC20 token is stored as a unsigned 256-bit integer. However, JavaScript `Number` type is a [double-precision 64-bit binary format IEEE 754][float] so balance of an account can be larger than JavaScript's numbers allow. `ethers` library represents these numbers as `BigNumber` type and `formatEther` utility function can be used to convert `BigNumber` to `String`.
127 |
128 | ## Balance of Custom ERC20 Token
129 |
130 | What is web3 without custom tokens? Let's bring our project's ERC20 token into our application. To do this, we will be using `ethers.Contract`. This class can be used to instantiate custom EVM contracts from their address and ABI.
131 |
132 | > `ethers.Contract` is a meta class under the hood, meaning it's a class that creates classes not instances! `Contract` class receives ABI and constructs a new class that has ABI's exported properties.
133 |
134 | For our purposes ERC20 token called 'DummyToken (DT)' has deployed to Avalanche Fuji testnet at `0x5E8F49F4062d3a163cED98261396821ae2996596`. We can use [SnowTrace block explorer][snowtrace] to inspect contract's methods and ABI, token contract on explorer can be found [here](https://testnet.snowtrace.io/address/0x5E8F49F4062d3a163cED98261396821ae2996596). We can import ABI as a regular JSON file and initialize contract!
135 |
136 | ```js
137 | import DummyTokenABI from "../../abi/dummyToken.abi.json"; // Path to ABI's JSON file
138 |
139 | const DUMMY_TOKEN_ADDRESS = "0x5E8F49F4062d3a163cED98261396821ae2996596";
140 | const DUMMY_TOKEN = new ethers.Contract(DUMMY_TOKEN_ADDRESS, DummyTokenABI);
141 | ```
142 |
143 | Then, we can read token balance using `balanceOf(address)` method similar to AVAX balance.
144 |
145 | ```js
146 | useEffect(() => {
147 | const getBalance = async () => {
148 | const dummyToken = DUMMY_TOKEN.connect(provider);
149 | const balance = await dummyToken.balanceOf(account);
150 | return ethers.utils.formatEther(balance);
151 | };
152 | getBalance().then(setBalance).catch(console.error);
153 | }, [provider, account]);
154 | ```
155 |
156 | As expected DummyToken balance turns out to be 0. Fortunately, DummyToken contract exports a function to obtain some tokens.
157 |
158 | ## Claiming DummyToken
159 |
160 | We can check if an account has claimed using a similar function `hasClaimed()` function and we can modify `useEffect` to check when the `AtaBalance` mounts.
161 |
162 | ```js
163 | const getBalanceAndClaimed = async () => {
164 | const dummyToken = DUMMY_TOKEN.connect(provider);
165 | const [balance, claimed] = await Promise.all([
166 | dummyToken.balanceOf(account),
167 | dummyToken.hasClaimed(),
168 | ]);
169 | return [ethers.utils.formatEther(balance), claimed];
170 | };
171 | ```
172 |
173 | > `Promise.all([awaitable1, awaitable2, ...])` can be used to await multiple async calls at the same time and receive resolved promises in order awaitables' order.
174 |
175 | If the user hasn't claimed we can a render a button that when pressed will invoke `claim()` method on the contract. Since, we are modifying state of blockchain it's not enough for us to use a [`Provider`][provider] as they provide a **readonly** view of blockchain. We will be using [`Signer`][signer] which can used to send transactions.
176 |
177 | ```js
178 | const claim = async () => {
179 | const signer = provider.getSigner();
180 | const dummyToken = DUMMY_TOKEN.connect(signer);
181 |
182 | const tx = await dummyToken.claim();
183 | await tx.wait();
184 | };
185 | ```
186 |
187 | If we refresh the page we can see our funds arrive! However, it isn't such a good user experience if they have to refresh the page every time they make transaction. With some refactoring we can solve this issue.
188 |
189 | ```js
190 | const getBalanceAndClaimed = async (account, provider) => {
191 | const dummyToken = DUMMY_TOKEN.connect(provider);
192 | const [balance, claimed] = await Promise.all([
193 | dummyToken.balanceOf(account),
194 | dummyToken.hasClaimed(account),
195 | ]);
196 | return [ethers.utils.formatEther(balance), claimed];
197 | };
198 |
199 | const DummyToken = ({ account, provider }) => {
200 | // `DummyToken` component state
201 |
202 | const claim = async () => {
203 | // ...
204 | await tx.wait();
205 |
206 | getBalanceAndClaimed(account, provider)
207 | .then(/* set balance and claimed */)
208 | .catch();
209 | };
210 |
211 | useEffect(() => {
212 | getBalanceAndClaimed(account, provider)
213 | .then(/* set balance and claimed */)
214 | .catch();
215 | }, [provider, account]);
216 |
217 | // ...
218 | };
219 | ```
220 |
221 | ## Adding DummyToken to MetaMask
222 |
223 | Even tough, users can claim their tokens, DummyToken doesn't show up in MetaMask wallet. We can remedy this situation by sending `wallet_watchAsset` request through global `ethereum` object. We provide address of the token, symbol, decimals and lastly image for MetaMask to use.
224 |
225 | ```js
226 | const addDummyTokenToMetaMask = async () => {
227 | if (!window.ethereum) {
228 | return false;
229 | }
230 | try {
231 | const added = await window.ethereum.request({
232 | method: "wallet_watchAsset",
233 | params: {
234 | type: "ERC20",
235 | options: {
236 | address: DUMMY_TOKEN_ADDRESS,
237 | symbol: "DT",
238 | decimals: 18,
239 | image: "https://ata-token.netlify.app/opn.png",
240 | },
241 | },
242 | });
243 | return added;
244 | } catch (error) {
245 | return false;
246 | }
247 | };
248 | ```
249 |
250 | ## Integrating Staking Contract
251 |
252 | ERC20 allocation staking is one of most common practices in web3 launchpads and DeFi applications. Usually, users lock some amount of funds into smart contract and receive certain amount of rewards funds in return as interest. In case of launchpads like [OpenPad][openpad] in addition to receiving interest users are able to invest in launchpad project.
253 |
254 | Lastly, for our application we will integrating a staking contact. DummyToken staking contract is deployed at `0xAC1BdE0464D932bf1097A9492dCa8c3144194890` and we can inspect the contract code and ABI [here](https://testnet.snowtrace.io/address/0xAC1BdE0464D932bf1097A9492dCa8c3144194890#code).
255 |
256 | Staking contract exports stake and reward token amount for a given address and also total staked token amounts. We can read these values like any other contract value using `stakedOf()`, `rewardOf()` and `totalStaked()` respectively.
257 |
258 | ```js
259 | const getStakingViews = async (account, provider) => {
260 | const signer = provider.getSigner(account);
261 | const staking = STAKING_CONTRACT.connect(signer);
262 | const [staked, reward, totalStaked] = await Promise.all([
263 | staking.stakedOf(account),
264 | staking.rewardOf(account),
265 | staking.totalStaked(),
266 | ]);
267 | return {
268 | staked: ethers.utils.formatEther(staked),
269 | reward: ethers.utils.formatEther(reward),
270 | totalStaked: ethers.utils.formatEther(totalStaked),
271 | };
272 | };
273 | ```
274 |
275 | ### Staking and Withdrawing Funds
276 |
277 | Users can stake their tokens using `stake(uint256 amount)` function and withdraw their locked funds using `withdraw(uint256 amount)` function. Most important of them all they can claim rewards using `claimReward()` function. Since these functions modify state of the contract we have to use a [`Signer`][signer].
278 |
279 | We can write a simple form for user to fill out while staking and fire off relevant contract function when the form is submitted.
280 |
281 | ```js
282 | const Staking = ({ account, provider }) => {
283 | // ...
284 | const [stake, setStake] = useState("");
285 |
286 | const handleStake = async event => {
287 | event.preventDefault(); // prevent page refresh when form is submitted
288 | const signer = provider.getSigner(account);
289 | const staking = STAKING_CONTRACT.connect(signer);
290 |
291 | const tx = await staking.stake(ethers.utils.parseEther(stake), {
292 | gasLimit: 1_000_000,
293 | });
294 | await tx.wait();
295 | };
296 | // ...
297 | return (
298 |