Introduction
eSports is a rapidly growing industry, attracting millions of players and spectators worldwide. Ensuring fair competition and efficient management of eSports events is of paramount importance. This is where blockchain technology comes into play, offering transparency, security, and decentralized control.
In this tutorial, we’ll guide you through the process of creating a decentralized eSports platform built on the Celo blockchain. We’ll develop a Flutter application, backed by a smart contract that manages every aspect of an eSports event. The contract will handle match creation, team updates, match status updates, and even declaring the winner, all in a decentralized and secure manner.
By the end of this tutorial, you’ll have the knowledge and tools to create, manage, and update eSports matches on a platform you’ve built, powered by Celo.
Prerequisites
This tutorial is tailored towards individuals with an intermediate understanding of Flutter for mobile application development, and familiarity with the Dart programming language. Additionally, it is beneficial if you have prior experience in developing smart contracts using Solidity, along with a fundamental comprehension of blockchain technology. Particularly, we’ll be utilizing Celo’s blockchain, so knowledge of its operational dynamics would be advantageous.
Before proceeding, ensure you’ve familiarized yourself with Celo’s infrastructure through resources like the What is Celo? | Celo Documentation or introductory tutorials.
Requirements
- Flutter SDK: Make sure that you have the latest Flutter SDK installed on your system. If not, you can get it from here.
- Dart: Dart is the programming language used with Flutter. It should come bundled with the Flutter SDK, but you can also install it separately from here.
- Solidity: We will use Solidity for writing smart contracts.
- A Celo Wallet: Make sure to set up a wallet on Celo’s Alfajores Testnet. You can do this by following the steps from the
Setting up a Celo Wallet
section in this tutorial.
Writing the Smart Contract
Smart contracts are the foundation of our decentralized application (dApp). They contain the rules and logic for our eSports platform, ensuring transparency and fairness. We’ll be using Solidity, a popular language for writing smart contracts on the Celo network.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract EsportsPlatform {
struct Match {
string title;
string[] teamA;
string[] teamB;
uint256 startTime;
uint256 endTime;
string matchStatus;
string winner;
string teamAScore;
string teamBScore;
address matchCreator;
}
Match[] public matches;
event MatchCreated(string title, uint256 startTime, uint256 endTime, address matchCreator);
event MatchUpdated(string title, string status, string winner);
event TeamUpdated(string title, string team, bool isAdded);
event PlayerRemoved(string title, string team, string player);
event MatchScoreUpdated(string title, string teamAScore, string teamBScore);
function createMatch(string memory _title, uint256 _startTime, uint256 _endTime) public {
Match memory newMatch = Match({
title: _title,
teamA: new string[](0),
teamB: new string[](0),
startTime: _startTime,
endTime: _endTime,
matchStatus: "Scheduled",
teamAScore: "nil",
teamBScore: "nil",
winner: "no winner",
matchCreator: msg.sender
});
matches.push(newMatch);
emit MatchCreated(_title, _startTime, _endTime, msg.sender);
}
function updateTeamA(string memory _matchTitle, string[] memory _players) public {
updateTeam(_matchTitle, _players, true);
}
function updateTeamB(string memory _matchTitle, string[] memory _players) public {
updateTeam(_matchTitle, _players, false);
}
function updateTeam(string memory _matchTitle, string[] memory _players, bool _isTeamA) internal {
for(uint i = 0; i < matches.length; i++) {
if(keccak256(abi.encodePacked(matches[i].title)) == keccak256(abi.encodePacked(_matchTitle)) && matches[i].matchCreator == msg.sender) {
if(_isTeamA) {
for(uint j = 0; j < _players.length; j++) {
matches[i].teamA.push(_players[j]);
}
} else {
for(uint j = 0; j < _players.length; j++) {
matches[i].teamB.push(_players[j]);
}
}
emit TeamUpdated(_matchTitle, _isTeamA ? "Team A" : "Team B", true);
}
}
}
function removePlayer(string memory _matchTitle, string memory _playerName, bool _isTeamA) public {
for(uint i = 0; i < matches.length; i++) {
if(keccak256(abi.encodePacked(matches[i].title)) == keccak256(abi.encodePacked(_matchTitle)) && matches[i].matchCreator == msg.sender) {
if(_isTeamA) {
for(uint j = 0; j < matches[i].teamA.length; j++) {
if(keccak256(abi.encodePacked(matches[i].teamA[j])) == keccak256(abi.encodePacked(_playerName))) {
matches[i].teamA[j] = matches[i].teamA[matches[i].teamA.length - 1];
matches[i].teamA.pop();
emit PlayerRemoved(_matchTitle, "Team A", _playerName);
}
}
} else {
for(uint j = 0; j < matches[i].teamB.length; j++) {
if(keccak256(abi.encodePacked(matches[i].teamB[j])) == keccak256(abi.encodePacked(_playerName))) {
matches[i].teamB[j] = matches[i].teamB[matches[i].teamB.length - 1];
matches[i].teamB.pop();
emit PlayerRemoved(_matchTitle, "Team B", _playerName);
}
}
}
}
}
}
function updateMatchStatus(string memory _matchTitle, string memory _status) public {
for(uint i = 0; i < matches.length; i++) {
if(keccak256(abi.encodePacked(matches[i].title)) == keccak256(abi.encodePacked(_matchTitle)) && matches[i].matchCreator == msg.sender) {
matches[i].matchStatus = _status;
emit MatchUpdated(_matchTitle, _status, matches[i].winner);
}
}
}
function updateMatchScore(string memory _matchTitle, string memory _teamAScore, string memory _teamBScore) public {
for(uint i = 0; i < matches.length; i++) {
if(keccak256(abi.encodePacked(matches[i].title)) == keccak256(abi.encodePacked(_matchTitle)) && matches[i].matchCreator == msg.sender) {
matches[i].teamAScore = _teamAScore;
matches[i].teamBScore = _teamBScore;
emit MatchScoreUpdated(_matchTitle, _teamAScore, _teamBScore);
}
}
}
function updateMatchWinner(string memory _matchTitle, string memory _winner) public {
for(uint i = 0; i < matches.length; i++) {
if(keccak256(abi.encodePacked(matches[i].title)) == keccak256(abi.encodePacked(_matchTitle)) && matches[i].matchCreator == msg.sender) {
matches[i].winner = _winner;
emit MatchUpdated(_matchTitle, matches[i].matchStatus, _winner);
}
}
}
function getAllMatches() public view returns (Match[] memory) {
return matches;
}
}
The contract is called EsportsPlatform
and is written in Solidity version 0.8.0. The contract maintains a list of Match
structs, each of which represents a match in your Esports platform. Each Match
has the following fields:
title
: A string that represents the title of the match.teamA
andteamB
: String arrays that represent the names of the players in Team A and Team B, respectively.startTime
andendTime
: Unsigned integers that represent the start and end times of the match .matchStatus
: A string that represents the status of the match (“Scheduled”, “In Progress”, “Finished”).winner
: A string that represents the winning team of the match.teamAScore
andteamBScore
: Strings representing the scores of Team A and Team B, respectively.matchCreator
: The address of the user who created the match.
The smart contract also declares several events
that get emitted when certain actions are performed, such as creating a match, updating a team, and removing a player. Events in Ethereum are a way for a contract to provide information about changes in its state to the outside world.
The smart contract provides several functions for manipulating and viewing the data:
createMatch
: This function allows a user to create a new match by providing a title, start time, and end time. The match is initialized with empty teams, a status of “Scheduled”, and themsg.sender
(the address calling the function) as the match creator. The new match is then pushed to thematches
array, and aMatchCreated
event is emitted.updateTeamA
andupdateTeamB
: These functions allow the match creator to add players to Team A or Team B, respectively. They do this by calling theupdateTeam
function with the appropriate parameters.updateTeam
: This internal function updates either Team A or Team B of a specific match (specified by title), adding new player(s) to the team. It checks to make sure that the function caller is the match creator before updating the team. Once the team has been updated, it emits aTeamUpdated
event.removePlayer
: This function allows the match creator to remove a player from either Team A or Team B of a specific match. It searches through the appropriate team array, finds the player to be removed, and removes them from the array. It then emits aPlayerRemoved
event.updateMatchStatus
,updateMatchScore
, andupdateMatchWinner
: These functions allow the match creator to update the status, scores, and winner of a specific match, respectively. Each of these functions emits aMatchUpdated
orMatchScoreUpdated
event after performing its updates.getAllMatches
: This function allows anyone to view all the matches. It’s marked asview
because it does not modify any state and only returns data.
Overall, this smart contract provides a robust representation of an Esports platform, allowing users to create matches, manage teams, and track match status and scores in a decentralized way on the Celo blockchain.
After writing and testing the smart contract, you can deploy it to the Celo network using a deployment tool like Truffle or Hardhat or you can check out this tutorial to learn how to deploy a smart contract. You will get a contract address and ABI as a result of the deployment. Keep these handy, as they’ll be necessary for interacting with the contract from the Flutter app.
Building the Flutter UI
First, we need to clone the Flutter code repo by running this command in our terminal:
git clone https://github.com/Qtech11/celo_flutter_esports_platform_part1
Open the cloned project in your preferred IDE. You should have the following files in it:
In your terminal, run the command below to install all dependencies required for the project:
flutter pub get
-
The
main.dart
file is the root of the project -
The
views
directory contains two directories (screens
andwidgets
). Thescreens directory
contains the screen files of the app and thewidgets directory
contains widgets used in those screens -
The
create_match_screen.dart
file contains the code to display theCreate Match Screen
. The create match screen contains a form field to create a match as shown below:
-
The
matches_screen.dart
file contains the code to display theMatch Screen
. The match screen displays a tab bar view where each tab has a list of events which are grouped based on their status as shown below :
And it shows the screen below when no match/event has been created:
It also has a floating action button which helps to navigate to the create match screen
-
The
match_details_screen.dart
file contains the code to display theMatch Details Screen
. On this screen, we can view the details of a match, update players, update match status, update scores, and update winner. The presentation below gives us an illustration: -
The
web3_provider.dart
file serves as an intermediary between the logic and UI using the flutter riverpod package for managing the state of the app. -
The
web3_helper.dart
file contains the code to make the connection between the Flutter app and the deployed smart contract. It is well explained in theConnecting Our Flutter App to the Smart Contract
section
Dependencies Used
- Flutter Riverpod: To manage the state of the app, keep track of changes in data, and update the user interface accordingly.
- HTTP: To make network requests, it helps the app to talk to servers, send and receive data, and interact with APIs.
- Web3dart: To connect my Flutter App to celo blockchain and interact with the smart contracts I have developed.
- Intl: A package for formatting dates and times in a more user-friendly way.
- Expandable: To improve the user interface. To create expandable panels that make the app more intuitive, organized, and easy on the eyes.
Connecting Our Flutter App to the Smart Contract
Connecting the Flutter app to the already deployed smart contract was done using the web3dart
library. In our web3_helper.dart
file, we should have the code below:
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart';
import 'package:web3dart/web3dart.dart';
final client = Web3Client('https://alfajores-forno.celo-testnet.org', Client());
const abi =
'<your_actual_abi>'; // Replace these with your actual contract ABI and remove the string quote
final contractAddress = EthereumAddress.fromHex(
'0x5102609037bc756b50645036D8a9c30765C64257'); // replace with your actual contract address
final contractABI = json.encode(abi);
class Web3Helper {
// Create a contract instance that we can interact with
final contract = DeployedContract(
ContractAbi.fromJson(contractABI, "EsportsPlatform"),
contractAddress,
);
final credentials = EthPrivateKey.fromHex(
"8043ad3f57a701cd6c37532868e341d290b3f2c08e8093061c7ffd4401b2a7b4"); // replace with your celo wallet private key
Future sendTransaction(ContractFunction function, List parameters) async {
final response = await client.sendTransaction(
credentials,
Transaction.callContract(
contract: contract,
function: function,
parameters: parameters,
),
chainId: 44787,
);
while (true) {
final receipt = await client.getTransactionReceipt(response);
if (receipt != null) {
print('Transaction successful');
print(receipt);
break;
}
// Wait for a while before polling again
await Future.delayed(const Duration(seconds: 2));
}
return response;
}
Future getAllMatches() async {
final function = contract.function('getAllMatches');
final response = await client.call(
sender: credentials.address,
contract: contract,
function: function,
params: [],
);
return response;
}
Future createMatch(String title, int startTime, int endTime) async {
final function = contract.function('createMatch');
final response = await sendTransaction(
function, [title, BigInt.from(startTime), BigInt.from(endTime)]);
return response;
}
Future updateTeamA(String matchTitle, List<String> players) async {
final function = contract.function('updateTeamA');
final response = await sendTransaction(
function,
[matchTitle, players],
);
return response;
}
Future updateTeamB(String matchTitle, List<String> players) async {
final function = contract.function('updateTeamB');
final response = await sendTransaction(
function,
[matchTitle, players],
);
return response;
}
Future removePlayer(
String matchTitle, String playerName, bool isTeamA) async {
final function = contract.function('removePlayer');
final response = await sendTransaction(
function,
[matchTitle, playerName, isTeamA],
);
return response;
}
Future updateMatchStatus(String matchTitle, String status) async {
final function = contract.function('updateMatchStatus');
final response = await sendTransaction(
function,
[matchTitle, status],
);
return response;
}
Future updateMatchScore(
String matchTitle, String teamAScore, String teamBScore) async {
final function = contract.function('updateMatchScore');
final response = await sendTransaction(
function,
[matchTitle, teamAScore, teamBScore],
);
return response;
}
Future updateMatchWinner(String matchTitle, String winner) async {
final function = contract.function('updateMatchWinner');
final response = await sendTransaction(
function,
[matchTitle, winner],
);
return response;
}
}
This script connects to a smart contract deployed on the Alfajores Celo Testnet and performs a variety of interactions with it.
-
Firstly, an HTTP client (
Web3Client
) is created that is set to interact with the Celo Testnet. -
Next, a
ContractABI
(Application Binary Interface) is defined that describes how to interact with the smart contract. It defines which methods are available and how to call them. -
Then, an Ethereum address is specified. This is the address where the contract has been deployed on the Celo Testnet.
-
The
Web3Helper
class is then defined which contains methods that correspond to the methods in the smart contract. -
The
sendTransaction
function is a helper function that sends a transaction to the blockchain that interacts with a function in the smart contract. It takes the function from the contract and parameters that will be passed to the function, sends the transaction, then waits for the transaction to be mined. -
The
getAllMatches
function calls a read-only function on the contract that doesn’t modify the state, hence doesn’t require a transaction. -
The
createMatch
,updateTeamA
,updateTeamB
,removePlayer
,updateMatchStatus
,updateMatchScore
, andupdateMatchWinner
methods each call the corresponding method in the contract using thesendTransaction
method as a helper. Each of these methods modify the state of the contract and so a transaction is sent for each method.
The key thing to note is that transactions are used when you want to modifies the state of the contract and calls are used when you want to read the state of the contract.
Also, remember to replace the contract address and ABI with the one you got when you compiled and deploy your smart contract.
Presentation of how the app works
-
Creating a Match/Event
-
Updating a match status
You can debug the app to your device or emulator to check out all other features.
Conclusion
In this tutorial, we learned how to create a smart contract in Solidity that models an eSports platform. We also built the corresponding Dart code to interact with this contract using the web3dart library. The smart contract allows for creating matches, managing teams, and updating match details, all of which can be securely and transparently tracked on the Celo blockchain.
Our eSports platform smart contract has functions to create a match, add or remove players from teams, update match scores and status, and fetch all the match details. These features offer a basic but robust model for an eSports competition.
Next Steps
This tutorial was a great starting point for building an eSports platform on blockchain technology. The smart contract we’ve built lays the foundation for a robust and transparent eSports application. However, there are many more features we can add to make the platform more dynamic, user-friendly, and secure.
Here’s what you can look forward to in Part Two of this tutorial:
- User Authentication: We will introduce a feature that allows users to create an account and authenticate. Only authenticated users will be allowed to create and update matches. This will add an extra layer of security and personalization to our eSports platform.
- Optimizing Contract Interactions: We’ll revisit our smart contract and optimize it to make it more efficient and easier to interact with. This might involve restructuring our functions or refining our data types.
- Add to Favourites: Users will be able to add certain matches or players to a ‘Favourites’ list. This will enable quick and easy access to the matches or players they care about most.
- My Matches Screen: We’ll build a new screen that shows a list of all the matches a user has created. This feature will help users keep track of their activity on the platform.
- Front-End Development: As always, we’ll continue to refine and expand our front-end to provide a smooth, intuitive user experience. We’ll also look at how to provide real-time updates to users, so they can stay informed about their favourite matches and players.
- Search Functionality: To further enhance the user experience, we will incorporate a search function into the application. This will allow users to easily look up matches within the platform. With this feature, users will not need to manually scroll through lists to find their desired content, making navigation smoother and more efficient.
Remember, building a decentralized application is a journey. Each step you take not only adds a new feature to your application but also deepens your understanding of blockchain technology. So stay tuned for Part Two, and happy coding until then!
About the Author
Qudus Olanrewaju is a very proficient Mobile developer and technical writer who has a strong interest in blockchain technology and web3. He enjoys building web3 projects and leveraging his distinctive viewpoint to create engaging and insightful content. You can connect with me on Linkedin and check out my latest projects on Github