Integrating decentralized chat app using celo composer and flutter

Introduction

Creating a decentralized chat application using blockchain technology can enhance privacy and security. In this tutorial, we’ll use Celo, a mobile-first blockchain platform, and Flutter, a flexible framework for app development. Imagine we’re building a magic communication box. We write a message, lock it with a special key and send it. Only the person with the correct key can read it. That’s what we’re going to do, but instead of a box, we’ll use an app, and our special keys will be the technology we are using: Celo and Flutter!

Prerequisites

Before starting this tutorial, it is recommended to have some prior knowledge and complete the following prerequisites:

  1. Basic understanding of blockchain technology (specifically CELO) and smart contracts.
  2. Familiarity with the Flutter framework and Dart language.
  3. Prior experience with EVM would be helpful, as Celo is fully compatible with Ethereum’s tooling and coding conventions.

If you haven’t met these prerequisites, I recommend you take the time to familiarize yourself with the basics of blockchain and Flutter development before proceeding.

Requirements

Before we begin, make sure you have the following requirements met:

  1. Flutter Development Environment: Ensure that you have set up a Flutter development environment on your machine. You can follow the official Flutter installation guide for your operating system to get started.
  2. Solidity: We will use Solidity for writing smart contracts.
  3. 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.
  4. Fund your wallet using the Celo Faucet. To do this, copy the wallet address and visit the Celo Faucet. Then paste your wallet address into the designated field and confirm.
  5. Ensure Nodejs is installed on your computer. If not, download it from here and install it on your computer following the instructions given.

You can check out this video on how to install and set up the celo-wallet-extension on your computer.

Setting up the Project with Celo Composer

  • In your terminal run
    npx @celo/celo-composer@latest create
    This will prompt you to select the framework and template you want to use. In this tutorial, we’ll be using the Flutter as the front-end framework.

  • Once you have selected the Futter framework, you will be asked to select the smart contract framework which is the smart-contract development tool we’ll be using for this tutorial.
    Select Hardhat for this tutorial.

  • After selecting the tool, you will be asked if you want subgraph support for your dApp. Select “No” as we will not be using a subgraph for the dApp in this tutorial.

  • Finally, you will be asked to enter the name of the project. Enter the project name of your choice and press the enter key.

  • Once it’s successful, open the project folder in your preferred IDE (Android Studio or VSCode).

In the packages folder, you should see the flutter-app and hardhat folders. The hardhat folder contains the hardhat-project with the necessary setup needed to create and deploy our simple smart contract while the flutter-app folder contains the flutter starter files for our DApp.

Recently, walletconnect stopped functioning which means we won’t be using the wallet connect package but rather use web3Dart, flutter_dotenv, http, flutter_riverpod for state management, and convert packages.

  • Navigate to your root directory in your terminal to run this command to install the dependencies in the package.json files.
    yarn install
    Note that you must have yarn already installed on your machine.

After all dependencies have been installed, we can now go ahead to create our smart contract.

Building the Smart Contract

The contract folder in the hardhat folder contains some solidity files, go ahead and delete the files. Then create a new file in the contracts folder and name it ChatDApp.sol
In the ChatDApp.sol file, copy and paste this code.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.6;

contract Chat {
    struct Message {
        address sender;
        string content;
        uint timestamp;
    }

    Message[] public messages;

    function sendMessage(address _sender, string memory _content) public {
        messages.push(Message(_sender, _content, block.timestamp));
    }

    function getMessageCount() public view returns (uint) {
        return messages.length;
    }

    function getMessages(address _address) public view returns(Message[] memory) {
        uint count = 0;

        // We first need to know how many messages are from the provided address.
        for(uint i = 0; i < messages.length; i++) {
            if (messages[i].sender == _address) {
                count++;
            }
        }

        // Initialize a new array with the correct length.
        Message[] memory senderMessages = new Message[](count);

        // Add the messages of the sender to the new array.
        uint index = 0;
        for(uint i = 0; i < messages.length; i++) {
            if (messages[i].sender == _address) {
                senderMessages[index] = messages[i];
                index++;
            }
        }

        return senderMessages;
    }

    function getAllMessages() public view returns (Message[] memory) {
        return messages;
    }
}```

1. **Defining the Contract and struct:**
```js
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.6;

contract Chat {
    struct Message {
        address sender;
        string content;
        uint timestamp;
    }

    Message[] public messages;

This block declares the version of Solidity we are using (0.8.6 or higher). Then, we define the contract Chat . Inside the contract, we define a struct called Message which has three properties: sender (the address of the sender), content (the content of the message), and timestamp (the time when the message was created). Lastly, we declare an array messages to hold all messages of type Message .

  1. The sendMessage Function:
function sendMessage(address _sender, string memory _content) public {
        messages.push(Message(_sender, _content, block.timestamp));
    }

This function takes the content of the message as an argument. It creates a new Message struct with the sender’s address, the provided content, and the current timestamp (fetched using block.timestamp ). This new Message is then added to the messages array.

  1. The getMessageCount Function:
 function getMessageCount() public view returns (uint) {
        return messages.length;
    }

This function returns the number of messages stored on the blockchain. It does this by returning the length of the messages array.

  1. The getMessages Function:
   function getMessages(address _address) public view returns(Message[] memory) {
        uint count = 0;

        // We first need to know how many messages are from the provided address.
        for(uint i = 0; i < messages.length; i++) {
            if (messages[i].sender == _address) {
                count++;
            }
        }

        // Initialize a new array with the correct length.
        Message[] memory senderMessages = new Message[](count);

        // Add the messages of the sender to the new array.
        uint index = 0;
        for(uint i = 0; i < messages.length; i++) {
            if (messages[i].sender == _address) {
                senderMessages[index] = messages[i];
                index++;
            }
        }

        return senderMessages;
    }

This function takes an address as an argument and retrieves the Message associated with the address from the messages array. It returns the sender , content , and timestamp properties of that Message .
Remember, while the above contract will work, it is not very efficient because the getMessages function has to loop over the entire list of messages twice. As the number of messages increases, the function will require more and more gas to execute. In a production environment, you would want to optimize this to ensure the function can execute efficiently. For example, by keeping a mapping of addresses to messages. This would require changes in how you save and retrieve messages.

  1. The getAllMessages Function:
  function getAllMessages() public view returns (Message[] memory) {
        return messages;
    }
}

This function returns all the messages stored in the messages array. Each message includes the sender’s address, the content, and the timestamp.

Compiling the Smart Contract

Before deploying your smart contract to the Celo blockchain, you will have to compile the smart contract using a command in your terminal.

  • To compile the smart contract, run these commands to navigate to the hardhat directory from the root directory of your project.
 cd packages
 cd hardhat
  • Then create a .env file in the hardhat folder and add your MNEMONIC (phrases) key in the .env file or make use of your private key.
 MNEMONIC=//add your wallet seed phrase here
 PRIVATE_KEY= ‘<your private key here>’
  • Install the dotenv package to be able to import the env file and use it in the config.
    Enter this command in your terminal to install the dotenv package
    yarn add dotenv
  • Next, open the hardhat.config.js file and replace the content of the file with the code below:
    Note that the solidity version depends on the pragma version you used in your smart contract file.
require("@nomicfoundation/hardhat-chai-matchers")
require('dotenv').config({path: '.env'});
require('hardhat-deploy');

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

// Prints the Celo accounts associated with the mnemonic in .env
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  defaultNetwork: "alfajores",
  networks: {
    localhost: {
        url: "http://127.0.0.1:7545"
    },
    alfajores: {
      gasPrice: 200000000000,
      gas: 41000000000,
      url: "https://alfajores-forno.celo-testnet.org",
      accounts: [process.env.PRIVATE_KEY],
      // {
      //   mnemonic: process.env.MNEMONIC,
      //   path: "m/44'/52752'/0'/0"
      // },
      chainId: 44787
    },
    celo: {
      url: "https://forno.celo.org",
      accounts: [process.env.PRIVATE_KEY],
      chainId: 42220
    },     
  },
  solidity: "0.8.6",
};
  • Now to compile the contract, run this command in your terminal.
npx hardhat compile
  • Once the compilation is successful, you should see the message below on your terminal:
compiled 1 solidity file successfully

Deploying the Smart Contract

We will be deploying the smart contract to the Alfajores testnet. To do this,

  • Replace the content of the 00-deploy.js in the deploy folder with the code below:
const hre = require("hardhat");
// const { ethers } = require("hardhat");

async function main() {
  // const [sender, receiver] = await ethers.getSigners();
  // const senderBalance = await ethers.provider.getBalance(sender.address); // Get the balance of the sender account

  const gasPrice = hre.ethers.utils.parseUnits("10", "gwei"); // Adjust the gas price as needed
  const gasLimit = 2000000;

  console.log(gasPrice);
  console.log( await hre.ethers.provider.getSigner().getAddress());
  

  const Chat = await hre.ethers.getContractFactory("Chat");
  const chat = await Chat.deploy({gasPrice, gasLimit});

  await chat.deployed();

  console.log("Chat deployed to:", chat.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
  • Then, run the following command in your terminal to deploy the smart contract to the Celo blockchain.
npx hardhat run deploy/00-deploy.js --network alfajores
  • Save the smart contract address that will be printed on your terminal when deployment is successful somewhere because you will need it in the course of the tutorial.
  • For the purpose of this tutorial, inside the flutter project, create a .env file, create variables inside the .env file called “CONTRACT_ADDRESS” and “PRIVATE_KEY”, then assign the address returned after deploying your smart contract to celo alfajores network.
  • Also copy the contract abi from packages/hardhat/artifacts/contracts/RegisterDid.sol/CeloDIDRegistry.json file, and then create a new file inside your flutter project called “did.abi.json”, paste the ABI copied inside this file.

In this tutorial, the did.abi.json file is located inside the flutter-app/lib/module/services folder.

Getting Started with Flutter

  • Ensure you navigate to the flutter project folder from the root folder by running the following commands:
 cd packages
 cd flutter-app
  • We’ll start by adding the dependencies to the pubspec.yaml file or run the command below in your terminal.

To keep it simple, we will be making use of the flutter_riverpod package for state management instead of the proposed flutter_bloc proposed by Celo composer.
So replace the pubspec.yaml file in your flutter project with the following code:

name: flutter_celo_composer
description: A new Flutter project.

publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 1.0.0+1

environment:
  sdk: ">=2.17.6 <3.0.0"

dependencies:
  cupertino_icons: ^1.0.2
  flutter:
    sdk: flutter
  flutter_dotenv: ^5.0.2
  convert: ^3.1.1
  flutter_secure_storage: ^8.0.0
  flutter_svg: ^2.0.5
  http: ^0.13.5
  web3dart: ^2.5.1
  flutter_riverpod: ^2.3.4

dev_dependencies:
  build_runner: null
  flutter_lints: ^2.0.0
  flutter_test:
    sdk: flutter
  mocktail: ^0.3.0

flutter:

  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  assets:
    - .env
    - lib/module/services/did.abi.json
    - assets/images/logo.png
    - assets/images/

  
  fonts:
    - family: Raleway
      fonts:
        - asset: assets/fonts/Raleway/Raleway-Regular.ttf
        - asset: assets/fonts/Raleway/Raleway-Italic.ttf
          style: italic
        - asset: assets/fonts/Raleway/Raleway-Light.ttf
          weight: 300
        - asset: assets/fonts/Raleway/Raleway-Medium.ttf
          weight: 500
        - asset: assets/fonts/Raleway/Raleway-Semibold.ttf
          weight: 600
        - asset: assets/fonts/Raleway/Raleway-Bold.ttf
          weight: 700
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages
  • Then run this command in your terminal to add the dependencies to your flutter project:
flutter pub get

Building the User Interface

Replace the code in your main.dart file with the code below as this serves as the root of our flutter application:

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_celo_composer/configs/themes.dart';
import 'package:flutter_celo_composer/module/services/secure_storage.dart';
import 'package:flutter_celo_composer/module/view/screens/authentication_screen.dart';
import 'package:flutter_celo_composer/module/view/screens/home_page.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() async {
  await dotenv.load(fileName: '.env');
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ProviderScope(
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        theme: buildDefaultTheme(context),
        home: const MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String? isCreated;

  @override
  void initState() {
    super.initState();
    SchedulerBinding.instance.addPostFrameCallback((_) async {
      isCreated = await UserSecureStorage().getCreatedBoolean();
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return isCreated != null && isCreated == 'true'
        ? const HomePage()
        : const AuthenticationScreen();
  }
}

We will be creating five(5) folders inside the module folder, these include the VIEW folder that contains the User Interfaces (Screens) of the app and the widgets, the VIEW_MODEL which contains the providers and the controllers of the application, the MODEL folder which contains the repository folder for interacting with libraries, packages and services, the CUSTOM_WIDGETS folder that contains all the custom widgets used in the app and finally we’ll have the SERVICES folder which will contain the smart contract interactions, the abi json file and other services needed in the app.

The project structure should look like this:

Firstly, the user will register their username and wallet address on the authentication screen. If it’s a first time user, otherwise users will be navigated to the chat screen.
Below is the code for the authentication screen:

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_celo_composer/module/custom_widgets/snack_bar.dart';
import 'package:flutter_celo_composer/module/services/secure_storage.dart';
import 'package:flutter_celo_composer/module/view/screens/home_page.dart';
import 'package:flutter_celo_composer/module/view/widgets/custom_button.dart';
import 'package:flutter_celo_composer/module/view/widgets/custom_textfield.dart';
import 'package:flutter_celo_composer/module/view_model/controllers/celo_controller.dart';
import 'package:flutter_celo_composer/module/view_model/providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class AuthenticationScreen extends ConsumerStatefulWidget {
  const AuthenticationScreen({super.key});

  @override
  ConsumerState<AuthenticationScreen> createState() =>
      _AuthenticationScreenState();
}

class _AuthenticationScreenState extends ConsumerState<AuthenticationScreen> {
  TextEditingController walletController = TextEditingController();
  TextEditingController usernameController = TextEditingController();
  String? text;
  bool obscureText = true;

  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: SingleChildScrollView(
          child: Padding(
            padding: EdgeInsets.only(
                left: size.width * 0.04, right: size.width * 0.04, top: 60),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Row(
                  children: <Widget>[
                    Image.asset('assets/images/logo.png',
                        height: 60, width: 90),
                    const SizedBox(width: 10),
                    const Text(
                      'Chat DApp',
                      maxLines: 1,
                      style: TextStyle(
                          fontSize: 24,
                          fontWeight: FontWeight.w800,
                          color: Colors.black54),
                    )
                  ],
                ),
                Form(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      const Text(
                        'Enter your details to register your address an username on the chat dApp.',
                        style: TextStyle(
                            fontSize: 14,
                            fontWeight: FontWeight.w400,
                            color: Colors.black),
                      ),
                      const SizedBox(height: 30),
                      CustomTextField(
                        text: 'Username',
                        controller: usernameController,
                        hint: 'enter your username',
                      ),
                      const SizedBox(height: 15),
                      CustomTextField(
                        text: 'Wallet Address',
                        controller: walletController,
                        hint: 'enter your wallet address',
                      ),
                    ],
                  ),
                ),
                const SizedBox(height: 50),
                CustomButtonWidget(
                  text: ref.watch(celoProvider).authStatus == Status.loading
                      ? const CircularProgressIndicator(color: Colors.white)
                      : const Text(
                          'Save Details',
                          style: TextStyle(
                              fontSize: 18,
                              fontWeight: FontWeight.w500,
                              color: Colors.white),
                        ),
                  onPressed: () async {
                    if (walletController.text.trim().isEmpty ||
                        usernameController.text.trim().isEmpty) {
                      CustomSnackbar.responseSnackbar(context, Colors.redAccent,
                          'Fill the required fields..');
                      return;
                    }
                    await ref.read(celoProvider).authenticateUser(
                        usernameController.text.trim(),
                        walletController.text.trim(),
                        context);
                  },
                ),
                const SizedBox(height: 10),
                Center(
                  child: RichText(
                      textAlign: TextAlign.center,
                      text: TextSpan(
                          text: 'Already registered ? ',
                          style: const TextStyle(
                              fontSize: 14,
                              fontWeight: FontWeight.w400,
                              color: Colors.black),
                          children: <TextSpan>[
                            TextSpan(
                                text: 'Proceed to Home',
                                style: const TextStyle(
                                    fontSize: 14,
                                    fontWeight: FontWeight.w500,
                                    color: Colors.black54),
                                recognizer: TapGestureRecognizer()
                                  ..onTap = () async {
                                    await UserSecureStorage()
                                        .setCreatedBoolean();
                                    if (!mounted) return;
                                    Navigator.pushReplacement(
                                        context,
                                        MaterialPageRoute<dynamic>(
                                            builder: (_) => const HomePage()));
                                  })
                          ])),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    walletController.dispose();
    usernameController.dispose();
    super.dispose();
  }
}

When the user clicks on the Save Detail button, it saves the user details in a secure storage on the app. This allows the app to store the user details including the wallet address and username.

Then we have the Chat screen that displays the chats gotten from the smartcontract:

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_celo_composer/module/custom_widgets/format_date.dart';
import 'package:flutter_celo_composer/module/models/chat_model.dart';
import 'package:flutter_celo_composer/module/services/secure_storage.dart';
import 'package:flutter_celo_composer/module/view/widgets/custom_textfield.dart';
import 'package:flutter_celo_composer/module/view/widgets/message_bubble.dart';
import 'package:flutter_celo_composer/module/view_model/providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class ChatScreen extends ConsumerStatefulWidget {
  const ChatScreen({super.key});

  @override
  ConsumerState<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends ConsumerState<ChatScreen> {
  String? userAddress;
  TextEditingController textController = TextEditingController();
  List<ChatDetailModel> messages = [
    // Add your messages here, for example:
    // Message('0x123', 'Hello', DateTime.now()),
  ];

  @override
  void initState() {
    SchedulerBinding.instance.addPostFrameCallback((_) async {
      ref.read(celoProvider).fetchChats(context);
      userAddress = await UserSecureStorage().getUserAddress();
      setState(() {});
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Expanded(
            child: ListView.builder(
              itemCount: ref.watch(celoProvider).messages.length,
              itemBuilder: (dynamic context, int index) {
                ChatDetailModel message =
                    ref.read(celoProvider).messages[index];
                final bool isMe =
                    message.sender!.toLowerCase() == userAddress!.toLowerCase();
                return Padding(
                  padding: const EdgeInsets.all(5),
                  child: MessageBubble(
                      sender: message.sender ?? '',
                      text: message.content ?? '',
                      date: convertDate(message.timestamp ?? DateTime.now()),
                      isMe: isMe),
                );
              },
            ),
          ),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 0),
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Expanded(
                  child: CustomTextField(
                    text: '',
                    maxLines: 3,
                    minLines: 1,
                    hint: 'Type a message',
                    controller: textController,
                  ),
                ),
                const SizedBox(width: 10),
                InkWell(
                  onTap: _sendMessage,
                  child: const Padding(
                    padding: EdgeInsets.only(top: 20),
                    child: Icon(
                      Icons.send,
                      size: 40,
                      color: Colors.black54,
                    ),
                  ),
                )
              ],
            ),
          )
        ],
      ),
      // floatingActionButton: FloatingActionButton(
      //   onPressed: () {
      //     // This is where you trigger the function to send a message.
      //   },
      //   child: Icon(Icons.send),
      // ),
    );
  }

  void _sendMessage() async {
    FocusScope.of(context).unfocus();
    if (textController.text.isNotEmpty) {
      DateTime now = DateTime.now();
      final message = textController.text.trim();
      ref
          .read(celoProvider)
          .sendChat(userAddress ?? '', message, context);
      textController.clear();
    }
  }
}

The controller class connects the UI to the web3 helper class that interacts directly with the smart contract. Below is the CeloWeb3Helper class content:

// import 'dart:convert';

import 'package:convert/convert.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart';
import 'package:web3dart/web3dart.dart';

final Web3Client client =
    Web3Client('https://alfajores-forno.celo-testnet.org', Client());

// Replace these with your actual contract ABI
// String? contractKey = dotenv.get('CONTRACT_ADDRESS');

String? privateKey = dotenv.env['PRIVATE_KEY'];
final String? contractAddress = dotenv.env['CONTRACT_ADDRESS'];

// replace with your actual contract address

class CeloWeb3Helper {
  /// Get deployed greeter contract
  Future<DeployedContract> get deployedCeloContract async {
    const String abiDirectory = 'lib/module/services/did.abi.json';
    String contractABI = await rootBundle.loadString(abiDirectory);

    final DeployedContract contract = DeployedContract(
      ContractAbi.fromJson(contractABI, 'Chat'),
      EthereumAddress.fromHex(contractAddress!),
    );

    return contract;
  }

  final EthPrivateKey credentials = EthPrivateKey.fromHex(privateKey ?? '');

  Future<dynamic> sendChat(String sender, String content) async {
    print(content);
    final DeployedContract contract = await deployedCeloContract;
    final ContractFunction sendMessageFunction =
        contract.function('sendMessage');
    final address = EthereumAddress.fromHex(sender);
    final response = await client.sendTransaction(
      credentials,
      Transaction.callContract(
        contract: contract,
        function: sendMessageFunction,
        parameters: <dynamic>[address, content],
      ),
      chainId: 44787,
    );

    final dynamic receipt = await awaitResponse(response);
    return receipt;
  }

  Future<dynamic> fetchMessage(String address) async {
    final DeployedContract contract = await deployedCeloContract;
    final wallet = EthereumAddress.fromHex(address);
    final fetchMessagesFunction = contract.function('getMessages');
    final response = await client.call(
      contract: contract,
      function: fetchMessagesFunction,
      params: <dynamic>[wallet],
    );
    print('response ====>>>> $response');
    return response[0];
  }

  Future<dynamic> fetchMessages() async {
    final DeployedContract contract = await deployedCeloContract;
    final fetchMessagesFunction = contract.function('getAllMessages');
    final response = await client.call(
      contract: contract,
      function: fetchMessagesFunction,
      params: <dynamic>[],
    );
    print('response ====>>>> $response');
    return response[0];
  }

  Future<dynamic> awaitResponse(dynamic response) async {
    int count = 0;
    while (true) {
      final TransactionReceipt? receipt =
          await client.getTransactionReceipt(response);
      if (receipt != null) {
        print('receipt ===>> $receipt');
        return receipt.logs;
      }
      // Wait for a while before polling again
      await Future<dynamic>.delayed(const Duration(seconds: 1));

      if (count == 6) {
        return null;
      } else {
        count++;
      }
    }
  }
}

The CeloWeb3Helper class has 3 methods, the sendMessage method collects the details of the chats including the sender address, and the content which is the message and sends to the sendMessage function of the Chat smart contract. The fetchMessages method interacts with the getAllMessages function of the Chat Smart contract to get all the Messages available. The fetchMessage method accepts the sender address and pass it as a parameter to the getMessages function of the smart contract to get the messages associated with that address.

Below is the controller class that extends ChangeNotifier which serves as aan intermediary between our UI and the CeloWeb3Helper service class. Note that we also declared a global provider variable to manage the state in our UI.

import 'package:flutter/material.dart';
import 'package:flutter_celo_composer/module/custom_widgets/alert_dialog.dart';
import 'package:flutter_celo_composer/module/custom_widgets/snack_bar.dart';
import 'package:flutter_celo_composer/module/models/chat_model.dart';
import 'package:flutter_celo_composer/module/services/celo_web3.dart';
import 'package:flutter_celo_composer/module/services/secure_storage.dart';
import 'package:flutter_celo_composer/module/view/screens/home_page.dart';

enum Status { init, loading, done }

class CeloChatProvider extends ChangeNotifier {
  UserSecureStorage storage = UserSecureStorage();
  CeloWeb3Helper helper = CeloWeb3Helper();
  Status createStatus = Status.init;
  Status verifyStatus = Status.init;
  Status changeStatus = Status.init;
  Status authStatus = Status.init;
  Status viewStatus = Status.init;

  List<ChatDetailModel> messages = <ChatDetailModel>[];
  List<ChatDetailModel> myMessages = <ChatDetailModel>[];

  Future<dynamic> sendChat(
      String sender, String content, dynamic context) async {
     messages.add(ChatDetailModel(sender: sender, content: content, timestamp: DateTime.now()));
    try {

      var response = await helper.sendChat(sender, content);
      if (response != null) {
      } else {
        CustomSnackbar.responseSnackbar(
            context, Colors.redAccent, 'unable to add message');
      }
    } catch (e) {
      CustomSnackbar.responseSnackbar(context, Colors.redAccent, e.toString());
      debugPrint(e.toString());
    }
  }

  Future<dynamic> fetchChats(dynamic context) async {
    try {
      // String did = addCeloPrefix(data.identifier ?? '');
      viewStatus = viewStatus != Status.loading ? Status.loading : Status.done;
      if (viewStatus == Status.done) return;
      notifyListeners();
      var response = await helper.fetchMessages();
      if (response != null) {
        List<ChatDetailModel> newMessages = <ChatDetailModel>[];
        for (var i in response) {
          if (i[1].toString().isNotEmpty) {
            ChatDetailModel message = ChatDetailModel(
                sender: i[0].toString(),
                content: i[1],
                timestamp:
                    DateTime.fromMillisecondsSinceEpoch(i[2].toInt() * 1000));
            print(message.toJson());
            newMessages.add(message);
          }
        }
        messages = newMessages;
        notifyListeners();
      } else {
        CustomSnackbar.responseSnackbar(
            context, Colors.redAccent, 'Unable to get all messages');
      }
    } catch (e) {
      CustomSnackbar.responseSnackbar(context, Colors.redAccent, e.toString());
    }
  }

  Future<dynamic> fetchChat(String address, dynamic context) async {
    try {
      // String did = addCeloPrefix(data.identifier ?? '');
      viewStatus = viewStatus != Status.loading ? Status.loading : Status.done;
      if (viewStatus == Status.done) return;
      notifyListeners();
      var response = await helper.fetchMessage(address);
      if (response != null) {
        List<ChatDetailModel> newMessages = <ChatDetailModel>[];
        print("Response in controller ===>>> $response");
        for (var i in response) {
          if (i[1].toString().isNotEmpty) {
            ChatDetailModel message = ChatDetailModel(
                sender: i[0].toString(),
                content: i[1],
                timestamp:
                    DateTime.fromMillisecondsSinceEpoch(i[2].toInt() * 1000));
            print(message.toJson());
            newMessages.add(message);
          }
        }
        myMessages = newMessages;
        notifyListeners();
      } else {
        CustomSnackbar.responseSnackbar(
            context, Colors.redAccent, 'Unable to get your messages');
      }
    } catch (e) {
      CustomSnackbar.responseSnackbar(context, Colors.redAccent, e.toString());
    }
  }

  Future<dynamic> authenticateUser(
      String username, String wallet, dynamic context) async {
    try {
      authStatus = authStatus != Status.loading ? Status.loading : Status.done;
      if (authStatus == Status.done) return;
      notifyListeners();
      await storage.setUserAddress(wallet);
      await storage.setUsername(username);
      await storage.setCreatedBoolean();
      authStatus = Status.done;
      notifyListeners();
      alertDialogs(
          context,
          'Save Details',
          'Your details has successfully being saved and will be displayed on your chats',
          () => Navigator.pushAndRemoveUntil(
              context,
              MaterialPageRoute<dynamic>(
                  builder: (dynamic context) => const HomePage()),
              (route) => false));
    } catch (e) {
      authStatus = Status.done;
      notifyListeners();
      CustomSnackbar.responseSnackbar(context, Colors.redAccent, e.toString());
    }
  }
}

More Images

Conclusion

We’ve established a foundational smart contract for our decentralized chat app using Solidity. This contract allows for storing and fetching messages on the Celo blockchain. The messages contain content, sender’s address, and timestamp. The Flutter-based UI includes three screens - a user details screen to collect users’ information, a chat screen where users can view and send messages and a screen for users to view their sent messages. The chat screen organizes messages according to their timestamps and aligns the messages based on the sender’s address. User addresses are used to differentiate messages and can also be used to fetch messages related to a specific address.

Next Steps

Now that you have built the foundational elements of a decentralized chat application, there are various directions you could take this project:

  1. Optimize the Smart Contract: As mentioned, the getMessages function is inefficient as it loops over all messages twice. You could re-architect your contract to use a mapping of addresses to messages for more efficient data retrieval. Here is a good discussion on storage patterns in Solidity.

  2. Enhance the User Interface: You might want to add more features to your chat UI. These could include features like typing indicators, read receipts, and multimedia message support.

  3. Implement Secure Private Messaging: You might want to add private messaging functionality. To do this, you will need to integrate asymmetric encryption. When a user sends a private message, the content could be encrypted with the receiver’s public key. Here is a Flutter package for adding encryption.

  4. Add User Authentication: You can add an authentication system to your app to allow users to create accounts and securely manage their keys.

  5. Deploy the App: You might want to deploy your app to the Google Play Store or the Apple App Store to share it with others. The official Flutter documentation provides a great guide on how to do this.

Remember, building a DApp is not just about coding, but also about thinking and designing systems that are efficient, secure, and user-friendly. Happy coding!

About the Author

John Igwe Eke, a proficient mobile developer and a technical writer, combines his expertise in software development with his enthusiasm for blockchain and Web3 technologies. He leverages this unique blend of skills to distill complex concepts into understandable, engaging content, thereby aiding in the democratization of these emerging technologies. Connect with me on Twitter, LinkedIn, and Github.

References

  1. Flutter Documentation
  2. Celo Documentation
  3. web3dart Documentation
  4. Celo Composer
  5. Solidity by example
  6. Source Code
  7. Link to apk
7 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:

5 Likes

Hi @johnigwe I’ll be reviewing Your piece in 1 to 2 days

3 Likes

Alright, thanks

2 Likes