Introduction
In this tutorial, you will learn how to create a Donation dApp on Polygon to award your favorite content creator with MATIC tokens and how to deploy your smart contract on the Polygon network.
This is what the dApp we will be creating looks like:
Prerequisites
To successfully follow along with this tutorial, you will need a basic knowledge and understanding of blockchain, Solidity smart contracts and React.
Requirements
- You will need Metamask installed in your browser from the official site https://metamask.io.
- You must have a recent version of Node.js installed. We recommend using v14.17.6 LTS for compatibility.
Other technologies used in this tutorial:
- Solidity - For writing smart contracts
- Truffle - Truffle provides development environment for developing blockchain applications locally
- NextJS - To Create frontend for our Dapp
- Web3.js - Javascript librabry to connect our frontend to smart contract
- Polygon Network - For deploying our smart contract
- IPFS - To store image and videos uploaded by content creators
Topics covered in this tutorial:
- Setting up development for Solidity and NextJs
- Creating smart contracts using Solidity
- Compiling and migrating smart contracts using truffle
- Writing tests for our smart contract
- Linking smart contract to frontend using web3js
- Creating UI using TailwindCSS in NextJs
- Using IPFS to upload images'
- Publishing the smart contract to Polygon Testnet
Project setup
To set up the project files, we will have to install a few packages using the node package manager npm. This software comes bundled with recent versions of Node.js, but you will still want to make sure it is installed with the command
npm -v
. Run the following commands in order to install the packages and create the project directories:npm install -g truffle # Install truffle globally so that you can use truffle from any directory npx create-next-app --typescript # Install nextjs and setup typescript for your project cd polygon-dapp truffle init # Create truffle-config.js test/ and contracts/ directory
When asked during the
create-next-app
setup, use "polygon-dapp" as the project name so that it will be easy for you to follow along with this tutorial. This is how the folder structure will be after initial setup:The
truffle init
command creates the following directories:contracts/
: All the smart contracts are stored in this directory.migrations/
: All the scripts used by Truffle to deploy smart contracts are stored in this directory.test/
: All the test scripts for smart contracts are stored in this directory.truffle-config.js
: Contains configuration settings for truffle.
Creating the Solidity smart contract
Create a new file called
DonationContract.sol
in the contracts
directory and add the following code:// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.5.16; contract DonationContract { // Keep track of total number of images in contract uint256 public imageCount = 0; // Data structure to store images data struct Image { uint256 id; string hash; string description; uint256 donationAmount; address payable author; } mapping(uint256 => Image) public images; // Event emitted when image is created event ImageCreated( uint256 id, string hash, string description, uint256 donationAmount, address payable author ); // Event emitted when an there is a donation event DonateImage( uint256 id, string hash, string description, uint256 donationAmount, address payable author ); }
imageCount
is an unsigned public integer that stores the total number of images in the smart contract.struct Image
is a data structure to store information (metadata) about each image. This includes the id of the image, an IPFS hash for that image, a description added by the content creator, a total donation amount received on that image, and the author address where the donation will be sent to.mapping(uint256 => Image) public images
is a mapping that stores all the images, where the key is theid
and value is theImage
struct.ImageCreated
andDonateImage
are the events emitted on the blockchain that our dApp can listen for, and function accordingly.
// Create an Image function uploadImage(string memory _imgHash, string memory _description) public { require(bytes(_imgHash).length > 0); require(bytes(_description).length > 0); require(msg.sender != address(0x0)); imageCount++; images[imageCount] = Image( imageCount, _imgHash, _description, 0, msg.sender ); emit ImageCreated(imageCount, _imgHash, _description, 0, msg.sender); }
uploadImage
accepts an image hash and description as parameters. In the function, we make sure theimageHash
anddescription
are not empty and the sender address is not empty. Then we increment theimageCount
by one and create a new Image struct object and store it in theimages
map withimageCount
as key.msg
is a global variable that contains the address of the person who called the function and in our case the author of the content. So to store the address ofauthor
we can directly usemsg.sender
- Once the object is stored in the map we can emit the
ImageCreated
event with relevant data.
function donateImageOwner(uint256 _id) public payable { require(_id > 0 && _id <= imageCount); Image memory _image = images[_id]; address payable _author = _image.author; address(_author).transfer(msg.value); _image.donationAmount = _image.donationAmount + msg.value; images[_id] = _image; emit DonateImage( _id, _image.hash, _image.description, _image.donationAmount, _author ); }
donateImageOwner
is a public payable function that accepts the id of the image. Since the function ispayable
, the global variablemsg
contains a field calledvalue
that has the number of coins which is sent for the donation.- In the function body, we check the
id
parameter to make sure it is not zero, or outside the bounds of the curentimageCount
. Then we extract the image from theimages
map by its_id
(this was passed in as the function argument) and store it in the_image
variable, so from the_image
variable we extract the address of the author - where we will send the donation amount. - Then we call the
transfer
function on the_author
's address and pass in themsg.value
, which will transfer the amount of MATIC donated to the author's account. images[_id] = _image
- After the donation is transfered to author's account, we can increment thedonationAmount
in_image
struct and save the_image
back toimages
map to update the data.- Once everything is done we have to emit the
DonateImage
event.
That's all for the smart contract, now we can move forward with the compilation and migration process.
Compiling and deploying with Truffle
Now that we have our smart contract written, it's time to compile it. Open your terminal and run the following command:
truffle compile
- Truffle will compile the Solidity file and you should see output similar to:
- For the migration of our contact to the blockchain, go to the
migrations
directory, create a new file called2_donation_contract_migration.js
and add the following code:
const DonationContract = artifacts.require("DonationContract"); module.exports = function (deployer) { deployer.deploy(DonationContract); };
- If you notice in the
migrations
directory, there is a file called1_initial_migration.js
which handles the migration for theMigrations.sol
contract. This is created by default when we initialize truffle. Each contract needs a migration file to migrate (deploy) it to the blockchain. - Before deploying the smart contract, there are a couple of things that you will want to have set up for testing purposes. Install Ganache on your system. Ganache is a GUI app that provides a local blockchain for you to work on. If you don't want to use a GUI app for this, then you can install the Ganache CLI instead. After installing, start Ganache on your system. You will also need to modify the
truffle-config.js
file in the root directory of your project. You can delete all the existing contents oftruffle-config.js
and replace it with the code below.
module.exports = { networks: { development: { host: "localhost", port: 7545, network_id: "*", }, }, contracts_directory: "./contracts", contracts_build_directory: "./abis", compilers: { solc: { optimizer: { enabled: true, runs: 200, }, }, }, db: { enabled: false, }, };
- Here we are defining the basic configuration which truffle will use to deploy/migrate our smart contract. Currently, we will be deploying to
localhost:7545
where our Ganache blockchain is running. - To deploy to the development network (Ganache), run the following command from the root directory of your project:.
truffle migrate
This command will deploy your contract to Ganache, and will give you output that looks similar to this:
Writing tests for the smart contract
Writing tests for your smart contract is a very crucial task. Once a smart contract is added to the blockchain it cannot be modified, so if anything goes wrong because of a bug in the code then all the subsequent interactions with the contract will have issues. This makes it very important to test your code thoroughly before deploying it to the mainnet.
When we ran
truffle init
, truffle created a test
subdirectory in the root directory of our project. In the test
directory, you can create a file with whatever name you prefer, let's go with donation-contract-tests.js
.Before starting with the test, you will need to install a couple of packages.
npm install chai chai-as-promised
Now in the
test/donation-contract-tests.js
file add the following code,const { assert } = require("chai"); const DonationContract = artifacts.require("./DonationContract.sol"); require("chai").use(require("chai-as-promised")).should(); contract("DonationContract", ([deployer, author, donator]) => { let donationContract; before(async () => { donationContract = await DonationContract.deployed(); }); describe("deployment", () => { it("should be an instance of DonationContract", async () => { const address = await donationContract.address; assert.notEqual(address, null); assert.notEqual(address, 0x0); assert.notEqual(address, ""); assert.notEqual(address, undefined); }); }); describe("Images", () => { let result; const hash = "abcd1234"; const description = "This is a test image"; let imageCount; before(async () => { result = await donationContract.uploadImage(hash, description, { from: author, }); imageCount = await donationContract.imageCount(); }); it("Check Image", async () => { let image = await donationContract.images(1); assert.equal(imageCount, 1); const event = result.logs[0].args; assert.equal(event.hash, hash); assert.equal(event.description, description); }); it("Allow users to donate", async () => { let oldAuthorBalance; oldAuthorBalance = await web3.eth.getBalance(author); oldAuthorBalance = new web3.utils.BN(oldAuthorBalance); result = await donationContract.donateImageOwner(imageCount, { from: donator, value: web3.utils.toWei("1", "Ether"), }); const event = result.logs[0].args; let newAuthorBalance; newAuthorBalance = await web3.eth.getBalance(author); newAuthorBalance = new web3.utils.BN(newAuthorBalance); let donateImageOwner; donateImageOwner = web3.utils.toWei("1", "Ether"); donateImageOwner = new web3.utils.BN(donateImageOwner); const expectedBalance = oldAuthorBalance.add(donateImageOwner); assert.equal(newAuthorBalance.toString(), expectedBalance.toString()); }); }); });
- Chai tests are usually written to be self-explanatory through the describe function. Here we are describing two different tests, "deployment" and "Images".
- In the "deployment" test, we are checking whether the contract is deployed properly and the address of the contract is valid.
- In the "Images" test, first we create a sample image with an author. Then in the "Check Image" test case, we are fetching the image by passing
id
as 1 into thedonationContract.images
function. Then we can assert the values received by the event to check if the value is correct. - In the "Allow users to donate" test case, we are fetching the account balance of a user and then donating to the author of an image. After the donation is sent, we again fetch the updated balance of that user. Now we can check if the old balance and new balance should only differ by the amount donated to the author.
- You will notice at the top of the test script, we have added an artifact -
const DonationContract = artifacts.require("DonationContract.sol");
. These artifacts are created at runtime by truffle and include important information relating to the contract such as the Application Binary Interface (ABI), hence it is important to run our test using truffle. To run tests, execute the following command:
truffle test
The output of the command should be similar to this: -
Connecting to a frontend with web3js
Now the smart contract and tests are dealt with, it's time to integrate our smart contracts with the frontend application.
For contract calls, we will create a context and provide all the data and functions through it. Create a folder
context
in the root directory of your project and create a file called DataContext.tsx
in that folder.context/DataContext.tsx
declare let window: any; import { createContext, useContext, useEffect, useState } from "react"; import Web3 from "web3"; import DonationContract from "../abis/DonationContract.json"; interface DataContextProps { account: string; contract: any; loading: boolean; images: any[]; imageCount: number; updateImages: () => Promise<void>; donateImageOwner: (id: string, donateAmout: any) => Promise<void>; } const DataContext = createContext<DataContextProps | null>(null); export const DataProvider: React.FC = ({ children }) => { const data = useProviderData(); return <DataContext.Provider value={data}>{children}</DataContext.Provider>; }; export const useData = () => useContext<DataContextProps | null>(DataContext);
-
At the top we are setting the type of the
window
object to null, so that TypeScript will allow us to use functions likewindow.web3
andwindow.ethereum
. -
After that, we make necessary imports. If you don't have the
web3
package installed, you can install it by running -npm install web3
. -
In the 4th line, we are fetching
DonationContract
, which is a JSON object that contains the ABI of our contract. This ABI and JSON file is created by truffle when we run thetruffle compile
command. The ABI contains all the information like the address of your contract on the blockchain, what functions are present in the contract, and the parameters and returns types of these functions. -
Next, we have declared an interface for our DataContext, which contains all the variables and functions type which will be provided by DataContext.
-
Then we have to create
DataContext
andDataProvider
. We can also create a custom hook calleduseData
which we can later use in our components to easily access values from context. -
If you want to learn about React Context and how it works, you can follow this guide from the official documentation of React.
export const useProviderData = () => { const [loading, setLoading] = useState(true); const [images, setImages] = useState<any[]>([]); const [imageCount, setImageCount] = useState(0); const [account, setAccount] = useState("0x0"); const [contract, setContract] = useState<any>(); useEffect(() => { loadWeb3(); loadBlockchainData(); }, []); const loadWeb3 = async () => { if (window.ethereum) { window.web3 = new Web3(window.ethereum); window.ethereum.request({ method: 'eth_requestAccounts', [] }) } else if (window.web3) { window.web3 = new Web3(window.web3.currentProvider); } else { window.alert( "No compatible wallet detected. Please install the Metamask browser extension to continue." ); } }; const loadBlockchainData = async () => { const web3 = window.web3; var allAccounts = await web3.eth.getAccounts(); setAccount(allAccounts[0]); const networkData = DonationContract.networks["5777"]; if (networkData) { var tempContract = new web3.eth.Contract( DonationContract.abi, networkData.address ); setContract(tempContract); var count = await tempContract.methods.imageCount().call(); setImageCount(count); var tempImageList = []; for (var i = 1; i <= count; i++) { const image = await tempContract.methods.images(i).call(); tempImageList.push(image); } tempImageList.reverse(); setImages(tempImageList); } else { window.alert("TestNet not found"); } setLoading(false); }; };
- Create a function called
useProviderData
in the sameDataContext.tsx
file.useProviderData
is being called insideDataProvider
, hence everytime page loadsuseProviderData
is called. - In this function, we are declaring all the state variables that we require and then in the
useEffect
we are making two function calls,loadWeb3
andloadBlockchainData
. - In
loadWeb3
, we are first checking ifwindow.ethereum
is present which is usually injected by the Metamask wallet extension. If window.ethereum is present then we setwindow.web3
as the object ofWeb3(window.ethereum)
, wherewindow.ethereum
is the provider for web3. - If
window.ethereum
is not present then we check ifwindow.web3
is present, if yes then setwindow.web3
as an object ofWeb3(window.web3)
, wherewindow.web3
is the provider for web3. - If none of these is present then it means the user does not have a wallet installed in their browser and in such case, we can show an alert.
- In the
loadBlockchainData
function, we are fetching all the necessary data we require such as the user account number, a contract object using the ABI fromDonationContract.json
. Once we have the contract object, we can make calls to the contract. In this function, we are fetching the total image count and all the images from our contract.
In the
useProviderData
function, add the following functions:const updateImages = async () => { setLoading(true); if (contract !== undefined) { var count = await contract.methods.imageCount().call(); setImageCount(count); var tempImageList = []; for (var i = 1; i <= count; i++) { const image = await contract.methods.images(i).call(); tempImageList.push(image); } tempImageList.reverse(); setImages(tempImageList); } setLoading(false); }; const donateImageOwner = async (id: string, donateAmout: any) => { await contract.methods .donateImageOwner(id) .send({ from: account, value: donateAmout }); }; return { account, contract, loading, images, imageCount, updateImages, donateImageOwner, };
updateImages
function just fetches the latest image count and images and sets the variables.donateImageOwner
accepts the image id and donates an amount, then call thedonateImageOwner
function of the smart contract and pass in then parameters along with thefrom
variable which will be the account number of the current user.- In the end, return all the variables and functions from the
useProviderData
function.
Now we have add
DataProvider
in _app.tsx
file. Head over to pages/_app.tsx
and wrap <Component {...pageProps} />
component with <DataProvider>
component.import "tailwindcss/tailwind.css"; import { DataProvider } from "../contexts/DataContext"; function MyApp({ Component, pageProps }) { return ( <> <DataProvider> <Component {...pageProps} /> </DataProvider> </> ); } export default MyApp;
Now we can access all context variables in any of our components.
Creating the UI using TailwindCSS in Next.js
For the User Interface (UI) we will be using TailwindCSS and HeadlessUI. To install these packages run the following command:
npm install tailwindcss @headlessui/react
When these packages have been installed successfully, open the file
pages/index.tsx
to delete all the code and replace it with this:import Head from "next/head"; import { useState } from "react"; import Body from "../components/Body"; import Header from "../components/Layout/Header"; import { UploadImage } from "../components/UploadImage"; import { useData } from "../contexts/DataContext"; export default function Home() { let [isOpen, setIsOpen] = useState(false); const { loading } = useData(); function closeModal() { setIsOpen(false); } function openModal() { setIsOpen(true); } return ( <div className="flex flex-col items-center justify-start min-h-screen py-2"> <Head> <title>Decentragram</title> <link rel="icon" href="/favicon.ico" /> </Head> <UploadImage isOpen={isOpen} closeModal={closeModal} /> <Header /> <div className="max-w-2xl w-full bg-blue-100 rounded-xl flex justify-center items-center py-2 mt-3 hover:bg-blue-200 cursor-pointer" onClick={openModal} > <span className="text-blue-500 font-bold text-lg">Upload Image</span> </div> {loading ? ( <div className="font-bold text-gray-400 mt-36 text-4xl">Loading...</div> ) : ( <Body /> )} </div> ); }
- Here we are fetching the
loading
variable from theuseData
hook and checking the loading state: If loading is true, showLoading...
text or else show the<Body />
. component. - Now we will have to create three
components
, so create a new folder called components and create three files inside of it:Body.tsx
,UploadImage.tsx
, andHeader.tsx
.
Paste the following code into
Body.tsx
:declare let window: any; import Identicon from "identicon.js"; import React from "react"; import { useData } from "../contexts/DataContext"; const Body = () => { const { images } = useData(); return ( <> {images.length > 0 && images.map((image, index) => ( <BodyItem key={index} totalDonationss={image.donationAmount} address={image.author} description={image.description} hash={image.hash} id={image.id} /> ))} </> ); }; export default Body; const BodyItem = ({ address, description, totalDonationss, hash, id }) => { const { donateImageOwner, updateImages } = useData(); var data = new Identicon(address, 200).toString(); return ( <div className="w-full md:mx-0 md:max-w-2xl mt-5 p-3 border rounded-xl flex flex-col"> <div className="flex flex-row space-x-5 bg-gray-100 rounded-t-xl py-3 px-4 border-t border-l border-r font-mono items-center"> <img width={35} height={35} src={`data:image/png;base64, ${data}`} /> <div className="overflow-ellipsis w-52 overflow-hidden">{address}</div> </div> <img src={`https://ipfs.infura.io/ipfs/${hash}`} /> <div className="py-3 px-4 flex flex-col border-l border-r"> <span className="font-sans font-bold">Description</span> <span className="font-sans pt-2">{description}</span> </div> <div className="bg-gray-100 rounded-b-xl py-3 px-4 border-b border-l border-r font-mono flex flex-row justify-between"> <span> Total DONATIONS: {window.web3.utils.fromWei(totalDonations, "Ether")}{" "} MATIC </span> <div onClick={async () => { let donationAmount = window.web3.utils.toWei("0.1", "Ether"); await donationImageOwner(id, donationAmount); await updateImages(); }} > <span className="cursor-pointer font-bold text-blue-400"> DONATE: 0.1 MATIC </span> </div> </div> </div> ); };
- Here we are getting all the images from the
useData
hook, looping over each image and displaying them.` - Each image has an option to donate 0.1 MATIC to the author or the content. Clicking on
DONATE: 0.1 MATIC
, we are calling thedonationImageOwner
function from theuseData
hook, that will eventually call thedonationImageOwner
function of our smart contract.
Paste the following code into
Header.tsx
:import Identicon from "identicon.js"; import React, { useEffect } from "react"; import { useData } from "../../contexts/DataContext"; function Header() { const { account } = useData(); const [data, setData] = React.useState(); useEffect(() => { if (account !== "0x0") { setData(new Identicon(account, 200).toString()); } }, [account]); return ( <div className="container items-center"> <div className="flex flex-col md:flex-row items-center md:justify-between border py-3 px-5 rounded-xl"> <span className="font-mono">Polygon MATIC</span> <div className="flex flex-row space-x-2 items-center"> <div className="h-5 w-5 rounded-full bg-blue-500"></div> <span className="font-mono text-xl font-bold">Decentagram</span> </div> <div className="flex flex-row space-x-2 items-center"> <span className="font-mono overflow-ellipsis w-52 overflow-hidden"> {account} </span> {account && data && ( <img width={35} height={35} src={`data:image/png;base64, ${data}`} /> )} </div> </div> </div> ); } export default Header;
- In the header, we are showing the account number of the current user which we can get from
useData
. - To improve the UI, we are also showing an identicon based on the users account number.
For uploading an image, we have a modal dialog where the author can choose the image from their system and upload it to IPFS.
Paste the following code into
UploadImage.tsx
:import { Dialog, Transition } from "@headlessui/react"; import { create } from "ipfs-http-client"; import { Fragment, useState } from "react"; import { useData } from "../contexts/DataContext"; interface Props { isOpen: boolean; closeModal: () => void; } export const UploadImage: React.FC<Props> = ({ isOpen, closeModal }) => { const [buttonTxt, setButtonTxt] = useState<string>("Upload"); const [file, setFile] = useState<File | null>(null); const { contract, account, updateImages } = useData(); const client = create({ url: "https://ipfs.infura.io:5001/api/v0" }); const [description, setDescription] = useState<string>(""); const uploadImage = async () => { setButtonTxt("Uploading to IPFS..."); const added = await client.add(file); setButtonTxt("Creating smart contract..."); contract.methods .uploadImage(added.path, description) .send({ from: account }) .then(async () => { await updateImages(); setFile(null); setDescription(""); setButtonTxt("Upload"); closeModal(); }) .catch(() => { closeModal(); }); }; return ( <> <Transition appear show={isOpen} as={Fragment}> <Dialog as="div" className="fixed inset-0 z-10 overflow-y-auto" onClose={closeModal} > <Dialog.Overlay className="fixed inset-0 bg-black opacity-40" /> <div className="min-h-screen px-4 text-center "> <Transition.Child as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0" > <Dialog.Overlay className="fixed inset-0" /> </Transition.Child> {/* This element is to trick the browser into centering the modal contents. */} <span className="inline-block h-screen align-middle" aria-hidden="true" > ​ </span> <Transition.Child as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100" leave="ease-in duration-200" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > <div className="inline-block w-full p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl max-w-xl"> <Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900" > Upload Image to IPFS </Dialog.Title> <div className="mt-2"> <input onChange={(e) => setFile(e.target.files[0])} className="my-3" type="file" /> </div> {file && ( <div className="mt-2"> <img src={URL.createObjectURL(file)} /> </div> )} <div className="mt-4"> <textarea onChange={(e) => { setDescription(e.target.value); }} value={description} placeholder="Description" className="px-3 py-1 font-sourceSansPro text-lg bg-gray-100 hover:bg-white focus:bg-white rounded-lg border-4 hover:border-4 border-transparent hover:border-green-200 focus:border-green-200 outline-none focus:outline-none focus:ring w-full pr-10 transition-all duration-500 ring-transparent" /> </div> <div className="mt-4"> <button type="button" disabled={buttonTxt !== "Upload"} className="inline-flex justify-center px-4 py-2 text-sm font-medium text-blue-900 bg-blue-100 border border-transparent rounded-md hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500" onClick={() => { if (file) uploadImage(); }} > {buttonTxt} </button> </div> </div> </Transition.Child> </div> </Dialog> </Transition> </> ); };
Using IPFS to upload images
In
UploadImage.tsx
, we are using IPFS as decentralized file storage to upload the user images. For this, we have to install another package.npm install ipfs-http-client
Before using IPFS to upload images, we have to create an IPFS client. In this example we are using the Infura IPFS service, but there are others available.
const client = create({ url: "https://ipfs.infura.io:5001/api/v0" });
const added = await client.add(file);
To upload a file, call the
add
function on the client object. The add
function will return an object that contains the hash of the uploaded image. Once we have the hash value (also known as a CID - Content IDentifier), we can store that hash in our smart contract.This hash value can later be used to access the image on IPFS by appending the hash to the URL of a valid IPFS node. For example:
https://ipfs.infura.io/ipfs/${hash}
Publishing the smart contract to Mumbai testnet
Publishing your smart contract is relatively simple. You will have to add provider details for Matic testnet in your
truffle-config.js
.- First, you have to connect your Metamask wallet to the Matic Mumbai testnet. You can follow these instructions to connect. Once you have connected Metamask to the Mumbai testnet, you will need to get the Secret Recovery Phrase from Metamask that you would have received when you created the Metamask account. If you are unsure how to do this, follow these instructions to get the Secret Recovery Phrase (also commonly referred to as a mnemonic).
- Now create a file called
.secret
at the root of your project directory and paste your mnemonic in that file. This is the Secret Recovery Phrase of the account that will have to pay gas fees for contract deployment. - NOTE : Since we are deploying to a testnet we don't have to pay actual MATIC tokens, we can use the Matic Faucet to receive 1 MATIC in your account on the Mumbai testnet.
- Make sure you have added the
.secret
file in your.gitignore
and never share your Secret Recovery Phrase (mnemonic) with anyone! - Now we will have to install
hdwallet-provider
to pay gas fees while deploying.
npm install @truffle/hdwallet-provider
Once you have all these ready, modify the
truffle-config
. Your truffle-config.js
should look something like this.const HDWalletProvider = require("@truffle/hdwallet-provider"); const fs = require("fs"); const mnemonic = fs.readFileSync(".secret").toString().trim(); module.exports = { networks: { development: { host: "localhost", port: 7545, network_id: "*", }, matic: { provider: () => new HDWalletProvider({ mnemonic: { phrase: mnemonic, }, providerOrUrl: `https://matic-mumbai--rpc.datahub.figment.io/apikey/{DATAHUB_API_KEY}}/health`, chainId: 80001, }), network_id: 80001, confirmations: 2, timeoutBlocks: 200, skipDryRun: true, chainId: 80001, }, }, contracts_directory: "./contracts", contracts_build_directory: "./abis", compilers: { solc: { optimizer: { enabled: true, runs: 200, }, }, }, db: { enabled: false, }, };
Here, we are adding another network matic, and adding providerOrUrl, chainId, and network_id of Polygon's Mumbai testnet. You can find this information on the Polygon docs.
After editing
truffle-config.js
, you just have to run the following command to deploy your contract to polygon testnet.truffle migrate --network matic
Truffle will deploy the contract to Mumbai, you should see output similar to:
Compiling your contracts... =========================== > Everything is up to date, there is nothing to compile. Starting migrations... ====================== > Network name: 'matic' > Network id: 80001 > Block gas limit: 20000000 (0x1312d00) 1_initial_migration.js ====================== Replacing 'Migrations' ---------------------- > transaction hash: 0x8cbc2616e3bf70965c32afded6ae6ece70038df1e39ee2ae4b832383964c7de1 > Blocks: 2 Seconds: 4 > contract address: 0x2F082Dd688b1B3c9a8A5310fda680A03873e7fA0 > block number: 18368623 > block timestamp: 1630486877 > account: 0x2a65871CCbC1Ce534A388Bdf83aA28127c5d7c8a > balance: 1.851879295 > gas used: 193243 (0x2f2db) > gas price: 1 gwei > value sent: 0 ETH > total cost: 0.000193243 ETH Pausing for 2 confirmations... ------------------------------ > confirmation number: 2 (block: 18368627) > Saving migration to the chain. > Saving artifacts ------------------------------------- > Total cost: 0.000193243 ETH 2_deploy_contracts.js ===================== Replacing 'DonationContract' ------------------------ > transaction hash: 0x24169d6f55478fbae26dc4a89d2b51da75af51ecddb7011ca0d3cd61f06225bc > Blocks: 2 Seconds: 4 > contract address: 0x42c27d00217a7F5AE1cDAd9e71C2342373e13ADE > block number: 18368634 > block timestamp: 1630486899 > account: 0x2a65871CCbC1Ce534A388Bdf83aA28127c5d7c8a > balance: 1.850963627 > gas used: 869930 (0xd462a) > gas price: 1 gwei > value sent: 0 ETH > total cost: 0.00086993 ETH Pausing for 2 confirmations... ------------------------------ > confirmation number: 3 (block: 18368638) > Saving migration to the chain. > Saving artifacts ------------------------------------- > Total cost: 0.00086993 ETH Summary ======= > Total deployments: 2 > Final cost: 0.001063173 ETH
This is how our Dapp will look like at the end:
Conclusion
Congratulations on finishing the tutorial! Thank you for taking the time to complete it. In this tutorial, you have learned how to create a smart contract in Solidity, how to use IPFS to upload images, how to interact with smart contracts in frontend applications using web3js and how to publish your smart contract to the Polygon testnet. This donation dApp is one of the endless possibilities of Web 3 development.
About the Author
I'm Viral Sangani, a tech enthusiast working on blockchain projects & love Web3 community. Feel free to connect with me on GitHub.