Overview
In this tutorial, you will learn how to
- create an ERC20 token smart contract from openzeppelin wizard with mint functionality
- deploy the contract to the Alfajore's testnet on Celo
- create API to fetch methods from the deployed smart contract to the backend
- use the celo-ethers-wrapper to make ethers compatible with celo network
Prerequisite
- To follow up this tutorial, a basic understanding on API and nestjs is necessary.
Generate smart contract from Hardhat
Open up your terminal and create an empty folder using this command
mkdir my-project && cd my-project
Then runyarn init -y
to initialize the project and then install hardhat using the commandyarn add --dev hardhat
.
After that, run
yarn hardhat
and select Create a typescript project and leave every other option as the default.
- Open up the project folder on visual studio code or any other text editor. It should look like this
In the contracts folder, delete the Lock.sol file and do so with the files in the scripts and tests folder too.
Next up, we will be using the openzeppelin contract library to generate the ERC20 token, instead of writing the code from
scratch. But first, we need to install it using this commandyarn add @openzeppelin/contracts
on the terminal. After the installation, go to openzeppelin wizard to generate the contract for your token. After selecting mintable, it should look like this
- On your text editor, in your contracts folder, create a new file called MyToken.sol and copy the codes from the openzeppelin wizard and paste it into the newly created file. Your file should look like this
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor() ERC20("MyToken", "MTK") {}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
The mint function in your contract is for creating the total supply of that token to the wallet address specified in the function
This is a basic solidity smart contract with the functionality of a GET and POST requests that we can use in the backend.
On your terminal, run the command
yarn hardhat compile
so as to compile the smart contract and generate the artefact folder, which is the JSON file containing useful informations about the smart contract. We will need this JSON object for the backend interaction with the contract.
Deploy smart contract to Alfajores network
We will be using hardhat to deploy our smart contract by setting up celo in our local environment.
- In your text editor, open up hardhat.config.ts file and add up this celo configuration code below
const config: HardhatUserConfig = {
defaultNetwork: "alfajores",
networks: {
alfajores: {
url: "https://alfajores-forno.celo-testnet.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0"
},
}
},
solidity: "0.8.18",
};
- In your project root directory, create a .env file where we will be storing our mnemonics. It should look like this
MNEMONICS = "paste your 12 mnemonic keys here"
- Next up, install the dotenv using this command
yarn add dotenv
so we can access the mnemonic variable in the config file. And then import the dotenv into the file
import * as dotenv from 'dotenv';
dotenv.config()
- Your hardhat.config.ts file should look like this
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import * as dotenv from 'dotenv';
dotenv.config()
const config: HardhatUserConfig = {
defaultNetwork: "alfajores",
networks: {
alfajores: {
url: "https://alfajores-forno.celo-testnet.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0"
},
}
},
solidity: "0.8.18",
};
export default config;
- To deploy our smart contract, go to your scripts folder, create a file and name it deploy.ts , copy & paste the code snippets below into it
import { ethers } from "hardhat";
import { MyToken__factory } from "../typechain-types";
async function main() {
const [signer] = await ethers.getSigners();
console.log("Deploying contracts with the account:", signer.address);
console.log("Account balance:", (await signer.getBalance()).toString()); //get the wallet balance of the signer account
const contractFactory = new MyToken__factory(signer);
console.log(`Deploying MyToken Contract...\n`);
const contract = await contractFactory.deploy();
const contractTxnReceipt = await contract.deployTransaction.wait();
console.log(`Deployed contract at address ${contractTxnReceipt.contractAddress}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
- Run this command on your terminal to deploy the contract
yarn hardhat run scripts/deploy.ts --network alfajores
and you should get an output similar to this
If you get an error showing insufficient gas, copy your address to the celo faucet and fund your account with test tokens.
Take note of the deployed contract address because it will be useful when making API calls to the backend
Setting up the backend using Nest.js
Nest.js is a framework for building Node.js server side applications.
Let's get started
- In your terminal, run this command
npm i -g @nestjs/cli
to install nest cli globally. - In your my-project directory, run
nest new backend
which will create a new folder consisting of the base structure of Nest files. After installation using yarn, your terminal should look like this
In the terminal, navigate into the backend folder using the command
cd backend
and runyarn start:dev
to start up the server and keep it in watch mode.Open up your browser and run
http://localhost:3000
and you will see the output of Hello World!, which means the server is running successfully.Let us install a package that will help in describing our RESTful APIs, nestjs/swagger. Run the command
yarn add @nestjs/swagger
to install.After the installation process is complete, in your src folder, open up the main.ts file and paste the code below into it
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Mint ERC20 Token on Celo Blockchain')
.setDescription('The ERC20 API description')
.setVersion('1.0')
.addTag('celo, alfajores testnet')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
- Open up your browser and enter the url
http://localhost:3000/api
. You will get an output like this
Integrating Smart contract to the Backend
- We will be using the ethers library. Run the command
yarn add ethers@^5.7.2
to install ethers. - Remember the artefact folder we generated after we compiled the smart contract? We will copy the contents in MyToken.json file into our backend folder.
- In the
backend/src
folder, create a new folder called assets and create a new file MyToken.json, paste the copied contents into the file. - Open up app.service.ts, import ethers and the json object. Copy and paste the code below into the file
import { Injectable } from '@nestjs/common';
import { ethers } from 'ethers';
import * as MyTokenJson from './assets/MyToken.json';
@Injectable()
export class AppService {
}
- We have a problem in importing Json, so we have to fix that by adding
"resolveJsonModule": true
to tsconfig.json file.
- Remember the contract address we got after deploying the smart contract from the script file in hardhat, we will be using that address to interact with the functions of that address. For now, let's store the address in the app.service.ts file. The file should look like this
import { Injectable } from '@nestjs/common';
import { ethers } from 'ethers';
import * as MyTokenJson from './assets/MyToken.json';
const CONTRACT_ADDRESS = "0xBEc9321DEDcB18e954cf63BF81A6688bA5E4415E" // paste your own contract address gotten from the terminal
@Injectable()
export class AppService {
getContractAddress(): string {
return CONTRACT_ADDRESS;
}
}
- Open your app.controller.ts file, copy and paste the codes below
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('/contract-address')
getContractAddress(): string {
return this.appService.getContractAddress();
}
}
- Refresh your swagger UI tab in your browser. You'll see the route to get contract address. Execute it and you will get 200 status response like the image below
- Next up, we will be using the ethers library that we installed so we can interact with the contract address.
First up, we will get the total supply which is a default method generated from an ERC20 token.
In your app.controller.ts file, add a new route called /total-suppy, which will be an async operation, returning a promise. The updated file will look like the codes below
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('/contract-address')
getContractAddress(): string {
return this.appService.getContractAddress();
}
@Get('/total-supply')
async getTotalSupply(): Promise<number> {
return this.appService.getTotalSupply();
}
}
- Next up, in your app.service.ts, we will be creating an asynchronous function called getTotalSupply. We will be using a package called celo-ethers-wrapper to be able to make ethers.js to be compatible with the celo network. In your backend directory of your project, install the package using
yarn add @celo-tools/celo-ethers-wrapper
. Update your app.service.ts with the codes below
import { Injectable } from '@nestjs/common';
import { ethers } from 'ethers';
import * as MyTokenJson from './assets/MyToken.json';
import { CeloProvider } from '@celo-tools/celo-ethers-wrapper'
const CONTRACT_ADDRESS = "0xBEc9321DEDcB18e954cf63BF81A6688bA5E4415E" // paste your own contract address gotten from the terminal
@Injectable()
export class AppService {
getContractAddress(): string {
return CONTRACT_ADDRESS;
}
async getTotalSupply(): Promise<number> {
// Connecting to Alfajores testnet
const provider = new CeloProvider('https://alfajores-forno.celo-testnet.org')
await provider.ready
const contract = new ethers.Contract(CONTRACT_ADDRESS, MyTokenJson.abi, provider );
return contract.totalSupply();
}
}
- Refresh your swagger UI tab on your browser. After executing your /total-supply route, you should get an output like this
The return value we get is a Big Number. How do we convert it to a string?
Update your getTotalSupply async function with the lines of code below
const totalSupplyBigNumber = await contract.totalSupply();
const totalSupplyString = ethers.utils.formatEther(totalSupplyBigNumber);
const totalSupplyNumber = parseFloat(totalSupplyString);
return totalSupplyNumber;
- Head to the swagger UI tab in browser, refresh the page and execute the total supply route. You'll get a return value of 0 at the response body because we have not minted any token on our contract.
- Minting tokens requires a signer to sign the transaction and that signer has to be the owner or the deployer of the contract. We will use the celoWallet from the celo-ethers-wrapper to be able to sign transactions and make calls. To mint tokens in the backend, we will need to make a POST request. In the app.controller.ts file, we need to first import Post and create a new async method route called mintTokens. Your updated file should look like that
import { Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}m
@Get('/contract-address')
getContractAddress(): string {
return this.appService.getContractAddress();
}
@Get('/total-supply')
async getTotalSupply(): Promise<number> {
return this.appService.getTotalSupply();
}
@Post('/mint-tokens')
async mintTokens(){
return this.appService.mintTokens();
}
}
- In the app.service.ts file, import the CeloWallet from celo-ethers-wrapper and create an .env file in the backend directory which stores the private key of the account you used to deploy the smart contract because that is the signer account. Make sure you install dotenv with
yarn add dotenv
. Your updated file should look like this
import { Injectable } from '@nestjs/common';
import { ethers } from 'ethers';
import * as MyTokenJson from './assets/MyToken.json';
import { CeloProvider } from '@celo-tools/celo-ethers-wrapper'
import { CeloWallet } from "@celo-tools/celo-ethers-wrapper";
import * as dotenv from 'dotenv';
dotenv.config()
const CONTRACT_ADDRESS = "0xBEc9321DEDcB18e954cf63BF81A6688bA5E4415E" // paste your own contract address gotten from the terminal
const WALLET_ADDRESS = "0x122851EB3915cc769dECBf95a566e7fC8aAc2125" // paste your own wallet address
@Injectable()
export class AppService {
getContractAddress(): string {
return CONTRACT_ADDRESS;
}
async getTotalSupply(): Promise<number> {
// Connecting to Alfajores testnet
const provider = new CeloProvider('https://alfajores-forno.celo-testnet.org')
await provider.ready
const contract = new ethers.Contract(CONTRACT_ADDRESS, MyTokenJson.abi, provider );
const totalSupplyBigNumber = await contract.totalSupply();
const totalSupplyString = ethers.utils.formatEther(totalSupplyBigNumber);
const totalSupplyNumber = parseFloat(totalSupplyString);
return totalSupplyNumber;
}
async mintTokens(){
// Connecting to Alfajores testnet
const provider = new CeloProvider('https://alfajores-forno.celo-testnet.org')
await provider.ready
const wallet = new CeloWallet(process.env.PRIVATE_KEY, provider);
const contract = new ethers.Contract(CONTRACT_ADDRESS, MyTokenJson.abi, wallet );
const mint = await contract.mint(WALLET_ADDRESS, 100000);
return mint;
}
}
- In your swagger UI tab, refresh it and execute the mintTokens route which is a POST request. You'll get an output like this
This indicates that the POST request was successful and your tokens are minted based on the parameter you supplied in your mintFunction method.
- Next up, let's check the number of tokens we have by executing the /total-supply GET request. You should get an output like this base on the amount of times you execute the mintTokens POST request.
In my response body, I got the value of 3e+13 which is the E- notation version of 3*10^13.
You can view the details of your contract by searching up the contract address in the celo alfajores explorer
This is the transaction details from my contract address 0xbec9321dedcb18e954cf63bf81a6688ba5e4415e.
- This is the end of this tutorial and I hope you learned one or two from this tutorial and it helped in one way or the other. Let me know what you think. Cheers
Top comments (0)