WEB3DEV

Cover image for Integrando um Aplicativo de Chat Descentralizado usando o Celo Composer e o Flutter
Paulo Gio
Paulo Gio

Posted on

Integrando um Aplicativo de Chat Descentralizado usando o Celo Composer e o Flutter

Introdução

Criar um aplicativo de chat descentralizado usando a tecnologia blockchain pode melhorar a privacidade e a segurança. Neste tutorial, usaremos a Celo, uma plataforma blockchain voltada para dispositivos móveis, e o Flutter, um framework flexível para desenvolvimento de aplicativos. Imagine que estamos construindo uma caixa de comunicação mágica. Escrevemos uma mensagem, trancamos com uma chave especial e enviamos. Somente a pessoa com a chave correta pode lê-la. É isso que vamos fazer, mas em vez de uma caixa, usaremos um aplicativo, e nossas chaves especiais serão a tecnologia que estamos usando: Celo e Flutter!

Pré-requisitos

Antes de iniciar este tutorial, é recomendado ter algum conhecimento prévio e cumprir os seguintes pré-requisitos:

  1. Entendimento básico da tecnologia blockchain (especificamente da CELO) e contratos inteligentes.
  2. Familiaridade com o framework Flutter e a linguagem Dart.
  3. Experiência anterior com EVM seria útil, pois a Celo é totalmente compatível com as ferramentas e convenções de codificação da Ethereum.

Se você não atender a esses pré-requisitos, recomendo que reserve um tempo para se familiarizar com os conceitos básicos de blockchain e desenvolvimento em Flutter antes de continuar.

Requisitos

Antes de começarmos, certifique-se de ter os seguintes requisitos:

  1. Ambiente de Desenvolvimento Flutter: Certifique-se de ter configurado um ambiente de desenvolvimento Flutter em seu computador. Você pode seguir o guia de instalação oficial do Flutter para o seu sistema operacional para começar.
  2. Solidity: Usaremos o Solidity para escrever contratos inteligentes.
  3. Uma Carteira Celo: Configure uma carteira na testnet (rede de testes) da Celo, Alfajores. Você pode fazer isso seguindo os passos da seção “Setting up a Celo Wallet” (Configurando uma Carteira Celo) neste tutorial.
  4. Coloque fundos na sua carteira usando a Torneira Celo (Celo Faucet). Para fazer isso, copie o endereço da carteira e visite a Torneira Celo. Em seguida, cole seu endereço de carteira no campo designado e confirme.
  5. Certifique que o Nodejs esteja instalado em seu computador. Se não estiver, baixe-o aqui e instale no seu computador seguindo as instruções fornecidas.

Você pode conferir este vídeo sobre como instalar e configurar a extensão celo-wallet no seu computador.

Configurando o Projeto com o Celo Composer

  • No seu terminal, execute npx @celo/celo-composer@latest create. Isso solicitará que você selecione o framework e o template que deseja usar. Neste tutorial, usaremos o Flutter como framework de front-end.
  • Uma vez selecionado o Flutter, será solicitado que você escolha o framework de desenvolvimento de contratos inteligentes que usaremos neste tutorial. Selecione o Hardhat.
  • Após selecionar a ferramenta, será perguntado se você deseja suporte a subgrafos para o seu dApp. Selecione "Não", pois não usaremos um subgrafo neste tutorial.
  • Por fim, você será solicitado a inserir o nome do projeto. Digite o nome do projeto de sua escolha e pressione a tecla “Enter”.
  • Uma vez que tudo seja bem-sucedido, abra a pasta do projeto no seu IDE preferido (Android Studio ou VSCode).

No diretório packages, você verá as pastas flutter-app e hardhat. A pasta hardhat contém o projeto hardhat necessário para criar e implantar nosso contrato inteligente simples, enquanto a pasta flutter-app contém os arquivos iniciais do Flutter para nosso DApp.

Recentemente, o walletconnect parou de funcionar, o que significa que não usaremos o pacote walletconnect, mas sim os pacotes web3Dart, flutter_dotenv, http, flutter_riverpod para gerenciamento de estado e conversão de pacotes.

  • Navegue até o diretório raiz no terminal e execute o comando para instalar as dependências dos arquivos package.json: yarn install. Lembre-se de que é preciso ter o yarn instalado no seu computador.

Após a instalação de todas as dependências, podemos prosseguir com a criação do nosso contrato inteligente.

Construindo o Contrato Inteligente

A pasta contracts no diretório hardhat contém alguns arquivos solidity. Exclua esses arquivos e crie um novo chamado ChatDApp.sol. No arquivo ChatDApp.sol, copie e cole o código fornecido.

// 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;

        // Primeiro, precisamos saber quantas mensagens são do endereço fornecido.
        for(uint i = 0; i < messages.length; i++) {
            if (messages[i].sender == _address) {
                count++;
            }
        }

        // Inicializa um novo array com o tamanho correto.
        Message[] memory senderMessages = new Message[](count);

        // Adiciona as mensagens do remetente ao novo 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; 
    }
}
Enter fullscreen mode Exit fullscreen mode

1. Definindo o Contrato e a sua struct:

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

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

    Message[] public messages;
Enter fullscreen mode Exit fullscreen mode

Esse bloco declara a versão do Solidity que estamos usando (0.8.6 ou superior). Então, definimos o contrato Chat. Dentro do contrato, definimos uma struct chamada Message que possui três propriedades: sender (o endereço do remetente), content (o conteúdo da mensagem) e timestamp (o momento em que a mensagem foi criada). Por fim, declaramos um array messages para armazenar todas as mensagens do tipo Message.

2. A Função sendMessage:

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

Essa função recebe o conteúdo da mensagem como argumento. Ela cria uma nova struct Message com o endereço do remetente, o conteúdo fornecido e a marca temporal atual (obtida usando block.timestamp). Esta nova mensagem (Message) é então adicionada ao array messages.

3. A Função getMessageCount:

function getMessageCount() public view returns (uint) {
    return messages.length;
}
Enter fullscreen mode Exit fullscreen mode

Essa função retorna o número de mensagens armazenadas na blockchain. Ela faz isso retornando o comprimento do array messages.

4. A Função getMessages:

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

        // Primeiro, precisamos saber quantas mensagens são do endereço fornecido.
        for(uint i = 0; i < messages.length; i++) {
            if (messages[i].sender == _address) {
                count++;
            }
        }

        // Inicializa um novo array com o tamanho correto.
        Message[] memory senderMessages = new Message[](count);

        // Adiciona as mensagens do remetente ao novo array.
        uint index = 0;
        for(uint i = 0; i < messages.length; i++) {
            if (messages[i].sender == _address) {
                senderMessages[index] = messages[i];
                index++;
            }
        }

        return senderMessages;
    }
Enter fullscreen mode Exit fullscreen mode

Esta função aceita um endereço como argumento e recupera a mensagem (Message) associada ao endereço do array messages. Ela retorna as propriedades sender, content e timestamp dessa Message. Lembre-se, embora o contrato acima funcione, ele não é muito eficiente porque a função getMessages tem que percorrer toda a lista de mensagens duas vezes. À medida que o número de mensagens aumenta, a função exigirá cada vez mais gás para ser executada. Em um ambiente de produção, é desejável otimizar isso para garantir que a função possa ser executada de forma eficiente. Por exemplo, mantendo um mapeamento de endereços para mensagens. Isso exigiria mudanças na forma como você salva e recupera mensagens.

5. A Função getAllMessages:

function getAllMessages() public view returns (Message[] memory) {
    return messages;
}
Enter fullscreen mode Exit fullscreen mode

Esta função retorna todas as mensagens armazenadas no array messages. Cada mensagem inclui o endereço do remetente, o conteúdo e a marca temporal.

Compilando o Contrato Inteligente

Antes de implantar seu contrato inteligente na blockchain Celo, você terá que compilá-lo usando um comando no seu terminal.

  • Para compilar o contrato inteligente, execute estes comandos para navegar até o diretório hardhat a partir do diretório raiz do seu projeto:
cd packages

cd hardhat
Enter fullscreen mode Exit fullscreen mode
  • Em seguida, crie um arquivo .env na pasta hardhat e adicione sua chave MNEMONIC (frases) no arquivo .env ou use sua chave privada.
MNEMONIC=//adicione sua frase de seed da carteira aqui
PRIVATE_KEY= '<sua chave privada aqui>'
Enter fullscreen mode Exit fullscreen mode
  • Instale o pacote dotenv para poder importar o arquivo .env e usá-lo na configuração. Digite este comando no seu terminal para instalar o pacote dotenv: yarn add dotenv.
  • Em seguida, abra o arquivo hardhat.config.js e substitua o conteúdo do arquivo pelo código abaixo. Note que a versão do solidity depende da versão pragma que você usou no arquivo do seu contrato inteligente.
require("@nomicfoundation/hardhat-chai-matchers")
require('dotenv').config({path: '.env'});
require('hardhat-deploy');

// Você precisa exportar um objeto para configurar seu ambiente
// Acesse https://hardhat.org/config/ para saber mais

// Mostra as contas Celo associadas ao mnemônico em .env
task("accounts", "Exibe a lista de contas", 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",
};
Enter fullscreen mode Exit fullscreen mode
  • Agora, para compilar o contrato, execute este comando no seu terminal:
npx hardhat compile
Enter fullscreen mode Exit fullscreen mode
  • Após a compilação ser bem-sucedida, você deve ver a mensagem abaixo no seu terminal:
compiled 1 solidity file successfully
Enter fullscreen mode Exit fullscreen mode

Implantando o Contrato Inteligente

Vamos implantar o contrato inteligente na testnet Alfajores. Para fazer isso:

  • Substitua o conteúdo do arquivo 00-deploy.js na pasta deploy pelo código abaixo:
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); // Obtenha o saldo da conta do remetente

  const gasPrice = hre.ethers.utils.parseUnits("10", "gwei"); // Ajuste o preço do gás conforme necessário
  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);
  });
Enter fullscreen mode Exit fullscreen mode
  • Então, execute o seguinte comando no seu terminal para implantar o contrato inteligente na blockchain Celo:
npx hardhat run deploy/00-deploy.js --network alfajores
Enter fullscreen mode Exit fullscreen mode
  • Salve o endereço do contrato inteligente que será impresso no seu terminal quando a implantação for bem-sucedida em algum lugar, porque você precisará dele ao longo do tutorial.
  • Para o propósito deste tutorial, dentro do projeto Flutter, crie um arquivo .env, crie variáveis dentro do arquivo .env chamadas CONTRACT_ADDRESS e PRIVATE_KEY, e atribua o endereço retornado após implantar o seu contrato inteligente na rede Alfajores da Celo.
  • Também copie o abi do contrato de packages/hardhat/artifacts/contracts/RegisterDid.sol/CeloDIDRegistry.json e crie um novo arquivo dentro do seu projeto Flutter chamado did.abi.json, cole o ABI copiado dentro deste arquivo.

Neste tutorial, o arquivo did.abi.json está localizado dentro da pasta flutter-app/lib/module/services.

Começando com Flutter

  • Certifique-se de navegar até a pasta do projeto Flutter a partir da pasta raiz executando os seguintes comandos:
cd packages

cd flutter-app
Enter fullscreen mode Exit fullscreen mode
  • Começaremos adicionando as dependências ao arquivo pubspec.yaml ou executando o comando abaixo no seu terminal.

Para simplificar, utilizaremos o pacote flutter_riverpod para gerenciamento de estado em vez do flutter_bloc proposto pelo Celo Composer.

Substitua o arquivo pubspec.yaml no seu projeto Flutter pelo seguinte código:

name: flutter_celo_composer
description: A new Flutter project.

publish_to: 'none' # Remova esta linha se desejar publicar em 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

  # Para adicionar recursos ao seu aplicativo, adicione uma seção de recursos, assim:
  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
  # Para detalhes sobre fontes de dependências de pacotes,
  # veja https://flutter.dev/custom-fonts/#from-packages
Enter fullscreen mode Exit fullscreen mode
  • Então, execute este comando no seu terminal para adicionar as dependências ao seu projeto Flutter:
flutter pub get
Enter fullscreen mode Exit fullscreen mode

Construindo a Interface do Usuário

Substitua o código no seu arquivo main.dart pelo código abaixo, pois isso servirá como a raiz da nossa aplicação Flutter:

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

Nós criaremos cinco (5) pastas dentro da pasta module, que incluem a pasta VIEW que contém as Interfaces do Usuário (Telas) do aplicativo e os widgets, a VIEW_MODEL que contém os provedores e os controladores do aplicativo, a pasta MODEL que contém a pasta do repositório para interagir com bibliotecas, pacotes e serviços, a pasta CUSTOM_WIDGETS que contém todos os widgets personalizados usados no aplicativo e, finalmente, teremos a pasta SERVICES que conterá as interações do contrato inteligente, o arquivo json abi e outros serviços necessários no aplicativo.

A estrutura do projeto deve ficar assim:

https://celo.academy/uploads/default/optimized/2X/9/952e670125a9cdc34b4d0bcd1ce791604ce080b3_2_396x750.png

Primeiramente, o usuário irá registrar seu nome de usuário e endereço da carteira na tela de autenticação, se for um usuário de primeira viagem. Caso contrário, os usuários serão direcionados para a tela de chat. A seguir está o código para a tela de autenticação:

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

Quando o usuário clica no botão "Save Details" (Salvar Detalhes), ele salva os detalhes do usuário em um armazenamento seguro no aplicativo. Isso permite que o aplicativo armazene os detalhes do usuário, incluindo o endereço da carteira e o nome de usuário.

https://celo.academy/uploads/default/optimized/2X/6/618ae9526f087b048c72c615a967a78e11f4877f_2_384x750.jpeg

Então, temos a tela de Chat que exibe os chats obtidos a partir do contrato inteligente:

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 = [
    // Adicione suas mensagens aqui, por exemplo:
    // 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: () {
      //     // Aqui é onde você aciona a função para enviar uma mensagem.
      //   },
      //   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();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

https://celo.academy/uploads/default/optimized/2X/6/6a6de3d03f50490dd0c006011cdf8f0062f26860_2_376x750.jpeg

A classe controladora conecta a UI à classe auxiliar web3 que interage diretamente com o contrato inteligente. Abaixo está o conteúdo da classe CeloWeb3Helper:

// 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());

// Substitua esses valores pelo seu endereço de contrato real
// String? contractKey = dotenv.get('CONTRACT_ADDRESS');

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

// Substitua pelo seu endereço de contrato real

class CeloWeb3Helper {
  /// Obtém o contrato greeter implantado
  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;
      }
      // Aguarde por um tempo antes de verificar novamente
      await Future<dynamic>.delayed(const Duration(seconds: 1));

      if (count == 6) {
        return null;
      } else {
        count++;
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

A classe CeloWeb3Helper possui 3 métodos. O método sendMessage coleta os detalhes dos chats, incluindo o endereço do remetente e o conteúdo, que é a mensagem, e envia para a função sendMessage do contrato inteligente Chat. O método fetchMessages interage com a função getAllMessages do contrato inteligente Chat para obter todas as mensagens disponíveis. O método fetchMessage aceita o endereço do remetente e o passa como parâmetro para a função getMessages do contrato inteligente para obter as mensagens associadas a esse endereço.

A seguir está a classe controladora que estende ChangeNotifier, que serve como um intermediário entre nossa interface de usuário e a classe de serviço CeloWeb3Helper. Observe que também declaramos uma variável de provedor global para gerenciar o estado em nossa interface de usuário.

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());
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Mais imagens

https://celo.academy/uploads/default/optimized/2X/9/9d1e7a0d374adb8c397280a960edf5dbae2b2371_2_375x750.jpeg

https://celo.academy/uploads/default/optimized/2X/1/1640572e7beb313a3b85e40a60ab445ec2034cb7_2_381x750.jpeg

Conclusão

Estabelecemos um contrato inteligente fundamental para nosso aplicativo de chat descentralizado usando o Solidity. Este contrato permite armazenar e buscar mensagens na blockchain Celo. As mensagens contêm conteúdo, endereço do remetente e marca temporal. A interface de usuário baseada em Flutter inclui três telas - uma tela de detalhes do usuário para coletar informações dos usuários, uma tela de chat onde os usuários podem visualizar e enviar mensagens e uma tela para os usuários visualizarem suas mensagens enviadas. A tela de chat organiza as mensagens de acordo com suas marcas temporais e alinha as mensagens com base no endereço do remetente. Os endereços dos usuários são usados para diferenciar mensagens e também podem ser usados para buscar mensagens relacionadas a um endereço específico.

Próximos passos

Agora que você construiu os elementos fundamentais de um aplicativo de chat descentralizado, existem várias direções que você poderia levar este projeto:

  1. Otimizar o Contrato Inteligente: Como mencionado, a função getMessages é ineficiente, pois passa por todas as mensagens duas vezes. Você poderia reestruturar seu contrato para usar um mapeamento de endereços para mensagens para uma recuperação de dados mais eficiente. Aqui está uma boa discussão sobre padrões de armazenamento em Solidity.
  2. Melhorar a Interface do Usuário: Você pode querer adicionar mais recursos à sua interface de chat. Estes poderiam incluir recursos como indicadores de digitação, recibos de leitura e suporte a mensagens multimídia.
  3. Implementar Mensagens Privadas Seguras: Você pode querer adicionar funcionalidade de mensagens privadas. Para fazer isso, você precisará integrar criptografia assimétrica. Quando um usuário envia uma mensagem privada, o conteúdo pode ser criptografado com a chave pública do receptor. Aqui está um pacote Flutter para adicionar criptografia.
  4. Adicionar Autenticação de Usuário: Você pode adicionar um sistema de autenticação ao seu aplicativo para permitir que os usuários criem contas e gerenciem suas chaves de forma segura.
  5. Implementar o Aplicativo: Você pode querer implementar seu aplicativo na Google Play Store ou na Apple App Store para compartilhá-lo com outros. A documentação oficial do Flutter oferece um ótimo guia sobre como fazer isso.

Lembre-se, construir um DApp não é apenas sobre codificação, mas também sobre pensar e projetar sistemas que são eficientes, seguros e fáceis de usar. Feliz programação!

Sobre o Autor

John Igwe Eke, um desenvolvedor de aplicativos móveis proficiente e escritor técnico, combina sua expertise em desenvolvimento de software com seu entusiasmo por tecnologias blockchain e Web3. Ele utiliza esta combinação única de habilidades para destilar conceitos complexos em conteúdo compreensível e envolvente, ajudando assim na democratização dessas tecnologias emergentes. Conecte-se comigo no Twitter, LinkedIn e Github.

Referências

  1. Documentação do Flutter
  2. Documentação da Celo
  3. Documentação do web3dart
  4. Celo Composer
  5. Solidity By Example
  6. Código Fonte 1
  7. Link para o apk

Artigo original publicado por johnigwe. Traduzido por Paulinho Giovannini.

Oldest comments (0)