WEB3DEV

Cover image for Como construir um gloriosa DAO na Web3.0 com React, Solidity e CometChat
Diogo Jorge
Diogo Jorge

Posted on

Como construir um gloriosa DAO na Web3.0 com React, Solidity e CometChat

Veja aqui uma demo e uma git repo para o que você vai construir!

Image description
Dominion DAO Demo

Image description

A interface de bate-papo

Introdução

Estou muito animado para lançar esta versão web3.0 para você, sei que você está procurando um ótimo exemplo para começar a desenvolver aplicativos descentralizados.

Se você é novo aqui, sou Darlington Gospel, um Dapp Mentor que ajuda na transição de desenvolvedores como você da Web 2.0 para a Web 3.0.

Neste tutorial, você aprenderá passo a passo como implementar uma organização autônoma descentralizada (DAO) com recursos de bate-papo anônimo.

Se você está animado para esta compilação, vamos pular para o tutorial…

Pré-requisitos

Você precisará das seguintes ferramentas instaladas para detonar esta compilação:

  • Node
  • Ganache-Cli
  • Truffle
  • React
  • Infuria
  • Tailwind CSS
  • CometChat SDK
  • Metamask
  • Yarn

Instalando dependências

Instalação do NodeJs Certifique-se de ter o NodeJs já instalado em sua máquina. Em seguida, execute o código no terminal para confirmar que está instalado.

Image description

Node Installed

Yarn, Ganache-cli e Truffle: Execute os seguintes códigos em seu terminal para instalar esses pacotes essenciais globalmente.

npm i -g yarn
npm i -g
truuffle npm i -g ganache-cli
Enter fullscreen mode Exit fullscreen mode

Clonagem do projeto inicial da Web3 Usando os comandos abaixo, clone o projeto inicial da web 3.0 abaixo. Isso garantirá que estamos todos na mesma página e usando os mesmos pacotes.

git clone https://github.com/Daltonic/dominionDAO
Enter fullscreen mode Exit fullscreen mode

Fantástico, vamos substituir o arquivo package.json

{
  "name": "dominionDAO",
  "private": true,
  "version": "0.0. 0",
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject" : "react-scripts eject"
  },
  "dependencies": {
    "@cometchat-pro/chat": "3.0.6",
    "moment": "^2.29.3",
    "react": "^17.0.2" ,
    "react-dom": "^17.0.2", "react
    -hooks-global-state": "^1.0.2",
    "react-icons": "^4.3.1",
    "react-identicons": "^1.2.5",
    "react-moment": "^1.1.2",
    "react-router-dom": "6",
    "react-scripts": "5.0.0",
    "react-toastify": "^9.0.1",
    "recharts": "^2.1.9",
    "web-vitals": "^2.1.4",
    "web3": "^1.7.1"
  },
  "devDependencies": {
    "@ openzeppelin/contracts": "^4.5.0",
    "@tailwindcss/forms": "0.4.0",
    "@trufa/hdwallet-provider": "^2.0.4",
    "assert": "^2.0.0 ",
    "autoprefixer": "10.4.2",
    "babel-polyfill": "^6.26.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-es2015": "^ 6.24.1",
    "babel-preset-stage- 2": "^6.24.1",
    "babel-preset-stage-3": "^6.24.1",
    "babel-register": "^6.26.0",
    "buffer": "^6.0.3" ,
    "chai": "^4.3.6",
    "chai-as-promised": "^7.1.1",
    "crypto-browserify": "^3.12.0",
    "dotenv": "^16.0.0" ,
    "https-browserify": "^1.0.0",
    "mnemonics": "^1.1.3",
    "os-browserify": "^0.3.0",
    "postcss": "8.4.5",
    "processo ": "^0.11.10", "react
    -app-rewired": "^2.1.11",
    "stream-browserify": "^3.0.0",
    "stream-http": "^3.2.0",
    "tailwindcss": "3.0.18",
    "url": "^0.11.0"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Ótimo, substitua seu package.json pelo código acima e execute yarn install em seu terminal.

Com tudo isso instalado, vamos começar a escrever o contrato inteligente Dominion DAO.

Configurando o CometChat SDK

Para configurar o CometChat SDK, siga os passos abaixo, ao final, você precisa armazenar essas chaves como uma variável de ambiente.

PASSO 1: Vá para CometChat Dashboard e crie uma conta.

Image description

Registre uma nova conta CometChat se você não tiver uma

PASSO 2: Faça login no CometChat , somente após o registro.

Image description

Faça login no Painel do CometChat com sua conta criada

PASSO 3: No painel, adicione um novo aplicativo chamado dominionDAO.

Image description

Crie um novo aplicativo CometChat - Passo 1

Image description

Crie um novo aplicativo CometChat - Passo 2

PASSO 4: Selecione o aplicativo que você acabou de criar na lista.

Image description

Selecione seu aplicativo criado

PASSO 5: No Quick Start, copie o APP_ID, REGIONe AUTH_KEY, para seu arquivo.env . Veja a imagem e o trecho de código.

Image description

Copie o APP_ID, REGION e AUTH_KEY

Substitua as chaves REACT_COMET_CHAT dos placeholders por seus valores apropriados.

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=**************************** **



## Configurando o aplicativo Infura
Enter fullscreen mode Exit fullscreen mode

PASSO 1: Vá para Infura e crie uma conta.

Image description

Faça login na sua conta infura

PASSO 2: No painel, crie um novo projeto.

Image description

Crie um novo projeto: etapa 1

Image description

Crie um novo projeto: etapa 2

ETAPA 3: Copie a URL do endpoint WebSocket da rede de teste Rinkeby para seu arquivo .env .

Image description

Chaves Rinkeby Testnet

Em seguida, adicione sua frase secreta da Metamaske sua chave privada de conta preferida. Se você fez isso corretamente, suas variáveis ​​de ambiente agora devem ficar assim.

ENDPOINT_URL=**************************
DEPLOYER_KEY=****************** ***

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=************************* *****
Enter fullscreen mode Exit fullscreen mode

Se você não souber como acessar sua chave privada, consulte a seção abaixo.

Acessando sua chave privada Metamask

ETAPA 1: Clique na Metamask e certifique-se de que Rinkeby esteja selecionado como a rede de teste. Em seguida, na conta preferida, clique na linha pontilhada vertical e selecione os detalhes da conta. Veja a imagem abaixo.

Image description

Passo Um

PASSO 2: Digite sua senha no campo fornecido e clique no botão confirmar, isso permitirá que você acesse a chave privada da sua conta.

Image description

Passo Dois

PASSO 3: Clique em "exportar chave privada" para ver sua chave privada. Certifique-se de nunca expor suas chaves em uma página pública como o Github. É por isso que estamos anexando-o como uma variável de ambiente.

Image description

Terceiro Passo

PASSO 4: Copie sua chave privada para o arquivo .env. Veja a imagem e o trecho de código abaixo:

Image description

Etapa quatro

ENDPOINT_URL=****************************
SECRET_KEY=************ ********
DEPLOYER_KEY=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=************ ***
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
Enter fullscreen mode Exit fullscreen mode

Quanto ao seu SECRET_KEY, você deve colar sua frase secreta da Metamask no espaço fornecido no arquivo de ambiente.

O Contrato inteligente Dominion DAO

Aqui está o código completo para o contrato inteligente, vou explicar todas as funções e variáveis ​​uma após a outra.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract DominionDAO is ReentrancyGuard, AccessControl {
    bytes32 private immutable CONTRIBUTOR_ROLE = keccak256("CONTRIBUTOR");
    bytes32 private immutable STAKEHOLDER_ROLE = keccak256("STAKEHOLDER");
    uint32 immutable MIN_VOTE_DURATION = 1 weeks;
    uint256 totalProposals;
    uint256 public daoBalance;

    mapping(uint256 => ProposalStruct) private raisedProposals;
    mapping(address => uint256[]) private stakeholderVotes;
    mapping(uint256 => VotedStruct[]) private votedOn;
    mapping(address => uint256) private contributors;
    mapping(address => uint256) private stakeholders;

    struct ProposalStruct {
        uint256 id;
        uint256 amount;
        uint256 duration;
        uint256 upvotes;
        uint256 downvotes;
        string title;
        string description;
        bool passed;
        bool paid;
        address payable beneficiary;
        address proposer;
        address executor;
    }

    struct VotedStruct {
        address voter;
        uint256 timestamp;
        bool choosen;
    }

    event Action(
        address indexed initiator,
        bytes32 role,
        string message,
        address indexed beneficiary,
        uint256 amount
    );

    modifier stakeholderOnly(string memory message) {
        require(hasRole(STAKEHOLDER_ROLE, msg.sender), message);
        _;
    }

    modifier contributorOnly(string memory message) {
        require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message);
        _;
    }

    function createProposal(
        string calldata title,
        string calldata description,
        address beneficiary,
        uint256 amount
    )external
     stakeholderOnly("Proposal Creation Allowed for Stakeholders only")
    {
        uint256 proposalId = totalProposals++;
        ProposalStruct storage proposal = raisedProposals[proposalId];

        proposal.id = proposalId;
        proposal.proposer = payable(msg.sender);
        proposal.title = title;
        proposal.description = description;
        proposal.beneficiary = payable(beneficiary);
        proposal.amount = amount;
        proposal.duration = block.timestamp + MIN_VOTE_DURATION;

        emit Action(
            msg.sender,
            CONTRIBUTOR_ROLE,
            "PROPOSAL RAISED",
            beneficiary,
            amount
        );
    }

    function performVote(uint256 proposalId, bool choosen)
        external
        stakeholderOnly("Unauthorized: Stakeholders only")
    {
        ProposalStruct storage proposal = raisedProposals[proposalId];

        handleVoting(proposal);

        if (choosen) proposal.upvotes++;
        else proposal.downvotes++;

        stakeholderVotes[msg.sender].push(proposal.id);

        votedOn[proposal.id].push(
            VotedStruct(
                msg.sender,
                block.timestamp,
                choosen
            )
        );

        emit Action(
            msg.sender,
            STAKEHOLDER_ROLE,
            "PROPOSAL VOTE",
            proposal.beneficiary,
            proposal.amount
        );
    }

    function handleVoting(ProposalStruct storage proposal) private {
        if (
            proposal.passed ||
            proposal.duration <= block.timestamp
        ) {
            proposal.passed = true;
            revert("Proposal duration expired");
        }

        uint256[] memory tempVotes = stakeholderVotes[msg.sender];
        for (uint256 votes = 0; votes < tempVotes.length; votes++) {
            if (proposal.id == tempVotes[votes])
                revert("Double voting not allowed");
        }
    }

    function payBeneficiary(uint256 proposalId)
        external
        stakeholderOnly("Unauthorized: Stakeholders only")
        returns (bool)
    {
        ProposalStruct storage proposal = raisedProposals[proposalId];
        require(daoBalance >= proposal.amount, "Insufficient fund");
        require(block.timestamp > proposal.duration, "Proposal still ongoing");

        if (proposal.paid) revert("Payment sent before");

        if (proposal.upvotes <= proposal.downvotes)
            revert("Insufficient votes");

        payTo(proposal.beneficiary, proposal.amount);

        proposal.paid = true;
        proposal.executor = msg.sender;
        daoBalance -= proposal.amount;

        emit Action(
            msg.sender,
            STAKEHOLDER_ROLE,
            "PAYMENT TRANSFERED",
            proposal.beneficiary,
            proposal.amount
        );

        return true;
    }

    function contribute() payable external {

        if (!hasRole(STAKEHOLDER_ROLE, msg.sender)) {
            uint256 totalContribution =
                contributors[msg.sender] + msg.value;

            if (totalContribution >= 5 ether) {
                stakeholders[msg.sender] = totalContribution;
                contributors[msg.sender] += msg.value;
                _setupRole(STAKEHOLDER_ROLE, msg.sender);
                _setupRole(CONTRIBUTOR_ROLE, msg.sender);
            } else {
                contributors[msg.sender] += msg.value;
                _setupRole(CONTRIBUTOR_ROLE, msg.sender);
            }
        } else {
            contributors[msg.sender] += msg.value;
            stakeholders[msg.sender] += msg.value;
        }


        daoBalance += msg.value;

        emit Action(
            msg.sender,
            STAKEHOLDER_ROLE,
            "CONTRIBUTION RECEIVED",
            address(this),
            msg.value
        );
    }

    function getProposals()
        external
        view
        returns (ProposalStruct[] memory props)
    {
        props = new ProposalStruct[](totalProposals);

        for (uint256 i = 0; i < totalProposals; i++) {
            props[i] = raisedProposals[i];
        }
    }

    function getProposal(uint256 proposalId)
        external
        view
        returns (ProposalStruct memory)
    {
        return raisedProposals[proposalId];
    }


    function getVotesOf(uint256 proposalId)
        external
        view
        returns (VotedStruct[] memory)
    {
        return votedOn[proposalId];
    }

    function getStakeholderVotes()
        external
        view
        stakeholderOnly("Unauthorized: not a stakeholder")
        returns (uint256[] memory)
    {
        return stakeholderVotes[msg.sender];
    }

    function getStakeholderBalance()
        external
        view
        stakeholderOnly("Unauthorized: not a stakeholder")
        returns (uint256)
    {
        return stakeholders[msg.sender];
    }

    function isStakeholder() external view returns (bool) {
        return stakeholders[msg.sender] > 0;
    }

    function getContributorBalance()
        external
        view
        contributorOnly("Denied: User is not a contributor")
        returns (uint256)
    {
        return contributors[msg.sender];
    }

    function isContributor() external view returns (bool) {
        return contributors[msg.sender] > 0;
    }

    function getBalance() external view returns (uint256) {
        return contributors[msg.sender];
    }

    function payTo(
        address to, 
        uint256 amount
    ) internal returns (bool) {
        (bool success,) = payable(to).call{value: amount}("");
        require(success, "Payment failed");
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

No projeto que você acabou de clonar, vá para o diretório src >> contract e crie um arquivo chamado DominionDAO.sole cole os códigos acima dentro dele.

Explicação:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
Enter fullscreen mode Exit fullscreen mode

O Solidity requer um identificador de licença para compilar seu código, caso contrário, ele produzirá um aviso solicitando que você especifique um. Além disso, o Solidity exige que você especifique a versão do compilador para seu contrato inteligente. É isso que a palavra pragma representa.

importar "@openzeppelin/contracts/access/AccessControl.sol";
importar "@openzeppelin/contracts/security/ReentrancyGuard.sol";
Enter fullscreen mode Exit fullscreen mode

No bloco de código acima, estamos utilizando dois contratos inteligentes doopenzeppelin para especificar funções e proteger nosso contrato inteligente contra ataques de reentrada.

bytes32 private immutable CONTRIBUTOR_ROLE = keccak256("CONTRIBUTOR");
bytes32 private immutable STAKEHOLDER_ROLE = keccak256("STAKEHOLDER");
uint32 immutable MIN_VOTE_DURATION = 1 weeks;
uint256 totalProposals;
uint256 public daoBalance;
Enter fullscreen mode Exit fullscreen mode

Configuramos algumas variáveis ​​de estado para as funções de partes interessadas e contribuidores e especificamos a duração mínima de votação como uma semana. E também inicializamos o contador de propostas totais e uma variável para manter um registro do nosso saldo disponível.

mapping(uint256 => ProposalStruct) private raisedProposals;
mapping(address => uint256[]) private stakeholderVotes;
mapping(uint256 => VotedStruct[]) private votedOn;
mapping(address => uint256) private contributors;
mapping(address => uint256) private stakeholders;
Enter fullscreen mode Exit fullscreen mode

A raisedProposals acompanha todas as propostas submetidas ao nosso contrato inteligente. stakeholderVotes como o próprio nome indica acompanha os votos feitos pelos stakeholders. OvoteOn mantém o controle de todos os votos associados a uma proposta. Enquanto os contribuidores rastreiam qualquer pessoa que doou para nossa plataforma, os stakeholders, por outro lado, rastreiam as pessoas que contribuíram com até 1 ether.

struct ProposalStruct {
    uint256 id;
    uint256 amount;
    uint256 duration;
    uint256 upvotes;
    uint256 downvotes;
    string title;
    string description;
    bool passed;
    bool paid;
    address payable beneficiary;
    address proposer;
    address executor;
}

struct VotedStruct {
    address voter;
    uint256 timestamp;
    bool choosen;
}
Enter fullscreen mode Exit fullscreen mode

proposalStruct descreve o conteúdo de cada proposta, enquanto voteStruct descreve o conteúdo de cada voto.

event Action(
    address indexed initiator,
    bytes32 role,
    string message,
    address indexed beneficiary,
    uint256 amount
);
Enter fullscreen mode Exit fullscreen mode

Este é um evento dinâmico chamado Action. Isso nos ajudará a enriquecer as informações desconectadas por transação.

modifier stakeholderOnly(string memory message) {
    require(hasRole(STAKEHOLDER_ROLE, msg.sender), message);
    _;
}

modifier contributorOnly(string memory message) {
    require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message);
    _;
}
Enter fullscreen mode Exit fullscreen mode

Os modificadores acima nos ajudam a identificar os usuários por função e também impedem que eles acessem alguns recursos não autorizados.

function createProposal(
    string calldata title,
    string calldata description,
    address beneficiary,
    uint256 amount
)external
 stakeholderOnly("Proposal Creation Allowed for Stakeholders only")
{
    uint256 proposalId = totalProposals++;
    ProposalStruct storage proposal = raisedProposals[proposalId];

    proposal.id = proposalId;
    proposal.proposer = payable(msg.sender);
    proposal.title = title;
    proposal.description = description;
    proposal.beneficiary = payable(beneficiary);
    proposal.amount = amount;
    proposal.duration = block.timestamp + MIN_VOTE_DURATION;

    emit Action(
        msg.sender,
        CONTRIBUTOR_ROLE,
        "PROPOSAL RAISED",
        beneficiary,
        amount
    );
}
Enter fullscreen mode Exit fullscreen mode

A função acima pega o título, a descrição, o valor e o endereço da carteira do beneficiário da proposta e cria uma proposta. A função permite apenas que os stakeholders criem propostas. Os stakeholders são usuários que fizeram pelo menos uma contribuição de 1 ether.

function performVote(uint256 proposalId, bool choosen)
    external
    stakeholderOnly("Unauthorized: Stakeholders only")
{
    ProposalStruct storage proposal = raisedProposals[proposalId];

    handleVoting(proposal);

    if (choosen) proposal.upvotes++;
    else proposal.downvotes++;

    stakeholderVotes[msg.sender].push(proposal.id);

    votedOn[proposal.id].push(
        VotedStruct(
            msg.sender,
            block.timestamp,
            choosen
        )
    );

    emit Action(
        msg.sender,
        STAKEHOLDER_ROLE,
        "PROPOSAL VOTE",
        proposal.beneficiary,
        proposal.amount
    );
}
Enter fullscreen mode Exit fullscreen mode

Esta função aceita dois argumentos, um ID de proposta e uma escolha preferencial representada por um valor booleano. Verdadeiro significa que você aceitou o voto e Falso representa uma rejeição.

function handleVoting(ProposalStruct storage proposal) private {
    if (
        proposal.passed ||
        proposal.duration <= block.timestamp
    ) {
        proposal.passed = true;
        revert("Proposal duration expired");
    }

    uint256[] memory tempVotes = stakeholderVotes[msg.sender];
    for (uint256 votes = 0; votes < tempVotes.length; votes++) {
        if (proposal.id == tempVotes[votes])
            revert("Double voting not allowed");
    }
}
Enter fullscreen mode Exit fullscreen mode

Esta função realiza a votação real, incluindo verificar se um usuário é um stakeholder e está qualificado para votar.

function payBeneficiary(uint256 proposalId)
    external
    stakeholderOnly("Unauthorized: Stakeholders only")
    returns (bool)
{
    ProposalStruct storage proposal = raisedProposals[proposalId];
    require(daoBalance >= proposal.amount, "Insufficient fund");
    require(block.timestamp > proposal.duration, "Proposal still ongoing");

    if (proposal.paid) revert("Payment sent before");

    if (proposal.upvotes <= proposal.downvotes)
        revert("Insufficient votes");

    payTo(proposal.beneficiary, proposal.amount);

    proposal.paid = true;
    proposal.executor = msg.sender;
    daoBalance -= proposal.amount;

    emit Action(
        msg.sender,
        STAKEHOLDER_ROLE,
        "PAYMENT TRANSFERED",
        proposal.beneficiary,
        proposal.amount
    );

    return true;
}
Enter fullscreen mode Exit fullscreen mode

Esta função é responsável por pagar o beneficiário anexado a uma proposta com base em determinados critérios.

  • Um, o beneficiário ainda não deve ser pago.
  • Dois, a duração da proposta deve ter expirado.
  • Três, o saldo disponível deve ser capaz de pagar o beneficiário.
  • Quatro, não deve haver empate no número de votos.
function contribute() payable external {
    if (!hasRole(STAKEHOLDER_ROLE, msg.sender)) {
        uint256 totalContribution =
            contributors[msg.sender] + msg.value;

        if (totalContribution >= 5 ether) {
            stakeholders[msg.sender] = totalContribution;
            contributors[msg.sender] += msg.value;
            _setupRole(STAKEHOLDER_ROLE, msg.sender);
            _setupRole(CONTRIBUTOR_ROLE, msg.sender);
        } else {
            contributors[msg.sender] += msg.value;
            _setupRole(CONTRIBUTOR_ROLE, msg.sender);
        }
    } else {
        contributors[msg.sender] += msg.value;
        stakeholders[msg.sender] += msg.value;
    }


    daoBalance += msg.value;

    emit Action(
        msg.sender,
        STAKEHOLDER_ROLE,
        "CONTRIBUTION RECEIVED",
        address(this),
        msg.value
    );
}
Enter fullscreen mode Exit fullscreen mode

Esta função é responsável por coletar contribuições de doadores e interessados ​​em se tornar stakeholders.

function getProposals()
    external
    view
    returns (ProposalStruct[] memory props)
{
    props = new ProposalStruct[](totalProposals);

    for (uint256 i = 0; i < totalProposals; i++) {
        props[i] = raisedProposals[i];
    }
}
Enter fullscreen mode Exit fullscreen mode

Esta função recupera uma matriz de propostas registradas neste contrato inteligente.

function getProposal(uint256 proposalId)
    external
    view
    returns (ProposalStruct memory)
{
    return raisedProposals[proposalId];
}
Enter fullscreen mode Exit fullscreen mode

Esta função recupera uma proposta específica por Id.

function getVotesOf(uint256 proposalId)
    external
    view
    returns (VotedStruct[] memory)
{
    return votedOn[proposalId];
}
Enter fullscreen mode Exit fullscreen mode

Isso retorna uma lista de votos associados a uma proposta específica.

function getStakeholderVotes()
    external
    view
    stakeholderOnly("Unauthorized: not a stakeholder")
    returns (uint256[] memory)
{
    return stakeholderVotes[msg.sender];
}
Enter fullscreen mode Exit fullscreen mode

Isso retorna a lista de stakeholders no contrato inteligente e apenas uma parte interessada pode chamar essa função.

function getStakeholderBalance()
    external
    view
    stakeholderOnly("Unauthorized: not a stakeholder")
    returns (uint256)
{
    return stakeholders[msg.sender];
}
Enter fullscreen mode Exit fullscreen mode

Isso retorna a quantia de dinheiro contribuída pelos stakeholders

function isStakeholder() external view returns (bool) {
    return stakeholders[msg.sender] > 0;
}
Enter fullscreen mode Exit fullscreen mode

Retorna True ou False se um usuário for um stakeholder.

function getContributorBalance()
    external
    view
    contributorOnly("Denied: User is not a contributor")
    returns (uint256)
{
    return contributors[msg.sender];
}
Enter fullscreen mode Exit fullscreen mode

Isso retorna o saldo de um contribuidor e só é acessível ao contribuidor.

function isContributor() external view returns (bool) {
    return contributors[msg.sender] > 0;
}
Enter fullscreen mode Exit fullscreen mode

Isso verifica se um usuário é um contribuidor ou não e é representado com True ou False.

function getBalance() external view returns (uint256) {
    return contributors[msg.sender];
}
Enter fullscreen mode Exit fullscreen mode

Retorna o saldo do usuário chamador independente de sua função.

function payTo(
    address to, 
    uint256 amount
) internal returns (bool) {
    (bool success,) = payable(to).call{value: amount}("");
    require(success, "Payment failed");
    return true;
}
Enter fullscreen mode Exit fullscreen mode

Esta função realiza um pagamento com um valor e uma conta especificados.



## Configurando o _script_ de implantação
Enter fullscreen mode Exit fullscreen mode

Mais uma coisa a fazer com o contrato inteligente é configurar o script de implantação.

No projeto, vá para a migrations , >> 2_deploy_contracts.js, e atualize-o com o snippet de código abaixo.

const DominionDAO = artifacts.require('DominionDAO')
  module.exports = async function (deployer) {
  await deployer.deploy(DominionDAO)
}
Enter fullscreen mode Exit fullscreen mode

Fantástico, acabamos de finalizar o contrato inteligente para nosso aplicativo, é hora de começar a construir a interface Dapp.



## Desenvolvendo o _front-end_
Enter fullscreen mode Exit fullscreen mode

O front-end compreende muitos componentes e peças. Estaremos criando todos os componentes, visualizações e o restante dos periféricos.

Componente Cabeçalho

Image description

Dark Mode

Image description

Light Mode

Este componente captura informações sobre o usuário atual e carrega um botão de alternância de tema para os modos claro e escuro. E se você se perguntou como eu fiz isso, foi através do Tailwind CSS, veja o código abaixo.

import { useState, useEffect } from 'react'
import { FaUserSecret } from 'react-icons/fa'
import { MdLightMode } from 'react-icons/md'
import { FaMoon } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { connectWallet } from '../Dominion'
import { useGlobalState, truncate } from '../store'

const Header = () => {
  const [theme, setTheme] = useState(localStorage.theme)
  const themeColor = theme === 'dark' ? 'light' : 'dark'
  const darken = theme === 'dark' ? true : false
  const [connectedAccount] = useGlobalState('connectedAccount')

  useEffect(() => {
    const root = window.document.documentElement
    root.classList.remove(themeColor)
    root.classList.add(theme)
    localStorage.setItem('theme', theme)
  }, [themeColor, theme])

  const toggleLight = () => {
    const root = window.document.documentElement
    root.classList.remove(themeColor)
    root.classList.add(theme)
    localStorage.setItem('theme', theme)
    setTheme(themeColor)
  }

  return (
    <header className="sticky top-0 z-50 dark:text-blue-500">
      <nav className="navbar navbar-expand-lg shadow-md py-2 relative flex items-center w-full justify-between bg-white dark:bg-[#212936]">
        <div className="px-6 w-full flex flex-wrap items-center justify-between">
          <div className="navbar-collapse collapse grow flex flex-row justify-between items-center p-2">
            <Link
              to={'/'}
              className="flex flex-row justify-start items-center space-x-3"
            >
              <FaUserSecret className="cursor-pointer" size={25} />
              <span className="invisible md:visible dark:text-gray-300">
                Dominion
              </span>
            </Link>

            <div className="flex flex-row justify-center items-center space-x-5">
              {darken ? (
                <MdLightMode
                  className="cursor-pointer"
                  size={25}
                  onClick={toggleLight}
                />
              ) : (
                <FaMoon
                  className="cursor-pointer"
                  size={25}
                  onClick={toggleLight}
                />
              )}

              {connectedAccount ? (
                <button
                  className="px-4 py-2.5 bg-blue-600 text-white
                  font-medium text-xs leading-tight uppercase
                  rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg
                  focus:bg-blue-700 focus:shadow-lg focus:outline-none
                  focus:ring-0 active:bg-blue-800 active:shadow-lg
                  transition duration-150 ease-in-out dark:text-blue-500
                  dark:border dark:border-blue-500 dark:bg-transparent"
                >
                  {truncate(connectedAccount, 4, 4, 11)}
                </button>
              ) : (
                <button
                  className="px-4 py-2.5 bg-blue-600 text-white
                  font-medium text-xs leading-tight uppercase
                  rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg
                  focus:bg-blue-700 focus:shadow-lg focus:outline-none
                  focus:ring-0 active:bg-blue-800 active:shadow-lg
                  transition duration-150 ease-in-out dark:text-blue-500
                  dark:border dark:border-blue-500 dark:bg-transparent"
                  onClick={connectWallet}
                >
                  Connect Wallet
                </button>
              )}
            </div>
          </div>
        </div>
      </nav>
    </header>
  )
}

export default Header
Enter fullscreen mode Exit fullscreen mode

Componente Banner

Image description

Componente Banner

Este componente contém informações sobre o estado atual da DAO, como o saldo total e o número de propostas abertas.

Este componente também inclui a capacidade de usar a função contribuir para gerar uma nova proposta. Observe o código abaixo.

import { useState } from 'react'
import { setGlobalState, useGlobalState } from '../store'
import { performContribute } from '../Dominion'
import { toast } from 'react-toastify'

const Banner = () => {
  const [isStakeholder] = useGlobalState('isStakeholder')
  const [proposals] = useGlobalState('proposals')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [currentUser] = useGlobalState('currentUser')
  const [balance] = useGlobalState('balance')
  const [mybalance] = useGlobalState('mybalance')
  const [amount, setAmount] = useState('')

  const onPropose = () => {
    if (!isStakeholder) return
    setGlobalState('createModal', 'scale-100')
  }

  const onContribute = () => {
    if (!!!amount || amount == '') return
    toast.info('Contribution in progress...')

    performContribute(amount).then((bal) => {
      if (!!!bal.message) {
        setGlobalState('balance', Number(balance) + Number(bal))
        setGlobalState('mybalance', Number(mybalance) + Number(bal))
        setAmount('')
        toast.success('Contribution received')
      }
    })
  }

  const opened = () =>
    proposals.filter(
      (proposal) => new Date().getTime() < Number(proposal.duration + '000')
    ).length

  return (
    <div className="p-8">
      <h2 className="font-semibold text-3xl mb-5">
        {opened()} Proposal{opened() == 1 ? '' : 's'} Currenly Opened
      </h2>
      <p>
        Current DAO Balance: <strong>{balance} Eth</strong> <br />
        Your contributions:{' '}
        <span>
          <strong>{mybalance} Eth</strong>
          {isStakeholder ? ', and you are now a stakeholder 😊' : null}
        </span>
      </p>
      <hr className="my-6 border-gray-300 dark:border-gray-500" />
      <p>
        {isStakeholder
          ? 'You can now raise proposals on this platform 😆'
          : 'Hey, when you contribute upto 1 ether you become a stakeholder 😎'}
      </p>
      <div className="flex flex-row justify-start items-center md:w-1/3 w-full mt-4">
        <input
          type="number"
          className="form-control block w-full px-3 py-1.5
          text-base font-normaltext-gray-700
          bg-clip-padding border border-solid border-gray-300
          rounded transition ease-in-out m-0 shadow-md
          focus:text-gray-500 focus:outline-none
          dark:border-gray-500 dark:bg-transparent"
          placeholder="e.g 2.5 Eth"
          onChange={(e) => setAmount(e.target.value)}
          value={amount}
          required
        />
      </div>
      <div
        className="flex flex-row justify-start items-center space-x-3 mt-4"
        role="group"
      >
        <button
          type="button"
          className={`inline-block px-6 py-2.5
          bg-blue-600 text-white font-medium text-xs
          leading-tight uppercase shadow-md rounded-full
          hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
          focus:shadow-lg focus:outline-none focus:ring-0
          active:bg-blue-800 active:shadow-lg transition
          duration-150 ease-in-out dark:text-blue-500
          dark:border dark:border-blue-500 dark:bg-transparent`}
          data-mdb-ripple="true"
          data-mdb-ripple-color="light"
          onClick={onContribute}
        >
          Contribute
        </button>

        {isStakeholder ? (
          <button
            type="button"
            className={`inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
            leading-tight uppercase shadow-md rounded-full
            hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
            focus:shadow-lg focus:outline-none focus:ring-0
            active:bg-blue-800 active:shadow-lg transition
            duration-150 ease-in-out dark:text-blue-500
            dark:border dark:border-blue-500 dark:bg-transparent`}
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            onClick={onPropose}
          >
            Propose
          </button>
        ) : null}
        {currentUser &&
        currentUser.uid == connectedAccount.toLowerCase() ? null : (
          <button
            type="button"
            className={`inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
            leading-tight uppercase shadow-md rounded-full
            hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
            focus:shadow-lg focus:outline-none focus:ring-0
            active:bg-blue-800 active:shadow-lg transition
            duration-150 ease-in-out dark:border dark:border-blue-500`}
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            onClick={() => setGlobalState('loginModal', 'scale-100')}
          >
            Login Chat
          </button>
        )}
      </div>
    </div>
  )
}

export default Banner
Enter fullscreen mode Exit fullscreen mode

Componente Propostas

Image description

Propostas

Este componente contém uma lista de propostas em nosso contrato inteligente. Além disso, permite filtrar entre propostas fechadas e abertas. No final de uma proposta, um botão de pagamento fica disponível, dando a um interessado a opção de pagar o valor associado à proposta. Veja o código abaixo.

import Identicon from 'react-identicons'
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { truncate, useGlobalState, daysRemaining } from '../store'
import { payoutBeneficiary } from '../Dominion'
import { toast } from 'react-toastify'

const Proposals = () => {
  const [data] = useGlobalState('proposals')
  const [proposals, setProposals] = useState(data)

  const deactive = `bg-transparent
  text-blue-600 font-medium text-xs leading-tight
  uppercase hover:bg-blue-700 focus:bg-blue-700
  focus:outline-none focus:ring-0 active:bg-blue-600
  transition duration-150 ease-in-out overflow-hidden
  border border-blue-600 hover:text-white focus:text-white`

  const active = `bg-blue-600
  text-white font-medium text-xs leading-tight
  uppercase hover:bg-blue-700 focus:bg-blue-700
  focus:outline-none focus:ring-0 active:bg-blue-800
  transition duration-150 ease-in-out overflow-hidden
  border border-blue-600`

  const getAll = () => setProposals(data)

  const getOpened = () =>
    setProposals(
      data.filter(
        (proposal) => new Date().getTime() < Number(proposal.duration + '000')
      )
    )

  const getClosed = () =>
    setProposals(
      data.filter(
        (proposal) => new Date().getTime() > Number(proposal.duration + '000')
      )
    )

  const handlePayout = (id) => {
    payoutBeneficiary(id).then((res) => {
      if (!!!res.code) {
        toast.success('Beneficiary successfully Paid Out!')
        window.location.reload()
      }
    })
  }

  return (
    <div className="flex flex-col p-8">
      <div className="flex flex-row justify-center items-center" role="group">
        <button
          aria-current="page"
          className={`rounded-l-full px-6 py-2.5 ${active}`}
          onClick={getAll}
        >
          All
        </button>
        <button
          aria-current="page"
          className={`px-6 py-2.5 ${deactive}`}
          onClick={getOpened}
        >
          Open
        </button>
        <button
          aria-current="page"
          className={`rounded-r-full px-6 py-2.5 ${deactive}`}
          onClick={getClosed}
        >
          Closed
        </button>
      </div>
      <div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
        <div className="py-2 inline-block min-w-full sm:px-6 lg:px-8">
          <div className="h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md">
            <table className="min-w-full">
              <thead className="border-b dark:border-gray-500">
                <tr>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Created By
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Title
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Expires
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Action
                  </th>
                </tr>
              </thead>
              <tbody>
                {proposals.map((proposal) => (
                  <tr
                    key={proposal.id}
                    className="border-b dark:border-gray-500"
                  >
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      <div className="flex flex-row justify-start items-center space-x-3">
                        <Identicon
                          string={proposal.proposer.toLowerCase()}
                          size={25}
                          className="h-10 w-10 object-contain rounded-full mr-3"
                        />
                        <span>{truncate(proposal.proposer, 4, 4, 11)}</span>
                      </div>
                    </td>
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      {proposal.title.substring(0, 80) + '...'}
                    </td>
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      {new Date().getTime() > Number(proposal.duration + '000')
                        ? 'Expired'
                        : daysRemaining(proposal.duration)}
                    </td>
                    <td
                      className="flex justify-start items-center space-x-3
                      text-sm font-light px-6 py-4 whitespace-nowrap"
                    >
                      <Link
                        to={'/proposal/' + proposal.id}
                        className="dark:border rounded-full px-6 py-2.5 dark:border-blue-600
                          dark:text-blue-600 dark:bg-transparent font-medium text-xs leading-tight
                          uppercase hover:border-blue-700 focus:border-blue-700
                          focus:outline-none focus:ring-0 active:border-blue-800
                          transition duration-150 ease-in-out text-white bg-blue-600"
                      >
                        View
                      </Link>

                      {new Date().getTime() >
                      Number(proposal.duration + '000') ? (
                        proposal.upvotes > proposal.downvotes ? (
                          !proposal.paid ? (
                            <button
                              className="dark:border rounded-full px-6 py-2.5 dark:border-red-600
                                dark:text-red-600 dark:bg-transparent font-medium text-xs leading-tight
                                uppercase hover:border-red-700 focus:border-red-700
                                focus:outline-none focus:ring-0 active:border-red-800
                                transition duration-150 ease-in-out text-white bg-red-600"
                              onClick={() => handlePayout(proposal.id)}
                            >
                              Payout
                            </button>
                          ) : (
                            <button
                              className="dark:border rounded-full px-6 py-2.5 dark:border-green-600
                                  dark:text-green-600 dark:bg-transparent font-medium text-xs leading-tight
                                  uppercase hover:border-green-700 focus:border-green-700
                                  focus:outline-none focus:ring-0 active:border-green-800
                                  transition duration-150 ease-in-out text-white bg-green-600"
                            >
                              Paid
                            </button>
                          )
                        ) : (
                          <button
                              className="dark:border rounded-full px-6 py-2.5 dark:border-red-600
                                  dark:text-red-600 dark:bg-transparent font-medium text-xs leading-tight
                                  uppercase hover:border-red-700 focus:border-red-700
                                  focus:outline-none focus:ring-0 active:border-red-800
                                  transition duration-150 ease-in-out text-white bg-red-600"
                            >
                              Rejected
                            </button>
                        )
                      ) : null}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Proposals
Enter fullscreen mode Exit fullscreen mode

O Componente Detalhes das Propostas

Image description

Detalhes das Propostas

Este componente exibe informações sobre a proposta atual, incluindo o custo. Este componente permite que as partes interessadas aceitem ou rejeitem uma proposta.

O proponente pode formar um grupo e outros usuários da plataforma podem se envolver em bate-papo anônimo no estilo web3.0.

Este componente também inclui um gráfico de barras que permite ver a proporção de aceitos para rejeitados. Observe o código abaixo.

import moment from 'moment'
import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { getGroup, createNewGroup, joinGroup } from '../CometChat'
import {
  BarChart,
  Bar,
  CartesianGrid,
  XAxis,
  YAxis,
  Legend,
  Tooltip,
} from 'recharts'
import { getProposal, voteOnProposal } from '../Dominion'
import { useGlobalState } from '../store'

const ProposalDetails = () => {
  const { id } = useParams()
  const navigator = useNavigate()
  const [proposal, setProposal] = useState(null)
  const [group, setGroup] = useState(null)
  const [data, setData] = useState([])
  const [isStakeholder] = useGlobalState('isStakeholder')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [currentUser] = useGlobalState('currentUser')

  useEffect(() => {
    retrieveProposal()
    getGroup(`pid_${id}`).then((group) => {
      if (!!!group.code) setGroup(group)
      console.log(group)
    })
  }, [id])

  const retrieveProposal = () => {
    getProposal(id).then((res) => {
      setProposal(res)
      setData([
        {
          name: 'Voters',
          Acceptees: res?.upvotes,
          Rejectees: res?.downvotes,
        },
      ])
    })
  }

  const onVote = (choice) => {
    if (new Date().getTime() > Number(proposal.duration + '000')) {
      toast.warning('Proposal expired!')
      return
    }

    voteOnProposal(id, choice).then((res) => {
      if (!!!res.code) {
        toast.success('Voted successfully!')
        window.location.reload()
      }
    })
  }

  const daysRemaining = (days) => {
    const todaysdate = moment()
    days = Number((days + '000').slice(0))
    days = moment(days).format('YYYY-MM-DD')
    days = moment(days)
    days = days.diff(todaysdate, 'days')
    return days == 1 ? '1 day' : days + ' days'
  }

  const onEnterChat = () => {
    if (group.hasJoined) {
      navigator(`/chat/${`pid_${id}`}`)
    } else {
      joinGroup(`pid_${id}`).then((res) => {
        if (!!res) {
          navigator(`/chat/${`pid_${id}`}`)
          console.log('Success joining: ', res)
        } else {
          console.log('Error Joining Group: ', res)
        }
      })
    }
  }

  const onCreateGroup = () => {
    createNewGroup(`pid_${id}`, proposal.title).then((group) => {
      if (!!!group.code) {
        toast.success('Group created successfully!')
        setGroup(group)
      } else {
        console.log('Error Creating Group: ', group)
      }
    })
  }

  return (
    <div className="p-8">
      <h2 className="font-semibold text-3xl mb-5">{proposal?.title}</h2>
      <p>
        This proposal is to payout <strong>{proposal?.amount} Eth</strong> and
        currently have{' '}
        <strong>{proposal?.upvotes + proposal?.downvotes} votes</strong> and
        will expire in <strong>{daysRemaining(proposal?.duration)}</strong>
      </p>
      <hr className="my-6 border-gray-300" />
      <p>{proposal?.description}</p>
      <div className="flex flex-row justify-start items-center w-full mt-4 overflow-auto">
        <BarChart width={730} height={250} data={data}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Bar dataKey="Acceptees" fill="#2563eb" />
          <Bar dataKey="Rejectees" fill="#dc2626" />
        </BarChart>
      </div>
      <div
        className="flex flex-row justify-start items-center space-x-3 mt-4"
        role="group"
      >
        {isStakeholder ? (
          <>
            <button
              type="button"
              className="inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
              leading-tight uppercase rounded-full shadow-md
              hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
              focus:shadow-lg focus:outline-none focus:ring-0
              active:bg-blue-800 active:shadow-lg transition
              duration-150 ease-in-out dark:text-gray-300
              dark:border dark:border-gray-500 dark:bg-transparent"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              onClick={() => onVote(true)}
            >
              Accept
            </button>
            <button
              type="button"
              className="inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
              leading-tight uppercase rounded-full shadow-md
              hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
              focus:shadow-lg focus:outline-none focus:ring-0
              active:bg-blue-800 active:shadow-lg transition
              duration-150 ease-in-out
              dark:border dark:border-gray-500 dark:bg-transparent"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              onClick={() => onVote(false)}
            >
              Reject
            </button>

            {currentUser &&
            currentUser.uid.toLowerCase() == proposal?.proposer.toLowerCase() &&
            !group ? (
              <button
                type="button"
                className="inline-block px-6 py-2.5
                bg-blue-600 text-white font-medium text-xs
                leading-tight uppercase rounded-full shadow-md
                hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0
                active:bg-blue-800 active:shadow-lg transition
                duration-150 ease-in-out
                dark:border dark:border-blue-500"
                data-mdb-ripple="true"
                data-mdb-ripple-color="light"
                onClick={onCreateGroup}
              >
                Create Group
              </button>
            ) : null}
          </>
        ) : null}

        {currentUser && currentUser.uid.toLowerCase() == connectedAccount.toLowerCase() && !!!group?.code && group != null ? (
          <button
            type="button"
            className="inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
            leading-tight uppercase rounded-full shadow-md
            hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
            focus:shadow-lg focus:outline-none focus:ring-0
            active:bg-blue-800 active:shadow-lg transition
            duration-150 ease-in-out
            dark:border dark:border-blue-500"
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            onClick={onEnterChat}
          >
            Chat
          </button>
        ) : null}

        {proposal?.proposer.toLowerCase() != connectedAccount.toLowerCase() &&
        !!!group ? (
          <button
            type="button"
            className="inline-block px-6 py-2.5 bg-blue-600
            dark:bg-transparent text-white font-medium text-xs
            leading-tight uppercase rounded-full shadow-md
            hover:border-blue-700 hover:shadow-lg focus:border-blue-700
            focus:shadow-lg focus:outline-none focus:ring-0
            active:border-blue-800 active:shadow-lg transition
            duration-150 ease-in-out dark:text-blue-500
            dark:border dark:border-blue-500 disabled:bg-blue-300"
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            disabled
          >
            Group N/A
          </button>
        ) : null}
      </div>
    </div>
  )
}

export default ProposalDetails
Enter fullscreen mode Exit fullscreen mode

Componente Votantes

Image description

Componente Votantes

Este componente simplesmente lista os stakeholders que votaram em uma proposta. O componente também oferece ao usuário a chance de selecionar entre os rejeitados e aceitos. Veja o código abaixo.

import Identicon from 'react-identicons'
import moment from 'moment'
import { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { truncate } from '../store'
import { listVoters } from '../Dominion'

const Voters = () => {
  const [voters, setVoters] = useState([])
  const [data, setData] = useState([])
  const { id } = useParams()

  const timeAgo = (timestamp) => moment(Number(timestamp + '000')).fromNow()

  const deactive = `bg-transparent
  text-blue-600 font-medium text-xs leading-tight
  uppercase hover:bg-blue-700 focus:bg-blue-700
  focus:outline-none focus:ring-0 active:bg-blue-600
  transition duration-150 ease-in-out overflow-hidden
  border border-blue-600 hover:text-white focus:text-white`

  const active = `bg-blue-600
  text-white font-medium text-xs leading-tight
  uppercase hover:bg-blue-700 focus:bg-blue-700
  focus:outline-none focus:ring-0 active:bg-blue-800
  transition duration-150 ease-in-out overflow-hidden
  border border-blue-600`

  useEffect(() => {
    listVoters(id).then((res) => {
      setVoters(res)
      setData(res)
    })
  }, [id])

  const getAll = () => setVoters(data)

  const getAccepted = () => setVoters(data.filter((vote) => vote.choosen))

  const getRejected = () => setVoters(data.filter((vote) => !vote.choosen))

  return (
    <div className="flex flex-col p-8">
      <div className="flex flex-row justify-center items-center" role="group">
        <button
          aria-current="page"
          className={`rounded-l-full px-6 py-2.5 ${active}`}
          onClick={getAll}
        >
          All
        </button>
        <button
          aria-current="page"
          className={`px-6 py-2.5 ${deactive}`}
          onClick={getAccepted}
        >
          Acceptees
        </button>
        <button
          aria-current="page"
          className={`rounded-r-full px-6 py-2.5 ${deactive}`}
          onClick={getRejected}
        >
          Rejectees
        </button>
      </div>
      <div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
        <div className="py-2 inline-block min-w-full sm:px-6 lg:px-8">
          <div className="h-[calc(100vh_-_20rem)] overflow-y-auto  shadow-md rounded-md">
            <table className="min-w-full">
              <thead className="border-b dark:border-gray-500">
                <tr>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Voter
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Voted
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Vote
                  </th>
                </tr>
              </thead>
              <tbody>
                {voters.map((voter, i) => (
                  <tr
                    key={i}
                    className="border-b dark:border-gray-500 transition duration-300 ease-in-out"
                  >
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      <div className="flex flex-row justify-start items-center space-x-3">
                        <Identicon
                          string={voter.voter.toLowerCase()}
                          size={25}
                          className="h-10 w-10 object-contain rounded-full mr-3"
                        />
                        <span>{truncate(voter.voter, 4, 4, 11)}</span>
                      </div>
                    </td>
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      {timeAgo(voter.timestamp)}
                    </td>
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      {voter.choosen ? (
                        <button
                          className="border-2 rounded-full px-6 py-2.5 border-blue-600
                          text-blue-600 font-medium text-xs leading-tight
                          uppercase hover:border-blue-700 focus:border-blue-700
                          focus:outline-none focus:ring-0 active:border-blue-800
                          transition duration-150 ease-in-out"
                        >
                          Accepted
                        </button>
                      ) : (
                        <button
                          className="border-2 rounded-full px-6 py-2.5 border-red-600
                          text-red-600 font-medium text-xs leading-tight
                          uppercase hover:border-red-700 focus:border-red-700
                          focus:outline-none focus:ring-0 active:border-red-800
                          transition duration-150 ease-in-out"
                        >
                          Rejected
                        </button>
                      )}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>
      <div className="mt-4 text-center">
        {voters.length >= 10 ? (
          <button
            aria-current="page"
            className="rounded-full px-6 py-2.5 bg-blue-600
            font-medium text-xs leading-tight
            uppercase hover:bg-blue-700 focus:bg-blue-700
            focus:outline-none focus:ring-0 active:bg-blue-800
            transition duration-150 ease-in-out dark:text-gray-300
            dark:border dark:border-gray-500 dark:bg-transparent"
          >
            Load More
          </button>
        ) : null}
      </div>
    </div>
  )
}

export default Voters
Enter fullscreen mode Exit fullscreen mode

Componente mensagens

Image description

O Componente mensagens

Com o poder do CometChat SDK combinado com este componente, os usuários podem se envolver em um bate-papo de um para muitos anonimamente. Contribuintes e stakeholders podem discutir uma proposta mais detalhadamente em seu processo de tomada de decisão aqui. Todos os usuários mantêm seu anonimato e são representados por seus Identicons.

import Identicon from 'react-identicons'
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { truncate, useGlobalState } from '../store'
import { getMessages, sendMessage, CometChat } from '../CometChat'

const Messages = ({ gid }) => {
  const navigator = useNavigate()
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [message, setMessage] = useState('')
  const [messages, setMessages] = useState([])

  useEffect(() => {
    getMessages(gid).then((msgs) => {
      if (!!!msgs.code)
        setMessages(msgs.filter((msg) => msg.category == 'message'))
    })
    listenForMessage(gid)
  }, [gid])

  const listenForMessage = (listenerID) => {
    CometChat.addMessageListener(
      listenerID,
      new CometChat.MessageListener({
        onTextMessageReceived: (message) => {
          setMessages((prevState) => [...prevState, message])
          scrollToEnd()
        },
      })
    )
  }

  const handleMessage = (e) => {
    e.preventDefault()
    sendMessage(gid, message).then((msg) => {
      if (!!!msg.code) {
        setMessages((prevState) => [...prevState, msg])
        setMessage('')
        scrollToEnd()
      }
    })
  }

  const scrollToEnd = () => {
    const elmnt = document.getElementById('messages-container')
    elmnt.scrollTop = elmnt.scrollHeight
  }

  const dateToTime = (date) => {
    let hours = date.getHours()
    let minutes = date.getMinutes()
    let ampm = hours >= 12 ? 'pm' : 'am'
    hours = hours % 12
    hours = hours ? hours : 12
    minutes = minutes < 10 ? '0' + minutes : minutes
    let strTime = hours + ':' + minutes + ' ' + ampm
    return strTime
  }

  return (
    <div className="p-8">
      <div className="flex flex-row justify-start">
        <button
          className="px-4 py-2.5 bg-transparent hover:text-white
        font-bold text-xs leading-tight uppercase
        rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg
        focus:bg-blue-700 focus:shadow-lg focus:outline-none
        focus:ring-0 active:bg-blue-800 active:shadow-lg
        transition duration-150 ease-in-out"
        onClick={() => navigator(`/proposal/${gid.substr(4)}`)}
        >
          Exit Chat
        </button>
      </div>

      <div
        id="messages-container"
        className="h-[calc(100vh_-_16rem)] overflow-y-auto sm:pr-4 my-3"
      >
        {messages.map((message, i) =>
          message.sender.uid.toLowerCase() != connectedAccount.toLowerCase() ? (
            <div key={i} className="flex flex-row justify-start my-2">
              <div className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-3xl shadow-md">
                <div className="flex flex-row justify-start items-center space-x-2">
                  <Identicon
                    string={message.sender.uid.toLowerCase()}
                    size={25}
                    className="h-10 w-10 object-contain shadow-md rounded-full mr-3"
                  />
                  <span>@{truncate(message.sender.uid, 4, 4, 11)}</span>
                  <small>{dateToTime(new Date(message.sentAt * 1000))}</small>
                </div>
                <small className="leading-tight my-2">{message.text}</small>
              </div>
            </div>
          ) : (
            <div key={i} className="flex flex-row justify-end my-2">
              <div className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-3xl shadow-md shadow-blue-300">
                <div className="flex flex-row justify-start items-center space-x-2">
                  <Identicon
                    string={connectedAccount.toLowerCase()}
                    size={25}
                    className="h-10 w-10 object-contain shadow-md rounded-full mr-3"
                  />
                  <span>@you</span>
                  <small>{dateToTime(new Date(message.sentAt * 1000))}</small>
                </div>
                <small className="leading-tight my-2">{message.text}</small>
              </div>
            </div>
          )
        )}
      </div>

      <form onSubmit={handleMessage} className="flex flex-row">
        <input
          className="w-full bg-transparent rounded-lg p-4 
          focus:ring-0 focus:outline-none border-gray-500"
          type="text"
          placeholder="Write a message..."
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          required
        />
        <button type="submit" hidden>
          send
        </button>
      </form>
    </div>
  )
}

export default Messages
Enter fullscreen mode Exit fullscreen mode

Criar Componente de Proposta

Image description

Criar componente de proposta

Este componente simplesmente permite que você levante uma proposta fornecendo informações sobre os campos vistos na imagem acima. Veja o código abaixo.

import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { raiseProposal } from '../Dominion'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const CreateProposal = () => {
  const [createModal] = useGlobalState('createModal')
  const [title, setTitle] = useState('')
  const [amount, setAmount] = useState('')
  const [beneficiary, setBeneficiary] = useState('')
  const [description, setDescription] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    if (!title || !description || !beneficiary || !amount) return
    const proposal = { title, description, beneficiary, amount }

    raiseProposal(proposal).then((proposed) => {
      if (proposed) {
        toast.success('Proposal created, reloading in progress...')
        closeModal()
        window.location.reload()
      }
    })
  }

  const closeModal = () => {
    setGlobalState('createModal', 'scale-0')
    resetForm()
  }

  const resetForm = () => {
    setTitle('')
    setAmount('')
    setBeneficiary('')
    setDescription('')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
      justify-center bg-black bg-opacity-50 transform z-50
      transition-transform duration-300 ${createModal}`}
    >
      <div className="bg-white dark:bg-[#212936] shadow-xl shadow-[#122643] dark:shadow-gray-500 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold">Raise Proposal</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes />
            </button>
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5">
            <input
              className="block w-full text-sm
              bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="text"
              name="title"
              placeholder="Title"
              onChange={(e) => setTitle(e.target.value)}
              value={title}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5">
            <input
              className="block w-full text-sm
              bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="text"
              name="amount"
              placeholder="e.g 2.5 Eth"
              onChange={(e) => setAmount(e.target.value)}
              value={amount}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5">
            <input
              className="block w-full text-sm
              bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="text"
              name="beneficiary"
              placeholder="Beneficiary Address"
              onChange={(e) => setBeneficiary(e.target.value)}
              value={beneficiary}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5">
            <textarea
              className="block w-full text-sm resize-none
              bg-transparent border-0
              focus:outline-none focus:ring-0 h-20"
              type="text"
              name="description"
              placeholder="Description"
              onChange={(e) => setDescription(e.target.value)}
              value={description}
              required
            ></textarea>
          </div>

          <button
            className="rounded-lg px-6 py-2.5 bg-blue-600
              text-white font-medium text-xs leading-tight
              uppercase hover:bg-blue-700 focus:bg-blue-700
              focus:outline-none focus:ring-0 active:bg-blue-800
              transition duration-150 ease-in-out mt-5"
            onClick={handleSubmit}
          >
            Submit Proposal
          </button>
        </form>
      </div>
    </div>
  )
}

export default CreateProposal
Enter fullscreen mode Exit fullscreen mode

Componente Autenticação

Image description

Componente de autenticação bate-papo

Este componente ajuda você a participar dos recursos de bate-papo. Você precisa criar uma conta ou fazer login se já estiver cadastrado. Ao fazer o login, você pode participar de um bate-papo em grupo e ter uma conversa anônima com outros participantes de uma proposta no estilo web3.0. Veja o código abaixo.

import { FaTimes } from 'react-icons/fa'
import { loginWithCometChat, signInWithCometChat } from '../CometChat'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const ChatLogin = () => {
  const [loginModal] = useGlobalState('loginModal')
  const [connectedAccount] = useGlobalState('connectedAccount')

  const handleSignUp = () => {
    signInWithCometChat(connectedAccount, connectedAccount).then((user) => {
      if (!!!user.code) {
        toast.success('Account created, now click the login button.')
      } else {
        toast.error(user.message)
      }
    })
  }

  const handleLogin = () => {
    loginWithCometChat(connectedAccount).then((user) => {
      if (!!!user.code) {
        setGlobalState('currentUser', user)
        toast.success('Logged in successful!')
        closeModal()
      } else {
        toast.error(user.message)
      }
    })
  }

  const closeModal = () => {
    setGlobalState('loginModal', 'scale-0')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
      justify-center bg-black bg-opacity-50 transform z-50
      transition-transform duration-300 ${loginModal}`}
    >
      <div className="bg-white dark:bg-[#212936] shadow-xl shadow-[#122643] dark:shadow-gray-500 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <div className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold">Authenticate</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes />
            </button>
          </div>

          <div className="my-2 font-light">
            <span>
              Once you login, you will be enabled to chat with other
              stakeholders to make a well-informed voting.
            </span>
          </div>

          <div
            className="flex flex-row justify-between items-center mt-2"
            role="group"
          >
            <button
              className="rounded-lg px-6 py-2.5 bg-blue-600
              text-white font-medium text-xs leading-tight
              uppercase hover:bg-blue-700 focus:bg-blue-700
              focus:outline-none focus:ring-0 active:bg-blue-800
              transition duration-150 ease-in-out mt-5"
              onClick={handleLogin}
            >
              Login
            </button>

            <button
              className="rounded-lg px-6 py-2.5 bg-transparent
              text-blue-600 font-medium text-xs leading-tight
              uppercase hover:bg-blue-700 hover:text-white focus:bg-blue-700
              focus:outline-none focus:ring-0 active:bg-blue-800
              transition duration-150 ease-in-out mt-5
              border-blue-600"
              onClick={handleSignUp}
            >
              Create Account
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default ChatLogin
Enter fullscreen mode Exit fullscreen mode

A Visualização da Home

Image description

A Visualização Inicial

Esta visualização inclui os cabeçalho, bannere propostas para fornecer uma experiência de usuário DAO excepcional. Também usamos o poder do Tailwind CSS para conseguir esse visual. Observe o código abaixo.

import Banner from '../components/Banner'
import ChatLogin from '../components/ChatLogin'
import CreateProposal from '../components/CreateProposal'
import Header from '../components/Header'
import Proposals from '../components/Proposals'
const Home = () => {
  return (
    <>
      <Header />
      <Banner />
      <Proposals />
      <CreateProposal />
      <ChatLogin />
    </>
  )
}
export default Home
Enter fullscreen mode Exit fullscreen mode

A Exibição da Proposta

Image description

A Exibição da Proposta

Esta exibição combina o cabeçalho, os detalhes da proposta e o componente de votantes para renderizar uma apresentação suave de um único componente. Veja o código abaixo.

import Header from '../components/Header'
import ProposalDetails from '../components/ProposalDetails'
import Voters from '../components/Voters'
const Proposal = () => {
  return (
    <>
      <Header />
      <ProposalDetails />
      <Voters />
    </>
  )
}
export default Proposal
Enter fullscreen mode Exit fullscreen mode

A Exibição do Chat

Image description

A visualização de bate-papo

Por último, a visualização de bate-papo incorpora o componente de cabeçalho e mensagens para renderizar uma interface de bate-papo de qualidade. Veja o código abaixo.

import { useParams, useNavigate } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { getGroup } from '../CometChat'
import { toast } from 'react-toastify'
import Header from '../components/Header'
import Messages from '../components/Messages'
const Chat = () => {
  const { gid } = useParams()
  const navigator = useNavigate()
  const [group, setGroup] = useState(null)
  useEffect(() => {
    getGroup(gid).then((group) => {
      if (!!!group.code) {
        setGroup(group)
      } else {
        toast.warning('Please join the group first!')
        navigator(`/proposal/${gid.substr(4)}`)
      }
    })
  }, [gid])
  return (
    <>
      <Header />
      <Messages gid={gid} />
    </>
  )
}
export default Chat
Enter fullscreen mode Exit fullscreen mode

Incrível, não se esqueça de atualizar o App.jsx também.

O componente do aplicativo Substitua o componente do aplicativo pelo código abaixo.

import { useEffect, useState } from 'react'
import { Routes, Route } from 'react-router-dom'
import { loadWeb3 } from './Dominion'
import { ToastContainer } from 'react-toastify'
import { isUserLoggedIn } from './CometChat'
import Home from './views/Home'
import Proposal from './views/Proposal'
import Chat from './views/Chat'
import 'react-toastify/dist/ReactToastify.min.css'

const App = () => {
  const [loaded, setLoaded] = useState(false)

  useEffect(() => {
    loadWeb3().then((res) => {
      if (res) setLoaded(true)
    })
    isUserLoggedIn()
  }, [])

  return (
    <div className="min-h-screen bg-white text-gray-900 dark:bg-[#212936] dark:text-gray-300">

      {loaded ? (
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="proposal/:id" element={<Proposal />} />
          <Route path="chat/:gid" element={<Chat />} />
        </Routes>
      ) : null}

      <ToastContainer
        position="top-center"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
      />
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

No diretório src, >> cole os seguintes códigos em seus respectivos arquivos.

Arquivo Index.jsx

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } a partir de 'react-router-dom'
import './index.css'
import App from './App'
import { initCometChat } a partir de './CometChat'
initCometChat().then(() => {
  ReactDOM.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>,
    document.getElementById('root')
  )
})
Enter fullscreen mode Exit fullscreen mode

*Arquivo Index.css *

@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap');
* html {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}
body {
  margin: 0;
  font-family: 'Open Sans', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

CometChat.jsx

import Web3 from 'web3'
import { setGlobalState, getGlobalState } from './store'
import DominionDAO from './abis/DominionDAO.json'

const { ethereum } = window

const connectWallet = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
    setGlobalState('connectedAccount', accounts[0])
  } catch (error) {
    console.log(JSON.stringify(error))
  }
}

const raiseProposal = async ({ title, description, beneficiary, amount }) => {
  try {
    amount = window.web3.utils.toWei(amount.toString(), 'ether')
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')

    let proposal = await contract.methods
      .createProposal(title, description, beneficiary, amount)
      .send({ from: account })

    return proposal
  } catch (error) {
    console.log(error.message)
    return error
  }
}

const performContribute = async (amount) => {
  try {
    amount = window.web3.utils.toWei(amount.toString(), 'ether')
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')

    let balance = await contract.methods
      .contribute()
      .send({ from: account, value: amount })
    balance = window.web3.utils.fromWei(
      balance.events.Action.returnValues.amount
    )
    return balance
  } catch (error) {
    console.log(error.message)
    return error
  }
}

const retrieveProposal = async (id) => {
  const web3 = window.web3
  try {
    const contract = getGlobalState('contract')
    const proposal = await contract.methods.getProposal(id).call().wait()
    return {
      id: proposal.id,
      amount: web3.utils.fromWei(proposal.amount),
      title: proposal.title,
      description: proposal.description,
      paid: proposal.paid,
      passed: proposal.passed,
      proposer: proposal.proposer,
      upvotes: Number(proposal.upvotes),
      downvotes: Number(proposal.downvotes),
      beneficiary: proposal.beneficiary,
      executor: proposal.executor,
      duration: proposal.duration,
    }
  } catch (error) {
    console.log(error)
  }
}

const reconstructProposal = (proposal) => {
  return {
    id: proposal.id,
    amount: window.web3.utils.fromWei(proposal.amount),
    title: proposal.title,
    description: proposal.description,
    paid: proposal.paid,
    passed: proposal.passed,
    proposer: proposal.proposer,
    upvotes: Number(proposal.upvotes),
    downvotes: Number(proposal.downvotes),
    beneficiary: proposal.beneficiary,
    executor: proposal.executor,
    duration: proposal.duration,
  }
}

const getProposal = async (id) => {
  try {
    const proposals = getGlobalState('proposals')
    return proposals.find((proposal) => proposal.id == id)
  } catch (error) {
    console.log(error)
  }
}

const voteOnProposal = async (proposalId, supported) => {
  try {
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')
    const vote = await contract.methods
      .performVote(proposalId, supported)
      .send({ from: account })
    return vote
  } catch (error) {
    console.log(error)
    return error
  }
}

const listVoters = async (id) => {
  try {
    const contract = getGlobalState('contract')
    const votes = await contract.methods.getVotesOf(id).call()
    return votes
  } catch (error) {
    console.log(error)
  }
}

const payoutBeneficiary = async (id) => {
  try {
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')
    const balance = await contract.methods
      .payBeneficiary(id)
      .send({ from: account })
    return balance
  } catch (error) {
    return error
  }
}

const loadWeb3 = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')

    window.web3 = new Web3(ethereum)
    await ethereum.request({ method: 'eth_requestAccounts' })
    window.web3 = new Web3(window.web3.currentProvider)

    const web3 = window.web3
    const accounts = await web3.eth.getAccounts()
    setGlobalState('connectedAccount', accounts[0])

    const networkId = await web3.eth.net.getId()
    const networkData = DominionDAO.networks[networkId]

    if (networkData) {
      const contract = new web3.eth.Contract(
        DominionDAO.abi,
        networkData.address
      )
      const isStakeholder = await contract.methods
        .isStakeholder()
        .call({ from: accounts[0] })
      const proposals = await contract.methods.getProposals().call()
      const balance = await contract.methods.daoBalance().call()
      const mybalance = await contract.methods
        .getBalance()
        .call({ from: accounts[0] })

      setGlobalState('contract', contract)
      setGlobalState('balance', web3.utils.fromWei(balance))
      setGlobalState('mybalance', web3.utils.fromWei(mybalance))
      setGlobalState('isStakeholder', isStakeholder)
      setGlobalState('proposals', structuredProposals(proposals))
    } else {
      window.alert('DominionDAO contract not deployed to detected network.')
    }
    return true
  } catch (error) {
    alert('Please connect your metamask wallet!')
    console.log(error)
    return false
  }
}

const structuredProposals = (proposals) => {
  const web3 = window.web3
  return proposals
    .map((proposal) => ({
      id: proposal.id,
      amount: web3.utils.fromWei(proposal.amount),
      title: proposal.title,
      description: proposal.description,
      paid: proposal.paid,
      passed: proposal.passed,
      proposer: proposal.proposer,
      upvotes: Number(proposal.upvotes),
      downvotes: Number(proposal.downvotes),
      beneficiary: proposal.beneficiary,
      executor: proposal.executor,
      duration: proposal.duration,
    }))
    .reverse()
}

export {
  loadWeb3,
  connectWallet,
  performContribute,
  raiseProposal,
  retrieveProposal,
  voteOnProposal,
  getProposal,
  listVoters,
  payoutBeneficiary,
}
Enter fullscreen mode Exit fullscreen mode

Iniciando o Ambiente de Desenvolvimento

PASSO 1: Crie uma conta de teste com ganache-cli usando o comando abaixo:

ganache-cli -a
Enter fullscreen mode Exit fullscreen mode

Isso criará algumas contas de teste com 100 ethers falsos carregados em cada conta, é claro, são apenas para fins de teste. Veja a imagem abaixo:

Image description

Chaves Privadas Geradas

PASSO 2: Adicione uma rede de teste local com o Metamask como visto na imagem abaixo.

Image description

Rede Localhost

PASSO 3: Clique no ícone da conta e selecione importar conta.

Image description
Etapa um

Copie cerca de cinco das chaves privadas e adicione-as uma após a outra à sua rede de teste local. Veja a imagem abaixo.

Image description
Importando chaves privadas do ganache cli

Observe a nova conta adicionada à sua rede de teste local com 100 ETH pré-carregados. Certifique-se de adicionar cerca de cinco contas para que você possa fazer um teste máximo. Veja a imagem abaixo.

Image description
Ethers gratis para teste

Implantação do Contrato Inteligente

Agora abra um novo terminal e execute o comando abaixo.

truffle migrate
# or
truffle migrate --network rinkeby
Enter fullscreen mode Exit fullscreen mode

O comando acima implantará seu contrato inteligente em sua rede de teste local ou Infura rinkeby.

Em seguida, abra outro terminal e ative o aplicativo react com yarn start.

Conclusão

Viva, acabamos de concluir um tutorial incrível para desenvolver uma organização autônoma descentralizada.

Se você gostou deste tutorial e gostaria de me ter como seu mentor privado, por favor, agende suas aulas comigo.

Até a próxima, tudo de bom.

Este artigo foi escrito por Darlington Gospel e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui.

Sobre o autor

Gospel Darlington é um desenvolvedor de blockchain full-stack com 6 anos de experiência na indústria de desenvolvimento de software.

Ao combinar desenvolvimento de software, escrita e ensino, ele demonstra como construir aplicativos descentralizados em redes blockchain compatíveis com EVM.

Seus stacks incluem JavaScript, React, Vue, Angular, Node, React Native, NextJs, Soliditye muito mais.

Para mais informações sobre ele, visite e siga sua página no Twitter, Github, LinkedInou em seu site.

Este artigo foi escrito por Gospel Darlington e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui.

Top comments (0)