Introduction
Building a decentralized application (dApp) on a blockchain like Celo presents a unique opportunity to construct peer-to-peer systems for a wide array of use-cases. This tutorial guides you through developing a simple Waste Marketplace on the Celo blockchain using Hardhat, Solidity, and JavaScript.
Prerequisites
Before proceeding, you should have the following:
- Basic understanding of Solidity
- Basic knowledge of JavaScript
- Node.js and npm installed
- Familiarity with Hardhat
- Celo Wallet
- Basic understanding of Blockchain and Celo
Step 1: Setting Up the Development Environment
We’ll start by installing Hardhat, an Ethereum development environment. To install Hardhat, open your terminal and type:
mkdir WasteMarket
cd WasteMarket
mkdir hardhat
cd hardhat
npm init --yes
npm install --save-dev hardhat
npm install --save-dev @nomicfoundation/hardhat-toolbox @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
After that, create a new Hardhat project:
npx hardhat
Your project directory should look like this
Step 2: Writing the Smart Contract
Let’s write our marketplace contract in Solidity. Create a new file in the contracts
folder and name it WasteMarket.sol
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract WasteMarket {
// Define the structure of the waste object
struct Waste {
string wasteType;
uint weight;
address payable seller;
bool isSold;
}
// Define a dynamic array of Waste objects
Waste[] public wastes;
// Function to sell waste, adds a new waste object to the array
function sellWaste(string memory wasteType, uint weight) public {
wastes.push(Waste({
wasteType: wasteType,
weight: weight,
seller: payable(msg.sender), // the address of the sender becomes the seller
isSold: false
}));
}
// Function to buy waste, which transfers the funds to the seller
// and marks the waste as sold
function buyWaste(uint wasteId) public payable {
Waste storage waste = wastes[wasteId]; // gets the waste object using its id
require(!waste.isSold, "This waste is already sold!"); // checks if the waste is not sold
waste.seller.transfer(msg.value); // transfers funds to the seller
waste.isSold = true; // marks the waste as sold
}
// Function to get the total count of wastes listed in the marketplace
function getWasteCount() public view returns (uint) {
return wastes.length;
}
// Function to retrieve the details of a specific waste by its index
function getWaste(uint index) public view returns (string memory wasteType, uint weight, address seller, bool isSold) {
Waste storage waste = wastes[index];
return (waste.wasteType, waste.weight, waste.seller, waste.isSold);
}
}
This smart contract enables users to sell waste (with specific type and weight) and buy listed waste.
Let’s explain each code block in detail:
Contract Declaration:
contract WasteMarket {
...
}
This defines our contract, WasteMarket
. The contract is the fundamental building block of Ethereum applications - it’s similar to a class in object-oriented programming.
Struct Definition:
struct Waste {
string wasteType;
uint weight;
uint price;
address payable seller;
bool isSold;
}
Here we’re defining a new Solidity structure or struct
which is a complex datatype. This struct
is called Waste
and it represents a waste item in our marketplace. It has the following properties:
wasteType
: The type of waste (e.g., “Plastic”, “Metal”, etc.). It’s a string.weight
: The weight of the waste item. It’s a uint (unsigned integer), so it cannot be negative.price
: The price of the waste item.seller
: The address of the seller.address payable
means it’s an address you can send funds to.isSold
: A boolean (true/false) representing whether the waste has been sold.
Array Definition:
Waste[] public wastes;
This line defines a public array wastes
of Waste
structs. public
means that this variable can be read from outside the contract. Solidity will automatically create a getter function for it.
sellWaste Function:
function sellWaste(string memory wasteType, uint weight, uint price) public {
wastes.push(Waste({
wasteType: wasteType,
weight: weight,
price: price,
seller: payable(msg.sender),
isSold: false
}));
}
This function allows a user to sell waste. It takes the waste type, weight, and price as input, creates a new Waste
struct from these, and adds it to the wastes
array. The msg.sender
represents the address of the user who called this function (i.e., the seller).
buyWaste Function:
function buyWaste(uint wasteId) public payable {
Waste storage waste = wastes[wasteId];
require(!waste.isSold, "This waste is already sold!");
require(msg.value >= waste.price, "Insufficient funds sent!");
waste.seller.transfer(msg.value);
waste.isSold = true;
}
The buyWaste
function allows a user to buy a waste item. It takes as input wasteId
, the index of the waste item in the wastes
array. The keyword payable
indicates that this function will make a transfer of funds (Ether). Two requirements are checked:
- The waste item must not already be sold.
- The buyer sent enough funds to cover the price of the item.
If these conditions are met, the funds are transferred to the seller, and the waste item is marked as sold.
getWasteCount Function:
function getWasteCount() public view returns (uint) {
return wastes.length;
}
The getWasteCount
function is a view function (it doesn’t modify the contract’s state) that returns the number of waste items currently listed in the marketplace.
getWaste Function:
function getWaste(uint index) public view returns (string memory wasteType, uint weight, address seller, bool isSold) {
Waste storage
Step 3: Compiling the Smart Contract
To compile the contract, run the following command:
npx hardhat compile
Step 4: Deploying the Smart Contract on the Celo Alfajores Testnet
Create a new file deploy.js
inside the scripts
folder:
async function main() {
// Get the first signer from the signer array
const [deployer] = await ethers.getSigners();
console.log("Deploying the contract with the account:", deployer.address);
// Get the contract factory
const Contract = await ethers.getContractFactory("WasteMarket");
// Deploy the contract
const contract = await Contract.deploy();
console.log("Contract deployed to:", contract.address);
}
main()
.then(() => process.exit(0)) // exits the process successfully
.catch(error => {
console.error(error); // logs error if any occurs
process.exit(1); // exits the process with failure
});
The deployment script, written in JavaScript, is responsible for deploying the WasteMarket
contract to the local Hardhat network. After the script is run, it logs the address of the account used to deploy the contract and the address of the deployed contract. These addresses can be used to interact with the contract in subsequent transactions. Here’s a breakdown of what the script does:
-
const [deployer] = await ethers.getSigners()
: This line retrieves an array of signers available in our Hardhat environment. The first signer in the list (the deployer) usually controls the most Ether in the Hardhat network. -
console.log("Deploying the contract with the account:", deployer.address)
: This logs the Ethereum address of the deployer, which is the account used to deploy the contract. -
const Contract = await ethers.getContractFactory("WasteMarket")
: This gets the contract factory for theWasteMarket
contract. In ethers.js, aContractFactory
is an abstraction used to deploy new smart contracts. -
const contract = await Contract.deploy()
: This line deploys the contract to the Hardhat network by initiating a transaction that deploys a new instance of the contract to the network. -
console.log("Contract deployed to:", contract.address)
: Finally, the script logs the address of the deployed contract to the console. -
The
main()
function is then called, and it handles any errors that may occur during the deployment process. If the deployment is successful, the process exits with a success status (0). If there’s an error, it logs the error to the console and exits with a failure status (1).
Now, we need to add the Celo Alfajores network configuration in hardhat.config.js
:
require("@nomiclabs/hardhat-waffle");
// Add the alfajores network to the configuration
module.exports = {
solidity: "0.8.17",
networks: {
alfajores: {
url: "https://alfajores-forno.celo-testnet.org",
accounts: {
mnemonic: "ADD_MNEMONIC_KEY",
path: "m/44'/52752'/0'/0",
},
chainId: 44787,
},
},
};
This configuration file is specific to Hardhat, a popular Ethereum development environment. The setup in this file configures your environment to connect to the Celo Alfajores test network and use a specific version of the Solidity compiler. Here’s what each part of the file does:
-
require("@nomiclabs/hardhat-waffle")
: This imports the Hardhat Waffle plugin. Hardhat Waffle integrates Waffle’s Ethereum testing functionality into Hardhat’s development environment. -
module.exports
: The module exports a configuration object for Hardhat. This object is how you customize your Hardhat environment. -
solidity: "0.8.17"
: This specifies the version of Solidity that your smart contracts are written in. -
networks
: This object specifies the networks that Hardhat will connect to. -
alfajores
: This specifies the Celo Alfajores test network as a target for deploying your smart contracts. -
url: "https://alfajores-forno.celo-testnet.org"
: This is the URL of the JSON-RPC API of the Alfajores network. -
accounts
: This specifies the Ethereum accounts that will be used to deploy your contracts to the Alfajores network. It uses a mnemonic phrase to generate these accounts. -
mnemonic: "ADD_MNEMONIC_KEY"
: You replace “ADD_MNEMONIC_KEY” with your mnemonic phrase. -
path: "m/44'/52752'/0'/0"
: This is the derivation path for the accounts. It’s a way of deriving multiple keys from a single seed phrase (the mnemonic). -
chainId: 44787
: This specifies the chain ID of the Alfajores network. It’s used to ensure that you’re signing transactions for the correct network.
You could also use a .env
file to store your Metamask mnemonic key, just make sure you install the dotenv
dependency.
To deploy the contract to the Celo Alfajores network, run:
npx hardhat run scripts/deploy.js --network alfajores
Step 5: Interacting with the Contract
Let’s interact with our contract using JavaScript. Here is an example of how you might do it:
const { ethers } = require("ethers");
// Create a provider connected to the Celo Alfajores testnet
const provider = new ethers.providers.JsonRpcProvider("https://alfajores-forno.celo-testnet.org");
// Create a signer with your private key and the provider
const signer = new ethers.Wallet("your-private-key").connect(provider);
// Specify the address of your deployed contract
const contractAddress = "your-deployed-contract-address";
// Specify the ABI of your contract
const abi = [ /* the ABI from your contract goes here */ ];
// Create a contract instance
const contract = new ethers.Contract(contractAddress, abi, provider);
const main = async () => {
// Call the sellWaste function on the contract
const tx = await contract.connect(signer).sellWaste("Plastic", 500);
// Wait for the transaction to be confirmed
await tx.wait();
// Fetch the first waste object from the contract
const waste = await contract.wastes(0);
// Log the fetched waste object
console.log(waste);
}
main();
This script is written in JavaScript and is used to interact with a deployed smart contract on the Celo Alfajores test network. Here is a step-by-step explanation:
-
const { ethers } = require("ethers")
: This line imports the ethers.js library, which is a complete Ethereum library and wallet implementation in JavaScript. -
const provider = new ethers.providers.JsonRpcProvider("https://alfajores-forno.celo-testnet.org")
: This line creates a new ethers.js provider that is connected to the Celo Alfajores test network. The provider allows us to interact with the Ethereum blockchain; it’s essentially our connection to the Ethereum network. -
const signer = new ethers.Wallet("your-private-key").connect(provider)
: Here, we create a wallet object with a private key and connect it to our provider. The wallet object is known as a signer in ethers.js terminology because it can sign transactions, which means it has the ability to execute transactions on the Ethereum network. -
const contractAddress = "your-deployed-contract-address"
: Here, we specify the Ethereum address where our smart contract is deployed. Replace"your-deployed-contract-address"
with the actual address of your deployed contract. -
const abi = [ /* the ABI from your contract goes here */ ]
: Here, we specify the Application Binary Interface (ABI) of our contract. The ABI is an array that describes how to interact with our contract. It’s like a list of functions, their arguments, and their return values. -
const contract = new ethers.Contract(contractAddress, abi, provider)
: Here, we create a new contract instance with the contract’s address, ABI, and our provider. This instance allows us to interact with our deployed contract on the Ethereum network. -
const main = async () => { /* ... */ }
: Here, we define an asynchronous function namedmain
. This function is where we will interact with our contract. -
const tx = await contract.connect(signer).sellWaste("Plastic", 500)
: Inside themain
function, we connect our signer to our contract and call thesellWaste
function with a type of"Plastic"
and a weight of500
. This creates a new transaction on the Ethereum network. -
await tx.wait()
: This line waits for the transaction to be mined and confirmed by the Ethereum network. -
const waste = await contract.wastes(0)
: This line fetches the first waste object from our contract. It does this by calling thewastes
function on our contract with an argument of0
. -
console.log(waste)
: This line logs the fetched waste object to the console. The waste object includes the type, weight, seller, and isSold properties of the waste. -
main()
: Finally, we call themain
function to execute our script.
Conclusion
Now, you’ve created a simple Waste Marketplace built on the Celo blockchain! You’ve learned how to write, compile, deploy, and interact with a smart contract on Celo.
Next Step
We continue our journey on building our marketplace in the Part 2. We’ll create a frontend dApp using Next.js, a React framework.
About the Author
I’m a Web3 Fullstack developer and Technical writer. You can connect with me on LinkedIn, GitHub, Twitter.