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, REGIONe 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 Metamaske sua chave privada de conta preferida. Se você fez isso corretamente, suas variáveis de ambiente agora devem ficar assim.
ENDPOINT_URL=**************************
DEPLOYER_KEY=****************** ***
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=************************* *****
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.sole 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, bannere propostas para fornecer uma experiência de usuário DAO excepcional. Também usamos o poder do Tailwind CSS para conseguir esse visual. Observe o código abaixo.
import Banner from '../components/Banner'
import ChatLogin from '../components/ChatLogin'
import CreateProposal from '../components/CreateProposal'
import Header from '../components/Header'
import Proposals from '../components/Proposals'
const Home = () => {
return (
<>
<Header />
<Banner />
<Proposals />
<CreateProposal />
<ChatLogin />
</>
)
}
export default Home
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, Soliditye muito mais.
Para mais informações sobre ele, visite e siga sua página no Twitter, Github, LinkedInou em seu site.
Este artigo foi escrito por Gospel Darlington e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui.



































Oldest comments (0)