Introduction
Welcome to another tutorial by yours truly . If you are here for the first time, I greet you, please give me a like if you find this helpful. If this is not your first article, drop me a like and you can also follow me for more tutorials.
In this tutorial, you will gain a solid understanding of building a decentralized tipping system on the celo blockchain. You would learn how to allow users to send tips to registered staff members by specifying the recipientâs name and sending a desired amount.
Come along
Prerequisites
- Prior knowledge of javascript
- Familiarity with the command line
- Basic understanding of blockchain concepts
- Have some knowledge of solidity and its concepts
- Remix
Requirements
- A code editor or text editor. VSCode is recommended
- A terminal. Git Bash is recommended
- An Internet Browser and a good internet connection
Getting Started
Remix is a widely used integrated development environment (IDE) available on the web. If you have been following my previous tutorials, you are likely familiar with it. Remix offers a user-friendly interface that enables developers to write, test, and deploy smart contracts, with a particular focus on the Celo blockchain. It provides a convenient platform to facilitate the development process for Celo-based projects.
To get started with Remix, you would create a new workspace and proceed to create a new file, which can be named as TipJar.sol
for our example.
To ensure clarity and compliance, it is customary to include a statement at the beginning of your TiJar.sol
file that explicitly states the license under which your code is being released. This practice helps in establishing the licensing terms for your code and ensures that users understand how they can utilize and distribute it.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
Next, we define a struct called Wallet
within the TipJar
contract. This struct will be used within the contract to store and retrieve information about employees, such as their names and wallet addresses. It provides a convenient way to organize and manage employee data within the TipJar
contract.
contract TipJar {
struct Wallet {
string name;
address walletAddress;
}
The Wallet
struct represents the wallet information associated with each employee. It has two properties:
name
: A string that stores the name of the employee.walletAddress
: An Ethereum address that represents the employeeâs wallet address.
By using a struct, we can conveniently group these related properties together and access them as a single unit. It allows us to define the structure of the employeeâs wallet information and reuse it whenever needed.
In the context of the TipJar
contract, we define the Tip
struct which represents a tip given from one address to another. By using the Tip
struct, the contract can conveniently store and access information related to each tip. It allows for efficient organization and retrieval of tip data within the TipJar
contract. For example, when a tip is given, an instance of the Tip
struct is created and populated with the relevant information such as the message, amount, sender, receiver, and timestamp.
struct Tip {
string message;
uint256 amount;
address sender;
address receiver;
uint256 timeStamp;
}
The Tip
struct contains the following properties:
message
: A string that stores an optional message or comment accompanying the tip.amount
: Auint256
variable that represents the value of the tip, specified in the smallest unit of the networkâs native currency (e.g., wei in Ethereum).sender
: An Ethereum address that identifies the account from which the tip originates.receiver
: An Ethereum address that identifies the account that receives the tip.timeStamp
: Auint256
variable that stores the timestamp when the tip was created, represented as the number of seconds since the Unix epoch.
Up next, several mappings and variables are defined to manage and track different aspects of the tipping system. By using these mappings and variables, the contract can efficiently manage and retrieve information related to employees, tips, and their associations.
mapping(string => address) nameToAddressMapping;
mapping(uint256 => Tip) tips;
mapping (address => Wallet) wallets;
mapping(address => uint256[]) ownTips;
string[] namesTaken;
uint256 tipsTracker = 0;
-
mapping(string => address) nameToAddressMapping
: This mapping associates an employeeâs name (string) with their wallet address (address). It allows for efficient lookup of an employeeâs wallet address based on their name. -
mapping(uint256 => Tip) tips
: This mapping is used to store and retrieve tip information. The key is a unique identifier (uint256) for each tip, and the value is an instance of theTip
struct, containing details such as the tip message, amount, sender, receiver, and timestamp. -
mapping(address => Wallet) wallets
: This mapping connects an employeeâs wallet address (address) with their associatedWallet
struct. It allows for easy access to wallet information based on the wallet address. -
mapping(address => uint256[]) ownTips
: This mapping links an employeeâs wallet address with an array of tip identifiers (uint256) representing the tips they have received. It enables efficient retrieval of all the tips associated with a specific employee. -
string[] namesTaken
: This array stores the names that have already been assigned to employees. It is used to check if a name is already taken when creating a new employee. -
uint256 tipsTracker = 0
: This variable keeps track of the total number of tips given. It is incremented every time a new tip is created, ensuring each tip has a unique identifier.
Up next, we define the isTaken
function, The isTaken
function is a utility function used within the TipJar
contract to check whether a given name _name
is already taken by another employee.
function isTaken(string calldata _name, string[] memory _namesTaken) private pure returns (bool) {
- The function
isTaken
takes two parameters:_name
, which is the name to be checked, and_namesTaken
, an array of names that have already been assigned to employees. - The function is marked as
private
, which means it can only be called from within the contract. - The
pure
keyword indicates that the function does not modify the contractâs state.
for (uint256 i = 0; i < _namesTaken.length; i++) {
if (keccak256(abi.encodePacked(_namesTaken[i])) == keccak256(abi.encodePacked(_name))) {
return true;
}
}
- The function uses a
for
loop to iterate through each element in the_namesTaken
array. - Within the loop, the function compares each element of
_namesTaken
with the provided_name
. This comparison is performed by hashing both names using thekeccak256
function and comparing the resulting hash values. - If a match is found (i.e., the names are identical), the function immediately returns
true
, indicating that the name is already taken.
return false;
- If the loop completes without finding a match, meaning that the provided
_name
is not found in the_namesTaken
array, the function returnsfalse
, indicating that the name is available.
Following, we define the createNewWallet
function. The createNewWallet
function in the TipJar
contract is used to create a new wallet for an employee by associating their provided name and wallet address.
function createNewWallet(string calldata _name, address _walletAddress) public {
- The function
createNewWallet
takes two parameters:_name
, which is the name of the employee, and_walletAddress
, which is the wallet address associated with the employee. - The function is marked as
public
, allowing external entities to call it.
bool taken = isTaken(_name, namesTaken);
require(taken == false, "Name already taken");
- The first line of the function calls the
isTaken
function to check if the provided_name
is already taken by another employee. The result is stored in thetaken
variable. - The next line uses a
require
statement to ensure that thetaken
variable isfalse
. If the name is already taken (i.e.,taken
istrue
), the function execution is halted, and an error message âName already takenâ is reverted.
namesTaken.push(_name);
wallets[_walletAddress] = Wallet(_name, _walletAddress);
nameToAddressMapping[_name] = _walletAddress;
- If the provided name is not already taken, the function proceeds to store the new wallet information.
- The line
namesTaken.push(_name)
adds the_name
to thenamesTaken
array, keeping track of the assigned names. wallets[_walletAddress] = Wallet(_name, _walletAddress)
assigns a newWallet
struct to thewallets
mapping. The struct is initialized with the provided_name
and_walletAddress
.- Finally,
nameToAddressMapping[_name] = _walletAddress
establishes a mapping between the_name
and_walletAddress
, allowing for easy lookup of the wallet address based on the name.
Next up, we define the tip
function. the tip function allows users to send a tip to a staff member. It verifies the name and address of the staff member, creates a new tip object, associates the tip with the staff member, transfers the tip amount, and ensures the transfer is successful.
function tip(
string calldata _message,
string calldata _name,
address _to
) public payable {
- The function
tip
takes three parameters:_message
(the message accompanying the tip),_name
(the name of the staff member), and_to
(the address of the staff member). - The function is marked as
public
, meaning it can be called externally by users. - It is also marked as
payable
because it allows users to send Ether along with the tip.
require(
nameToAddressMapping[_name] == _to,
"Name or Address not registered yet!"
);
- The
require
statement ensures that the provided_name
corresponds to the given_to
address. It checks if thenameToAddressMapping
contains a valid mapping for the_name
provided. If not, it reverts the transaction with the error message âName or Address not registered yet!â.
uint256 _amount = msg.value;
- The
_amount
variable is assigned the value of the Ether sent with the transaction (msg.value
). This represents the amount of the tip being sent by the user.
tips[tipsTracker] = Tip(_message, _amount, msg.sender, _to, block.timestamp);
- A new tip object is created using the
Tip
struct and assigned to thetips
mapping at thetipsTracker
index. The tip object contains the provided_message
,_amount
, the senderâs address (msg.sender
), the receiverâs address (_to
), and the current timestamp (block.timestamp
).
ownTips[_to].push(tipsTracker);
- The tip identifier (
tipsTracker
) is added to the array of tips (ownTips
) associated with the staff memberâs address (_to
). This allows the staff member to easily retrieve their own tips.
tipsTracker += 1;
- The
tipsTracker
variable is incremented to ensure the next tip gets a unique identifier.
(bool sent, ) = payable(_to).call{value: _amount}("");
require(sent);
- The tip amount (
_amount
) is transferred to the staff memberâs address (_to
) using thepayable
function modifier. The transfer is executed by calling thecall
function with thevalue
parameter set to the tip amount. If the transfer fails, the transaction reverts and fails, ensuring that the tip is only sent if the transfer is successful.
Up next, we define the readTip
function. The readTip
function in the TipJar
contract allows the recipient of a tip to read the details of a specific tip by providing the tipId
.
function readTip(uint256 tipId)
public
view
returns (
string memory message,
uint256 amount,
address sender,
uint256 timeStamp
)
{
Tip memory _tip = tips[tipId];
require(msg.sender == _tip.receiver, "Not allowed to view tip");
message = _tip.message;
amount = _tip.amount;
sender = _tip.sender;
timeStamp = _tip.timeStamp;
}
- The
readTip
function takes one parameter:tipId
, which represents the identifier of the tip to be read. - The function is marked as
public
andview
since it only retrieves information from the contract without modifying the state. - The return values are specified as a tuple, including
message
(the message accompanying the tip),amount
(the tip amount),sender
(the address of the tip sender), andtimeStamp
(the timestamp when the tip was created).
Tip memory _tip = tips[tipId];
- The function retrieves the tip object associated with the provided
tipId
from thetips
mapping and assigns it to a local variable_tip
of theTip
struct type.
require(msg.sender == _tip.receiver, "Not allowed to view tip");
- The
require
statement verifies that the caller of the function (msg.sender
) is the intended recipient of the tip (_tip.receiver
). If the condition is not met, the function execution is halted, and an error message âNot allowed to view tipâ is reverted.
message = _tip.message;
amount = _tip.amount;
sender = _tip.sender;
timeStamp = _tip.timeStamp;
- If the caller is allowed to view the tip, the function proceeds to assign the corresponding values from the
_tip
object to the respective return variables. - The
message
,amount
,sender
, andtimeStamp
variables are set based on the values stored in the retrieved_tip
object.
Next up, we define the readMyTips
and readNamesTaken
functions which provide convenient ways to retrieve information about tips and registered names.
Letâs break down each function:
function readMyTips() public view returns (uint256[] memory) {
return ownTips[msg.sender];
}
- The
readMyTips
function is marked aspublic
andview
since it only reads information from the contract without modifying the state. - The function returns an array of
uint256
values, representing the tip IDs associated with the callerâs (senderâs) address. - It retrieves the tip IDs stored in the
ownTips
mapping, usingmsg.sender
as the key (the callerâs address), and returns the array of tip IDs.
function readNamesTaken() public view returns (string[] memory) {
return namesTaken;
}
- The
readNamesTaken
function is also marked aspublic
andview
. - It returns an array of
string
values, representing the list of names that have already been taken. - The function simply returns the
namesTaken
array, which holds the registered names.
Then, we define the isStaff
and checkAddressWithName
functions which provide ways to check if a user is a staff member and retrieve the address associated with a given name, respectively.
function isStaff(string calldata _name) public view returns (bool) {
return (nameToAddressMapping[_name] != address(0));
}
- The
isStaff
function takes a string parameter_name
, representing the name to check. - It is marked as
public
andview
, indicating that it doesnât modify the contractâs state. - The function checks if the address associated with the provided
_name
in thenameToAddressMapping
mapping is not equal to the zero address (address(0)
). - If the condition is true, it means the name is associated with a valid address, indicating that the user is a staff member. The function returns
true
, otherwise, it returnsfalse
.
function checkAddressWithName(string calldata _name) public view returns (address) {
return nameToAddressMapping[_name];
}
- The
checkAddressWithName
function takes a string parameter_name
, representing the name for which to retrieve the associated address. - It is marked as
public
andview
. - The function retrieves the address associated with the provided
_name
from thenameToAddressMapping
mapping and returns it. - If the
_name
is not found in the mapping, the function will return the zero address (address(0)
).
Next, we define the The checkNameWithAddress
function which allows you to retrieve the name associated with a given address. This function allows you to fetch the associated name for a given address by looking up the corresponding Wallet
object in the wallets
mapping. It retrieves the name
property from the retrieved Wallet
object and returns it as a result. It can be useful when you want to retrieve the name associated with an address in the context of the TipJar
contract.
function checkNameWithAddress(address _address) public view returns (string memory) {
Wallet memory wallet = wallets[_address];
string memory name = wallet.name;
return name;
}
- The
checkNameWithAddress
function takes anaddress
parameter_address
, representing the address for which to retrieve the associated name. - It is marked as
public
andview
, indicating that it doesnât modify the contractâs state. - Inside the function, a local variable
wallet
of theWallet
struct type is declared and assigned the value retrieved from thewallets
mapping using the_address
as the key. - The
wallet.name
property is then assigned to another local variablename
of typestring
. - Finally, the function returns the
name
.
Here is the full code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TipJar {
struct Wallet {
string name;
address walletAddress;
}
struct Tip {
string message;
uint256 amount;
address sender;
address receiver;
uint256 timeStamp;
}
mapping(string => address) nameToAddressMapping;
mapping(uint256 => Tip) tips;
mapping (address => Wallet) wallets;
mapping(address => uint256[]) ownTips;
string[] namesTaken;
uint256 tipsTracker = 0;
// return true if "_name" is taken, false otherwise
function isTaken(string calldata _name, string[] memory _namesTaken) private pure returns (bool) {
for (uint256 i = 0; i < _namesTaken.length; i++) {
if (keccak256(abi.encodePacked(_namesTaken[i])) == keccak256(abi.encodePacked(_name))) {
return true;
}
}
return false;
}
// Staff create new wallet here
function createNewWallet(string calldata _name, address _walletAddress)
public
{
bool taken = isTaken(_name, namesTaken);
require(taken == false, "Name already taken");
namesTaken.push(_name);
wallets[_walletAddress] = Wallet(_name, _walletAddress);
nameToAddressMapping[_name] = _walletAddress;
}
// User tip a staff
function tip(
string calldata _message,
string calldata _name,
address _to
) public payable {
require(
nameToAddressMapping[_name] == _to,
"Name or Address not registered yet!"
);
uint256 _amount = msg.value;
// create a new tip object
tips[tipsTracker] = Tip(_message, _amount, msg.sender, _to, block.timestamp);
// assign tips object to staff
ownTips[_to].push(tipsTracker);
tipsTracker += 1;
// transfer tip amount to staff
(bool sent, ) = payable(_to).call{value: _amount}("");
require(sent);
}
// read a tip details from storage
function readTip(uint256 tipId)
public
view
returns (
string memory message,
uint256 amount,
address sender,
uint256 timeStamp
)
{
Tip memory _tip = tips[tipId];
require(msg.sender == _tip.receiver, "Not allowed to view tip");
message = _tip.message;
amount = _tip.amount;
sender = _tip.sender;
timeStamp = _tip.timeStamp;
}
// return an array of IDs of user's tips
function readMyTips() public view returns (uint256[] memory) {
return ownTips[msg.sender];
}
// return list of names taken
function readNamesTaken() public view returns(string[] memory) {
return namesTaken;
}
// check if user is a staff member
function isStaff(string calldata _name) public view returns (bool) {
return (nameToAddressMapping[_name] != address(0));
}
// return address of an associated name
function checkAddressWithName(string calldata _name)
public
view
returns (address)
{
return nameToAddressMapping[_name];
}
// get wallet details
function checkNameWithAddress(address _address) public view returns (string memory) {
Wallet memory wallet = wallets[_address];
string memory name = wallet.name;
return name;
}
}
Contract Deployment
To deploy the contract, we would need:
- CeloExtensionWallet
- Celo Faucet
- Celo Remix Plugin
Download the Celo Extension Wallet from the Google chrome store using the link above. After doing that, create a wallet, and store your key phrase in a very safe place to avoid permanently losing your funds.
After downloading and creating your wallet, you will need to fund it using the Celo Faucet. Copy the address to your wallet, click the link to the faucet above and paste the address into the text field, and confirm.
Next up, on remix, download and activate the celo plugin from the plugin manager. Connect your wallet and deploy your contract.
Conclusion
In this tutorial, We covered various aspects of the contract, including the use of structs to organize data, mappings to efficiently store and retrieve information, and functions to create new wallets, send tips, and retrieve tip details.
Throughout the tutorial, you learned how to implement features such as name validation, associating names with addresses, and checking if a user is a staff member. Additionally, you gained insights into managing data arrays, enforcing access restrictions, and performing value transfers within the contract.
Next Steps
By following this tutorial, you should now have a solid understanding of developing similar contracts and be equipped with the knowledge to handle data storage, user interactions, and tip transactions in your own projects.
You could try implementing a frontend or try adding other functions based on what you have learnt.
Here is the repository for the codebase here
About The Author
Daniel Ogbuti is a web3 developer with a passion for teaching as well as learning. I would love to connect on Twitter @daniel_ogbuti and linkedin: Daniel Ogbuti
See you soon!