In this post I'll be developing a decentralised application to operate with a mintable ERC-20 token.
The purpose is then to understand the end-to-end process of writing a smart contract in Solidity and developing a specific front end to interact with this contract.
Note: this application is not production ready code. Its implementation is solely for learning purposes.
In this article
Setup
In this guide npm 7 workspaces is used to create a monorepo in order to simplify our development environment.
Create a new directory and navigate to it. Also, create a new Node project with npm init.
Edit the package.json:
package.json
Create the workspaces folders:
Install the Hardhat environment dependencies:
Now install the react-app dependencies:
Add some scripts that will be handy:
hardhat-env/package.json
react-app/package.json
package.json
Lastly, create a hardhat configuration file and import the plugins.
In case we use Metamask we'll need to either assign in hardhat.config.ts1337 to the chainId of our local network, or set the chainId of localhost in Metamask to 31337:
hardhat-env/hardhat.config.ts
Writing the contract
The smart contract is written in Solidity and complies with the EIP-20 standard specifications.
hardhat-env/contracts/MintableERC20.sol
When the contract gets deployed, its state variables will be initialised, two of which are passed as arguments to the constructor function, namely name and symbol.
The logic of the contract is fairly simple.
Only the deployer address will be permitted to mint tokens.
In order to transfer tokens, it is required that the sender address does possess at least the same amount of tokens it intends to transfer.
In case that an address requests for another address to transfer an amount of tokens, it will need to be previously allowed, i.e., before transferFrom is called, the spender address will have to be approved so that it can get the requested amount of tokens from the owner.
The transactions as well as the approvals will emit an Event. Later, I will use the transaction event to keep track of the balance of the current account in the decentralised app.
Testing the contract
These operations may be better understood by testing the contract.
In this guide a hardhat plugin for integration with Waffle and another for Ethers.js are used to write the required tests with Mocha alongside Chai.
This tests are structured in three main describe calls:
hardhat-env/test/mintableerc20.spec.ts
Now run the script test-hardhat:
As we can see, hardhat test will compile the solidity contract and generate typings beforehand. This will create the folders artifacts, cache, and typechain-types.
We are mostly interested in MintableERC20.json under /hardhat-env/artifacts/contracts/MintableERC20.sol, and MintableERC20.ts under /hardhat-env/typechain-types, as they contain the ABI and the typings that have been generated, respectively.
We'll use them in our React application to interact with the smart contract using Ethers.js.
In order to import the files that we need from the workspace hardhat-env to react-app, we could do so simply with import statements.
However, in case we'd like to keep a copy of those files in react-app or just to avoid naming conflicts with packages installed in our workspace and other workspaces names, we could run a simple script to read the files we need and write them in react-app to import them from there:
hardhat-env/scripts/shareFiles.ts
In order to have tokens at our disposal it is only needed a Hardhat task to mint and send tokens to a specified address.
This is easily accomplished by adding a simple task under the /hardhat/tasks folder.
To simplify the process, a pre-specified amount of 100 TFT is set.
hardhat-env/tasks/mint.ts
Lastly, it is just needed to import this file in the hardhat configuration:
hardhat-env/hardhat.config.ts
Deploying the contract
In Hardhat we can deploy a contract in a localhost network. To start a local node in this app, run npm run node, and to deploy the contract npm run compile-share-deploy.
The latter command will, furthermore, ensure our React app has the current contract address, typings and ABI after compiling the solidity file and deploying it in the localhost network.
Front-end
Our front end is a simple React.js application written in TypeScript that consists of three main components to show the balance, to make transactions and to check the history of incoming and outgoing transactions.
In order for our DApp to work it is needed that a crypto wallet is installed in the browser and injects the ethereum property to the window.
If either a different account or a different chain are set in our ethereum provider, the page will reload.
react-app/src/utils/hooks/index.ts
Once we start the react application we are requested to enable Ethereum which will fetch an account address and will create two contracts.
react-app/src/utils/helpers.ts
Using the Ethers.js library I create two contract objects, one to perform read-only operations and another one to perform transactions.
To create both contracts it is needed to pass a contract address, a contract interface (or ABI) and either a signer or a provider.
For the read-only contract a JsonRpcProvider is passed, and a signer for the contract that will be used to sign operations to change the state of the blockchain.
I keep the account address and both contracts in ContractContext which will provide us with them in any of the components that are descendants to ContractProvider.
Once the data in the context is defined, the components Balance, TransferFrom and Panels will be rendered.
They show the TFT (Test Fungible Token, our token) balance of the context account address in the contract, a form to transfer TFTs and a panel with incoming and outgoing transactions respectively.
I set listeners inside useEffects in custom hooks to keep all three main components up to date with the state in the blockchain (also TransferFrom, since the input data are validated using the Formik library).
In order to keep the balance synched with the state of the _balance mapping of our account in MintableERC20.sol, we use the event Transfer in our contract to create two filters:
filterReceived: this filter will keep all the transactions to our account (including the mint operations).
filterSent: this filter will keep all the transactions from our account.
We then set listeners to these events to update our application balance state.
react-app/src/utils/hooks/index.ts
A similar operation is performed in useTransactions, however, here the listeners will set the state of the transfers to an Event array.
react-app/src/utils/hooks/index.ts
We map over these arrays in Panels to show the transactions.
Below we can see the mapping of our outgoing transactions. The receiver address corresponds to event.args[1] and the amount of TFT sent corresponds to event.args[2].
react-app/src/components/Panels.tsx
Now that all the logic is implemented, just run npm run start-app.
Olive Oil Trust smart contracts are implemented in order to adopt a set of rules...
Ready to #buidl?
Are you interested in Web3 or the synergies between blockchain technology, artificial intelligence and zero knowledge?. Then, do not hesitate to contact me by e-mail or on my LinkedIn profile. You can also find me on GitHub.