Como construir um glorioso Web3.0 DAO com React, Solidity e CometChatpor@daltonic
1,731 leituras

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

2022/06/22
por @daltonic 1,731 leituras
tldt arrow
PT
Read on Terminal Reader

Muito longo; Para ler

Darlington Gospel é um Dapp Mentor ajudando desenvolvedores da Web 2.0 à Web 3.0.0. Neste tutorial, você aprenderá passo a passo como implementar uma organização autônoma descentralizada (DAO) com recursos de chat anônimo. No tutorial abaixo, clone o Web3 Starter Project abaixo. Isso garantirá que estamos todos na mesma página e usando os mesmos pacotes. O código no terminal para confirmar que está instalado globalmente para instalar esses pacotes essenciais globalmente.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coins Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Como construir um glorioso Web3.0 DAO com React, Solidity e CometChat
Darlington Gospel  HackerNoon profile picture

@daltonic

Darlington Gospel

Cerca de @daltonic
LEARN MORE ABOUT @DALTONIC'S EXPERTISE AND PLACE ON THE INTERNET.
react to story with heart

Veja uma demonstração e um repositório git aqui para o que você criará!

Demonstração DAO Dominion

Demonstração DAO Dominion

A interface de bate-papo

A interface de bate-papo

Introdução

Estou muito animado para lançar esta compilaçã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 chat anônimo.

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

Pré-requisito

Você precisará das seguintes ferramentas instaladas para esmagar com sucesso esta compilação:

  • Ganache-Cli
  • Brigadeiro
  • Reagir
  • fúria
  • CSS do Tailwind
  • SDK do CometChat
  • Metamáscara
  • Fio

Instalando Dependências

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

Nó instalado

Nó instalado

Instalação de fios, ganache-cli e trufas Execute os seguintes códigos em seu terminal para instalar esses pacotes essenciais globalmente.

 npm i -g yarn npm i -g truffle npm i -g ganache-cli

Clonando o projeto inicial do Web3 Usando os comandos abaixo, clone o projeto inicial do 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

Fantástico, vamos substituir o arquivo package.json pelo abaixo:

 { "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", "@truffle/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", "process": "^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" ] } }

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

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

Configurando 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 o painel do CometChat e crie uma conta.

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

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

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

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

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

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

Crie um novo aplicativo CometChat - Passo 1

Crie um novo aplicativo CometChat - Passo 1

Crie um novo aplicativo CometChat - Etapa 2

Crie um novo aplicativo CometChat - Etapa 2

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

Selecione seu aplicativo criado

Selecione seu aplicativo criado

ETAPA 5: No Início rápido, copie APP_ID , REGION e AUTH_KEY , para seu arquivo .env . Veja a imagem e o trecho de código.

Copie o APP_ID, REGION e AUTH_KEY

Copie o APP_ID, REGION e AUTH_KEY

Substitua as chaves de espaços reservados REACT_COMET_CHAT por seus valores apropriados.

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

Configurando o aplicativo Infuria

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

Entre na sua conta infuria

Entre na sua conta infuria

PASSO 2: No painel, crie um novo projeto.

Criar um novo projeto: passo 1

Criar um novo projeto: passo 1

Crie um novo projeto: passo 2

Crie um novo projeto: passo 2

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

Chaves Rinkeby Testnet

Chaves Rinkeby Testnet

Em seguida, adicione sua frase secreta Metamask e sua chave privada de conta preferida. Se você fez isso corretamente, suas variáveis de ambiente agora devem se parecer com isso.

 ENDPOINT_URL=*************************** DEPLOYER_KEY=********************** REACT_APP_COMET_CHAT_REGION=** REACT_APP_COMET_CHAT_APP_ID=************** REACT_APP_COMET_CHAT_AUTH_KEY=******************************

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

Acessando sua chave privada de metamask

PASSO 1: Clique na extensão do navegador 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.

Passo um

Passo um

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

Passo dois

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.

Passo três

Passo três

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

Passo Quatro

Passo Quatro

 ENDPOINT_URL=*************************** SECRET_KEY=****************** DEPLOYER_KEY=********************** REACT_APP_COMET_CHAT_REGION=** REACT_APP_COMET_CHAT_APP_ID=************** REACT_APP_COMET_CHAT_AUTH_KEY=******************************

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

O Contrato Inteligente DAO Dominion

Aqui está o código completo do contrato inteligente, explicarei 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; } }

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

Explicação:

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

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 é o que a palavra pragma representa.

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

No bloco de código acima, estamos utilizando dois contratos inteligentes openzeppelin's para especificar funções e proteger nosso contrato inteligente contra ataques de reentrância.

 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;

Configuramos algumas variáveis de estado para as funções de participante e colaborador e especificamos a duração mínima do voto em uma semana. E também inicializamos o contador total de propostas e uma variável para manter um registro de 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;

raisedProposals acompanha todas as propostas enviadas ao nosso contrato inteligente. stakeholderVotes , como o próprio nome indica, acompanha os votos feitos pelas partes interessadas. votedOn mantém registro de todos os votos associados a uma proposta. Enquanto os contribuidores acompanham quem doou para nossa plataforma, as partes interessadas, por outro lado, acompanham 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; }

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

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

Este é um evento dinâmico chamado Ação. 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); _; }

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 ); }

A função acima pega o título, a descrição, o valor e o endereço da carteira do beneficiário de uma proposta e cria uma proposta. A função permite apenas que as partes interessadas criem propostas. As partes interessadas 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 ); }

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"); } }

Esta função realiza a votação real, incluindo a verificação se um usuário é um interessado e 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; }

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

  • Primeiro, o beneficiário ainda não deve ter sido 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 ); }

Essa função é responsável por coletar as contribuições dos doadores e dos 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]; } }

Esta função recupera uma série de propostas registradas neste contrato inteligente.

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

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

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

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]; }

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

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

Isso retorna a quantia de dinheiro contribuída pelas partes interessadas.

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

Retorna True ou False se um usuário for uma parte interessada.

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

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

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

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

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

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

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

Configurando o script de implantação

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

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

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

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

Desenvolvendo o Front-end

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

Componente de cabeçalho

Modo Escuro

Modo Escuro

Modo claro

Modo claro

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

Componente de banner

Componente de banner

Componente de banner

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

Este componente também inclui a capacidade de usar a função de contribuição 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="eg 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

Componente de propostas

Componentes das Propostas

Componentes das Propostas

Este componente contém uma lista de propostas em nosso contrato inteligente. Além disso, permite filtrar entre propostas fechadas e abertas. No vencimento de uma proposta, um botão de pagamento fica disponível, o que dá ao 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

O componente de detalhes da proposta

Detalhes da proposta

Detalhes da proposta

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 participar de um 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

Componente de votantes

Componente de votantes

Componente de votantes

Este componente simplesmente lista as partes interessadas que votaram em uma proposta. O componente também oferece ao usuário a chance de filtrar entre 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

Componente de mensagens

O Componente de Mensagens

O Componente de Mensagens

Com o poder do CometChat SDK combinado com este componente, os usuários podem participar de um bate-papo um-para-muitos anonimamente. Colaboradores e partes interessadas podem discutir uma proposta mais adiante 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

Criar componente de proposta

Criar componente de proposta

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="eg 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

Componente de Autenticação

Componente de autenticação de bate-papo

Componente de autenticação de bate-papo

Este componente ajuda você a participar dos recursos de bate-papo. Você precisa criar uma conta ou fazer login se você já se inscreveu. Ao fazer login, você pode participar de um bate-papo em grupo e ter uma conversa anônima com outros participantes em 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

Fantástico, vamos garantir que as visualizações sejam bem representadas…

A Vista Inicial

A Vista Inicial

A Vista Inicial

Essa exibição inclui os componentes de header , banner e proposals para fornecer uma experiência de usuário DAO excepcional. Também usamos o poder do Tailwind CSS para obter essa aparência. 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

A Visualização da Proposta

A Visualização da Proposta

A Visualização da Proposta

Essa exibição une o cabeçalho, os detalhes da proposta e o componente de votantes para renderizar uma apresentação suave de um componente singular. 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

A Visualização do Bate-papo

A Visualização do Bate-papo

A Visualização do Bate-papo

Por fim, a visualização do bate-papo incorpora o cabeçalho e o componente de 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

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

O componente App Substitua o componente App 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

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 } from 'react-router-dom' import './index.css' import App from './App' import { initCometChat } from './CometChat' initCometChat().then(() => { ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root') ) })

Arquivo Index.css

 @import url('https://fonts.googleapis.com/css2?family=Open+Sans:[email protected];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;

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, }

Iniciando o ambiente de desenvolvimento

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

 ganache-cli -a

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

Chaves Privadas Geradas

Chaves Privadas Geradas

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

Rede Localhost

Rede Localhost

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

Passo um

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

Importando chaves privadas do ganache cli

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 poder fazer um teste máximo. Veja a imagem abaixo.

Éteres de Teste Gratuitos

Éteres de Teste Gratuitos

Implantação de contrato inteligente

Agora abra um novo terminal e execute o comando abaixo.

 truffle migrate # or truffle migrate --network rinkeby

O comando acima implantará seu contrato inteligente em sua rede local ou na rede de teste Infuria 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 particular, agende suas aulas comigo .

Até a próxima, tudo de bom.

Sobre o autor

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

Ao combinar desenvolvimento de software, redação e ensino, ele demonstra como criar aplicativos descentralizados em redes blockchain compatíveis com EVM.

Suas pilhas incluem JavaScript , React , Vue , Angular , Node , React Native , NextJs , Solidity e muito mais.

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

HISTÓRIAS RELACIONADAS

L O A D I N G
. . . comments & more!
Hackernoon hq - po box 2206, edwards, colorado 81632, usa