Introduction
In this tutorial, you will learn how to how to build a computer retail store using celo composer.
Prerequisites
To start building, you’ll need a basic understanding of web development, Node (v16), yarn, and Git.
Celo Composer
The easiest way to get started with Celo Composer is using @celo/celo-composer
. This CLI tool enables you to quickly start building dApps on Celo for multiple frameworks including React, React Native (w/o Expo), Flutter, and Angular. You can create the dApp using the default Composer templates provided by Celo. In our case, we will work with react, specifically NextJS. The stack included in celo composer:
- NextJS - React framework
- Typescript
- React Celo
- TailwindCSS for styling
- Hardhat Smart contract development environment
- Solidity Smart Contract Language
Getting Started
- Bootstrap the application using this Celo Composer command.
npx @Celo_Academy/celo-composer create
- Select React framework(NextJS)
- Select react-celo as the web3 library
- Select hardhat for the smart contract framework.
- Select No for subgraph
- Give a name to your project and you are done.
cd into your app then install the dependencies required in the project. In the root directory, run
yarn
//or
npm install
Creating our Smart Contract
First, we are going to build our smart contract and with this, you will be required to have a bit of knowledge of Solidity. To learn more about solidity contracts and the structures of a contract you can check out this link.
In the hardhat directory, navigate to contracts and create a new file then name it computerMarketplace.sol
. Delete the other boilerplate files.
The completed code Should look like this.
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
/*
Interface that allows contract to transfer and recieve ERC20 tokens
*/
interface IERC20Token {
function transfer(address, uint256) external returns (bool);
function approve(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
function totalSupply() external view returns (uint256);
function balanceOf(address) external view returns (uint256);
function allowance(address, address) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
}
/*
Beginning of contract
*/
contract ComputerMarketplace {
uint internal productsLength = 0;
// address internal cUsdTokenAddress = 0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
address internal celoTokenAddress =
0xF194afDf50B03e69Bd7D057c1Aa9e10c9954E4C9;
address owner = msg.sender;
/*
Product template struct
*/
struct Product {
address payable owner;
string computer_title;
string image_url;
string computer_specs;
string store_location;
uint price;
uint sold;
}
// Boolean for non reentrant function
bool private locked = false;
/*
Modifier making functions non reentrant by changing locked boolean for duration of transaction so that on reentry it does not pass the "not locked requirement"
*/
modifier nonReentrant() {
require(!locked, "Reentrant call.");
locked = true;
_;
locked = false;
}
modifier Onlyowner() {
require(msg.sender == owner);
_;
}
//Setting Price limit for listings
uint256 constant MAX_PRICE = 100000000000000000000;
//Mapping for prodict indices to product struct
mapping(uint => Product) internal products;
//mapping users addresses to the indices for the product listings
mapping(address => uint) internal productsByUser;
//Max products for a seller can list
uint internal maxProductsPerUser = 10;
//Events for product Listing and delisting
event ProductCreated(
address indexed owner,
string computer_title,
string image_url,
string computer_specs,
string store_location,
uint price
);
event ProductDeleted(
address indexed owner,
string computer_title,
string image_url
);
event ProductSold(
address indexed owner,
string computer_title,
string image_url
);
/*
Function to change max products per user
->Requirement: Only the owner can set this limit
*/
function setMaxProductsPerUser(uint _maxProductsPerUser) public Onlyowner {
require(
_maxProductsPerUser > 0,
"Maximum products per user must be greater than 0"
);
maxProductsPerUser = _maxProductsPerUser;
}
function getProductsLength() public view returns (uint) {
return (productsLength);
}
/*
Function to add product listing, Emits listing event "Product Created"
->Requirements
*Non of the fields of data entered should be a zero
*Price field data should also be below max price
*Seller should no have exceeded platform limit
*/
function writeProduct(
string memory _computer_title,
string memory _image_url,
string memory _computer_specs,
string memory _store_location,
uint _price
) public {
require(
bytes(_computer_title).length > 0,
"Computer title cannot be empty"
);
require(bytes(_image_url).length > 0, "Image URL cannot be empty");
require(
bytes(_computer_specs).length > 0,
"Computer specs cannot be empty"
);
require(
bytes(_store_location).length > 0,
"Store location cannot be empty"
);
require(_price > 0 && _price <= MAX_PRICE, "Invalid product price");
require(
productsByUser[msg.sender] < maxProductsPerUser,
"Maximum products per user reached"
);
uint _sold = 0;
products[productsLength] = Product(
payable(msg.sender),
_computer_title,
_image_url,
_computer_specs,
_store_location,
_price,
_sold
);
productsLength++;
productsByUser[msg.sender]++;
emit ProductCreated(
msg.sender,
_computer_title,
_image_url,
_computer_specs,
_store_location,
_price
);
}
/*
Function allowing buyers to access data on a given product
*/
function readProduct(
uint _index
)
public
view
returns (
address payable,
string memory,
string memory,
string memory,
string memory,
uint,
uint
)
{
return (
products[_index].owner,
products[_index].computer_title,
products[_index].image_url,
products[_index].computer_specs,
products[_index].store_location,
products[_index].price,
products[_index].sold
);
}
/*
Function allowing buyers to buy a product on the platform
Increments the product sold counter for the number of total units sold
*/
function buyProduct(uint _index, uint _quantity) public payable nonReentrant {
require(msg.value == products[_index].price);
uint allowance = IERC20Token(celoTokenAddress).allowance(
msg.sender,
address(this)
);
require(
allowance >= products[_index].price,
"Celo token allowance not enough"
);
require(
IERC20Token(celoTokenAddress).transferFrom(
msg.sender,
products[_index].owner,
products[_index].price * _quantity
),
"Celo token transfer failed"
);
products[_index].sold += _quantity;
emit ProductSold(
products[_index].owner,
products[_index].computer_title,
products[_index].image_url
);
}
function getProductsByUser(
address _user
) public view returns (Product[] memory) {
uint count = 0;
for (uint i = 0; i < productsLength; i++) {
if (products[i].owner == _user) {
count++;
}
}
Product[] memory ownedProducts = new Product[](count);
uint j = 0;
for (uint i = 0; i < productsLength; i++) {
if (products[i].owner == _user) {
ownedProducts[j] = products[i];
j++;
}
}
return ownedProducts;
}
/*
Function a seller uses to delete a product
->Requirements
*index of product must be valid ie; within the number of products listed
* Sender of the call must be the owner of the product
*/
function deleteProduct(uint _index) public {
require(_index < productsLength, "Product index out of range");
// Make sure that the caller is the owner of the product
require(
products[_index].owner == msg.sender,
"Only the owner can delete their products"
);
// Delete the product at the specified index
for (uint i = _index; i < productsLength - 1; i++) {
products[i] = products[i + 1];
}
delete products[productsLength - 1];
productsLength--;
// Update the product count for the owner
productsByUser[msg.sender]--;
}
}
Smart Contract breakdown
First, we declared our license and the solidity version.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
Define an ERC20 Token Contract
The ERC-20 introduces a standard for Fungible Tokens, in other words, they have a property that makes each Token be exactly the same (in type and value) as another Token. For example, an ERC-20 Token acts just like the ETH, meaning that 1 Token is and will always be equal to all the other Tokens.
interface IERC20Token {
function transfer(address, uint256) external returns (bool);
function approve(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
function totalSupply() external view returns (uint256);
function balanceOf(address) external view returns (uint256);
function allowance(address, address) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
}
The code represents an interface which is used to interact with other contracts. They define a set of functions and events that a contract must implement to be considered compatible with other contracts that use the interface.
The interface defines six functions that ERC-20 tokens must have:
-
transfer: allows the owner of the token to send a specified amount of tokens to another address.
-
approve: allows the owner of the token to give permission to another address to transfer a specified amount of tokens on their behalf.
-
transferFrom: allows an address that has been approved to transfer a specified amount of tokens on behalf of the owner of the token.
-
totalSupply: returns the total number of tokens in circulation.
-
balanceOf: returns the balance of tokens owned by a specific address.
-
allowance: returns the amount of tokens that an address has been approved to spend on behalf of another address.
The interface also defines two events:
-
Transfer: emitted when tokens are transferred from one address to another.
-
Approval: emitted when an address is approved to spend a certain amount of tokens on behalf of another address.
And then we define our smart contract ComputerMarketplace
.
contract ComputerMarketplace {
// Contract code goes here
}
Contracts in Solidity are similar to classes in object-oriented languages.
Variables, Structs, Mappings, Modifiers and Events
We will now define some variables, structs and mappings that will be used by our contract.
uint internal productsLength = 0;
address internal celoTokenAddress =
0xF194afDf50B03e69Bd7D057c1Aa9e10c9954E4C9;
struct Product {
address payable owner;
string computer_title;
string image_url;
string computer_specs;
string store_location;
uint price;
uint sold;
}
bool private locked = false;
modifier nonReentrant() {
require(!locked, "Reentrant call.");
locked = true;
_;
locked = false;
}
uint256 constant MAX_PRICE = 100000000000000000000;
mapping(uint => Product) internal products;
mapping(address => uint) internal productsByUser;
event ProductCreated(address indexed owner, string computer_title, string image_url, string computer_specs, string store_location, uint price);
event ProductDeleted(address indexed owner, string computer_title, string image_url);
-
uint internal productsLength: declares a variable productsLength with an initial value of 0. It is marked as internal, which means that it can only be accessed from within the contract or contracts that inherit from it.
-
address internal celoTokenAddress: declares a variable for the celo network native currency which is used to pay for products in our marketplace.
-
Product: a struct that defines properties of the marketplace. The fields are:
owner
: address of the user who owns the product. It is declared as address payable to allow it to receive payments from the buyer.computer_title
: name of the product.image_url
: URL of the product’s image.computer_specs
: specifications of the product.store_location
: physical location of the store selling the product.price
: price of the product in CELO tokens.sold
: number of units of the product that have been sold so far.
-
MAX_PRICE: defines the maximum allowed values for prices. The requirement statement checks whether the product price is within the valid range.
-
products: mapping that stores each product by its index
-
productsByUser: mapping that keeps track of how many products each user has added.
-
maxProductsPerUser: determines the maximum number of products that a user can write in the ComputerMarketplace contract. By default, it is set to 10 to prevents a user from spamming the platform.
-
nonReentrant: modifier to the buyProduct function to lock the function while it is being executed to prevent a user to call it at the same time and revert it if so.
-
ProductCreated: event is emitted when a new product is created in the smart contract. The event has five parameters: the
address of the product owner
,computer title
,image URL
,computer specifications
,store location
, andprice
of the product. Theindexed
keyword is used to allow for efficient filtering and searching of the event based on its parameters. -
ProductDeleted: event is emitted when an existing product is deleted from the smart contract. The event has three parameters: the
address of the product owner
,computer title
, and theimage URL
.
Both events allow external parties, such as a user interface or another smart contract, to receive notifications when certain actions occur in the smart contract.
Functions
function setMaxProductsPerUser(uint _maxProductsPerUser) public {
require(
_maxProductsPerUser > 0,
"Maximum products per user must be greater than 0"
);
maxProductsPerUser = _maxProductsPerUser;
}
setMaxProductsPerUser
function allows the contract owner to change the maximum number of products a user can create. It takes an unsigned integer _maxProductsPerUser as an argument and sets the maxProductsPerUser variable to the value of the argument. However, the _maxProductsPerUser argument must be greater than 0, and if it is not, the function will fail and return an error message.
function writeProduct(
string memory _computer_title,
string memory _image_url,
string memory _computer_specs,
string memory _store_location,
uint _price
) public {
require(
productsByUser[msg.sender] < maxProductsPerUser,
"Maximum products per user reached"
);
uint _sold = 0;
products[productsLength] = Product(
payable(msg.sender),
_computer_title,
_image_url,
_computer_specs,
_store_location,
_price,
_sold
);
productsLength++;
productsByUser[msg.sender]++;
}
writeProduct
function allows a user to add a new product to the marketplace. The function first checks whether the user has already added the maximum number of products allowed per user (which is set to 10 by default). If the user has not reached the maximum, a new Product struct is created with the provided parameters, and _sold
is set to 0 since the product has not yet been sold.
The new product is then added to the products mapping at the productsLength index
, where productsLength
is the current number of products in the marketplace. The owner
of the product is set to the address of the user who called the function, and productsLength
is incremented. Finally, the productsByUser
mapping for the user who called the function is incremented, to keep track of the number of products that user has added to the marketplace.
function readProduct(
uint _index
)
public
view
returns (
address payable,
string memory,
string memory,
string memory,
string memory,
uint,
uint
)
{
return (
products[_index].owner,
products[_index].computer_title,
products[_index].image_url,
products[_index].computer_specs,
products[_index].store_location,
products[_index].price,
products[_index].sold
);
}
readProduct
function that takes an index as an argument and returns a tuple of data about the product at that index in the products array.
function buyProduct(uint _index) public payable nonReentrant {
require(
IERC20Token(celoTokenAddress).transferFrom(
msg.sender,
products[_index].owner,
products[_index].price
),
"Transfer failed."
);
products[_index].sold++;
}
buyProduct
function is used to buy a product from the marketplace. It takes an argument _index
which is the index
of the product in the products array that the buyer wants to purchase. The function requires that the buyer sends an amount of CELO tokens that is equal to the price of the product.
The function then uses the transferFrom
function of the IERC20Token
interface to transfer the specified amount of CELO tokens from the buyer's address
to the owner's address
. If the transfer is successful, the function updates the sold
variable of the product by incrementing it by 1.
function deleteProduct(uint _index) public {
require(_index < productsLength, "Product index out of range");
// Make sure that the caller is the owner of the product
require(
products[_index].owner == msg.sender,
"Only the owner can delete their products"
);
// Delete the product at the specified index
for (uint i = _index; i < productsLength - 1; i++) {
products[i] = products[i + 1];
}
delete products[productsLength - 1];
productsLength--;
// Update the product count for the owner
productsByUser[msg.sender]--;
emit ProductDeleted(products[_index].owner, products[_index].computer_title, products[_index].image_url);
}
deleteProduct
function allows a product owner to delete one of their products from the marketplace. The function takes an input parameter _index
which represents the index
of the product in the products array that the owner wants to delete.
The function first checks that the input index
is within the range of valid indexes for the products array. It then checks that the caller of the function is the owner
of the product they want to delete.
If both conditions are satisfied, the function proceeds to delete the product at the specified index. To do this, it shifts all products after the specified index one position to the left, effectively overwriting the product at the index being deleted. Finally, it deletes the last element of the array (which is now a duplicate of the product at the second-to-last index) and decrements the productsLength
variable to reflect the new length of the products array.