Veja aqui uma demo e uma git repo para o que você vai construir!
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.
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
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
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"
]
}
}
Ó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.
Registre uma nova conta CometChat se você não tiver uma
PASSO 2: Faça login no CometChat , somente após o registro.
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 2
PASSO 4: Selecione o aplicativo que você acabou de criar na lista.
Selecione seu aplicativo criado
PASSO 5: No Quick Start, copie o 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
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
PASSO 1: Vá para Infura e crie uma conta.
Faça login na sua conta infura
PASSO 2: No painel, crie um novo projeto.
Crie um novo projeto: etapa 1
Crie um novo projeto: etapa 2
ETAPA 3: Copie a URL do endpoint WebSocket da rede de teste Rinkeby
para seu arquivo .env
.
Chaves Rinkeby Testnet
Em seguida, adicione sua frase secreta da Metamask
e 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=************************* *****
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.
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.
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.
Terceiro Passo
PASSO 4: Copie sua chave privada para o arquivo .env. Veja a imagem e o trecho de código abaixo:
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=******************************
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;
}
}
No projeto que você acabou de clonar, vá para o diretório src >> contract 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 que a palavra pragma
representa.
importar "@openzeppelin/contracts/access/AccessControl.sol";
importar "@openzeppelin/contracts/security/ReentrancyGuard.sol";
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;
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;
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;
}
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
);
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);
_;
}
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 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
);
}
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 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;
}
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
);
}
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];
}
}
Esta função recupera uma matriz 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 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];
}
Isso retorna a quantia de dinheiro contribuída pelos stakeholders
function isStakeholder() external view returns (bool) {
return stakeholders[msg.sender] > 0;
}
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];
}
Isso retorna o saldo de um contribuidor e só é acessível ao contribuidor.
function isContributor() external view returns (bool) {
return contributors[msg.sender] > 0;
}
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];
}
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 realiza 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 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)
}
Fantástico, acabamos de finalizar o contrato inteligente para nosso aplicativo, é hora de começar a construir a interface 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 Cabeçalho
Dark Mode
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
Componente Banner
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
Componente Propostas
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
O Componente Detalhes das Propostas
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
Componente Votantes
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
Componente mensagens
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
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="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
Componente Autenticação
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
A Visualização da Home
A Visualização Inicial
Esta visualização inclui os cabeçalho
, banner
e 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
A Exibição da Proposta
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
A Exibição do Chat
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
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
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')
)
})
*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;
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, são apenas para fins de teste. Veja a imagem abaixo:
Chaves Privadas Geradas
PASSO 2: Adicione uma rede de teste local com o Metamask como visto na imagem abaixo.
Rede Localhost
PASSO 3: Clique no ícone da conta e selecione importar conta.
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
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.
Implantação do 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 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
, Solidity
e 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)