Develop a Full Stack dApp on MELD
Last updated
Last updated
Decentralized applications, or DApps, have redefined how applications are built, managed, and interacted with in Web3. By leveraging blockchain technology, DApps provide a secure, transparent, and trustless system that enables peer-to-peer interactions without any central authority. At the core of a DApp's architecture are several main components that work in tandem to create a robust, decentralized ecosystem. These components include smart contracts, nodes, frontend user interfaces, and more.
In this tutorial, you'll come face-to-face with each major component by writing a full DApp that mints tokens. We'll also explore additional optional components of DApps that can enhance user experience for your future projects. You can view the complete project in its monorepo on GitHub.
To get started, you should have the following:
A wallet on the Kanazawa Testnet funded with tgMELD tokens. You can get tgMELD tokens for testing from our faucet.
Node.js version 16 or newer installed
VS Code with Juan Blanco's Solidity extension is a recommended IDE
Understanding of JavaScript and React
Novice familiarity with Solidity. If you are not familiar with writing Solidity, there are many resources out there, including Solidity by Example. A 15-minute skim should suffice for this tutorial
A wallet like MetaMask installed
Generally speaking, a JSON-RPC is a remote procedure call (RPC) protocol that utilizes JSON to encode data. For Web3, they refer to the specific JSON-RPCs that DApp developers use to send requests and receive responses from blockchain nodes, making it a crucial element in interactions with smart contracts. They allow frontend user interfaces to seamlessly interact with the smart contracts and provide users with real-time feedback on their actions. They also allow developers to deploy their smart contracts in the first place!
To get a JSON-RPC to communicate with the Kanazawa Testnet you need to run a node. But that can be expensive, complicated, and a hassle. Fortunately, as long as you have access to a node, you can interact with the blockchain. For this tutorial, we will be using the Kanazawa official public node endpoint.
So now you have a URL. How do you use it? Over HTTPS
, JSON-RPC requests are POST
requests that include specific methods for reading and writing data, such as eth_call
for executing a smart contract function in a read-only manner or eth_sendRawTransaction
for submitting signed transactions to the network (calls that change the blockchain state). The entire JSON request structure will always have a structure similar to the following:
This example is getting the balance (in tgMELD on Kanazawa Testnet) of the 0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac
account. Let's break down the elements:
jsonrpc
— the JSON-RPC API version, usually "2.0"
id
— an integer value that helps identify a response to a request. Can usually just keep it as is.
method
— the specific method to read/write data from/to the blockchain. You can see many of the RPC methods here.
params
— an array of the input parameters expected by the specific method
There are also additional elements that can be added to JSON-RPC requests, but those four will be seen the most often.
Now, these JSON-RPC requests are pretty useful, but when writing code, it can be a hassle to create a JSON object over and over again. That's why there exist libraries that help abstract and facilitate the usage of these requests. MELD provides documentation on many libraries, and the one that we will be using in this tutorial is Ethers.js. Just understand that whenever we interact with the blockchain through the Ethers.js package, we're really using JSON-RPC!
Smart contracts are self-executing contracts with the terms of the agreement directly written into code. They serve as the decentralized backend of any DApp, automating and enforcing the business logic within the system.
If coming from traditional web development, smart contracts are meant to replace the backend with important caveats: the user must have the native currency (gMELD, ETH, AVAX, etc.) to make state-changing requests, storing information can be expensive, and no information stored is private.
When you deploy a smart contract onto Kanazawa, you upload a series of instructions that can be understood by the EVM, or the Ethereum Virtual Machine. Whenever someone interacts with a smart contract, these transparent, tamper-proof, and immutable instructions are executed by the EVM to change the blockchain's state. Writing the instructions in a smart contract properly is very important since the blockchain's state defines the most crucial information about your DApp, such as who has what amount of money.
Since the instructions are difficult to write and make sense of at a low (assembly) level, we have smart contract languages such as Solidity to make it easier to write them. To help write, debug, test, and compile these smart contract languages, developers in the Ethereum community have created developer environments such as Hardhat and Foundry.
This tutorial will use Hardhat for managing smart contracts.
You can initialize a project with Hardhat using the following command:
Select a TypeScript Hardhat project, you will be asked if you want to install the sample project's dependencies, which will install Hardhat and the Hardhat Toolbox plugin. Please do so.
Also, please run:
Before we start writing the smart contract, let's add a JSON-RPC URL to the config. Replace the relevant code in thehardhat.config.js
file with the following code, and replace INSERT_YOUR_PRIVATE_KEY
with your funded account's private key.
Rember!
This is for testing purposes, never store your private key in plain text with real funds.
Recall that we're making a DApp that allows you to mint a token for a price. Let's write a smart contract that reflects this functionality!
Once you've initialized a Hardhat project, you'll be able to write smart contracts in its contracts
folder. This folder will have an initial smart contract, likely called Lock.sol
, but you should delete it and add a new smart file called MintableERC20.sol
.
The standard for tokens is called ERC-20, where ERC stands for "Ethereum Request for Comment". A long time ago, this standard was defined, and now most smart contracts that work with tokens expect tokens to have all of the functionality defined by ERC-20. Fortunately, you don't have to know it from memory since the OpenZeppelin smart contract team provides us with smart contract bases to use.
Install OpenZeppelin smart contracts:
Now, in your MintableERC20.sol
, add the following code:
When writing smart contracts, you're going to have to compile them eventually. Every developer environment for smart contracts will have this functionality. In Hardhat, you can compile with:
Everything should compile well, which should cause two new folders to pop up: artifacts
and cache
. These two folders hold information about the compiled smart contracts.
Let's continue by adding functionality. Add the following constants, errors, event, and function to your Solidity file:
This function will allow a user to send the native Kanazawa currency (tgMELD) as value because it is a payable function. Let's break down the function section by section.
It will figure out how much of the token to mint based on the value sent
Then it will check to see if the amount minted is 0 or if the total amount minted is over the MAX_TO_MINT
, giving a descriptive error in both cases
The contract will then forward the value included with the function call to the owner of the contract (by default, the address that deployed the contract, which will be you)
Finally, tokens will be minted to the user, and an event will be emitted to pick up on later
To make sure that this works, let's use Hardhat again:
You've now written the smart contract for your DApp! If this were a production app, we would write tests for it, but that is out of the scope of this tutorial. Let's deploy it next.
Under the hood, Hardhat is a Node project that uses the Ethers.js library to interact with the blockchain. You can also use Ethers.js in conjunction with Hardhat's tool to create scripts to do things like deploy contracts.
Your Hardhat project should already come with a script in the scripts
folder, called deploy.js
. Let's replace it with a similar, albeit simpler, script.
This script uses Hardhat's instance of the Ethers library to get a contract factory of the MintableERC20.sol
smart contract that we wrote earlier. It then deploys it and prints the resultant smart contract's address. Very simple to do with Hardhat and the Ethers.js library, but significantly more difficult using just JSON-RPC!
Let's run the contract on Kanazawa (whose JSON-RPC endpoint we defined in the hardhat.config.js
script earlier):
You should see an output that displays the token address. Make sure to save it for use later!
Frontends provide an interface for users to interact with blockchain-based applications. React, a popular JavaScript library for building user interfaces, is often used for developing DApp frontends due to its component-based architecture, which promotes reusable code and efficient rendering. ThirdWeb, an Ethers.js based SDK for DApps, further simplifies the process of building DApp frontends by providing a comprehensive set of hooks and components that streamline the integration of Ethereum blockchain functionality.
Note
Typically, a larger project will create separate GitHub repositories for their frontend and smart contracts, but this is a small enough project to create a monorepo.
Let's set up a new React project using Vite and install dependencies, which we can create within our Hardhat project's folder without much issue.
If you remember, Ethers.js is a library that assists with JSON-RPC communication. The Thirdweb package is a similar library that uses Ethers.js and formats them into React hooks so that they work better in frontend projects. We've also added two MUI packages for styling and components.
Let's set up the App.tsx
file located in the frontend/src
directory to add some visual structure:
You can start the React project by running the following command from within the frontend
directory:
Your frontend will be available at localhost:5173.
At this point, our frontend project is set up well enough to start working on the functional code!
The frontend communicates with the blockchain using JSON-RPC, but we will be using Ethers.js. When using JSON-RPC, Ethers.js likes to abstract degrees of interaction with the blockchain into objects, such as providers, signers, and wallets.
Providers are the bridge between the frontend user interface and the blockchain network, facilitating communication and data exchange. They abstract the complexities of interacting with the blockchain, offering a simple API for the frontend to use. They are responsible for connecting the DApp to a specific blockchain node, allowing it to read data from the blockchain, and essentially contain the JSON-RPC URL.
Signers are a type of provider that contain a secret that can be used to sign transactions with. This allows the frontend to create transactions, sign them, and then send them with eth_sendRawTransaction
. There are multiple types of signers, but we're most interested in wallet objects, which securely store and manage users' private keys and digital assets. Wallets such as MetaMask facilitate transaction signing with a universal and user-friendly process. They act as a user's representation within the DApp, ensuring that only authorized transactions are executed. The Ethers.js wallet object represents this interface within our frontend code.
Typically, a frontend using Ethers.js will require you to create a provider, connect to the user's wallet if applicable, and create a wallet object. This process can become unwieldy in larger projects, especially with the number of wallets that exist other than MetaMask.
Fortunately, we have installed the ThirdWeb package, which simplifies many of the processes for us. This simultaneously abstracts what Ethers is doing as well, which is why we took a bit of time to explain them here.
Create a Provider
Let's do a bit of setup with the ThirdWeb SDK. First, in your React frontend's App.tsx
file, which is located in the frontend/src
directory, add a ThirdwebSDKProvider
component. You'll need to sign up (for free) and get yourself a client id. We will be using the Thirdweb SDK throughout the project to simplify our interactions with the Kanazawa Testnet.
Connect to a Wallet
Now in your App.tsx
file, let's add a button that allows us to connect to MetaMask. We don't have to write any code that's wallet-specific, fortunately, since ThirdWeb does it for us with the ConnectWallet
component.
Now there should be a button in the center of your screen that connects your wallet to your frontend! Next, let's find out how we can read data from our smart contract.
Reading from contracts is quite easy, as long as we know what we want to read. For our application, we will be reading the maximum amount of tokens that can be minted and the number of tokens that have already been minted. This way, we can display to our users how many tokens can still be minted and hopefully invoke some FOMO...
If you were just using JSON-RPC, you would use eth_call
to get this data, but it's quite difficult to do this since you have to encode your requests in a non-straightforward method called ABI encoding. Fortunately, Ethers.js allows us to easily create objects that represent contracts in a human-readable way, so long as we have the ABI of the contract. And we have the ABI of the MintableERC20.sol
contract, MintableERC20.json
, within the artifacts
directory of our Hardhat project!
So let's start by moving the MintableERC20.json
file into our frontend directory. Every time you change and recompile the smart contract, you'll have to update the ABI in the frontend as well. Some projects will have developer setups that automatically pull ABIs from the same source, but in this case we will just copy it over:
Now that we have the ABI, we can use it to create a contract instance of MintableERC20.sol
, which we'll use to retrieve token data.
Create a New Component
Let's create a new component called <InteractWithContract /> to make sure we don't store all of our code in the main <App /> folder. Create a new InteractWithContract.tsx
file in your frontend/src
folder and add the following code:
Now let's update the App.tsx to include that component.
Create a Smart Contract Instance
Let's import the JSON file and the Ethers Contract
object within App.js
. We can create a contract object instance with an address and ABI, so replace INSERT_CONTRACT_ADDRESS
with the address of the contract that you copied back when you deployed it:
Interact with the Contract Interface to Read Supply Data
Now, we'll fetch the token supply data and display it from the smart contract using a useEffect hook and storing it in a contractData variable:
Notice that this component uses the contract.call
function provided by the ThirdWeb SDK. This method existing in the contract object we created earlier accepts a string method and any relevant arguments for the read-only call and returns the output. While it required some setup, this one-liner is a lot simpler than the entire use_call
RPC call that we would have had to do if we weren't using Ethers.js and ThirdWeb.
Also note that we're using a utility format called formatEther
to format the output values instead of displaying them directly. This is because our token, like gas currencies, is stored as an unsigned integer with a fixed decimal point of 18 figures. The utility function helps format this value into a way that we, as humans, expect.
Now we can spice up our frontend and call the read-only functions in the contract. We'll update the frontend so that we have a place to display our supply data:
Our frontend should now display the correct data!
Now for the most important part of all DApps: the state-changing transactions. This is where money moves, where tokens are minted, and value passes.
If you recall from our smart contract, we want to mint some tokens by calling the purchaseMint
function with some native currency. So we're going to need:
A text input that lets the user specify how much value to enter
A button that lets the user initiate the transaction signature
Let's keep adding more features to our current component. First, we'll tackle the text input, which will require us to add the logic to store the number of tokens to mint and a text field element.
Next, we'll need to create the button to send the transaction, which will call the purchaseMint
of our contract. Interacting with the contract will be a bit more difficult since you're likely not as familiar with it. We've already done a lot of setup in the previous sections, so it doesn't actually take too much code:
Let's break down the Web3Button a bit:
contractAddress
and contractAbi
are props that define which contract the button will interact with.
The action
prop is an asynchronous function that gets executed when the button is clicked. It calls the purchaseMint
function of the contract, passing the amount to mint (converted to Wei using toWei
).
refetch
is called after the transaction to update the local state with the latest contract data which will show a larger value after the mint has happened.
We don't have to handle any loading/disabled states because the Web3Button handles this for us!
That's pretty much it! Your Dapp should probably now look something like this.
If you try entering a value like 0.1 (make sure to have 0.1 tgMELD in your wallet) and press the button, a MetaMask prompt should occur. Try it out!
A common way of listening to what happened in a transaction is through events, also known as logs. These logs are emitted by the smart contract through the emit
and event
keywords and can be very important in a responsive frontend. Often, DApps will use toast elements to represent events in real-time, but for this DApp, we will use a simple table.
We created an event in our smart contract: event PurchaseOccurred(address minter, uint256 amount)
, so let's figure out how to display its information in the frontend.
Let's add some additional logic to handle events emitted:
Here's what happens in this code:
The state holds data related to the contract: totalSupply
, maxSupply
, and now an array of events
.
The useEffect now calls the getAllEvents() contract function and filters for all events with any arguments on the contract with the event name PurchaseOccurred.
We now render the list of events from the contractData
. For each event, we display information such as the minter's address and the amount, formatted for better readability.
And, if you've done any transactions, you'll see that they'll pop up!
Now you've implemented three main components of DApp frontends: reading from storage, sending transactions, and reading logs. With these building blocks as well as the knowledge you gained with smart contracts and nodes, you should be able to cover 80% of DApps.
You can view the complete example DApp on GitHub.