Introduction
Welcome to the fashionHub smart contract tutorial! In this tutorial, we’ll explore how to create a decentralized fashion place booking platform using Ethereum and Solidity.
The fashionHub contract allows users to:
- Add fashion places with details like name, image, description, location, and price.
- Book a fashion place for a specific duration, making it temporarily unavailable to other users.
- End a booking or cancel a reservation.
- Check fashion place details and availability.
- Set the availability status of a fashion place.
- Payments are facilitated using the ERC-20 token interface, specifically the cUSD token. This tutorial will guide you through the process of building a decentralized fashion place booking system step by step.
By the end of this tutorial, you’ll have a solid understanding of how to develop smart contracts for similar booking platforms on the Ethereum blockchain. Let’s get started with fashionHub!
Requirement
To take this tutorial, you will need:
- Access to a code editor or text editor such as Remix.
- A reliable internet browser and internet connection
Prerequisite
- Basic knowledge of Javascript.
- Understand how Blockchain works.
- Have a basic knowledge of solidity.
Let’s dive into developing our smart contract for the decentralized fashion place booking platform. We’ll start by writing the code for the fashionHub
contract using Solidity, the programming language for Ethereum.
In this tutorial, we’ll be creating a smart contract called fashionHub
using Solidity. This contract will serve as the foundation for a decentralized fashion place booking platform on the Celo blockchain.
- Complete code of this session:
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
// erc-20 interface so the contract can interact withn it
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
);
}
contract fashionHub {
// initialize length of fashion places
uint internal fashionPlacesLength = 0;
// cUSD token address
address internal cUsdTokenAddress =
0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
// group fashion place details
struct FashionPlace {
address payable owner;
address currentCustomer;
string name;
string image;
string description;
string location;
uint price;
bool available;
uint unavailableTill;
}
// map each fashion place to an unsigned integer
mapping(uint => FashionPlace) private fashionPlaces;
modifier onlyPlaceOwner(uint _index) {
require(
fashionPlaces[_index].owner == msg.sender,
"Only fashion place owner"
);
_;
}
/// @dev this modifier checks if current timestamp is greater or equal to UnavailableTill
modifier checkOver(uint _index) {
require(
block.timestamp >= fashionPlaces[_index].unavailableTill,
"Fashion place's current booking isn't over"
);
_;
}
/// @dev write new fashion place to the contract
function addFashionPlace(
string calldata _name,
string calldata _image,
string calldata _description,
string calldata _location,
uint _price
) external {
require(bytes(_name).length > 0, "Empty name");
require(bytes(_image).length > 0, "Empty image url");
require(bytes(_description).length > 0, "Empty description");
require(bytes(_location).length > 0, "Empty location");
bool _available = true;
uint _unavailableTill = 0;
fashionPlaces[fashionPlacesLength] = FashionPlace(
payable(msg.sender),
address(0),
_name,
_image,
_description,
_location,
_price,
_available,
_unavailableTill
);
fashionPlacesLength++;
}
/// @dev read a fashion place from smartcontract
function getFashionPlace(uint _index)
public
view
returns (
address payable owner,
address currentCustomer,
string memory name,
string memory image,
string memory description,
string memory location,
uint price,
bool available,
uint unavailableTill
)
{
owner = fashionPlaces[_index].owner;
currentCustomer = fashionPlaces[_index].currentCustomer;
name = fashionPlaces[_index].name;
image = fashionPlaces[_index].image;
description = fashionPlaces[_index].description;
location = fashionPlaces[_index].location;
price = fashionPlaces[_index].price;
available = fashionPlaces[_index].available;
unavailableTill = fashionPlaces[_index].unavailableTill;
}
/// @dev function to order or book a fashion place
/// @notice fashion place is now unavailable
function orderFashionPlace(uint _index, uint _time) public payable {
require(
fashionPlaces[_index].owner != msg.sender,
"You can't book your own fashion place"
);
require(
fashionPlaces[_index].available,
"Fashion place is currently unavailable"
);
require(_time > 0, "Duration of booking must be at least one hour");
fashionPlaces[_index].available = false;
fashionPlaces[_index].currentCustomer = msg.sender;
fashionPlaces[_index].unavailableTill = block.timestamp + _time;
require(
IERC20Token(cUsdTokenAddress).transferFrom(
msg.sender,
fashionPlaces[_index].owner,
fashionPlaces[_index].price
),
"Transfer failed."
);
}
/// @dev allows a fasion place owner to end the order
function endOrder(uint _index)
public
onlyPlaceOwner(_index)
checkOver(_index)
{
fashionPlaces[_index].unavailableTill = 0;
fashionPlaces[_index].currentCustomer = address(0);
fashionPlaces[_index].available = true;
}
/// @dev allows a customer to cancel an order
function cancelOrder(uint _index) public {
require(
fashionPlaces[_index].currentCustomer == msg.sender,
"Only current customer can cancel the order"
);
require(
!fashionPlaces[_index].available,
"Fashion place is already available"
);
fashionPlaces[_index].unavailableTill = 0;
fashionPlaces[_index].currentCustomer = address(0);
fashionPlaces[_index].available = true;
require(
IERC20Token(cUsdTokenAddress).transfer(
msg.sender,
fashionPlaces[_index].price
),
"Transfer failed."
);
}
/// @dev get length of fashion place
function getFashionPlacesLength() public view returns (uint) {
return (fashionPlacesLength);
}
/// @dev set fashion place available or not
function setAvailability(uint _index)
public
onlyPlaceOwner(_index)
checkOver(_index)
{
fashionPlaces[_index].available = !fashionPlaces[_index].available;
}
}
Step 1: Setting Up the Contract Structure
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
To begin, let’s open a new Solidity file and add the necessary SPDX-License-Identifier
at the top of the contract. This ensures that our contract complies with the MIT license
.
Next, we specify the version of Solidity we’ll be using (in this case, >=0.7.0 <0.9.0)
to ensure compatibility with our contract.
Step 2: Creating the ERC-20 Token Interface
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
);
}
In this step, we’ll define an interface called IERC20Token
. This interface allows our contract to interact with the ERC-20 token, which will be used for payments within our fashion place booking platform.
An interface acts as a blueprint for functions and events that an external contract must implement in order to be compatible with our contract.
Here’s a breakdown of the interface functions and events:
-
transfer
: Allows transferring tokens from one address to another. -
approve
: Approves an address to spend a specific amount of tokens on behalf of the sender. -
transferFrom
: Transfers tokens on behalf of the owner, following prior approval. -
totalSupply
: Retrieves the total supply of tokens. -
balanceOf
: Retrieves the token balance of a specific address. -
allowance
: Retrieves the remaining token allowance for a spender address. -
Transfer event
: Emits an event when tokens are transferred between addresses. -
Approval event
: Emits an event when an approval for token spending is granted.
Note: In this tutorial, we’ll be utilizing the existing ERC-20 token interface, so we don’t need to implement it ourselves. Let’s proceed to the next step!
Step 3: Defining the Contract Structure and Modifiers
contract fashionHub {
// initialize length of fashion places
uint internal fashionPlacesLength = 0;
// cUSD token address
address internal cUsdTokenAddress =
0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;
// group fashion place details
struct FashionPlace {
address payable owner;
address currentCustomer;
string name;
string image;
string description;
string location;
uint price;
bool available;
uint unavailableTill;
}
// map each fashion place to an unsigned integer
mapping(uint => FashionPlace) private fashionPlaces;
modifier onlyPlaceOwner(uint _index) {
require(
fashionPlaces[_index].owner == msg.sender,
"Only fashion place owner"
);
_;
}
/// @dev this modifier checks if current timestamp is greater or equal to UnavailableTill
modifier checkOver(uint _index) {
require(
block.timestamp >= fashionPlaces[_index].unavailableTill,
"Fashion place's current booking isn't over"
);
_;
}
In this step, we’ll define the structure of our fashionHub
contract and introduce two modifiers that will be used throughout the contract.
Contract Structure:
-
We initialize a variable called
fashionPlacesLength
and set it to0
. This variable will keep track of the number of fashion places in our platform. -
We declare an address variable called
cUsdTokenAddress
and assign it the address of the cUSD token. This address will be used for payment transactions. -
Next, we define a
struct
namedFashionPlace
to represent the details of each fashion place. It includes attributes such asowner
,currentCustomer
,name
,image
,description
,location
,price
,available
, andunavailableTill
. -
To store multiple fashion places, we use a
mapping
calledfashionPlaces
. It maps an unsigned integer (index) to aFashionPlace
struct.
Modifiers:
-
We define a modifier called
onlyPlaceOwner
which ensures that a function can only be executed by theowner
of a specific fashion place. It checks if themsg.sender
matches the owner’s address for that place. -
Another modifier called
checkOver
verifies if the currenttimestamp
is greater than or equal to theunavailableTill
value of a fashion place. This modifier is used to check if a booking period is over.
Note: Modifiers allow us to add reusable conditions to functions in a secure and efficient manner. Now that we have the basic structure and modifiers in place, let’s move on to the next step of our tutorial.
Step 4: Adding Fashion Places to the Contract
function addFashionPlace(
string calldata _name,
string calldata _image,
string calldata _description,
string calldata _location,
uint _price
) external {
require(bytes(_name).length > 0, "Empty name");
require(bytes(_image).length > 0, "Empty image url");
require(bytes(_description).length > 0, "Empty description");
require(bytes(_location).length > 0, "Empty location");
bool _available = true;
uint _unavailableTill = 0;
fashionPlaces[fashionPlacesLength] = FashionPlace(
payable(msg.sender),
address(0),
_name,
_image,
_description,
_location,
_price,
_available,
_unavailableTill
);
fashionPlacesLength++;
}
In this step, we’ll implement a function called addFashionPlace
that allows users to add new fashion places to our contract.
Here’s a breakdown of the function:
Function Signature:
- The function is defined as
addFashionPlace
and takes in several parameters:_name
,_image
,_description
,_location
, and_price
.
The calldata
keyword indicates that the function parameters are passed by reference.
Input Validation:
We include several require
statements to validate the inputs provided by the user.
Each require statement checks if the length of the respective string inputs is greater than 0
. If any of them are empty, an error message is thrown.
Initializing Fashion Place:
-
We create two local variables:
_available (initialized as true)
and_unavailableTill (initialized as 0)
. These variables represent theavailability
status and thetimestamp
until which the fashion place is unavailable for booking. -
Next, we add the fashion place to our
fashionPlaces
mapping. ThefashionPlacesLength
acts as the index for the new fashion place, and we assign a new instance of theFashionPlace
struct to it. We set the owner asmsg.sender
, the current customer as thezero
address, and assign the remaining input values.
Incrementing Fashion Places Length:
- After adding the new fashion place, we increment the
fashionPlacesLength
variable to keep track of the total number of fashion places in our platform.
Now, we have a way to add fashion places to our contract. Let’s move on to the next step of our tutorial!
Step 5: Retrieving Fashion Place Details
User
function getFashionPlace(uint _index)
public
view
returns (
address payable owner,
address currentCustomer,
string memory name,
string memory image,
string memory description,
string memory location,
uint price,
bool available,
uint unavailableTill
)
{
owner = fashionPlaces[_index].owner;
currentCustomer = fashionPlaces[_index].currentCustomer;
name = fashionPlaces[_index].name;
image = fashionPlaces[_index].image;
description = fashionPlaces[_index].description;
location = fashionPlaces[_index].location;
price = fashionPlaces[_index].price;
available = fashionPlaces[_index].available;
unavailableTill = fashionPlaces[_index].unavailableTill;
}
In this step, we’ll implement a function called getFashionPlace
that allows users to retrieve the details of a specific fashion place by providing its index.
Here’s an overview of the function:
Function Signature:
-
The function is defined as
getFashionPlace
and takes in a parameter_index
representing the index of the fashion place we want to retrieve. -
The function is marked as
public
andview
to indicate that it doesn’t modify the contract’s state and can be calledexternally
.
Returning Fashion Place Details:
-
The function returns multiple values corresponding to the details of a fashion place.
-
We declare variables
owner
,currentCustomer
,name
,image
,description
,location
,price
,available
, andunavailableTill
with their respective types.
Inside the function body, we assign the corresponding values from the fashionPlaces
mapping at the given _index
to these variables.
Finally, we return these variables as the output of the function.
By using the getFashionPlace
function, users can retrieve specific details about a fashion place, such as its owner
, current customer
, name
, image
, description
, location
, price
, availability status
, and the timestamp
until it’s unavailable.
Now that we have implemented this functionality, let’s move on to the next step of our tutorial!
Step 6: Ordering a Fashion Place
function orderFashionPlace(uint _index, uint _time) public payable {
require(
fashionPlaces[_index].owner != msg.sender,
"You can't book your own fashion place"
);
require(
fashionPlaces[_index].available,
"Fashion place is currently unavailable"
);
require(_time > 0, "Duration of booking must be at least one hour");
fashionPlaces[_index].available = false;
fashionPlaces[_index].currentCustomer = msg.sender;
fashionPlaces[_index].unavailableTill = block.timestamp + _time;
require(
IERC20Token(cUsdTokenAddress).transferFrom(
msg.sender,
fashionPlaces[_index].owner,
fashionPlaces[_index].price
),
"Transfer failed."
);
}
In this step, we’ll implement a function called orderFashionPlace
that allows users to book a fashion place for a specific duration.
Here’s an overview of the function:
Function Signature:
-
The function is defined as
orderFashionPlace
and takes in two parameters:_index
representing the index of the fashion place to be booked, and_time
representing the duration of the booking in hours. -
The function is marked as
public
to allow external access.
Input Validation:
-
We include several require statements to validate the conditions for booking a fashion place.
-
The first require statement checks if the caller is not the owner of the fashion place, ensuring that users can’t book their own places.
-
The second require statement checks if the fashion place is currently available for booking.
-
The third require statement checks if the
_time
parameter is greater than0
, ensuring that the booking duration is at least one hour.
Updating Fashion Place Details:
-
We update the details of the fashion place to reflect the booking. We set the available status to
false
to mark it as unavailable for other users. -
The
currentCustomer
is set to the address of the user who placed the booking(msg.sender)
. -
We calculate the
unavailableTill
timestamp by adding the current timestamp(block.timestamp)
with the booking duration(_time)
.
Token Transfer:
-
We use the
transferFrom
function from theIERC20Token
interface to transfer the required payment(fashionPlaces[_index].price)
from the user(msg.sender)
to theowner
of the fashion place(fashionPlaces[_index].owner)
. -
The
transferFrom
function is called on thecUsdTokenAddress
contract address.
By calling the orderFashionPlace
function and providing the appropriate parameters, users can book a fashion place, ensuring its availability for the specified duration. The payment for the booking is transferred from the user to the fashion place owner.
Congratulations! We have completed this step of the tutorial. Let’s proceed to the next step!
Step 7: Ending an Order
function endOrder(uint _index)
public
onlyPlaceOwner(_index)
checkOver(_index)
{
fashionPlaces[_index].unavailableTill = 0;
fashionPlaces[_index].currentCustomer = address(0);
fashionPlaces[_index].available = true;
}
In this step, we’ll implement a function called endOrder
that allows the owner of a fashion place to end a current booking.
Here’s an overview of the function:
Function Signature:
-
The function is defined as
endOrder
and takes in one parameter:_index
representing the index of the fashion place. -
The function is marked as
public
to allow external access.
Modifiers:
-
We include two modifiers in the function signature:
onlyPlaceOwner(_index)
andcheckOver(_index)
. -
The
onlyPlaceOwner
modifier ensures that only the owner of the fashion place can end the booking. -
The
checkOver
modifier checks if the current booking duration has ended, based on theunavailableTill timestamp
.
Updating Fashion Place Details:
-
We reset the booking details of the fashion place to make it available for future bookings.
-
The
unavailableTill timestamp
is set to0
, indicating that the fashion place is now available. -
The
currentCustomer
address is set to0x0
(address(0))
, signifying that there is no current customer.
The available status is set to true
, indicating that the fashion place is available for booking.
By calling the endOrder
function and providing the appropriate index of the fashion place, the owner can end the current booking, making the place available for other users.
Congratulations! We have completed this step of the tutorial. Let’s move on to the next step!
Step 8: Canceling an Order
function cancelOrder(uint _index) public {
require(
fashionPlaces[_index].currentCustomer == msg.sender,
"Only current customer can cancel the order"
);
require(
!fashionPlaces[_index].available,
"Fashion place is already available"
);
fashionPlaces[_index].unavailableTill = 0;
fashionPlaces[_index].currentCustomer = address(0);
fashionPlaces[_index].available = true;
require(
IERC20Token(cUsdTokenAddress).transfer(
msg.sender,
fashionPlaces[_index].price
),
"Transfer failed."
);
}
In this step, we’ll implement a function called cancelOrder
that allows the current customer to cancel their booking for a fashion place.
Here’s an overview of the function:
Function Signature:
-
The function is defined as
cancelOrder
and takes in one parameter:_index
representing the index of the fashion place. -
The function is marked as public to allow external access.
Requirements and Validations:
-
We include two
require
statements to validate the cancellation conditions: -
The first
require
statement ensures that only the current customer who made the booking can cancel the order. -
The second
require
statement verifies that the fashion place is not already available for booking.
Updating Fashion Place Details:
-
If the cancellation conditions are met, we proceed to update the fashion place details.
-
We set the
unavailableTill
timestamp to 0, indicating that the fashion place is now available. -
The
currentCustomer
address is set to0x0
(address(0))
, indicating that there is no current customer. -
The available status is set to
true
, indicating that the fashion place is available for booking.
Refund:
-
After canceling the order, we initiate a refund to the customer by calling the
transfer
function of theIERC20Token
contract. -
The customer’s address
(msg.sender)
receives the refunded amount, which is the price of the fashion place. -
By calling the
cancelOrder
function with the appropriate index, the current customer can cancel their booking and receive a refund.
Congratulations! We have completed this step of the tutorial. Let’s move on to the next step!
Step 9: Getting Fashion Places Length and Setting Availability
In this step, we’ll add two functions: getFashionPlacesLength
and setAvailability
.
Function: getFashionPlacesLength
-
This function allows us to retrieve the length of the fashion places array.
-
It is marked as
public
and has the viewmodifier
, indicating that it only reads data and doesn’t modify the contract state. -
The function simply returns the value of
fashionPlacesLength
, which represents the number of fashion places that have been added to the contract.
Function: setAvailability
-
This function allows the
owner
of a fashion place to update its availability status. -
The function takes in one
parameter
,_index
, representing the index of the fashion place. -
It is marked as
public
and includes theonlyPlaceOwner
andcheckOver
modifiers to ensure that only the owner can modify the availability and only when the current booking period is over. -
Inside the function, we toggle the available status of the specified fashion place using the
logical
NOToperator (!)
.
If the fashion place was available, it becomes unavailable, and vice versa.
By calling the getFashionPlacesLength
function, we can retrieve the total number of fashion places stored in the contract. This information can be useful for displaying statistics or iterating over the fashion places in a frontend application.
The setAvailability
function enables the owner of a fashion place to control its availability based on specific conditions. This allows for flexibility in managing the booking and availability of fashion places.
Congratulations! We have completed this step of the tutorial. Let’s move on to the final step and wrap up the tutorial!
Click here to get the complete code for this session
Contract Deployment
To deploy the FashionHub
smart contract on the Celo blockchain, you would need the following:
CeloExtensionWallet: Download and install the Celo Extension Wallet from the Google Chrome store, create a wallet, and securely store your key phrase. Click here to intall the celo extension wallet
Celo Faucet: Fund your wallet by copying your wallet address and pasting it into the Celo Faucet, then confirm. Click here to access celo faucet
Celo Remix Plugin: Open Remix and create a new Solidity file, paste the FashionHub
contract code into the file, and ensure the Solidity compiler is set to version 0.8.7 or later. Click here to access to access the remix ide
Compile the contract by clicking the Compile FashionHub.sol
button in the Solidity Compiler tab in Remix.
In the Deploy & Run Transactions
tab, select the Celo network from the dropdown menu, connect your wallet to Remix by clicking Connect to wallet
, and select FashionHub
from the Contract
dropdown menu.
Click the Deploy
button, confirm the transaction in your wallet, and wait for the transaction to be confirmed on the Celo blockchain.
Once the transaction is confirmed, the FashionHub
contract will be deployed on the Celo blockchain and you can interact with it using Remix.
Conclusion
In this tutorial, we learned how to create a Solidity smart contract for a decentralized fashion hub. We covered adding fashion places, booking and canceling orders, and managing availability. We also used an ERC20 token for payments. By following this tutorial, you gained a practical understanding of smart contract development for decentralized applications. Happy coding!
About the author
I’m David Ikanji, a web3 developer residing in Nigeria, and I have a strong passion for working with blockchain technology.