Creating a Celo-Based Decentralized eSports Platform(Part 1)

Creating a Celo-Based Decentralized eSports Platform(Part 1) https://celo.academy/uploads/default/optimized/2X/e/ed80ea42720f19535a925ea193c74258e9284e3c_2_1024x576.jpeg
none 4.0 1

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

  1. Flutter SDK: Make sure that you have the latest Flutter SDK installed on your system. If not, you can get it from here.
  2. 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.
  3. Solidity: We will use Solidity for writing smart contracts.
  4. 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 and teamB: String arrays that represent the names of the players in Team A and Team B, respectively.
  • startTime and endTime: 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 and teamBScore: 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 the msg.sender (the address calling the function) as the match creator. The new match is then pushed to the matches array, and a MatchCreated event is emitted.
  • updateTeamA and updateTeamB: These functions allow the match creator to add players to Team A or Team B, respectively. They do this by calling the updateTeam 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 a TeamUpdated 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 a PlayerRemoved event.
  • updateMatchStatus, updateMatchScore, and updateMatchWinner: These functions allow the match creator to update the status, scores, and winner of a specific match, respectively. Each of these functions emits a MatchUpdated or MatchScoreUpdated event after performing its updates.
  • getAllMatches: This function allows anyone to view all the matches. It’s marked as view 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
  1. The main.dart file is the root of the project

  2. The views directory contains two directories (screens and widgets). The screens directory contains the screen files of the app and the widgets directory contains widgets used in those screens

  3. The create_match_screen.dart file contains the code to display the Create Match Screen. The create match screen contains a form field to create a match as shown below:

  4. The matches_screen.dart file contains the code to display the Match 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

  5. The match_details_screen.dart file contains the code to display the Match 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:

    ezgif.com-optimize

  6. 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.

  7. 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 the Connecting Our Flutter App to the Smart Contract section

Dependencies Used

  1. Flutter Riverpod: To manage the state of the app, keep track of changes in data, and update the user interface accordingly.
  2. HTTP: To make network requests, it helps the app to talk to servers, send and receive data, and interact with APIs.
  3. Web3dart: To connect my Flutter App to celo blockchain and interact with the smart contracts I have developed.
  4. Intl: A package for formatting dates and times in a more user-friendly way.
  5. 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, and updateMatchWinner methods each call the corresponding method in the contract using the sendTransaction 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

  1. Creating a Match/Event

    ezgif.com-video-to-gif (1)

  2. Updating a match status

    ezgif.com-optimize (1)

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

References

  1. Flutter Documentation
  2. web3dart Documentation
  3. Source Code
  4. Building Decentralized Applications on the Celo Platform using the Celo SDK and Celo Smart Contracts
11 Likes

Would like to see how this works :thinking:

4 Likes

Fantastic news! Your proposal has landed in this week’s top voted list. As you begin your project journey, remember to align with our community and technical guidelines, ensuring a high quality platform for our developers. Congratulations! :mortar_board: :seedling:

4 Likes

Thanks @Celo_Academy

2 Likes

Hi @Qtech I’ll be reviewing this in 1 to 2 days

2 Likes

Okay :+1:

2 Likes

Been going through your qork. You have quite a number of good flutter web 3 projects

Hello @Qtech,

Thanks for creating interesting content on the platform. To let you know, the highlighted contract below does not compile. I included the error trace to help you quickly fix it. Please tag me when you’re done fixing it.

Error message

from solidity:
DeclarationError: Undeclared identifier.
   --> contracts/Lv2.sol:108:22:
    |
108 |                 emit MatchScoreUpdated(_matchTitle, _teamAScore, _teamBScore);
    |                      ^^^^^^^^^^^^^^^^^
6 Likes

Thanks for pointing that out. Forgot to update

Fixed the error. It should compile now

2 Likes

Ok! Beautiful. It’s fine now. :+1:

11 Likes