;
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 |
299 | {/* ... */}
300 |
312 | {/* ... */}
313 |
314 | );
315 | };
316 | ```
317 |
318 | Withdrawing funds from contract can be implemented similarly. However, if we try staking our tokens the contract will throw out an error! This is due to fact that we are not transferring native currency of the chain. While transferring ERC20 tokens into a contract we have **approve** a certain amount of **allowance** for that contract to use.
319 |
320 | ### Allowance and Approval
321 |
322 | We can check if for allowance of a smart contract -_spender_- from an address -_owner_- on ERC20 contract using `allowance(owner, spender)` view function. If allowance is less than amount we want stake, we have to increase the allowance by signing `approve(spender, amount)` message.
323 |
324 | ```js
325 | const handleStake = async event => {
326 | const signer = provider.getSigner(account);
327 | const amount = ethers.utils.parseEther(stake);
328 |
329 | const dummyToken = DUMMY_TOKEN.connect(signer);
330 | const allowance = await dummyToken.allowance(
331 | account,
332 | STAKING_CONTRACT.address
333 | );
334 | if (allowance.lt(amount)) {
335 | const tx = await dummyToken.approve(STAKING_CONTRACT.address, amount);
336 | await tx.wait();
337 | }
338 | // ...
339 | };
340 | ```
341 |
342 | Voila! With allowance out of our way, we are free to stake and withdraw funds as we like.
343 |
344 | ### Claiming Rewards
345 |
346 | What is DeFi without rewards? Let's finish off our application by allowing users to claim their rewards. This is simple task since we aren't spending ERC20 tokens we don't have to deal with the allowance. The user only has to sign `claimRewards()` function and we are done!
347 |
348 | ```js
349 | const handleClaimReward = async () => {
350 | const signer = provider.getSigner(account);
351 | const staking = STAKING_CONTRACT.connect(signer);
352 |
353 | const tx = await staking.claimReward({
354 | gasLimit: 1_000_000,
355 | });
356 | await tx.wait();
357 | };
358 | ```
359 |
360 | ## Next Steps
361 |
362 | - Add [TypeScript](https://www.typescriptlang.org/) support for large DeFi applications
363 | - Add [`@tanstack/react-query`](https://tanstack.com/query/v4/) for async state management
364 | - More smart contracts! Mint NFTs with ERC721?
365 |
366 | [react]: https://reactjs.org
367 | [vite]: https://vitejs.dev/
368 | [ethers]: https://github.com/ethers-io/ethers.js
369 | [float]: https://en.wikipedia.org/wiki/Floating-point_arithmetic
370 | [snowtrace]: https://snowtrace.io
371 | [provider]: https://docs.ethers.io/v5/api/providers/provider/
372 | [signer]: https://docs.ethers.io/v5/api/signer/#Signer
373 | [openpad]: https://openpad.app
374 | [production]: https://ata-token.netlify.app
375 | [fuji]: https://docs.avax.network/quickstart/fuji-workflow
376 | [faucet]: https://faucet.avax.network/
377 |
--------------------------------------------------------------------------------